1use num_enum::{IntoPrimitive, TryFromPrimitive};
4
5use std::{
6 ffi::{CStr, CString},
7 fmt,
8 marker::PhantomData,
9 num::NonZeroUsize,
10 os::raw::c_char,
11 ptr, slice, str,
12};
13
14use crate::{
15 capture::{AudioKind, FrameFree, FrameSyncAudioFree, Guard, VideoKind},
16 ndi_lib::*,
17 recv_guard::{RecvAudioGuard, RecvMetadataGuard},
18 Error, Result,
19};
20
21#[derive(Debug, TryFromPrimitive, IntoPrimitive, Clone, Copy, PartialEq, Eq)]
48#[non_exhaustive]
49#[repr(u32)]
50pub enum PixelFormat {
51 UYVY = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_UYVY as _,
53 UYVA = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_UYVA as _,
55 P216 = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_P216 as _,
57 PA16 = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_PA16 as _,
59 YV12 = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_YV12 as _,
61 I420 = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_I420 as _,
63 NV12 = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_NV12 as _,
65 BGRA = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_BGRA as _,
67 BGRX = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_BGRX as _,
69 RGBA = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_RGBA as _,
71 RGBX = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_RGBX as _,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90#[non_exhaustive]
91pub enum FormatCategory {
92 Packed,
94 Planar420,
96 SemiPlanar420,
98 PackedAlpha,
101 SemiPlanar422,
104 SemiPlanar422Alpha,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct PixelFormatInfo {
130 bytes_per_pixel: u8,
133 category: FormatCategory,
135}
136
137impl PixelFormatInfo {
138 #[must_use]
142 pub const fn bytes_per_pixel(&self) -> u8 {
143 self.bytes_per_pixel
144 }
145
146 #[must_use]
148 pub const fn category(&self) -> FormatCategory {
149 self.category
150 }
151
152 #[must_use]
154 pub const fn is_planar_420(&self) -> bool {
155 matches!(
156 self.category,
157 FormatCategory::Planar420 | FormatCategory::SemiPlanar420
158 )
159 }
160
161 pub fn try_buffer_len(&self, y_stride: i32, height: i32) -> Result<usize> {
196 if y_stride <= 0 {
197 return Err(Error::InvalidFrame(format!(
198 "Video line stride must be positive, got {y_stride}"
199 )));
200 }
201 if height <= 0 {
202 return Err(Error::InvalidFrame(format!(
203 "Video frame height must be positive, got {height}"
204 )));
205 }
206
207 let y_stride = usize::try_from(y_stride)
208 .map_err(|_| Error::InvalidFrame(format!("Invalid y_stride value: {y_stride}")))?;
209 let height = usize::try_from(height)
210 .map_err(|_| Error::InvalidFrame(format!("Invalid height value: {height}")))?;
211
212 let len = calculate_buffer_len_for_info_checked(*self, y_stride, height)?;
213 validate_video_data_len(len)?;
214 Ok(len)
215 }
216}
217
218impl PixelFormat {
219 #[must_use]
240 pub const fn info(self) -> PixelFormatInfo {
241 match self {
242 Self::BGRA | Self::BGRX | Self::RGBA | Self::RGBX => PixelFormatInfo {
243 bytes_per_pixel: 4,
244 category: FormatCategory::Packed,
245 },
246 Self::UYVY => PixelFormatInfo {
247 bytes_per_pixel: 2,
248 category: FormatCategory::Packed,
249 },
250 Self::UYVA => PixelFormatInfo {
251 bytes_per_pixel: 2,
252 category: FormatCategory::PackedAlpha,
253 },
254 Self::P216 => PixelFormatInfo {
255 bytes_per_pixel: 2,
256 category: FormatCategory::SemiPlanar422,
257 },
258 Self::PA16 => PixelFormatInfo {
259 bytes_per_pixel: 2,
260 category: FormatCategory::SemiPlanar422Alpha,
261 },
262 Self::YV12 | Self::I420 => PixelFormatInfo {
263 bytes_per_pixel: 1,
264 category: FormatCategory::Planar420,
265 },
266 Self::NV12 => PixelFormatInfo {
267 bytes_per_pixel: 1,
268 category: FormatCategory::SemiPlanar420,
269 },
270 }
271 }
272
273 pub fn try_line_stride(self, width: i32) -> Result<i32> {
304 validate_video_width(width)?;
305 let width_usize = usize::try_from(width)
306 .map_err(|_| Error::InvalidFrame(format!("Invalid width value: {width}")))?;
307 let stride = min_video_line_stride_checked(self, width_usize)?;
308 i32::try_from(stride).map_err(|_| {
309 Error::InvalidFrame(format!("Video line stride {stride} exceeds i32 range"))
310 })
311 }
312
313 pub fn try_buffer_size(self, width: i32, height: i32) -> Result<usize> {
338 let layout = ValidatedVideoLayout::new_uncompressed(self, width, height, None)?;
339 Ok(layout.data_len_bytes)
340 }
341}
342
343impl From<PixelFormat> for i32 {
344 fn from(value: PixelFormat) -> Self {
345 let u32_value: u32 = value.into();
346 u32_value as i32
347 }
348}
349
350#[derive(Debug, TryFromPrimitive, IntoPrimitive, Clone, Copy, PartialEq, Eq)]
373#[non_exhaustive]
374#[repr(u32)]
375pub enum ScanType {
376 Progressive = NDIlib_frame_format_type_e_NDIlib_frame_format_type_progressive as _,
378 Interlaced = NDIlib_frame_format_type_e_NDIlib_frame_format_type_interleaved as _,
380 Field0 = NDIlib_frame_format_type_e_NDIlib_frame_format_type_field_0 as _,
382 Field1 = NDIlib_frame_format_type_e_NDIlib_frame_format_type_field_1 as _,
384}
385
386impl From<ScanType> for i32 {
387 fn from(value: ScanType) -> Self {
388 let u32_value: u32 = value.into();
389 u32_value as i32
390 }
391}
392
393#[derive(Debug, Clone, Copy, PartialEq, Eq)]
396pub enum LineStrideOrSize {
397 LineStrideBytes(i32),
400}
401
402impl From<LineStrideOrSize> for NDIlib_video_frame_v2_t__bindgen_ty_1 {
403 fn from(value: LineStrideOrSize) -> Self {
404 match value {
407 LineStrideOrSize::LineStrideBytes(stride) =>
408 {
409 #[allow(clippy::field_reassign_with_default)]
410 NDIlib_video_frame_v2_t__bindgen_ty_1 {
411 line_stride_in_bytes: stride,
412 }
413 }
414 }
415 }
416}
417
418pub struct VideoFrame {
423 layout: ValidatedVideoLayout,
424 frame_rate_n: i32,
425 frame_rate_d: i32,
426 picture_aspect_ratio: f32,
427 scan_type: ScanType,
428 timecode: i64,
429 data: Vec<u8>,
430 metadata: Option<FrameMetadata>,
431 timestamp: i64,
432}
433
434impl fmt::Debug for VideoFrame {
435 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
436 f.debug_struct("VideoFrame")
437 .field("width", &self.width())
438 .field("height", &self.height())
439 .field("pixel_format", &self.pixel_format())
440 .field("frame_rate_n", &self.frame_rate_n)
441 .field("frame_rate_d", &self.frame_rate_d)
442 .field("picture_aspect_ratio", &self.picture_aspect_ratio)
443 .field("scan_type", &self.scan_type)
444 .field("timecode", &self.timecode)
445 .field("data (bytes)", &self.data.len())
446 .field("line_stride_or_size", &self.line_stride_or_size())
447 .field("metadata", &self.metadata())
448 .field("timestamp", &self.timestamp)
449 .finish()
450 }
451}
452
453impl Default for VideoFrame {
454 fn default() -> Self {
455 VideoFrame::builder()
456 .build()
457 .expect("Default VideoFrame should always succeed")
458 }
459}
460
461impl VideoFrame {
462 pub(crate) fn to_raw(&self) -> NDIlib_video_frame_v2_t {
463 NDIlib_video_frame_v2_t {
464 xres: self.layout.width,
465 yres: self.layout.height,
466 FourCC: self.layout.pixel_format.into(),
467 frame_rate_N: self.frame_rate_n,
468 frame_rate_D: self.frame_rate_d,
469 picture_aspect_ratio: self.picture_aspect_ratio,
470 frame_format_type: self.scan_type.into(),
471 timecode: self.timecode,
472 p_data: self.data.as_ptr() as *mut u8,
473 __bindgen_anon_1: self.layout.line_stride_or_size.into(),
474 p_metadata: self
475 .metadata
476 .as_ref()
477 .map_or(ptr::null(), FrameMetadata::as_ptr),
478 timestamp: self.timestamp,
479 }
480 }
481
482 pub fn width(&self) -> i32 {
484 self.layout.width
485 }
486
487 pub fn height(&self) -> i32 {
489 self.layout.height
490 }
491
492 pub fn pixel_format(&self) -> PixelFormat {
494 self.layout.pixel_format
495 }
496
497 pub fn frame_rate_n(&self) -> i32 {
499 self.frame_rate_n
500 }
501
502 pub fn frame_rate_d(&self) -> i32 {
504 self.frame_rate_d
505 }
506
507 pub fn picture_aspect_ratio(&self) -> f32 {
509 self.picture_aspect_ratio
510 }
511
512 pub fn scan_type(&self) -> ScanType {
514 self.scan_type
515 }
516
517 pub fn timecode(&self) -> i64 {
522 self.timecode
523 }
524
525 pub fn set_timecode(&mut self, timecode: i64) {
528 self.timecode = timecode;
529 }
530
531 pub fn set_scan_type(&mut self, scan_type: ScanType) {
533 self.scan_type = scan_type;
534 }
535
536 pub fn set_timestamp(&mut self, timestamp: i64) {
539 self.timestamp = timestamp;
540 }
541
542 pub fn timestamp(&self) -> i64 {
548 self.timestamp
549 }
550
551 pub fn line_stride_or_size(&self) -> LineStrideOrSize {
553 self.layout.line_stride_or_size
554 }
555
556 pub(crate) fn validated_layout(&self) -> ValidatedVideoLayout {
557 self.layout
558 }
559
560 pub fn data(&self) -> &[u8] {
562 &self.data
563 }
564
565 pub fn data_mut(&mut self) -> &mut [u8] {
568 &mut self.data
569 }
570
571 pub fn replace_data(&mut self, data: Vec<u8>) -> Result<()> {
578 if data.len() != self.layout.data_len_bytes {
579 return Err(Error::InvalidFrame(format!(
580 "Video data length {}, expected {} bytes for validated layout",
581 data.len(),
582 self.layout.data_len_bytes
583 )));
584 }
585
586 self.data = data;
587 Ok(())
588 }
589
590 pub fn metadata(&self) -> Option<&str> {
592 self.metadata.as_ref().map(FrameMetadata::as_str)
593 }
594
595 pub(crate) fn metadata_cstr(&self) -> Option<&CStr> {
596 self.metadata.as_ref().map(FrameMetadata::as_cstr)
597 }
598
599 pub(crate) fn validated_metadata(&self) -> ValidatedFrameMetadata {
600 ValidatedFrameMetadata {
601 len_with_nul: self
602 .metadata_cstr()
603 .and_then(|m| NonZeroUsize::new(m.to_bytes_with_nul().len())),
604 text_len: self.metadata().map_or(0, str::len),
605 }
606 }
607
608 pub fn set_metadata<S: Into<String>>(&mut self, metadata: Option<S>) -> Result<()> {
616 self.metadata = metadata.map(FrameMetadata::new).transpose()?;
617 Ok(())
618 }
619
620 pub fn set_frame_rate(&mut self, numerator: i32, denominator: i32) -> Result<()> {
627 validate_video_frame_metadata(numerator, denominator, self.picture_aspect_ratio)?;
628 self.frame_rate_n = numerator;
629 self.frame_rate_d = denominator;
630 Ok(())
631 }
632
633 pub fn set_picture_aspect_ratio(&mut self, ratio: f32) -> Result<()> {
639 validate_video_frame_metadata(self.frame_rate_n, self.frame_rate_d, ratio)?;
640 self.picture_aspect_ratio = ratio;
641 Ok(())
642 }
643
644 #[cfg(feature = "image-encoding")]
694 pub fn encode_png(&self) -> Result<Vec<u8>> {
695 ImagePixelSource::new(self.layout, &self.data)?.encode_png()
696 }
697
698 #[cfg(feature = "image-encoding")]
746 pub fn encode_jpeg(&self, quality: u8) -> Result<Vec<u8>> {
747 ImagePixelSource::new(self.layout, &self.data)?.encode_jpeg(quality)
748 }
749
750 #[cfg(feature = "image-encoding")]
787 pub fn encode_data_url(&self, format: ImageFormat) -> Result<String> {
788 ImagePixelSource::new(self.layout, &self.data)?.encode_data_url(format)
789 }
790
791 pub unsafe fn from_raw(c_frame: &NDIlib_video_frame_v2_t) -> Result<VideoFrame> {
802 let layout = validate_video_layout(c_frame)?;
804 let metadata_layout = unsafe { validate_frame_metadata(c_frame.p_metadata)? };
805
806 Self::from_raw_validated(c_frame, layout, metadata_layout)
807 }
808
809 pub(crate) unsafe fn from_raw_validated(
810 c_frame: &NDIlib_video_frame_v2_t,
811 layout: ValidatedVideoLayout,
812 metadata_layout: ValidatedFrameMetadata,
813 ) -> Result<VideoFrame> {
814 let owned_layout = ValidatedVideoLayout::new_uncompressed(
815 layout.pixel_format,
816 layout.width,
817 layout.height,
818 None,
819 )?;
820 let mut data = Vec::with_capacity(layout.payload_len_bytes);
821 if layout.is_contiguous() {
824 data.extend_from_slice(unsafe {
825 slice::from_raw_parts(c_frame.p_data, layout.payload_len_bytes)
826 });
827 } else {
828 for plane_layout in layout.planes() {
829 let plane = unsafe { VideoPlane::new(c_frame.p_data, *plane_layout) };
830 for row in plane.rows() {
831 data.extend_from_slice(row);
832 }
833 }
834 }
835 let layout = owned_layout;
836
837 let metadata =
838 unsafe { FrameMetadata::copy_from_raw_validated(c_frame.p_metadata, metadata_layout) };
839
840 #[allow(clippy::unnecessary_cast)] let scan_type = ScanType::try_from(c_frame.frame_format_type as u32).map_err(|_| {
842 Error::InvalidFrame(format!(
843 "Unknown scan type: 0x{:08X}",
844 c_frame.frame_format_type
845 ))
846 })?;
847
848 Ok(VideoFrame {
849 layout,
850 frame_rate_n: c_frame.frame_rate_N,
851 frame_rate_d: c_frame.frame_rate_D,
852 picture_aspect_ratio: c_frame.picture_aspect_ratio,
853 scan_type,
854 timecode: c_frame.timecode,
855 data,
856 metadata,
857 timestamp: c_frame.timestamp,
858 })
859 }
860
861 pub fn builder() -> VideoFrameBuilder {
863 VideoFrameBuilder::new()
864 }
865}
866
867#[derive(Debug, Clone)]
869pub struct VideoFrameBuilder {
870 width: Option<i32>,
871 height: Option<i32>,
872 pixel_format: Option<PixelFormat>,
873 frame_rate_n: Option<i32>,
874 frame_rate_d: Option<i32>,
875 picture_aspect_ratio: Option<f32>,
876 scan_type: Option<ScanType>,
877 timecode: Option<i64>,
878 metadata: Option<String>,
879 timestamp: Option<i64>,
880 data: Option<Vec<u8>>,
881}
882
883impl VideoFrameBuilder {
884 pub fn new() -> Self {
886 Self {
887 width: None,
888 height: None,
889 pixel_format: None,
890 frame_rate_n: None,
891 frame_rate_d: None,
892 picture_aspect_ratio: None,
893 scan_type: None,
894 timecode: None,
895 metadata: None,
896 timestamp: None,
897 data: None,
898 }
899 }
900
901 #[must_use]
904 pub fn resolution(mut self, width: i32, height: i32) -> Self {
905 self.width = Some(width);
906 self.height = Some(height);
907 self
908 }
909
910 #[must_use]
912 pub fn pixel_format(mut self, pixel_format: PixelFormat) -> Self {
913 self.pixel_format = Some(pixel_format);
914 self
915 }
916
917 #[must_use]
919 pub fn frame_rate(mut self, numerator: i32, denominator: i32) -> Self {
920 self.frame_rate_n = Some(numerator);
921 self.frame_rate_d = Some(denominator);
922 self
923 }
924
925 #[must_use]
927 pub fn aspect_ratio(mut self, ratio: f32) -> Self {
928 self.picture_aspect_ratio = Some(ratio);
929 self
930 }
931
932 #[must_use]
934 pub fn scan_type(mut self, scan_type: ScanType) -> Self {
935 self.scan_type = Some(scan_type);
936 self
937 }
938
939 #[must_use]
941 pub fn timecode(mut self, tc: i64) -> Self {
942 self.timecode = Some(tc);
943 self
944 }
945
946 #[must_use]
948 pub fn metadata<S: Into<String>>(mut self, meta: S) -> Self {
949 self.metadata = Some(meta.into());
950 self
951 }
952
953 #[must_use]
955 pub fn timestamp(mut self, ts: i64) -> Self {
956 self.timestamp = Some(ts);
957 self
958 }
959
960 #[must_use]
965 pub fn data(mut self, data: Vec<u8>) -> Self {
966 self.data = Some(data);
967 self
968 }
969
970 pub fn build(self) -> Result<VideoFrame> {
975 let width = self.width.unwrap_or(1920);
976 let height = self.height.unwrap_or(1080);
977 let pixel_format = self.pixel_format.unwrap_or(PixelFormat::BGRA);
978 let frame_rate_n = self.frame_rate_n.unwrap_or(60);
979 let frame_rate_d = self.frame_rate_d.unwrap_or(1);
980 let picture_aspect_ratio = self.picture_aspect_ratio.unwrap_or(16.0 / 9.0);
981 let scan_type = self.scan_type.unwrap_or(ScanType::Progressive);
982
983 validate_video_frame_metadata(frame_rate_n, frame_rate_d, picture_aspect_ratio)?;
984 let layout = ValidatedVideoLayout::new_uncompressed(pixel_format, width, height, None)?;
985 let metadata = self.metadata.map(FrameMetadata::new).transpose()?;
986 let data = match self.data {
987 Some(data) => {
988 if data.len() != layout.data_len_bytes {
989 return Err(Error::InvalidFrame(format!(
990 "Video data length {}, expected {} bytes for validated layout",
991 data.len(),
992 layout.data_len_bytes
993 )));
994 }
995 data
996 }
997 None => vec![0u8; layout.data_len_bytes],
998 };
999
1000 Ok(VideoFrame {
1001 layout,
1002 frame_rate_n,
1003 frame_rate_d,
1004 picture_aspect_ratio,
1005 scan_type,
1006 timecode: self.timecode.unwrap_or(NDIlib_send_timecode_synthesize),
1007 data,
1008 metadata,
1009 timestamp: self.timestamp.unwrap_or(0),
1010 })
1011 }
1012}
1013
1014impl Default for VideoFrameBuilder {
1015 fn default() -> Self {
1016 Self::new()
1017 }
1018}
1019
1020impl Drop for VideoFrame {
1021 fn drop(&mut self) {
1022 }
1025}
1026
1027#[derive(Debug)]
1032pub struct AudioFrame {
1033 layout: ValidatedAudioLayout,
1034 timecode: i64,
1035 data: Vec<f32>,
1036 metadata: Option<FrameMetadata>,
1037 timestamp: i64,
1038}
1039
1040impl AudioFrame {
1041 pub(crate) fn to_raw(&self) -> NDIlib_audio_frame_v3_t {
1042 NDIlib_audio_frame_v3_t {
1043 sample_rate: self.layout.sample_rate,
1044 no_channels: self.num_channels(),
1045 no_samples: self.num_samples(),
1046 timecode: self.timecode,
1047 FourCC: self.format().into(),
1048 p_data: self.data.as_ptr() as *mut f32 as *mut u8,
1049 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
1050 channel_stride_in_bytes: self.layout.channel_stride_in_bytes,
1051 },
1052 p_metadata: self
1053 .metadata
1054 .as_ref()
1055 .map_or(ptr::null(), FrameMetadata::as_ptr),
1056 timestamp: self.timestamp,
1057 }
1058 }
1059
1060 #[cfg(test)]
1061 pub(crate) unsafe fn from_raw(raw: NDIlib_audio_frame_v3_t) -> Result<AudioFrame> {
1065 let layout = validate_audio_layout(&raw)?;
1067 let metadata_layout = unsafe { validate_frame_metadata(raw.p_metadata)? };
1068
1069 unsafe { Self::from_raw_validated(raw, layout, metadata_layout) }
1070 }
1071
1072 pub(crate) unsafe fn from_raw_validated(
1077 raw: NDIlib_audio_frame_v3_t,
1078 layout: ValidatedAudioLayout,
1079 metadata_layout: ValidatedFrameMetadata,
1080 ) -> Result<AudioFrame> {
1081 if layout.is_empty() {
1082 return Err(Error::InvalidFrame(
1083 "Cannot create owned AudioFrame from an empty audio layout".into(),
1084 ));
1085 }
1086
1087 let packed_layout = validate_outbound_audio_layout(
1090 layout.sample_rate,
1091 layout.no_channels as i32,
1092 layout.no_samples as i32,
1093 layout.format().expect("non-empty audio has a format"),
1094 )?;
1095 let mut data = Vec::with_capacity(packed_layout.sample_count);
1096 for channel in 0..layout.no_channels {
1097 let range = layout.channel_range(channel).expect("validated channel");
1098 let samples = unsafe {
1101 slice::from_raw_parts(raw.p_data.cast::<f32>().add(range.start), layout.no_samples)
1102 };
1103 data.extend_from_slice(samples);
1104 }
1105
1106 let metadata =
1108 unsafe { FrameMetadata::copy_from_raw_validated(raw.p_metadata, metadata_layout) };
1109
1110 Ok(AudioFrame {
1111 layout: packed_layout,
1112 timecode: raw.timecode,
1113 data,
1114 metadata,
1115 timestamp: raw.timestamp,
1116 })
1117 }
1118
1119 pub fn builder() -> AudioFrameBuilder {
1121 AudioFrameBuilder::new()
1122 }
1123
1124 pub fn sample_rate(&self) -> i32 {
1126 self.layout.sample_rate
1127 }
1128
1129 pub fn num_channels(&self) -> i32 {
1131 self.layout.no_channels as i32
1132 }
1133
1134 pub fn num_samples(&self) -> i32 {
1136 self.layout.no_samples as i32
1137 }
1138
1139 pub fn timecode(&self) -> i64 {
1144 self.timecode
1145 }
1146
1147 pub fn timestamp(&self) -> i64 {
1153 self.timestamp
1154 }
1155
1156 pub fn format(&self) -> AudioFormat {
1158 self.layout
1159 .format()
1160 .expect("owned AudioFrame always has a concrete audio format")
1161 }
1162
1163 pub fn channel_stride_in_bytes(&self) -> i32 {
1165 self.layout.channel_stride_in_bytes
1166 }
1167
1168 pub fn data(&self) -> &[f32] {
1171 &self.data
1172 }
1173
1174 pub fn data_mut(&mut self) -> &mut [f32] {
1176 &mut self.data
1177 }
1178
1179 pub fn replace_data(&mut self, data: Vec<f32>) -> Result<()> {
1186 if data.len() != self.layout.sample_count {
1187 return Err(Error::InvalidFrame(format!(
1188 "Audio data length {}, expected {} samples for validated layout",
1189 data.len(),
1190 self.layout.sample_count
1191 )));
1192 }
1193
1194 self.data = data;
1195 Ok(())
1196 }
1197
1198 pub fn metadata(&self) -> Option<&str> {
1200 self.metadata.as_ref().map(FrameMetadata::as_str)
1201 }
1202
1203 pub fn set_metadata<S: Into<String>>(&mut self, metadata: Option<S>) -> Result<()> {
1211 self.metadata = metadata.map(FrameMetadata::new).transpose()?;
1212 Ok(())
1213 }
1214
1215 pub fn channel_data(&self, channel: usize) -> Option<&[f32]> {
1220 let range = self.layout.channel_range(channel)?;
1221 Some(&self.data[range])
1222 }
1223
1224 pub fn channel_data_mut(&mut self, channel: usize) -> Option<&mut [f32]> {
1226 let range = self.layout.channel_range(channel)?;
1227 Some(&mut self.data[range])
1228 }
1229}
1230
1231#[derive(Debug, Clone)]
1233pub struct AudioFrameBuilder {
1234 sample_rate: Option<i32>,
1235 num_channels: Option<i32>,
1236 num_samples: Option<i32>,
1237 timecode: Option<i64>,
1238 format: Option<AudioFormat>,
1239 data: Option<Vec<f32>>,
1240 layout: Option<AudioLayout>,
1241 metadata: Option<String>,
1242 timestamp: Option<i64>,
1243}
1244
1245impl AudioFrameBuilder {
1246 pub fn new() -> Self {
1248 Self {
1249 sample_rate: None,
1250 num_channels: None,
1251 num_samples: None,
1252 timecode: None,
1253 format: None,
1254 data: None,
1255 layout: None,
1256 metadata: None,
1257 timestamp: None,
1258 }
1259 }
1260
1261 #[must_use]
1263 pub fn sample_rate(mut self, rate: i32) -> Self {
1264 self.sample_rate = Some(rate);
1265 self
1266 }
1267
1268 #[must_use]
1270 pub fn channels(mut self, channels: i32) -> Self {
1271 self.num_channels = Some(channels);
1272 self
1273 }
1274
1275 #[must_use]
1277 pub fn samples(mut self, samples: i32) -> Self {
1278 self.num_samples = Some(samples);
1279 self
1280 }
1281
1282 #[must_use]
1284 pub fn timecode(mut self, tc: i64) -> Self {
1285 self.timecode = Some(tc);
1286 self
1287 }
1288
1289 #[must_use]
1291 pub fn format(mut self, format: AudioFormat) -> Self {
1292 self.format = Some(format);
1293 self
1294 }
1295
1296 #[must_use]
1316 pub fn layout(mut self, layout: AudioLayout) -> Self {
1317 self.layout = Some(layout);
1318 self
1319 }
1320
1321 #[must_use]
1328 pub fn data(mut self, data: Vec<f32>) -> Self {
1329 self.data = Some(data);
1330 self
1331 }
1332
1333 #[must_use]
1335 pub fn metadata<S: Into<String>>(mut self, meta: S) -> Self {
1336 self.metadata = Some(meta.into());
1337 self
1338 }
1339
1340 #[must_use]
1342 pub fn timestamp(mut self, ts: i64) -> Self {
1343 self.timestamp = Some(ts);
1344 self
1345 }
1346
1347 pub fn build(self) -> Result<AudioFrame> {
1356 let sample_rate = self.sample_rate.unwrap_or(48000);
1357 let num_channels = self.num_channels.unwrap_or(2);
1358 let num_samples = self.num_samples.unwrap_or(1024);
1359 let format = self.format.unwrap_or(AudioFormat::FLTP);
1360 let layout = self.layout.unwrap_or(AudioLayout::Planar);
1361 let timecode = self.timecode.unwrap_or(NDIlib_send_timecode_synthesize);
1362 let audio_layout =
1363 validate_outbound_audio_layout(sample_rate, num_channels, num_samples, format)?;
1364 let sample_count = audio_layout.sample_count;
1365
1366 let data = if let Some(input_data) = self.data {
1367 if input_data.len() != sample_count {
1368 return Err(Error::InvalidFrame(format!(
1369 "Audio data length {}, expected {} ({}ch x {}samples)",
1370 input_data.len(),
1371 sample_count,
1372 num_channels,
1373 num_samples
1374 )));
1375 }
1376
1377 match layout {
1378 AudioLayout::Planar => input_data,
1379 AudioLayout::Interleaved => {
1380 let nc = audio_layout.no_channels;
1381 let ns = audio_layout.no_samples;
1382 let mut planar = vec![0.0f32; sample_count];
1383 for ch in 0..nc {
1384 for s in 0..ns {
1385 let dst = ch
1386 .checked_mul(ns)
1387 .and_then(|idx| idx.checked_add(s))
1388 .ok_or_else(|| {
1389 Error::InvalidFrame(
1390 "Audio planar conversion index overflow".into(),
1391 )
1392 })?;
1393 let src = s
1394 .checked_mul(nc)
1395 .and_then(|idx| idx.checked_add(ch))
1396 .ok_or_else(|| {
1397 Error::InvalidFrame(
1398 "Audio interleaved conversion index overflow".into(),
1399 )
1400 })?;
1401 planar[dst] = input_data[src];
1402 }
1403 }
1404 planar
1405 }
1406 }
1407 } else {
1408 vec![0.0f32; sample_count]
1409 };
1410
1411 let metadata = self.metadata.map(FrameMetadata::new).transpose()?;
1412
1413 Ok(AudioFrame {
1414 layout: audio_layout,
1415 timecode,
1416 data,
1417 metadata,
1418 timestamp: self.timestamp.unwrap_or(0),
1419 })
1420 }
1421}
1422
1423impl Default for AudioFrameBuilder {
1424 fn default() -> Self {
1425 Self::new()
1426 }
1427}
1428
1429impl Default for AudioFrame {
1430 fn default() -> Self {
1431 AudioFrame::builder()
1432 .build()
1433 .expect("Default AudioFrame should always succeed")
1434 }
1435}
1436
1437impl Drop for AudioFrame {
1438 fn drop(&mut self) {
1439 }
1442}
1443
1444#[derive(Debug, TryFromPrimitive, IntoPrimitive, Clone, Copy, PartialEq, Eq)]
1465#[non_exhaustive]
1466#[repr(u32)]
1467pub enum AudioFormat {
1468 FLTP = NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP as _,
1470}
1471
1472#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1476pub enum AudioLayout {
1477 Planar,
1484
1485 Interleaved,
1493}
1494
1495impl From<AudioFormat> for i32 {
1496 fn from(value: AudioFormat) -> Self {
1497 let u32_value: u32 = value.into();
1498 u32_value as i32
1499 }
1500}
1501
1502const MAX_VIDEO_BYTES: usize = 256 * 1024 * 1024;
1504
1505const MAX_AUDIO_BYTES: usize = 64 * 1024 * 1024;
1508
1509pub(crate) const MAX_METADATA_BYTES: usize = 4 * 1024 * 1024;
1512
1513#[derive(Debug, Clone)]
1518pub struct MetadataFrame {
1519 data: FrameMetadata,
1520 timecode: i64,
1521}
1522
1523impl MetadataFrame {
1524 pub fn new() -> Self {
1526 MetadataFrame {
1527 data: FrameMetadata::new("").expect("empty metadata is valid"),
1528 timecode: NDIlib_send_timecode_synthesize,
1529 }
1530 }
1531
1532 pub fn with_data(data: impl Into<String>, timecode: i64) -> Result<Self> {
1540 let data = FrameMetadata::new(data)?;
1541 Ok(MetadataFrame { data, timecode })
1542 }
1543
1544 pub fn data(&self) -> &str {
1546 self.data.as_str()
1547 }
1548
1549 pub fn into_data(self) -> String {
1551 self.data.into_string()
1552 }
1553
1554 pub fn timecode(&self) -> i64 {
1559 self.timecode
1560 }
1561
1562 pub fn set_data(&mut self, data: impl Into<String>) -> Result<()> {
1570 self.data = FrameMetadata::new(data)?;
1571 Ok(())
1572 }
1573
1574 pub fn set_timecode(&mut self, timecode: i64) {
1576 self.timecode = timecode;
1577 }
1578
1579 pub fn with_timecode(mut self, timecode: i64) -> Self {
1581 self.timecode = timecode;
1582 self
1583 }
1584
1585 pub(crate) fn to_raw(&self) -> NDIlib_metadata_frame_t {
1588 NDIlib_metadata_frame_t {
1589 length: self.data.inner.as_bytes_with_nul().len() as i32,
1590 timecode: self.timecode,
1591 p_data: self.data.as_ptr() as *mut c_char,
1592 }
1593 }
1594
1595 #[cfg(test)]
1604 pub(crate) unsafe fn from_raw(raw: &NDIlib_metadata_frame_t) -> Result<Self> {
1605 let layout = unsafe { validate_metadata_layout(raw)? };
1606 Ok(unsafe { Self::from_raw_validated(raw, layout) })
1607 }
1608
1609 pub(crate) unsafe fn from_raw_validated(
1616 raw: &NDIlib_metadata_frame_t,
1617 layout: ValidatedMetadataLayout,
1618 ) -> Self {
1619 let data = if layout.len_with_nul == 0 {
1620 FrameMetadata::new("").expect("empty metadata is valid")
1621 } else {
1622 unsafe {
1623 FrameMetadata::copy_from_raw_validated(
1624 raw.p_data,
1625 ValidatedFrameMetadata {
1626 len_with_nul: std::num::NonZeroUsize::new(layout.len_with_nul),
1627 text_len: layout.text_len,
1628 },
1629 )
1630 .expect("non-null metadata")
1631 }
1632 };
1633
1634 Self {
1635 data,
1636 timecode: raw.timecode,
1637 }
1638 }
1639}
1640
1641impl Default for MetadataFrame {
1642 fn default() -> Self {
1643 Self::new()
1644 }
1645}
1646
1647#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1653pub(crate) struct ValidatedMetadataLayout {
1654 pub len_with_nul: usize,
1657 pub text_len: usize,
1659}
1660
1661pub(crate) unsafe fn validate_metadata_layout(
1670 raw: &NDIlib_metadata_frame_t,
1671) -> Result<ValidatedMetadataLayout> {
1672 if raw.length < 0 {
1673 return Err(Error::InvalidFrame(format!(
1674 "Metadata frame has negative length: {}",
1675 raw.length
1676 )));
1677 }
1678
1679 if raw.length == 0 {
1680 if raw.p_data.is_null() {
1681 return Ok(ValidatedMetadataLayout {
1682 len_with_nul: 0,
1683 text_len: 0,
1684 });
1685 }
1686
1687 let metadata = unsafe { validate_frame_metadata(raw.p_data)? };
1690 return Ok(ValidatedMetadataLayout {
1691 len_with_nul: metadata.len_with_nul.expect("non-null metadata").get(),
1692 text_len: metadata.text_len,
1693 });
1694 }
1695
1696 if raw.p_data.is_null() {
1697 return Err(Error::InvalidFrame(
1698 "Metadata frame has non-zero length with null data pointer".into(),
1699 ));
1700 }
1701
1702 let len_with_nul = usize::try_from(raw.length).map_err(|_| {
1703 Error::InvalidFrame(format!("Invalid metadata length value: {}", raw.length))
1704 })?;
1705 validate_metadata_len_with_nul(len_with_nul)?;
1706
1707 let bytes = unsafe { slice::from_raw_parts(raw.p_data.cast::<u8>(), len_with_nul) };
1708 if bytes[len_with_nul - 1] != 0 {
1709 return Err(Error::InvalidFrame(
1710 "Metadata frame length does not include a trailing NUL terminator".into(),
1711 ));
1712 }
1713
1714 let payload = &bytes[..len_with_nul - 1];
1715 if payload.contains(&0) {
1716 return Err(Error::InvalidFrame(
1717 "Metadata frame contains an interior NUL byte".into(),
1718 ));
1719 }
1720
1721 str::from_utf8(payload).map_err(|err| Error::InvalidUtf8(err.to_string()))?;
1722
1723 Ok(ValidatedMetadataLayout {
1724 len_with_nul,
1725 text_len: payload.len(),
1726 })
1727}
1728
1729fn validate_metadata_len_with_nul(len_with_nul: usize) -> Result<i32> {
1730 if len_with_nul == 0 {
1731 return Err(Error::InvalidFrame(
1732 "Metadata length must include a trailing NUL terminator".into(),
1733 ));
1734 }
1735
1736 if len_with_nul > MAX_METADATA_BYTES {
1737 return Err(Error::InvalidFrame(format!(
1738 "Metadata exceeds maximum size: {} bytes > {} bytes",
1739 len_with_nul, MAX_METADATA_BYTES
1740 )));
1741 }
1742
1743 metadata_len_to_i32(len_with_nul)
1744}
1745
1746fn metadata_len_to_i32(len_with_nul: usize) -> Result<i32> {
1747 i32::try_from(len_with_nul).map_err(|_| {
1748 Error::InvalidFrame(format!(
1749 "Metadata length {len_with_nul} exceeds SDK i32 range"
1750 ))
1751 })
1752}
1753
1754unsafe fn metadata_payload_bytes(
1758 raw: &NDIlib_metadata_frame_t,
1759 layout: ValidatedMetadataLayout,
1760) -> &[u8] {
1761 if layout.text_len == 0 {
1762 &[]
1763 } else {
1764 unsafe { slice::from_raw_parts(raw.p_data.cast::<u8>(), layout.text_len) }
1767 }
1768}
1769
1770#[derive(Clone, PartialEq, Eq)]
1777pub(crate) struct FrameMetadata {
1778 inner: CString,
1779}
1780
1781impl FrameMetadata {
1782 pub(crate) fn new<S: Into<String>>(metadata: S) -> Result<Self> {
1783 let metadata = metadata.into();
1784 let len_with_nul = metadata.len().checked_add(1).ok_or_else(|| {
1785 Error::InvalidFrame("Frame metadata length overflow while adding terminator".into())
1786 })?;
1787 validate_metadata_len_with_nul(len_with_nul)?;
1788
1789 Ok(Self {
1790 inner: CString::new(metadata).map_err(Error::InvalidCString)?,
1791 })
1792 }
1793
1794 pub(crate) fn as_str(&self) -> &str {
1795 unsafe { str::from_utf8_unchecked(self.inner.as_bytes()) }
1797 }
1798
1799 pub(crate) fn into_string(self) -> String {
1800 self.inner
1801 .into_string()
1802 .expect("FrameMetadata validates UTF-8 at construction")
1803 }
1804
1805 pub(crate) fn as_cstr(&self) -> &CStr {
1806 self.inner.as_c_str()
1807 }
1808
1809 pub(crate) fn as_ptr(&self) -> *const c_char {
1810 self.inner.as_ptr()
1811 }
1812
1813 pub(crate) unsafe fn copy_from_raw_validated(
1821 p_metadata: *const c_char,
1822 metadata_layout: ValidatedFrameMetadata,
1823 ) -> Option<Self> {
1824 metadata_layout.len_with_nul?;
1825 debug_assert!(!p_metadata.is_null());
1826
1827 let mut bytes = Vec::with_capacity(metadata_layout.text_len + 1);
1828 if metadata_layout.text_len != 0 {
1829 bytes.extend_from_slice(unsafe {
1830 slice::from_raw_parts(p_metadata.cast::<u8>(), metadata_layout.text_len)
1831 });
1832 }
1833 bytes.push(0);
1834
1835 let inner = unsafe { CString::from_vec_with_nul_unchecked(bytes) };
1836 Some(Self { inner })
1837 }
1838}
1839
1840impl fmt::Debug for FrameMetadata {
1841 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1842 f.debug_tuple("FrameMetadata")
1843 .field(&self.as_str())
1844 .finish()
1845 }
1846}
1847
1848#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1854pub(crate) struct ValidatedFrameMetadata {
1855 pub(crate) len_with_nul: Option<NonZeroUsize>,
1858 pub(crate) text_len: usize,
1860}
1861
1862impl ValidatedFrameMetadata {
1863 pub(crate) fn from_cstr(metadata: Option<&CStr>) -> Result<Self> {
1865 let Some(metadata) = metadata else {
1866 return Ok(Self {
1867 len_with_nul: None,
1868 text_len: 0,
1869 });
1870 };
1871 let len = metadata.to_bytes_with_nul().len();
1872 validate_metadata_len_with_nul(len)?;
1873 metadata
1874 .to_str()
1875 .map_err(|e| Error::InvalidUtf8(e.to_string()))?;
1876 Ok(Self {
1877 len_with_nul: NonZeroUsize::new(len),
1878 text_len: len - 1,
1879 })
1880 }
1881}
1882
1883pub(crate) unsafe fn validate_frame_metadata(
1895 p_metadata: *const c_char,
1896) -> Result<ValidatedFrameMetadata> {
1897 if p_metadata.is_null() {
1898 return Ok(ValidatedFrameMetadata {
1899 len_with_nul: None,
1900 text_len: 0,
1901 });
1902 }
1903
1904 let metadata = p_metadata.cast::<u8>();
1905 for text_len in 0..MAX_METADATA_BYTES {
1906 let byte = unsafe { metadata.add(text_len).read() };
1907 if byte == 0 {
1908 if text_len > 0 {
1909 let payload = unsafe { slice::from_raw_parts(metadata, text_len) };
1910 str::from_utf8(payload).map_err(|err| Error::InvalidUtf8(err.to_string()))?;
1911 }
1912
1913 return Ok(ValidatedFrameMetadata {
1914 len_with_nul: NonZeroUsize::new(text_len + 1),
1915 text_len,
1916 });
1917 }
1918 }
1919
1920 Err(Error::InvalidFrame(format!(
1921 "Frame metadata is missing a NUL terminator within {MAX_METADATA_BYTES} bytes"
1922 )))
1923}
1924
1925pub(crate) unsafe fn frame_metadata_str<'a>(
1932 p_metadata: *const c_char,
1933 metadata_layout: ValidatedFrameMetadata,
1934) -> Option<&'a str> {
1935 metadata_layout.len_with_nul?;
1936 debug_assert!(!p_metadata.is_null());
1937
1938 if metadata_layout.text_len == 0 {
1939 return Some("");
1940 }
1941
1942 let bytes = unsafe { slice::from_raw_parts(p_metadata.cast::<u8>(), metadata_layout.text_len) };
1943 Some(unsafe { str::from_utf8_unchecked(bytes) })
1944}
1945
1946#[cfg(feature = "image-encoding")]
1966#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1967#[non_exhaustive]
1968pub enum ImageFormat {
1969 Png,
1971 Jpeg(u8),
1973}
1974
1975#[cfg(feature = "image-encoding")]
1976#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1977enum ImageChannelOrder {
1978 Rgba,
1979 Bgra,
1980}
1981
1982#[cfg(feature = "image-encoding")]
1983impl ImageChannelOrder {
1984 fn red_index(self) -> usize {
1985 match self {
1986 Self::Rgba => 0,
1987 Self::Bgra => 2,
1988 }
1989 }
1990
1991 fn blue_index(self) -> usize {
1992 match self {
1993 Self::Rgba => 2,
1994 Self::Bgra => 0,
1995 }
1996 }
1997}
1998
1999#[cfg(feature = "image-encoding")]
2000#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2001enum ImageAlphaPolicy {
2002 Preserve,
2003 Opaque,
2004}
2005
2006#[cfg(feature = "image-encoding")]
2007#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2008struct ImagePixelFormat {
2009 channel_order: ImageChannelOrder,
2010 alpha_policy: ImageAlphaPolicy,
2011}
2012
2013#[cfg(feature = "image-encoding")]
2014impl ImagePixelFormat {
2015 const BYTES_PER_PIXEL: usize = 4;
2016
2017 fn from_pixel_format(pixel_format: PixelFormat) -> Result<Self> {
2018 match pixel_format {
2019 PixelFormat::RGBA => Ok(Self {
2020 channel_order: ImageChannelOrder::Rgba,
2021 alpha_policy: ImageAlphaPolicy::Preserve,
2022 }),
2023 PixelFormat::BGRA => Ok(Self {
2024 channel_order: ImageChannelOrder::Bgra,
2025 alpha_policy: ImageAlphaPolicy::Preserve,
2026 }),
2027 PixelFormat::RGBX => Ok(Self {
2028 channel_order: ImageChannelOrder::Rgba,
2029 alpha_policy: ImageAlphaPolicy::Opaque,
2030 }),
2031 PixelFormat::BGRX => Ok(Self {
2032 channel_order: ImageChannelOrder::Bgra,
2033 alpha_policy: ImageAlphaPolicy::Opaque,
2034 }),
2035 _ => Err(Error::InvalidFrame(format!(
2036 "Unsupported format for image encoding: {pixel_format:?}. Only RGBA/RGBX/BGRA/BGRX are supported."
2037 ))),
2038 }
2039 }
2040
2041 fn can_borrow_png_rows(self) -> bool {
2042 self.channel_order == ImageChannelOrder::Rgba
2043 && self.alpha_policy == ImageAlphaPolicy::Preserve
2044 }
2045
2046 fn rgba_pixel(self, pixel: &[u8]) -> [u8; 4] {
2047 [
2048 pixel[self.channel_order.red_index()],
2049 pixel[1],
2050 pixel[self.channel_order.blue_index()],
2051 match self.alpha_policy {
2052 ImageAlphaPolicy::Preserve => pixel[3],
2053 ImageAlphaPolicy::Opaque => 255,
2054 },
2055 ]
2056 }
2057
2058 fn rgb_pixel(self, pixel: &[u8]) -> [u8; 3] {
2059 [
2060 pixel[self.channel_order.red_index()],
2061 pixel[1],
2062 pixel[self.channel_order.blue_index()],
2063 ]
2064 }
2065}
2066
2067#[cfg(feature = "image-encoding")]
2068#[derive(Debug)]
2069struct ImagePixelSource<'a> {
2070 plane: VideoPlane<'a>,
2071 width: usize,
2072 height: usize,
2073 pixel_format: ImagePixelFormat,
2074}
2075
2076#[cfg(feature = "image-encoding")]
2077impl<'a> ImagePixelSource<'a> {
2078 fn encode_png(&self) -> Result<Vec<u8>> {
2079 use png::{BitDepth, ColorType, Encoder};
2080
2081 use std::io::Write;
2082
2083 let width = u32::try_from(self.width).map_err(|_| {
2084 Error::InvalidFrame(format!("PNG width {} exceeds u32 range", self.width))
2085 })?;
2086 let height = u32::try_from(self.height).map_err(|_| {
2087 Error::InvalidFrame(format!("PNG height {} exceeds u32 range", self.height))
2088 })?;
2089 let mut conversion = self.png_conversion_row()?;
2090
2091 let mut png_data = Vec::new();
2093 let mut encoder = Encoder::new(&mut png_data, width, height);
2094 encoder.set_color(ColorType::Rgba);
2095 encoder.set_depth(BitDepth::Eight);
2096
2097 let png_error = |e| Error::InvalidFrame(format!("PNG encoding failed: {e}"));
2098 let mut writer = encoder.write_header().map_err(png_error)?;
2099 {
2100 let mut stream = writer.stream_writer().map_err(png_error)?;
2101 for row in self.plane.rows() {
2102 let rgba = if self.pixel_format.can_borrow_png_rows() {
2103 row
2104 } else {
2105 for (out, pixel) in conversion
2106 .as_chunks_mut::<4>()
2107 .0
2108 .iter_mut()
2109 .zip(row.as_chunks::<{ ImagePixelFormat::BYTES_PER_PIXEL }>().0)
2110 {
2111 *out = self.pixel_format.rgba_pixel(pixel);
2112 }
2113 &conversion
2114 };
2115 stream
2116 .write_all(rgba)
2117 .map_err(|e| Error::InvalidFrame(format!("PNG encoding failed: {e}")))?;
2118 }
2119 stream.finish().map_err(png_error)?;
2120 }
2121 writer.finish().map_err(png_error)?;
2122
2123 Ok(png_data)
2124 }
2125
2126 fn encode_jpeg(&self, quality: u8) -> Result<Vec<u8>> {
2127 use jpeg_encoder::Encoder as JpegEncoder;
2128
2129 let (width, height) = self.jpeg_dimensions(quality)?;
2130
2131 let mut jpeg_data = Vec::new();
2133 let encoder = JpegEncoder::new(&mut jpeg_data, quality);
2134 encoder
2135 .encode_image(JpegRows {
2136 source: self,
2137 width,
2138 height,
2139 })
2140 .map_err(|e| Error::InvalidFrame(format!("JPEG encoding failed: {e}")))?;
2141
2142 Ok(jpeg_data)
2143 }
2144
2145 fn encode_data_url(&self, format: ImageFormat) -> Result<String> {
2146 use base64::{engine::general_purpose::STANDARD, Engine};
2147
2148 let (mime_type, image_bytes) = match format {
2149 ImageFormat::Png => ("image/png", self.encode_png()?),
2150 ImageFormat::Jpeg(quality) => ("image/jpeg", self.encode_jpeg(quality)?),
2151 };
2152
2153 let base64_data = STANDARD.encode(&image_bytes);
2154 Ok(format!("data:{mime_type};base64,{base64_data}"))
2155 }
2156
2157 fn new(layout: ValidatedVideoLayout, data: &'a [u8]) -> Result<Self> {
2158 if data.len() != layout.data_len_bytes {
2159 return Err(Error::InvalidFrame(format!(
2160 "Video data length {}, expected {} bytes for validated layout",
2161 data.len(),
2162 layout.data_len_bytes
2163 )));
2164 }
2165
2166 let plane = unsafe { VideoPlane::new(data.as_ptr(), layout.plane_layouts[0]) };
2169 Self::from_plane(layout, plane)
2170 }
2171
2172 fn from_plane(layout: ValidatedVideoLayout, plane: VideoPlane<'a>) -> Result<Self> {
2173 let pixel_format = ImagePixelFormat::from_pixel_format(layout.pixel_format)?;
2174 Ok(Self {
2175 plane,
2176 width: layout.width as usize,
2177 height: layout.height as usize,
2178 pixel_format,
2179 })
2180 }
2181
2182 fn png_conversion_row(&self) -> Result<Vec<u8>> {
2183 if self.pixel_format.can_borrow_png_rows() {
2184 return Ok(Vec::new());
2185 }
2186 let len = self.width.checked_mul(4).ok_or_else(|| {
2187 Error::InvalidFrame(format!("PNG row size overflow: {}x4", self.width))
2188 })?;
2189 Ok(vec![0; len])
2190 }
2191
2192 fn jpeg_dimensions(&self, quality: u8) -> Result<(u16, u16)> {
2193 if !(1..=100).contains(&quality) {
2194 return Err(Error::InvalidFrame(format!(
2195 "JPEG quality must be in 1..=100, got {quality}"
2196 )));
2197 }
2198
2199 let width = u16::try_from(self.width).map_err(|_| {
2200 Error::InvalidFrame(format!(
2201 "JPEG width {} exceeds maximum supported value {}",
2202 self.width,
2203 u16::MAX
2204 ))
2205 })?;
2206 let height = u16::try_from(self.height).map_err(|_| {
2207 Error::InvalidFrame(format!(
2208 "JPEG height {} exceeds maximum supported value {}",
2209 self.height,
2210 u16::MAX
2211 ))
2212 })?;
2213
2214 Ok((width, height))
2215 }
2216}
2217
2218#[cfg(feature = "image-encoding")]
2221struct JpegRows<'s, 'a> {
2222 source: &'s ImagePixelSource<'a>,
2223 width: u16,
2224 height: u16,
2225}
2226
2227#[cfg(feature = "image-encoding")]
2228impl jpeg_encoder::ImageBuffer for JpegRows<'_, '_> {
2229 fn get_jpeg_color_type(&self) -> jpeg_encoder::JpegColorType {
2230 jpeg_encoder::JpegColorType::Ycbcr
2231 }
2232
2233 fn width(&self) -> u16 {
2234 self.width
2235 }
2236 fn height(&self) -> u16 {
2237 self.height
2238 }
2239
2240 fn fill_buffers(&self, row: u16, buffers: &mut [Vec<u8>; 4]) {
2241 let row = self
2242 .source
2243 .plane
2244 .row(usize::from(row))
2245 .expect("codec row is in bounds");
2246 for chunk in row.as_chunks::<64>().0 {
2249 let mut components = [[0; 16]; 3];
2250 for (i, pixel) in chunk.as_chunks::<4>().0.iter().enumerate() {
2251 let [r, g, b] = self.source.pixel_format.rgb_pixel(pixel);
2252 let (y, cb, cr) = jpeg_encoder::rgb_to_ycbcr(r, g, b);
2253 components[0][i] = y;
2254 components[1][i] = cb;
2255 components[2][i] = cr;
2256 }
2257 for (buffer, values) in buffers.iter_mut().zip(components) {
2258 buffer.extend_from_slice(&values);
2259 }
2260 }
2261 for pixel in row.as_chunks::<64>().1.as_chunks::<4>().0 {
2262 let [r, g, b] = self.source.pixel_format.rgb_pixel(pixel);
2263 let (y, cb, cr) = jpeg_encoder::rgb_to_ycbcr(r, g, b);
2264 buffers[0].push(y);
2265 buffers[1].push(cb);
2266 buffers[2].push(cr);
2267 }
2268 }
2269}
2270
2271#[derive(Debug, Clone, Copy)]
2282pub(crate) struct ValidatedVideoLayout {
2283 pub width: i32,
2285 pub height: i32,
2287 pub pixel_format: PixelFormat,
2289 pub data_len_bytes: usize,
2291 payload_len_bytes: usize,
2293 plane_layouts: [VideoPlaneLayout; 3],
2294 plane_count: usize,
2295 pub line_stride_or_size: LineStrideOrSize,
2297}
2298
2299impl ValidatedVideoLayout {
2300 pub(crate) fn new_uncompressed(
2301 pixel_format: PixelFormat,
2302 width: i32,
2303 height: i32,
2304 line_stride: Option<i32>,
2305 ) -> Result<Self> {
2306 validate_video_dimensions_for_format(pixel_format, width, height)?;
2307
2308 let width_usize = usize::try_from(width)
2309 .map_err(|_| Error::InvalidFrame(format!("Invalid width value: {width}")))?;
2310 let height_usize = usize::try_from(height)
2311 .map_err(|_| Error::InvalidFrame(format!("Invalid height value: {height}")))?;
2312
2313 let min_stride = min_video_line_stride_checked(pixel_format, width_usize)?;
2314 let line_stride_usize = match line_stride {
2315 Some(0) => min_stride,
2316 Some(stride) => {
2317 if stride <= 0 {
2318 return Err(Error::InvalidFrame(format!(
2319 "Uncompressed video frame has invalid line_stride_in_bytes: {stride}"
2320 )));
2321 }
2322
2323 let stride_usize = usize::try_from(stride).map_err(|_| {
2324 Error::InvalidFrame(format!("Invalid line_stride_in_bytes value: {stride}"))
2325 })?;
2326
2327 if stride_usize < min_stride {
2328 return Err(Error::InvalidFrame(format!(
2329 "Video line_stride_in_bytes {stride} is smaller than minimum row size {min_stride} for {pixel_format:?} width {width}"
2330 )));
2331 }
2332
2333 stride_usize
2334 }
2335 None => min_stride,
2336 };
2337
2338 if pixel_format.info().is_planar_420() && !line_stride_usize.is_multiple_of(2) {
2339 return Err(Error::InvalidFrame(format!(
2340 "Planar 4:2:0 video frame has odd line_stride_in_bytes: {line_stride_usize}"
2341 )));
2342 }
2343
2344 let (plane_layouts, plane_count, data_len_bytes, payload_len_bytes) = video_plane_layouts(
2345 pixel_format.info(),
2346 line_stride_usize,
2347 min_stride,
2348 height_usize,
2349 )?;
2350 validate_video_data_len(data_len_bytes)?;
2351
2352 let line_stride_i32 = i32::try_from(line_stride_usize).map_err(|_| {
2353 Error::InvalidFrame(format!(
2354 "Video line stride {line_stride_usize} exceeds i32 range"
2355 ))
2356 })?;
2357
2358 Ok(Self {
2359 width,
2360 height,
2361 pixel_format,
2362 data_len_bytes,
2363 payload_len_bytes,
2364 plane_layouts,
2365 plane_count,
2366 line_stride_or_size: LineStrideOrSize::LineStrideBytes(line_stride_i32),
2367 })
2368 }
2369
2370 fn planes(&self) -> &[VideoPlaneLayout] {
2371 &self.plane_layouts[..self.plane_count]
2372 }
2373
2374 fn is_contiguous(&self) -> bool {
2375 let last = self.planes().last().expect("validated video has a plane");
2379 last.offset + (last.row_count - 1) * last.row_stride + last.active_row_bytes
2380 == self.payload_len_bytes
2381 }
2382}
2383
2384pub(crate) fn validate_video_layout(raw: &NDIlib_video_frame_v2_t) -> Result<ValidatedVideoLayout> {
2400 if raw.p_data.is_null() {
2401 return Err(Error::InvalidFrame(
2402 "Video frame has null data pointer".into(),
2403 ));
2404 }
2405
2406 validate_video_frame_metadata(raw.frame_rate_N, raw.frame_rate_D, raw.picture_aspect_ratio)?;
2407
2408 #[allow(clippy::unnecessary_cast)]
2409 ScanType::try_from(raw.frame_format_type as u32).map_err(|_| {
2410 Error::InvalidFrame(format!(
2411 "Unknown scan type: 0x{:08X}",
2412 raw.frame_format_type
2413 ))
2414 })?;
2415
2416 #[allow(clippy::unnecessary_cast)] let pixel_format = PixelFormat::try_from(raw.FourCC as u32).map_err(|_| {
2418 Error::InvalidFrame(format!("Unknown pixel format FourCC: 0x{:08X}", raw.FourCC))
2419 })?;
2420
2421 let line_stride = unsafe { raw.__bindgen_anon_1.line_stride_in_bytes };
2422 ValidatedVideoLayout::new_uncompressed(pixel_format, raw.xres, raw.yres, Some(line_stride))
2423}
2424
2425fn validate_video_dimensions_for_format(
2426 pixel_format: PixelFormat,
2427 width: i32,
2428 height: i32,
2429) -> Result<()> {
2430 validate_video_width(width)?;
2431 if height <= 0 {
2432 return Err(Error::InvalidFrame(format!(
2433 "Video frame has invalid height: {height}"
2434 )));
2435 }
2436
2437 if pixel_format.info().is_planar_420() && height % 2 != 0 {
2438 return Err(Error::InvalidFrame(format!(
2439 "Planar 4:2:0 video frames require even dimensions, got {}x{}",
2440 width, height
2441 )));
2442 }
2443
2444 Ok(())
2445}
2446
2447fn validate_video_width(width: i32) -> Result<()> {
2448 if width <= 0 {
2449 return Err(Error::InvalidFrame(format!(
2450 "Video frame has invalid width: {width}"
2451 )));
2452 }
2453
2454 if width % 2 != 0 {
2457 return Err(Error::InvalidFrame(format!(
2458 "NDI video frames require even width, got {width}"
2459 )));
2460 }
2461
2462 Ok(())
2463}
2464
2465pub(crate) fn validate_video_frame_metadata(
2466 frame_rate_n: i32,
2467 frame_rate_d: i32,
2468 picture_aspect_ratio: f32,
2469) -> Result<()> {
2470 if frame_rate_n <= 0 {
2471 return Err(Error::InvalidFrame(format!(
2472 "Video frame has invalid frame rate numerator: {frame_rate_n}"
2473 )));
2474 }
2475 if frame_rate_d <= 0 {
2476 return Err(Error::InvalidFrame(format!(
2477 "Video frame has invalid frame rate denominator: {frame_rate_d}"
2478 )));
2479 }
2480 if !picture_aspect_ratio.is_finite() || picture_aspect_ratio < 0.0 {
2481 return Err(Error::InvalidFrame(format!(
2482 "Video frame has invalid picture aspect ratio: {picture_aspect_ratio}"
2483 )));
2484 }
2485
2486 Ok(())
2487}
2488
2489fn validate_video_data_len(data_len_bytes: usize) -> Result<()> {
2490 if data_len_bytes == 0 {
2491 return Err(Error::InvalidFrame(
2492 "Video frame has zero calculated size".into(),
2493 ));
2494 }
2495
2496 if data_len_bytes > MAX_VIDEO_BYTES {
2497 return Err(Error::InvalidFrame(format!(
2498 "Video frame exceeds maximum size: {} bytes > {} bytes",
2499 data_len_bytes, MAX_VIDEO_BYTES
2500 )));
2501 }
2502
2503 Ok(())
2504}
2505
2506fn min_video_line_stride_checked(pixel_format: PixelFormat, width: usize) -> Result<usize> {
2507 width
2508 .checked_mul(pixel_format.info().bytes_per_pixel() as usize)
2509 .ok_or_else(|| {
2510 Error::InvalidFrame(format!(
2511 "Video line stride overflow for {:?} width {}",
2512 pixel_format, width
2513 ))
2514 })
2515}
2516
2517#[cfg(test)]
2519fn calculate_buffer_len_checked(
2520 pixel_format: PixelFormat,
2521 y_stride: usize,
2522 height: usize,
2523) -> Result<usize> {
2524 calculate_buffer_len_for_info_checked(pixel_format.info(), y_stride, height)
2525}
2526
2527fn calculate_buffer_len_for_info_checked(
2528 info: PixelFormatInfo,
2529 y_stride: usize,
2530 height: usize,
2531) -> Result<usize> {
2532 video_plane_layouts(info, y_stride, y_stride, height).map(|(_, _, storage, _)| storage)
2533}
2534
2535#[derive(Debug, Clone, Copy, Default)]
2536struct VideoPlaneLayout {
2537 offset: usize,
2538 row_stride: usize,
2539 active_row_bytes: usize,
2540 row_count: usize,
2541}
2542
2543fn video_plane_layouts(
2547 info: PixelFormatInfo,
2548 stride: usize,
2549 active: usize,
2550 height: usize,
2551) -> Result<([VideoPlaneLayout; 3], usize, usize, usize)> {
2552 let mut dimensions = [(stride, active, height); 3];
2553 let count = match info.category() {
2554 FormatCategory::Packed => 1,
2555 FormatCategory::Planar420 => {
2556 if !height.is_multiple_of(2) || !stride.is_multiple_of(2) {
2557 return Err(Error::InvalidFrame(
2558 "Planar 4:2:0 video frames require even height and stride".into(),
2559 ));
2560 }
2561 dimensions[1] = (stride / 2, active / 2, height / 2);
2562 dimensions[2] = dimensions[1];
2563 3
2564 }
2565 FormatCategory::SemiPlanar420 => {
2566 if !height.is_multiple_of(2) {
2567 return Err(Error::InvalidFrame(
2568 "Semi-planar 4:2:0 video frames require even height".into(),
2569 ));
2570 }
2571 dimensions[1] = (stride, active, height / 2);
2572 2
2573 }
2574 FormatCategory::PackedAlpha => {
2575 if !stride.is_multiple_of(2) {
2576 return Err(Error::InvalidFrame(
2577 "UYVA video frames require even stride".into(),
2578 ));
2579 }
2580 dimensions[1] = (stride / 2, active / 2, height);
2581 2
2582 }
2583 FormatCategory::SemiPlanar422 => 2,
2584 FormatCategory::SemiPlanar422Alpha => 3,
2585 };
2586 let mut planes = [VideoPlaneLayout::default(); 3];
2587 let mut storage = 0usize;
2588 let mut payload = 0usize;
2589 for (plane, &(row_stride, active_row_bytes, row_count)) in
2590 planes.iter_mut().zip(&dimensions[..count])
2591 {
2592 *plane = VideoPlaneLayout {
2593 offset: storage,
2594 row_stride,
2595 active_row_bytes,
2596 row_count,
2597 };
2598 storage = row_stride
2599 .checked_mul(row_count)
2600 .and_then(|n| storage.checked_add(n))
2601 .ok_or_else(|| Error::InvalidFrame("Video buffer size overflow".into()))?;
2602 payload = active_row_bytes
2603 .checked_mul(row_count)
2604 .and_then(|n| payload.checked_add(n))
2605 .ok_or_else(|| Error::InvalidFrame("Video payload size overflow".into()))?;
2606 }
2607 Ok((planes, count, storage, payload))
2608}
2609
2610#[derive(Debug, Clone, Copy)]
2617pub(crate) struct ValidatedAudioLayout {
2618 pub format: Option<AudioFormat>,
2620 pub sample_rate: i32,
2622 pub no_channels: usize,
2624 pub no_samples: usize,
2626 pub channel_stride_in_bytes: i32,
2628 pub channel_stride_samples: usize,
2630 pub sample_count: usize,
2634}
2635
2636impl ValidatedAudioLayout {
2637 pub(crate) fn is_empty(self) -> bool {
2638 self.sample_count == 0
2639 }
2640
2641 pub(crate) fn format(self) -> Option<AudioFormat> {
2642 self.format
2643 }
2644
2645 pub(crate) fn channel_range(self, channel: usize) -> Option<std::ops::Range<usize>> {
2646 if self.is_empty() || channel >= self.no_channels {
2647 return None;
2648 }
2649
2650 let start = channel.checked_mul(self.channel_stride_samples)?;
2651 let end = start.checked_add(self.no_samples)?;
2652
2653 (end <= self.sample_count).then_some(start..end)
2654 }
2655}
2656
2657fn validate_outbound_audio_layout(
2658 sample_rate: i32,
2659 no_channels: i32,
2660 no_samples: i32,
2661 format: AudioFormat,
2662) -> Result<ValidatedAudioLayout> {
2663 if sample_rate <= 0 {
2664 return Err(Error::InvalidFrame(format!(
2665 "Invalid sample rate: {sample_rate}"
2666 )));
2667 }
2668 validate_audio_format(format.into())?;
2669 let packed = validate_packed_audio_dimensions(no_channels, no_samples)?;
2670
2671 Ok(ValidatedAudioLayout {
2672 format: Some(format),
2673 sample_rate,
2674 no_channels: packed.no_channels,
2675 no_samples: packed.no_samples,
2676 channel_stride_in_bytes: packed.channel_stride_in_bytes,
2677 channel_stride_samples: packed.no_samples,
2678 sample_count: packed.sample_count,
2679 })
2680}
2681
2682pub(crate) struct PackedAudioDimensions {
2685 no_channels: usize,
2686 no_samples: usize,
2687 channel_stride_in_bytes: i32,
2688 sample_count: usize,
2689}
2690
2691pub(crate) fn validate_packed_audio_dimensions(
2692 no_channels: i32,
2693 no_samples: i32,
2694) -> Result<PackedAudioDimensions> {
2695 if no_channels <= 0 {
2696 return Err(Error::InvalidFrame(format!(
2697 "Invalid number of channels: {no_channels}"
2698 )));
2699 }
2700 if no_samples <= 0 {
2701 return Err(Error::InvalidFrame(format!(
2702 "Invalid number of samples: {no_samples}"
2703 )));
2704 }
2705
2706 let no_channels = usize::try_from(no_channels)
2707 .map_err(|_| Error::InvalidFrame(format!("Invalid no_channels value: {no_channels}")))?;
2708 let no_samples = usize::try_from(no_samples)
2709 .map_err(|_| Error::InvalidFrame(format!("Invalid no_samples value: {no_samples}")))?;
2710
2711 let channel_stride_bytes = no_samples
2712 .checked_mul(std::mem::size_of::<f32>())
2713 .ok_or_else(|| {
2714 Error::InvalidFrame(format!(
2715 "Audio channel stride overflow: {} samples × {} bytes",
2716 no_samples,
2717 std::mem::size_of::<f32>()
2718 ))
2719 })?;
2720 let channel_stride_in_bytes = i32::try_from(channel_stride_bytes).map_err(|_| {
2721 Error::InvalidFrame(format!(
2722 "Audio channel stride {channel_stride_bytes} exceeds i32 range"
2723 ))
2724 })?;
2725
2726 let sample_count = no_channels.checked_mul(no_samples).ok_or_else(|| {
2727 Error::InvalidFrame(format!(
2728 "Audio sample count overflow: {no_channels} channels × {no_samples} samples"
2729 ))
2730 })?;
2731 let byte_size = sample_count
2732 .checked_mul(std::mem::size_of::<f32>())
2733 .ok_or_else(|| {
2734 Error::InvalidFrame(format!(
2735 "Audio byte size overflow: {} samples × {} bytes",
2736 sample_count,
2737 std::mem::size_of::<f32>()
2738 ))
2739 })?;
2740
2741 if byte_size > MAX_AUDIO_BYTES {
2742 return Err(Error::InvalidFrame(format!(
2743 "Audio frame exceeds maximum size: {} bytes > {} bytes",
2744 byte_size, MAX_AUDIO_BYTES
2745 )));
2746 }
2747
2748 Ok(PackedAudioDimensions {
2749 no_channels,
2750 no_samples,
2751 channel_stride_in_bytes,
2752 sample_count,
2753 })
2754}
2755
2756pub(crate) fn validate_audio_layout(raw: &NDIlib_audio_frame_v3_t) -> Result<ValidatedAudioLayout> {
2773 validate_audio_layout_inner(raw, false)
2774}
2775
2776pub(crate) fn validate_audio_layout_allow_empty(
2779 raw: &NDIlib_audio_frame_v3_t,
2780) -> Result<ValidatedAudioLayout> {
2781 validate_audio_layout_inner(raw, true)
2782}
2783
2784fn validate_audio_layout_inner(
2785 raw: &NDIlib_audio_frame_v3_t,
2786 allow_empty: bool,
2787) -> Result<ValidatedAudioLayout> {
2788 if raw.no_samples == 0 {
2789 if allow_empty {
2790 return validate_empty_audio_layout(raw);
2791 }
2792
2793 return Err(Error::InvalidFrame("Invalid number of samples: 0".into()));
2794 }
2795
2796 if raw.p_data.is_null() {
2797 return Err(Error::InvalidFrame(
2798 "Audio frame has null data pointer".into(),
2799 ));
2800 }
2801
2802 if !raw.p_data.cast::<f32>().is_aligned() {
2803 return Err(Error::InvalidFrame(
2804 "Audio data pointer is not aligned for f32".into(),
2805 ));
2806 }
2807
2808 if raw.sample_rate <= 0 {
2809 return Err(Error::InvalidFrame(format!(
2810 "Invalid sample rate: {}",
2811 raw.sample_rate
2812 )));
2813 }
2814
2815 if raw.no_channels <= 0 {
2816 return Err(Error::InvalidFrame(format!(
2817 "Invalid number of channels: {}",
2818 raw.no_channels
2819 )));
2820 }
2821
2822 if raw.no_samples <= 0 {
2823 return Err(Error::InvalidFrame(format!(
2824 "Invalid number of samples: {}",
2825 raw.no_samples
2826 )));
2827 }
2828
2829 let format = validate_audio_format(raw.FourCC)?;
2830 let channel_stride_in_bytes = unsafe { raw.__bindgen_anon_1.channel_stride_in_bytes };
2831 if channel_stride_in_bytes <= 0 {
2832 return Err(Error::InvalidFrame(format!(
2833 "Invalid channel_stride_in_bytes: {}",
2834 channel_stride_in_bytes
2835 )));
2836 }
2837
2838 let no_samples = usize::try_from(raw.no_samples).map_err(|_| {
2840 Error::InvalidFrame(format!("Invalid no_samples value: {}", raw.no_samples))
2841 })?;
2842
2843 let no_channels = usize::try_from(raw.no_channels).map_err(|_| {
2844 Error::InvalidFrame(format!("Invalid no_channels value: {}", raw.no_channels))
2845 })?;
2846
2847 let channel_stride_bytes = usize::try_from(channel_stride_in_bytes).map_err(|_| {
2848 Error::InvalidFrame(format!(
2849 "Invalid channel_stride_in_bytes value: {}",
2850 channel_stride_in_bytes
2851 ))
2852 })?;
2853
2854 if channel_stride_bytes % std::mem::size_of::<f32>() != 0 {
2855 return Err(Error::InvalidFrame(format!(
2856 "channel_stride_in_bytes must be a multiple of {}, got {}",
2857 std::mem::size_of::<f32>(),
2858 channel_stride_in_bytes
2859 )));
2860 }
2861
2862 let minimum_channel_stride = no_samples
2863 .checked_mul(std::mem::size_of::<f32>())
2864 .ok_or_else(|| {
2865 Error::InvalidFrame(format!(
2866 "Audio channel stride overflow: {} samples × {} bytes",
2867 no_samples,
2868 std::mem::size_of::<f32>()
2869 ))
2870 })?;
2871
2872 if channel_stride_bytes < minimum_channel_stride {
2873 return Err(Error::InvalidFrame(format!(
2874 "channel_stride_in_bytes {} is smaller than one channel of audio samples {}",
2875 channel_stride_in_bytes, minimum_channel_stride
2876 )));
2877 }
2878
2879 let channel_stride_samples = channel_stride_bytes / std::mem::size_of::<f32>();
2880 let last_channel_offset = no_channels
2881 .checked_sub(1)
2882 .and_then(|last| last.checked_mul(channel_stride_samples))
2883 .ok_or_else(|| {
2884 Error::InvalidFrame(format!(
2885 "Audio channel offset overflow: {} channels × {} stride samples",
2886 no_channels, channel_stride_samples
2887 ))
2888 })?;
2889
2890 let sample_count = last_channel_offset.checked_add(no_samples).ok_or_else(|| {
2891 Error::InvalidFrame(format!(
2892 "Audio backing sample count overflow: channel offset {} + {} samples",
2893 last_channel_offset, no_samples
2894 ))
2895 })?;
2896
2897 let byte_size = sample_count
2899 .checked_mul(std::mem::size_of::<f32>())
2900 .ok_or_else(|| {
2901 Error::InvalidFrame(format!(
2902 "Audio byte size overflow: {} samples × {} bytes",
2903 sample_count,
2904 std::mem::size_of::<f32>()
2905 ))
2906 })?;
2907
2908 if byte_size > MAX_AUDIO_BYTES {
2909 return Err(Error::InvalidFrame(format!(
2910 "Audio frame exceeds maximum size: {} bytes > {} bytes",
2911 byte_size, MAX_AUDIO_BYTES
2912 )));
2913 }
2914
2915 Ok(ValidatedAudioLayout {
2916 format: Some(format),
2917 sample_rate: raw.sample_rate,
2918 no_channels,
2919 no_samples,
2920 channel_stride_in_bytes,
2921 channel_stride_samples,
2922 sample_count,
2923 })
2924}
2925
2926fn validate_empty_audio_layout(raw: &NDIlib_audio_frame_v3_t) -> Result<ValidatedAudioLayout> {
2927 if !raw.p_data.is_null() {
2928 return Err(Error::InvalidFrame(
2929 "Zero-length audio frame has non-null data pointer".into(),
2930 ));
2931 }
2932
2933 if raw.sample_rate < 0 {
2934 return Err(Error::InvalidFrame(format!(
2935 "Invalid sample rate: {}",
2936 raw.sample_rate
2937 )));
2938 }
2939
2940 if raw.no_channels < 0 {
2941 return Err(Error::InvalidFrame(format!(
2942 "Invalid number of channels: {}",
2943 raw.no_channels
2944 )));
2945 }
2946
2947 let channel_stride_in_bytes = unsafe { raw.__bindgen_anon_1.channel_stride_in_bytes };
2948 if channel_stride_in_bytes != 0 {
2949 return Err(Error::InvalidFrame(format!(
2950 "Zero-length audio frame has non-zero channel_stride_in_bytes: {}",
2951 channel_stride_in_bytes
2952 )));
2953 }
2954
2955 let no_source = raw.sample_rate == 0 && raw.no_channels == 0;
2956 let query_format = raw.sample_rate > 0 && raw.no_channels > 0;
2957 if !no_source && !query_format {
2958 return Err(Error::InvalidFrame(format!(
2959 "Invalid zero-length audio query state: sample_rate={}, no_channels={}",
2960 raw.sample_rate, raw.no_channels
2961 )));
2962 }
2963
2964 let format = if no_source && raw.FourCC == 0 {
2965 None
2966 } else {
2967 Some(validate_audio_format(raw.FourCC)?)
2968 };
2969
2970 let no_channels = usize::try_from(raw.no_channels).map_err(|_| {
2971 Error::InvalidFrame(format!("Invalid no_channels value: {}", raw.no_channels))
2972 })?;
2973
2974 Ok(ValidatedAudioLayout {
2975 format,
2976 sample_rate: raw.sample_rate,
2977 no_channels,
2978 no_samples: 0,
2979 channel_stride_in_bytes: 0,
2980 channel_stride_samples: 0,
2981 sample_count: 0,
2982 })
2983}
2984
2985fn validate_audio_format(fourcc: NDIlib_FourCC_audio_type_e) -> Result<AudioFormat> {
2986 match fourcc {
2987 NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP => Ok(AudioFormat::FLTP),
2988 _ => Err(Error::InvalidFrame(format!(
2989 "Unknown audio format FourCC: 0x{:08X}",
2990 fourcc
2991 ))),
2992 }
2993}
2994
2995#[derive(Debug, Clone, Copy)]
3022pub struct VideoPlane<'a> {
3023 data: *const u8,
3024 layout: VideoPlaneLayout,
3025 lifetime: PhantomData<&'a [u8]>,
3026}
3027
3028unsafe impl Send for VideoPlane<'_> {}
3039unsafe impl Sync for VideoPlane<'_> {}
3041
3042impl<'a> VideoPlane<'a> {
3043 unsafe fn new(data: *const u8, layout: VideoPlaneLayout) -> Self {
3046 Self {
3047 data,
3048 layout,
3049 lifetime: PhantomData,
3050 }
3051 }
3052
3053 pub fn row_stride(&self) -> usize {
3055 self.layout.row_stride
3056 }
3057
3058 pub fn active_row_bytes(&self) -> usize {
3060 self.layout.active_row_bytes
3061 }
3062
3063 pub fn row_count(&self) -> usize {
3065 self.layout.row_count
3066 }
3067
3068 pub fn row(&self, index: usize) -> Option<&'a [u8]> {
3070 if index >= self.layout.row_count {
3071 return None;
3072 }
3073 Some(unsafe {
3077 slice::from_raw_parts(
3078 self.data
3079 .add(self.layout.offset + index * self.layout.row_stride),
3080 self.layout.active_row_bytes,
3081 )
3082 })
3083 }
3084
3085 pub fn rows(&self) -> VideoRows<'a> {
3087 VideoRows {
3088 plane: *self,
3089 indices: 0..self.row_count(),
3090 }
3091 }
3092
3093 pub fn data(&self) -> Option<&'a [u8]> {
3096 if self.row_count() != 1 && self.row_stride() != self.active_row_bytes() {
3097 return None;
3098 }
3099 Some(unsafe {
3102 slice::from_raw_parts(
3103 self.data.add(self.layout.offset),
3104 self.row_count() * self.active_row_bytes(),
3105 )
3106 })
3107 }
3108}
3109
3110#[derive(Debug, Clone)]
3113pub struct VideoRows<'a> {
3114 plane: VideoPlane<'a>,
3115 indices: std::ops::Range<usize>,
3116}
3117
3118impl<'a> Iterator for VideoRows<'a> {
3119 type Item = &'a [u8];
3120 fn next(&mut self) -> Option<Self::Item> {
3121 self.plane.row(self.indices.next()?)
3122 }
3123 fn size_hint(&self) -> (usize, Option<usize>) {
3124 self.indices.size_hint()
3125 }
3126}
3127impl DoubleEndedIterator for VideoRows<'_> {
3128 fn next_back(&mut self) -> Option<Self::Item> {
3129 self.plane.row(self.indices.next_back()?)
3130 }
3131}
3132impl ExactSizeIterator for VideoRows<'_> {}
3133impl std::iter::FusedIterator for VideoRows<'_> {}
3134
3135#[derive(Debug, Clone)]
3138pub struct VideoPlanes<'a> {
3139 data: *const u8,
3140 layouts: slice::Iter<'a, VideoPlaneLayout>,
3141}
3142unsafe impl Send for VideoPlanes<'_> {}
3148unsafe impl Sync for VideoPlanes<'_> {}
3150impl<'a> Iterator for VideoPlanes<'a> {
3151 type Item = VideoPlane<'a>;
3152 fn next(&mut self) -> Option<Self::Item> {
3153 let layout = *self.layouts.next()?;
3154 Some(unsafe { VideoPlane::new(self.data, layout) })
3156 }
3157 fn size_hint(&self) -> (usize, Option<usize>) {
3158 self.layouts.size_hint()
3159 }
3160}
3161impl ExactSizeIterator for VideoPlanes<'_> {}
3162impl std::iter::FusedIterator for VideoPlanes<'_> {}
3163
3164#[allow(private_bounds)]
3198pub struct VideoRef<'a, S: FrameFree<RawFrame = NDIlib_video_frame_v2_t>> {
3199 guard: Guard<'a, S>,
3200 layout: ValidatedVideoLayout,
3203 metadata: ValidatedFrameMetadata,
3204}
3205
3206pub type VideoFrameRef<'rx> = VideoRef<'rx, VideoKind>;
3213
3214#[allow(private_bounds)]
3215impl<'a, S: FrameFree<RawFrame = NDIlib_video_frame_v2_t>> VideoRef<'a, S> {
3216 pub(crate) unsafe fn new(guard: Guard<'a, S>) -> Result<Self> {
3232 let layout = validate_video_layout(guard.frame())?;
3233 let metadata = unsafe { validate_frame_metadata(guard.frame().p_metadata)? };
3234
3235 Ok(Self {
3236 guard,
3237 layout,
3238 metadata,
3239 })
3240 }
3241
3242 pub fn width(&self) -> i32 {
3244 self.guard.frame().xres
3245 }
3246
3247 pub fn height(&self) -> i32 {
3249 self.guard.frame().yres
3250 }
3251
3252 pub fn pixel_format(&self) -> PixelFormat {
3256 self.layout.pixel_format
3257 }
3258
3259 pub fn frame_rate_n(&self) -> i32 {
3261 self.guard.frame().frame_rate_N
3262 }
3263
3264 pub fn frame_rate_d(&self) -> i32 {
3266 self.guard.frame().frame_rate_D
3267 }
3268
3269 pub fn picture_aspect_ratio(&self) -> f32 {
3271 self.guard.frame().picture_aspect_ratio
3272 }
3273
3274 pub fn scan_type(&self) -> ScanType {
3278 #[allow(clippy::unnecessary_cast)]
3279 ScanType::try_from(self.guard.frame().frame_format_type as u32)
3280 .expect("VideoFrameRef validates scan type during construction")
3281 }
3282
3283 pub fn timecode(&self) -> i64 {
3285 self.guard.frame().timecode
3286 }
3287
3288 pub fn timestamp(&self) -> i64 {
3290 self.guard.frame().timestamp
3291 }
3292
3293 pub fn line_stride_or_size(&self) -> LineStrideOrSize {
3297 self.layout.line_stride_or_size
3298 }
3299
3300 pub fn metadata(&self) -> Option<&str> {
3302 unsafe { frame_metadata_str(self.guard.frame().p_metadata, self.metadata) }
3303 }
3304
3305 pub fn data(&self) -> Option<&[u8]> {
3312 if !self.layout.is_contiguous() {
3313 return None;
3314 }
3315 Some(unsafe {
3319 slice::from_raw_parts(self.guard.frame().p_data, self.layout.payload_len_bytes)
3320 })
3321 }
3322
3323 pub fn plane(&self, index: usize) -> Option<VideoPlane<'_>> {
3325 let layout = *self.layout.planes().get(index)?;
3326 Some(unsafe { VideoPlane::new(self.guard.frame().p_data, layout) })
3329 }
3330
3331 pub fn planes(&self) -> VideoPlanes<'_> {
3333 VideoPlanes {
3334 data: self.guard.frame().p_data,
3335 layouts: self.layout.planes().iter(),
3336 }
3337 }
3338
3339 #[cfg(feature = "image-encoding")]
3343 pub fn encode_png(&self) -> Result<Vec<u8>> {
3344 ImagePixelSource::from_plane(self.layout, self.plane(0).expect("validated video plane"))?
3345 .encode_png()
3346 }
3347
3348 #[cfg(feature = "image-encoding")]
3351 pub fn encode_jpeg(&self, quality: u8) -> Result<Vec<u8>> {
3352 ImagePixelSource::from_plane(self.layout, self.plane(0).expect("validated video plane"))?
3353 .encode_jpeg(quality)
3354 }
3355
3356 #[cfg(feature = "image-encoding")]
3359 pub fn encode_data_url(&self, format: ImageFormat) -> Result<String> {
3360 ImagePixelSource::from_plane(self.layout, self.plane(0).expect("validated video plane"))?
3361 .encode_data_url(format)
3362 }
3363
3364 pub fn to_owned(&self) -> Result<VideoFrame> {
3370 unsafe { VideoFrame::from_raw_validated(self.guard.frame(), self.layout, self.metadata) }
3371 }
3372}
3373
3374#[allow(private_bounds)]
3375impl<'a, S: FrameFree<RawFrame = NDIlib_video_frame_v2_t>> fmt::Debug for VideoRef<'a, S> {
3376 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3377 f.debug_struct(S::REF_DEBUG_NAME)
3378 .field("width", &self.width())
3379 .field("height", &self.height())
3380 .field("pixel_format", &self.pixel_format())
3381 .field("frame_rate_n", &self.frame_rate_n())
3382 .field("frame_rate_d", &self.frame_rate_d())
3383 .field("picture_aspect_ratio", &self.picture_aspect_ratio())
3384 .field("scan_type", &self.scan_type())
3385 .field("timecode", &self.timecode())
3386 .field("active data (bytes)", &self.layout.payload_len_bytes)
3387 .field("line_stride_or_size", &self.line_stride_or_size())
3388 .field("metadata", &self.metadata())
3389 .field("timestamp", &self.timestamp())
3390 .finish()
3391 }
3392}
3393
3394#[allow(private_bounds)]
3416pub struct AudioRef<'a, S: FrameFree<RawFrame = NDIlib_audio_frame_v3_t>> {
3417 guard: Guard<'a, S>,
3418 layout: ValidatedAudioLayout,
3421 metadata: ValidatedFrameMetadata,
3422}
3423
3424pub type AudioFrameRef<'rx> = AudioRef<'rx, AudioKind>;
3433
3434#[allow(private_bounds)]
3435impl<'a, S: FrameFree<RawFrame = NDIlib_audio_frame_v3_t>> AudioRef<'a, S> {
3436 pub(crate) unsafe fn from_validated_layout(
3445 guard: Guard<'a, S>,
3446 layout: ValidatedAudioLayout,
3447 ) -> Result<Self> {
3448 let metadata = unsafe { validate_frame_metadata(guard.frame().p_metadata)? };
3449
3450 Ok(Self {
3451 guard,
3452 layout,
3453 metadata,
3454 })
3455 }
3456
3457 pub fn sample_rate(&self) -> i32 {
3459 self.layout.sample_rate
3460 }
3461
3462 pub fn num_channels(&self) -> i32 {
3464 self.layout.no_channels as i32
3465 }
3466
3467 pub fn num_samples(&self) -> i32 {
3469 self.layout.no_samples as i32
3470 }
3471
3472 pub fn timecode(&self) -> i64 {
3474 self.guard.frame().timecode
3475 }
3476
3477 pub fn timestamp(&self) -> i64 {
3479 self.guard.frame().timestamp
3480 }
3481
3482 pub fn channel_stride_in_bytes(&self) -> i32 {
3484 self.layout.channel_stride_in_bytes
3485 }
3486
3487 pub fn metadata(&self) -> Option<&str> {
3489 unsafe { frame_metadata_str(self.guard.frame().p_metadata, self.metadata) }
3490 }
3491
3492 pub fn data(&self) -> Option<&[f32]> {
3498 if self.layout.is_empty() {
3499 return Some(&[]);
3500 }
3501 if self.layout.no_channels > 1
3502 && self.layout.channel_stride_samples != self.layout.no_samples
3503 {
3504 return None;
3505 }
3506 Some(unsafe {
3509 slice::from_raw_parts(
3510 self.guard.frame().p_data.cast::<f32>(),
3511 self.layout.sample_count,
3512 )
3513 })
3514 }
3515
3516 pub fn channel_data(&self, channel: usize) -> Option<&[f32]> {
3518 let range = self.layout.channel_range(channel)?;
3519 Some(unsafe {
3521 slice::from_raw_parts(
3522 self.guard.frame().p_data.cast::<f32>().add(range.start),
3523 self.layout.no_samples,
3524 )
3525 })
3526 }
3527
3528 pub fn channels(&self) -> impl ExactSizeIterator<Item = &[f32]> + '_ {
3531 let count = if self.layout.is_empty() {
3532 0
3533 } else {
3534 self.layout.no_channels
3535 };
3536 (0..count).map(|channel| self.channel_data(channel).expect("validated channel"))
3537 }
3538}
3539
3540impl<'rx> AudioRef<'rx, AudioKind> {
3541 pub(crate) unsafe fn new(guard: RecvAudioGuard<'rx>) -> Result<Self> {
3557 let layout = validate_audio_layout(guard.frame())?;
3558 unsafe { Self::from_validated_layout(guard, layout) }
3559 }
3560
3561 pub fn format(&self) -> AudioFormat {
3565 self.layout
3566 .format()
3567 .expect("validate_audio_layout requires a concrete audio format")
3568 }
3569
3570 pub fn to_owned(&self) -> Result<AudioFrame> {
3575 unsafe { AudioFrame::from_raw_validated(*self.guard.frame(), self.layout, self.metadata) }
3577 }
3578}
3579
3580impl<'fs> AudioRef<'fs, FrameSyncAudioFree> {
3581 pub(crate) unsafe fn new(
3592 guard: Guard<'fs, FrameSyncAudioFree>,
3593 query_input: bool,
3594 ) -> Result<Self> {
3595 let layout = if query_input {
3596 if guard.frame().no_samples != 0 {
3597 return Err(Error::InvalidFrame(format!(
3598 "FrameSync audio query returned {} samples",
3599 guard.frame().no_samples
3600 )));
3601 }
3602 validate_audio_layout_allow_empty(guard.frame())?
3603 } else {
3604 validate_audio_layout(guard.frame())?
3605 };
3606 unsafe { Self::from_validated_layout(guard, layout) }
3607 }
3608
3609 pub fn is_empty(&self) -> bool {
3611 self.layout.is_empty()
3612 }
3613
3614 pub fn format(&self) -> Option<AudioFormat> {
3616 self.layout.format()
3617 }
3618
3619 pub fn to_owned(&self) -> Result<Option<AudioFrame>> {
3627 if self.layout.is_empty() {
3628 Ok(None)
3629 } else {
3630 unsafe {
3632 AudioFrame::from_raw_validated(*self.guard.frame(), self.layout, self.metadata)
3633 }
3634 .map(Some)
3635 }
3636 }
3637}
3638
3639#[allow(private_bounds)]
3640impl<'a, S: FrameFree<RawFrame = NDIlib_audio_frame_v3_t>> fmt::Debug for AudioRef<'a, S> {
3641 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3642 f.debug_struct(S::REF_DEBUG_NAME)
3643 .field("sample_rate", &self.sample_rate())
3644 .field("num_channels", &self.num_channels())
3645 .field("num_samples", &self.num_samples())
3646 .field("timecode", &self.timecode())
3647 .field("format", &self.layout.format())
3651 .field("samples per channel", &self.layout.no_samples)
3652 .field("channel_stride_in_bytes", &self.channel_stride_in_bytes())
3653 .field("metadata", &self.metadata())
3654 .field("timestamp", &self.timestamp())
3655 .finish()
3656 }
3657}
3658
3659pub struct MetadataFrameRef<'rx> {
3691 guard: RecvMetadataGuard<'rx>,
3692 layout: ValidatedMetadataLayout,
3695}
3696
3697impl<'rx> MetadataFrameRef<'rx> {
3698 pub(crate) unsafe fn new(guard: RecvMetadataGuard<'rx>) -> Result<Self> {
3705 let layout = unsafe { validate_metadata_layout(guard.frame())? };
3707
3708 Ok(Self { guard, layout })
3709 }
3710
3711 pub fn timecode(&self) -> i64 {
3713 self.guard.frame().timecode
3714 }
3715
3716 pub fn data(&self) -> &str {
3721 let bytes = self.as_bytes();
3722 unsafe { str::from_utf8_unchecked(bytes) }
3725 }
3726
3727 pub fn as_bytes(&self) -> &[u8] {
3730 unsafe { metadata_payload_bytes(self.guard.frame(), self.layout) }
3732 }
3733
3734 pub fn to_owned(&self) -> MetadataFrame {
3739 unsafe { MetadataFrame::from_raw_validated(self.guard.frame(), self.layout) }
3740 }
3741}
3742
3743impl<'rx> fmt::Debug for MetadataFrameRef<'rx> {
3744 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3745 f.debug_struct("MetadataFrameRef")
3746 .field("data", &self.data())
3747 .field("data (bytes)", &self.as_bytes().len())
3748 .field("timecode", &self.timecode())
3749 .finish()
3750 }
3751}
3752
3753#[cfg(test)]
3754mod tests {
3755 use super::*;
3756
3757 #[test]
3758 fn packed_audio_dimensions_match_wide_oracle_and_owned_validation() {
3759 fn check(channels: i32, samples: i32) {
3760 let bytes = i128::from(channels) * i128::from(samples) * 4;
3761 let stride = i128::from(samples) * 4;
3762 let valid = channels > 0
3763 && samples > 0
3764 && stride <= i128::from(i32::MAX)
3765 && bytes <= MAX_AUDIO_BYTES as i128;
3766 let packed = validate_packed_audio_dimensions(channels, samples);
3767 let owned =
3768 validate_outbound_audio_layout(48_000, channels, samples, AudioFormat::FLTP);
3769 let request = crate::FrameSyncAudioRequest::capture(channels, samples);
3770 assert_eq!(
3771 packed.is_ok(),
3772 valid,
3773 "channels={channels}, samples={samples}"
3774 );
3775 assert_eq!(owned.is_ok(), valid);
3776 assert_eq!(request.is_ok(), valid);
3777 if let (Ok(packed), Ok(owned)) = (packed, owned) {
3778 assert_eq!(packed.sample_count as i128 * 4, bytes);
3779 assert_eq!(i128::from(packed.channel_stride_in_bytes), stride);
3780 assert_eq!(packed.no_channels, owned.no_channels);
3781 assert_eq!(packed.no_samples, owned.no_samples);
3782 assert_eq!(
3783 packed.channel_stride_in_bytes,
3784 owned.channel_stride_in_bytes
3785 );
3786 assert_eq!(packed.sample_count, owned.sample_count);
3787 }
3788 }
3789 let values = [
3790 i32::MIN,
3791 -1,
3792 0,
3793 1,
3794 2,
3795 3,
3796 16,
3797 1024,
3798 8_388_608,
3799 8_388_609,
3800 16_777_216,
3801 16_777_217,
3802 i32::MAX / 4,
3803 i32::MAX / 4 + 1,
3804 1_073_741_825,
3805 i32::MAX,
3806 ];
3807 for channels in values {
3808 for samples in values {
3809 check(channels, samples);
3810 }
3811 }
3812 let mut seed = 102u64;
3815 for _ in 0..4096 {
3816 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
3817 let channels = (seed >> 32) as i32;
3818 check(channels, seed as i32);
3819 let channels = ((seed >> 32) % 4096 + 1) as i32;
3820 let boundary = (MAX_AUDIO_BYTES / 4) as i32 / channels;
3821 for samples in [boundary - 1, boundary, boundary + 1] {
3822 check(channels, samples);
3823 }
3824 }
3825 let at_stride_limit = validate_packed_audio_dimensions(1, i32::MAX / 4);
3827 assert!(at_stride_limit
3828 .err()
3829 .unwrap()
3830 .to_string()
3831 .contains("maximum size"));
3832 let over_stride_limit = validate_packed_audio_dimensions(1, i32::MAX / 4 + 1);
3833 assert!(over_stride_limit
3834 .err()
3835 .unwrap()
3836 .to_string()
3837 .contains("stride"));
3838 }
3839
3840 #[test]
3844 #[ignore = "Manual optimized video throughput comparison"]
3845 fn video_padding_throughput() {
3846 use crate::capture::{FrameSyncVideoFree, Guard};
3847 use std::{hint::black_box, time::Instant};
3848 for (width, height) in [(1920, 1080), (3840, 2160)] {
3849 for padding in [0, 64] {
3850 let stride = width * 4 + padding;
3851 let mut storage = vec![17u8; (stride * height) as usize];
3852 let raw = NDIlib_video_frame_v2_t {
3853 xres: width,
3854 yres: height,
3855 FourCC: PixelFormat::BGRA.into(),
3856 frame_rate_N: 60,
3857 frame_rate_D: 1,
3858 frame_format_type: ScanType::Progressive.into(),
3859 p_data: storage.as_mut_ptr(),
3860 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
3861 line_stride_in_bytes: stride,
3862 },
3863 ..Default::default()
3864 };
3865 let frame = unsafe {
3866 VideoRef::new(Guard::<FrameSyncVideoFree>::new(ptr::null_mut(), raw))
3867 }
3868 .unwrap();
3869 let iterations = 100;
3870 black_box(storage.clone());
3872 black_box(frame.to_owned().unwrap());
3873 let start = Instant::now();
3874 for _ in 0..iterations {
3875 black_box(black_box(&storage).clone());
3876 }
3877 let baseline = start.elapsed();
3878 let start = Instant::now();
3879 for _ in 0..iterations {
3880 black_box(black_box(&frame).to_owned().unwrap());
3881 }
3882 let packed = start.elapsed();
3883 let start = Instant::now();
3884 for _ in 0..iterations * 100 {
3885 for plane in black_box(&frame).planes() {
3886 for row in plane.rows() {
3887 black_box(row);
3888 }
3889 }
3890 }
3891 let traversal = start.elapsed();
3892 eprintln!("{width}x{height} padding={padding}: old storage copy {:.3} ms/frame, active copy {:.3} ms/frame, row traversal {:.3} us/frame, copied {} active bytes",
3893 baseline.as_secs_f64() * 1000.0 / f64::from(iterations),
3894 packed.as_secs_f64() * 1000.0 / f64::from(iterations),
3895 traversal.as_secs_f64() * 1_000_000.0 / f64::from(iterations * 100),
3896 frame.layout.payload_len_bytes);
3897 }
3898 }
3899 }
3900
3901 #[test]
3902 fn video_padding_all_formats_expose_and_copy_only_initialized_rows() {
3903 use crate::capture::{FrameSyncVideoFree, Guard};
3904 use std::mem::MaybeUninit;
3905 let cases: &[(PixelFormat, &[(usize, usize)])] = &[
3907 (PixelFormat::BGRA, &[(16, 4)]),
3908 (PixelFormat::BGRX, &[(16, 4)]),
3909 (PixelFormat::RGBA, &[(16, 4)]),
3910 (PixelFormat::RGBX, &[(16, 4)]),
3911 (PixelFormat::UYVY, &[(8, 4)]),
3912 (PixelFormat::UYVA, &[(8, 4), (4, 4)]),
3913 (PixelFormat::P216, &[(8, 4), (8, 4)]),
3914 (PixelFormat::PA16, &[(8, 4), (8, 4), (8, 4)]),
3915 (PixelFormat::YV12, &[(4, 4), (2, 2), (2, 2)]),
3916 (PixelFormat::I420, &[(4, 4), (2, 2), (2, 2)]),
3917 (PixelFormat::NV12, &[(4, 4), (4, 2)]),
3918 ];
3919 for &(format, dimensions) in cases {
3920 for padding in [0, 4, 8] {
3921 let stride = dimensions[0].0 + padding;
3922 let mut storage = vec![MaybeUninit::<u8>::uninit(); 512];
3923 let mut expected = Vec::new();
3924 let mut offsets = Vec::new();
3925 let mut offset = 0;
3926 for (i, &(active, rows)) in dimensions.iter().enumerate() {
3927 let plane_stride = stride * active / dimensions[0].0;
3928 offsets.push(offset);
3929 for row in 0..rows {
3930 for byte in 0..active {
3931 let value = (i * 71 + row * 17 + byte) as u8;
3932 storage[offset + row * plane_stride + byte].write(value);
3933 expected.push(value);
3934 }
3935 }
3936 offset += plane_stride * rows;
3937 }
3938 let metadata = CString::new("<video-padding/>").unwrap();
3939 let raw = NDIlib_video_frame_v2_t {
3940 xres: 4,
3941 yres: 4,
3942 FourCC: format.into(),
3943 frame_rate_N: 30000,
3944 frame_rate_D: 1001,
3945 picture_aspect_ratio: 1.25,
3946 frame_format_type: ScanType::Interlaced.into(),
3947 timecode: 12345,
3948 timestamp: 67890,
3949 p_data: storage.as_mut_ptr().cast(),
3950 p_metadata: metadata.as_ptr(),
3951 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
3952 line_stride_in_bytes: stride as i32,
3953 },
3954 };
3955 let make = || unsafe {
3958 VideoRef::new(Guard::<FrameSyncVideoFree>::new(ptr::null_mut(), raw)).unwrap()
3959 };
3960 let (frame, allocations) = crate::test_alloc::allocation_count(make);
3961 assert_eq!(allocations, 0);
3962 let second = make();
3963 let ((), allocations) = crate::test_alloc::allocation_count(|| {
3964 assert_eq!(frame.planes().len(), dimensions.len());
3965 assert!(frame.plane(dimensions.len()).is_none());
3966 assert_eq!(frame.data().is_some(), padding == 0);
3967 if let Some(data) = frame.data() {
3968 assert_eq!(data, expected);
3969 }
3970 let mut position = 0;
3971 for (i, plane) in frame.planes().enumerate() {
3972 let (active, rows) = dimensions[i];
3973 assert_eq!(plane.row_count(), rows);
3974 assert_eq!(plane.active_row_bytes(), active);
3975 assert_eq!(plane.row_stride(), stride * active / dimensions[0].0);
3976 assert_eq!(plane.data().is_some(), padding == 0);
3977 assert!(plane.row(rows).is_none());
3978 let mut iterator = plane.rows();
3979 assert_eq!(iterator.len(), rows);
3980 assert_eq!(iterator.next_back(), plane.row(rows - 1));
3981 assert_eq!(iterator.len(), rows - 1);
3982 for (row_index, row) in plane.rows().enumerate() {
3983 assert_eq!(row.as_ptr(), unsafe {
3984 storage
3985 .as_ptr()
3986 .cast::<u8>()
3987 .add(offsets[i] + row_index * plane.row_stride())
3988 });
3989 assert_eq!(row, &expected[position..position + active]);
3990 position += active;
3991 }
3992 }
3993 assert_eq!(position, expected.len());
3994 });
3995 assert_eq!(allocations, 0);
3996 let planes = frame.planes();
3999 let moved_planes = planes.clone();
4000 let expected_sum: u64 = expected.iter().map(|&b| u64::from(b)).sum();
4001 std::thread::scope(|scope| {
4002 let shared = &planes;
4003 let reader = scope.spawn(move || {
4004 shared
4005 .clone()
4006 .flat_map(|p| p.rows())
4007 .flatten()
4008 .map(|&b| u64::from(b))
4009 .sum::<u64>()
4010 });
4011 let moved = scope.spawn(move || {
4012 moved_planes
4013 .flat_map(|p| p.rows().rev())
4014 .flatten()
4015 .map(|&b| u64::from(b))
4016 .sum::<u64>()
4017 });
4018 assert_eq!(reader.join().unwrap(), expected_sum);
4019 assert_eq!(moved.join().unwrap(), expected_sum);
4020 });
4021 for plane in frame.planes() {
4022 let rows = plane.rows();
4023 let clone = rows.clone();
4024 let serial = rows.clone().flatten().copied().collect::<Vec<_>>();
4025 std::thread::scope(|scope| {
4026 let shared = &plane;
4027 let reader = scope
4028 .spawn(move || shared.rows().flatten().copied().collect::<Vec<_>>());
4029 let moved =
4030 scope.spawn(move || rows.flatten().copied().collect::<Vec<_>>());
4031 let reversed = scope.spawn(move || {
4032 clone.rev().rev().flatten().copied().collect::<Vec<_>>()
4033 });
4034 assert_eq!(reader.join().unwrap(), serial);
4035 assert_eq!(moved.join().unwrap(), serial);
4036 assert_eq!(reversed.join().unwrap(), serial);
4037 });
4038 }
4039 assert_eq!(frame.layout.data_len_bytes, offset);
4040 assert_eq!(frame.layout.payload_len_bytes, expected.len());
4041 let owned = frame.to_owned().unwrap();
4042 let raw_owned = unsafe { VideoFrame::from_raw(&raw) }.unwrap();
4043 assert_eq!(owned.data(), expected);
4044 assert_eq!(raw_owned.data(), expected);
4045 assert_eq!(owned.pixel_format(), format);
4046 assert_eq!(
4047 owned.line_stride_or_size(),
4048 LineStrideOrSize::LineStrideBytes(dimensions[0].0 as i32)
4049 );
4050 assert_eq!(owned.metadata(), Some("<video-padding/>"));
4051 assert_eq!((owned.frame_rate_n(), owned.frame_rate_d()), (30000, 1001));
4052 assert_eq!(owned.picture_aspect_ratio(), 1.25);
4053 assert_eq!(owned.scan_type(), ScanType::Interlaced);
4054 assert_eq!((owned.timecode(), owned.timestamp()), (12345, 67890));
4055 drop(frame);
4056 assert_eq!(
4057 second.plane(0).unwrap().row(0).unwrap(),
4058 &expected[..dimensions[0].0]
4059 );
4060 let raw = NDIlib_video_frame_v2_t {
4062 p_metadata: ptr::null(),
4063 ..raw
4064 };
4065 let (copied, allocations) = crate::test_alloc::allocation_count(|| {
4066 unsafe { VideoFrame::from_raw(&raw) }.unwrap()
4067 });
4068 assert_eq!(allocations, 1);
4069 assert_eq!(copied.data(), expected);
4070 assert!(VideoFrame::builder()
4071 .resolution(4, 4)
4072 .pixel_format(format)
4073 .data(vec![0; expected.len() - 1])
4074 .build()
4075 .is_err());
4076 assert!(crate::BorrowedVideoFrame::try_from_uncompressed(
4077 &vec![0; expected.len() - 1],
4078 4,
4079 4,
4080 format,
4081 30,
4082 1,
4083 )
4084 .is_err());
4085 let mut initialized = vec![99; offset];
4089 let mut position = 0;
4090 for (i, &(active, rows)) in dimensions.iter().enumerate() {
4091 let plane_stride = stride * active / dimensions[0].0;
4092 for row in 0..rows {
4093 let start = offsets[i] + row * plane_stride;
4094 initialized[start..start + active]
4095 .copy_from_slice(&expected[position..position + active]);
4096 position += active;
4097 }
4098 }
4099 let send = crate::BorrowedVideoFrame::builder(&initialized, 4, 4, format)
4100 .frame_rate(30000, 1001)
4101 .picture_aspect_ratio(1.25)
4102 .scan_type(ScanType::Interlaced)
4103 .timecode(12345)
4104 .line_stride(stride as i32)
4105 .metadata(&metadata)
4106 .timestamp(67890)
4107 .build()
4108 .unwrap();
4109 let normalized = send.try_to_owned().unwrap();
4110 assert_eq!(normalized.data(), expected);
4111 assert_eq!(
4112 normalized.line_stride_or_size(),
4113 copied.line_stride_or_size()
4114 );
4115 assert_eq!(normalized.metadata(), Some("<video-padding/>"));
4116 }
4117 }
4118 }
4119
4120 #[test]
4121 fn video_padding_single_rows_and_zero_stride() {
4122 use crate::capture::{FrameSyncVideoFree, Guard};
4123 use std::mem::MaybeUninit;
4124 for format in [
4125 PixelFormat::BGRA,
4126 PixelFormat::BGRX,
4127 PixelFormat::RGBA,
4128 PixelFormat::RGBX,
4129 PixelFormat::UYVY,
4130 PixelFormat::UYVA,
4131 PixelFormat::P216,
4132 PixelFormat::PA16,
4133 ] {
4134 let tight = ValidatedVideoLayout::new_uncompressed(format, 2, 1, Some(0)).unwrap();
4135 assert!(tight.is_contiguous());
4136 let active = format.try_line_stride(2).unwrap();
4137 let padded =
4138 ValidatedVideoLayout::new_uncompressed(format, 2, 1, Some(active + 4)).unwrap();
4139 let mut storage = [MaybeUninit::<u8>::uninit(); 64];
4140 for plane in padded.planes() {
4141 for byte in &mut storage[plane.offset..plane.offset + plane.active_row_bytes] {
4142 byte.write(42);
4143 }
4144 }
4145 let raw = NDIlib_video_frame_v2_t {
4146 xres: 2,
4147 yres: 1,
4148 FourCC: format.into(),
4149 frame_rate_N: 30,
4150 frame_rate_D: 1,
4151 p_data: storage.as_mut_ptr().cast(),
4152 frame_format_type: ScanType::Progressive.into(),
4153 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
4154 line_stride_in_bytes: active + 4,
4155 },
4156 ..Default::default()
4157 };
4158 let frame =
4159 unsafe { VideoRef::new(Guard::<FrameSyncVideoFree>::new(ptr::null_mut(), raw)) }
4160 .unwrap();
4161 assert_eq!(frame.data().is_some(), padded.plane_count == 1);
4162 for plane in frame.planes() {
4163 assert_eq!(plane.data().unwrap(), vec![42; plane.active_row_bytes()]);
4164 }
4165 assert_eq!(
4166 frame.to_owned().unwrap().data(),
4167 vec![42; tight.payload_len_bytes]
4168 );
4169 }
4170 for format in [PixelFormat::YV12, PixelFormat::I420, PixelFormat::NV12] {
4171 let tight = ValidatedVideoLayout::new_uncompressed(format, 4, 2, Some(0)).unwrap();
4172 assert!(tight.is_contiguous());
4173 let padded = ValidatedVideoLayout::new_uncompressed(format, 4, 2, Some(8)).unwrap();
4174 assert!(!padded.is_contiguous()); }
4176 }
4177
4178 #[cfg(feature = "image-encoding")]
4179 #[test]
4180 fn video_padding_encoders_never_read_uninitialized_gaps() {
4181 use crate::capture::{FrameSyncVideoFree, Guard};
4182 use std::mem::MaybeUninit;
4183 for format in [
4184 PixelFormat::BGRA,
4185 PixelFormat::BGRX,
4186 PixelFormat::RGBA,
4187 PixelFormat::RGBX,
4188 ] {
4189 let mut storage = [MaybeUninit::<u8>::uninit(); 24];
4190 let pixels = [1, 2, 3, 255, 5, 6, 7, 255, 9, 10, 11, 255, 13, 14, 15, 255];
4191 for row in 0..2 {
4192 for byte in 0..8 {
4193 storage[row * 12 + byte].write(pixels[row * 8 + byte]);
4194 }
4195 }
4196 let raw = NDIlib_video_frame_v2_t {
4197 xres: 2,
4198 yres: 2,
4199 FourCC: format.into(),
4200 frame_rate_N: 30,
4201 frame_rate_D: 1,
4202 frame_format_type: ScanType::Progressive.into(),
4203 p_data: storage.as_mut_ptr().cast(),
4204 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
4205 line_stride_in_bytes: 12,
4206 },
4207 ..Default::default()
4208 };
4209 let borrowed =
4210 unsafe { VideoRef::new(Guard::<FrameSyncVideoFree>::new(ptr::null_mut(), raw)) }
4211 .unwrap();
4212 let packed = image_test_frame(format, 2, 2, None, pixels.to_vec());
4213 let png = borrowed.encode_png().unwrap();
4214 assert_eq!(
4215 decode_png_rgba(&png),
4216 decode_png_rgba(&packed.encode_png().unwrap())
4217 );
4218 assert_eq!(
4219 borrowed.encode_jpeg(85).unwrap(),
4220 packed.encode_jpeg(85).unwrap()
4221 );
4222 assert_eq!(
4223 borrowed.encode_data_url(ImageFormat::Png).unwrap(),
4224 packed.encode_data_url(ImageFormat::Png).unwrap()
4225 );
4226 assert_eq!(borrowed.to_owned().unwrap().data(), pixels);
4227 }
4228 }
4229
4230 #[test]
4231 fn video_padding_reported_642_pixel_reproduction() {
4232 use crate::capture::{FrameSyncVideoFree, Guard};
4233 use std::mem::MaybeUninit;
4234 let mut storage = vec![MaybeUninit::<u8>::uninit(); 2592 * 32];
4235 for row in 0..32 {
4236 for byte in &mut storage[row * 2592..row * 2592 + 2568] {
4237 byte.write(17);
4238 }
4239 }
4240 let raw = NDIlib_video_frame_v2_t {
4241 xres: 642,
4242 yres: 32,
4243 FourCC: PixelFormat::BGRA.into(),
4244 frame_rate_N: 30,
4245 frame_rate_D: 1,
4246 frame_format_type: ScanType::Progressive.into(),
4247 p_data: storage.as_mut_ptr().cast(),
4248 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
4249 line_stride_in_bytes: 2592,
4250 },
4251 ..Default::default()
4252 };
4253 let frame =
4254 unsafe { VideoRef::new(Guard::<FrameSyncVideoFree>::new(ptr::null_mut(), raw)) }
4255 .unwrap();
4256 assert!(frame.data().is_none());
4257 let owned = frame.to_owned().unwrap();
4258 assert_eq!(owned.data().len(), 82_176);
4259 assert_eq!(
4260 owned.line_stride_or_size(),
4261 LineStrideOrSize::LineStrideBytes(2568)
4262 );
4263 assert!(owned.data().iter().all(|&byte| byte == 17));
4264 }
4265
4266 #[test]
4267 fn video_default_matches_builder_and_reuse_updates_preserve_pixels() {
4268 let default = VideoFrame::default();
4269 let built = VideoFrame::builder().build().unwrap();
4270 assert_eq!(default.scan_type(), built.scan_type());
4271 assert_eq!(default.scan_type(), ScanType::Progressive);
4272 assert_eq!(default.frame_rate_n(), built.frame_rate_n());
4273 assert_eq!(default.timecode(), built.timecode());
4274 let mut frame = VideoFrame::builder().resolution(2, 2).build().unwrap();
4275 let pixels = frame.data().as_ptr();
4276 frame.set_timecode(123);
4277 frame.set_timestamp(456);
4278 frame.set_scan_type(ScanType::Field1);
4279 let raw = frame.to_raw();
4280 assert_eq!(raw.timecode, 123);
4281 assert_eq!(raw.timestamp, 456);
4282 assert_eq!(frame.scan_type(), ScanType::Field1);
4283 assert_eq!(frame.data().as_ptr(), pixels);
4284 }
4285
4286 #[test]
4287 fn video_builder_retains_supplied_allocation_and_rejects_wrong_lengths() {
4288 let data = vec![37; 16];
4289 let pixels = data.as_ptr();
4290 let frame = VideoFrame::builder()
4291 .resolution(2, 2)
4292 .data(data)
4293 .build()
4294 .unwrap();
4295 assert_eq!(frame.data().as_ptr(), pixels);
4296 assert_eq!(frame.data(), &[37; 16]);
4297 for len in [0, 15, 17] {
4298 assert!(VideoFrame::builder()
4299 .resolution(2, 2)
4300 .data(vec![0; len])
4301 .build()
4302 .is_err());
4303 }
4304 }
4305
4306 #[cfg(feature = "image-encoding")]
4307 #[test]
4308 fn borrowed_video_encoding_uses_padded_pixels_without_owned_conversion() {
4309 use crate::capture::{FrameSyncVideoFree, Guard};
4310 let frame = image_test_frame(
4311 PixelFormat::BGRA,
4312 2,
4313 2,
4314 Some(12),
4315 vec![
4316 0, 0, 255, 255, 0, 255, 0, 128, 99, 99, 99, 99, 255, 0, 0, 64, 255, 255, 255, 0,
4317 99, 99, 99, 99,
4318 ],
4319 );
4320 let guard = unsafe { Guard::<FrameSyncVideoFree>::new(ptr::null_mut(), frame.to_raw()) };
4323 let borrowed = unsafe { VideoRef::new(guard) }.unwrap();
4324 assert!(borrowed.data().is_none());
4325 assert_eq!(
4326 borrowed.plane(0).unwrap().row(0).unwrap().as_ptr(),
4327 frame.data().as_ptr()
4328 );
4329 let png = borrowed.encode_png().unwrap();
4330 assert_eq!(
4331 decode_png_rgba(&png),
4332 (
4333 2,
4334 2,
4335 vec![255, 0, 0, 255, 0, 255, 0, 128, 0, 0, 255, 64, 255, 255, 255, 0,]
4336 )
4337 );
4338 assert_eq!(png, frame.encode_png().unwrap());
4339 assert_eq!(
4340 borrowed.encode_jpeg(85).unwrap(),
4341 frame.encode_jpeg(85).unwrap()
4342 );
4343 assert_eq!(
4344 borrowed.encode_data_url(ImageFormat::Png).unwrap(),
4345 frame.encode_data_url(ImageFormat::Png).unwrap()
4346 );
4347 assert_eq!(
4348 borrowed.encode_data_url(ImageFormat::Jpeg(85)).unwrap(),
4349 frame.encode_data_url(ImageFormat::Jpeg(85)).unwrap()
4350 );
4351 assert!(borrowed.encode_jpeg(0).is_err());
4352
4353 let unsupported = VideoFrame::builder()
4354 .resolution(2, 2)
4355 .pixel_format(PixelFormat::UYVY)
4356 .build()
4357 .unwrap();
4358 let guard =
4359 unsafe { Guard::<FrameSyncVideoFree>::new(ptr::null_mut(), unsupported.to_raw()) };
4360 let borrowed = unsafe { VideoRef::new(guard) }.unwrap();
4361 assert!(borrowed.encode_png().is_err());
4362 assert!(borrowed.encode_jpeg(85).is_err());
4363 }
4364
4365 #[cfg(feature = "image-encoding")]
4366 fn image_test_frame(
4367 pixel_format: PixelFormat,
4368 width: i32,
4369 height: i32,
4370 line_stride: Option<i32>,
4371 data: Vec<u8>,
4372 ) -> VideoFrame {
4373 let layout =
4374 ValidatedVideoLayout::new_uncompressed(pixel_format, width, height, line_stride)
4375 .unwrap();
4376 assert_eq!(data.len(), layout.data_len_bytes);
4377
4378 VideoFrame {
4379 layout,
4380 frame_rate_n: 60,
4381 frame_rate_d: 1,
4382 picture_aspect_ratio: 16.0 / 9.0,
4383 scan_type: ScanType::Progressive,
4384 timecode: 0,
4385 data,
4386 metadata: None,
4387 timestamp: 0,
4388 }
4389 }
4390
4391 #[cfg(feature = "image-encoding")]
4392 fn decode_png_rgba(png_bytes: &[u8]) -> (u32, u32, Vec<u8>) {
4393 let decoder = png::Decoder::new(std::io::Cursor::new(png_bytes));
4394 let mut reader = decoder.read_info().unwrap();
4395 let output_size = reader.output_buffer_size().unwrap();
4396 let mut output = vec![0; output_size];
4397 let info = reader.next_frame(&mut output).unwrap();
4398 output.truncate(info.buffer_size());
4399
4400 assert_eq!(info.color_type, png::ColorType::Rgba);
4401 assert_eq!(info.bit_depth, png::BitDepth::Eight);
4402
4403 (info.width, info.height, output)
4404 }
4405
4406 #[cfg(feature = "image-encoding")]
4407 fn assert_jpeg_markers(jpeg_bytes: &[u8]) {
4408 assert!(jpeg_bytes.len() >= 4);
4409 assert_eq!(&jpeg_bytes[..2], &[0xFF, 0xD8]);
4410 assert_eq!(&jpeg_bytes[jpeg_bytes.len() - 2..], &[0xFF, 0xD9]);
4411 }
4412
4413 #[cfg(feature = "image-encoding")]
4414 #[test]
4415 fn test_encode_png_decodes_exact_supported_pixels() {
4416 let cases = [
4417 (
4418 PixelFormat::RGBA,
4419 vec![10, 20, 30, 40, 50, 60, 70, 80],
4420 vec![10, 20, 30, 40, 50, 60, 70, 80],
4421 ),
4422 (
4423 PixelFormat::BGRA,
4424 vec![30, 20, 10, 40, 70, 60, 50, 80],
4425 vec![10, 20, 30, 40, 50, 60, 70, 80],
4426 ),
4427 (
4428 PixelFormat::RGBX,
4429 vec![10, 20, 30, 0, 50, 60, 70, 99],
4430 vec![10, 20, 30, 255, 50, 60, 70, 255],
4431 ),
4432 (
4433 PixelFormat::BGRX,
4434 vec![30, 20, 10, 0, 70, 60, 50, 99],
4435 vec![10, 20, 30, 255, 50, 60, 70, 255],
4436 ),
4437 ];
4438
4439 for (pixel_format, data, expected) in cases {
4440 let frame = image_test_frame(pixel_format, 2, 1, None, data);
4441 let png = frame.encode_png().unwrap();
4442 let (width, height, decoded) = decode_png_rgba(&png);
4443
4444 assert_eq!((width, height), (2, 1), "{pixel_format:?}");
4445 assert_eq!(decoded, expected, "{pixel_format:?}");
4446 }
4447 }
4448
4449 #[cfg(feature = "image-encoding")]
4450 #[test]
4451 fn test_encode_png_skips_padded_rows() {
4452 let data = vec![
4453 1, 2, 3, 0, 4, 5, 6, 0, 200, 201, 202, 203, 7, 8, 9, 0, 10, 11, 12, 0, 204, 205, 206,
4454 207,
4455 ];
4456 let frame = image_test_frame(PixelFormat::RGBX, 2, 2, Some(12), data);
4457
4458 let png = frame.encode_png().unwrap();
4459 let (width, height, decoded) = decode_png_rgba(&png);
4460
4461 assert_eq!((width, height), (2, 2));
4462 assert_eq!(
4463 decoded,
4464 vec![1, 2, 3, 255, 4, 5, 6, 255, 7, 8, 9, 255, 10, 11, 12, 255,]
4465 );
4466 }
4467
4468 #[cfg(feature = "image-encoding")]
4469 #[test]
4470 fn test_encode_jpeg_accepts_supported_image_formats() {
4471 let cases = [
4472 (PixelFormat::RGBA, vec![10, 20, 30, 40]),
4473 (PixelFormat::BGRA, vec![30, 20, 10, 40]),
4474 (PixelFormat::RGBX, vec![10, 20, 30, 0]),
4475 (PixelFormat::BGRX, vec![30, 20, 10, 0]),
4476 ];
4477
4478 for (pixel_format, pixel) in cases {
4479 let data = pixel.repeat(16);
4480 let frame = image_test_frame(pixel_format, 4, 4, None, data);
4481 let jpeg = frame.encode_jpeg(90).unwrap();
4482 assert_jpeg_markers(&jpeg);
4483 }
4484 }
4485
4486 #[cfg(feature = "image-encoding")]
4487 #[test]
4488 fn test_encode_jpeg_skips_padded_rows() {
4489 let data = vec![
4490 3, 2, 1, 0, 6, 5, 4, 0, 250, 251, 252, 253, 9, 8, 7, 0, 12, 11, 10, 0, 254, 255, 0, 1,
4491 ];
4492 let frame = image_test_frame(PixelFormat::BGRX, 2, 2, Some(12), data);
4493
4494 let jpeg = frame.encode_jpeg(85).unwrap();
4495 assert_jpeg_markers(&jpeg);
4496 }
4497
4498 #[cfg(feature = "image-encoding")]
4499 #[test]
4500 fn test_encode_jpeg_rejects_invalid_quality() {
4501 let frame = image_test_frame(PixelFormat::RGBA, 2, 2, None, vec![0; 16]);
4502
4503 for quality in [0, 101, 255] {
4504 let err = frame.encode_jpeg(quality).unwrap_err();
4505 let err_msg = err.to_string();
4506 assert!(
4507 matches!(&err, Error::InvalidFrame(message) if message.contains("quality")),
4508 "unexpected error for quality {quality}: {err_msg}"
4509 );
4510 }
4511 }
4512
4513 #[cfg(feature = "image-encoding")]
4514 #[test]
4515 fn test_encode_jpeg_rejects_oversized_dimensions_before_casting() {
4516 let wide = image_test_frame(PixelFormat::RGBA, 65_536, 1, None, vec![0; 65_536 * 4]);
4517 let err = wide.encode_jpeg(85).unwrap_err();
4518 let err_msg = err.to_string();
4519 assert!(
4520 matches!(&err, Error::InvalidFrame(message) if message.contains("JPEG width")),
4521 "unexpected error: {err_msg}"
4522 );
4523
4524 let tall = image_test_frame(PixelFormat::RGBA, 2, 65_536, None, vec![0; 65_536 * 8]);
4525 let err = tall.encode_jpeg(85).unwrap_err();
4526 let err_msg = err.to_string();
4527 assert!(
4528 matches!(&err, Error::InvalidFrame(message) if message.contains("JPEG height")),
4529 "unexpected error: {err_msg}"
4530 );
4531 }
4532
4533 #[cfg(feature = "image-encoding")]
4534 #[test]
4535 fn test_encode_jpeg_rejects_unsupported_format() {
4536 let frame = image_test_frame(PixelFormat::UYVY, 2, 2, None, vec![0; 8]);
4537
4538 let err = frame.encode_jpeg(85).unwrap_err();
4539 let err_msg = err.to_string();
4540 assert!(
4541 matches!(&err, Error::InvalidFrame(message) if message.contains("Unsupported format")),
4542 "unexpected error: {err_msg}"
4543 );
4544 }
4545
4546 #[cfg(feature = "image-encoding")]
4547 #[test]
4548 fn png_conversion_scratch_is_bounded_by_one_row() {
4549 for format in [
4550 PixelFormat::RGBA,
4551 PixelFormat::BGRA,
4552 PixelFormat::RGBX,
4553 PixelFormat::BGRX,
4554 ] {
4555 for height in [1, 19, 257] {
4556 let frame =
4557 image_test_frame(format, 18, height, Some(84), vec![42; 84 * height as usize]);
4558 let source = ImagePixelSource::new(frame.layout, frame.data()).unwrap();
4559 let (scratch, allocations) =
4560 crate::test_alloc::allocation_count(|| source.png_conversion_row().unwrap());
4561 let expected = if format == PixelFormat::RGBA {
4562 0
4563 } else {
4564 18 * 4
4565 };
4566 assert_eq!(scratch.len(), expected);
4567 assert_eq!(scratch.capacity(), expected);
4568 assert_eq!(allocations, usize::from(expected != 0));
4569 let mut buffers = std::array::from_fn(|_| Vec::with_capacity(18));
4573 let adapter = JpegRows {
4574 source: &source,
4575 width: 18,
4576 height: height as u16,
4577 };
4578 let (_, allocations) = crate::test_alloc::allocation_count(|| {
4579 for row in 0..height as u16 {
4580 for buffer in &mut buffers {
4581 buffer.clear();
4582 }
4583 jpeg_encoder::ImageBuffer::fill_buffers(&adapter, row, &mut buffers);
4584 }
4585 });
4586 assert_eq!(allocations, 0);
4587 }
4588 }
4589 }
4590
4591 #[cfg(feature = "image-encoding")]
4594 #[test]
4595 #[ignore = "Manual optimized image time/peak-allocation comparison"]
4596 fn image_encoding_resources() {
4597 use std::{hint::black_box, time::Instant};
4598 fn staged(source: &ImagePixelSource<'_>, format: ImageFormat) -> Vec<u8> {
4599 match format {
4600 ImageFormat::Png => {
4601 let mut pixels = Vec::with_capacity(source.width * source.height * 4);
4602 for row in source.plane.rows() {
4603 for pixel in row.as_chunks::<4>().0 {
4604 pixels.extend_from_slice(&source.pixel_format.rgba_pixel(pixel));
4605 }
4606 }
4607 let mut output = Vec::new();
4608 let mut encoder =
4609 png::Encoder::new(&mut output, source.width as u32, source.height as u32);
4610 encoder.set_color(png::ColorType::Rgba);
4611 encoder.set_depth(png::BitDepth::Eight);
4612 encoder
4613 .write_header()
4614 .unwrap()
4615 .write_image_data(&pixels)
4616 .unwrap();
4617 output
4618 }
4619 ImageFormat::Jpeg(quality) => {
4620 let mut pixels = Vec::with_capacity(source.width * source.height * 3);
4621 for row in source.plane.rows() {
4622 for pixel in row.as_chunks::<4>().0 {
4623 pixels.extend_from_slice(&source.pixel_format.rgb_pixel(pixel));
4624 }
4625 }
4626 let mut output = Vec::new();
4627 jpeg_encoder::Encoder::new(&mut output, quality)
4628 .encode(
4629 &pixels,
4630 source.width as u16,
4631 source.height as u16,
4632 jpeg_encoder::ColorType::Rgb,
4633 )
4634 .unwrap();
4635 output
4636 }
4637 }
4638 }
4639 eprintln!(
4640 "width,height,padding,entropy,format,path,median_ms,peak_encode_bytes,output_bytes"
4641 );
4642 for (width, height) in [(1920, 1080), (3840, 2160), (7680, 4320)] {
4643 for padding in [0, 64] {
4644 for entropy in [false, true] {
4645 let stride = width * 4 + padding;
4646 let mut data = vec![42; stride * height];
4647 if entropy {
4648 let mut seed = 118u64;
4649 for row in data.chunks_exact_mut(stride) {
4650 for pixel in row[..width * 4].as_chunks_mut::<4>().0 {
4651 for byte in &mut pixel[..3] {
4652 seed ^= seed << 13;
4653 seed ^= seed >> 7;
4654 seed ^= seed << 17;
4655 *byte = seed as u8;
4656 }
4657 pixel[3] = 255;
4658 }
4659 }
4660 }
4661 let frame = image_test_frame(
4662 PixelFormat::BGRA,
4663 width as i32,
4664 height as i32,
4665 Some(stride as i32),
4666 data,
4667 );
4668 let source = ImagePixelSource::new(frame.layout, frame.data()).unwrap();
4669 for format in [
4670 ImageFormat::Png,
4671 ImageFormat::Jpeg(1),
4672 ImageFormat::Jpeg(85),
4673 ImageFormat::Jpeg(100),
4674 ] {
4675 for old in [true, false] {
4676 let mut times = [0.0f64; 3];
4677 let mut peak = 0;
4678 let mut output_bytes = 0;
4679 for time in &mut times {
4680 let start = Instant::now();
4681 let (output, bytes) = crate::test_alloc::peak_bytes(|| {
4682 if old {
4683 staged(black_box(&source), format)
4684 } else {
4685 match format {
4686 ImageFormat::Png => source.encode_png().unwrap(),
4687 ImageFormat::Jpeg(q) => source.encode_jpeg(q).unwrap(),
4688 }
4689 }
4690 });
4691 *time = start.elapsed().as_secs_f64() * 1000.0;
4692 peak = peak.max(bytes);
4693 output_bytes = output.len();
4694 black_box(output);
4695 }
4696 times.sort_by(f64::total_cmp);
4697 eprintln!("{width},{height},{padding},{entropy},{format:?},{},{:.3},{peak},{output_bytes}", if old { "staged" } else { "rows" }, times[1]);
4698 }
4699 }
4700 }
4701 }
4702 }
4703 }
4704
4705 #[cfg(feature = "image-encoding")]
4706 #[test]
4707 fn streamed_images_match_independently_packed_pixels() {
4708 for format in [
4709 PixelFormat::RGBA,
4710 PixelFormat::BGRA,
4711 PixelFormat::RGBX,
4712 PixelFormat::BGRX,
4713 ] {
4714 for (width, height) in [(2, 1), (2, 2), (18, 19), (64, 33)] {
4715 for padding in [0, 12] {
4716 let stride = width * 4 + padding;
4717 let mut data = vec![0xCD; stride * height];
4718 let mut rgba = Vec::new();
4719 let mut rgb = Vec::new();
4720 for y in 0..height {
4721 for x in 0..width {
4722 let r = (x * 17 + y * 7) as u8;
4723 let g = (x * 31 + y * 13) as u8;
4724 let b = (x * 43 + y * 19) as u8;
4725 let a = (x * 59 + y * 23) as u8;
4726 let pixel = if matches!(format, PixelFormat::BGRA | PixelFormat::BGRX) {
4727 [b, g, r, a]
4728 } else {
4729 [r, g, b, a]
4730 };
4731 data[y * stride + x * 4..y * stride + x * 4 + 4]
4732 .copy_from_slice(&pixel);
4733 rgba.extend_from_slice(&[
4734 r,
4735 g,
4736 b,
4737 if matches!(format, PixelFormat::RGBX | PixelFormat::BGRX) {
4738 255
4739 } else {
4740 a
4741 },
4742 ]);
4743 rgb.extend_from_slice(&[r, g, b]);
4744 }
4745 }
4746 let frame = image_test_frame(
4747 format,
4748 width as i32,
4749 height as i32,
4750 Some(stride as i32),
4751 data,
4752 );
4753 assert_eq!(
4754 decode_png_rgba(&frame.encode_png().unwrap()),
4755 (width as u32, height as u32, rgba)
4756 );
4757 for quality in [1, 85, 100] {
4758 let mut baseline = Vec::new();
4759 jpeg_encoder::Encoder::new(&mut baseline, quality)
4760 .encode(
4761 &rgb,
4762 width as u16,
4763 height as u16,
4764 jpeg_encoder::ColorType::Rgb,
4765 )
4766 .unwrap();
4767 assert_eq!(
4768 frame.encode_jpeg(quality).unwrap(),
4769 baseline,
4770 "{format:?} {width}x{height} padding={padding} quality={quality}"
4771 );
4772 }
4773 let mut changed = image_test_frame(
4775 format,
4776 width as i32,
4777 height as i32,
4778 Some(stride as i32),
4779 frame.data().to_vec(),
4780 );
4781 for y in 0..height {
4782 changed.data_mut()[y * stride + width * 4..(y + 1) * stride].fill(0x12);
4783 if matches!(format, PixelFormat::RGBX | PixelFormat::BGRX) {
4784 for x in 0..width {
4785 changed.data_mut()[y * stride + x * 4 + 3] ^= 0xFF;
4786 }
4787 }
4788 }
4789 assert_eq!(frame.encode_png().unwrap(), changed.encode_png().unwrap());
4790 assert_eq!(
4791 frame.encode_jpeg(85).unwrap(),
4792 changed.encode_jpeg(85).unwrap()
4793 );
4794 }
4795 }
4796 }
4797 }
4798
4799 #[cfg(feature = "image-encoding")]
4800 #[test]
4801 fn test_image_pixel_source_rejects_bad_data_len() {
4802 let layout = ValidatedVideoLayout::new_uncompressed(PixelFormat::RGBA, 2, 1, None).unwrap();
4803 let err = ImagePixelSource::new(layout, &[0; 7]).unwrap_err();
4804 let err_msg = err.to_string();
4805 assert!(
4806 matches!(&err, Error::InvalidFrame(message) if message.contains("Video data length")),
4807 "unexpected error: {err_msg}"
4808 );
4809 }
4810
4811 #[test]
4813 fn test_pixel_format_info_packed_rgb() {
4814 let formats = [
4815 PixelFormat::BGRA,
4816 PixelFormat::BGRX,
4817 PixelFormat::RGBA,
4818 PixelFormat::RGBX,
4819 ];
4820
4821 for fmt in formats {
4822 let info = fmt.info();
4823 assert_eq!(
4824 info.bytes_per_pixel(),
4825 4,
4826 "Format {:?} bytes per pixel",
4827 fmt
4828 );
4829 assert_eq!(
4830 info.category(),
4831 FormatCategory::Packed,
4832 "Format {:?} category",
4833 fmt
4834 );
4835 assert!(
4836 !info.is_planar_420(),
4837 "Format {:?} should not be planar",
4838 fmt
4839 );
4840
4841 let len = info.try_buffer_len(7680, 1080).unwrap();
4843 assert_eq!(len, 7680 * 1080, "Format {:?} even dimensions", fmt);
4844
4845 let len = info.try_buffer_len(7684, 1081).unwrap();
4847 assert_eq!(len, 7684 * 1081, "Format {:?} odd dimensions", fmt);
4848 }
4849 }
4850
4851 #[test]
4853 fn test_pixel_format_info_packed_yuv() {
4854 let info = PixelFormat::UYVY.info();
4856 assert_eq!(info.bytes_per_pixel(), 2);
4857 assert_eq!(info.category(), FormatCategory::Packed);
4858 let len = info.try_buffer_len(3840, 1080).unwrap();
4859 assert_eq!(len, 3840 * 1080);
4860
4861 let info = PixelFormat::UYVA.info();
4863 assert_eq!(info.bytes_per_pixel(), 2);
4864 assert_eq!(info.category(), FormatCategory::PackedAlpha);
4865 assert!(!info.is_planar_420());
4866 let len = info.try_buffer_len(3840, 1080).unwrap();
4867 assert_eq!(len, 3840 * 1080 + 1920 * 1080);
4868 let len = info.try_buffer_len(4096, 1080).unwrap();
4870 assert_eq!(len, 4096 * 1080 + 2048 * 1080);
4871 assert!(info.try_buffer_len(3841, 1080).is_err());
4872
4873 let info = PixelFormat::P216.info();
4875 assert_eq!(info.bytes_per_pixel(), 2);
4876 assert_eq!(info.category(), FormatCategory::SemiPlanar422);
4877 assert!(!info.is_planar_420());
4878 let len = info.try_buffer_len(3840, 1080).unwrap();
4879 assert_eq!(len, 3840 * 1080 * 2);
4880
4881 let info = PixelFormat::PA16.info();
4883 assert_eq!(info.bytes_per_pixel(), 2);
4884 assert_eq!(info.category(), FormatCategory::SemiPlanar422Alpha);
4885 assert!(!info.is_planar_420());
4886 let len = info.try_buffer_len(3840, 1080).unwrap();
4887 assert_eq!(len, 3840 * 1080 * 3);
4888 }
4889
4890 #[test]
4892 fn test_pixel_format_info_planar_420_even() {
4893 let y_stride = 1920;
4899
4900 let info = PixelFormat::YV12.info();
4901 assert_eq!(info.bytes_per_pixel(), 1);
4902 assert_eq!(info.category(), FormatCategory::Planar420);
4903 assert!(info.is_planar_420());
4904 let len = info.try_buffer_len(y_stride, 1080).unwrap();
4905 assert_eq!(len, 3_110_400, "YV12 1920x1080");
4906
4907 let info = PixelFormat::I420.info();
4908 assert_eq!(info.category(), FormatCategory::Planar420);
4909 let len = info.try_buffer_len(y_stride, 1080).unwrap();
4910 assert_eq!(len, 3_110_400, "I420 1920x1080");
4911 }
4912
4913 #[test]
4915 fn test_pixel_format_info_planar_420_odd() {
4916 let y_stride = 1921;
4917
4918 assert!(PixelFormat::YV12
4919 .info()
4920 .try_buffer_len(y_stride, 1081)
4921 .is_err());
4922 assert!(PixelFormat::I420
4923 .info()
4924 .try_buffer_len(y_stride, 1081)
4925 .is_err());
4926 }
4927
4928 #[test]
4930 fn test_pixel_format_info_nv12_even() {
4931 let y_stride = 1920;
4936
4937 let info = PixelFormat::NV12.info();
4938 assert_eq!(info.bytes_per_pixel(), 1);
4939 assert_eq!(info.category(), FormatCategory::SemiPlanar420);
4940 assert!(info.is_planar_420());
4941 let len = info.try_buffer_len(y_stride, 1080).unwrap();
4942 assert_eq!(len, 3_110_400, "NV12 1920x1080");
4943 }
4944
4945 #[test]
4947 fn test_pixel_format_info_nv12_odd() {
4948 let y_stride = 1921;
4949 assert!(PixelFormat::NV12
4950 .info()
4951 .try_buffer_len(y_stride, 1081)
4952 .is_err());
4953 }
4954
4955 #[test]
4957 fn test_pixel_format_line_stride() {
4958 assert_eq!(PixelFormat::BGRA.try_line_stride(1920).unwrap(), 7680);
4960 assert_eq!(PixelFormat::BGRX.try_line_stride(1920).unwrap(), 7680);
4961 assert_eq!(PixelFormat::RGBA.try_line_stride(1920).unwrap(), 7680);
4962 assert_eq!(PixelFormat::RGBX.try_line_stride(1920).unwrap(), 7680);
4963
4964 assert_eq!(PixelFormat::UYVY.try_line_stride(1920).unwrap(), 3840);
4966
4967 assert_eq!(PixelFormat::UYVA.try_line_stride(1920).unwrap(), 3840);
4969
4970 assert_eq!(PixelFormat::P216.try_line_stride(1920).unwrap(), 3840);
4972 assert_eq!(PixelFormat::PA16.try_line_stride(1920).unwrap(), 3840);
4973
4974 assert_eq!(PixelFormat::YV12.try_line_stride(1920).unwrap(), 1920);
4976 assert_eq!(PixelFormat::I420.try_line_stride(1920).unwrap(), 1920);
4977 assert_eq!(PixelFormat::NV12.try_line_stride(1920).unwrap(), 1920);
4978 }
4979
4980 #[test]
4982 fn test_pixel_format_buffer_size() {
4983 assert_eq!(
4985 PixelFormat::BGRA.try_buffer_size(1920, 1080).unwrap(),
4986 8_294_400
4987 );
4988 assert_eq!(
4989 PixelFormat::RGBA.try_buffer_size(1920, 1080).unwrap(),
4990 8_294_400
4991 );
4992
4993 assert_eq!(
4995 PixelFormat::YV12.try_buffer_size(1920, 1080).unwrap(),
4996 3_110_400
4997 );
4998 assert_eq!(
4999 PixelFormat::I420.try_buffer_size(1920, 1080).unwrap(),
5000 3_110_400
5001 );
5002
5003 assert_eq!(
5005 PixelFormat::NV12.try_buffer_size(1920, 1080).unwrap(),
5006 3_110_400
5007 );
5008
5009 assert_eq!(
5011 PixelFormat::UYVA.try_buffer_size(1920, 1080).unwrap(),
5012 6_220_800
5013 );
5014
5015 assert_eq!(
5017 PixelFormat::P216.try_buffer_size(1920, 1080).unwrap(),
5018 8_294_400
5019 );
5020
5021 assert_eq!(
5023 PixelFormat::PA16.try_buffer_size(1920, 1080).unwrap(),
5024 12_441_600
5025 );
5026 }
5027
5028 #[test]
5030 fn test_pixel_format_info_is_planar_420() {
5031 assert!(PixelFormat::YV12.info().is_planar_420());
5032 assert!(PixelFormat::I420.info().is_planar_420());
5033 assert!(PixelFormat::NV12.info().is_planar_420());
5034
5035 assert!(!PixelFormat::BGRA.info().is_planar_420());
5036 assert!(!PixelFormat::RGBA.info().is_planar_420());
5037 assert!(!PixelFormat::UYVY.info().is_planar_420());
5038 assert!(!PixelFormat::UYVA.info().is_planar_420());
5039 }
5040
5041 #[test]
5043 fn test_videoframe_builder_planar_even() {
5044 let frame = VideoFrame::builder()
5045 .resolution(1920, 1080)
5046 .pixel_format(PixelFormat::NV12)
5047 .build()
5048 .expect("Builder should succeed");
5049
5050 assert_eq!(frame.width(), 1920);
5051 assert_eq!(frame.height(), 1080);
5052 assert_eq!(frame.pixel_format(), PixelFormat::NV12);
5053 assert_eq!(frame.data().len(), 3_110_400, "NV12 1920x1080 buffer size");
5054 }
5055
5056 #[test]
5057 fn test_video_dimensions_across_all_formats() {
5058 let formats = [
5059 PixelFormat::BGRA,
5060 PixelFormat::BGRX,
5061 PixelFormat::RGBA,
5062 PixelFormat::RGBX,
5063 PixelFormat::UYVY,
5064 PixelFormat::UYVA,
5065 PixelFormat::P216,
5066 PixelFormat::PA16,
5067 PixelFormat::YV12,
5068 PixelFormat::I420,
5069 PixelFormat::NV12,
5070 ];
5071 let mut data = vec![0u8; 4096];
5072 for format in formats {
5073 for (width, height) in [(0, 2), (-2, 2), (1, 2), (3, 2), (2, 0), (2, -1)] {
5074 assert!(
5075 format.try_buffer_size(width, height).is_err(),
5076 "{format:?} {width}x{height}"
5077 );
5078 assert!(VideoFrame::builder()
5079 .resolution(width, height)
5080 .pixel_format(format)
5081 .build()
5082 .is_err());
5083 assert!(crate::BorrowedVideoFrame::try_from_uncompressed(
5084 &data, width, height, format, 30, 1
5085 )
5086 .is_err());
5087 let raw = NDIlib_video_frame_v2_t {
5088 xres: width,
5089 yres: height,
5090 FourCC: format.into(),
5091 frame_rate_N: 30,
5092 frame_rate_D: 1,
5093 picture_aspect_ratio: 0.0,
5094 frame_format_type: ScanType::Progressive.into(),
5095 timecode: 0,
5096 p_data: data.as_mut_ptr(),
5097 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
5098 line_stride_in_bytes: 128,
5099 },
5100 p_metadata: ptr::null(),
5101 timestamp: 0,
5102 };
5103 assert!(matches!(
5104 validate_video_layout(&raw),
5105 Err(Error::InvalidFrame(_))
5106 ));
5107 assert!(unsafe { VideoFrame::from_raw(&raw) }.is_err());
5109 }
5110 for width in [0, -2, 1, 3] {
5111 assert!(
5112 format.try_line_stride(width).is_err(),
5113 "{format:?} width={width}"
5114 );
5115 }
5116 for height in [1, 2, 3] {
5117 let valid = !format.info().is_planar_420() || height % 2 == 0;
5118 assert_eq!(format.try_buffer_size(2, height).is_ok(), valid);
5119 assert_eq!(
5120 VideoFrame::builder()
5121 .resolution(2, height)
5122 .pixel_format(format)
5123 .build()
5124 .is_ok(),
5125 valid
5126 );
5127 assert_eq!(
5128 crate::BorrowedVideoFrame::try_from_uncompressed(
5129 &data, 2, height, format, 30, 1
5130 )
5131 .is_ok(),
5132 valid
5133 );
5134 }
5135 assert!(format.try_line_stride(6).is_ok());
5137 assert!(ValidatedVideoLayout::new_uncompressed(format, 6, 2, Some(128)).is_ok());
5138 }
5139 assert!(ValidatedVideoLayout::new_uncompressed(PixelFormat::BGRA, 2, 3, Some(9)).is_ok());
5141 }
5142
5143 #[test]
5145 fn test_videoframe_builder_planar_odd() {
5146 let result = VideoFrame::builder()
5147 .resolution(1921, 1081)
5148 .pixel_format(PixelFormat::I420)
5149 .build();
5150
5151 assert!(
5152 matches!(result, Err(Error::InvalidFrame(_))),
5153 "Planar 4:2:0 odd dimensions should be rejected"
5154 );
5155 }
5156
5157 #[test]
5159 fn test_videoframe_builder_packed() {
5160 let frame = VideoFrame::builder()
5161 .resolution(1920, 1080)
5162 .pixel_format(PixelFormat::BGRA)
5163 .build()
5164 .expect("Builder should succeed");
5165
5166 assert_eq!(frame.width(), 1920);
5167 assert_eq!(frame.height(), 1080);
5168 assert_eq!(frame.pixel_format(), PixelFormat::BGRA);
5169 assert_eq!(
5170 frame.data().len(),
5171 1920 * 1080 * 4,
5172 "BGRA buffer size unchanged"
5173 );
5174 }
5175
5176 #[test]
5179 fn test_videoframe_builder_semi_planar_422_and_alpha() {
5180 for (format, expected_len) in [
5181 (PixelFormat::UYVA, 256 * 2 * 64 + 256 * 64),
5182 (PixelFormat::P216, 256 * 2 * 64 * 2),
5183 (PixelFormat::PA16, 256 * 2 * 64 * 3),
5184 ] {
5185 let frame = VideoFrame::builder()
5186 .resolution(256, 64)
5187 .pixel_format(format)
5188 .build()
5189 .expect("Builder should succeed");
5190
5191 assert_eq!(
5192 frame.line_stride_or_size(),
5193 LineStrideOrSize::LineStrideBytes(256 * 2),
5194 "{format:?} line stride"
5195 );
5196 assert_eq!(frame.data().len(), expected_len, "{format:?} buffer size");
5197 }
5198 }
5199
5200 #[test]
5202 fn test_videoframe_from_raw_nv12() {
5203 let width = 1920;
5205 let height = 1080;
5206 let y_stride = 1920;
5207 let expected_size = 3_110_400; let mut data = vec![0u8; expected_size];
5210 data[expected_size - 1] = 0xFF;
5212
5213 let c_frame = NDIlib_video_frame_v2_t {
5214 xres: width,
5215 yres: height,
5216 FourCC: PixelFormat::NV12.into(),
5217 frame_rate_N: 60,
5218 frame_rate_D: 1,
5219 picture_aspect_ratio: 16.0 / 9.0,
5220 frame_format_type: ScanType::Progressive.into(),
5221 timecode: 0,
5222 p_data: data.as_mut_ptr(),
5223 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
5224 line_stride_in_bytes: y_stride,
5225 },
5226 p_metadata: ptr::null(),
5227 timestamp: 0,
5228 };
5229
5230 let frame = unsafe { VideoFrame::from_raw(&c_frame) }.expect("from_raw should succeed");
5231
5232 assert_eq!(frame.width(), width);
5233 assert_eq!(frame.height(), height);
5234 assert_eq!(frame.pixel_format(), PixelFormat::NV12);
5235 assert_eq!(
5236 frame.data().len(),
5237 expected_size,
5238 "Should copy full Y+UV buffer"
5239 );
5240 assert_eq!(
5241 frame.data()[expected_size - 1],
5242 0xFF,
5243 "Last byte should be copied"
5244 );
5245 }
5246
5247 #[test]
5249 fn test_videoframe_from_raw_i420_odd() {
5250 let width = 1921;
5251 let height = 1081;
5252 let y_stride = 1921;
5253 let expected_size = 3_116_403; let mut data = vec![0u8; expected_size];
5256
5257 let c_frame = NDIlib_video_frame_v2_t {
5258 xres: width,
5259 yres: height,
5260 FourCC: PixelFormat::I420.into(),
5261 frame_rate_N: 30,
5262 frame_rate_D: 1,
5263 picture_aspect_ratio: 16.0 / 9.0,
5264 frame_format_type: ScanType::Progressive.into(),
5265 timecode: 0,
5266 p_data: data.as_mut_ptr(),
5267 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
5268 line_stride_in_bytes: y_stride,
5269 },
5270 p_metadata: ptr::null(),
5271 timestamp: 0,
5272 };
5273
5274 let result = unsafe { VideoFrame::from_raw(&c_frame) };
5275 assert!(
5276 matches!(result, Err(Error::InvalidFrame(_))),
5277 "Planar 4:2:0 odd dimensions should be rejected"
5278 );
5279 }
5280
5281 #[test]
5283 fn test_videoframe_from_raw_packed_regression() {
5284 let width = 1920;
5285 let height = 1080;
5286 let stride = 1920 * 4; let expected_size = (stride * height) as usize;
5288
5289 let mut data = vec![0u8; expected_size];
5290
5291 let c_frame = NDIlib_video_frame_v2_t {
5292 xres: width,
5293 yres: height,
5294 FourCC: PixelFormat::BGRA.into(),
5295 frame_rate_N: 60,
5296 frame_rate_D: 1,
5297 picture_aspect_ratio: 16.0 / 9.0,
5298 frame_format_type: ScanType::Progressive.into(),
5299 timecode: 0,
5300 p_data: data.as_mut_ptr(),
5301 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
5302 line_stride_in_bytes: stride,
5303 },
5304 p_metadata: ptr::null(),
5305 timestamp: 0,
5306 };
5307
5308 let frame = unsafe { VideoFrame::from_raw(&c_frame) }.expect("from_raw should succeed");
5309 assert_eq!(
5310 frame.data().len(),
5311 expected_size,
5312 "BGRA buffer size unchanged"
5313 );
5314 }
5315
5316 #[test]
5318 fn test_videoframeref_unknown_fourcc() {
5319 use crate::capture::{Guard, VideoKind};
5320
5321 let width = 1920;
5322 let height = 1080;
5323 let stride = 1920 * 4;
5324 let expected_size = (stride * height) as usize;
5325 let mut data = vec![0u8; expected_size];
5326
5327 #[allow(clippy::unnecessary_cast)]
5330 let c_frame = NDIlib_video_frame_v2_t {
5331 xres: width,
5332 yres: height,
5333 #[cfg(target_os = "windows")]
5334 FourCC: 0xDEADBEEFu32 as i32, #[cfg(not(target_os = "windows"))]
5336 FourCC: 0xDEADBEEF, frame_rate_N: 60,
5338 frame_rate_D: 1,
5339 picture_aspect_ratio: 16.0 / 9.0,
5340 frame_format_type: ScanType::Progressive.into(),
5341 timecode: 0,
5342 p_data: data.as_mut_ptr(),
5343 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
5344 line_stride_in_bytes: stride,
5345 },
5346 p_metadata: ptr::null(),
5347 timestamp: 0,
5348 };
5349
5350 let mock_instance = ptr::null_mut();
5352 let guard = unsafe { Guard::<VideoKind>::new(mock_instance, c_frame) };
5353
5354 let result = unsafe { VideoFrameRef::new(guard) };
5356 assert!(result.is_err(), "Should reject unknown FourCC");
5357
5358 if let Err(Error::InvalidFrame(ref msg)) = result {
5359 assert!(
5360 msg.contains("0xDEADBEEF"),
5361 "Error message should include FourCC: {}",
5362 msg
5363 );
5364 } else {
5365 panic!("Expected InvalidFrame error");
5366 }
5367
5368 std::mem::forget(result);
5370 }
5371
5372 #[test]
5374 fn test_videoframeref_known_fourcc() {
5375 use crate::capture::{Guard, VideoKind};
5376
5377 let width = 1920;
5378 let height = 1080;
5379 let stride = 1920 * 4;
5380 let expected_size = (stride * height) as usize;
5381 let mut data = vec![0u8; expected_size];
5382
5383 let c_frame = NDIlib_video_frame_v2_t {
5384 xres: width,
5385 yres: height,
5386 FourCC: PixelFormat::BGRA.into(),
5387 frame_rate_N: 60,
5388 frame_rate_D: 1,
5389 picture_aspect_ratio: 16.0 / 9.0,
5390 frame_format_type: ScanType::Progressive.into(),
5391 timecode: 0,
5392 p_data: data.as_mut_ptr(),
5393 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
5394 line_stride_in_bytes: stride,
5395 },
5396 p_metadata: ptr::null(),
5397 timestamp: 0,
5398 };
5399
5400 let mock_instance = ptr::null_mut();
5401 let guard = unsafe { Guard::<VideoKind>::new(mock_instance, c_frame) };
5402
5403 let frame_ref = unsafe { VideoFrameRef::new(guard) }.expect("Should accept BGRA FourCC");
5404 assert_eq!(
5405 frame_ref.pixel_format(),
5406 PixelFormat::BGRA,
5407 "Should store validated pixel format"
5408 );
5409
5410 std::mem::forget(frame_ref);
5412 }
5413
5414 #[test]
5416 fn test_audioframeref_unknown_fourcc() {
5417 use crate::recv_guard::RecvAudioGuard;
5418
5419 let num_samples = 1024;
5420 let num_channels = 2;
5421 let sample_count = (num_samples * num_channels) as usize;
5422 let mut data = vec![0.0f32; sample_count];
5423
5424 let c_frame = NDIlib_audio_frame_v3_t {
5426 sample_rate: 48000,
5427 no_channels: num_channels,
5428 no_samples: num_samples,
5429 timecode: 0,
5430 FourCC: 0xBADC0DE, p_data: data.as_mut_ptr() as *mut u8,
5432 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
5433 channel_stride_in_bytes: num_samples * 4,
5434 },
5435 p_metadata: ptr::null(),
5436 timestamp: 0,
5437 };
5438
5439 let mock_instance = ptr::null_mut();
5440 let guard = unsafe { RecvAudioGuard::new(mock_instance, c_frame) };
5441
5442 let result = unsafe { AudioFrameRef::new(guard) };
5443 assert!(result.is_err(), "Should reject unknown audio FourCC");
5444
5445 if let Err(Error::InvalidFrame(ref msg)) = result {
5446 assert!(
5447 msg.contains("0x0BADC0DE"),
5448 "Error message should include FourCC: {}",
5449 msg
5450 );
5451 } else {
5452 panic!("Expected InvalidFrame error");
5453 }
5454
5455 std::mem::forget(result);
5456 }
5457
5458 #[test]
5460 fn test_audioframeref_known_fourcc() {
5461 use crate::recv_guard::RecvAudioGuard;
5462
5463 let num_samples = 1024;
5464 let num_channels = 2;
5465 let sample_count = (num_samples * num_channels) as usize;
5466 let mut data = vec![0.0f32; sample_count];
5467
5468 let c_frame = NDIlib_audio_frame_v3_t {
5469 sample_rate: 48000,
5470 no_channels: num_channels,
5471 no_samples: num_samples,
5472 timecode: 0,
5473 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
5474 p_data: data.as_mut_ptr() as *mut u8,
5475 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
5476 channel_stride_in_bytes: num_samples * 4,
5477 },
5478 p_metadata: ptr::null(),
5479 timestamp: 0,
5480 };
5481
5482 let mock_instance = ptr::null_mut();
5483 let guard = unsafe { RecvAudioGuard::new(mock_instance, c_frame) };
5484
5485 let frame_ref = unsafe { AudioFrameRef::new(guard) }.expect("Should accept FLTP FourCC");
5486 assert_eq!(
5487 frame_ref.format(),
5488 AudioFormat::FLTP,
5489 "Should store validated audio format"
5490 );
5491
5492 std::mem::forget(frame_ref);
5493 }
5494
5495 #[test]
5497 fn test_videoframeref_data_uses_validated_format() {
5498 use crate::capture::{Guard, VideoKind};
5499
5500 let width = 1920;
5502 let height = 1080;
5503 let stride = 1920 * 4;
5504 let expected_size = (stride * height) as usize;
5505 let mut data = vec![0xAB_u8; expected_size];
5506
5507 let c_frame = NDIlib_video_frame_v2_t {
5508 xres: width,
5509 yres: height,
5510 FourCC: PixelFormat::BGRA.into(),
5511 frame_rate_N: 60,
5512 frame_rate_D: 1,
5513 picture_aspect_ratio: 16.0 / 9.0,
5514 frame_format_type: ScanType::Progressive.into(),
5515 timecode: 0,
5516 p_data: data.as_mut_ptr(),
5517 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
5518 line_stride_in_bytes: stride,
5519 },
5520 p_metadata: ptr::null(),
5521 timestamp: 0,
5522 };
5523
5524 let mock_instance = ptr::null_mut();
5525 let guard = unsafe { Guard::<VideoKind>::new(mock_instance, c_frame) };
5526 let frame_ref = unsafe { VideoFrameRef::new(guard) }.expect("Should create frame ref");
5527
5528 assert_eq!(
5530 frame_ref.data().unwrap().len(),
5531 expected_size,
5532 "data() should use validated pixel format for size calculation"
5533 );
5534
5535 assert_eq!(
5537 frame_ref.line_stride_or_size(),
5538 LineStrideOrSize::LineStrideBytes(stride),
5539 "line_stride_or_size() should use validated format"
5540 );
5541
5542 std::mem::forget(frame_ref);
5543 }
5544
5545 #[test]
5550 fn test_receiver_and_framesync_video_refs_agree() {
5551 use crate::capture::{FrameSyncVideoFree, Guard, VideoKind};
5552
5553 let width = 16;
5554 let height = 8;
5555 let stride = width * 4;
5556 let len = (stride * height) as usize;
5557
5558 let mut recv_data: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
5561 let mut fs_data = recv_data.clone();
5562 let mut recv_meta = b"shared frame\0".to_vec();
5563 let mut fs_meta = recv_meta.clone();
5564
5565 let raw = |data: &mut [u8], meta: &mut [u8]| NDIlib_video_frame_v2_t {
5566 xres: width,
5567 yres: height,
5568 FourCC: PixelFormat::BGRA.into(),
5569 frame_rate_N: 30,
5570 frame_rate_D: 1,
5571 picture_aspect_ratio: 16.0 / 9.0,
5572 frame_format_type: ScanType::Interlaced.into(),
5575 timecode: 123,
5576 p_data: data.as_mut_ptr(),
5577 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
5578 line_stride_in_bytes: stride,
5579 },
5580 p_metadata: meta.as_mut_ptr().cast(),
5581 timestamp: 456,
5582 };
5583
5584 let recv_raw = raw(&mut recv_data, &mut recv_meta);
5585 let fs_raw = raw(&mut fs_data, &mut fs_meta);
5586
5587 let recv_guard = unsafe { Guard::<VideoKind>::new(ptr::null_mut(), recv_raw) };
5588 let recv = unsafe { VideoFrameRef::new(recv_guard) }.expect("receiver video ref");
5589 let fs_guard = unsafe { Guard::<FrameSyncVideoFree>::new(ptr::null_mut(), fs_raw) };
5590 let fs =
5591 unsafe { VideoRef::<FrameSyncVideoFree>::new(fs_guard) }.expect("framesync video ref");
5592
5593 assert_eq!(recv.width(), fs.width());
5594 assert_eq!(recv.height(), fs.height());
5595 assert_eq!(recv.pixel_format(), fs.pixel_format());
5596 assert_eq!(recv.frame_rate_n(), fs.frame_rate_n());
5597 assert_eq!(recv.frame_rate_d(), fs.frame_rate_d());
5598 assert_eq!(recv.picture_aspect_ratio(), fs.picture_aspect_ratio());
5599 assert_eq!(recv.scan_type(), fs.scan_type());
5600 assert_eq!(recv.scan_type(), ScanType::Interlaced);
5601 assert_eq!(recv.timecode(), fs.timecode());
5602 assert_eq!(recv.timestamp(), fs.timestamp());
5603 assert_eq!(recv.line_stride_or_size(), fs.line_stride_or_size());
5604 assert_eq!(recv.metadata(), fs.metadata());
5605 assert_eq!(recv.data(), fs.data());
5606
5607 std::mem::forget(recv);
5610 std::mem::forget(fs);
5611 }
5612
5613 #[test]
5617 fn test_video_ref_rejects_out_of_range_scan_type() {
5618 use crate::capture::{FrameSyncVideoFree, Guard, VideoKind};
5619
5620 let width = 16;
5621 let height = 8;
5622 let stride = width * 4;
5623 let len = (stride * height) as usize;
5624 let mut recv_data = vec![0u8; len];
5625 let mut fs_data = vec![0u8; len];
5626
5627 let raw = |data: &mut [u8]| NDIlib_video_frame_v2_t {
5628 xres: width,
5629 yres: height,
5630 FourCC: PixelFormat::BGRA.into(),
5631 frame_rate_N: 30,
5632 frame_rate_D: 1,
5633 picture_aspect_ratio: 16.0 / 9.0,
5634 frame_format_type: 0x0BAD_F00D, timecode: 0,
5636 p_data: data.as_mut_ptr(),
5637 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
5638 line_stride_in_bytes: stride,
5639 },
5640 p_metadata: ptr::null(),
5641 timestamp: 0,
5642 };
5643
5644 assert!(matches!(
5646 validate_video_layout(&raw(&mut recv_data)),
5647 Err(Error::InvalidFrame(_))
5648 ));
5649
5650 let recv_guard = unsafe { Guard::<VideoKind>::new(ptr::null_mut(), raw(&mut recv_data)) };
5652 assert!(matches!(
5653 unsafe { VideoFrameRef::new(recv_guard) },
5654 Err(Error::InvalidFrame(_))
5655 ));
5656
5657 let fs_guard =
5658 unsafe { Guard::<FrameSyncVideoFree>::new(ptr::null_mut(), raw(&mut fs_data)) };
5659 assert!(matches!(
5660 unsafe { VideoRef::<FrameSyncVideoFree>::new(fs_guard) },
5661 Err(Error::InvalidFrame(_))
5662 ));
5663 }
5664
5665 #[test]
5667 fn test_max_video_bytes_constant() {
5668 assert_eq!(MAX_VIDEO_BYTES, 256 * 1024 * 1024);
5670 }
5671
5672 #[test]
5674 fn test_max_audio_bytes_constant() {
5675 assert_eq!(MAX_AUDIO_BYTES, 64 * 1024 * 1024);
5677 }
5678
5679 #[test]
5681 fn test_max_metadata_bytes_constant() {
5682 assert_eq!(MAX_METADATA_BYTES, 4 * 1024 * 1024);
5683 }
5684
5685 fn metadata_raw_from_bytes(data: &mut [u8], length: i32) -> NDIlib_metadata_frame_t {
5686 NDIlib_metadata_frame_t {
5687 length,
5688 timecode: 12345,
5689 p_data: data.as_mut_ptr().cast::<c_char>(),
5690 }
5691 }
5692
5693 fn null_metadata_raw(length: i32) -> NDIlib_metadata_frame_t {
5694 NDIlib_metadata_frame_t {
5695 length,
5696 timecode: 12345,
5697 p_data: ptr::null_mut(),
5698 }
5699 }
5700
5701 #[test]
5702 fn test_validate_metadata_layout_accepts_empty_null_frame() {
5703 let raw = null_metadata_raw(0);
5704 let layout = unsafe { validate_metadata_layout(&raw) }.expect("empty null frame is valid");
5705
5706 assert_eq!(
5707 layout,
5708 ValidatedMetadataLayout {
5709 len_with_nul: 0,
5710 text_len: 0,
5711 }
5712 );
5713 }
5714
5715 #[test]
5716 fn test_validate_metadata_layout_accepts_one_byte_empty_payload() {
5717 let mut data = vec![0u8];
5718 let raw = metadata_raw_from_bytes(&mut data, 1);
5719 let layout =
5720 unsafe { validate_metadata_layout(&raw) }.expect("explicit empty payload is valid");
5721
5722 assert_eq!(layout.len_with_nul, 1);
5723 assert_eq!(layout.text_len, 0);
5724 }
5725
5726 #[test]
5727 fn test_validate_metadata_layout_accepts_utf8_with_bounded_length() {
5728 let mut data = "<ndi tally=\"preview\"/>".as_bytes().to_vec();
5729 data.push(0);
5730 let length = data.len() as i32;
5731 let raw = metadata_raw_from_bytes(&mut data, length);
5732 let layout = unsafe { validate_metadata_layout(&raw) }.expect("valid UTF-8 metadata");
5733
5734 assert_eq!(layout.len_with_nul, data.len());
5735 assert_eq!(layout.text_len, data.len() - 1);
5736 }
5737
5738 #[test]
5739 fn test_validate_metadata_layout_ignores_bytes_after_length() {
5740 let mut data = b"ok\0\0\xFF".to_vec();
5741 let raw = metadata_raw_from_bytes(&mut data, 3);
5742 let layout =
5743 unsafe { validate_metadata_layout(&raw) }.expect("extra bytes after length ignored");
5744
5745 assert_eq!(layout.text_len, 2);
5746 let owned = unsafe { MetadataFrame::from_raw_validated(&raw, layout) };
5747 assert_eq!(owned.data(), "ok");
5748 }
5749
5750 #[test]
5751 fn test_validate_metadata_layout_rejects_negative_length() {
5752 let raw = null_metadata_raw(-1);
5753
5754 assert!(matches!(
5755 unsafe { validate_metadata_layout(&raw) },
5756 Err(Error::InvalidFrame(_))
5757 ));
5758 }
5759
5760 #[test]
5761 fn test_validate_metadata_layout_accepts_lengthless_non_null_data() {
5762 let mut data = b"<ndi/>\0ignored".to_vec();
5763 let raw = metadata_raw_from_bytes(&mut data, 0);
5764 let layout = unsafe { validate_metadata_layout(&raw) }.expect("SDK C-string sentinel");
5765 assert_eq!(layout.text_len, 6);
5766 assert_eq!(layout.len_with_nul, 7);
5767 let owned = unsafe { MetadataFrame::from_raw_validated(&raw, layout) };
5768 assert_eq!(owned.data(), "<ndi/>");
5769 }
5770
5771 #[test]
5772 fn test_lengthless_metadata_rejects_invalid_utf8_and_missing_nul() {
5773 let mut invalid = vec![0xff, 0];
5774 assert!(matches!(
5775 unsafe { validate_metadata_layout(&metadata_raw_from_bytes(&mut invalid, 0)) },
5776 Err(Error::InvalidUtf8(_))
5777 ));
5778 let mut unterminated = vec![b'x'; MAX_METADATA_BYTES];
5779 assert!(unsafe {
5780 validate_metadata_layout(&metadata_raw_from_bytes(&mut unterminated, 0))
5781 }
5782 .is_err());
5783 }
5784
5785 #[test]
5786 fn test_validate_metadata_layout_rejects_nonzero_length_null_data() {
5787 let raw = null_metadata_raw(1);
5788
5789 assert!(matches!(
5790 unsafe { validate_metadata_layout(&raw) },
5791 Err(Error::InvalidFrame(_))
5792 ));
5793 }
5794
5795 #[test]
5796 fn test_validate_metadata_layout_rejects_missing_trailing_nul() {
5797 let mut data = b"abc".to_vec();
5798 let length = data.len() as i32;
5799 let raw = metadata_raw_from_bytes(&mut data, length);
5800
5801 assert!(matches!(
5802 unsafe { validate_metadata_layout(&raw) },
5803 Err(Error::InvalidFrame(_))
5804 ));
5805 }
5806
5807 #[test]
5808 fn test_validate_metadata_layout_rejects_interior_nul() {
5809 let mut data = b"a\0b\0".to_vec();
5810 let length = data.len() as i32;
5811 let raw = metadata_raw_from_bytes(&mut data, length);
5812
5813 assert!(matches!(
5814 unsafe { validate_metadata_layout(&raw) },
5815 Err(Error::InvalidFrame(_))
5816 ));
5817 }
5818
5819 #[test]
5820 fn test_validate_metadata_layout_rejects_oversized_length_before_reading() {
5821 let mut data = vec![0u8];
5822 let raw = metadata_raw_from_bytes(&mut data, (MAX_METADATA_BYTES + 1) as i32);
5823
5824 assert!(matches!(
5825 unsafe { validate_metadata_layout(&raw) },
5826 Err(Error::InvalidFrame(_))
5827 ));
5828 }
5829
5830 #[test]
5831 fn test_validate_metadata_layout_rejects_invalid_utf8() {
5832 let mut data = vec![0xFF, 0];
5833 let length = data.len() as i32;
5834 let raw = metadata_raw_from_bytes(&mut data, length);
5835
5836 assert!(matches!(
5837 unsafe { validate_metadata_layout(&raw) },
5838 Err(Error::InvalidUtf8(_))
5839 ));
5840 }
5841
5842 #[test]
5843 fn test_metadata_frame_constructor_and_accessors_preserve_text() {
5844 let frame = MetadataFrame::with_data("<ndi_product/>", 9876).unwrap();
5845
5846 assert_eq!(frame.data(), "<ndi_product/>");
5847 assert_eq!(frame.timecode(), 9876);
5848 assert_eq!(frame.clone().into_data(), "<ndi_product/>");
5849 }
5850
5851 #[test]
5852 fn test_metadata_frame_rejects_interior_nul_input() {
5853 assert!(matches!(
5854 MetadataFrame::with_data("bad\0metadata", 0),
5855 Err(Error::InvalidCString(_))
5856 ));
5857
5858 let mut frame = MetadataFrame::new();
5859 assert!(matches!(
5860 frame.set_data("bad\0metadata"),
5861 Err(Error::InvalidCString(_))
5862 ));
5863 }
5864
5865 #[test]
5866 fn test_metadata_frame_rejects_oversized_input() {
5867 let oversized = "x".repeat(MAX_METADATA_BYTES);
5868
5869 assert!(matches!(
5870 MetadataFrame::with_data(oversized, 0),
5871 Err(Error::InvalidFrame(_))
5872 ));
5873 }
5874
5875 #[test]
5876 fn test_metadata_frame_setters_preserve_invariants() {
5877 let mut frame = MetadataFrame::new();
5878
5879 frame.set_data("updated").unwrap();
5880 frame.set_timecode(42);
5881
5882 assert_eq!(frame.data(), "updated");
5883 assert_eq!(frame.timecode(), 42);
5884 assert_eq!(MetadataFrame::new().with_timecode(7).timecode(), 7);
5885 assert!(frame.set_data("bad\0text").is_err());
5886 assert_eq!(frame.data(), "updated");
5887 }
5888
5889 #[test]
5890 fn validated_metadata_reuses_storage_and_copies_once() {
5891 let text = "é".repeat(550);
5892 let frame = MetadataFrame::with_data(text, 9).unwrap();
5893 let expected = frame.data.as_ptr() as *mut c_char;
5894 let ((), allocations) = crate::test_alloc::allocation_count(|| {
5895 for _ in 0..100 {
5896 let raw = frame.to_raw();
5897 assert_eq!(raw.p_data, expected);
5898 assert_eq!(raw.length, 1101);
5899 assert_eq!(frame.data().len(), 1100);
5900 }
5901 });
5902 assert_eq!(allocations, 0);
5903
5904 let bytes = frame.data.inner.as_bytes_with_nul();
5905 let layout = ValidatedFrameMetadata {
5906 len_with_nul: NonZeroUsize::new(bytes.len()),
5907 text_len: bytes.len() - 1,
5908 };
5909 let (copy, allocations) = crate::test_alloc::allocation_count(|| unsafe {
5910 FrameMetadata::copy_from_raw_validated(bytes.as_ptr().cast(), layout).unwrap()
5911 });
5912 assert_eq!(allocations, 1);
5913 assert_eq!(copy.as_str(), frame.data());
5914 let (text, allocations) = crate::test_alloc::allocation_count(|| copy.into_string());
5915 assert_eq!(allocations, 0);
5916 assert_eq!(text, frame.data());
5917 }
5918
5919 #[test]
5920 fn test_metadata_frame_from_raw_validated_copies_only_payload() {
5921 let mut data = b"copy-me\0\0\xFF".to_vec();
5922 let raw = metadata_raw_from_bytes(&mut data, 8);
5923 let layout = unsafe { validate_metadata_layout(&raw) }.unwrap();
5924 let frame = unsafe { MetadataFrame::from_raw_validated(&raw, layout) };
5925
5926 assert_eq!(frame.data(), "copy-me");
5927 assert_eq!(frame.timecode(), 12345);
5928 }
5929
5930 #[test]
5931 fn test_metadata_frame_from_raw_reports_invalid_utf8() {
5932 let mut data = vec![0xFF, 0];
5933 let raw = metadata_raw_from_bytes(&mut data, 2);
5934
5935 assert!(matches!(
5936 unsafe { MetadataFrame::from_raw(&raw) },
5937 Err(Error::InvalidUtf8(_))
5938 ));
5939 }
5940
5941 #[test]
5942 fn test_metadata_frame_to_raw_includes_trailing_nul() {
5943 let frame = MetadataFrame::with_data("abc", 101).unwrap();
5944 let raw = frame.to_raw();
5945
5946 assert_eq!(raw.length, 4);
5947 assert_eq!(raw.timecode, 101);
5948 assert_eq!(raw.p_data, frame.data.as_ptr() as *mut c_char);
5949 assert_eq!(frame.data.inner.as_bytes_with_nul(), b"abc\0");
5950 }
5951
5952 #[test]
5953 fn test_empty_metadata_frame_to_raw_sends_explicit_nul() {
5954 let frame = MetadataFrame::new();
5955 let raw = frame.to_raw();
5956
5957 assert_eq!(raw.length, 1);
5958 assert_eq!(frame.data.inner.as_bytes_with_nul(), b"\0");
5959 }
5960
5961 #[test]
5962 fn test_metadata_raw_length_conversion_is_checked() {
5963 assert!(metadata_len_to_i32(i32::MAX as usize).is_ok());
5964 assert!(matches!(
5965 metadata_len_to_i32(i32::MAX as usize + 1),
5966 Err(Error::InvalidFrame(_))
5967 ));
5968 }
5969
5970 #[test]
5971 fn test_metadata_frame_ref_uses_cached_layout() {
5972 use crate::capture::RecvMetadataGuard;
5973
5974 let mut data = b"zero-copy\0\0\xFF".to_vec();
5975 let raw = metadata_raw_from_bytes(&mut data, 10);
5976 let guard = unsafe { RecvMetadataGuard::new(ptr::null_mut(), raw) };
5977 let frame_ref = unsafe { MetadataFrameRef::new(guard) }.expect("valid metadata ref");
5978
5979 assert_eq!(frame_ref.data(), "zero-copy");
5980 assert_eq!(frame_ref.as_bytes(), b"zero-copy");
5981 assert_eq!(frame_ref.layout.text_len, 9);
5982
5983 let owned = frame_ref.to_owned();
5984 assert_eq!(owned.data(), "zero-copy");
5985 assert_eq!(owned.timecode(), 12345);
5986
5987 std::mem::forget(frame_ref);
5988 }
5989
5990 #[test]
5991 fn test_validate_frame_metadata_accepts_null_without_allocation_state() {
5992 let layout = unsafe { validate_frame_metadata(ptr::null()) }.expect("null is valid");
5993
5994 assert_eq!(
5995 layout,
5996 ValidatedFrameMetadata {
5997 len_with_nul: None,
5998 text_len: 0,
5999 }
6000 );
6001 }
6002
6003 #[test]
6004 fn test_validate_frame_metadata_accepts_explicit_empty() {
6005 let mut data = b"\0".to_vec();
6006 let layout = unsafe { validate_frame_metadata(data.as_mut_ptr().cast::<c_char>()) }
6007 .expect("explicit empty metadata is valid");
6008
6009 assert_eq!(layout.len_with_nul.unwrap().get(), 1);
6010 assert_eq!(layout.text_len, 0);
6011 assert_eq!(
6012 unsafe { frame_metadata_str(data.as_ptr().cast::<c_char>(), layout) },
6013 Some("")
6014 );
6015 }
6016
6017 #[test]
6018 fn test_validate_frame_metadata_accepts_utf8_and_ignores_after_first_nul() {
6019 let mut data = b"hello metadata\0\0\xFF".to_vec();
6020 let layout = unsafe { validate_frame_metadata(data.as_mut_ptr().cast::<c_char>()) }
6021 .expect("valid UTF-8 metadata");
6022
6023 assert_eq!(layout.text_len, "hello metadata".len());
6024 assert_eq!(
6025 unsafe { frame_metadata_str(data.as_ptr().cast::<c_char>(), layout) },
6026 Some("hello metadata")
6027 );
6028 }
6029
6030 #[test]
6031 fn test_validate_frame_metadata_accepts_max_boundary() {
6032 let mut data = vec![b'x'; MAX_METADATA_BYTES];
6033 data[MAX_METADATA_BYTES - 1] = 0;
6034
6035 let layout = unsafe { validate_frame_metadata(data.as_mut_ptr().cast::<c_char>()) }
6036 .expect("metadata exactly at cap including terminator is valid");
6037
6038 assert_eq!(layout.len_with_nul.unwrap().get(), MAX_METADATA_BYTES);
6039 assert_eq!(layout.text_len, MAX_METADATA_BYTES - 1);
6040 }
6041
6042 #[test]
6043 fn test_validate_frame_metadata_rejects_missing_nul_within_cap() {
6044 let mut data = vec![b'x'; MAX_METADATA_BYTES];
6045
6046 assert!(matches!(
6047 unsafe { validate_frame_metadata(data.as_mut_ptr().cast::<c_char>()) },
6048 Err(Error::InvalidFrame(_))
6049 ));
6050 }
6051
6052 #[test]
6053 fn test_validate_frame_metadata_rejects_invalid_utf8_before_nul() {
6054 let mut data = vec![0xFF, 0];
6055
6056 assert!(matches!(
6057 unsafe { validate_frame_metadata(data.as_mut_ptr().cast::<c_char>()) },
6058 Err(Error::InvalidUtf8(_))
6059 ));
6060 }
6061
6062 #[test]
6063 fn test_video_frame_ref_metadata_is_cached_and_owned_conversion_appends_nul() {
6064 use crate::capture::{Guard, VideoKind};
6065
6066 let width = 16;
6067 let height = 8;
6068 let stride = width * 4;
6069 let expected_len = (stride * height) as usize;
6070 let data = vec![0u8; expected_len];
6071 let mut metadata = b"cached\0\xFF".to_vec();
6072
6073 let raw = NDIlib_video_frame_v2_t {
6074 xres: width,
6075 yres: height,
6076 FourCC: PixelFormat::BGRA.into(),
6077 frame_rate_N: 60,
6078 frame_rate_D: 1,
6079 picture_aspect_ratio: 16.0 / 9.0,
6080 frame_format_type: ScanType::Progressive.into(),
6081 timecode: 123,
6082 p_data: data.as_ptr() as *mut u8,
6083 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
6084 line_stride_in_bytes: stride,
6085 },
6086 p_metadata: metadata.as_mut_ptr().cast::<c_char>(),
6087 timestamp: 456,
6088 };
6089
6090 let guard = unsafe { Guard::<VideoKind>::new(ptr::null_mut(), raw) };
6091 let frame_ref = unsafe { VideoFrameRef::new(guard) }.expect("valid video ref");
6092
6093 assert_eq!(frame_ref.metadata(), Some("cached"));
6094 metadata[6] = b'!';
6095 assert_eq!(frame_ref.metadata(), Some("cached"));
6096 assert!(format!("{frame_ref:?}").contains("metadata: Some(\"cached\")"));
6097
6098 let owned = frame_ref.to_owned().expect("owned conversion");
6099 assert_eq!(owned.metadata(), Some("cached"));
6100 assert_eq!(owned.data().len(), expected_len);
6101
6102 std::mem::forget(frame_ref);
6103 }
6104
6105 #[test]
6106 fn test_audio_frame_ref_metadata_is_text_and_owned_conversion_preserves_it() {
6107 use crate::capture::RecvAudioGuard;
6108
6109 let no_samples = 4;
6110 let no_channels = 2;
6111 let sample_count = (no_samples * no_channels) as usize;
6112 let data = vec![0.25f32; sample_count];
6113 let mut metadata = b"audio meta\0".to_vec();
6114
6115 let raw = NDIlib_audio_frame_v3_t {
6116 sample_rate: 48000,
6117 no_channels,
6118 no_samples,
6119 timecode: 123,
6120 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
6121 p_data: data.as_ptr() as *mut u8,
6122 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
6123 channel_stride_in_bytes: no_samples * 4,
6124 },
6125 p_metadata: metadata.as_mut_ptr().cast::<c_char>(),
6126 timestamp: 456,
6127 };
6128
6129 let guard = unsafe { RecvAudioGuard::new(ptr::null_mut(), raw) };
6130 let frame_ref = unsafe { AudioFrameRef::new(guard) }.expect("valid audio ref");
6131
6132 assert_eq!(frame_ref.metadata(), Some("audio meta"));
6133 let owned = frame_ref.to_owned().expect("owned conversion");
6134 assert_eq!(owned.metadata(), Some("audio meta"));
6135 assert_eq!(owned.data().len(), sample_count);
6136
6137 std::mem::forget(frame_ref);
6138 }
6139
6140 #[test]
6141 fn test_owned_video_from_raw_rejects_malformed_frame_metadata() {
6142 let width = 16;
6143 let height = 8;
6144 let stride = width * 4;
6145 let expected_len = (stride * height) as usize;
6146 let mut data = vec![0u8; expected_len];
6147 let mut metadata = vec![b'x'; MAX_METADATA_BYTES];
6148
6149 let raw = NDIlib_video_frame_v2_t {
6150 xres: width,
6151 yres: height,
6152 FourCC: PixelFormat::BGRA.into(),
6153 frame_rate_N: 60,
6154 frame_rate_D: 1,
6155 picture_aspect_ratio: 16.0 / 9.0,
6156 frame_format_type: ScanType::Progressive.into(),
6157 timecode: 0,
6158 p_data: data.as_mut_ptr(),
6159 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
6160 line_stride_in_bytes: stride,
6161 },
6162 p_metadata: metadata.as_mut_ptr().cast::<c_char>(),
6163 timestamp: 0,
6164 };
6165
6166 assert!(matches!(
6167 unsafe { VideoFrame::from_raw(&raw) },
6168 Err(Error::InvalidFrame(_))
6169 ));
6170
6171 metadata[0] = 0xFF;
6172 metadata[1] = 0;
6173 assert!(matches!(
6174 unsafe { VideoFrame::from_raw(&raw) },
6175 Err(Error::InvalidUtf8(_))
6176 ));
6177 }
6178
6179 #[test]
6180 fn test_owned_frame_metadata_builders_setters_and_raw_conversion() {
6181 let mut video = VideoFrame::builder().metadata("").build().unwrap();
6182 assert_eq!(video.metadata(), Some(""));
6183 let raw = video.to_raw();
6184 assert!(!raw.p_metadata.is_null());
6185 assert_eq!(
6186 unsafe { slice::from_raw_parts(raw.p_metadata.cast::<u8>(), 1) },
6187 b"\0"
6188 );
6189
6190 video.set_metadata(Some("video meta")).unwrap();
6191 assert_eq!(video.metadata(), Some("video meta"));
6192 let raw = video.to_raw();
6193 assert_eq!(
6194 unsafe { slice::from_raw_parts(raw.p_metadata.cast::<u8>(), 11) },
6195 b"video meta\0"
6196 );
6197
6198 video.set_metadata(Option::<String>::None).unwrap();
6199 assert!(video.metadata().is_none());
6200 assert!(video.to_raw().p_metadata.is_null());
6201
6202 let mut audio = AudioFrame::builder().metadata("").build().unwrap();
6203 assert_eq!(audio.metadata(), Some(""));
6204 assert!(!audio.to_raw().p_metadata.is_null());
6205 audio.set_metadata(Some("audio meta")).unwrap();
6206 assert_eq!(audio.metadata(), Some("audio meta"));
6207
6208 assert!(matches!(
6209 VideoFrame::builder().metadata("bad\0metadata").build(),
6210 Err(Error::InvalidCString(_))
6211 ));
6212 assert!(matches!(
6213 AudioFrame::builder().metadata("bad\0metadata").build(),
6214 Err(Error::InvalidCString(_))
6215 ));
6216 assert!(matches!(
6217 video.set_metadata(Some("bad\0metadata")),
6218 Err(Error::InvalidCString(_))
6219 ));
6220 assert!(matches!(
6221 audio.set_metadata(Some("bad\0metadata")),
6222 Err(Error::InvalidCString(_))
6223 ));
6224
6225 let oversized = "x".repeat(MAX_METADATA_BYTES);
6226 assert!(matches!(
6227 VideoFrame::builder().metadata(oversized.clone()).build(),
6228 Err(Error::InvalidFrame(_))
6229 ));
6230 assert!(matches!(
6231 AudioFrame::builder().metadata(oversized.clone()).build(),
6232 Err(Error::InvalidFrame(_))
6233 ));
6234 assert!(matches!(
6235 video.set_metadata(Some(oversized.clone())),
6236 Err(Error::InvalidFrame(_))
6237 ));
6238 assert!(matches!(
6239 audio.set_metadata(Some(oversized)),
6240 Err(Error::InvalidFrame(_))
6241 ));
6242 }
6243
6244 #[test]
6246 fn test_audio_overflow_checked_mul() {
6247 let no_samples = 1024;
6249 let no_channels = i32::MAX;
6250 let mut dummy_data = vec![0f32; 1024]; let raw = NDIlib_audio_frame_v3_t {
6253 sample_rate: 48000,
6254 no_channels,
6255 no_samples,
6256 timecode: 0,
6257 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
6258 p_data: dummy_data.as_mut_ptr() as *mut u8,
6259 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
6260 channel_stride_in_bytes: no_samples * 4,
6261 },
6262 p_metadata: ptr::null(),
6263 timestamp: 0,
6264 };
6265
6266 let result = unsafe { AudioFrame::from_raw(raw) };
6267 assert!(
6268 result.is_err(),
6269 "Should reject audio frame with sample count overflow or exceeding size limit"
6270 );
6271
6272 if let Err(Error::InvalidFrame(msg)) = result {
6273 assert!(
6275 msg.contains("overflow") || msg.contains("exceeds maximum size"),
6276 "Error message should mention overflow or size limit, got: {msg}"
6277 );
6278 } else {
6279 panic!("Expected InvalidFrame error");
6280 }
6281 }
6282
6283 #[test]
6285 fn test_audio_within_bounds_succeeds() {
6286 let sample_rate = 48000;
6288 let no_channels = 2;
6289 let no_samples = 1024;
6290 let sample_count = (no_samples * no_channels) as usize;
6291 let mut data = vec![0.5f32; sample_count];
6292
6293 let raw = NDIlib_audio_frame_v3_t {
6294 sample_rate,
6295 no_channels,
6296 no_samples,
6297 timecode: 12345,
6298 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
6299 p_data: data.as_mut_ptr() as *mut u8,
6300 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
6301 channel_stride_in_bytes: no_samples * 4,
6302 },
6303 p_metadata: ptr::null(),
6304 timestamp: 67890,
6305 };
6306
6307 let result = unsafe { AudioFrame::from_raw(raw) };
6308 assert!(
6309 result.is_ok(),
6310 "Should accept normal audio frame within bounds"
6311 );
6312
6313 let frame = result.unwrap();
6314 assert_eq!(frame.data().len(), sample_count);
6315 assert_eq!(frame.num_samples(), no_samples);
6316 assert_eq!(frame.num_channels(), no_channels);
6317 }
6318
6319 #[test]
6320 fn test_audio_builder_rejects_invalid_dimensions() {
6321 assert!(matches!(
6322 AudioFrame::builder().sample_rate(0).build(),
6323 Err(Error::InvalidFrame(_))
6324 ));
6325 assert!(matches!(
6326 AudioFrame::builder().channels(0).build(),
6327 Err(Error::InvalidFrame(_))
6328 ));
6329 assert!(matches!(
6330 AudioFrame::builder().samples(0).build(),
6331 Err(Error::InvalidFrame(_))
6332 ));
6333 assert!(matches!(
6334 AudioFrame::builder().samples(-1).build(),
6335 Err(Error::InvalidFrame(_))
6336 ));
6337 }
6338
6339 #[test]
6340 fn test_audio_builder_rejects_oversized_layout() {
6341 let samples = (MAX_AUDIO_BYTES / std::mem::size_of::<f32>()) as i32 + 1;
6342 let result = AudioFrame::builder().channels(1).samples(samples).build();
6343
6344 assert!(matches!(result, Err(Error::InvalidFrame(_))));
6345 }
6346
6347 #[test]
6348 fn test_default_outbound_timecodes_are_synthesized() {
6349 assert_eq!(
6350 VideoFrame::builder()
6351 .resolution(16, 16)
6352 .build()
6353 .unwrap()
6354 .timecode(),
6355 NDIlib_send_timecode_synthesize
6356 );
6357 assert_eq!(
6358 AudioFrame::builder().samples(1).build().unwrap().timecode(),
6359 NDIlib_send_timecode_synthesize
6360 );
6361 assert_eq!(
6362 MetadataFrame::new().timecode(),
6363 NDIlib_send_timecode_synthesize
6364 );
6365 assert_eq!(
6366 VideoFrame::builder()
6367 .resolution(16, 16)
6368 .timecode(0)
6369 .build()
6370 .unwrap()
6371 .timecode(),
6372 0
6373 );
6374 assert_eq!(
6375 AudioFrame::builder()
6376 .samples(1)
6377 .timecode(0)
6378 .build()
6379 .unwrap()
6380 .timecode(),
6381 0
6382 );
6383 assert_eq!(MetadataFrame::with_data("<x/>", 0).unwrap().timecode(), 0);
6384 }
6385
6386 #[test]
6387 fn test_video_builder_rejects_invalid_send_metadata() {
6388 assert!(matches!(
6389 VideoFrame::builder().frame_rate(0, 1).build(),
6390 Err(Error::InvalidFrame(_))
6391 ));
6392 assert!(matches!(
6393 VideoFrame::builder().frame_rate(30, 0).build(),
6394 Err(Error::InvalidFrame(_))
6395 ));
6396 assert!(matches!(
6397 VideoFrame::builder().aspect_ratio(f32::NAN).build(),
6398 Err(Error::InvalidFrame(_))
6399 ));
6400 assert!(matches!(
6401 VideoFrame::builder().aspect_ratio(-1.0).build(),
6402 Err(Error::InvalidFrame(_))
6403 ));
6404 }
6405
6406 #[test]
6407 fn test_zero_aspect_ratio_uses_sdk_square_pixel_sentinel() {
6408 let frame = VideoFrame::builder()
6409 .resolution(16, 16)
6410 .aspect_ratio(0.0)
6411 .build()
6412 .unwrap();
6413 assert_eq!(frame.picture_aspect_ratio(), 0.0);
6414 assert_eq!(frame.to_raw().picture_aspect_ratio, 0.0);
6415 }
6416
6417 #[test]
6418 fn test_owned_frame_data_replacement_preserves_layout_size() {
6419 let mut video = VideoFrame::builder()
6420 .resolution(16, 16)
6421 .pixel_format(PixelFormat::BGRA)
6422 .build()
6423 .unwrap();
6424 assert!(video.replace_data(vec![0; video.data().len() - 1]).is_err());
6425
6426 let mut audio = AudioFrame::builder()
6427 .channels(2)
6428 .samples(16)
6429 .build()
6430 .unwrap();
6431 assert!(audio
6432 .replace_data(vec![0.0; audio.data().len() + 1])
6433 .is_err());
6434 }
6435
6436 #[test]
6437 fn test_video_cap_allows_8k_bgra_without_allocating() {
6438 let layout =
6439 ValidatedVideoLayout::new_uncompressed(PixelFormat::BGRA, 7680, 4320, None).unwrap();
6440 assert_eq!(layout.data_len_bytes, 132_710_400);
6441 assert!(matches!(
6442 ValidatedVideoLayout::new_uncompressed(PixelFormat::BGRA, 16_384, 8_640, None),
6443 Err(Error::InvalidFrame(_))
6444 ));
6445 }
6446
6447 #[test]
6453 fn test_validate_video_layout_valid_uncompressed() {
6454 let width = 1920;
6455 let height = 1080;
6456 let stride = width * 4; let expected_size = (stride * height) as usize;
6458 let mut data = vec![0u8; expected_size];
6459
6460 let raw = NDIlib_video_frame_v2_t {
6461 xres: width,
6462 yres: height,
6463 FourCC: PixelFormat::BGRA.into(),
6464 frame_rate_N: 60,
6465 frame_rate_D: 1,
6466 picture_aspect_ratio: 16.0 / 9.0,
6467 frame_format_type: ScanType::Progressive.into(),
6468 timecode: 0,
6469 p_data: data.as_mut_ptr(),
6470 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
6471 line_stride_in_bytes: stride,
6472 },
6473 p_metadata: ptr::null(),
6474 timestamp: 0,
6475 };
6476
6477 let result = validate_video_layout(&raw);
6478 assert!(result.is_ok(), "Should validate valid uncompressed frame");
6479
6480 let layout = result.unwrap();
6481 assert_eq!(layout.pixel_format, PixelFormat::BGRA);
6482 assert_eq!(layout.data_len_bytes, expected_size);
6483 assert_eq!(
6484 layout.line_stride_or_size,
6485 LineStrideOrSize::LineStrideBytes(stride)
6486 );
6487 }
6488
6489 #[test]
6492 fn test_validate_video_layout_semi_planar_422_and_alpha() {
6493 let width = 256;
6494 let height = 64;
6495 let stride = width * 2;
6496 let plane = (stride * height) as usize;
6497
6498 for (format, expected_size) in [
6499 (PixelFormat::UYVA, plane + plane / 2),
6500 (PixelFormat::P216, plane * 2),
6501 (PixelFormat::PA16, plane * 3),
6502 ] {
6503 let mut data = vec![0u8; expected_size];
6504
6505 let raw = NDIlib_video_frame_v2_t {
6506 xres: width,
6507 yres: height,
6508 FourCC: format.into(),
6509 frame_rate_N: 60,
6510 frame_rate_D: 1,
6511 picture_aspect_ratio: 16.0 / 9.0,
6512 frame_format_type: ScanType::Progressive.into(),
6513 timecode: 0,
6514 p_data: data.as_mut_ptr(),
6515 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
6516 line_stride_in_bytes: stride,
6517 },
6518 p_metadata: ptr::null(),
6519 timestamp: 0,
6520 };
6521
6522 let layout = validate_video_layout(&raw)
6523 .unwrap_or_else(|e| panic!("{format:?} stride {stride} rejected: {e}"));
6524 assert_eq!(layout.pixel_format, format);
6525 assert_eq!(layout.data_len_bytes, expected_size, "{format:?}");
6526 assert_eq!(
6527 layout.line_stride_or_size,
6528 LineStrideOrSize::LineStrideBytes(stride)
6529 );
6530 }
6531 }
6532
6533 #[test]
6536 fn test_validate_video_layout_rejects_odd_uyva_stride() {
6537 let width = 256;
6538 let height = 64;
6539 let stride = width * 2 + 1;
6540 let mut data = vec![0u8; (stride * height * 2) as usize];
6541
6542 let raw = NDIlib_video_frame_v2_t {
6543 xres: width,
6544 yres: height,
6545 FourCC: PixelFormat::UYVA.into(),
6546 frame_rate_N: 60,
6547 frame_rate_D: 1,
6548 picture_aspect_ratio: 16.0 / 9.0,
6549 frame_format_type: ScanType::Progressive.into(),
6550 timecode: 0,
6551 p_data: data.as_mut_ptr(),
6552 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
6553 line_stride_in_bytes: stride,
6554 },
6555 p_metadata: ptr::null(),
6556 timestamp: 0,
6557 };
6558
6559 assert!(matches!(
6560 validate_video_layout(&raw),
6561 Err(Error::InvalidFrame(_))
6562 ));
6563 }
6564
6565 #[test]
6567 fn test_validate_video_layout_null_pointer() {
6568 let raw = NDIlib_video_frame_v2_t {
6569 xres: 1920,
6570 yres: 1080,
6571 FourCC: PixelFormat::BGRA.into(),
6572 frame_rate_N: 60,
6573 frame_rate_D: 1,
6574 picture_aspect_ratio: 16.0 / 9.0,
6575 frame_format_type: ScanType::Progressive.into(),
6576 timecode: 0,
6577 p_data: ptr::null_mut(),
6578 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
6579 line_stride_in_bytes: 7680,
6580 },
6581 p_metadata: ptr::null(),
6582 timestamp: 0,
6583 };
6584
6585 let result = validate_video_layout(&raw);
6586 assert!(result.is_err(), "Should reject null data pointer");
6587
6588 if let Err(Error::InvalidFrame(msg)) = result {
6589 assert!(
6590 msg.contains("null data pointer"),
6591 "Error should mention null pointer, got: {msg}"
6592 );
6593 } else {
6594 panic!("Expected InvalidFrame error");
6595 }
6596 }
6597
6598 #[test]
6600 fn test_validate_video_layout_invalid_stride() {
6601 let mut data = vec![0u8; 1024];
6602
6603 let raw = NDIlib_video_frame_v2_t {
6604 xres: 1920,
6605 yres: 1080,
6606 FourCC: PixelFormat::BGRA.into(),
6607 frame_rate_N: 60,
6608 frame_rate_D: 1,
6609 picture_aspect_ratio: 16.0 / 9.0,
6610 frame_format_type: ScanType::Progressive.into(),
6611 timecode: 0,
6612 p_data: data.as_mut_ptr(),
6613 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
6614 line_stride_in_bytes: -1, },
6616 p_metadata: ptr::null(),
6617 timestamp: 0,
6618 };
6619
6620 let result = validate_video_layout(&raw);
6621 assert!(result.is_err(), "Should reject negative line_stride");
6622
6623 if let Err(Error::InvalidFrame(msg)) = result {
6624 assert!(
6625 msg.contains("invalid line_stride_in_bytes"),
6626 "Error should mention invalid stride, got: {msg}"
6627 );
6628 } else {
6629 panic!("Expected InvalidFrame error");
6630 }
6631 }
6632
6633 #[test]
6634 fn test_validate_video_layout_accepts_default_zero_stride() {
6635 let mut data = vec![0u8; 16 * 16 * 4];
6636 let raw = NDIlib_video_frame_v2_t {
6637 xres: 16,
6638 yres: 16,
6639 FourCC: PixelFormat::BGRA.into(),
6640 frame_rate_N: 60,
6641 frame_rate_D: 1,
6642 picture_aspect_ratio: 1.0,
6643 frame_format_type: ScanType::Progressive.into(),
6644 timecode: 0,
6645 p_data: data.as_mut_ptr(),
6646 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
6647 line_stride_in_bytes: 0,
6648 },
6649 p_metadata: ptr::null(),
6650 timestamp: 0,
6651 };
6652 let layout = validate_video_layout(&raw).unwrap();
6653 assert_eq!(layout.data_len_bytes, data.len());
6654 assert_eq!(
6655 layout.line_stride_or_size,
6656 LineStrideOrSize::LineStrideBytes(64)
6657 );
6658 let owned = unsafe { VideoFrame::from_raw(&raw) }.unwrap();
6659 assert_eq!(owned.data().len(), data.len());
6660 }
6661
6662 #[test]
6664 fn test_validate_video_layout_rejects_short_stride() {
6665 let mut data = vec![0u8; 1024];
6666
6667 let raw = NDIlib_video_frame_v2_t {
6668 xres: 1920,
6669 yres: 1080,
6670 FourCC: PixelFormat::BGRA.into(),
6671 frame_rate_N: 60,
6672 frame_rate_D: 1,
6673 picture_aspect_ratio: 16.0 / 9.0,
6674 frame_format_type: ScanType::Progressive.into(),
6675 timecode: 0,
6676 p_data: data.as_mut_ptr(),
6677 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
6678 line_stride_in_bytes: 1919 * 4,
6679 },
6680 p_metadata: ptr::null(),
6681 timestamp: 0,
6682 };
6683
6684 let result = validate_video_layout(&raw);
6685 assert!(matches!(result, Err(Error::InvalidFrame(_))));
6686 }
6687
6688 #[test]
6690 fn test_validate_video_layout_rejects_planar_odd_dimensions() {
6691 let mut data = vec![0u8; 4096];
6692
6693 let raw = NDIlib_video_frame_v2_t {
6694 xres: 641,
6695 yres: 480,
6696 FourCC: PixelFormat::I420.into(),
6697 frame_rate_N: 60,
6698 frame_rate_D: 1,
6699 picture_aspect_ratio: 4.0 / 3.0,
6700 frame_format_type: ScanType::Progressive.into(),
6701 timecode: 0,
6702 p_data: data.as_mut_ptr(),
6703 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
6704 line_stride_in_bytes: 642,
6705 },
6706 p_metadata: ptr::null(),
6707 timestamp: 0,
6708 };
6709
6710 let result = validate_video_layout(&raw);
6711 assert!(matches!(result, Err(Error::InvalidFrame(_))));
6712 }
6713
6714 #[test]
6716 fn test_validate_video_layout_negative_dimensions() {
6717 let mut data = vec![0u8; 1024];
6718
6719 let raw = NDIlib_video_frame_v2_t {
6720 xres: 1920,
6721 yres: -1, FourCC: PixelFormat::BGRA.into(),
6723 frame_rate_N: 60,
6724 frame_rate_D: 1,
6725 picture_aspect_ratio: 16.0 / 9.0,
6726 frame_format_type: ScanType::Progressive.into(),
6727 timecode: 0,
6728 p_data: data.as_mut_ptr(),
6729 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
6730 line_stride_in_bytes: 7680,
6731 },
6732 p_metadata: ptr::null(),
6733 timestamp: 0,
6734 };
6735
6736 let result = validate_video_layout(&raw);
6737 assert!(result.is_err(), "Should reject negative height");
6738
6739 if let Err(Error::InvalidFrame(msg)) = result {
6740 assert!(
6741 msg.contains("invalid height"),
6742 "Error should mention invalid height, got: {msg}"
6743 );
6744 } else {
6745 panic!("Expected InvalidFrame error");
6746 }
6747 }
6748
6749 #[test]
6751 fn test_validate_video_layout_exceeds_max() {
6752 let width = 16_384;
6754 let height = 8_640;
6755 let stride = width * 4;
6756 let mut data = [0u8; 1];
6757
6758 let raw = NDIlib_video_frame_v2_t {
6759 xres: width,
6760 yres: height,
6761 FourCC: PixelFormat::BGRA.into(),
6762 frame_rate_N: 60,
6763 frame_rate_D: 1,
6764 picture_aspect_ratio: 16.0 / 9.0,
6765 frame_format_type: ScanType::Progressive.into(),
6766 timecode: 0,
6767 p_data: data.as_mut_ptr(),
6768 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
6769 line_stride_in_bytes: stride,
6770 },
6771 p_metadata: ptr::null(),
6772 timestamp: 0,
6773 };
6774
6775 let result = validate_video_layout(&raw);
6776 assert!(result.is_err(), "Should reject oversized frame");
6777
6778 if let Err(Error::InvalidFrame(msg)) = result {
6779 assert!(
6780 msg.contains("exceeds maximum size"),
6781 "Error should mention size limit, got: {msg}"
6782 );
6783 } else {
6784 panic!("Expected InvalidFrame error");
6785 }
6786 }
6787
6788 #[test]
6790 fn test_validate_audio_layout_valid() {
6791 let no_samples = 1024;
6792 let no_channels = 2;
6793 let sample_count = (no_samples * no_channels) as usize;
6794 let mut data = vec![0.0f32; sample_count];
6795
6796 let raw = NDIlib_audio_frame_v3_t {
6797 sample_rate: 48000,
6798 no_channels,
6799 no_samples,
6800 timecode: 0,
6801 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
6802 p_data: data.as_mut_ptr() as *mut u8,
6803 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
6804 channel_stride_in_bytes: no_samples * 4,
6805 },
6806 p_metadata: ptr::null(),
6807 timestamp: 0,
6808 };
6809
6810 let result = validate_audio_layout(&raw);
6811 assert!(result.is_ok(), "Should validate valid audio frame");
6812
6813 let layout = result.unwrap();
6814 assert_eq!(layout.format, Some(AudioFormat::FLTP));
6815 assert_eq!(layout.sample_count, sample_count);
6816 }
6817
6818 #[test]
6819 fn test_audio_channel_views_and_owned_conversion_skip_padding() {
6820 use crate::capture::FrameSyncAudioFree;
6821 use std::mem::MaybeUninit;
6822 let mut storage = [MaybeUninit::<f32>::uninit(); 10];
6824 for (index, value) in [(0, 1.0), (1, 2.0), (2, 3.0), (6, 4.0), (7, 5.0), (8, 6.0)] {
6825 storage[index].write(value);
6826 }
6827 let raw = NDIlib_audio_frame_v3_t {
6828 sample_rate: 48000,
6829 no_channels: 2,
6830 no_samples: 3,
6831 FourCC: AudioFormat::FLTP.into(),
6832 p_data: storage.as_mut_ptr().cast(),
6833 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
6834 channel_stride_in_bytes: 24,
6835 },
6836 timecode: 123,
6837 timestamp: 456,
6838 p_metadata: ptr::null(),
6839 };
6840 let guard = unsafe { Guard::<FrameSyncAudioFree>::new(ptr::null_mut(), raw) };
6841 let frame = unsafe { AudioRef::<FrameSyncAudioFree>::new(guard, false) }.unwrap();
6842 assert!(frame.data().is_none());
6843 assert_eq!(frame.channel_data(0).unwrap(), &[1.0, 2.0, 3.0]);
6844 assert_eq!(frame.channel_data(1).unwrap(), &[4.0, 5.0, 6.0]);
6845 assert_eq!(frame.channel_data(1).unwrap().as_ptr(), unsafe {
6846 storage.as_ptr().add(6).cast::<f32>()
6847 });
6848 assert!(frame.channel_data(2).is_none());
6849 assert_eq!(frame.channels().len(), 2);
6850 assert_eq!(
6851 frame.channels().flatten().copied().collect::<Vec<_>>(),
6852 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
6853 );
6854 let mut owned = frame.to_owned().unwrap().unwrap();
6855 assert_eq!(owned.channel_stride_in_bytes(), 12);
6856 assert_eq!(owned.data(), &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
6857 assert_eq!(owned.timecode(), 123);
6858 assert_eq!(owned.timestamp(), 456);
6859 assert_eq!(owned.channel_data(1).unwrap().as_ptr(), unsafe {
6860 owned.data().as_ptr().add(3)
6861 });
6862 owned.channel_data_mut(1).unwrap()[0] = 99.0;
6863 assert_eq!(owned.data()[3], 99.0);
6864 assert!(owned.channel_data_mut(2).is_none());
6865 assert!(owned.replace_data(vec![0.0; 10]).is_err());
6866 }
6867
6868 #[test]
6869 fn test_audio_rejects_unaligned_float_pointer_before_access() {
6870 let mut storage = [0.0f32; 4];
6871 let raw = NDIlib_audio_frame_v3_t {
6872 sample_rate: 48000,
6873 no_channels: 1,
6874 no_samples: 1,
6875 FourCC: AudioFormat::FLTP.into(),
6876 p_data: unsafe { storage.as_mut_ptr().cast::<u8>().add(1) },
6877 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
6878 channel_stride_in_bytes: 4,
6879 },
6880 timecode: 0,
6881 timestamp: 0,
6882 p_metadata: ptr::null(),
6883 };
6884 assert!(matches!(
6885 validate_audio_layout(&raw),
6886 Err(Error::InvalidFrame(_))
6887 ));
6888 }
6889
6890 #[test]
6892 fn test_validate_audio_layout_strided_planar() {
6893 let no_samples = 4;
6894 let no_channels = 2;
6895 let stride_samples = 6;
6896 let backing_samples = (stride_samples + no_samples) as usize;
6897 let mut data = vec![0.0f32; backing_samples];
6898
6899 let raw = NDIlib_audio_frame_v3_t {
6900 sample_rate: 48000,
6901 no_channels,
6902 no_samples,
6903 timecode: 0,
6904 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
6905 p_data: data.as_mut_ptr() as *mut u8,
6906 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
6907 channel_stride_in_bytes: stride_samples * 4,
6908 },
6909 p_metadata: ptr::null(),
6910 timestamp: 0,
6911 };
6912
6913 let layout = validate_audio_layout(&raw).expect("strided FLTP should validate");
6914 assert_eq!(layout.sample_count, backing_samples);
6915 assert_eq!(layout.channel_stride_samples, stride_samples as usize);
6916 assert_eq!(layout.channel_range(1), Some(6..10));
6917 }
6918
6919 #[test]
6921 fn test_validate_audio_layout_empty_query_no_source() {
6922 let raw = NDIlib_audio_frame_v3_t::default();
6923
6924 let layout =
6925 validate_audio_layout_allow_empty(&raw).expect("all-zero query state should validate");
6926 assert!(layout.is_empty());
6927 assert_eq!(layout.format(), None);
6928 assert_eq!(layout.sample_count, 0);
6929 }
6930
6931 #[test]
6933 fn test_validate_audio_layout_empty_query_with_source_format() {
6934 let raw = NDIlib_audio_frame_v3_t {
6935 sample_rate: 48000,
6936 no_channels: 2,
6937 no_samples: 0,
6938 timecode: 0,
6939 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
6940 p_data: ptr::null_mut(),
6941 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
6942 channel_stride_in_bytes: 0,
6943 },
6944 p_metadata: ptr::null(),
6945 timestamp: 0,
6946 };
6947
6948 let layout = validate_audio_layout_allow_empty(&raw)
6949 .expect("query source format should validate without samples");
6950 assert!(layout.is_empty());
6951 assert_eq!(layout.format(), Some(AudioFormat::FLTP));
6952 assert_eq!(layout.sample_rate, 48000);
6953 assert_eq!(layout.no_channels, 2);
6954 }
6955
6956 #[test]
6958 fn test_validate_audio_layout_rejects_partial_empty_query() {
6959 let raw = NDIlib_audio_frame_v3_t {
6960 sample_rate: 48000,
6961 no_channels: 0,
6962 no_samples: 0,
6963 timecode: 0,
6964 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
6965 p_data: ptr::null_mut(),
6966 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
6967 channel_stride_in_bytes: 0,
6968 },
6969 p_metadata: ptr::null(),
6970 timestamp: 0,
6971 };
6972
6973 let result = validate_audio_layout_allow_empty(&raw);
6974 assert!(matches!(result, Err(Error::InvalidFrame(_))));
6975 }
6976
6977 #[test]
6979 fn test_validate_audio_layout_rejects_invalid_channel_stride() {
6980 let mut data = vec![0.0f32; 2048];
6981
6982 let raw = NDIlib_audio_frame_v3_t {
6983 sample_rate: 48000,
6984 no_channels: 2,
6985 no_samples: 1024,
6986 timecode: 0,
6987 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
6988 p_data: data.as_mut_ptr() as *mut u8,
6989 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
6990 channel_stride_in_bytes: 1024 * 4 - 4,
6991 },
6992 p_metadata: ptr::null(),
6993 timestamp: 0,
6994 };
6995
6996 let result = validate_audio_layout(&raw);
6997 assert!(matches!(result, Err(Error::InvalidFrame(_))));
6998 }
6999
7000 #[test]
7002 fn test_validate_audio_layout_null_pointer() {
7003 let raw = NDIlib_audio_frame_v3_t {
7004 sample_rate: 48000,
7005 no_channels: 2,
7006 no_samples: 1024,
7007 timecode: 0,
7008 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
7009 p_data: ptr::null_mut(),
7010 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
7011 channel_stride_in_bytes: 1024 * 4,
7012 },
7013 p_metadata: ptr::null(),
7014 timestamp: 0,
7015 };
7016
7017 let result = validate_audio_layout(&raw);
7018 assert!(result.is_err(), "Should reject null data pointer");
7019
7020 if let Err(Error::InvalidFrame(msg)) = result {
7021 assert!(
7022 msg.contains("null data pointer"),
7023 "Error should mention null pointer, got: {msg}"
7024 );
7025 } else {
7026 panic!("Expected InvalidFrame error");
7027 }
7028 }
7029
7030 #[test]
7032 fn test_validate_audio_layout_negative_samples() {
7033 let mut data = vec![0.0f32; 1024];
7034
7035 let raw = NDIlib_audio_frame_v3_t {
7036 sample_rate: 48000,
7037 no_channels: 2,
7038 no_samples: -1, timecode: 0,
7040 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
7041 p_data: data.as_mut_ptr() as *mut u8,
7042 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
7043 channel_stride_in_bytes: 1024 * 4,
7044 },
7045 p_metadata: ptr::null(),
7046 timestamp: 0,
7047 };
7048
7049 let result = validate_audio_layout(&raw);
7050 assert!(result.is_err(), "Should reject negative sample count");
7051
7052 if let Err(Error::InvalidFrame(msg)) = result {
7053 assert!(
7054 msg.contains("Invalid number of samples"),
7055 "Error should mention invalid samples, got: {msg}"
7056 );
7057 } else {
7058 panic!("Expected InvalidFrame error");
7059 }
7060 }
7061
7062 #[test]
7064 fn test_validate_audio_layout_overflow() {
7065 let mut data = vec![0.0f32; 1024];
7066
7067 let raw = NDIlib_audio_frame_v3_t {
7068 sample_rate: 48000,
7069 no_channels: i32::MAX,
7070 no_samples: 1024,
7071 timecode: 0,
7072 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
7073 p_data: data.as_mut_ptr() as *mut u8,
7074 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
7075 channel_stride_in_bytes: 1024 * 4,
7076 },
7077 p_metadata: ptr::null(),
7078 timestamp: 0,
7079 };
7080
7081 let result = validate_audio_layout(&raw);
7082 assert!(
7083 result.is_err(),
7084 "Should reject audio frame with overflow potential"
7085 );
7086
7087 if let Err(Error::InvalidFrame(msg)) = result {
7088 assert!(
7089 msg.contains("overflow") || msg.contains("exceeds maximum size"),
7090 "Error should mention overflow or size limit, got: {msg}"
7091 );
7092 } else {
7093 panic!("Expected InvalidFrame error");
7094 }
7095 }
7096
7097 #[test]
7099 fn test_calculate_buffer_len_checked_matches_public_helper() {
7100 let test_cases = [
7101 (PixelFormat::BGRA, 7680usize, 1080usize),
7102 (PixelFormat::UYVY, 3840usize, 1080usize),
7103 (PixelFormat::NV12, 1920usize, 1080usize),
7104 (PixelFormat::YV12, 1920usize, 1080usize),
7105 (PixelFormat::I420, 1920usize, 1080usize),
7106 ];
7107
7108 for (format, stride, height) in test_cases {
7109 let expected = format
7110 .info()
7111 .try_buffer_len(stride as i32, height as i32)
7112 .unwrap();
7113 let result = calculate_buffer_len_checked(format, stride, height);
7114
7115 assert!(
7116 result.is_ok(),
7117 "Should succeed for valid inputs: {:?}",
7118 format
7119 );
7120 assert_eq!(
7121 result.unwrap(),
7122 expected,
7123 "Checked calculation should match unchecked for {:?}",
7124 format
7125 );
7126 }
7127 }
7128
7129 #[test]
7131 fn test_video_frame_ref_uses_cached_length() {
7132 use crate::capture::{Guard, VideoKind};
7133
7134 let width = 1920;
7136 let height = 1080;
7137 let stride = width * 4;
7138 let expected_size = (stride * height) as usize;
7139 let data = vec![0u8; expected_size];
7140
7141 let raw = NDIlib_video_frame_v2_t {
7142 xres: width,
7143 yres: height,
7144 FourCC: PixelFormat::BGRA.into(),
7145 frame_rate_N: 60,
7146 frame_rate_D: 1,
7147 picture_aspect_ratio: 16.0 / 9.0,
7148 frame_format_type: ScanType::Progressive.into(),
7149 timecode: 12345,
7150 p_data: data.as_ptr() as *mut u8,
7151 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
7152 line_stride_in_bytes: stride,
7153 },
7154 p_metadata: ptr::null(),
7155 timestamp: 67890,
7156 };
7157
7158 let guard = unsafe { Guard::<VideoKind>::new(ptr::null_mut(), raw) };
7161
7162 let frame_ref = unsafe { VideoFrameRef::new(guard) };
7164 assert!(frame_ref.is_ok(), "Should create valid VideoFrameRef");
7165
7166 let frame_ref = frame_ref.unwrap();
7167
7168 assert_eq!(
7170 frame_ref.data().unwrap().len(),
7171 expected_size,
7172 "data() should return slice with cached length"
7173 );
7174 assert_eq!(
7175 frame_ref.layout.data_len_bytes, expected_size,
7176 "Cached data_len_bytes should match expected"
7177 );
7178
7179 std::mem::forget(frame_ref);
7181 }
7182
7183 #[test]
7185 fn test_audio_frame_ref_uses_cached_sample_count() {
7186 use crate::capture::RecvAudioGuard;
7187
7188 let no_samples = 1024;
7189 let no_channels = 2;
7190 let sample_count = (no_samples * no_channels) as usize;
7191 let data = vec![0.5f32; sample_count];
7192
7193 let raw = NDIlib_audio_frame_v3_t {
7194 sample_rate: 48000,
7195 no_channels,
7196 no_samples,
7197 timecode: 12345,
7198 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
7199 p_data: data.as_ptr() as *mut u8,
7200 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
7201 channel_stride_in_bytes: no_samples * 4,
7202 },
7203 p_metadata: ptr::null(),
7204 timestamp: 67890,
7205 };
7206
7207 let guard = unsafe { RecvAudioGuard::new(ptr::null_mut(), raw) };
7209
7210 let frame_ref = unsafe { AudioFrameRef::new(guard) };
7212 assert!(frame_ref.is_ok(), "Should create valid AudioFrameRef");
7213
7214 let frame_ref = frame_ref.unwrap();
7215
7216 assert_eq!(
7218 frame_ref.data().unwrap().len(),
7219 sample_count,
7220 "data() should return slice with cached sample count"
7221 );
7222 assert_eq!(
7223 frame_ref.layout.sample_count, sample_count,
7224 "Cached sample_count should match expected"
7225 );
7226
7227 std::mem::forget(frame_ref);
7229 }
7230}