1use std::{
4 ffi::{CStr, CString},
5 fmt, ptr,
6 sync::Mutex,
7 time::Duration,
8};
9
10use crate::{
11 finder::Source,
12 frames::{
13 AudioFrame, LineStrideOrSize, MetadataFrame, PixelFormat, ScanType, ValidatedFrameMetadata,
14 ValidatedVideoLayout, VideoFrame,
15 },
16 ndi_lib::*,
17 receiver::Tally,
18 to_ms_checked, Error, Result, NDI,
19};
20
21#[cfg(not(target_has_atomic = "ptr"))]
22compile_error!(
23 "This crate requires atomic pointer support. Please use a target with atomics enabled."
24);
25
26#[cfg(target_os = "windows")]
27static FLUSH_MUTEX: Mutex<()> = Mutex::new(());
28
29fn async_flush_frame_ptr() -> *const NDIlib_video_frame_v2_t {
36 ptr::null()
37}
38
39fn flush_null_frame(instance: NDIlib_send_instance_t) {
40 #[cfg(target_os = "windows")]
41 {
42 let _lock = FLUSH_MUTEX
43 .lock()
44 .unwrap_or_else(|poisoned| poisoned.into_inner());
45 unsafe {
46 NDIlib_send_send_video_async_v2(instance, async_flush_frame_ptr());
47 }
48 }
49
50 #[cfg(not(target_os = "windows"))]
51 unsafe {
52 NDIlib_send_send_video_async_v2(instance, async_flush_frame_ptr());
53 }
54}
55
56struct Inner {
58 instance: NDIlib_send_instance_t,
59 _name: CString,
60 _groups: Option<CString>,
61 video_callback: Option<AsyncCallback>,
62 video: Mutex<VideoState>,
64}
65
66#[derive(Debug)]
67pub struct Sender {
68 inner: Inner,
69 _ndi: NDI,
70}
71
72type AsyncCallback = Box<dyn Fn(usize) + Send + Sync>;
73
74#[derive(Default)]
75struct VideoState {
76 pending: Option<VideoFrame>,
77 in_flight: bool,
78}
79
80impl VideoState {
81 fn completed(&mut self) -> Option<usize> {
84 if std::mem::take(&mut self.in_flight) {
85 self.pending.as_ref().map(|frame| frame.data().len())
86 } else {
87 None
88 }
89 }
90}
91
92impl Inner {
93 fn notify(&self, len: Option<usize>) {
94 if let (Some(len), Some(callback)) = (len, self.video_callback.as_ref()) {
95 callback(len);
96 }
97 }
98
99 fn complete(&self) {
100 let len = {
101 let mut video = self.video.lock().unwrap_or_else(|e| e.into_inner());
102 if video.in_flight {
103 flush_null_frame(self.instance);
104 }
105 video.completed()
106 };
107 self.notify(len);
108 }
109}
110
111impl fmt::Debug for Inner {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 f.debug_struct("Inner")
114 .field("instance", &self.instance)
115 .field("video_callback_set", &self.video_callback.is_some())
116 .finish()
117 }
118}
119
120unsafe impl Send for Inner {}
123unsafe impl Sync for Inner {}
124
125pub struct BorrowedVideoFrame<'buf> {
134 layout: ValidatedVideoLayout,
135 frame_rate_n: i32,
136 frame_rate_d: i32,
137 picture_aspect_ratio: f32,
138 scan_type: ScanType,
139 timecode: i64,
140 data: &'buf [u8],
141 metadata: Option<&'buf CStr>,
142 metadata_layout: ValidatedFrameMetadata,
143 timestamp: i64,
144}
145
146impl<'buf> BorrowedVideoFrame<'buf> {
147 pub fn try_from_uncompressed(
185 data: &'buf [u8],
186 width: i32,
187 height: i32,
188 pixel_format: PixelFormat,
189 frame_rate_n: i32,
190 frame_rate_d: i32,
191 ) -> Result<Self> {
192 Self::builder(data, width, height, pixel_format)
193 .frame_rate(frame_rate_n, frame_rate_d)
194 .build()
195 }
196
197 pub fn builder(
201 data: &'buf [u8],
202 width: i32,
203 height: i32,
204 pixel_format: PixelFormat,
205 ) -> BorrowedVideoFrameBuilder<'buf> {
206 BorrowedVideoFrameBuilder {
207 data,
208 width,
209 height,
210 pixel_format,
211 line_stride: None,
212 frame_rate_n: 30,
213 frame_rate_d: 1,
214 picture_aspect_ratio: 16.0 / 9.0,
215 scan_type: ScanType::Progressive,
216 timecode: NDIlib_send_timecode_synthesize,
217 metadata: None,
218 timestamp: 0,
219 }
220 }
221
222 pub fn try_to_owned(&self) -> Result<VideoFrame> {
228 unsafe { VideoFrame::from_raw_validated(&self.to_raw(), self.layout, self.metadata_layout) }
231 }
232
233 pub fn width(&self) -> i32 {
235 self.layout.width
236 }
237
238 pub fn height(&self) -> i32 {
240 self.layout.height
241 }
242
243 pub fn pixel_format(&self) -> PixelFormat {
245 self.layout.pixel_format
246 }
247
248 pub fn fourcc(&self) -> u32 {
250 self.layout.pixel_format.into()
251 }
252
253 pub fn frame_rate_n(&self) -> i32 {
255 self.frame_rate_n
256 }
257
258 pub fn frame_rate_d(&self) -> i32 {
260 self.frame_rate_d
261 }
262
263 pub fn picture_aspect_ratio(&self) -> f32 {
265 self.picture_aspect_ratio
266 }
267
268 pub fn scan_type(&self) -> ScanType {
270 self.scan_type
271 }
272
273 pub fn timecode(&self) -> i64 {
275 self.timecode
276 }
277
278 pub fn data(&self) -> &[u8] {
280 self.data
281 }
282
283 pub fn line_stride_or_size(&self) -> LineStrideOrSize {
285 self.layout.line_stride_or_size
286 }
287
288 pub fn validated_data_len(&self) -> usize {
290 self.layout.data_len_bytes
291 }
292
293 pub fn metadata(&self) -> Option<&str> {
295 unsafe {
297 crate::frames::frame_metadata_str(
298 self.metadata.map_or(ptr::null(), |m| m.as_ptr()),
299 self.metadata_layout,
300 )
301 }
302 }
303
304 pub fn timestamp(&self) -> i64 {
306 self.timestamp
307 }
308
309 #[must_use]
329 pub fn with_timecode(mut self, timecode: i64) -> Self {
330 self.timecode = timecode;
331 self
332 }
333
334 pub fn with_picture_aspect_ratio(mut self, ratio: f32) -> Result<Self> {
343 crate::frames::validate_video_frame_metadata(self.frame_rate_n, self.frame_rate_d, ratio)?;
344 self.picture_aspect_ratio = ratio;
345 Ok(self)
346 }
347
348 fn to_raw(&self) -> NDIlib_video_frame_v2_t {
349 NDIlib_video_frame_v2_t {
351 xres: self.layout.width,
352 yres: self.layout.height,
353 FourCC: self.layout.pixel_format.into(),
354 frame_rate_N: self.frame_rate_n,
355 frame_rate_D: self.frame_rate_d,
356 picture_aspect_ratio: self.picture_aspect_ratio,
357 frame_format_type: self.scan_type.into(),
358 timecode: self.timecode,
359 p_data: self.data.as_ptr() as *mut u8,
360 __bindgen_anon_1: self.layout.line_stride_or_size.into(),
361 p_metadata: self.metadata.map_or(ptr::null(), |m| m.as_ptr()),
362 timestamp: self.timestamp,
363 }
364 }
365}
366
367pub struct BorrowedVideoFrameBuilder<'buf> {
369 data: &'buf [u8],
370 width: i32,
371 height: i32,
372 pixel_format: PixelFormat,
373 line_stride: Option<i32>,
374 frame_rate_n: i32,
375 frame_rate_d: i32,
376 picture_aspect_ratio: f32,
377 scan_type: ScanType,
378 timecode: i64,
379 metadata: Option<&'buf CStr>,
380 timestamp: i64,
381}
382
383impl<'buf> BorrowedVideoFrameBuilder<'buf> {
384 #[must_use]
386 pub fn line_stride(mut self, stride: i32) -> Self {
387 self.line_stride = Some(stride);
388 self
389 }
390 #[must_use]
392 pub fn frame_rate(mut self, numerator: i32, denominator: i32) -> Self {
393 self.frame_rate_n = numerator;
394 self.frame_rate_d = denominator;
395 self
396 }
397 #[must_use]
399 pub fn picture_aspect_ratio(mut self, ratio: f32) -> Self {
400 self.picture_aspect_ratio = ratio;
401 self
402 }
403 #[must_use]
405 pub fn scan_type(mut self, scan_type: ScanType) -> Self {
406 self.scan_type = scan_type;
407 self
408 }
409 #[must_use]
411 pub fn timecode(mut self, timecode: i64) -> Self {
412 self.timecode = timecode;
413 self
414 }
415 #[must_use]
417 pub fn metadata(mut self, metadata: &'buf CStr) -> Self {
418 self.metadata = Some(metadata);
419 self
420 }
421 #[must_use]
423 pub fn timestamp(mut self, timestamp: i64) -> Self {
424 self.timestamp = timestamp;
425 self
426 }
427
428 pub fn build(self) -> Result<BorrowedVideoFrame<'buf>> {
433 crate::frames::validate_video_frame_metadata(
434 self.frame_rate_n,
435 self.frame_rate_d,
436 self.picture_aspect_ratio,
437 )?;
438 let layout = ValidatedVideoLayout::new_uncompressed(
439 self.pixel_format,
440 self.width,
441 self.height,
442 self.line_stride,
443 )?;
444 if self.data.len() < layout.data_len_bytes {
445 return Err(Error::InvalidFrame(format!(
446 "Buffer too small for {:?}: got {} bytes, expected at least {}",
447 self.pixel_format,
448 self.data.len(),
449 layout.data_len_bytes,
450 )));
451 }
452 let metadata_layout = ValidatedFrameMetadata::from_cstr(self.metadata)?;
453 Ok(BorrowedVideoFrame {
454 layout,
455 data: self.data,
456 frame_rate_n: self.frame_rate_n,
457 frame_rate_d: self.frame_rate_d,
458 picture_aspect_ratio: self.picture_aspect_ratio,
459 scan_type: self.scan_type,
460 timecode: self.timecode,
461 metadata: self.metadata,
462 metadata_layout,
463 timestamp: self.timestamp,
464 })
465 }
466}
467
468impl<'buf> From<&'buf VideoFrame> for BorrowedVideoFrame<'buf> {
469 fn from(frame: &'buf VideoFrame) -> Self {
470 let layout = frame.validated_layout();
471 BorrowedVideoFrame {
472 frame_rate_n: frame.frame_rate_n(),
473 frame_rate_d: frame.frame_rate_d(),
474 picture_aspect_ratio: frame.picture_aspect_ratio(),
475 scan_type: frame.scan_type(),
476 timecode: frame.timecode(),
477 data: frame.data(),
478 layout,
479 metadata: frame.metadata_cstr(),
480 metadata_layout: frame.validated_metadata(),
481 timestamp: frame.timestamp(),
482 }
483 }
484}
485
486impl Sender {
487 pub fn new(ndi: &NDI, create_settings: &SenderOptions) -> Result<Self> {
496 let name_cstr =
497 CString::new(create_settings.name.clone()).map_err(Error::InvalidCString)?;
498 let groups_cstr = match &create_settings.groups {
499 Some(groups) => Some(CString::new(groups.clone()).map_err(Error::InvalidCString)?),
500 None => None,
501 };
502
503 let c_settings = NDIlib_send_create_t {
504 p_ndi_name: name_cstr.as_ptr(),
505 p_groups: groups_cstr.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
506 clock_video: create_settings.clock_video,
507 clock_audio: create_settings.clock_audio,
508 };
509
510 let instance = unsafe { NDIlib_send_create(&c_settings) };
511 if instance.is_null() {
512 Err(Error::InitializationFailed(
513 "Failed to create NDI send instance".into(),
514 ))
515 } else {
516 let inner = Inner {
517 instance,
518 _name: name_cstr,
519 _groups: groups_cstr,
520 video_callback: None,
521 video: Mutex::new(VideoState::default()),
522 };
523
524 Ok(Self {
525 inner,
526 _ndi: ndi.clone(),
527 })
528 }
529 }
530
531 pub fn send_video(&self, video_frame: &VideoFrame) {
536 let len = {
537 let mut video = self.inner.video.lock().unwrap_or_else(|e| e.into_inner());
538 unsafe {
540 NDIlib_send_send_video_v2(self.inner.instance, &video_frame.to_raw());
541 }
542 video.completed()
543 };
544 self.inner.notify(len);
545 }
546
547 pub fn send_video_async(&mut self, frame: VideoFrame) -> Option<VideoFrame> {
560 let raw = frame.to_raw();
561 let (previous, len) = {
562 let mut video = self.inner.video.lock().unwrap_or_else(|e| e.into_inner());
563 let len = video.completed();
564 let previous = video.pending.replace(frame);
568 video.in_flight = true;
569 unsafe {
570 NDIlib_send_send_video_async_v2(self.inner.instance, &raw);
571 }
572 (previous, len)
573 };
574 self.inner.notify(len);
577 previous
578 }
579
580 pub fn finish_video_async(&mut self) -> Option<VideoFrame> {
587 let (previous, len) = {
588 let mut video = self.inner.video.lock().unwrap_or_else(|e| e.into_inner());
589 if video.in_flight {
590 flush_null_frame(self.inner.instance);
591 }
592 let len = video.completed();
593 (video.pending.take(), len)
594 };
595 self.inner.notify(len);
596 previous
597 }
598
599 pub fn send_video_async_copy(
611 &mut self,
612 frame: &BorrowedVideoFrame<'_>,
613 ) -> Result<Option<VideoFrame>> {
614 let owned = frame.try_to_owned()?;
615 Ok(self.send_video_async(owned))
616 }
617
618 pub fn send_audio(&self, audio_frame: &AudioFrame) {
657 unsafe {
658 NDIlib_send_send_audio_v3(self.inner.instance, &audio_frame.to_raw());
659 }
660 }
661
662 pub fn send_metadata(&self, metadata_frame: &MetadataFrame) {
665 let raw = metadata_frame.to_raw();
666 unsafe {
668 NDIlib_send_send_metadata(self.inner.instance, &raw);
669 }
670 }
671
672 pub fn tally(&self, timeout: Duration) -> Result<Option<Tally>> {
707 let timeout_ms = to_ms_checked(timeout)?;
708 let mut raw_tally = Tally::new(false, false).to_raw();
709 let success =
710 unsafe { NDIlib_send_get_tally(self.inner.instance, &mut raw_tally, timeout_ms) };
711
712 if success {
713 Ok(Some(Tally {
714 on_program: raw_tally.on_program,
715 on_preview: raw_tally.on_preview,
716 }))
717 } else {
718 Ok(None)
719 }
720 }
721
722 pub fn connection_count(&self, timeout: Duration) -> Result<u32> {
755 let timeout_ms = to_ms_checked(timeout)?;
756 let count = unsafe { NDIlib_send_get_no_connections(self.inner.instance, timeout_ms) };
757
758 if count < 0 {
759 Err(Error::Timeout("Failed to obtain connection count".into()))
760 } else {
761 Ok(count as u32)
762 }
763 }
764
765 pub fn clear_connection_metadata(&self) {
766 unsafe { NDIlib_send_clear_connection_metadata(self.inner.instance) }
767 }
768
769 pub fn add_connection_metadata(&self, metadata_frame: &MetadataFrame) {
772 let raw = metadata_frame.to_raw();
773 unsafe { NDIlib_send_add_connection_metadata(self.inner.instance, &raw) }
775 }
776
777 pub fn set_failover(&self, source: &Source) -> Result<()> {
783 let raw_source = source.to_raw()?;
784 unsafe { NDIlib_send_set_failover(self.inner.instance, &raw_source.raw) }
785 Ok(())
786 }
787
788 pub fn clear_failover(&self) {
790 unsafe { NDIlib_send_set_failover(self.inner.instance, ptr::null()) }
791 }
792
793 pub fn source(&self) -> Result<Source> {
814 let source_ptr = unsafe { NDIlib_send_get_source_name(self.inner.instance) };
815 unsafe { Source::try_from_raw(source_ptr) }
818 }
819
820 pub fn on_async_video_done<F>(&mut self, handler: F)
833 where
834 F: Fn(usize) + Send + Sync + 'static,
835 {
836 self.inner.video_callback = Some(Box::new(handler));
837 }
838
839 pub fn clear_async_video_done(&mut self) {
841 self.inner.video_callback = None;
842 }
843
844 pub fn flush_async_blocking(&self) {
853 self.inner.complete();
854 }
855}
856
857impl Drop for Inner {
858 fn drop(&mut self) {
859 let video = self.video.get_mut().unwrap_or_else(|e| e.into_inner());
860 if video.in_flight {
861 flush_null_frame(self.instance);
862 }
863 let len = video.completed();
864 unsafe { NDIlib_send_destroy(self.instance) };
867 if std::thread::panicking() {
868 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.notify(len)));
869 } else {
870 self.notify(len);
871 }
872 }
873}
874
875unsafe impl Send for Sender {}
890
891unsafe impl Sync for Sender {}
903
904#[derive(Debug, Clone)]
905pub struct SenderOptions {
906 pub name: String,
907 pub groups: Option<String>,
908 pub clock_video: bool,
909 pub clock_audio: bool,
910}
911
912impl SenderOptions {
913 pub fn builder<S: Into<String>>(name: S) -> SenderOptionsBuilder {
915 SenderOptionsBuilder::new(name)
916 }
917}
918
919#[derive(Debug, Clone)]
921pub struct SenderOptionsBuilder {
922 name: String,
923 groups: Option<String>,
924 clock_video: Option<bool>,
925 clock_audio: Option<bool>,
926}
927
928impl SenderOptionsBuilder {
929 pub fn new<S: Into<String>>(name: S) -> Self {
931 Self {
932 name: name.into(),
933 groups: None,
934 clock_video: None,
935 clock_audio: None,
936 }
937 }
938
939 #[must_use]
941 pub fn groups<S: Into<String>>(mut self, groups: S) -> Self {
942 self.groups = Some(groups.into());
943 self
944 }
945
946 #[must_use]
948 pub fn clock_video(mut self, clock: bool) -> Self {
949 self.clock_video = Some(clock);
950 self
951 }
952
953 #[must_use]
955 pub fn clock_audio(mut self, clock: bool) -> Self {
956 self.clock_audio = Some(clock);
957 self
958 }
959
960 pub fn build(self) -> SenderOptions {
977 let clock_video = self.clock_video.unwrap_or(true);
978 let clock_audio = self.clock_audio.unwrap_or(true);
979
980 SenderOptions {
981 name: self.name,
982 groups: self.groups,
983 clock_video,
984 clock_audio,
985 }
986 }
987}
988#[cfg(test)]
989mod tests {
990 use super::*;
991 use std::sync::Arc;
992
993 fn test_owned_frame(value: u8) -> VideoFrame {
994 let mut frame = VideoFrame::builder()
995 .resolution(16, 16)
996 .pixel_format(PixelFormat::BGRA)
997 .frame_rate(30, 1)
998 .metadata(format!("<frame value='{value}'/>"))
999 .build()
1000 .unwrap();
1001 frame.data_mut().fill(value);
1002 frame
1003 }
1004
1005 fn test_sender() -> Sender {
1006 use std::sync::atomic::{AtomicUsize, Ordering};
1007 static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
1008 let name = format!(
1009 "Owned async regression {} {}",
1010 std::process::id(),
1011 NEXT_ID.fetch_add(1, Ordering::Relaxed)
1012 );
1013 let ndi = NDI::new().unwrap();
1014 Sender::new(
1015 &ndi,
1016 &SenderOptions::builder(name)
1017 .clock_video(false)
1018 .clock_audio(false)
1019 .build(),
1020 )
1021 .unwrap()
1022 }
1023
1024 #[test]
1025 fn metadata_send_methods_reuse_validated_text_with_sdk() {
1026 let sender = test_sender();
1027 let frame = MetadataFrame::with_data("<ndi_text>é</ndi_text>", 42).unwrap();
1028 let expected_ptr = frame.to_raw().p_data;
1029 for _ in 0..3 {
1030 sender.send_metadata(&frame);
1031 assert_eq!(frame.to_raw().p_data, expected_ptr);
1032 }
1033 sender.add_connection_metadata(&frame);
1034 sender.clear_connection_metadata();
1035 }
1036
1037 #[test]
1038 fn owned_async_recycles_original_pixel_and_metadata_allocations() {
1039 let mut sender = test_sender();
1040 let first = test_owned_frame(11);
1041 let pixels = first.data().as_ptr();
1042 let metadata = first.metadata_cstr().unwrap().as_ptr();
1043 assert!(sender.send_video_async(first).is_none());
1044 {
1045 let video = sender.inner.video.lock().unwrap();
1046 let Some(frame) = &video.pending else {
1047 panic!("missing owned frame")
1048 };
1049 assert_eq!(frame.to_raw().p_data, pixels as *mut u8);
1050 assert_eq!(frame.to_raw().p_metadata, metadata);
1051 }
1052 let second = test_owned_frame(22);
1053 let second_pixels = second.data().as_ptr();
1054 let recycled = sender.send_video_async(second).unwrap();
1055 assert_eq!(recycled.data().as_ptr(), pixels);
1056 assert_eq!(recycled.metadata_cstr().unwrap().as_ptr(), metadata);
1057 assert_eq!(recycled.metadata(), Some("<frame value='11'/>"));
1058 let final_frame = sender.finish_video_async().unwrap();
1059 assert_eq!(final_frame.data().as_ptr(), second_pixels);
1060 assert!(sender.finish_video_async().is_none());
1061 }
1062
1063 #[test]
1064 fn mixed_sync_flush_and_copy_sends_notify_once_and_preserve_recovery() {
1065 use std::sync::atomic::{AtomicUsize, Ordering};
1066 let count = Arc::new(AtomicUsize::new(0));
1067 let observed = count.clone();
1068 let mut sender = test_sender();
1069 sender.on_async_video_done(move |len| {
1070 assert_eq!(len, 1024);
1071 observed.fetch_add(1, Ordering::SeqCst);
1072 });
1073 let frame = test_owned_frame(1);
1074 let pixels = frame.data().as_ptr();
1075 sender.send_video_async(frame);
1076 sender.send_video(&test_owned_frame(9));
1077 sender.flush_async_blocking();
1078 assert_eq!(count.load(Ordering::SeqCst), 1);
1079 assert_eq!(sender.finish_video_async().unwrap().data().as_ptr(), pixels);
1080 assert_eq!(count.load(Ordering::SeqCst), 1);
1081
1082 sender.send_video_async(test_owned_frame(2));
1084 let data = vec![3; 1024];
1085 let metadata = CString::new("<copy/>").unwrap();
1086 {
1087 let borrowed = BorrowedVideoFrame::builder(&data, 16, 16, PixelFormat::BGRA)
1088 .metadata(&metadata)
1089 .build()
1090 .unwrap();
1091 assert_eq!(
1092 sender
1093 .send_video_async_copy(&borrowed)
1094 .unwrap()
1095 .unwrap()
1096 .data()[0],
1097 2
1098 );
1099 }
1100 {
1101 let video = sender.inner.video.lock().unwrap();
1102 let Some(copy) = &video.pending else {
1103 panic!("missing copied snapshot")
1104 };
1105 assert_ne!(copy.data().as_ptr(), data.as_ptr());
1106 assert_ne!(copy.metadata_cstr().unwrap().as_ptr(), metadata.as_ptr());
1107 }
1108 drop(data);
1109 drop(metadata);
1110 {
1111 let video = sender.inner.video.lock().unwrap();
1112 let Some(copy) = &video.pending else {
1113 panic!("missing copied snapshot")
1114 };
1115 assert_eq!(copy.data(), vec![3; 1024]);
1116 assert_eq!(copy.metadata().unwrap(), "<copy/>");
1117 }
1118 let copied = sender.send_video_async(test_owned_frame(4)).unwrap();
1120 assert_eq!(copied.data()[0], 3);
1121 assert_eq!(copied.metadata(), Some("<copy/>"));
1122 sender.finish_video_async().unwrap();
1123 sender.flush_async_blocking();
1124 assert_eq!(count.load(Ordering::SeqCst), 4);
1125 }
1126
1127 #[test]
1128 fn callback_panic_after_submission_keeps_new_owned_frame_recoverable() {
1129 use std::sync::atomic::{AtomicUsize, Ordering};
1130 let count = Arc::new(AtomicUsize::new(0));
1131 let observed = count.clone();
1132 let mut sender = test_sender();
1133 sender.on_async_video_done(move |_| {
1134 if observed.fetch_add(1, Ordering::SeqCst) == 0 {
1135 panic!("intentional completion panic");
1136 }
1137 });
1138 sender.send_video_async(test_owned_frame(1));
1139 let second = test_owned_frame(2);
1140 let pixels = second.data().as_ptr();
1141 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1142 sender.send_video_async(second);
1143 }));
1144 assert!(result.is_err());
1145 assert_eq!(sender.finish_video_async().unwrap().data().as_ptr(), pixels);
1146 assert_eq!(count.load(Ordering::SeqCst), 2);
1147 assert!(sender.finish_video_async().is_none());
1148 }
1149
1150 #[test]
1151 fn completion_callback_can_reenter_shared_flush_without_deadlock() {
1152 use std::sync::{
1153 atomic::{AtomicUsize, Ordering},
1154 Weak,
1155 };
1156 let reference = Arc::new(Mutex::new(Weak::<Sender>::new()));
1157 let callback_reference = reference.clone();
1158 let count = Arc::new(AtomicUsize::new(0));
1159 let observed = count.clone();
1160 let mut sender = test_sender();
1161 sender.on_async_video_done(move |_| {
1162 observed.fetch_add(1, Ordering::SeqCst);
1163 let sender = callback_reference.lock().unwrap().upgrade().unwrap();
1164 sender.flush_async_blocking();
1165 sender.send_video(&test_owned_frame(7));
1166 });
1167 sender.send_video_async(test_owned_frame(5));
1168 let sender = Arc::new(sender);
1169 *reference.lock().unwrap() = Arc::downgrade(&sender);
1170 std::thread::scope(|scope| {
1171 for _ in 0..4 {
1172 scope.spawn(|| sender.flush_async_blocking());
1173 }
1174 });
1175 assert_eq!(count.load(Ordering::SeqCst), 1);
1176 let mut sender = Arc::try_unwrap(sender).unwrap();
1177 assert_eq!(sender.finish_video_async().unwrap().data()[0], 5);
1178 }
1179
1180 #[test]
1181 fn sender_drop_notifies_once_for_copy() {
1182 use std::sync::atomic::{AtomicUsize, Ordering};
1183 let count = Arc::new(AtomicUsize::new(0));
1184 let observed = count.clone();
1185 let mut sender = test_sender();
1186 sender.on_async_video_done(move |_| {
1187 observed.fetch_add(1, Ordering::SeqCst);
1188 });
1189 let data = vec![0; 1024];
1190 {
1191 let frame =
1192 BorrowedVideoFrame::try_from_uncompressed(&data, 16, 16, PixelFormat::BGRA, 30, 1)
1193 .unwrap();
1194 assert!(sender.send_video_async_copy(&frame).unwrap().is_none());
1195 }
1196 drop(data);
1197 drop(sender);
1198 assert_eq!(count.load(Ordering::SeqCst), 1);
1199 }
1200
1201 #[test]
1202 fn completion_handler_can_be_replaced_and_cleared() {
1203 use std::sync::atomic::{AtomicUsize, Ordering};
1204 let count = Arc::new(AtomicUsize::new(0));
1205 let mut sender = test_sender();
1206 sender.on_async_video_done(|_| panic!("replaced handler must not run"));
1207 let observed = count.clone();
1208 sender.on_async_video_done(move |_| {
1209 observed.fetch_add(1, Ordering::SeqCst);
1210 });
1211 sender.send_video_async(test_owned_frame(1));
1212 sender.finish_video_async();
1213 sender.clear_async_video_done();
1214 sender.send_video_async(test_owned_frame(2));
1215 sender.finish_video_async();
1216 assert_eq!(count.load(Ordering::SeqCst), 1);
1217 }
1218
1219 #[test]
1220 fn test_async_flush_uses_null_frame_pointer() {
1221 assert!(async_flush_frame_ptr().is_null());
1222 }
1223
1224 #[test]
1225 fn borrowed_builder_validates_padded_layouts_and_preserves_headers_without_allocating() {
1226 for format in [
1227 PixelFormat::BGRA,
1228 PixelFormat::BGRX,
1229 PixelFormat::RGBA,
1230 PixelFormat::RGBX,
1231 PixelFormat::UYVY,
1232 PixelFormat::UYVA,
1233 PixelFormat::P216,
1234 PixelFormat::PA16,
1235 PixelFormat::I420,
1236 PixelFormat::YV12,
1237 PixelFormat::NV12,
1238 ] {
1239 for stride in [
1240 0,
1241 format.try_line_stride(4).unwrap(),
1242 format.try_line_stride(4).unwrap() + 8,
1243 ] {
1244 let layout =
1245 ValidatedVideoLayout::new_uncompressed(format, 4, 4, Some(stride)).unwrap();
1246 let data = vec![19; layout.data_len_bytes];
1247 let metadata = c"<pixels>é</pixels>";
1248 let (borrowed, allocations) = crate::test_alloc::allocation_count(|| {
1249 let frame = BorrowedVideoFrame::builder(&data, 4, 4, format)
1250 .line_stride(stride)
1251 .frame_rate(30000, 1001)
1252 .picture_aspect_ratio(1.25)
1253 .scan_type(ScanType::Interlaced)
1254 .timecode(0)
1255 .timestamp(67890)
1256 .metadata(metadata)
1257 .build()
1258 .unwrap();
1259 for _ in 0..3 {
1260 assert_eq!(frame.metadata(), Some("<pixels>é</pixels>"));
1261 assert_eq!(frame.validated_data_len(), data.len());
1262 assert_eq!(frame.pixel_format(), format);
1263 }
1264 frame
1265 });
1266 assert_eq!(allocations, 0);
1267 let (owned, allocations) =
1268 crate::test_alloc::allocation_count(|| borrowed.try_to_owned().unwrap());
1269 assert_eq!(allocations, 2);
1270 assert_eq!((owned.frame_rate_n(), owned.frame_rate_d()), (30000, 1001));
1271 assert_eq!(owned.picture_aspect_ratio(), 1.25);
1272 assert_eq!(owned.scan_type(), ScanType::Interlaced);
1273 assert_eq!((owned.timecode(), owned.timestamp()), (0, 67890));
1274 assert_eq!(owned.metadata(), borrowed.metadata());
1275 let (reborrowed, allocations) =
1276 crate::test_alloc::allocation_count(|| BorrowedVideoFrame::from(&owned));
1277 assert_eq!(allocations, 0);
1278 assert_eq!(reborrowed.metadata(), borrowed.metadata());
1279 assert!(
1280 BorrowedVideoFrame::builder(&data[..data.len() - 1], 4, 4, format)
1281 .line_stride(stride)
1282 .build()
1283 .is_err()
1284 );
1285 }
1286 for stride in [-1, 1, i32::MAX] {
1287 let expected = ValidatedVideoLayout::new_uncompressed(format, 4, 4, Some(stride));
1288 let result = BorrowedVideoFrame::builder(&[], 4, 4, format)
1289 .line_stride(stride)
1290 .build();
1291 assert!(expected.is_err());
1292 assert!(result.is_err());
1293 }
1294 }
1295 }
1296
1297 #[test]
1298 fn borrowed_metadata_is_bounded_utf8_and_preserves_empty_presence() {
1299 let data = [0; 16];
1300 for metadata in [None, Some(c"")] {
1301 let mut builder = BorrowedVideoFrame::builder(&data, 2, 2, PixelFormat::BGRA);
1302 if let Some(metadata) = metadata {
1303 builder = builder.metadata(metadata);
1304 }
1305 let frame = builder.build().unwrap();
1306 let (owned, allocations) =
1307 crate::test_alloc::allocation_count(|| frame.try_to_owned().unwrap());
1308 assert_eq!(allocations, 1 + usize::from(metadata.is_some()));
1309 assert_eq!(owned.metadata(), metadata.map(|_| ""));
1310 }
1311 let invalid = c"\xff";
1312 assert!(matches!(
1313 BorrowedVideoFrame::builder(&data, 2, 2, PixelFormat::BGRA)
1314 .metadata(invalid)
1315 .build(),
1316 Err(Error::InvalidUtf8(_))
1317 ));
1318 for len in [
1319 crate::frames::MAX_METADATA_BYTES,
1320 crate::frames::MAX_METADATA_BYTES + 1,
1321 ] {
1322 let mut bytes = vec![b'x'; len];
1323 bytes[len - 1] = 0;
1324 let metadata = CStr::from_bytes_with_nul(&bytes).unwrap();
1325 let result = BorrowedVideoFrame::builder(&data, 2, 2, PixelFormat::BGRA)
1326 .metadata(metadata)
1327 .build();
1328 assert_eq!(result.is_ok(), len == crate::frames::MAX_METADATA_BYTES);
1329 }
1330 }
1331
1332 #[test]
1333 fn test_borrowed_frame_setters() {
1334 let buffer = vec![0u8; 64 * 36 * 2];
1335 let frame =
1336 BorrowedVideoFrame::try_from_uncompressed(&buffer, 64, 36, PixelFormat::UYVY, 25, 1)
1337 .unwrap();
1338 assert_eq!(frame.timecode(), NDIlib_send_timecode_synthesize);
1339 assert_eq!(frame.picture_aspect_ratio(), 16.0 / 9.0);
1340
1341 let frame = frame
1342 .with_timecode(1234)
1343 .with_picture_aspect_ratio(0.0)
1344 .unwrap();
1345 assert_eq!(frame.timecode(), 1234);
1346 assert_eq!(frame.picture_aspect_ratio(), 0.0);
1347 let raw = frame.to_raw();
1348 assert_eq!((raw.timecode, raw.picture_aspect_ratio), (1234, 0.0));
1349
1350 for bad in [-1.0, f32::NAN, f32::INFINITY] {
1351 let frame = BorrowedVideoFrame::try_from_uncompressed(
1352 &buffer,
1353 64,
1354 36,
1355 PixelFormat::UYVY,
1356 25,
1357 1,
1358 )
1359 .unwrap();
1360 assert!(frame.with_picture_aspect_ratio(bad).is_err(), "{bad}");
1361 }
1362 }
1363
1364 #[test]
1365 fn test_try_from_uncompressed_exact_size() {
1366 let buffer = vec![0u8; 1920 * 1080 * 4];
1368 let result = BorrowedVideoFrame::try_from_uncompressed(
1369 &buffer,
1370 1920,
1371 1080,
1372 PixelFormat::BGRA,
1373 30,
1374 1,
1375 );
1376 assert!(result.is_ok());
1377 }
1378
1379 #[test]
1380 fn test_try_from_uncompressed_oversized_buffer() {
1381 let buffer = vec![0u8; 1920 * 1080 * 4 + 1000];
1383 let result = BorrowedVideoFrame::try_from_uncompressed(
1384 &buffer,
1385 1920,
1386 1080,
1387 PixelFormat::BGRA,
1388 30,
1389 1,
1390 );
1391 assert!(result.is_ok());
1392 }
1393
1394 #[test]
1395 fn test_try_from_uncompressed_undersized_buffer() {
1396 let buffer = vec![0u8; 1920 * 1080 * 4 - 1];
1398 let result = BorrowedVideoFrame::try_from_uncompressed(
1399 &buffer,
1400 1920,
1401 1080,
1402 PixelFormat::BGRA,
1403 30,
1404 1,
1405 );
1406 assert!(result.is_err());
1407 if let Err(Error::InvalidFrame(msg)) = result {
1408 assert!(msg.contains("Buffer too small"));
1409 assert!(msg.contains("BGRA"));
1410 }
1411 }
1412
1413 #[test]
1414 fn test_try_from_uncompressed_uyvy() {
1415 let expected_size = 1920 * 1080 * 2;
1417 let buffer = vec![0u8; expected_size];
1418 let result = BorrowedVideoFrame::try_from_uncompressed(
1419 &buffer,
1420 1920,
1421 1080,
1422 PixelFormat::UYVY,
1423 60,
1424 1,
1425 );
1426 assert!(result.is_ok());
1427 }
1428
1429 #[test]
1430 fn test_try_from_uncompressed_nv12() {
1431 let width = 1920;
1433 let height = 1080;
1434 let expected_size = PixelFormat::NV12.try_buffer_size(width, height).unwrap();
1435 let buffer = vec![0u8; expected_size];
1436
1437 let result = BorrowedVideoFrame::try_from_uncompressed(
1438 &buffer,
1439 width,
1440 height,
1441 PixelFormat::NV12,
1442 30,
1443 1,
1444 );
1445 assert!(result.is_ok());
1446 }
1447
1448 #[test]
1449 fn test_try_from_uncompressed_i420() {
1450 let width = 640;
1452 let height = 480;
1453 let expected_size = PixelFormat::I420.try_buffer_size(width, height).unwrap();
1454 let buffer = vec![0u8; expected_size];
1455
1456 let result = BorrowedVideoFrame::try_from_uncompressed(
1457 &buffer,
1458 width,
1459 height,
1460 PixelFormat::I420,
1461 30,
1462 1,
1463 );
1464 assert!(result.is_ok());
1465 }
1466
1467 #[test]
1468 fn test_try_from_uncompressed_rejects_invalid_layout() {
1469 let buffer = vec![0u8; 4096];
1470
1471 assert!(matches!(
1472 BorrowedVideoFrame::try_from_uncompressed(&buffer, 0, 480, PixelFormat::BGRA, 30, 1),
1473 Err(Error::InvalidFrame(_))
1474 ));
1475 assert!(matches!(
1476 BorrowedVideoFrame::try_from_uncompressed(&buffer, 640, -1, PixelFormat::BGRA, 30, 1),
1477 Err(Error::InvalidFrame(_))
1478 ));
1479 assert!(matches!(
1480 BorrowedVideoFrame::try_from_uncompressed(&buffer, 641, 480, PixelFormat::I420, 30, 1),
1481 Err(Error::InvalidFrame(_))
1482 ));
1483 assert!(matches!(
1484 BorrowedVideoFrame::try_from_uncompressed(&buffer, 640, 480, PixelFormat::BGRA, 0, 1),
1485 Err(Error::InvalidFrame(_))
1486 ));
1487 }
1488
1489 #[test]
1490 fn test_getters() {
1491 let buffer = vec![0u8; 1920 * 1080 * 4];
1492 let frame = BorrowedVideoFrame::try_from_uncompressed(
1493 &buffer,
1494 1920,
1495 1080,
1496 PixelFormat::BGRA,
1497 60,
1498 1,
1499 )
1500 .unwrap();
1501
1502 assert_eq!(frame.width(), 1920);
1503 assert_eq!(frame.height(), 1080);
1504 assert_eq!(frame.pixel_format(), PixelFormat::BGRA);
1505 assert_eq!(frame.frame_rate_n(), 60);
1506 assert_eq!(frame.frame_rate_d(), 1);
1507 assert_eq!(frame.picture_aspect_ratio(), 16.0 / 9.0);
1508 assert_eq!(frame.scan_type(), ScanType::Progressive);
1509 assert_eq!(frame.timecode(), NDIlib_send_timecode_synthesize);
1510 assert_eq!(frame.data().len(), buffer.len());
1511 assert!(frame.metadata().is_none());
1512 assert_eq!(frame.timestamp(), 0);
1513 }
1514
1515 #[test]
1516 fn test_all_pixel_formats_validation() {
1517 let test_cases = vec![
1519 (PixelFormat::BGRA, 1920, 1080, 1920 * 1080 * 4),
1520 (PixelFormat::RGBA, 1920, 1080, 1920 * 1080 * 4),
1521 (PixelFormat::BGRX, 1920, 1080, 1920 * 1080 * 4),
1522 (PixelFormat::RGBX, 1920, 1080, 1920 * 1080 * 4),
1523 (PixelFormat::UYVY, 1920, 1080, 1920 * 1080 * 2),
1524 (PixelFormat::UYVA, 1920, 1080, 1920 * 1080 * 3),
1525 (PixelFormat::P216, 1920, 1080, 1920 * 1080 * 4),
1526 (PixelFormat::PA16, 1920, 1080, 1920 * 1080 * 6),
1527 ];
1528
1529 for (format, width, height, expected_min_size) in test_cases {
1530 let buffer = vec![0u8; expected_min_size];
1532 let result =
1533 BorrowedVideoFrame::try_from_uncompressed(&buffer, width, height, format, 30, 1);
1534 assert!(result.is_ok(), "Failed for format {:?}", format);
1535
1536 if expected_min_size > 0 {
1538 let buffer = vec![0u8; expected_min_size - 1];
1539 let result = BorrowedVideoFrame::try_from_uncompressed(
1540 &buffer, width, height, format, 30, 1,
1541 );
1542 assert!(result.is_err(), "Should fail for undersized {:?}", format);
1543 }
1544 }
1545 }
1546
1547 #[test]
1548 fn test_planar_formats() {
1549 let width = 1920;
1551 let height = 1080;
1552
1553 for format in [PixelFormat::NV12, PixelFormat::I420, PixelFormat::YV12] {
1554 let expected_size = format.try_buffer_size(width, height).unwrap();
1555
1556 let buffer = vec![0u8; expected_size];
1557 let result =
1558 BorrowedVideoFrame::try_from_uncompressed(&buffer, width, height, format, 30, 1);
1559 assert!(result.is_ok(), "Failed for planar format {:?}", format);
1560
1561 if expected_size > 0 {
1563 let buffer = vec![0u8; expected_size - 1];
1564 let result = BorrowedVideoFrame::try_from_uncompressed(
1565 &buffer, width, height, format, 30, 1,
1566 );
1567 assert!(result.is_err(), "Should fail for undersized {:?}", format);
1568 }
1569 }
1570 }
1571}