mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2024-06-02 06:30:52 +00:00
Merge branch 'ocvr' into 'main'
ocvr: Added original content video rate plugins See merge request gstreamer/gst-plugins-rs!788
This commit is contained in:
commit
7c6537b89e
11
Cargo.toml
11
Cargo.toml
|
@ -51,10 +51,15 @@ members = [
|
|||
"video/gif",
|
||||
"video/gtk4",
|
||||
"video/hsv",
|
||||
"video/ocvr",
|
||||
"video/png",
|
||||
"video/rav1e",
|
||||
"video/videofx",
|
||||
"video/webp",
|
||||
"text/ahead",
|
||||
"text/wrap",
|
||||
"text/json",
|
||||
"text/regex",
|
||||
]
|
||||
|
||||
# Only plugins without external dependencies
|
||||
|
@ -101,6 +106,12 @@ default-members = [
|
|||
"video/hsv",
|
||||
"video/png",
|
||||
"video/rav1e",
|
||||
"video/hsv",
|
||||
"video/ocvr",
|
||||
"text/ahead",
|
||||
"text/wrap",
|
||||
"text/json",
|
||||
"text/regex",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
|
|
|
@ -96,6 +96,8 @@ You will find the following plugins in this repository:
|
|||
|
||||
- `png`: PNG encoder based on the [png](https://github.com/image-rs/image-png) library.
|
||||
|
||||
- `ocvr`: Elements for original content rate detection within captured video stream.
|
||||
|
||||
- `rav1e`: AV1 encoder based on the [rav1e](https://github.com/xiph/rav1e) library.
|
||||
|
||||
- `videofx`: Plugin with various video filters.
|
||||
|
|
|
@ -3709,6 +3709,381 @@
|
|||
"tracers": {},
|
||||
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
|
||||
},
|
||||
"ocvr": {
|
||||
"description": "Original content video rate detector",
|
||||
"elements": {
|
||||
"ocvrctrl": {
|
||||
"author": "Jochen Henneberg <jh@henneberg-systemdesign.com>",
|
||||
"description": "Precise check estimated framerate",
|
||||
"hierarchy": [
|
||||
"GstOcvrCtrl",
|
||||
"GstElement",
|
||||
"GstObject",
|
||||
"GInitiallyUnowned",
|
||||
"GObject"
|
||||
],
|
||||
"klass": "Parser/Video",
|
||||
"long-name": "Original content video rate controller",
|
||||
"pad-templates": {
|
||||
"sink": {
|
||||
"caps": "video/x-raw:\n format: { I420, NV12 }\n",
|
||||
"direction": "sink",
|
||||
"presence": "always"
|
||||
},
|
||||
"src": {
|
||||
"caps": "video/x-raw:\n format: { I420, NV12 }\n",
|
||||
"direction": "src",
|
||||
"presence": "always"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"capture-rates": {
|
||||
"blurb": "Sink pad framerates to check",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "60Hz+50Hz",
|
||||
"mutable": "playing",
|
||||
"readable": true,
|
||||
"type": "OcvrCtrlCaptureRate",
|
||||
"writable": true
|
||||
},
|
||||
"content-rate": {
|
||||
"blurb": "Framerate to detect",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "Hint (6)",
|
||||
"mutable": "playing",
|
||||
"readable": true,
|
||||
"type": "GstOcvrCtrlContentRate",
|
||||
"writable": true
|
||||
},
|
||||
"drop": {
|
||||
"blurb": "Change 60Hz capture rate to 30Hz always",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "false",
|
||||
"mutable": "playing",
|
||||
"readable": true,
|
||||
"type": "gboolean",
|
||||
"writable": true
|
||||
},
|
||||
"method": {
|
||||
"blurb": "Method to check for duplicate frames",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "Auto (0)",
|
||||
"mutable": "playing",
|
||||
"readable": true,
|
||||
"type": "GstOcvrCtrlMethod",
|
||||
"writable": true
|
||||
},
|
||||
"retries": {
|
||||
"blurb": "Retry times for tolerance 'lazy' or 'strict'",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "3",
|
||||
"max": "100",
|
||||
"min": "0",
|
||||
"mutable": "playing",
|
||||
"readable": true,
|
||||
"type": "guint",
|
||||
"writable": true
|
||||
},
|
||||
"rows": {
|
||||
"blurb": "Number of pixel rows considered for 'fuzzy' compare",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "10",
|
||||
"max": "-2",
|
||||
"min": "0",
|
||||
"mutable": "playing",
|
||||
"readable": true,
|
||||
"type": "guint",
|
||||
"writable": true
|
||||
},
|
||||
"send-caps": {
|
||||
"blurb": "Send caps event downstream on content rate change",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "true",
|
||||
"mutable": "playing",
|
||||
"readable": true,
|
||||
"type": "gboolean",
|
||||
"writable": true
|
||||
},
|
||||
"threshold": {
|
||||
"blurb": "Compare threshold for method 'fuzzy'",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "1",
|
||||
"max": "254",
|
||||
"min": "1",
|
||||
"mutable": "playing",
|
||||
"readable": true,
|
||||
"type": "guint",
|
||||
"writable": true
|
||||
},
|
||||
"tolerance": {
|
||||
"blurb": "How to handle failed comparisons",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "Strict (1)",
|
||||
"mutable": "playing",
|
||||
"readable": true,
|
||||
"type": "GstOcvrCtrlTolerance",
|
||||
"writable": true
|
||||
}
|
||||
},
|
||||
"rank": "none"
|
||||
},
|
||||
"ocvrhint": {
|
||||
"author": "Jochen Henneberg <jh@henneberg-systemdesign.com>",
|
||||
"description": "Detects the original content framerate from encoded video",
|
||||
"hierarchy": [
|
||||
"GstOcvrHint",
|
||||
"GstElement",
|
||||
"GstObject",
|
||||
"GInitiallyUnowned",
|
||||
"GObject"
|
||||
],
|
||||
"klass": "Parser/Video",
|
||||
"long-name": "Original content video rate hinter",
|
||||
"pad-templates": {
|
||||
"sink": {
|
||||
"caps": "video/x-h265:\n alignment: au\nvideo/x-h264:\n alignment: au\n",
|
||||
"direction": "sink",
|
||||
"presence": "always"
|
||||
},
|
||||
"src": {
|
||||
"caps": "video/x-h265:\n alignment: au\nvideo/x-h264:\n alignment: au\n",
|
||||
"direction": "src",
|
||||
"presence": "always"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"capture-rates": {
|
||||
"blurb": "Sink pad framerates to check",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "60Hz+50Hz",
|
||||
"mutable": "playing",
|
||||
"readable": true,
|
||||
"type": "OcvrHintCaptureRate",
|
||||
"writable": true
|
||||
},
|
||||
"content-rates": {
|
||||
"blurb": "Framerates to detect",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "60Hz+50Hz+30Hz+25Hz+24Hz",
|
||||
"mutable": "playing",
|
||||
"readable": true,
|
||||
"type": "OcvrHintContentRate",
|
||||
"writable": true
|
||||
},
|
||||
"threshold": {
|
||||
"blurb": "Framerate detect threshold",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "0.9",
|
||||
"max": "1",
|
||||
"min": "0",
|
||||
"mutable": "playing",
|
||||
"readable": true,
|
||||
"type": "gfloat",
|
||||
"writable": true
|
||||
},
|
||||
"window-size": {
|
||||
"blurb": "Multiple of GOP size",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "4",
|
||||
"max": "100",
|
||||
"min": "1",
|
||||
"mutable": "playing",
|
||||
"readable": true,
|
||||
"type": "guint",
|
||||
"writable": true
|
||||
}
|
||||
},
|
||||
"rank": "none"
|
||||
}
|
||||
},
|
||||
"filename": "gstocvr",
|
||||
"license": "MPL",
|
||||
"other-types": {
|
||||
"GstOcvrCtrlContentRate": {
|
||||
"kind": "enum",
|
||||
"values": [
|
||||
{
|
||||
"desc": "Search for matching rate",
|
||||
"name": "Auto",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"desc": "Content rate 24Hz (for 60Hz capture rate only)",
|
||||
"name": "24Hz",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"desc": "Content rate 25Hz (for 50Hz capture rate only)",
|
||||
"name": "25Hz",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"desc": "Content rate 30Hz",
|
||||
"name": "30Hz",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"desc": "Content rate 50Hz (for 50Hz capture rate only)",
|
||||
"name": "50Hz",
|
||||
"value": "4"
|
||||
},
|
||||
{
|
||||
"desc": "Content rate 60Hz (for 60Hz capture rate only)",
|
||||
"name": "60Hz",
|
||||
"value": "5"
|
||||
},
|
||||
{
|
||||
"desc": "From 'ocvrhint'",
|
||||
"name": "Hint",
|
||||
"value": "6"
|
||||
}
|
||||
]
|
||||
},
|
||||
"GstOcvrCtrlMethod": {
|
||||
"kind": "enum",
|
||||
"values": [
|
||||
{
|
||||
"desc": "Fallback to fuzzy if accurate fails.",
|
||||
"name": "Auto",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"desc": "Accurate compare by checksum.",
|
||||
"name": "Accurate",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"desc": "Fuzzy compare.",
|
||||
"name": "Fuzzy",
|
||||
"value": "2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"GstOcvrCtrlTolerance": {
|
||||
"kind": "enum",
|
||||
"values": [
|
||||
{
|
||||
"desc": "Resync unless content rate is reset.",
|
||||
"name": "Lazy",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"desc": "Strict compare and resync 'retries' times.",
|
||||
"name": "Strict",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"desc": "Like 'Strict' but ignore retries.",
|
||||
"name": "Paranoid",
|
||||
"value": "2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"OcvrCtrlCaptureRate": {
|
||||
"kind": "flags",
|
||||
"values": [
|
||||
{
|
||||
"desc": "Capture rate 50Hz",
|
||||
"name": "50Hz",
|
||||
"value": "0x00000001"
|
||||
},
|
||||
{
|
||||
"desc": "Capture rate 60Hz",
|
||||
"name": "60Hz",
|
||||
"value": "0x00000002"
|
||||
}
|
||||
]
|
||||
},
|
||||
"OcvrHintCaptureRate": {
|
||||
"kind": "flags",
|
||||
"values": [
|
||||
{
|
||||
"desc": "Capture rate 50Hz",
|
||||
"name": "50Hz",
|
||||
"value": "0x00000001"
|
||||
},
|
||||
{
|
||||
"desc": "Capture rate 60Hz",
|
||||
"name": "60Hz",
|
||||
"value": "0x00000002"
|
||||
}
|
||||
]
|
||||
},
|
||||
"OcvrHintContentRate": {
|
||||
"kind": "flags",
|
||||
"values": [
|
||||
{
|
||||
"desc": "Content rate 24Hz (60Hz capture rate only)",
|
||||
"name": "24Hz",
|
||||
"value": "0x00000001"
|
||||
},
|
||||
{
|
||||
"desc": "Content rate 25Hz (50Hz capture rate only)",
|
||||
"name": "25Hz",
|
||||
"value": "0x00000002"
|
||||
},
|
||||
{
|
||||
"desc": "Content rate 30Hz",
|
||||
"name": "30Hz",
|
||||
"value": "0x00000004"
|
||||
},
|
||||
{
|
||||
"desc": "Content rate 50Hz (50Hz capture rate only)",
|
||||
"name": "50Hz",
|
||||
"value": "0x00000008"
|
||||
},
|
||||
{
|
||||
"desc": "Content rate 60Hz (60Hz capture rate only)",
|
||||
"name": "60Hz",
|
||||
"value": "0x00000010"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"package": "gst-plugin-ocvr",
|
||||
"source": "gst-plugin-ocvr",
|
||||
"tracers": {},
|
||||
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
|
||||
},
|
||||
"raptorq": {
|
||||
"description": "GStreamer RaptorQ FEC Plugin",
|
||||
"elements": {
|
||||
|
|
|
@ -194,6 +194,7 @@ plugins = {
|
|||
},
|
||||
# gtk4 is added below
|
||||
'hsv': {'library': 'libgsthsv'},
|
||||
'ocvr': {'library': 'libgstocvr'},
|
||||
'png': {
|
||||
'library': 'libgstrspng',
|
||||
'examples': ['pngenc'],
|
||||
|
|
|
@ -54,6 +54,7 @@ option('ffv1', type: 'feature', value: 'auto', description: 'Build ffv1 plugin')
|
|||
option('gif', type: 'feature', value: 'auto', description: 'Build gif plugin')
|
||||
option('gtk4', type: 'feature', value: 'auto', description: 'Build GTK4 plugin')
|
||||
option('hsv', type: 'feature', value: 'auto', description: 'Build hsv plugin')
|
||||
option('ocvr', type: 'feature', value: 'auto', description: 'Build ocvr plugin')
|
||||
option('png', type: 'feature', value: 'auto', description: 'Build png plugin')
|
||||
option('rav1e', type: 'feature', value: 'auto', description: 'Build rav1e plugin')
|
||||
option('videofx', type: 'feature', value: 'auto', description: 'Build videofx plugin')
|
||||
|
|
48
video/ocvr/Cargo.toml
Normal file
48
video/ocvr/Cargo.toml
Normal file
|
@ -0,0 +1,48 @@
|
|||
# Copyright (C) 2022 Jochen Henneberg <jh@henneberg-systemdesign.com>
|
||||
#
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
[package]
|
||||
name = "gst-plugin-ocvr"
|
||||
version = "0.13.0"
|
||||
authors = ["Jochen Henneberg <jh@henneberg-systemdesign.com>"]
|
||||
repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
|
||||
license = "MPL-2.0"
|
||||
description = "Original content video rate detector"
|
||||
edition = "2021"
|
||||
rust-version = "1.57"
|
||||
|
||||
[dependencies]
|
||||
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
|
||||
gst-video = { package = "gstreamer-video", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
|
||||
once_cell = "1.0"
|
||||
crc = "3.0"
|
||||
|
||||
[dev-dependencies]
|
||||
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
|
||||
|
||||
[lib]
|
||||
name = "gstocvr"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[build-dependencies]
|
||||
gst-plugin-version-helper = { path="../../version-helper" }
|
||||
|
||||
[features]
|
||||
static = []
|
||||
capi = []
|
||||
doc = ["gst/v1_18"]
|
||||
|
||||
[package.metadata.capi]
|
||||
min_version = "0.0.0"
|
||||
|
||||
[package.metadata.capi.header]
|
||||
enabled = false
|
||||
|
||||
[package.metadata.capi.library]
|
||||
install_subdir = "gstreamer-1.0"
|
||||
versioning = false
|
||||
|
||||
[package.metadata.capi.pkg_config]
|
||||
requires_private = "gstreamer-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0"
|
7
video/ocvr/build.rs
Normal file
7
video/ocvr/build.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
// Copyright (C) 2022 Jochen Henneberg <jh@henneberg-systemdesign.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
fn main() {
|
||||
gst_plugin_version_helper::info()
|
||||
}
|
31
video/ocvr/src/lib.rs
Normal file
31
video/ocvr/src/lib.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
// Copyright (C) 2022 Jochen Henneberg <jh@henneberg-systemdesign.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
/**
|
||||
* plugin-ocvr
|
||||
*
|
||||
* Since: plugins-rs-0.9.0
|
||||
*/
|
||||
use gst::glib;
|
||||
|
||||
mod ocvrctrl;
|
||||
mod ocvrhint;
|
||||
|
||||
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
ocvrhint::register(plugin)?;
|
||||
ocvrctrl::register(plugin)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
gst::plugin_define!(
|
||||
ocvr,
|
||||
env!("CARGO_PKG_DESCRIPTION"),
|
||||
plugin_init,
|
||||
concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")),
|
||||
"MPL",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_REPOSITORY"),
|
||||
env!("BUILD_REL_DATE")
|
||||
);
|
736
video/ocvr/src/ocvrctrl/imp.rs
Normal file
736
video/ocvr/src/ocvrctrl/imp.rs
Normal file
|
@ -0,0 +1,736 @@
|
|||
// Copyright (C) 2022 Jochen Henneberg <jh@henneberg-systemdesign.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
use gst::subclass::prelude::*;
|
||||
|
||||
use std::cmp;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crc::{Crc, CRC_32_ISCSI};
|
||||
use gst_video::video_frame::*;
|
||||
use gst_video::{VideoFormat, VideoInfo};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
mod syncstate;
|
||||
use syncstate::SyncState;
|
||||
mod data;
|
||||
use data::Data;
|
||||
mod settings;
|
||||
use settings::*;
|
||||
|
||||
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||
gst::DebugCategory::new(
|
||||
"ocvrctrl",
|
||||
gst::DebugColorFlags::empty(),
|
||||
Some("Original content video rate controller"),
|
||||
)
|
||||
});
|
||||
|
||||
const CASTAGNOLI: Crc<u32> = Crc::<u32>::new(&CRC_32_ISCSI);
|
||||
|
||||
// Original content framerate to detect - in case of 'hint' listen to
|
||||
// downstream events from ocvrhint, used in syncstate child module and
|
||||
// thus needs to be pub
|
||||
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum)]
|
||||
#[repr(u32)]
|
||||
#[enum_type(name = "GstOcvrCtrlContentRate")]
|
||||
pub enum ContentRate {
|
||||
#[enum_value(name = "Search for matching rate", nick = "Auto")]
|
||||
Auto,
|
||||
#[enum_value(name = "Content rate 24Hz (for 60Hz capture rate only)", nick = "24Hz")]
|
||||
Hz24,
|
||||
#[enum_value(name = "Content rate 25Hz (for 50Hz capture rate only)", nick = "25Hz")]
|
||||
Hz25,
|
||||
#[enum_value(name = "Content rate 30Hz", nick = "30Hz")]
|
||||
Hz30,
|
||||
#[enum_value(name = "Content rate 50Hz (for 50Hz capture rate only)", nick = "50Hz")]
|
||||
Hz50,
|
||||
#[enum_value(name = "Content rate 60Hz (for 60Hz capture rate only)", nick = "60Hz")]
|
||||
Hz60,
|
||||
#[enum_value(name = "From 'ocvrhint'", nick = "Hint")]
|
||||
Hint,
|
||||
}
|
||||
|
||||
impl Default for ContentRate {
|
||||
fn default() -> Self {
|
||||
ContentRate::Hint
|
||||
}
|
||||
}
|
||||
|
||||
impl ContentRate {
|
||||
pub fn next(self, rate: CaptureRate) -> ContentRate {
|
||||
match rate {
|
||||
CaptureRate::HZ_50 => match self {
|
||||
ContentRate::Hz25 => ContentRate::Hz30,
|
||||
ContentRate::Hz30 => ContentRate::Hz50,
|
||||
ContentRate::Hz50 => ContentRate::Hz25,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
CaptureRate::HZ_60 => match self {
|
||||
ContentRate::Hz24 => ContentRate::Hz30,
|
||||
ContentRate::Hz30 => ContentRate::Hz60,
|
||||
ContentRate::Hz60 => ContentRate::Hz24,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn first(rate: CaptureRate) -> ContentRate {
|
||||
match rate {
|
||||
CaptureRate::HZ_50 => ContentRate::Hz25,
|
||||
CaptureRate::HZ_60 => ContentRate::Hz24,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture framerates to check, used in syncstate child module and
|
||||
// thus needs to be pub
|
||||
#[glib::flags(name = "OcvrCtrlCaptureRate")]
|
||||
pub enum CaptureRate {
|
||||
#[flags_value(name = "Capture rate 50Hz", nick = "50Hz")]
|
||||
HZ_50 = 0b00000001,
|
||||
#[flags_value(name = "Capture rate 60Hz", nick = "60Hz")]
|
||||
HZ_60 = 0b00000010,
|
||||
}
|
||||
|
||||
// Method used to check rate
|
||||
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum)]
|
||||
#[repr(u32)]
|
||||
#[enum_type(name = "GstOcvrCtrlMethod")]
|
||||
pub enum Method {
|
||||
#[enum_value(name = "Fallback to fuzzy if accurate fails.", nick = "Auto")]
|
||||
Auto,
|
||||
#[enum_value(name = "Accurate compare by checksum.", nick = "Accurate")]
|
||||
Accurate,
|
||||
#[enum_value(name = "Fuzzy compare.", nick = "Fuzzy")]
|
||||
Fuzzy,
|
||||
}
|
||||
|
||||
impl Default for Method {
|
||||
fn default() -> Self {
|
||||
Method::Auto
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum)]
|
||||
#[repr(u32)]
|
||||
#[enum_type(name = "GstOcvrCtrlTolerance")]
|
||||
pub enum Tolerance {
|
||||
#[enum_value(name = "Resync unless content rate is reset.", nick = "Lazy")]
|
||||
Lazy,
|
||||
#[enum_value(name = "Strict compare and resync 'retries' times.", nick = "Strict")]
|
||||
Strict,
|
||||
#[enum_value(name = "Like 'Strict' but ignore retries.", nick = "Paranoid")]
|
||||
Paranoid,
|
||||
}
|
||||
|
||||
impl Default for Tolerance {
|
||||
fn default() -> Self {
|
||||
Tolerance::Strict
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OcvrCtrl {
|
||||
srcpad: gst::Pad,
|
||||
sinkpad: gst::Pad,
|
||||
settings: Mutex<Settings>,
|
||||
data: Mutex<Data>,
|
||||
}
|
||||
|
||||
impl OcvrCtrl {
|
||||
fn calc_checksum(win: &[u8]) -> u32 {
|
||||
let crc = CASTAGNOLI;
|
||||
let mut digest = crc.digest();
|
||||
|
||||
digest.update(win);
|
||||
digest.finalize()
|
||||
}
|
||||
|
||||
fn compare_fuzzy(ping: &[u8], pong: &[u8], threshold: u32) -> bool {
|
||||
let it = ping.iter();
|
||||
let mut r = true;
|
||||
pong.iter().zip(it).all(|(a, b)| {
|
||||
let ax = cmp::max(a, b);
|
||||
let bx = cmp::min(a, b);
|
||||
let d = ax - bx;
|
||||
if d > threshold as u8 {
|
||||
r = false;
|
||||
}
|
||||
r
|
||||
});
|
||||
|
||||
gst::log!(CAT, "Fuzzy frame compare: {:?}", r);
|
||||
r
|
||||
}
|
||||
|
||||
fn save_frame(&self, buffer: &gst::Buffer) {
|
||||
let mut data = self.data.lock().unwrap();
|
||||
|
||||
if !data.sync_state.needs_save(data.capture_rate.unwrap()) {
|
||||
gst::log!(CAT, "Save frame not needed -> {:?}", data.sync_state);
|
||||
return;
|
||||
}
|
||||
|
||||
gst::log!(CAT, "Save frame -> {:?}", data.sync_state);
|
||||
let info = VideoInfo::builder(
|
||||
data.frame_format.unwrap(),
|
||||
data.frame_size.0,
|
||||
data.frame_size.1,
|
||||
)
|
||||
.build()
|
||||
.unwrap();
|
||||
let frame =
|
||||
VideoFrameRef::<&gst::BufferRef>::from_buffer_ref_readable(buffer, &info).unwrap();
|
||||
let plane: u32 = match data.frame_format.unwrap() {
|
||||
VideoFormat::I420 => 0, // luma
|
||||
VideoFormat::Nv12 => 0, // luma
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
let frame_data = frame.plane_data(plane).unwrap();
|
||||
let info = frame.info();
|
||||
let width = info.width() as usize;
|
||||
let height = info.height() as usize;
|
||||
let stride = info.stride()[plane as usize] as usize;
|
||||
|
||||
let wn: &str;
|
||||
let win = if data.is_ping {
|
||||
wn = "pong";
|
||||
&mut data.pong_window
|
||||
} else {
|
||||
wn = "ping";
|
||||
&mut data.ping_window
|
||||
};
|
||||
gst::log!(CAT, "Save frame to {}", wn);
|
||||
|
||||
win.clear();
|
||||
let settings = self.settings.lock().unwrap();
|
||||
for line in frame_data
|
||||
.chunks_exact(stride)
|
||||
.step_by(height / settings.rows as usize)
|
||||
{
|
||||
win.extend_from_slice(&line[..width]);
|
||||
}
|
||||
|
||||
data.is_ping = !data.is_ping;
|
||||
}
|
||||
|
||||
fn compare_frames(&self) -> Result<bool, ()> {
|
||||
let data = self.data.lock().unwrap();
|
||||
|
||||
if !data.can_compare() {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let n = data.sync_state.needs_compare(data.capture_rate.unwrap());
|
||||
|
||||
if !n {
|
||||
gst::log!(CAT, "Comparison not needed -> {:?}", data.sync_state);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let r = match data.method {
|
||||
Method::Fuzzy => {
|
||||
gst::log!(CAT, "Compare frames 'fuzzy' -> {:?}", data.sync_state);
|
||||
let settings = self.settings.lock().unwrap();
|
||||
Self::compare_fuzzy(&data.ping_window, &data.pong_window, settings.threshold)
|
||||
}
|
||||
Method::Accurate | Method::Auto => {
|
||||
gst::log!(CAT, "Compare frames 'accurate' -> {:?}", data.sync_state);
|
||||
let crc_ping = Self::calc_checksum(&data.ping_window);
|
||||
let crc_pong = Self::calc_checksum(&data.pong_window);
|
||||
crc_ping == crc_pong
|
||||
}
|
||||
};
|
||||
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
fn sink_chain(
|
||||
&self,
|
||||
pad: &gst::Pad,
|
||||
mut buffer: gst::Buffer,
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
{
|
||||
let mut data = self.data.lock().unwrap();
|
||||
|
||||
// if we don't have supported caps just forward the frame
|
||||
if data.capture_rate.is_none() || data.sync_state.is_idle() {
|
||||
gst::log!(CAT, obj: pad, "Passthrough");
|
||||
drop(data);
|
||||
return self.srcpad.push(buffer);
|
||||
}
|
||||
|
||||
// otherwise advance the frame counter
|
||||
if data.capture_rate.is_some() {
|
||||
let r = data.capture_rate.unwrap();
|
||||
data.sync_state.advance(r);
|
||||
}
|
||||
}
|
||||
|
||||
// save the buffer and check for frame match
|
||||
self.save_frame(&buffer);
|
||||
let m = match self.compare_frames() {
|
||||
Err(_) => {
|
||||
gst::log!(CAT, obj: pad, "Need at least two frames for comparison");
|
||||
return self.srcpad.push(buffer);
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
|
||||
let mut data = self.data.lock().unwrap();
|
||||
let r = data.capture_rate.unwrap();
|
||||
let settings = self.settings.lock().unwrap();
|
||||
|
||||
// if we are synced and frame comparison failed or if we lost
|
||||
// sync let's try to recover
|
||||
if !data.sync_state.eval_compare(r, m) {
|
||||
let s = data.on_sync_lost(
|
||||
settings.method,
|
||||
settings.retries,
|
||||
settings.tolerance,
|
||||
settings.content_rate,
|
||||
);
|
||||
gst::info!(
|
||||
CAT,
|
||||
obj: pad,
|
||||
"Unexpected frame mismatch - lost sync (solution: {:?}) -> {:?}",
|
||||
s,
|
||||
data.sync_state
|
||||
);
|
||||
|
||||
// if we are idle (no sync retries left) tell ocvrhint to
|
||||
// start pattern matching again
|
||||
let mut hint = None;
|
||||
let mut caps = None;
|
||||
if settings.in_hint_mode() && data.sync_state.is_idle() {
|
||||
// let downstream know about the original caps
|
||||
caps = Some(data.upstream_caps.copy());
|
||||
hint = Some(
|
||||
gst::Structure::builder("ocvrctrl")
|
||||
.field("synced", false)
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
|
||||
drop(settings);
|
||||
drop(data);
|
||||
if let Some(s) = hint {
|
||||
self.srcpad
|
||||
.push_event(gst::event::Caps::new(&caps.unwrap()));
|
||||
self.srcpad
|
||||
.push_event(gst::event::CustomDownstream::builder(s).build());
|
||||
}
|
||||
return self.srcpad.push(buffer);
|
||||
}
|
||||
|
||||
let c = data.sync_state.update(r, m);
|
||||
if m {
|
||||
gst::log!(
|
||||
CAT,
|
||||
obj: pad,
|
||||
"Frames match or no check needed -> {:?}",
|
||||
data.sync_state
|
||||
);
|
||||
} else {
|
||||
gst::log!(CAT, obj: pad, "Frames mismatch -> {:?}", data.sync_state);
|
||||
}
|
||||
|
||||
let mut hint = None;
|
||||
let mut caps = None;
|
||||
if c && data.sync_state.is_synced() {
|
||||
// reset sync method and retries once we are synced
|
||||
data.on_synced(settings.retries);
|
||||
gst::info!(CAT, obj: pad, "Synced -> {:?}", data.sync_state);
|
||||
// if we receive hints and we are synced tell the hinter
|
||||
// to stop looking for pattern matches because we start
|
||||
// dropping frames and matching will not work anymore
|
||||
if settings.in_hint_mode() {
|
||||
hint = Some(
|
||||
gst::Structure::builder("ocvrctrl")
|
||||
.field("synced", true)
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
|
||||
// let downstream know about the new caps
|
||||
if settings.send_caps {
|
||||
caps = Some(data.synced_caps(settings.drop));
|
||||
}
|
||||
}
|
||||
|
||||
// check if we should drop the frame
|
||||
let d = data.sync_state.drop(settings.drop, r);
|
||||
|
||||
// adjust PTS and duration if buffer is not dropped
|
||||
if data.sync_state.is_synced() && !d {
|
||||
if let (Some(mut pts), Some(mut dur)) = (buffer.pts(), buffer.duration()) {
|
||||
let b = buffer.make_mut();
|
||||
if data.sync_state.ts_adjust(r, &mut pts) {
|
||||
b.set_pts(pts);
|
||||
}
|
||||
|
||||
if data.sync_state.dur_adjust(r, settings.drop, &mut dur) {
|
||||
b.set_duration(dur);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if d {
|
||||
gst::info!(CAT, obj: pad, "Drop frame -> {:?}", data.sync_state);
|
||||
} else {
|
||||
gst::log!(CAT, obj: pad, "Fwd frame");
|
||||
}
|
||||
|
||||
drop(settings);
|
||||
drop(data);
|
||||
|
||||
if let Some(h) = hint {
|
||||
self.srcpad
|
||||
.push_event(gst::event::CustomDownstream::builder(h).build());
|
||||
}
|
||||
|
||||
if let Some(c) = caps {
|
||||
self.srcpad.push_event(gst::event::Caps::new(&c));
|
||||
}
|
||||
|
||||
if !d {
|
||||
self.srcpad.push(buffer)
|
||||
} else {
|
||||
Ok(gst::FlowSuccess::Ok)
|
||||
}
|
||||
}
|
||||
|
||||
fn sink_event(&self, pad: &gst::Pad, event: gst::Event) -> bool {
|
||||
if let gst::EventView::Caps(e) = event.view() {
|
||||
gst::log!(CAT, obj: pad, "Handling event {:?}", e);
|
||||
|
||||
let mut data = self.data.lock().unwrap();
|
||||
let settings = self.settings.lock().unwrap();
|
||||
data.reset(settings.content_rate, settings.method, settings.retries);
|
||||
|
||||
if let Ok(info) = VideoInfo::from_caps(e.caps()) {
|
||||
data.upstream_caps = e.caps().copy();
|
||||
data.capture_rate = match info.fps().round().numer() {
|
||||
50 => {
|
||||
if settings.capture_rate(CaptureRate::HZ_50) {
|
||||
Some(CaptureRate::HZ_50)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
60 => {
|
||||
if settings.capture_rate(CaptureRate::HZ_60) {
|
||||
Some(CaptureRate::HZ_60)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
gst::log!(CAT, obj: pad, "Input framerate {:?}", data.capture_rate);
|
||||
|
||||
// Remember format and size
|
||||
data.frame_format = Some(info.format());
|
||||
data.frame_size = (info.width(), info.height());
|
||||
gst::log!(CAT, obj: pad, "Input format {:?}", data.frame_format);
|
||||
gst::log!(CAT, obj: pad, "Input size {:?}", data.frame_size);
|
||||
} else {
|
||||
data.frame_format = None;
|
||||
data.frame_size = (0, 0);
|
||||
}
|
||||
|
||||
// we may have to update the content rate from the capture rate
|
||||
data.reset_content_rate(settings.content_rate);
|
||||
gst::log!(CAT, obj: pad, "{:?}", data);
|
||||
}
|
||||
|
||||
self.srcpad.push_event(event)
|
||||
}
|
||||
|
||||
fn src_event(&self, pad: &gst::Pad, event: gst::Event) -> bool {
|
||||
gst::log!(CAT, obj: pad, "Handling event {:?}", event);
|
||||
|
||||
if let gst::EventView::CustomUpstream(e) = event.view() {
|
||||
// Extract the framerate hint
|
||||
if let Some(s) = e.structure() {
|
||||
if s.name() != "ocvrhint" {
|
||||
return self.sinkpad.push_event(event);
|
||||
}
|
||||
|
||||
// do we listen to ocvrhint
|
||||
let settings = self.settings.lock().unwrap();
|
||||
if !settings.in_hint_mode() {
|
||||
drop(settings);
|
||||
return self.sinkpad.push_event(event);
|
||||
}
|
||||
|
||||
let mut data = self.data.lock().unwrap();
|
||||
data.reset_on_hint(settings.method, settings.retries);
|
||||
|
||||
data.content_rate = match s.get::<u32>("rate").unwrap() {
|
||||
24 => Some(ContentRate::Hz24),
|
||||
30 => Some(ContentRate::Hz30),
|
||||
60 => Some(ContentRate::Hz60),
|
||||
_ => None,
|
||||
};
|
||||
gst::info!(
|
||||
CAT,
|
||||
obj: pad,
|
||||
"'ocvrhint' event found, rate {:?}",
|
||||
data.content_rate
|
||||
);
|
||||
if data.content_rate.is_some() && data.sync_state.is_idle() {
|
||||
data.method_overwrite();
|
||||
data.sync_state = SyncState::sync(data.content_rate.unwrap());
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
self.sinkpad.push_event(event)
|
||||
}
|
||||
} else {
|
||||
self.sinkpad.push_event(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for OcvrCtrl {
|
||||
const NAME: &'static str = "GstOcvrCtrl";
|
||||
type Type = super::OcvrCtrl;
|
||||
type ParentType = gst::Element;
|
||||
|
||||
fn with_class(klass: &Self::Class) -> Self {
|
||||
let templ = klass.pad_template("sink").unwrap();
|
||||
let sinkpad = gst::Pad::builder_from_template(&templ)
|
||||
.chain_function(|pad, parent, buffer| {
|
||||
OcvrCtrl::catch_panic_pad_function(
|
||||
parent,
|
||||
|| Err(gst::FlowError::Error),
|
||||
|ocvr_monitor| ocvr_monitor.sink_chain(pad, buffer),
|
||||
)
|
||||
})
|
||||
.event_function(|pad, parent, event| {
|
||||
OcvrCtrl::catch_panic_pad_function(
|
||||
parent,
|
||||
|| false,
|
||||
|ocvr_monitor| ocvr_monitor.sink_event(pad, event),
|
||||
)
|
||||
})
|
||||
.build();
|
||||
|
||||
let templ = klass.pad_template("src").unwrap();
|
||||
let srcpad = gst::Pad::builder_from_template(&templ)
|
||||
.event_function(|pad, parent, event| {
|
||||
OcvrCtrl::catch_panic_pad_function(
|
||||
parent,
|
||||
|| false,
|
||||
|ocvr_monitor| ocvr_monitor.src_event(pad, event),
|
||||
)
|
||||
})
|
||||
.build();
|
||||
|
||||
let settings: Mutex<Settings> = Default::default();
|
||||
let data: Mutex<Data> = Default::default();
|
||||
|
||||
Self {
|
||||
srcpad,
|
||||
sinkpad,
|
||||
settings,
|
||||
data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for OcvrCtrl {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
let obj = self.obj();
|
||||
obj.add_pad(&self.sinkpad).unwrap();
|
||||
obj.add_pad(&self.srcpad).unwrap();
|
||||
}
|
||||
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
// Metadata for the properties
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecEnum::builder::<ContentRate>("content-rate")
|
||||
.nick("Content rate")
|
||||
.blurb("Framerate to detect")
|
||||
.mutable_playing()
|
||||
.build(),
|
||||
glib::ParamSpecFlags::builder::<CaptureRate>("capture-rates")
|
||||
.nick("Capture rates")
|
||||
.blurb("Sink pad framerates to check")
|
||||
.default_value(CaptureRate {
|
||||
bits: DEFAULT_CAPTURE_RATES.bits(),
|
||||
})
|
||||
.mutable_playing()
|
||||
.build(),
|
||||
glib::ParamSpecEnum::builder::<Method>("method")
|
||||
.nick("Check method")
|
||||
.blurb("Method to check for duplicate frames")
|
||||
.mutable_playing()
|
||||
.build(),
|
||||
glib::ParamSpecUInt::builder("threshold")
|
||||
.nick("Threshold")
|
||||
.blurb("Compare threshold for method 'fuzzy'")
|
||||
.minimum(1)
|
||||
.maximum((u8::MAX - 1) as u32)
|
||||
.default_value(DEFAULT_THRESHOLD)
|
||||
.mutable_playing()
|
||||
.build(),
|
||||
glib::ParamSpecEnum::builder::<Tolerance>("tolerance")
|
||||
.nick("Check tolerance")
|
||||
.blurb("How to handle failed comparisons")
|
||||
.mutable_playing()
|
||||
.build(),
|
||||
glib::ParamSpecUInt::builder("retries")
|
||||
.nick("Times to retries check")
|
||||
.blurb("Retry times for tolerance 'lazy' or 'strict'")
|
||||
.minimum(0)
|
||||
.maximum(100)
|
||||
.default_value(DEFAULT_RETRIES)
|
||||
.mutable_playing()
|
||||
.build(),
|
||||
glib::ParamSpecUInt::builder("rows")
|
||||
.nick("Rows to consider")
|
||||
.blurb("Number of pixel rows considered for 'fuzzy' compare")
|
||||
.minimum(0)
|
||||
.maximum(u32::MAX - 1)
|
||||
.default_value(DEFAULT_ROWS)
|
||||
.mutable_playing()
|
||||
.build(),
|
||||
glib::ParamSpecBoolean::builder("drop")
|
||||
.nick("Make 30Hz from 60Hz")
|
||||
.blurb("Change 60Hz capture rate to 30Hz always")
|
||||
.default_value(DEFAULT_DROP)
|
||||
.mutable_playing()
|
||||
.build(),
|
||||
glib::ParamSpecBoolean::builder("send-caps")
|
||||
.nick("Send caps event on rate change")
|
||||
.blurb("Send caps event downstream on content rate change")
|
||||
.default_value(DEFAULT_SEND_CAPS)
|
||||
.mutable_playing()
|
||||
.build(),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
match pspec.name() {
|
||||
"content-rate" => {
|
||||
let rate = value.get().expect("type checked upstream");
|
||||
settings.content_rate = rate;
|
||||
let mut data = self.data.lock().unwrap();
|
||||
data.sync_state = SyncState::sync(rate);
|
||||
}
|
||||
"capture-rates" => {
|
||||
let rates = value.get().expect("type checked upstream");
|
||||
settings.capture_rates = rates;
|
||||
}
|
||||
"method" => {
|
||||
let method = value.get().expect("type checked upstream");
|
||||
settings.method = method;
|
||||
}
|
||||
"threshold" => {
|
||||
let threshold = value.get().expect("type checked upstream");
|
||||
settings.threshold = threshold;
|
||||
}
|
||||
"tolerance" => {
|
||||
let tolerance = value.get().expect("type checked upstream");
|
||||
settings.tolerance = tolerance;
|
||||
}
|
||||
"retries" => {
|
||||
let retries = value.get().expect("type checked upstream");
|
||||
settings.retries = retries;
|
||||
}
|
||||
"rows" => {
|
||||
let rows = value.get().expect("type checked upstream");
|
||||
settings.rows = rows;
|
||||
}
|
||||
"drop" => {
|
||||
let drop = value.get().expect("type checked upstream");
|
||||
settings.drop = drop;
|
||||
}
|
||||
"send-caps" => {
|
||||
let sc = value.get().expect("type checked upstream");
|
||||
settings.send_caps = sc;
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
match pspec.name() {
|
||||
"content-rate" => settings.content_rate.to_value(),
|
||||
"capture-rates" => settings.capture_rates.to_value(),
|
||||
"method" => settings.method.to_value(),
|
||||
"threshold" => (settings.threshold).to_value(),
|
||||
"tolerance" => settings.tolerance.to_value(),
|
||||
"retries" => (settings.retries).to_value(),
|
||||
"rows" => (settings.rows).to_value(),
|
||||
"drop" => settings.drop.to_value(),
|
||||
"send-caps" => settings.send_caps.to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GstObjectImpl for OcvrCtrl {}
|
||||
|
||||
impl ElementImpl for OcvrCtrl {
|
||||
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||
gst::subclass::ElementMetadata::new(
|
||||
"Original content video rate controller",
|
||||
"Parser/Video",
|
||||
"Precise check estimated framerate",
|
||||
"Jochen Henneberg <jh@henneberg-systemdesign.com>",
|
||||
)
|
||||
});
|
||||
|
||||
Some(&*ELEMENT_METADATA)
|
||||
}
|
||||
|
||||
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
|
||||
let caps = gst::Caps::builder("video/x-raw")
|
||||
.field(
|
||||
"format",
|
||||
gst::List::new([VideoFormat::I420.to_str(), VideoFormat::Nv12.to_str()]),
|
||||
)
|
||||
.build();
|
||||
|
||||
let src_pad_template = gst::PadTemplate::new(
|
||||
"src",
|
||||
gst::PadDirection::Src,
|
||||
gst::PadPresence::Always,
|
||||
&caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sink_pad_template = gst::PadTemplate::new(
|
||||
"sink",
|
||||
gst::PadDirection::Sink,
|
||||
gst::PadPresence::Always,
|
||||
&caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vec![src_pad_template, sink_pad_template]
|
||||
});
|
||||
|
||||
PAD_TEMPLATES.as_ref()
|
||||
}
|
||||
}
|
178
video/ocvr/src/ocvrctrl/imp/data.rs
Normal file
178
video/ocvr/src/ocvrctrl/imp/data.rs
Normal file
|
@ -0,0 +1,178 @@
|
|||
// Copyright (C) 2022 Jochen Henneberg <jh@henneberg-systemdesign.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst_video::VideoFormat;
|
||||
|
||||
use super::CaptureRate;
|
||||
use super::ContentRate;
|
||||
use super::Method;
|
||||
use super::SyncState;
|
||||
use super::Tolerance;
|
||||
|
||||
// Runtime value storage
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Data {
|
||||
pub content_rate: Option<ContentRate>,
|
||||
pub capture_rate: Option<CaptureRate>,
|
||||
pub frame_format: Option<VideoFormat>,
|
||||
pub frame_size: (u32, u32),
|
||||
pub ping_window: Vec<u8>,
|
||||
pub pong_window: Vec<u8>,
|
||||
pub is_ping: bool,
|
||||
pub sync_state: SyncState,
|
||||
pub method: Method,
|
||||
pub retries: u32,
|
||||
pub upstream_caps: gst::Caps,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ResyncSolution {
|
||||
FuzzyCompare,
|
||||
Retry(u32),
|
||||
None,
|
||||
}
|
||||
|
||||
impl Default for Data {
|
||||
fn default() -> Self {
|
||||
Data {
|
||||
content_rate: None,
|
||||
capture_rate: None,
|
||||
frame_format: None,
|
||||
frame_size: (0, 0),
|
||||
ping_window: vec![],
|
||||
pong_window: vec![],
|
||||
sync_state: SyncState::Idle,
|
||||
is_ping: true,
|
||||
method: Method::Auto,
|
||||
retries: 0,
|
||||
upstream_caps: gst::Caps::new_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Data {
|
||||
pub fn reset(&mut self, rate: ContentRate, method: Method, retries: u32) {
|
||||
self.capture_rate.take();
|
||||
// must be called after capture rate has been reset
|
||||
self.reset_content_rate(rate);
|
||||
self.frame_format.take();
|
||||
self.frame_size = (0, 0);
|
||||
self.reset_on_hint(method, retries);
|
||||
}
|
||||
|
||||
pub fn reset_content_rate(&mut self, rate: ContentRate) {
|
||||
self.content_rate = match rate {
|
||||
ContentRate::Auto => match self.capture_rate {
|
||||
Some(r) => match self.content_rate {
|
||||
Some(_) => Some(self.content_rate.unwrap().next(r)),
|
||||
None => Some(ContentRate::first(r)),
|
||||
},
|
||||
None => None,
|
||||
},
|
||||
ContentRate::Hint => None,
|
||||
_ => Some(rate),
|
||||
};
|
||||
|
||||
if self.content_rate.is_some() {
|
||||
self.sync_state = SyncState::sync(self.content_rate.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_on_hint(&mut self, method: Method, retries: u32) {
|
||||
self.ping_window = vec![];
|
||||
self.pong_window = vec![];
|
||||
match self.content_rate {
|
||||
None => self.sync_state.reset(),
|
||||
Some(r) => {
|
||||
self.method_overwrite();
|
||||
self.sync_state = SyncState::sync(r);
|
||||
}
|
||||
}
|
||||
self.is_ping = true;
|
||||
self.method = method;
|
||||
self.retries = retries;
|
||||
}
|
||||
|
||||
pub fn synced_caps(&self, drop: bool) -> gst::Caps {
|
||||
let r = match self.content_rate.unwrap() {
|
||||
ContentRate::Hz24 => 24,
|
||||
ContentRate::Hz25 => 25,
|
||||
ContentRate::Hz30 => 30,
|
||||
ContentRate::Hz50 => 50 / (drop as i32 + 1),
|
||||
ContentRate::Hz60 => 60 / (drop as i32 + 1),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let mut c = self.upstream_caps.clone();
|
||||
let s = c.make_mut().structure_mut(0).unwrap();
|
||||
s.set("framerate", gst::Fraction::new(r, 1));
|
||||
c
|
||||
}
|
||||
|
||||
pub fn method_overwrite(&mut self) {
|
||||
if self.method != Method::Auto {
|
||||
return;
|
||||
}
|
||||
|
||||
// in case of 60Hz content where fuzzy comparison might be
|
||||
// possible we have to choose fuzzy comparison otherwise we
|
||||
// may be stuck in the mismatch case forever
|
||||
if self.content_rate.unwrap() == ContentRate::Hz60 {
|
||||
self.method = Method::Fuzzy
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_compare(&self) -> bool {
|
||||
!self.ping_window.is_empty() && self.ping_window.len() == self.pong_window.len()
|
||||
}
|
||||
|
||||
pub fn on_synced(&mut self, retries: u32) {
|
||||
self.retries = retries;
|
||||
}
|
||||
|
||||
pub fn on_sync_lost(
|
||||
&mut self,
|
||||
method: Method,
|
||||
retries: u32,
|
||||
tolerance: Tolerance,
|
||||
rate: ContentRate,
|
||||
) -> ResyncSolution {
|
||||
self.ping_window = vec![];
|
||||
self.pong_window = vec![];
|
||||
self.is_ping = true;
|
||||
|
||||
match (self.method, self.retries, tolerance) {
|
||||
(Method::Auto, _, _) => {
|
||||
// in auto mode try again with fuzzy comparison
|
||||
self.method = Method::Fuzzy;
|
||||
self.sync_state.resync();
|
||||
ResyncSolution::FuzzyCompare
|
||||
}
|
||||
(_, _, Tolerance::Paranoid) | (_, 0, Tolerance::Strict) => {
|
||||
self.sync_state.reset();
|
||||
self.content_rate.take();
|
||||
self.method = method;
|
||||
self.retries = retries;
|
||||
ResyncSolution::None
|
||||
}
|
||||
(_, 0, Tolerance::Lazy) => {
|
||||
// endless retries
|
||||
self.retries = retries;
|
||||
self.sync_state.resync();
|
||||
ResyncSolution::Retry(self.retries)
|
||||
}
|
||||
(_, _, _) => {
|
||||
// retry
|
||||
if matches!(rate, ContentRate::Auto) {
|
||||
self.content_rate =
|
||||
Some(self.content_rate.unwrap().next(self.capture_rate.unwrap()));
|
||||
self.sync_state = SyncState::sync(self.content_rate.unwrap());
|
||||
}
|
||||
self.retries -= 1;
|
||||
self.sync_state.resync();
|
||||
ResyncSolution::Retry(self.retries)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
56
video/ocvr/src/ocvrctrl/imp/settings.rs
Normal file
56
video/ocvr/src/ocvrctrl/imp/settings.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
// Copyright (C) 2022 Jochen Henneberg <jh@henneberg-systemdesign.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use super::CaptureRate;
|
||||
use super::ContentRate;
|
||||
use super::Method;
|
||||
use super::Tolerance;
|
||||
|
||||
// Default values of properties
|
||||
pub const DEFAULT_CAPTURE_RATES: CaptureRate = CaptureRate::all();
|
||||
pub const DEFAULT_THRESHOLD: u32 = 1;
|
||||
pub const DEFAULT_RETRIES: u32 = 3;
|
||||
pub const DEFAULT_ROWS: u32 = 10;
|
||||
pub const DEFAULT_DROP: bool = false;
|
||||
pub const DEFAULT_SEND_CAPS: bool = true;
|
||||
|
||||
// Property value storage
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Settings {
|
||||
pub content_rate: ContentRate,
|
||||
pub capture_rates: CaptureRate,
|
||||
pub method: Method,
|
||||
pub threshold: u32,
|
||||
pub tolerance: Tolerance,
|
||||
pub retries: u32,
|
||||
pub rows: u32,
|
||||
pub drop: bool,
|
||||
pub send_caps: bool,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings {
|
||||
content_rate: ContentRate::default(),
|
||||
capture_rates: DEFAULT_CAPTURE_RATES,
|
||||
method: Method::default(),
|
||||
threshold: DEFAULT_THRESHOLD,
|
||||
tolerance: Tolerance::default(),
|
||||
retries: DEFAULT_RETRIES,
|
||||
rows: DEFAULT_ROWS,
|
||||
drop: DEFAULT_DROP,
|
||||
send_caps: DEFAULT_SEND_CAPS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn in_hint_mode(&self) -> bool {
|
||||
self.content_rate == ContentRate::Hint
|
||||
}
|
||||
|
||||
pub fn capture_rate(&self, rate: CaptureRate) -> bool {
|
||||
self.capture_rates.contains(rate)
|
||||
}
|
||||
}
|
503
video/ocvr/src/ocvrctrl/imp/syncstate.rs
Normal file
503
video/ocvr/src/ocvrctrl/imp/syncstate.rs
Normal file
|
@ -0,0 +1,503 @@
|
|||
// Copyright (C) 2022 Jochen Henneberg <jh@henneberg-systemdesign.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use super::CaptureRate;
|
||||
use super::ContentRate;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum SyncState {
|
||||
Idle,
|
||||
// rate, frame counter, consecutive matching frames
|
||||
Syncing(ContentRate, u32, u32),
|
||||
SyncLost(ContentRate),
|
||||
// current frame index within period
|
||||
Hz24(u32),
|
||||
Hz25(u32),
|
||||
Hz30(u32),
|
||||
// current frame index within period, compare match/mismatch
|
||||
Hz50(u32, bool),
|
||||
Hz60(u32, bool),
|
||||
}
|
||||
|
||||
macro_rules! advance_or_reset {
|
||||
($v: ident, $p: expr) => {{
|
||||
*$v += 1;
|
||||
if *$v == $p {
|
||||
*$v = 0;
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! advance_matches {
|
||||
($m: ident, $a: expr) => {{
|
||||
if $a {
|
||||
*$m += 1;
|
||||
} else {
|
||||
*$m = 0;
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
impl SyncState {
|
||||
// pattern matching explanation
|
||||
// 0: new frame
|
||||
// x: repeated frame
|
||||
// _: frame to save
|
||||
// ^: frame to compare with previous frame
|
||||
|
||||
// naming scheme explanation
|
||||
// HZxx_yy_PERIOD: the number of frames in a pattern
|
||||
// HZxx_yy_SYNC_PERIOD: max. number of frames to process while sync'ing
|
||||
// HZxx_yy_SYNC_MATCHES: required number of subsequent matches/mismatches
|
||||
// for successful sync
|
||||
// HZxx_yy_SYNCED_PERIOD: observation period when sync'ed
|
||||
// HZxx_yy_SYNCED_START: position within period when getting into
|
||||
// sync'ed state
|
||||
|
||||
// pattern: OxOx|OxOx|Ox...
|
||||
const HZ25_50_PERIOD: u32 = 2;
|
||||
// pattern: |OxOxOxOxOxOx|Ox...
|
||||
// check: _^
|
||||
const HZ25_50_SYNCED_PERIOD: u32 = 6 * Self::HZ25_50_PERIOD;
|
||||
// index to start with after sync
|
||||
const HZ25_50_SYNCED_START: u32 = 1;
|
||||
// pattern: OxOxOxOxOxOxOx
|
||||
// _^_^_^
|
||||
const HZ25_50_SYNC_PERIOD: u32 = 8;
|
||||
// match - mismatch - match - mismatch - match
|
||||
const HZ25_50_SYNC_MATCHES: u32 = 5;
|
||||
|
||||
// pattern: OxOx0|OxOx0|Ox...
|
||||
const HZ30_50_PERIOD: u32 = 5;
|
||||
// pattern: |OxOxOOxOxO|0x...
|
||||
// check: _^ _^
|
||||
const HZ30_50_SYNCED_PERIOD: u32 = 2 * Self::HZ30_50_PERIOD;
|
||||
// index to start with after sync
|
||||
const HZ30_50_SYNCED_START: u32 = 0;
|
||||
// pattern: OxOxO0xOx
|
||||
// _^_^_^
|
||||
const HZ30_50_SYNC_PERIOD: u32 = 3 * Self::HZ30_50_PERIOD;
|
||||
// match - mismatch - match - mismatch - mismatch
|
||||
const HZ30_50_SYNC_MATCHES: u32 = 5;
|
||||
|
||||
// pattern: O|O|O|O|O...
|
||||
const HZ50_50_PERIOD: u32 = 1;
|
||||
// pattern: |OOOOOOOOOO|O0...
|
||||
// check : _^^^
|
||||
const HZ50_50_SYNCED_PERIOD: u32 = 10 * Self::HZ50_50_PERIOD;
|
||||
|
||||
// pattern: OxxOx|OxxOx|Oxx...
|
||||
const HZ24_60_PERIOD: u32 = 5;
|
||||
// pattern: |OxxOxOxxOxOxxOx|Oxx...
|
||||
// check: _^
|
||||
const HZ24_60_SYNCED_PERIOD: u32 = 3 * Self::HZ24_60_PERIOD;
|
||||
// index to start with after sync
|
||||
const HZ24_60_SYNCED_START: u32 = 2;
|
||||
// pattern: OxxOxOxxOx
|
||||
// _^^
|
||||
const HZ24_60_SYNC_PERIOD: u32 = 8;
|
||||
// 2 consecutive matches -> 3 consecutive identical frames
|
||||
const HZ24_60_SYNC_MATCHES: u32 = 2;
|
||||
|
||||
// pattern: OxOx|OxOx|Ox...
|
||||
const HZ30_60_PERIOD: u32 = 2;
|
||||
// pattern: |OxOxOxOxOxOx|Ox...
|
||||
// check: _^
|
||||
const HZ30_60_SYNCED_PERIOD: u32 = 6 * Self::HZ30_60_PERIOD;
|
||||
// index to start with after sync
|
||||
const HZ30_60_SYNCED_START: u32 = 1;
|
||||
// pattern: OxOxOxOxOxOxOx
|
||||
// _^_^_^
|
||||
const HZ30_60_SYNC_PERIOD: u32 = 8;
|
||||
// match - mismatch - match - mismatch - match
|
||||
const HZ30_60_SYNC_MATCHES: u32 = 5;
|
||||
|
||||
// pattern: O|O|O|O|O...
|
||||
const HZ60_60_PERIOD: u32 = 1;
|
||||
// pattern: |OOOOOOOOOO|O0...
|
||||
// check : _^^^
|
||||
const HZ60_60_SYNCED_PERIOD: u32 = 10 * Self::HZ60_60_PERIOD;
|
||||
|
||||
pub fn sync_lost(&self) -> bool {
|
||||
matches!(self, SyncState::SyncLost(_))
|
||||
}
|
||||
|
||||
pub fn is_idle(&self) -> bool {
|
||||
matches!(self, SyncState::Idle)
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
*self = SyncState::Idle;
|
||||
}
|
||||
|
||||
pub fn is_synced(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
SyncState::Hz24(_)
|
||||
| SyncState::Hz25(_)
|
||||
| SyncState::Hz30(_)
|
||||
| SyncState::Hz50(_, _)
|
||||
| SyncState::Hz60(_, _)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn resync(&mut self) {
|
||||
*self = match self {
|
||||
SyncState::Syncing(r, _, _) => SyncState::Syncing(*r, 0, 0),
|
||||
SyncState::Hz24(_) => SyncState::Syncing(ContentRate::Hz24, 0, 0),
|
||||
SyncState::Hz25(_) => SyncState::Syncing(ContentRate::Hz25, 0, 0),
|
||||
SyncState::Hz30(_) => SyncState::Syncing(ContentRate::Hz30, 0, 0),
|
||||
SyncState::Hz50(_, _) => SyncState::Hz50(0, false),
|
||||
SyncState::Hz60(_, _) => SyncState::Hz60(0, false),
|
||||
SyncState::SyncLost(r) => SyncState::Syncing(*r, 0, 0),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sync(rate: ContentRate) -> Self {
|
||||
match rate {
|
||||
ContentRate::Hint => SyncState::Idle,
|
||||
_ => SyncState::Syncing(rate, 0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// will never change the state but just its parameters
|
||||
pub fn advance(&mut self, rate: CaptureRate) {
|
||||
match rate {
|
||||
CaptureRate::HZ_50 => match self {
|
||||
SyncState::Idle => (),
|
||||
SyncState::SyncLost(_) => (),
|
||||
SyncState::Syncing(_, i, _) => *i += 1,
|
||||
SyncState::Hz25(i) => advance_or_reset!(i, Self::HZ25_50_SYNCED_PERIOD),
|
||||
SyncState::Hz30(i) => advance_or_reset!(i, Self::HZ30_50_SYNCED_PERIOD),
|
||||
SyncState::Hz50(i, _) => advance_or_reset!(i, Self::HZ50_50_SYNCED_PERIOD),
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
CaptureRate::HZ_60 => match self {
|
||||
SyncState::Idle => (),
|
||||
SyncState::SyncLost(_) => (),
|
||||
SyncState::Syncing(_, i, _) => *i += 1,
|
||||
SyncState::Hz24(i) => advance_or_reset!(i, Self::HZ24_60_SYNCED_PERIOD),
|
||||
SyncState::Hz30(i) => advance_or_reset!(i, Self::HZ30_60_SYNCED_PERIOD),
|
||||
SyncState::Hz60(i, _) => advance_or_reset!(i, Self::HZ60_60_SYNCED_PERIOD),
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn eval_compare(&mut self, rate: CaptureRate, m: bool) -> bool {
|
||||
// if we lost sync let's try to recover
|
||||
if self.sync_lost() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut r = m;
|
||||
match rate {
|
||||
CaptureRate::HZ_50 => match self {
|
||||
SyncState::Idle => (),
|
||||
SyncState::SyncLost(_) => (),
|
||||
SyncState::Syncing(_, _, _) => (),
|
||||
SyncState::Hz25(_) => (),
|
||||
SyncState::Hz30(_) => (),
|
||||
SyncState::Hz50(i, v) => {
|
||||
if *i == 1 {
|
||||
*v = m; // use the current result for future comparisons
|
||||
}
|
||||
if !(*v) && self.needs_compare(rate) {
|
||||
r = !m; // invert the result if we are looking for mismatch
|
||||
}
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
CaptureRate::HZ_60 => match self {
|
||||
SyncState::Idle => (),
|
||||
SyncState::SyncLost(_) => (),
|
||||
SyncState::Syncing(_, _, _) => (),
|
||||
SyncState::Hz24(_) => (),
|
||||
SyncState::Hz30(_) => (),
|
||||
SyncState::Hz60(i, v) => {
|
||||
if *i == 1 {
|
||||
*v = m; // use the current result for future comparisons
|
||||
}
|
||||
if !(*v) && self.needs_compare(rate) {
|
||||
r = !m; // invert the result if we are looking for mismatch
|
||||
}
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
!self.is_synced() || r
|
||||
}
|
||||
|
||||
// returns true if a state change happened
|
||||
pub fn update(&mut self, rate: CaptureRate, alike: bool) -> bool {
|
||||
let mut ret: bool = false;
|
||||
|
||||
// first handle the comparison result during syncing
|
||||
match rate {
|
||||
CaptureRate::HZ_50 => match self {
|
||||
SyncState::Syncing(ContentRate::Hz25, _, m) => {
|
||||
if *m & 1 == 0 {
|
||||
advance_matches!(m, alike);
|
||||
} else {
|
||||
advance_matches!(m, !alike);
|
||||
}
|
||||
}
|
||||
SyncState::Syncing(ContentRate::Hz30, _, m) => {
|
||||
if *m == 0 || *m == 2 {
|
||||
advance_matches!(m, alike);
|
||||
} else {
|
||||
advance_matches!(m, !alike);
|
||||
}
|
||||
}
|
||||
SyncState::Syncing(_, _, m) => advance_matches!(m, alike),
|
||||
_ => (),
|
||||
},
|
||||
CaptureRate::HZ_60 => match self {
|
||||
SyncState::Syncing(ContentRate::Hz30, _, m) => {
|
||||
if *m & 1 == 0 {
|
||||
advance_matches!(m, alike);
|
||||
} else {
|
||||
advance_matches!(m, !alike);
|
||||
}
|
||||
}
|
||||
SyncState::Syncing(_, _, m) => advance_matches!(m, alike),
|
||||
_ => (),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
// next update the current state if necessary
|
||||
*self = match rate {
|
||||
CaptureRate::HZ_50 => match self {
|
||||
SyncState::Idle => SyncState::Idle,
|
||||
SyncState::SyncLost(r) => SyncState::SyncLost(*r),
|
||||
SyncState::Syncing(ContentRate::Hz25, Self::HZ25_50_SYNC_PERIOD, _) => {
|
||||
ret = true;
|
||||
SyncState::SyncLost(ContentRate::Hz25)
|
||||
}
|
||||
SyncState::Syncing(ContentRate::Hz30, Self::HZ30_50_SYNC_PERIOD, _) => {
|
||||
ret = true;
|
||||
SyncState::SyncLost(ContentRate::Hz30)
|
||||
}
|
||||
SyncState::Syncing(ContentRate::Hz25, _, Self::HZ25_50_SYNC_MATCHES) => {
|
||||
ret = true;
|
||||
SyncState::Hz25(Self::HZ25_50_SYNCED_START)
|
||||
}
|
||||
SyncState::Syncing(ContentRate::Hz30, _, Self::HZ30_50_SYNC_MATCHES) => {
|
||||
ret = true;
|
||||
SyncState::Hz30(Self::HZ30_50_SYNCED_START)
|
||||
}
|
||||
SyncState::Syncing(ContentRate::Hz50, _, _) => {
|
||||
ret = true;
|
||||
SyncState::Hz50(0, false)
|
||||
}
|
||||
SyncState::Syncing(r, i, m) => SyncState::Syncing(*r, *i, *m),
|
||||
SyncState::Hz25(i) => SyncState::Hz25(*i),
|
||||
SyncState::Hz30(i) => SyncState::Hz30(*i),
|
||||
SyncState::Hz50(i, m) => SyncState::Hz50(*i, *m),
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
CaptureRate::HZ_60 => match self {
|
||||
SyncState::Idle => SyncState::Idle,
|
||||
SyncState::SyncLost(r) => SyncState::SyncLost(*r),
|
||||
SyncState::Syncing(ContentRate::Hz24, Self::HZ24_60_SYNC_PERIOD, _) => {
|
||||
ret = true;
|
||||
SyncState::SyncLost(ContentRate::Hz24)
|
||||
}
|
||||
SyncState::Syncing(ContentRate::Hz30, Self::HZ30_60_SYNC_PERIOD, _) => {
|
||||
ret = true;
|
||||
SyncState::SyncLost(ContentRate::Hz30)
|
||||
}
|
||||
SyncState::Syncing(ContentRate::Hz24, _, Self::HZ24_60_SYNC_MATCHES) => {
|
||||
ret = true;
|
||||
SyncState::Hz24(Self::HZ24_60_SYNCED_START)
|
||||
}
|
||||
SyncState::Syncing(ContentRate::Hz30, _, Self::HZ30_60_SYNC_MATCHES) => {
|
||||
ret = true;
|
||||
SyncState::Hz30(Self::HZ30_60_SYNCED_START)
|
||||
}
|
||||
SyncState::Syncing(ContentRate::Hz60, _, _) => {
|
||||
ret = true;
|
||||
SyncState::Hz60(0, false)
|
||||
}
|
||||
SyncState::Syncing(r, i, m) => SyncState::Syncing(*r, *i, *m),
|
||||
SyncState::Hz24(i) => SyncState::Hz24(*i),
|
||||
SyncState::Hz30(i) => SyncState::Hz30(*i),
|
||||
SyncState::Hz60(i, m) => SyncState::Hz60(*i, *m),
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn needs_compare(&self, rate: CaptureRate) -> bool {
|
||||
match rate {
|
||||
CaptureRate::HZ_50 => match self {
|
||||
SyncState::Idle => false,
|
||||
SyncState::SyncLost(_) => false,
|
||||
SyncState::Syncing(_, _, _) => true,
|
||||
SyncState::Hz25(i) => *i == 1,
|
||||
SyncState::Hz30(i) => *i == 1 || *i == 6,
|
||||
SyncState::Hz50(i, _) => (1..=3).contains(i),
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
CaptureRate::HZ_60 => match self {
|
||||
SyncState::Idle => false,
|
||||
SyncState::SyncLost(_) => false,
|
||||
SyncState::Syncing(_, _, _) => true,
|
||||
SyncState::Hz24(i) => *i == 2,
|
||||
SyncState::Hz30(i) => *i == 1,
|
||||
SyncState::Hz60(i, _) => (1..=3).contains(i),
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn needs_save(&self, rate: CaptureRate) -> bool {
|
||||
match rate {
|
||||
CaptureRate::HZ_50 => match self {
|
||||
SyncState::Idle => false,
|
||||
SyncState::SyncLost(_) => false,
|
||||
SyncState::Syncing(_, _, _) => true,
|
||||
SyncState::Hz25(i) => *i == 0 || *i == 1,
|
||||
SyncState::Hz30(i) => *i == 0 || *i == 1 || *i == 5 || *i == 6,
|
||||
SyncState::Hz50(i, _) => (0..=3).contains(i),
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
CaptureRate::HZ_60 => match self {
|
||||
SyncState::Idle => false,
|
||||
SyncState::SyncLost(_) => false,
|
||||
SyncState::Syncing(_, _, _) => true,
|
||||
SyncState::Hz24(i) => *i == 1 || *i == 2,
|
||||
SyncState::Hz30(i) => *i == 0 || *i == 1,
|
||||
SyncState::Hz60(i, _) => (0..=3).contains(i),
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ts_adjust(&self, rate: CaptureRate, pts: &mut gst::ClockTime) -> bool {
|
||||
match rate {
|
||||
CaptureRate::HZ_50 => match self {
|
||||
SyncState::Idle => false,
|
||||
SyncState::SyncLost(_) => false,
|
||||
SyncState::Syncing(_, _, _) => false,
|
||||
SyncState::Hz25(_) => false,
|
||||
SyncState::Hz30(i) => {
|
||||
if *i % Self::HZ30_50_PERIOD == 2 {
|
||||
*pts = pts.checked_sub(gst::ClockTime::from_mseconds(7)).unwrap();
|
||||
true
|
||||
} else if *i % Self::HZ30_50_PERIOD == 4 {
|
||||
*pts = pts.checked_sub(gst::ClockTime::from_mseconds(14)).unwrap();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
SyncState::Hz50(_, _) => false,
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
CaptureRate::HZ_60 => match self {
|
||||
SyncState::Idle => false,
|
||||
SyncState::SyncLost(_) => false,
|
||||
SyncState::Syncing(_, _, _) => false,
|
||||
SyncState::Hz24(i) => {
|
||||
if *i % Self::HZ24_60_PERIOD == 3 {
|
||||
*pts = pts.checked_sub(gst::ClockTime::from_mseconds(6)).unwrap();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
SyncState::Hz30(_) => false,
|
||||
SyncState::Hz60(_, _) => false,
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dur_adjust(&self, rate: CaptureRate, drop: bool, dur: &mut gst::ClockTime) -> bool {
|
||||
match rate {
|
||||
CaptureRate::HZ_50 => match self {
|
||||
SyncState::Idle => false,
|
||||
SyncState::SyncLost(_) => false,
|
||||
SyncState::Syncing(_, _, _) => false,
|
||||
SyncState::Hz25(_) => {
|
||||
*dur = gst::ClockTime::SECOND / 25;
|
||||
true
|
||||
}
|
||||
SyncState::Hz30(_) => {
|
||||
*dur = gst::ClockTime::SECOND / 30;
|
||||
true
|
||||
}
|
||||
SyncState::Hz50(_, _) => {
|
||||
if drop {
|
||||
*dur = gst::ClockTime::SECOND / 25;
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
CaptureRate::HZ_60 => match self {
|
||||
SyncState::Idle => false,
|
||||
SyncState::SyncLost(_) => false,
|
||||
SyncState::Syncing(_, _, _) => false,
|
||||
SyncState::Hz24(_) => {
|
||||
*dur = gst::ClockTime::SECOND / 24;
|
||||
true
|
||||
}
|
||||
SyncState::Hz30(_) => {
|
||||
*dur = gst::ClockTime::SECOND / 30;
|
||||
true
|
||||
}
|
||||
SyncState::Hz60(_, _) => {
|
||||
if drop {
|
||||
*dur = gst::ClockTime::SECOND / 30;
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drop(&self, drop: bool, rate: CaptureRate) -> bool {
|
||||
match rate {
|
||||
CaptureRate::HZ_50 => match self {
|
||||
SyncState::Idle => false,
|
||||
SyncState::SyncLost(_) => false,
|
||||
SyncState::Syncing(_, _, _) => false,
|
||||
SyncState::Hz25(i) => *i & 1 != 0,
|
||||
SyncState::Hz30(i) => {
|
||||
*i % Self::HZ30_50_PERIOD == 1 || *i % Self::HZ30_50_PERIOD == 3
|
||||
}
|
||||
SyncState::Hz50(i, _) => drop && *i & 1 != 0,
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
CaptureRate::HZ_60 => match self {
|
||||
SyncState::Idle => false,
|
||||
SyncState::SyncLost(_) => false,
|
||||
SyncState::Syncing(_, _, _) => false,
|
||||
SyncState::Hz24(i) => {
|
||||
*i % Self::HZ24_60_PERIOD == 1
|
||||
|| *i % Self::HZ24_60_PERIOD == 2
|
||||
|| *i % Self::HZ24_60_PERIOD == 4
|
||||
}
|
||||
SyncState::Hz30(i) => *i & 1 != 0,
|
||||
SyncState::Hz60(i, _) => drop && *i & 1 != 0,
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
30
video/ocvr/src/ocvrctrl/mod.rs
Normal file
30
video/ocvr/src/ocvrctrl/mod.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
// Copyright (C) 2022 Jochen Henneberg <jh@henneberg-systemdesign.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
|
||||
mod imp;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct OcvrCtrl(ObjectSubclass<imp::OcvrCtrl>) @extends gst::Element, gst::Object;
|
||||
}
|
||||
|
||||
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
#[cfg(feature = "doc")]
|
||||
imp::ContentRate::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
|
||||
#[cfg(feature = "doc")]
|
||||
imp::CaptureRate::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
|
||||
#[cfg(feature = "doc")]
|
||||
imp::Method::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
|
||||
#[cfg(feature = "doc")]
|
||||
imp::Tolerance::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
|
||||
|
||||
gst::Element::register(
|
||||
Some(plugin),
|
||||
"ocvrctrl",
|
||||
gst::Rank::None,
|
||||
OcvrCtrl::static_type(),
|
||||
)
|
||||
}
|
551
video/ocvr/src/ocvrhint/imp.rs
Normal file
551
video/ocvr/src/ocvrhint/imp.rs
Normal file
|
@ -0,0 +1,551 @@
|
|||
// Copyright (C) 2022 Jochen Henneberg <jh@henneberg-systemdesign.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
use gst::subclass::prelude::*;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
mod correlation;
|
||||
use correlation::Corr;
|
||||
|
||||
mod rateprobe;
|
||||
use rateprobe::RateProbe;
|
||||
|
||||
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||
gst::DebugCategory::new(
|
||||
"ocvrhint",
|
||||
gst::DebugColorFlags::empty(),
|
||||
Some("Original content video rate hinter"),
|
||||
)
|
||||
});
|
||||
|
||||
// Original content framerates to detect - the order matters, the
|
||||
// correlation is checked with increasing value
|
||||
#[glib::flags(name = "OcvrHintContentRate")]
|
||||
pub enum ContentRate {
|
||||
#[flags_value(name = "Content rate 24Hz (60Hz capture rate only)", nick = "24Hz")]
|
||||
HZ_24 = 0b00000001,
|
||||
#[flags_value(name = "Content rate 25Hz (50Hz capture rate only)", nick = "25Hz")]
|
||||
HZ_25 = 0b00000010,
|
||||
#[flags_value(name = "Content rate 30Hz", nick = "30Hz")]
|
||||
HZ_30 = 0b00000100,
|
||||
#[flags_value(name = "Content rate 50Hz (50Hz capture rate only)", nick = "50Hz")]
|
||||
HZ_50 = 0b00001000,
|
||||
#[flags_value(name = "Content rate 60Hz (60Hz capture rate only)", nick = "60Hz")]
|
||||
HZ_60 = 0b00010000,
|
||||
}
|
||||
|
||||
// Capture framerates to check
|
||||
#[glib::flags(name = "OcvrHintCaptureRate")]
|
||||
pub enum CaptureRate {
|
||||
#[flags_value(name = "Capture rate 50Hz", nick = "50Hz")]
|
||||
HZ_50 = 0b00000001,
|
||||
#[flags_value(name = "Capture rate 60Hz", nick = "60Hz")]
|
||||
HZ_60 = 0b00000010,
|
||||
}
|
||||
|
||||
// Test vectors for different framerates at capture rates
|
||||
const HZ25_IN_HZ50: &[i64] = &[10, 1];
|
||||
const HZ30_IN_HZ50: &[i64] = &[10, 1, 10, 1, 10];
|
||||
const HZ50_IN_HZ50: &[i64] = &[1, 1];
|
||||
|
||||
const HZ24_IN_HZ60: &[i64] = &[10, 1, 10, 1, 1];
|
||||
const HZ30_IN_HZ60: &[i64] = &[10, 1];
|
||||
const HZ60_IN_HZ60: &[i64] = &[1, 1];
|
||||
|
||||
// Default values of properties
|
||||
const DEFAULT_WINDOW_SIZE: usize = 4;
|
||||
const DEFAULT_THRESHOLD: f64 = 0.9;
|
||||
const DEFAULT_CONTENT_RATES: ContentRate = ContentRate::all();
|
||||
const DEFAULT_CAPTURE_RATES: CaptureRate = CaptureRate::all();
|
||||
|
||||
// Property value storage
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Settings {
|
||||
window_size: usize,
|
||||
threshold: f64,
|
||||
content_rates: ContentRate,
|
||||
capture_rates: CaptureRate,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings {
|
||||
window_size: DEFAULT_WINDOW_SIZE,
|
||||
threshold: DEFAULT_THRESHOLD,
|
||||
content_rates: DEFAULT_CONTENT_RATES,
|
||||
capture_rates: DEFAULT_CAPTURE_RATES,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Runtime value storage
|
||||
#[derive(Default, Debug)]
|
||||
struct Data {
|
||||
window: Vec<i64>,
|
||||
gop_count: usize,
|
||||
gop_size: Option<usize>,
|
||||
probes: BTreeMap<ContentRate, RateProbe>,
|
||||
rate: Option<CaptureRate>,
|
||||
content_rate: Option<ContentRate>,
|
||||
frame_counter: usize,
|
||||
pause: bool,
|
||||
}
|
||||
|
||||
impl Data {
|
||||
pub fn reset_window(&mut self) {
|
||||
self.window.clear();
|
||||
self.gop_count = 0;
|
||||
}
|
||||
|
||||
pub fn set_pause(&mut self, pause: bool) {
|
||||
if !pause {
|
||||
self.reset_window();
|
||||
self.gop_size.take();
|
||||
self.probes.clear();
|
||||
self.content_rate.take();
|
||||
self.frame_counter = 0;
|
||||
self.pause = false;
|
||||
} else {
|
||||
self.pause = true;
|
||||
self.content_rate.take();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OcvrHint {
|
||||
srcpad: gst::Pad,
|
||||
sinkpad: gst::Pad,
|
||||
settings: Mutex<Settings>,
|
||||
data: Mutex<Data>,
|
||||
}
|
||||
|
||||
impl OcvrHint {
|
||||
fn check_rate(window: &[i64], gop_size: usize, threshold: f64, probe: &RateProbe) -> bool {
|
||||
let mut res: f64 = 0.0;
|
||||
let mut corr = Corr::default();
|
||||
|
||||
for v in probe.iter() {
|
||||
corr.set_x(v);
|
||||
//gst::log!(CAT, "Probe {:?}", v);
|
||||
window.chunks(gop_size).all(|wc| {
|
||||
res = match corr.corr_y(wc) {
|
||||
Some(c) => res.max(c),
|
||||
None => res,
|
||||
};
|
||||
//gst::log!(CAT, "Window {:?} -> {:?}", wc, res);
|
||||
res < threshold
|
||||
});
|
||||
|
||||
if res > threshold {
|
||||
break;
|
||||
}
|
||||
}
|
||||
res > threshold
|
||||
}
|
||||
|
||||
fn build_probes(
|
||||
rate: CaptureRate,
|
||||
gop_size: usize,
|
||||
probes: &mut BTreeMap<ContentRate, RateProbe>,
|
||||
) {
|
||||
match rate {
|
||||
CaptureRate::HZ_50 => {
|
||||
probes.insert(ContentRate::HZ_25, RateProbe::new(gop_size, HZ25_IN_HZ50));
|
||||
probes.insert(ContentRate::HZ_30, RateProbe::new(gop_size, HZ30_IN_HZ50));
|
||||
probes.insert(ContentRate::HZ_50, RateProbe::new(gop_size, HZ50_IN_HZ50));
|
||||
}
|
||||
CaptureRate::HZ_60 => {
|
||||
probes.insert(ContentRate::HZ_24, RateProbe::new(gop_size, HZ24_IN_HZ60));
|
||||
probes.insert(ContentRate::HZ_30, RateProbe::new(gop_size, HZ30_IN_HZ60));
|
||||
probes.insert(ContentRate::HZ_60, RateProbe::new(gop_size, HZ60_IN_HZ60));
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn rate_to_int(r: Option<ContentRate>) -> u32 {
|
||||
if r.is_none() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
match r.unwrap() {
|
||||
ContentRate::HZ_24 => 24,
|
||||
ContentRate::HZ_25 => 25,
|
||||
ContentRate::HZ_30 => 30,
|
||||
ContentRate::HZ_50 => 50,
|
||||
ContentRate::HZ_60 => 60,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn sink_chain(
|
||||
&self,
|
||||
pad: &gst::Pad,
|
||||
buffer: gst::Buffer,
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
let mut data = self.data.lock().unwrap();
|
||||
|
||||
// If this is an input data rate that we should not care about
|
||||
// just forward the buffer
|
||||
if data.rate.is_none() || data.pause {
|
||||
drop(data);
|
||||
return self.srcpad.push(buffer);
|
||||
}
|
||||
|
||||
// If we should not check for this input rate just forward the
|
||||
// buffer
|
||||
let settings = self.settings.lock().unwrap();
|
||||
if !settings.capture_rates.contains(data.rate.unwrap()) {
|
||||
drop(data);
|
||||
drop(settings);
|
||||
return self.srcpad.push(buffer);
|
||||
}
|
||||
|
||||
// If this is not a reference frame advance the frame counter,
|
||||
// remember the buffer size and push the buffer
|
||||
if buffer.flags().contains(gst::BufferFlags::DELTA_UNIT) {
|
||||
if !data.window.is_empty() {
|
||||
data.window.push(buffer.size() as i64);
|
||||
data.frame_counter += 1;
|
||||
}
|
||||
drop(data);
|
||||
drop(settings);
|
||||
return self.srcpad.push(buffer);
|
||||
}
|
||||
|
||||
// We have a reference frame - let's go
|
||||
|
||||
// If the GOP size changed or we didn't have one set it
|
||||
if data.frame_counter > 0
|
||||
&& data
|
||||
.gop_size
|
||||
.map_or(true, |gop_size| gop_size != data.frame_counter)
|
||||
{
|
||||
let s = data.frame_counter;
|
||||
data.gop_size = Some(s);
|
||||
gst::log!(CAT, obj: pad, "Found GOP size {:?}", s);
|
||||
|
||||
// Reserve enough space for the window vector
|
||||
let w = s * settings.window_size;
|
||||
data.window.reserve(w);
|
||||
|
||||
// If we have an input frame rate that we should probe
|
||||
// let's set the probe vectors
|
||||
Self::build_probes(data.rate.unwrap(), data.gop_size.unwrap(), &mut data.probes);
|
||||
}
|
||||
|
||||
// Advance the GOP counter - might be reset later from
|
||||
// reset_window() if we have collected enough GOPs to satisfy
|
||||
// window_size
|
||||
if data.frame_counter != 0 {
|
||||
data.gop_count += 1;
|
||||
}
|
||||
|
||||
// If the window is complete check for framerate matches
|
||||
let mut m: Option<ContentRate> = None;
|
||||
let mut hint = None;
|
||||
if data.gop_count == settings.window_size {
|
||||
gst::trace!(
|
||||
CAT,
|
||||
obj: pad,
|
||||
"Processing {:?} GOPs in data window of size {:?}",
|
||||
data.gop_count,
|
||||
data.window.len()
|
||||
);
|
||||
|
||||
for (r, p) in data.probes.iter() {
|
||||
if !settings.content_rates.contains(*r) {
|
||||
continue;
|
||||
}
|
||||
|
||||
gst::trace!(
|
||||
CAT,
|
||||
obj: pad,
|
||||
"Correlate {:?} GOPs for rate {:?}",
|
||||
data.gop_count,
|
||||
r
|
||||
);
|
||||
gst::trace!(CAT, obj: pad, "Window: {:?}", &data.window);
|
||||
if !Self::check_rate(&data.window, data.gop_size.unwrap(), settings.threshold, p) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// As soon as we have a match we can leave
|
||||
m = Some(*r);
|
||||
break;
|
||||
}
|
||||
|
||||
if m != data.content_rate {
|
||||
data.content_rate = m;
|
||||
|
||||
// Send a custom upstream event with the newly detected original content rate
|
||||
let r = Self::rate_to_int(m);
|
||||
hint = Some(gst::Structure::builder("ocvrhint").field("rate", r).build());
|
||||
gst::log!(
|
||||
CAT,
|
||||
obj: pad,
|
||||
"Original content frame rate changed to {:?}",
|
||||
m
|
||||
);
|
||||
}
|
||||
|
||||
// Correlation done, reset window
|
||||
data.reset_window();
|
||||
}
|
||||
|
||||
// A new GOP started or the current GOP is complete so the
|
||||
// frame counter can be reset
|
||||
data.frame_counter = 1;
|
||||
|
||||
// Remember the buffer size
|
||||
data.window.push(buffer.size() as i64);
|
||||
|
||||
drop(data);
|
||||
drop(settings);
|
||||
if let Some(s) = hint {
|
||||
self.sinkpad
|
||||
.push_event(gst::event::CustomUpstream::builder(s).build());
|
||||
}
|
||||
self.srcpad.push(buffer)
|
||||
}
|
||||
|
||||
fn sink_event(&self, pad: &gst::Pad, event: gst::Event) -> bool {
|
||||
match event.view() {
|
||||
gst::EventView::Caps(e) => {
|
||||
gst::log!(CAT, obj: pad, "Handling event {:?}", event);
|
||||
|
||||
// Extract the framerate from caps
|
||||
let c = e.caps();
|
||||
let r = c.structure(0).unwrap().get::<gst::Fraction>("framerate");
|
||||
if let Ok(cr) = r {
|
||||
let n = *cr.round().numer();
|
||||
let rate = match n {
|
||||
50 => Some(CaptureRate::HZ_50),
|
||||
60 => Some(CaptureRate::HZ_60),
|
||||
_ => None,
|
||||
};
|
||||
gst::log!(CAT, obj: pad, "Input framerate {:?} from {:?}", rate, n);
|
||||
|
||||
let mut data = self.data.lock().unwrap();
|
||||
if rate != data.rate {
|
||||
gst::info!(
|
||||
CAT,
|
||||
obj: pad,
|
||||
"Input frame rate changed - {:?} -> {:?}",
|
||||
data.rate,
|
||||
rate
|
||||
);
|
||||
*data = Data::default();
|
||||
data.rate = rate;
|
||||
}
|
||||
}
|
||||
}
|
||||
gst::EventView::CustomDownstream(e) => {
|
||||
// Extract the controller state event
|
||||
if let Some(s) = e.structure() {
|
||||
if s.name() == "ocvrctrl" {
|
||||
let mut data = self.data.lock().unwrap();
|
||||
data.set_pause(s.get::<bool>("synced").unwrap());
|
||||
gst::info!(
|
||||
CAT,
|
||||
obj: pad,
|
||||
"'ocvrctrl' event found, synced {:?}",
|
||||
data.pause
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self.srcpad.push_event(event)
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for OcvrHint {
|
||||
const NAME: &'static str = "GstOcvrHint";
|
||||
type Type = super::OcvrHint;
|
||||
type ParentType = gst::Element;
|
||||
|
||||
fn with_class(klass: &Self::Class) -> Self {
|
||||
let templ = klass.pad_template("sink").unwrap();
|
||||
let sinkpad = gst::Pad::builder_from_template(&templ)
|
||||
.chain_function(|pad, parent, buffer| {
|
||||
OcvrHint::catch_panic_pad_function(
|
||||
parent,
|
||||
|| Err(gst::FlowError::Error),
|
||||
|ocvr_hint| ocvr_hint.sink_chain(pad, buffer),
|
||||
)
|
||||
})
|
||||
.event_function(|pad, parent, event| {
|
||||
OcvrHint::catch_panic_pad_function(
|
||||
parent,
|
||||
|| false,
|
||||
|ocvr_hint| ocvr_hint.sink_event(pad, event),
|
||||
)
|
||||
})
|
||||
.build();
|
||||
|
||||
let templ = klass.pad_template("src").unwrap();
|
||||
let srcpad = gst::Pad::builder_from_template(&templ).build();
|
||||
|
||||
let settings: Mutex<Settings> = Default::default();
|
||||
let data = Mutex::<Data>::default();
|
||||
|
||||
Self {
|
||||
srcpad,
|
||||
sinkpad,
|
||||
settings,
|
||||
data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for OcvrHint {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
let obj = self.obj();
|
||||
obj.add_pad(&self.sinkpad).unwrap();
|
||||
obj.add_pad(&self.srcpad).unwrap();
|
||||
}
|
||||
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecUInt::builder("window-size")
|
||||
.nick("Window size")
|
||||
.blurb("Multiple of GOP size")
|
||||
.minimum(1)
|
||||
.maximum(100)
|
||||
.default_value(DEFAULT_WINDOW_SIZE as u32)
|
||||
.mutable_playing()
|
||||
.build(),
|
||||
glib::ParamSpecFloat::builder("threshold")
|
||||
.nick("Threshold")
|
||||
.blurb("Framerate detect threshold")
|
||||
.minimum(0.0)
|
||||
.maximum(1.0)
|
||||
.default_value(DEFAULT_THRESHOLD as f32)
|
||||
.mutable_playing()
|
||||
.build(),
|
||||
glib::ParamSpecFlags::builder::<ContentRate>("content-rates")
|
||||
.nick("Content rates")
|
||||
.blurb("Framerates to detect")
|
||||
.default_value(ContentRate {
|
||||
bits: DEFAULT_CONTENT_RATES.bits(),
|
||||
})
|
||||
.mutable_playing()
|
||||
.build(),
|
||||
glib::ParamSpecFlags::builder::<CaptureRate>("capture-rates")
|
||||
.nick("Capture rates")
|
||||
.blurb("Sink pad framerates to check")
|
||||
.default_value(CaptureRate {
|
||||
bits: DEFAULT_CAPTURE_RATES.bits(),
|
||||
})
|
||||
.mutable_playing()
|
||||
.build(),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
match pspec.name() {
|
||||
"window-size" => {
|
||||
let ws: u32 = value.get().expect("type checked upstream");
|
||||
settings.window_size = ws as usize;
|
||||
}
|
||||
"threshold" => {
|
||||
let threshold: f32 = value.get().expect("type checked upstream");
|
||||
settings.threshold = threshold as f64;
|
||||
}
|
||||
"content-rates" => {
|
||||
let rates = value.get().expect("type checked upstream");
|
||||
settings.content_rates = rates;
|
||||
}
|
||||
"capture-rates" => {
|
||||
let rates = value.get().expect("type checked upstream");
|
||||
settings.capture_rates = rates;
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
match pspec.name() {
|
||||
"window-size" => (settings.window_size as u32).to_value(),
|
||||
"threshold" => (settings.threshold as f32).to_value(),
|
||||
"content-rates" => settings.content_rates.to_value(),
|
||||
"capture-rates" => settings.capture_rates.to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GstObjectImpl for OcvrHint {}
|
||||
|
||||
impl ElementImpl for OcvrHint {
|
||||
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||
gst::subclass::ElementMetadata::new(
|
||||
"Original content video rate hinter",
|
||||
"Parser/Video",
|
||||
"Detects the original content framerate from encoded video",
|
||||
"Jochen Henneberg <jh@henneberg-systemdesign.com>",
|
||||
)
|
||||
});
|
||||
|
||||
Some(&*ELEMENT_METADATA)
|
||||
}
|
||||
|
||||
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
|
||||
let mut caps = gst::Caps::new_empty();
|
||||
{
|
||||
let caps = caps.get_mut().unwrap();
|
||||
|
||||
caps.append(
|
||||
gst::Caps::builder("video/x-h265")
|
||||
.field("alignment", "au")
|
||||
.build(),
|
||||
);
|
||||
caps.append(
|
||||
gst::Caps::builder("video/x-h264")
|
||||
.field("alignment", "au")
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
|
||||
let src_pad_template = gst::PadTemplate::new(
|
||||
"src",
|
||||
gst::PadDirection::Src,
|
||||
gst::PadPresence::Always,
|
||||
&caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sink_pad_template = gst::PadTemplate::new(
|
||||
"sink",
|
||||
gst::PadDirection::Sink,
|
||||
gst::PadPresence::Always,
|
||||
&caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vec![src_pad_template, sink_pad_template]
|
||||
});
|
||||
|
||||
PAD_TEMPLATES.as_ref()
|
||||
}
|
||||
}
|
139
video/ocvr/src/ocvrhint/imp/correlation.rs
Normal file
139
video/ocvr/src/ocvrhint/imp/correlation.rs
Normal file
|
@ -0,0 +1,139 @@
|
|||
// Copyright (C) 2022 Jochen Henneberg <jh@henneberg-systemdesign.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
struct Vector<'a> {
|
||||
vec: &'a [i64],
|
||||
mean: i64,
|
||||
std: f64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Corr<'a> {
|
||||
x: Option<Vector<'a>>,
|
||||
y: Option<Vector<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Corr<'a> {
|
||||
pub fn set_x(&mut self, v: &'a [i64]) {
|
||||
self.x = Some(Self::set(v));
|
||||
}
|
||||
|
||||
pub fn set_y(&mut self, v: &'a [i64]) {
|
||||
self.y = Some(Self::set(v));
|
||||
}
|
||||
|
||||
pub fn corr(&self) -> Option<f64> {
|
||||
if self.x.is_none() || self.y.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut c = self.cov();
|
||||
|
||||
let x = self.x.as_ref().unwrap();
|
||||
let y = self.y.as_ref().unwrap();
|
||||
c /= x.std * y.std;
|
||||
|
||||
Some(c.clamp(-1.0, 1.0))
|
||||
}
|
||||
|
||||
pub fn corr_y(&mut self, y: &'a [i64]) -> Option<f64> {
|
||||
self.set_y(y);
|
||||
self.corr()
|
||||
}
|
||||
|
||||
fn set(v: &'a [i64]) -> Vector {
|
||||
let m = Self::mean(v);
|
||||
let s = Self::std(v, m);
|
||||
Vector {
|
||||
vec: v,
|
||||
mean: m,
|
||||
std: s,
|
||||
}
|
||||
}
|
||||
|
||||
fn cov(&self) -> f64 {
|
||||
let mut s: f64 = 0.0;
|
||||
let x = self.x.as_ref().unwrap();
|
||||
let y = self.y.as_ref().unwrap();
|
||||
|
||||
assert_eq!(x.vec.len(), y.vec.len());
|
||||
|
||||
for it in Iterator::zip(x.vec.iter(), y.vec.iter()) {
|
||||
let (xi, yi) = it;
|
||||
s += (xi - x.mean) as f64 * (yi - y.mean) as f64;
|
||||
}
|
||||
s / ((x.vec.len() - 1) as f64)
|
||||
}
|
||||
|
||||
fn mean(x: &[i64]) -> i64 {
|
||||
assert!(!x.is_empty());
|
||||
let s = x.iter().sum::<i64>();
|
||||
let n = x.len() as f64;
|
||||
|
||||
((s as f64) / n) as i64
|
||||
}
|
||||
|
||||
fn std(v: &[i64], v_m: i64) -> f64 {
|
||||
let mut s = v.iter().map(|vi| (vi - v_m).pow(2)).sum::<i64>();
|
||||
s /= v.len() as i64 - 1;
|
||||
(s as f64).sqrt()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn check_corr_equal() {
|
||||
let x: Vec<i64> = vec![10, 1, 10, 1, 10, 1];
|
||||
let y: Vec<i64> = vec![10, 1, 10, 1, 10, 1];
|
||||
let mut corr = Corr { x: None, y: None };
|
||||
|
||||
corr.set_x(&x);
|
||||
corr.set_y(&y);
|
||||
let c = corr.corr();
|
||||
assert!(c.is_some());
|
||||
assert!(1.0 == c.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_corr_inverse() {
|
||||
let x: Vec<i64> = vec![10, 1, 10, 1, 10, 1];
|
||||
let y: Vec<i64> = vec![1, 10, 1, 10, 1, 10];
|
||||
let mut corr = Corr { x: None, y: None };
|
||||
|
||||
corr.set_x(&x);
|
||||
corr.set_y(&y);
|
||||
let c = corr.corr();
|
||||
assert!(c.is_some());
|
||||
assert!(-1.0 == c.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_corr_scaled_equal() {
|
||||
let x: Vec<i64> = vec![10, 1, 10, 1, 10, 1];
|
||||
let y: Vec<i64> = vec![100, 10, 100, 10, 100, 10];
|
||||
let mut corr = Corr { x: None, y: None };
|
||||
|
||||
corr.set_x(&x);
|
||||
corr.set_y(&y);
|
||||
let c = corr.corr();
|
||||
assert!(c.is_some());
|
||||
assert!(1.0 == c.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_corr_similar() {
|
||||
let x: Vec<i64> = vec![10, 1, 10, 1, 10, 1];
|
||||
let y: Vec<i64> = vec![10, 2, 8, 3, 9, 5];
|
||||
let mut corr = Corr { x: None, y: None };
|
||||
|
||||
corr.set_x(&x);
|
||||
corr.set_y(&y);
|
||||
let c = corr.corr();
|
||||
assert!(c.is_some());
|
||||
assert!(0.9 < c.unwrap());
|
||||
}
|
||||
}
|
88
video/ocvr/src/ocvrhint/imp/rateprobe.rs
Normal file
88
video/ocvr/src/ocvrhint/imp/rateprobe.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
// Copyright (C) 2022 Jochen Henneberg <jh@henneberg-systemdesign.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RateProbe {
|
||||
probes: Vec<Vec<i64>>,
|
||||
}
|
||||
|
||||
impl RateProbe {
|
||||
const IFRAME_SIZE: i64 = 20;
|
||||
const IFRAME_LARGE_SIZE: i64 = 50;
|
||||
|
||||
pub fn new(gop_size: usize, vec: &[i64]) -> RateProbe {
|
||||
assert!(vec.len() > 1);
|
||||
|
||||
// we rotate the input vector to get all variants and
|
||||
// duplicate each with a leading I-frame
|
||||
let mut vs: Vec<Vec<i64>> = Vec::with_capacity(vec.len());
|
||||
|
||||
// and we copy the slice into a vector for permutation
|
||||
let mut rv = Vec::from(vec);
|
||||
|
||||
for _ in (0..vec.len()).step_by(2) {
|
||||
let mut v = rv
|
||||
.iter()
|
||||
.cycle()
|
||||
.take(gop_size)
|
||||
.copied()
|
||||
.collect::<VecDeque<_>>();
|
||||
|
||||
// option 1: first frame is an I-frame and thus large
|
||||
let l = v[0];
|
||||
v[0] = Self::IFRAME_SIZE;
|
||||
vs.push(Vec::from(v.clone()));
|
||||
v[0] = Self::IFRAME_LARGE_SIZE;
|
||||
vs.push(Vec::from(v.clone()));
|
||||
v[0] = l; // restore original vector
|
||||
|
||||
// option 2: put an I-frame in front of the first frame
|
||||
v.pop_back();
|
||||
v.push_front(Self::IFRAME_SIZE);
|
||||
vs.push(Vec::from(v.clone()));
|
||||
v[0] = Self::IFRAME_LARGE_SIZE;
|
||||
vs.push(Vec::from(v.clone()));
|
||||
|
||||
// rotate
|
||||
rv.rotate_right(2);
|
||||
}
|
||||
|
||||
RateProbe { probes: vs }
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &Vec<i64>> {
|
||||
self.probes.iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn get_and_iterate() {
|
||||
let gop = 30;
|
||||
let probe = RateProbe::new(gop, &[10, 1, 10, 1, 1]);
|
||||
assert!(probe.probes.len() == 12);
|
||||
for p in probe.iter() {
|
||||
assert!(p.len() == gop);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_and_check_vecs() {
|
||||
let probe = RateProbe::new(30, &[10, 1, 10, 1, 1]);
|
||||
for (i, p) in probe.iter().enumerate() {
|
||||
match i {
|
||||
0 => assert_eq!(&p[..=4], &[RateProbe::IFRAME_SIZE, 1, 10, 1, 1]),
|
||||
1 => assert_eq!(&p[..=4], &[RateProbe::IFRAME_LARGE_SIZE, 1, 10, 1, 1]),
|
||||
2 => assert_eq!(&p[..=4], &[RateProbe::IFRAME_SIZE, 10, 1, 10, 1]),
|
||||
3 => assert_eq!(&p[..=4], &[RateProbe::IFRAME_LARGE_SIZE, 10, 1, 10, 1]),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
video/ocvr/src/ocvrhint/mod.rs
Normal file
26
video/ocvr/src/ocvrhint/mod.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Copyright (C) 2022 Jochen Henneberg <jh@henneberg-systemdesign.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
|
||||
mod imp;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct OcvrHint(ObjectSubclass<imp::OcvrHint>) @extends gst::Element, gst::Object;
|
||||
}
|
||||
|
||||
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
#[cfg(feature = "doc")]
|
||||
imp::ContentRate::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
|
||||
#[cfg(feature = "doc")]
|
||||
imp::CaptureRate::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
|
||||
|
||||
gst::Element::register(
|
||||
Some(plugin),
|
||||
"ocvrhint",
|
||||
gst::Rank::None,
|
||||
OcvrHint::static_type(),
|
||||
)
|
||||
}
|
BIN
video/ocvr/tests/24-30-60_in_60.mkv
Normal file
BIN
video/ocvr/tests/24-30-60_in_60.mkv
Normal file
Binary file not shown.
BIN
video/ocvr/tests/25-30-50_in_50.mkv
Normal file
BIN
video/ocvr/tests/25-30-50_in_50.mkv
Normal file
Binary file not shown.
391
video/ocvr/tests/ocvr.rs
Normal file
391
video/ocvr/tests/ocvr.rs
Normal file
|
@ -0,0 +1,391 @@
|
|||
// Copyright (C) 2022 Jochen Henneberg <jh@henneberg-systemdesign.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// The test videos can be generated with the script
|
||||
// test-video-generator.sh.
|
||||
|
||||
use gst::prelude::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum RateMatch {
|
||||
None,
|
||||
// for 60hz capture rate
|
||||
Hz24,
|
||||
Hz24_30,
|
||||
Hz24_30_60,
|
||||
// for 50Hz capture rate
|
||||
Hz25,
|
||||
Hz25_30,
|
||||
Hz25_30_50,
|
||||
}
|
||||
|
||||
struct TestSetup<'a> {
|
||||
pub dups: Vec<usize>,
|
||||
pub content_rate: i32,
|
||||
pub capture_rate: i32,
|
||||
pub prop: &'a str,
|
||||
pub frames: usize,
|
||||
}
|
||||
|
||||
fn init() {
|
||||
use std::sync::Once;
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
INIT.call_once(|| {
|
||||
gst::init().unwrap();
|
||||
gstocvr::plugin_register_static().expect("ocvr test");
|
||||
});
|
||||
}
|
||||
|
||||
// fix rounding error or odd/even drop offset
|
||||
const FIX: i32 = -1;
|
||||
|
||||
#[test]
|
||||
fn ctrl_25_in_50() {
|
||||
const SYNC_IN_PERIOD: i32 = 5 + 1;
|
||||
const CAPTURE_RATE: i32 = 50;
|
||||
const CONTENT_RATE: i32 = CAPTURE_RATE / 2;
|
||||
run_ctrl_test(TestSetup {
|
||||
dups: vec![2, 2, 2],
|
||||
content_rate: CONTENT_RATE,
|
||||
capture_rate: CAPTURE_RATE,
|
||||
prop: "25Hz",
|
||||
frames: (SYNC_IN_PERIOD + (CAPTURE_RATE - SYNC_IN_PERIOD) / 2 + FIX) as usize,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_25_auto_in_50() {
|
||||
// 5 matches + 1 initial reference frame
|
||||
const SYNC_IN_PERIOD: i32 = 5 + 1;
|
||||
const CAPTURE_RATE: i32 = 50;
|
||||
const CONTENT_RATE: i32 = CAPTURE_RATE / 2;
|
||||
run_ctrl_test(TestSetup {
|
||||
dups: vec![2, 2, 2],
|
||||
content_rate: CONTENT_RATE,
|
||||
capture_rate: CAPTURE_RATE,
|
||||
prop: "Auto",
|
||||
frames: (SYNC_IN_PERIOD + (CAPTURE_RATE - SYNC_IN_PERIOD) / 2 + FIX) as usize,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_30_in_50() {
|
||||
// 5 matches + 1 initial reference frame
|
||||
const SYNC_IN_PERIOD: i32 = 5 + 1;
|
||||
const CAPTURE_RATE: i32 = 50;
|
||||
const CONTENT_RATE: i32 = 30;
|
||||
run_ctrl_test(TestSetup {
|
||||
dups: vec![2, 2, 1],
|
||||
content_rate: CONTENT_RATE,
|
||||
capture_rate: CAPTURE_RATE,
|
||||
prop: "30Hz",
|
||||
frames: (SYNC_IN_PERIOD + (CAPTURE_RATE - SYNC_IN_PERIOD) * 3 / 5) as usize,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_30_auto_in_50() {
|
||||
// first try 25Hz then 30Hz
|
||||
const SYNC_IN_PERIOD: i32 = 8 + 1 + 5 + 1;
|
||||
const CAPTURE_RATE: i32 = 50;
|
||||
const CONTENT_RATE: i32 = 30;
|
||||
run_ctrl_test(TestSetup {
|
||||
dups: vec![2, 2, 1],
|
||||
content_rate: CONTENT_RATE,
|
||||
capture_rate: CAPTURE_RATE,
|
||||
prop: "Auto",
|
||||
frames: (SYNC_IN_PERIOD + (CAPTURE_RATE - SYNC_IN_PERIOD) * 3 / 5) as usize,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_50_in_50() {
|
||||
const CAPTURE_RATE: i32 = 50;
|
||||
run_ctrl_test(TestSetup {
|
||||
dups: vec![1, 1, 1],
|
||||
content_rate: CAPTURE_RATE,
|
||||
capture_rate: CAPTURE_RATE,
|
||||
prop: "50Hz",
|
||||
frames: CAPTURE_RATE as usize,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_50_auto_in_50() {
|
||||
const CAPTURE_RATE: i32 = 50;
|
||||
run_ctrl_test(TestSetup {
|
||||
dups: vec![1, 1, 1],
|
||||
content_rate: CAPTURE_RATE,
|
||||
capture_rate: CAPTURE_RATE,
|
||||
prop: "Auto",
|
||||
frames: CAPTURE_RATE as usize,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_24_in_60() {
|
||||
// 2 matches + 1 initial reference frame
|
||||
const SYNC_IN_PERIOD: i32 = 2 + 1;
|
||||
const CAPTURE_RATE: i32 = 60;
|
||||
const CONTENT_RATE: i32 = 24;
|
||||
run_ctrl_test(TestSetup {
|
||||
dups: vec![3, 2],
|
||||
content_rate: CONTENT_RATE,
|
||||
capture_rate: CAPTURE_RATE,
|
||||
prop: "24Hz",
|
||||
frames: (SYNC_IN_PERIOD + (CAPTURE_RATE - SYNC_IN_PERIOD) * 2 / 5) as usize,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_24_auto_in_60() {
|
||||
// 2 matches + 1 initial reference frame
|
||||
const SYNC_IN_PERIOD: i32 = 2 + 1;
|
||||
const CAPTURE_RATE: i32 = 60;
|
||||
const CONTENT_RATE: i32 = 24;
|
||||
run_ctrl_test(TestSetup {
|
||||
dups: vec![3, 2],
|
||||
content_rate: CONTENT_RATE,
|
||||
capture_rate: CAPTURE_RATE,
|
||||
prop: "Auto",
|
||||
frames: (SYNC_IN_PERIOD + (CAPTURE_RATE - SYNC_IN_PERIOD) * 2 / 5) as usize,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_30_in_60() {
|
||||
// 5 matches + 1 initial reference frame
|
||||
const SYNC_IN_PERIOD: i32 = 5 + 1;
|
||||
const CAPTURE_RATE: i32 = 60;
|
||||
const CONTENT_RATE: i32 = CAPTURE_RATE / 2;
|
||||
run_ctrl_test(TestSetup {
|
||||
dups: vec![2, 2, 2],
|
||||
content_rate: CONTENT_RATE,
|
||||
capture_rate: CAPTURE_RATE,
|
||||
prop: "30Hz",
|
||||
frames: (SYNC_IN_PERIOD + (CAPTURE_RATE - SYNC_IN_PERIOD) / 2 + FIX) as usize,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_30_auto_in_60() {
|
||||
// first try 24Hz then 30Hz with offset 1
|
||||
const SYNC_IN_PERIOD: i32 = 8 + 1 + 5 + 1 + 1;
|
||||
const CAPTURE_RATE: i32 = 60;
|
||||
const CONTENT_RATE: i32 = CAPTURE_RATE / 2;
|
||||
run_ctrl_test(TestSetup {
|
||||
dups: vec![2, 2, 2],
|
||||
content_rate: CONTENT_RATE,
|
||||
capture_rate: CAPTURE_RATE,
|
||||
prop: "Auto",
|
||||
frames: (SYNC_IN_PERIOD + (CAPTURE_RATE - SYNC_IN_PERIOD) / 2 + FIX) as usize,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_60_in_60() {
|
||||
const CAPTURE_RATE: i32 = 60;
|
||||
run_ctrl_test(TestSetup {
|
||||
dups: vec![1, 1, 1],
|
||||
content_rate: CAPTURE_RATE,
|
||||
capture_rate: CAPTURE_RATE,
|
||||
prop: "60Hz",
|
||||
frames: CAPTURE_RATE as usize,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_60_auto_in_60() {
|
||||
const CAPTURE_RATE: i32 = 60;
|
||||
run_ctrl_test(TestSetup {
|
||||
dups: vec![1, 1, 1],
|
||||
content_rate: CAPTURE_RATE,
|
||||
capture_rate: CAPTURE_RATE,
|
||||
prop: "Auto",
|
||||
frames: CAPTURE_RATE as usize,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hint_60() {
|
||||
run_hint_test("24-30-60_in_60", RateMatch::Hz24_30_60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hint_50() {
|
||||
run_hint_test("25-30-50_in_50", RateMatch::Hz25_30_50);
|
||||
}
|
||||
|
||||
fn run_ctrl_test(setup: TestSetup) {
|
||||
init();
|
||||
|
||||
// we need motion=sweep otherwise we will get duplicate frames
|
||||
// when ball debounces from wall which causes false positives
|
||||
let bin = gst::parse_bin_from_description(
|
||||
&format!("videotestsrc pattern=ball motion=sweep num-buffers={:?} ! capsfilter name=filter caps=\"video/x-raw,width=(int)800,height=(int)480,format=(string)NV12,framerate=(fraction){:?}/1,interlace-mode=(string)progressive\"", setup.capture_rate, setup.capture_rate), false).unwrap();
|
||||
|
||||
let srcpad = bin.by_name("filter").unwrap().static_pad("src").unwrap();
|
||||
let _ = bin.add_pad(&gst::GhostPad::with_target(&srcpad).unwrap());
|
||||
let mut g = gst_check::Harness::with_element(&bin, None, Some("src"));
|
||||
g.play();
|
||||
|
||||
// set our expected output framerate
|
||||
let mut h = gst_check::Harness::new("ocvrctrl");
|
||||
{
|
||||
let ctrl = h.element().unwrap();
|
||||
ctrl.set_property_from_str("content-rate", setup.prop);
|
||||
ctrl.set_property_from_str("tolerance", "Lazy");
|
||||
ctrl.set_property_from_str("method", "Accurate");
|
||||
}
|
||||
|
||||
h.play();
|
||||
let video_info = gst_video::VideoInfo::builder(gst_video::VideoFormat::Nv12, 800, 480)
|
||||
.fps((setup.capture_rate, 1))
|
||||
.build()
|
||||
.unwrap();
|
||||
h.set_src_caps(video_info.to_caps().unwrap());
|
||||
|
||||
for i in 0..setup.content_rate {
|
||||
let buf = g.pull().unwrap();
|
||||
for _ in 0..setup.dups[i as usize % setup.dups.len()] {
|
||||
h.push(buf.copy()).expect("failed to read buffer");
|
||||
}
|
||||
}
|
||||
|
||||
let mut frames_found = 0;
|
||||
while h.try_pull().is_some() {
|
||||
frames_found += 1;
|
||||
}
|
||||
println!(">= {} out of {} frames found", frames_found, setup.frames);
|
||||
assert!(frames_found == setup.frames);
|
||||
|
||||
let mut target_rate_found = false;
|
||||
while !target_rate_found {
|
||||
match h.try_pull_event() {
|
||||
Some(e) => {
|
||||
if let gst::EventView::Caps(e) = e.view() {
|
||||
let c = e.caps();
|
||||
let r = c
|
||||
.structure(0)
|
||||
.unwrap()
|
||||
.get::<gst::Fraction>("framerate")
|
||||
.unwrap();
|
||||
// if we find our expected output framerate we are done
|
||||
let numer = *r.round().numer();
|
||||
assert!(*r.round().denom() == 1i32);
|
||||
if numer == setup.content_rate {
|
||||
target_rate_found = true;
|
||||
break;
|
||||
}
|
||||
assert!(numer == setup.capture_rate);
|
||||
}
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
h.push_event(gst::event::Eos::new());
|
||||
|
||||
assert!(target_rate_found);
|
||||
}
|
||||
|
||||
fn run_hint_test(f: &str, rate_match: RateMatch) {
|
||||
init();
|
||||
|
||||
// the test file shall contain video material with changing
|
||||
// content framerates, for 60Hz video we should have 24Hz, 30Hz
|
||||
// and 60Hz and for 50Hz video we should have 25Hz, 30Hz and 50Hz
|
||||
let input_path = {
|
||||
let mut r = PathBuf::new();
|
||||
r.push(env!("CARGO_MANIFEST_DIR"));
|
||||
r.push("tests");
|
||||
r.push(f);
|
||||
r.set_extension("mkv");
|
||||
r
|
||||
};
|
||||
let pipeline = gst::parse_launch(&format!("filesrc location={:?} ! matroskademux ! h265parse ! ocvrhint name=h window-size=1 ! fakesink", input_path)).unwrap();
|
||||
|
||||
// add a PadProbe to monitor the custom upstream events with the
|
||||
// detected framerate
|
||||
let h = pipeline
|
||||
.downcast_ref::<gst::Bin>()
|
||||
.unwrap()
|
||||
.by_name("h")
|
||||
.unwrap();
|
||||
let p = h.static_pad("sink").unwrap();
|
||||
|
||||
// catch the rate events and let us know if all rates have been
|
||||
// found once the pipeline is done
|
||||
let rm = Arc::new(Mutex::new(RateMatch::None));
|
||||
let data = Arc::clone(&rm);
|
||||
p.add_probe(gst::PadProbeType::EVENT_UPSTREAM, move |_p, info| {
|
||||
let d = info.data.as_ref().unwrap();
|
||||
match d {
|
||||
gst::PadProbeData::Event(e) => {
|
||||
if let gst::EventView::CustomUpstream(ce) = e.view() {
|
||||
let mut rm = data.lock().unwrap();
|
||||
match ce.structure().unwrap().get::<u32>("rate").unwrap() {
|
||||
24 => {
|
||||
if *rm == RateMatch::None {
|
||||
*rm = RateMatch::Hz24;
|
||||
}
|
||||
gst::PadProbeReturn::Drop
|
||||
}
|
||||
25 => {
|
||||
if *rm == RateMatch::None {
|
||||
*rm = RateMatch::Hz25;
|
||||
}
|
||||
gst::PadProbeReturn::Drop
|
||||
}
|
||||
30 => {
|
||||
if *rm == RateMatch::Hz24 {
|
||||
*rm = RateMatch::Hz24_30;
|
||||
} else if *rm == RateMatch::Hz25 {
|
||||
*rm = RateMatch::Hz25_30;
|
||||
}
|
||||
gst::PadProbeReturn::Drop
|
||||
}
|
||||
50 => {
|
||||
if *rm == RateMatch::Hz25_30 {
|
||||
*rm = RateMatch::Hz25_30_50;
|
||||
}
|
||||
gst::PadProbeReturn::Remove
|
||||
}
|
||||
60 => {
|
||||
if *rm == RateMatch::Hz24_30 {
|
||||
*rm = RateMatch::Hz24_30_60;
|
||||
}
|
||||
gst::PadProbeReturn::Remove
|
||||
}
|
||||
_ => gst::PadProbeReturn::Ok,
|
||||
}
|
||||
} else {
|
||||
gst::PadProbeReturn::Ok
|
||||
}
|
||||
}
|
||||
_ => gst::PadProbeReturn::Ok,
|
||||
}
|
||||
});
|
||||
|
||||
let bus = pipeline.bus().unwrap();
|
||||
pipeline
|
||||
.set_state(gst::State::Playing)
|
||||
.expect("Unable to set the pipeline to the `Playing` state");
|
||||
|
||||
for msg in bus.iter_timed(gst::ClockTime::NONE) {
|
||||
if let gst::MessageView::Eos(..) = msg.view() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pipeline
|
||||
.set_state(gst::State::Null)
|
||||
.expect("Unable to set the pipeline to the `Null` state");
|
||||
|
||||
assert!(*rm.lock().unwrap() == rate_match);
|
||||
}
|
122
video/ocvr/tests/test-video-generator.sh
Executable file
122
video/ocvr/tests/test-video-generator.sh
Executable file
|
@ -0,0 +1,122 @@
|
|||
#!/bin/bash
|
||||
|
||||
# give 50 or 60 as first command line arg
|
||||
rate=$1
|
||||
|
||||
#helper
|
||||
copy_file() {
|
||||
orig=$(printf "%05d" $1)
|
||||
dupl=$(printf "%05d" $2)
|
||||
cp ./orig_frames/$orig ./video_frames/$dupl
|
||||
}
|
||||
|
||||
# number of frames per content rate
|
||||
FRAMES=60
|
||||
TOTAL_FRAMES=$(( 3 * $FRAMES ))
|
||||
|
||||
# copied file index
|
||||
e=0
|
||||
|
||||
do_24_in_60() {
|
||||
for (( i = 0; i < $FRAMES; ++i )) ; do
|
||||
if (( $i % 2 == 0)); then
|
||||
copy_file $i $e
|
||||
(( ++e ))
|
||||
copy_file $i $e
|
||||
(( ++e ))
|
||||
else
|
||||
copy_file $i $e
|
||||
(( ++e ))
|
||||
copy_file $i $e
|
||||
(( ++e ))
|
||||
copy_file $i $e
|
||||
(( ++e ))
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
do_30_in_60() {
|
||||
for (( i = $FRAMES; i < 2 * $FRAMES; ++i )) ; do
|
||||
copy_file $i $e
|
||||
(( ++e ))
|
||||
copy_file $i $e
|
||||
(( ++e ))
|
||||
done
|
||||
}
|
||||
|
||||
do_60_in_60() {
|
||||
for (( i = 2 * $FRAMES; i < 3 * $FRAMES; ++i )) ; do
|
||||
copy_file $i $e
|
||||
(( ++e ))
|
||||
done
|
||||
}
|
||||
|
||||
do_25_in_50() {
|
||||
for (( i = 0; i < $FRAMES; ++i )) ; do
|
||||
copy_file $i $e
|
||||
(( ++e ))
|
||||
copy_file $i $e
|
||||
(( ++e ))
|
||||
done
|
||||
}
|
||||
|
||||
do_30_in_50() {
|
||||
for (( i = $FRAMES; i < 2 * $FRAMES; ++i )) ; do
|
||||
if (( $i % 3 == 0)); then
|
||||
copy_file $i $e
|
||||
(( ++e ))
|
||||
copy_file $i $e
|
||||
(( ++e ))
|
||||
elif (( $i % 3 == 1)); then
|
||||
copy_file $i $e
|
||||
(( ++e ))
|
||||
copy_file $i $e
|
||||
(( ++e ))
|
||||
else
|
||||
copy_file $i $e
|
||||
(( ++e ))
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
do_50_in_50() {
|
||||
for (( i = 2 * $FRAMES; i < 3 * $FRAMES; ++i )) ; do
|
||||
copy_file $i $e
|
||||
(( ++e ))
|
||||
done
|
||||
}
|
||||
|
||||
# prepare
|
||||
mkdir ./orig_frames
|
||||
mkdir ./video_frames
|
||||
|
||||
# generate test frames
|
||||
gst-launch-1.0 -v videotestsrc pattern=ball motion=sweep num-buffers=$TOTAL_FRAMES ! \
|
||||
video/x-raw,width=800,height=480,format=NV12,framerate=$rate/1 ! \
|
||||
multifilesink location="orig_frames/%05d"
|
||||
|
||||
# generate video frames with duplicates
|
||||
video_file=""
|
||||
case $rate in
|
||||
50)
|
||||
video_file="25-30-50_in_50.mkv"
|
||||
do_25_in_50
|
||||
do_30_in_50
|
||||
do_50_in_50
|
||||
;;
|
||||
60)
|
||||
video_file="24-30-60_in_60.mkv"
|
||||
do_24_in_60
|
||||
do_30_in_60
|
||||
do_60_in_60
|
||||
;;
|
||||
esac
|
||||
|
||||
# generate test video
|
||||
gst-launch-1.0 -v multifilesrc do-timestamp=true location="video_frames/%05d" \
|
||||
caps="video/x-raw,width=(int)800,height=(int)480,format=(string)NV12,framerate=(fraction)$rate/1,interlace-mode=(string)progressive" ! \
|
||||
vaapih265enc quality-level=7 ! h265parse config-interval=-1 \
|
||||
matroskamux ! filesink location=$video_file
|
||||
|
||||
# cleanup
|
||||
rm --force --recursive ./orig_frames ./video_frames
|
Loading…
Reference in a new issue