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:
Jochen Henneberg 2024-03-07 15:47:04 +00:00
commit 7c6537b89e
21 changed files with 3296 additions and 0 deletions

View file

@ -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]

View file

@ -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.

View file

@ -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": {

View file

@ -194,6 +194,7 @@ plugins = {
},
# gtk4 is added below
'hsv': {'library': 'libgsthsv'},
'ocvr': {'library': 'libgstocvr'},
'png': {
'library': 'libgstrspng',
'examples': ['pngenc'],

View file

@ -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
View 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
View 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
View 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")
);

View 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()
}
}

View 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)
}
}
}
}

View 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)
}
}

View 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!(),
}
}
}

View 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(),
)
}

View 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()
}
}

View 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());
}
}

View 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]),
_ => (),
}
}
}
}

View 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(),
)
}

Binary file not shown.

Binary file not shown.

391
video/ocvr/tests/ocvr.rs Normal file
View 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);
}

View 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