Add property for selecting between different timestamping modes

This commit is contained in:
Sebastian Dröge 2019-07-16 17:43:03 +03:00
parent a4890d3295
commit 8b00e80316
4 changed files with 321 additions and 59 deletions

View file

@ -8,6 +8,7 @@ description = "NewTek NDI Plugin"
[dependencies]
glib = { version = "0.8.0", features = ["subclassing"] }
gobject-sys = "0.9"
gstreamer = { version = "0.14.3", features = ["subclassing"] }
gstreamer-base = { version = "0.14.0", features = ["subclassing"] }
gstreamer-audio = "0.14.0"

View file

@ -24,6 +24,14 @@ use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy)]
#[repr(u32)]
pub enum TimestampMode {
ReceiveTime = 0,
Timecode = 1,
Timestamp = 2,
}
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
if !ndi::initialize() {
return Err(glib_bool_error!("Cannot initialize NDI"));
@ -199,6 +207,98 @@ fn stop_ndi(cat: gst::DebugCategory, element: &gst_base::BaseSrc, id: usize) ->
true
}
impl glib::translate::ToGlib for TimestampMode {
type GlibType = i32;
fn to_glib(&self) -> i32 {
*self as i32
}
}
impl glib::translate::FromGlib<i32> for TimestampMode {
fn from_glib(value: i32) -> Self {
match value {
0 => TimestampMode::ReceiveTime,
1 => TimestampMode::Timecode,
2 => TimestampMode::Timestamp,
_ => unreachable!(),
}
}
}
impl StaticType for TimestampMode {
fn static_type() -> glib::Type {
timestamp_mode_get_type()
}
}
impl<'a> glib::value::FromValueOptional<'a> for TimestampMode {
unsafe fn from_value_optional(value: &glib::Value) -> Option<Self> {
Some(glib::value::FromValue::from_value(value))
}
}
impl<'a> glib::value::FromValue<'a> for TimestampMode {
unsafe fn from_value(value: &glib::Value) -> Self {
use glib::translate::ToGlibPtr;
glib::translate::from_glib(gobject_sys::g_value_get_enum(value.to_glib_none().0))
}
}
impl glib::value::SetValue for TimestampMode {
unsafe fn set_value(value: &mut glib::Value, this: &Self) {
use glib::translate::{ToGlib, ToGlibPtrMut};
gobject_sys::g_value_set_enum(value.to_glib_none_mut().0, this.to_glib())
}
}
fn timestamp_mode_get_type() -> glib::Type {
use std::sync::Once;
static ONCE: Once = Once::new();
static mut TYPE: glib::Type = glib::Type::Invalid;
ONCE.call_once(|| {
use std::ffi;
use std::ptr;
static mut VALUES: [gobject_sys::GEnumValue; 4] = [
gobject_sys::GEnumValue {
value: TimestampMode::ReceiveTime as i32,
value_name: b"Receive Time\0" as *const _ as *const _,
value_nick: b"receive-time\0" as *const _ as *const _,
},
gobject_sys::GEnumValue {
value: TimestampMode::Timecode as i32,
value_name: b"NDI Timecode\0" as *const _ as *const _,
value_nick: b"timecode\0" as *const _ as *const _,
},
gobject_sys::GEnumValue {
value: TimestampMode::Timestamp as i32,
value_name: b"NDI Timestamp\0" as *const _ as *const _,
value_nick: b"timestamp\0" as *const _ as *const _,
},
gobject_sys::GEnumValue {
value: 0,
value_name: ptr::null(),
value_nick: ptr::null(),
},
];
let name = ffi::CString::new("GstNdiTimestampMode").unwrap();
unsafe {
let type_ = gobject_sys::g_enum_register_static(name.as_ptr(), VALUES.as_ptr());
TYPE = glib::translate::from_glib(type_);
}
});
unsafe {
assert_ne!(TYPE, glib::Type::Invalid);
TYPE
}
}
gst_plugin_define!(
ndi,
env!("CARGO_PKG_DESCRIPTION"),

View file

@ -17,6 +17,7 @@ use ndi::*;
use ndisys;
use stop_ndi;
use TimestampMode;
use HASHMAP_RECEIVERS;
#[cfg(feature = "reference-timestamps")]
use TIMECODE_CAPS;
@ -30,6 +31,7 @@ struct Settings {
stream_name: String,
ip: String,
loss_threshold: u32,
timestamp_mode: TimestampMode,
}
impl Default for Settings {
@ -38,32 +40,33 @@ impl Default for Settings {
stream_name: String::from("Fixed ndi stream name"),
ip: String::from(""),
loss_threshold: 5,
timestamp_mode: TimestampMode::ReceiveTime,
}
}
}
static PROPERTIES: [subclass::Property; 3] = [
subclass::Property("stream-name", |_| {
static PROPERTIES: [subclass::Property; 4] = [
subclass::Property("stream-name", |name| {
glib::ParamSpec::string(
"stream-name",
"Sream Name",
name,
"Stream Name",
"Name of the streaming device",
None,
glib::ParamFlags::READWRITE,
)
}),
subclass::Property("ip", |_| {
subclass::Property("ip", |name| {
glib::ParamSpec::string(
"ip",
name,
"Stream IP",
"IP of the streaming device. Ex: 127.0.0.1:5961",
None,
glib::ParamFlags::READWRITE,
)
}),
subclass::Property("loss-threshold", |_| {
subclass::Property("loss-threshold", |name| {
glib::ParamSpec::uint(
"loss-threshold",
name,
"Loss threshold",
"Loss threshold",
0,
@ -72,11 +75,22 @@ static PROPERTIES: [subclass::Property; 3] = [
glib::ParamFlags::READWRITE,
)
}),
subclass::Property("timestamp-mode", |name| {
glib::ParamSpec::enum_(
name,
"Timestamp Mode",
"Timestamp information to use for outgoing PTS",
TimestampMode::static_type(),
TimestampMode::ReceiveTime as i32,
glib::ParamFlags::READWRITE,
)
}),
];
struct State {
info: Option<gst_audio::AudioInfo>,
id_receiver: Option<usize>,
current_latency: gst::ClockTime,
}
impl Default for State {
@ -84,6 +98,7 @@ impl Default for State {
State {
info: None,
id_receiver: None,
current_latency: gst::CLOCK_TIME_NONE,
}
}
}
@ -177,7 +192,6 @@ impl ObjectImpl for NdiAudioSrc {
stream_name
);
settings.stream_name = stream_name;
drop(settings);
}
subclass::Property("ip", ..) => {
let mut settings = self.settings.lock().unwrap();
@ -190,7 +204,6 @@ impl ObjectImpl for NdiAudioSrc {
ip
);
settings.ip = ip;
drop(settings);
}
subclass::Property("loss-threshold", ..) => {
let mut settings = self.settings.lock().unwrap();
@ -203,7 +216,22 @@ impl ObjectImpl for NdiAudioSrc {
loss_threshold
);
settings.loss_threshold = loss_threshold;
drop(settings);
}
subclass::Property("timestamp-mode", ..) => {
let mut settings = self.settings.lock().unwrap();
let timestamp_mode = value.get().unwrap();
gst_debug!(
self.cat,
obj: basesrc,
"Changing timestamp mode from {:?} to {:?}",
settings.timestamp_mode,
timestamp_mode
);
if settings.timestamp_mode != timestamp_mode {
let _ = basesrc
.post_message(&gst::Message::new_latency().src(Some(basesrc)).build());
}
settings.timestamp_mode = timestamp_mode;
}
_ => unimplemented!(),
}
@ -225,6 +253,10 @@ impl ObjectImpl for NdiAudioSrc {
let settings = self.settings.lock().unwrap();
Ok(settings.loss_threshold.to_value())
}
subclass::Property("timestamp-mode", ..) => {
let settings = self.settings.lock().unwrap();
Ok(settings.timestamp_mode.to_value())
}
_ => unimplemented!(),
}
}
@ -274,6 +306,24 @@ impl BaseSrcImpl for NdiAudioSrc {
q.add_scheduling_modes(&[gst::PadMode::Push]);
true
}
QueryView::Latency(ref mut q) => {
let state = self.state.lock().unwrap();
let settings = self.settings.lock().unwrap();
if state.current_latency.is_some() {
let latency = if settings.timestamp_mode == TimestampMode::Timestamp {
state.current_latency
} else {
0.into()
};
gst_debug!(self.cat, obj: element, "Returning latency {}", latency);
q.set(true, latency, gst::CLOCK_TIME_NONE);
true
} else {
false
}
}
_ => BaseSrcImplExt::parent_query(self, element, query),
}
}
@ -346,23 +396,52 @@ impl BaseSrcImpl for NdiAudioSrc {
// will want to work with the timestamp given by the NDI SDK if available
let now = clock.get_time();
let base_time = element.get_base_time();
let pts = now - base_time;
let receive_time = now - base_time;
let real_time_now = gst::ClockTime::from(glib::get_real_time() as u64 * 1000);
let timestamp = if audio_frame.timestamp() == ndisys::NDIlib_recv_timestamp_undefined {
gst::CLOCK_TIME_NONE
} else {
gst::ClockTime::from(audio_frame.timestamp() as u64 * 100)
};
let timecode = gst::ClockTime::from(audio_frame.timecode() as u64 * 100);
gst_log!(
self.cat,
obj: element,
"NDI audio frame received: {:?} with timecode {} and timestamp {}",
"NDI audio frame received: {:?} with timecode {} and timestamp {}, receive time {}, local time now {}",
audio_frame,
if audio_frame.timecode() == ndisys::NDIlib_send_timecode_synthesize {
gst::CLOCK_TIME_NONE
timecode,
timestamp,
receive_time,
real_time_now,
);
let pts = match settings.timestamp_mode {
TimestampMode::ReceiveTime => receive_time,
TimestampMode::Timecode => timecode,
TimestampMode::Timestamp if timestamp.is_none() => receive_time,
TimestampMode::Timestamp => {
// Timestamps are relative to the UNIX epoch
if real_time_now > timestamp {
let diff = real_time_now - timestamp;
if diff > receive_time {
0.into()
} else {
gst::ClockTime::from(audio_frame.timecode() as u64 * 100)
},
if audio_frame.timestamp() == ndisys::NDIlib_recv_timestamp_undefined {
gst::CLOCK_TIME_NONE
receive_time - diff
}
} else {
gst::ClockTime::from(audio_frame.timestamp() as u64 * 100)
},
let diff = timestamp - real_time_now;
receive_time + diff
}
}
};
gst_log!(
self.cat,
obj: element,
"Calculated pts for audio frame: {:?}",
pts
);
let info = gst_audio::AudioInfo::new(
@ -376,18 +455,20 @@ impl BaseSrcImpl for NdiAudioSrc {
if state.info.as_ref() != Some(&info) {
let caps = info.to_caps().unwrap();
state.info = Some(info);
state.current_latency = gst::SECOND
.mul_div_ceil(
audio_frame.no_samples() as u64,
audio_frame.sample_rate() as u64,
)
.unwrap_or(gst::CLOCK_TIME_NONE);
gst_debug!(self.cat, obj: element, "Configuring for caps {}", caps);
element
.set_caps(&caps)
.map_err(|_| gst::FlowError::NotNegotiated)?;
}
gst_log!(
self.cat,
obj: element,
"Calculated pts for audio frame: {:?}",
pts
);
let _ = element.post_message(&gst::Message::new_latency().src(Some(element)).build());
}
// We multiply by 2 because is the size in bytes of an i16 variable
let buff_size = (audio_frame.no_samples() * 2 * audio_frame.no_channels()) as usize;

View file

@ -20,6 +20,7 @@ use ndisys;
use connect_ndi;
use stop_ndi;
use TimestampMode;
use HASHMAP_RECEIVERS;
#[cfg(feature = "reference-timestamps")]
use TIMECODE_CAPS;
@ -31,6 +32,7 @@ struct Settings {
stream_name: String,
ip: String,
loss_threshold: u32,
timestamp_mode: TimestampMode,
}
impl Default for Settings {
@ -39,32 +41,33 @@ impl Default for Settings {
stream_name: String::from("Fixed ndi stream name"),
ip: String::from(""),
loss_threshold: 5,
timestamp_mode: TimestampMode::ReceiveTime,
}
}
}
static PROPERTIES: [subclass::Property; 3] = [
subclass::Property("stream-name", |_| {
static PROPERTIES: [subclass::Property; 4] = [
subclass::Property("stream-name", |name| {
glib::ParamSpec::string(
"stream-name",
name,
"Stream Name",
"Name of the streaming device",
None,
glib::ParamFlags::READWRITE,
)
}),
subclass::Property("ip", |_| {
subclass::Property("ip", |name| {
glib::ParamSpec::string(
"ip",
name,
"Stream IP",
"IP of the streaming device. Ex: 127.0.0.1:5961",
None,
glib::ParamFlags::READWRITE,
)
}),
subclass::Property("loss-threshold", |_| {
subclass::Property("loss-threshold", |name| {
glib::ParamSpec::uint(
"loss-threshold",
name,
"Loss threshold",
"Loss threshold",
0,
@ -73,11 +76,22 @@ static PROPERTIES: [subclass::Property; 3] = [
glib::ParamFlags::READWRITE,
)
}),
subclass::Property("timestamp-mode", |name| {
glib::ParamSpec::enum_(
name,
"Timestamp Mode",
"Timestamp information to use for outgoing PTS",
TimestampMode::static_type(),
TimestampMode::ReceiveTime as i32,
glib::ParamFlags::READWRITE,
)
}),
];
struct State {
info: Option<gst_video::VideoInfo>,
id_receiver: Option<usize>,
current_latency: gst::ClockTime,
}
impl Default for State {
@ -85,6 +99,7 @@ impl Default for State {
State {
info: None,
id_receiver: None,
current_latency: gst::CLOCK_TIME_NONE,
}
}
}
@ -212,7 +227,6 @@ impl ObjectImpl for NdiVideoSrc {
stream_name
);
settings.stream_name = stream_name;
drop(settings);
}
subclass::Property("ip", ..) => {
let mut settings = self.settings.lock().unwrap();
@ -225,7 +239,6 @@ impl ObjectImpl for NdiVideoSrc {
ip
);
settings.ip = ip;
drop(settings);
}
subclass::Property("loss-threshold", ..) => {
let mut settings = self.settings.lock().unwrap();
@ -238,7 +251,22 @@ impl ObjectImpl for NdiVideoSrc {
loss_threshold
);
settings.loss_threshold = loss_threshold;
drop(settings);
}
subclass::Property("timestamp-mode", ..) => {
let mut settings = self.settings.lock().unwrap();
let timestamp_mode = value.get().unwrap();
gst_debug!(
self.cat,
obj: basesrc,
"Changing timestamp mode from {:?} to {:?}",
settings.timestamp_mode,
timestamp_mode
);
if settings.timestamp_mode != timestamp_mode {
let _ = basesrc
.post_message(&gst::Message::new_latency().src(Some(basesrc)).build());
}
settings.timestamp_mode = timestamp_mode;
}
_ => unimplemented!(),
}
@ -260,6 +288,10 @@ impl ObjectImpl for NdiVideoSrc {
let settings = self.settings.lock().unwrap();
Ok(settings.loss_threshold.to_value())
}
subclass::Property("timestamp-mode", ..) => {
let settings = self.settings.lock().unwrap();
Ok(settings.timestamp_mode.to_value())
}
_ => unimplemented!(),
}
}
@ -304,6 +336,24 @@ impl BaseSrcImpl for NdiVideoSrc {
q.add_scheduling_modes(&[gst::PadMode::Push]);
true
}
QueryView::Latency(ref mut q) => {
let state = self.state.lock().unwrap();
let settings = self.settings.lock().unwrap();
if state.current_latency.is_some() {
let latency = if settings.timestamp_mode == TimestampMode::Timestamp {
state.current_latency
} else {
0.into()
};
gst_debug!(self.cat, obj: element, "Returning latency {}", latency);
q.set(true, latency, gst::CLOCK_TIME_NONE);
true
} else {
false
}
}
_ => BaseSrcImplExt::parent_query(self, element, query),
}
}
@ -380,23 +430,52 @@ impl BaseSrcImpl for NdiVideoSrc {
// will want to work with the timestamp given by the NDI SDK if available
let now = clock.get_time();
let base_time = element.get_base_time();
let pts = now - base_time;
let receive_time = now - base_time;
let real_time_now = gst::ClockTime::from(glib::get_real_time() as u64 * 1000);
let timestamp = if video_frame.timestamp() == ndisys::NDIlib_recv_timestamp_undefined {
gst::CLOCK_TIME_NONE
} else {
gst::ClockTime::from(video_frame.timestamp() as u64 * 100)
};
let timecode = gst::ClockTime::from(video_frame.timecode() as u64 * 100);
gst_log!(
self.cat,
obj: element,
"NDI video frame received: {:?} with timecode {} and timestamp {}",
"NDI video frame received: {:?} with timecode {} and timestamp {}, receive time {}, local time now {}",
video_frame,
if video_frame.timecode() == ndisys::NDIlib_send_timecode_synthesize {
gst::CLOCK_TIME_NONE
timecode,
timestamp,
receive_time,
real_time_now,
);
let pts = match settings.timestamp_mode {
TimestampMode::ReceiveTime => receive_time,
TimestampMode::Timecode => timecode,
TimestampMode::Timestamp if timestamp.is_none() => receive_time,
TimestampMode::Timestamp => {
// Timestamps are relative to the UNIX epoch
if real_time_now > timestamp {
let diff = real_time_now - timestamp;
if diff > receive_time {
0.into()
} else {
gst::ClockTime::from(video_frame.timecode() as u64 * 100)
},
if video_frame.timestamp() == ndisys::NDIlib_recv_timestamp_undefined {
gst::CLOCK_TIME_NONE
receive_time - diff
}
} else {
gst::ClockTime::from(video_frame.timestamp() as u64 * 100)
},
let diff = timestamp - real_time_now;
receive_time + diff
}
}
};
gst_log!(
self.cat,
obj: element,
"Calculated pts for video frame: {:?}",
pts
);
// YV12 and I420 are swapped in the NDI SDK compared to GStreamer
@ -476,18 +555,19 @@ impl BaseSrcImpl for NdiVideoSrc {
if state.info.as_ref() != Some(&info) {
let caps = info.to_caps().unwrap();
state.info = Some(info);
state.current_latency = gst::SECOND
.mul_div_ceil(
video_frame.frame_rate().1 as u64,
video_frame.frame_rate().0 as u64,
)
.unwrap_or(gst::CLOCK_TIME_NONE);
gst_debug!(self.cat, obj: element, "Configuring for caps {}", caps);
element
.set_caps(&caps)
.map_err(|_| gst::FlowError::NotNegotiated)?;
}
gst_log!(
self.cat,
obj: element,
"Calculated pts for video frame: {:?}",
pts
);
let _ = element.post_message(&gst::Message::new_latency().src(Some(element)).build());
}
let mut buffer = gst::Buffer::with_size(state.info.as_ref().unwrap().size()).unwrap();
{