1use num_enum::{IntoPrimitive, TryFromPrimitive};
4
5#[cfg(feature = "image-encoding")]
6use std::borrow::Cow;
7use std::{
8 ffi::{CStr, CString},
9 fmt,
10 num::NonZeroUsize,
11 os::raw::c_char,
12 ptr, slice, str,
13};
14
15use crate::{
16 capture::{AudioKind, FrameFree, FrameSyncAudioFree, Guard, VideoKind},
17 ndi_lib::*,
18 recv_guard::{RecvAudioGuard, RecvMetadataGuard},
19 Error, Result,
20};
21
22#[derive(Debug, TryFromPrimitive, IntoPrimitive, Clone, Copy, PartialEq, Eq)]
49#[non_exhaustive]
50#[repr(u32)]
51pub enum PixelFormat {
52 UYVY = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_UYVY as _,
54 UYVA = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_UYVA as _,
56 P216 = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_P216 as _,
58 PA16 = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_PA16 as _,
60 YV12 = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_YV12 as _,
62 I420 = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_I420 as _,
64 NV12 = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_NV12 as _,
66 BGRA = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_BGRA as _,
68 BGRX = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_BGRX as _,
70 RGBA = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_RGBA as _,
72 RGBX = NDIlib_FourCC_video_type_e_NDIlib_FourCC_video_type_RGBX as _,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87#[non_exhaustive]
88pub enum FormatCategory {
89 Packed,
91 Planar420,
93 SemiPlanar420,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub struct PixelFormatInfo {
118 bytes_per_pixel: u8,
120 category: FormatCategory,
122}
123
124impl PixelFormatInfo {
125 #[must_use]
127 pub const fn bytes_per_pixel(&self) -> u8 {
128 self.bytes_per_pixel
129 }
130
131 #[must_use]
133 pub const fn category(&self) -> FormatCategory {
134 self.category
135 }
136
137 #[must_use]
139 pub const fn is_planar_420(&self) -> bool {
140 matches!(
141 self.category,
142 FormatCategory::Planar420 | FormatCategory::SemiPlanar420
143 )
144 }
145
146 pub fn try_buffer_len(&self, y_stride: i32, height: i32) -> Result<usize> {
173 if y_stride <= 0 {
174 return Err(Error::InvalidFrame(format!(
175 "Video line stride must be positive, got {y_stride}"
176 )));
177 }
178 if height <= 0 {
179 return Err(Error::InvalidFrame(format!(
180 "Video frame height must be positive, got {height}"
181 )));
182 }
183
184 let y_stride = usize::try_from(y_stride)
185 .map_err(|_| Error::InvalidFrame(format!("Invalid y_stride value: {y_stride}")))?;
186 let height = usize::try_from(height)
187 .map_err(|_| Error::InvalidFrame(format!("Invalid height value: {height}")))?;
188
189 let len = calculate_buffer_len_for_info_checked(*self, y_stride, height)?;
190 validate_video_data_len(len)?;
191 Ok(len)
192 }
193}
194
195impl PixelFormat {
196 #[must_use]
217 pub const fn info(self) -> PixelFormatInfo {
218 match self {
219 Self::BGRA | Self::BGRX | Self::RGBA | Self::RGBX => PixelFormatInfo {
220 bytes_per_pixel: 4,
221 category: FormatCategory::Packed,
222 },
223 Self::UYVY => PixelFormatInfo {
224 bytes_per_pixel: 2,
225 category: FormatCategory::Packed,
226 },
227 Self::UYVA => PixelFormatInfo {
228 bytes_per_pixel: 3,
229 category: FormatCategory::Packed,
230 },
231 Self::P216 | Self::PA16 => PixelFormatInfo {
232 bytes_per_pixel: 4,
233 category: FormatCategory::Packed,
234 },
235 Self::YV12 | Self::I420 => PixelFormatInfo {
236 bytes_per_pixel: 1,
237 category: FormatCategory::Planar420,
238 },
239 Self::NV12 => PixelFormatInfo {
240 bytes_per_pixel: 1,
241 category: FormatCategory::SemiPlanar420,
242 },
243 }
244 }
245
246 pub fn try_line_stride(self, width: i32) -> Result<i32> {
273 validate_video_width_for_format(self, width)?;
274 let width_usize = usize::try_from(width)
275 .map_err(|_| Error::InvalidFrame(format!("Invalid width value: {width}")))?;
276 let stride = min_video_line_stride_checked(self, width_usize)?;
277 i32::try_from(stride).map_err(|_| {
278 Error::InvalidFrame(format!("Video line stride {stride} exceeds i32 range"))
279 })
280 }
281
282 pub fn try_buffer_size(self, width: i32, height: i32) -> Result<usize> {
307 let layout = ValidatedVideoLayout::new_uncompressed(self, width, height, None)?;
308 Ok(layout.data_len_bytes)
309 }
310}
311
312impl From<PixelFormat> for i32 {
313 fn from(value: PixelFormat) -> Self {
314 let u32_value: u32 = value.into();
315 u32_value as i32
316 }
317}
318
319#[derive(Debug, TryFromPrimitive, IntoPrimitive, Clone, Copy, PartialEq, Eq)]
342#[non_exhaustive]
343#[repr(u32)]
344pub enum ScanType {
345 Progressive = NDIlib_frame_format_type_e_NDIlib_frame_format_type_progressive as _,
347 Interlaced = NDIlib_frame_format_type_e_NDIlib_frame_format_type_interleaved as _,
349 Field0 = NDIlib_frame_format_type_e_NDIlib_frame_format_type_field_0 as _,
351 Field1 = NDIlib_frame_format_type_e_NDIlib_frame_format_type_field_1 as _,
353}
354
355impl From<ScanType> for i32 {
356 fn from(value: ScanType) -> Self {
357 let u32_value: u32 = value.into();
358 u32_value as i32
359 }
360}
361
362#[derive(Debug, Clone, Copy, PartialEq, Eq)]
368pub enum LineStrideOrSize {
369 LineStrideBytes(i32),
372 DataSizeBytes(i32),
374}
375
376impl From<LineStrideOrSize> for NDIlib_video_frame_v2_t__bindgen_ty_1 {
377 fn from(value: LineStrideOrSize) -> Self {
378 match value {
381 LineStrideOrSize::LineStrideBytes(stride) =>
382 {
383 #[allow(clippy::field_reassign_with_default)]
384 NDIlib_video_frame_v2_t__bindgen_ty_1 {
385 line_stride_in_bytes: stride,
386 }
387 }
388 LineStrideOrSize::DataSizeBytes(size) => NDIlib_video_frame_v2_t__bindgen_ty_1 {
389 data_size_in_bytes: size,
390 },
391 }
392 }
393}
394
395pub struct VideoFrame {
396 layout: ValidatedVideoLayout,
397 frame_rate_n: i32,
398 frame_rate_d: i32,
399 picture_aspect_ratio: f32,
400 scan_type: ScanType,
401 timecode: i64,
402 data: Vec<u8>,
403 metadata: Option<FrameMetadata>,
404 timestamp: i64,
405}
406
407impl fmt::Debug for VideoFrame {
408 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
409 f.debug_struct("VideoFrame")
410 .field("width", &self.width())
411 .field("height", &self.height())
412 .field("pixel_format", &self.pixel_format())
413 .field("frame_rate_n", &self.frame_rate_n)
414 .field("frame_rate_d", &self.frame_rate_d)
415 .field("picture_aspect_ratio", &self.picture_aspect_ratio)
416 .field("scan_type", &self.scan_type)
417 .field("timecode", &self.timecode)
418 .field("data (bytes)", &self.data.len())
419 .field("line_stride_or_size", &self.line_stride_or_size())
420 .field("metadata", &self.metadata())
421 .field("timestamp", &self.timestamp)
422 .finish()
423 }
424}
425
426impl Default for VideoFrame {
427 fn default() -> Self {
428 VideoFrame::builder()
429 .resolution(1920, 1080)
430 .pixel_format(PixelFormat::BGRA)
431 .frame_rate(60, 1)
432 .aspect_ratio(16.0 / 9.0)
433 .scan_type(ScanType::Interlaced)
434 .build()
435 .expect("Default VideoFrame should always succeed")
436 }
437}
438
439impl VideoFrame {
440 pub(crate) fn to_raw(&self) -> NDIlib_video_frame_v2_t {
441 NDIlib_video_frame_v2_t {
442 xres: self.layout.width,
443 yres: self.layout.height,
444 FourCC: self.layout.pixel_format.into(),
445 frame_rate_N: self.frame_rate_n,
446 frame_rate_D: self.frame_rate_d,
447 picture_aspect_ratio: self.picture_aspect_ratio,
448 frame_format_type: self.scan_type.into(),
449 timecode: self.timecode,
450 p_data: self.data.as_ptr() as *mut u8,
451 __bindgen_anon_1: self.layout.line_stride_or_size.into(),
452 p_metadata: self
453 .metadata
454 .as_ref()
455 .map_or(ptr::null(), FrameMetadata::as_ptr),
456 timestamp: self.timestamp,
457 }
458 }
459
460 pub fn width(&self) -> i32 {
462 self.layout.width
463 }
464
465 pub fn height(&self) -> i32 {
467 self.layout.height
468 }
469
470 pub fn pixel_format(&self) -> PixelFormat {
472 self.layout.pixel_format
473 }
474
475 pub fn frame_rate_n(&self) -> i32 {
477 self.frame_rate_n
478 }
479
480 pub fn frame_rate_d(&self) -> i32 {
482 self.frame_rate_d
483 }
484
485 pub fn picture_aspect_ratio(&self) -> f32 {
487 self.picture_aspect_ratio
488 }
489
490 pub fn scan_type(&self) -> ScanType {
492 self.scan_type
493 }
494
495 pub fn timecode(&self) -> i64 {
500 self.timecode
501 }
502
503 pub fn timestamp(&self) -> i64 {
508 self.timestamp
509 }
510
511 pub fn line_stride_or_size(&self) -> LineStrideOrSize {
513 self.layout.line_stride_or_size
514 }
515
516 pub(crate) fn validated_layout(&self) -> ValidatedVideoLayout {
517 self.layout
518 }
519
520 pub fn data(&self) -> &[u8] {
522 &self.data
523 }
524
525 pub fn data_mut(&mut self) -> &mut [u8] {
528 &mut self.data
529 }
530
531 pub fn replace_data(&mut self, data: Vec<u8>) -> Result<()> {
538 if data.len() != self.layout.data_len_bytes {
539 return Err(Error::InvalidFrame(format!(
540 "Video data length {}, expected {} bytes for validated layout",
541 data.len(),
542 self.layout.data_len_bytes
543 )));
544 }
545
546 self.data = data;
547 Ok(())
548 }
549
550 pub fn metadata(&self) -> Option<&str> {
552 self.metadata.as_ref().map(FrameMetadata::as_str)
553 }
554
555 pub(crate) fn metadata_cstr(&self) -> Option<&CStr> {
556 self.metadata.as_ref().map(FrameMetadata::as_cstr)
557 }
558
559 pub fn set_metadata<S: Into<String>>(&mut self, metadata: Option<S>) -> Result<()> {
567 self.metadata = metadata.map(FrameMetadata::new).transpose()?;
568 Ok(())
569 }
570
571 pub fn set_frame_rate(&mut self, numerator: i32, denominator: i32) -> Result<()> {
578 validate_video_frame_metadata(numerator, denominator, self.picture_aspect_ratio)?;
579 self.frame_rate_n = numerator;
580 self.frame_rate_d = denominator;
581 Ok(())
582 }
583
584 pub fn set_picture_aspect_ratio(&mut self, ratio: f32) -> Result<()> {
590 validate_video_frame_metadata(self.frame_rate_n, self.frame_rate_d, ratio)?;
591 self.picture_aspect_ratio = ratio;
592 Ok(())
593 }
594
595 #[cfg(feature = "image-encoding")]
642 pub fn encode_png(&self) -> Result<Vec<u8>> {
643 use png::{BitDepth, ColorType, Encoder};
644
645 let image = ImagePixelSource::new(self.layout, &self.data)?;
646 let (rgba_data, width, height) = image.png_rgba_input()?;
647
648 let mut png_data = Vec::new();
650 let mut encoder = Encoder::new(&mut png_data, width, height);
651 encoder.set_color(ColorType::Rgba);
652 encoder.set_depth(BitDepth::Eight);
653
654 encoder
655 .write_header()
656 .and_then(|mut writer| writer.write_image_data(rgba_data.as_ref()))
657 .map_err(|e| Error::InvalidFrame(format!("PNG encoding failed: {e}")))?;
658
659 Ok(png_data)
660 }
661
662 #[cfg(feature = "image-encoding")]
708 pub fn encode_jpeg(&self, quality: u8) -> Result<Vec<u8>> {
709 use jpeg_encoder::{ColorType as JpegColorType, Encoder as JpegEncoder};
710
711 let image = ImagePixelSource::new(self.layout, &self.data)?;
712 let (rgb_data, width, height) = image.jpeg_rgb_input(quality)?;
713
714 let mut jpeg_data = Vec::new();
716 let encoder = JpegEncoder::new(&mut jpeg_data, quality);
717 encoder
718 .encode(&rgb_data, width, height, JpegColorType::Rgb)
719 .map_err(|e| Error::InvalidFrame(format!("JPEG encoding failed: {e}")))?;
720
721 Ok(jpeg_data)
722 }
723
724 #[cfg(feature = "image-encoding")]
760 pub fn encode_data_url(&self, format: ImageFormat) -> Result<String> {
761 use base64::{engine::general_purpose::STANDARD, Engine};
762
763 let (mime_type, image_bytes) = match format {
764 ImageFormat::Png => ("image/png", self.encode_png()?),
765 ImageFormat::Jpeg(quality) => ("image/jpeg", self.encode_jpeg(quality)?),
766 };
767
768 let base64_data = STANDARD.encode(&image_bytes);
769 Ok(format!("data:{mime_type};base64,{base64_data}"))
770 }
771
772 pub unsafe fn from_raw(c_frame: &NDIlib_video_frame_v2_t) -> Result<VideoFrame> {
779 let layout = validate_video_layout(c_frame)?;
781 let metadata_layout = unsafe { validate_frame_metadata(c_frame.p_metadata)? };
782
783 Self::from_raw_validated(c_frame, layout, metadata_layout)
784 }
785
786 pub(crate) unsafe fn from_raw_validated(
787 c_frame: &NDIlib_video_frame_v2_t,
788 layout: ValidatedVideoLayout,
789 metadata_layout: ValidatedFrameMetadata,
790 ) -> Result<VideoFrame> {
791 let slice = slice::from_raw_parts(c_frame.p_data, layout.data_len_bytes);
793 let data = slice.to_vec();
794
795 let metadata =
796 unsafe { FrameMetadata::copy_from_raw_validated(c_frame.p_metadata, metadata_layout) };
797
798 #[allow(clippy::unnecessary_cast)] let scan_type = ScanType::try_from(c_frame.frame_format_type as u32).map_err(|_| {
800 Error::InvalidFrame(format!(
801 "Unknown scan type: 0x{:08X}",
802 c_frame.frame_format_type
803 ))
804 })?;
805
806 Ok(VideoFrame {
807 layout,
808 frame_rate_n: c_frame.frame_rate_N,
809 frame_rate_d: c_frame.frame_rate_D,
810 picture_aspect_ratio: c_frame.picture_aspect_ratio,
811 scan_type,
812 timecode: c_frame.timecode,
813 data,
814 metadata,
815 timestamp: c_frame.timestamp,
816 })
817 }
818
819 pub fn builder() -> VideoFrameBuilder {
821 VideoFrameBuilder::new()
822 }
823}
824
825#[derive(Debug, Clone)]
827pub struct VideoFrameBuilder {
828 width: Option<i32>,
829 height: Option<i32>,
830 pixel_format: Option<PixelFormat>,
831 frame_rate_n: Option<i32>,
832 frame_rate_d: Option<i32>,
833 picture_aspect_ratio: Option<f32>,
834 scan_type: Option<ScanType>,
835 timecode: Option<i64>,
836 metadata: Option<String>,
837 timestamp: Option<i64>,
838}
839
840impl VideoFrameBuilder {
841 pub fn new() -> Self {
843 Self {
844 width: None,
845 height: None,
846 pixel_format: None,
847 frame_rate_n: None,
848 frame_rate_d: None,
849 picture_aspect_ratio: None,
850 scan_type: None,
851 timecode: None,
852 metadata: None,
853 timestamp: None,
854 }
855 }
856
857 #[must_use]
859 pub fn resolution(mut self, width: i32, height: i32) -> Self {
860 self.width = Some(width);
861 self.height = Some(height);
862 self
863 }
864
865 #[must_use]
867 pub fn pixel_format(mut self, pixel_format: PixelFormat) -> Self {
868 self.pixel_format = Some(pixel_format);
869 self
870 }
871
872 #[must_use]
874 pub fn frame_rate(mut self, numerator: i32, denominator: i32) -> Self {
875 self.frame_rate_n = Some(numerator);
876 self.frame_rate_d = Some(denominator);
877 self
878 }
879
880 #[must_use]
882 pub fn aspect_ratio(mut self, ratio: f32) -> Self {
883 self.picture_aspect_ratio = Some(ratio);
884 self
885 }
886
887 #[must_use]
889 pub fn scan_type(mut self, scan_type: ScanType) -> Self {
890 self.scan_type = Some(scan_type);
891 self
892 }
893
894 #[must_use]
896 pub fn timecode(mut self, tc: i64) -> Self {
897 self.timecode = Some(tc);
898 self
899 }
900
901 #[must_use]
903 pub fn metadata<S: Into<String>>(mut self, meta: S) -> Self {
904 self.metadata = Some(meta.into());
905 self
906 }
907
908 #[must_use]
910 pub fn timestamp(mut self, ts: i64) -> Self {
911 self.timestamp = Some(ts);
912 self
913 }
914
915 pub fn build(self) -> Result<VideoFrame> {
917 let width = self.width.unwrap_or(1920);
918 let height = self.height.unwrap_or(1080);
919 let pixel_format = self.pixel_format.unwrap_or(PixelFormat::BGRA);
920 let frame_rate_n = self.frame_rate_n.unwrap_or(60);
921 let frame_rate_d = self.frame_rate_d.unwrap_or(1);
922 let picture_aspect_ratio = self.picture_aspect_ratio.unwrap_or(16.0 / 9.0);
923 let scan_type = self.scan_type.unwrap_or(ScanType::Progressive);
924
925 validate_video_frame_metadata(frame_rate_n, frame_rate_d, picture_aspect_ratio)?;
926 let layout = ValidatedVideoLayout::new_uncompressed(pixel_format, width, height, None)?;
927 let buffer_size = layout.data_len_bytes;
928 let data = vec![0u8; buffer_size];
929
930 let metadata = self.metadata.map(FrameMetadata::new).transpose()?;
931
932 Ok(VideoFrame {
933 layout,
934 frame_rate_n,
935 frame_rate_d,
936 picture_aspect_ratio,
937 scan_type,
938 timecode: self.timecode.unwrap_or(0),
939 data,
940 metadata,
941 timestamp: self.timestamp.unwrap_or(0),
942 })
943 }
944}
945
946impl Default for VideoFrameBuilder {
947 fn default() -> Self {
948 Self::new()
949 }
950}
951
952impl Drop for VideoFrame {
953 fn drop(&mut self) {
954 }
957}
958
959#[derive(Debug)]
960pub struct AudioFrame {
961 layout: ValidatedAudioLayout,
962 timecode: i64,
963 data: Vec<f32>,
964 metadata: Option<FrameMetadata>,
965 timestamp: i64,
966}
967
968impl AudioFrame {
969 pub(crate) fn to_raw(&self) -> NDIlib_audio_frame_v3_t {
970 NDIlib_audio_frame_v3_t {
971 sample_rate: self.layout.sample_rate,
972 no_channels: self.num_channels(),
973 no_samples: self.num_samples(),
974 timecode: self.timecode,
975 FourCC: self.format().into(),
976 p_data: self.data.as_ptr() as *mut f32 as *mut u8,
977 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
978 channel_stride_in_bytes: self.layout.channel_stride_in_bytes,
979 },
980 p_metadata: self
981 .metadata
982 .as_ref()
983 .map_or(ptr::null(), FrameMetadata::as_ptr),
984 timestamp: self.timestamp,
985 }
986 }
987
988 #[cfg(test)]
989 pub(crate) fn from_raw(raw: NDIlib_audio_frame_v3_t) -> Result<AudioFrame> {
990 let layout = validate_audio_layout(&raw)?;
992 let metadata_layout = unsafe { validate_frame_metadata(raw.p_metadata)? };
993
994 Self::from_raw_validated(raw, layout, metadata_layout)
995 }
996
997 pub(crate) fn from_raw_validated(
998 raw: NDIlib_audio_frame_v3_t,
999 layout: ValidatedAudioLayout,
1000 metadata_layout: ValidatedFrameMetadata,
1001 ) -> Result<AudioFrame> {
1002 if layout.is_empty() {
1003 return Err(Error::InvalidFrame(
1004 "Cannot create owned AudioFrame from an empty audio layout".into(),
1005 ));
1006 }
1007
1008 let slice = unsafe { slice::from_raw_parts(raw.p_data as *const f32, layout.sample_count) };
1010 let data = slice.to_vec();
1011
1012 let metadata =
1014 unsafe { FrameMetadata::copy_from_raw_validated(raw.p_metadata, metadata_layout) };
1015
1016 Ok(AudioFrame {
1017 layout,
1018 timecode: raw.timecode,
1019 data,
1020 metadata,
1021 timestamp: raw.timestamp,
1022 })
1023 }
1024
1025 pub fn builder() -> AudioFrameBuilder {
1027 AudioFrameBuilder::new()
1028 }
1029
1030 pub fn sample_rate(&self) -> i32 {
1032 self.layout.sample_rate
1033 }
1034
1035 pub fn num_channels(&self) -> i32 {
1037 self.layout.no_channels as i32
1038 }
1039
1040 pub fn num_samples(&self) -> i32 {
1042 self.layout.no_samples as i32
1043 }
1044
1045 pub fn timecode(&self) -> i64 {
1050 self.timecode
1051 }
1052
1053 pub fn timestamp(&self) -> i64 {
1058 self.timestamp
1059 }
1060
1061 pub fn format(&self) -> AudioFormat {
1063 self.layout
1064 .format()
1065 .expect("owned AudioFrame always has a concrete audio format")
1066 }
1067
1068 pub fn channel_stride_in_bytes(&self) -> i32 {
1070 self.layout.channel_stride_in_bytes
1071 }
1072
1073 pub fn data(&self) -> &[f32] {
1075 &self.data
1076 }
1077
1078 pub fn data_mut(&mut self) -> &mut [f32] {
1080 &mut self.data
1081 }
1082
1083 pub fn replace_data(&mut self, data: Vec<f32>) -> Result<()> {
1090 if data.len() != self.layout.sample_count {
1091 return Err(Error::InvalidFrame(format!(
1092 "Audio data length {}, expected {} samples for validated layout",
1093 data.len(),
1094 self.layout.sample_count
1095 )));
1096 }
1097
1098 self.data = data;
1099 Ok(())
1100 }
1101
1102 pub fn metadata(&self) -> Option<&str> {
1104 self.metadata.as_ref().map(FrameMetadata::as_str)
1105 }
1106
1107 pub fn set_metadata<S: Into<String>>(&mut self, metadata: Option<S>) -> Result<()> {
1115 self.metadata = metadata.map(FrameMetadata::new).transpose()?;
1116 Ok(())
1117 }
1118
1119 pub fn channel_data(&self, channel: usize) -> Option<Vec<f32>> {
1124 let range = self.layout.channel_range(channel)?;
1125 Some(self.data[range].to_vec())
1126 }
1127}
1128
1129#[derive(Debug, Clone)]
1131pub struct AudioFrameBuilder {
1132 sample_rate: Option<i32>,
1133 num_channels: Option<i32>,
1134 num_samples: Option<i32>,
1135 timecode: Option<i64>,
1136 format: Option<AudioFormat>,
1137 data: Option<Vec<f32>>,
1138 layout: Option<AudioLayout>,
1139 metadata: Option<String>,
1140 timestamp: Option<i64>,
1141}
1142
1143impl AudioFrameBuilder {
1144 pub fn new() -> Self {
1146 Self {
1147 sample_rate: None,
1148 num_channels: None,
1149 num_samples: None,
1150 timecode: None,
1151 format: None,
1152 data: None,
1153 layout: None,
1154 metadata: None,
1155 timestamp: None,
1156 }
1157 }
1158
1159 #[must_use]
1161 pub fn sample_rate(mut self, rate: i32) -> Self {
1162 self.sample_rate = Some(rate);
1163 self
1164 }
1165
1166 #[must_use]
1168 pub fn channels(mut self, channels: i32) -> Self {
1169 self.num_channels = Some(channels);
1170 self
1171 }
1172
1173 #[must_use]
1175 pub fn samples(mut self, samples: i32) -> Self {
1176 self.num_samples = Some(samples);
1177 self
1178 }
1179
1180 #[must_use]
1182 pub fn timecode(mut self, tc: i64) -> Self {
1183 self.timecode = Some(tc);
1184 self
1185 }
1186
1187 #[must_use]
1189 pub fn format(mut self, format: AudioFormat) -> Self {
1190 self.format = Some(format);
1191 self
1192 }
1193
1194 #[must_use]
1214 pub fn layout(mut self, layout: AudioLayout) -> Self {
1215 self.layout = Some(layout);
1216 self
1217 }
1218
1219 #[must_use]
1226 pub fn data(mut self, data: Vec<f32>) -> Self {
1227 self.data = Some(data);
1228 self
1229 }
1230
1231 #[must_use]
1233 pub fn metadata<S: Into<String>>(mut self, meta: S) -> Self {
1234 self.metadata = Some(meta.into());
1235 self
1236 }
1237
1238 #[must_use]
1240 pub fn timestamp(mut self, ts: i64) -> Self {
1241 self.timestamp = Some(ts);
1242 self
1243 }
1244
1245 pub fn build(self) -> Result<AudioFrame> {
1251 let sample_rate = self.sample_rate.unwrap_or(48000);
1252 let num_channels = self.num_channels.unwrap_or(2);
1253 let num_samples = self.num_samples.unwrap_or(1024);
1254 let format = self.format.unwrap_or(AudioFormat::FLTP);
1255 let layout = self.layout.unwrap_or(AudioLayout::Planar);
1256 let timecode = self.timecode.unwrap_or(0);
1257 let audio_layout =
1258 validate_outbound_audio_layout(sample_rate, num_channels, num_samples, format)?;
1259 let sample_count = audio_layout.sample_count;
1260
1261 let data = if let Some(input_data) = self.data {
1262 if input_data.len() != sample_count {
1263 return Err(Error::InvalidFrame(format!(
1264 "Audio data length {}, expected {} ({}ch x {}samples)",
1265 input_data.len(),
1266 sample_count,
1267 num_channels,
1268 num_samples
1269 )));
1270 }
1271
1272 match layout {
1273 AudioLayout::Planar => input_data,
1274 AudioLayout::Interleaved => {
1275 let nc = audio_layout.no_channels;
1276 let ns = audio_layout.no_samples;
1277 let mut planar = vec![0.0f32; sample_count];
1278 for ch in 0..nc {
1279 for s in 0..ns {
1280 let dst = ch
1281 .checked_mul(ns)
1282 .and_then(|idx| idx.checked_add(s))
1283 .ok_or_else(|| {
1284 Error::InvalidFrame(
1285 "Audio planar conversion index overflow".into(),
1286 )
1287 })?;
1288 let src = s
1289 .checked_mul(nc)
1290 .and_then(|idx| idx.checked_add(ch))
1291 .ok_or_else(|| {
1292 Error::InvalidFrame(
1293 "Audio interleaved conversion index overflow".into(),
1294 )
1295 })?;
1296 planar[dst] = input_data[src];
1297 }
1298 }
1299 planar
1300 }
1301 }
1302 } else {
1303 vec![0.0f32; sample_count]
1304 };
1305
1306 let metadata = self.metadata.map(FrameMetadata::new).transpose()?;
1307
1308 Ok(AudioFrame {
1309 layout: audio_layout,
1310 timecode,
1311 data,
1312 metadata,
1313 timestamp: self.timestamp.unwrap_or(0),
1314 })
1315 }
1316}
1317
1318impl Default for AudioFrameBuilder {
1319 fn default() -> Self {
1320 Self::new()
1321 }
1322}
1323
1324impl Default for AudioFrame {
1325 fn default() -> Self {
1326 AudioFrame::builder()
1327 .build()
1328 .expect("Default AudioFrame should always succeed")
1329 }
1330}
1331
1332impl Drop for AudioFrame {
1333 fn drop(&mut self) {
1334 }
1337}
1338
1339#[derive(Debug, TryFromPrimitive, IntoPrimitive, Clone, Copy, PartialEq, Eq)]
1360#[non_exhaustive]
1361#[repr(u32)]
1362pub enum AudioFormat {
1363 FLTP = NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP as _,
1365}
1366
1367#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1371pub enum AudioLayout {
1372 Planar,
1379
1380 Interleaved,
1388}
1389
1390impl From<AudioFormat> for i32 {
1391 fn from(value: AudioFormat) -> Self {
1392 let u32_value: u32 = value.into();
1393 u32_value as i32
1394 }
1395}
1396
1397const MAX_VIDEO_BYTES: usize = 100 * 1024 * 1024;
1399
1400const MAX_AUDIO_BYTES: usize = 64 * 1024 * 1024;
1403
1404pub(crate) const MAX_METADATA_BYTES: usize = 4 * 1024 * 1024;
1407
1408#[derive(Debug, Clone)]
1409pub struct MetadataFrame {
1410 data: String, timecode: i64,
1412}
1413
1414impl MetadataFrame {
1415 pub fn new() -> Self {
1417 MetadataFrame {
1418 data: String::new(),
1419 timecode: 0,
1420 }
1421 }
1422
1423 pub fn with_data(data: impl Into<String>, timecode: i64) -> Result<Self> {
1431 let data = data.into();
1432 validate_metadata_text(&data)?;
1433 Ok(MetadataFrame { data, timecode })
1434 }
1435
1436 pub fn data(&self) -> &str {
1438 &self.data
1439 }
1440
1441 pub fn into_data(self) -> String {
1443 self.data
1444 }
1445
1446 pub fn timecode(&self) -> i64 {
1451 self.timecode
1452 }
1453
1454 pub fn set_data(&mut self, data: impl Into<String>) -> Result<()> {
1462 let data = data.into();
1463 validate_metadata_text(&data)?;
1464 self.data = data;
1465 Ok(())
1466 }
1467
1468 pub fn set_timecode(&mut self, timecode: i64) {
1470 self.timecode = timecode;
1471 }
1472
1473 pub fn with_timecode(mut self, timecode: i64) -> Self {
1475 self.timecode = timecode;
1476 self
1477 }
1478
1479 pub(crate) fn to_raw(&self) -> Result<(CString, NDIlib_metadata_frame_t)> {
1481 let c_data = CString::new(self.data.as_bytes()).map_err(Error::InvalidCString)?;
1482 let length = validate_metadata_len_with_nul(c_data.as_bytes_with_nul().len())?;
1483 let raw = NDIlib_metadata_frame_t {
1484 length,
1485 timecode: self.timecode,
1486 p_data: c_data.as_ptr() as *mut c_char,
1487 };
1488 Ok((c_data, raw))
1489 }
1490
1491 #[cfg(test)]
1498 pub(crate) unsafe fn from_raw(raw: &NDIlib_metadata_frame_t) -> Result<Self> {
1499 let layout = validate_metadata_layout(raw)?;
1500 Ok(Self::from_raw_validated(raw, layout))
1501 }
1502
1503 pub(crate) unsafe fn from_raw_validated(
1510 raw: &NDIlib_metadata_frame_t,
1511 layout: ValidatedMetadataLayout,
1512 ) -> Self {
1513 let bytes = metadata_payload_bytes(raw, layout);
1514 let data = str::from_utf8_unchecked(bytes).to_owned();
1515
1516 Self {
1517 data,
1518 timecode: raw.timecode,
1519 }
1520 }
1521}
1522
1523impl Default for MetadataFrame {
1524 fn default() -> Self {
1525 Self::new()
1526 }
1527}
1528
1529#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1535pub(crate) struct ValidatedMetadataLayout {
1536 pub len_with_nul: usize,
1539 pub text_len: usize,
1541}
1542
1543pub(crate) fn validate_metadata_layout(
1549 raw: &NDIlib_metadata_frame_t,
1550) -> Result<ValidatedMetadataLayout> {
1551 if raw.length < 0 {
1552 return Err(Error::InvalidFrame(format!(
1553 "Metadata frame has negative length: {}",
1554 raw.length
1555 )));
1556 }
1557
1558 if raw.length == 0 {
1559 if raw.p_data.is_null() {
1560 return Ok(ValidatedMetadataLayout {
1561 len_with_nul: 0,
1562 text_len: 0,
1563 });
1564 }
1565
1566 return Err(Error::InvalidFrame(
1567 "Metadata frame uses lengthless non-null C-string data".into(),
1568 ));
1569 }
1570
1571 if raw.p_data.is_null() {
1572 return Err(Error::InvalidFrame(
1573 "Metadata frame has non-zero length with null data pointer".into(),
1574 ));
1575 }
1576
1577 let len_with_nul = usize::try_from(raw.length).map_err(|_| {
1578 Error::InvalidFrame(format!("Invalid metadata length value: {}", raw.length))
1579 })?;
1580 validate_metadata_len_with_nul(len_with_nul)?;
1581
1582 let bytes = unsafe { slice::from_raw_parts(raw.p_data.cast::<u8>(), len_with_nul) };
1583 if bytes[len_with_nul - 1] != 0 {
1584 return Err(Error::InvalidFrame(
1585 "Metadata frame length does not include a trailing NUL terminator".into(),
1586 ));
1587 }
1588
1589 let payload = &bytes[..len_with_nul - 1];
1590 if payload.contains(&0) {
1591 return Err(Error::InvalidFrame(
1592 "Metadata frame contains an interior NUL byte".into(),
1593 ));
1594 }
1595
1596 str::from_utf8(payload).map_err(|err| Error::InvalidUtf8(err.to_string()))?;
1597
1598 Ok(ValidatedMetadataLayout {
1599 len_with_nul,
1600 text_len: payload.len(),
1601 })
1602}
1603
1604fn validate_metadata_text(data: &str) -> Result<()> {
1605 let len_with_nul = data.len().checked_add(1).ok_or_else(|| {
1606 Error::InvalidFrame("Metadata length overflow while adding terminator".into())
1607 })?;
1608 validate_metadata_len_with_nul(len_with_nul)?;
1609 CString::new(data.as_bytes()).map_err(Error::InvalidCString)?;
1610 Ok(())
1611}
1612
1613fn validate_metadata_len_with_nul(len_with_nul: usize) -> Result<i32> {
1614 if len_with_nul == 0 {
1615 return Err(Error::InvalidFrame(
1616 "Metadata length must include a trailing NUL terminator".into(),
1617 ));
1618 }
1619
1620 if len_with_nul > MAX_METADATA_BYTES {
1621 return Err(Error::InvalidFrame(format!(
1622 "Metadata exceeds maximum size: {} bytes > {} bytes",
1623 len_with_nul, MAX_METADATA_BYTES
1624 )));
1625 }
1626
1627 metadata_len_to_i32(len_with_nul)
1628}
1629
1630fn metadata_len_to_i32(len_with_nul: usize) -> Result<i32> {
1631 i32::try_from(len_with_nul).map_err(|_| {
1632 Error::InvalidFrame(format!(
1633 "Metadata length {len_with_nul} exceeds SDK i32 range"
1634 ))
1635 })
1636}
1637
1638fn metadata_payload_bytes(raw: &NDIlib_metadata_frame_t, layout: ValidatedMetadataLayout) -> &[u8] {
1639 if layout.text_len == 0 {
1640 &[]
1641 } else {
1642 unsafe { slice::from_raw_parts(raw.p_data.cast::<u8>(), layout.text_len) }
1645 }
1646}
1647
1648#[derive(Clone, PartialEq, Eq)]
1655pub(crate) struct FrameMetadata {
1656 inner: CString,
1657}
1658
1659impl FrameMetadata {
1660 pub(crate) fn new<S: Into<String>>(metadata: S) -> Result<Self> {
1661 let metadata = metadata.into();
1662 let len_with_nul = metadata.len().checked_add(1).ok_or_else(|| {
1663 Error::InvalidFrame("Frame metadata length overflow while adding terminator".into())
1664 })?;
1665 validate_metadata_len_with_nul(len_with_nul)?;
1666
1667 Ok(Self {
1668 inner: CString::new(metadata).map_err(Error::InvalidCString)?,
1669 })
1670 }
1671
1672 pub(crate) fn as_str(&self) -> &str {
1673 self.inner
1674 .to_str()
1675 .expect("FrameMetadata validates UTF-8 at construction")
1676 }
1677
1678 pub(crate) fn as_cstr(&self) -> &CStr {
1679 self.inner.as_c_str()
1680 }
1681
1682 pub(crate) fn as_ptr(&self) -> *const c_char {
1683 self.inner.as_ptr()
1684 }
1685
1686 pub(crate) unsafe fn copy_from_raw_validated(
1694 p_metadata: *const c_char,
1695 metadata_layout: ValidatedFrameMetadata,
1696 ) -> Option<Self> {
1697 metadata_layout.len_with_nul?;
1698 debug_assert!(!p_metadata.is_null());
1699
1700 let mut bytes = if metadata_layout.text_len == 0 {
1701 Vec::with_capacity(1)
1702 } else {
1703 unsafe {
1704 slice::from_raw_parts(p_metadata.cast::<u8>(), metadata_layout.text_len).to_vec()
1705 }
1706 };
1707 bytes.push(0);
1708
1709 let inner = unsafe { CString::from_vec_with_nul_unchecked(bytes) };
1710 Some(Self { inner })
1711 }
1712}
1713
1714impl fmt::Debug for FrameMetadata {
1715 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1716 f.debug_tuple("FrameMetadata")
1717 .field(&self.as_str())
1718 .finish()
1719 }
1720}
1721
1722#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1728pub(crate) struct ValidatedFrameMetadata {
1729 pub(crate) len_with_nul: Option<NonZeroUsize>,
1732 pub(crate) text_len: usize,
1734}
1735
1736pub(crate) unsafe fn validate_frame_metadata(
1748 p_metadata: *const c_char,
1749) -> Result<ValidatedFrameMetadata> {
1750 if p_metadata.is_null() {
1751 return Ok(ValidatedFrameMetadata {
1752 len_with_nul: None,
1753 text_len: 0,
1754 });
1755 }
1756
1757 let metadata = p_metadata.cast::<u8>();
1758 for text_len in 0..MAX_METADATA_BYTES {
1759 let byte = unsafe { metadata.add(text_len).read() };
1760 if byte == 0 {
1761 if text_len > 0 {
1762 let payload = unsafe { slice::from_raw_parts(metadata, text_len) };
1763 str::from_utf8(payload).map_err(|err| Error::InvalidUtf8(err.to_string()))?;
1764 }
1765
1766 return Ok(ValidatedFrameMetadata {
1767 len_with_nul: NonZeroUsize::new(text_len + 1),
1768 text_len,
1769 });
1770 }
1771 }
1772
1773 Err(Error::InvalidFrame(format!(
1774 "Frame metadata is missing a NUL terminator within {MAX_METADATA_BYTES} bytes"
1775 )))
1776}
1777
1778pub(crate) unsafe fn frame_metadata_str<'a>(
1785 p_metadata: *const c_char,
1786 metadata_layout: ValidatedFrameMetadata,
1787) -> Option<&'a str> {
1788 metadata_layout.len_with_nul?;
1789 debug_assert!(!p_metadata.is_null());
1790
1791 if metadata_layout.text_len == 0 {
1792 return Some("");
1793 }
1794
1795 let bytes = unsafe { slice::from_raw_parts(p_metadata.cast::<u8>(), metadata_layout.text_len) };
1796 Some(unsafe { str::from_utf8_unchecked(bytes) })
1797}
1798
1799#[cfg(feature = "image-encoding")]
1819#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1820#[non_exhaustive]
1821pub enum ImageFormat {
1822 Png,
1824 Jpeg(u8),
1826}
1827
1828#[cfg(feature = "image-encoding")]
1829#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1830enum ImageChannelOrder {
1831 Rgba,
1832 Bgra,
1833}
1834
1835#[cfg(feature = "image-encoding")]
1836impl ImageChannelOrder {
1837 fn red_index(self) -> usize {
1838 match self {
1839 Self::Rgba => 0,
1840 Self::Bgra => 2,
1841 }
1842 }
1843
1844 fn blue_index(self) -> usize {
1845 match self {
1846 Self::Rgba => 2,
1847 Self::Bgra => 0,
1848 }
1849 }
1850}
1851
1852#[cfg(feature = "image-encoding")]
1853#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1854enum ImageAlphaPolicy {
1855 Preserve,
1856 Opaque,
1857}
1858
1859#[cfg(feature = "image-encoding")]
1860#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1861struct ImagePixelFormat {
1862 channel_order: ImageChannelOrder,
1863 alpha_policy: ImageAlphaPolicy,
1864}
1865
1866#[cfg(feature = "image-encoding")]
1867impl ImagePixelFormat {
1868 const BYTES_PER_PIXEL: usize = 4;
1869
1870 fn from_pixel_format(pixel_format: PixelFormat) -> Result<Self> {
1871 match pixel_format {
1872 PixelFormat::RGBA => Ok(Self {
1873 channel_order: ImageChannelOrder::Rgba,
1874 alpha_policy: ImageAlphaPolicy::Preserve,
1875 }),
1876 PixelFormat::BGRA => Ok(Self {
1877 channel_order: ImageChannelOrder::Bgra,
1878 alpha_policy: ImageAlphaPolicy::Preserve,
1879 }),
1880 PixelFormat::RGBX => Ok(Self {
1881 channel_order: ImageChannelOrder::Rgba,
1882 alpha_policy: ImageAlphaPolicy::Opaque,
1883 }),
1884 PixelFormat::BGRX => Ok(Self {
1885 channel_order: ImageChannelOrder::Bgra,
1886 alpha_policy: ImageAlphaPolicy::Opaque,
1887 }),
1888 _ => Err(Error::InvalidFrame(format!(
1889 "Unsupported format for image encoding: {pixel_format:?}. Only RGBA/RGBX/BGRA/BGRX are supported."
1890 ))),
1891 }
1892 }
1893
1894 fn can_borrow_tightly_packed_png(self) -> bool {
1895 self.channel_order == ImageChannelOrder::Rgba
1896 && self.alpha_policy == ImageAlphaPolicy::Preserve
1897 }
1898
1899 fn rgba_pixel(self, pixel: &[u8]) -> [u8; 4] {
1900 [
1901 pixel[self.channel_order.red_index()],
1902 pixel[1],
1903 pixel[self.channel_order.blue_index()],
1904 match self.alpha_policy {
1905 ImageAlphaPolicy::Preserve => pixel[3],
1906 ImageAlphaPolicy::Opaque => 255,
1907 },
1908 ]
1909 }
1910
1911 fn rgb_pixel(self, pixel: &[u8]) -> [u8; 3] {
1912 [
1913 pixel[self.channel_order.red_index()],
1914 pixel[1],
1915 pixel[self.channel_order.blue_index()],
1916 ]
1917 }
1918}
1919
1920#[cfg(feature = "image-encoding")]
1921#[derive(Debug)]
1922struct ImagePixelSource<'a> {
1923 data: &'a [u8],
1924 width: usize,
1925 height: usize,
1926 line_stride: usize,
1927 active_row_bytes: usize,
1928 pixel_format: ImagePixelFormat,
1929}
1930
1931#[cfg(feature = "image-encoding")]
1932impl<'a> ImagePixelSource<'a> {
1933 fn new(layout: ValidatedVideoLayout, data: &'a [u8]) -> Result<Self> {
1934 let pixel_format = ImagePixelFormat::from_pixel_format(layout.pixel_format)?;
1935
1936 if data.len() != layout.data_len_bytes {
1937 return Err(Error::InvalidFrame(format!(
1938 "Video data length {}, expected {} bytes for validated layout",
1939 data.len(),
1940 layout.data_len_bytes
1941 )));
1942 }
1943
1944 let line_stride = match layout.line_stride_or_size {
1945 LineStrideOrSize::LineStrideBytes(stride) => {
1946 if stride <= 0 {
1947 return Err(Error::InvalidFrame(format!(
1948 "Image line stride must be positive, got {stride}"
1949 )));
1950 }
1951
1952 usize::try_from(stride).map_err(|_| {
1953 Error::InvalidFrame(format!("Invalid image line stride value: {stride}"))
1954 })?
1955 }
1956 LineStrideOrSize::DataSizeBytes(size) => {
1957 return Err(Error::InvalidFrame(format!(
1958 "Cannot encode image from data-size frame ({size} bytes). Image encoding requires line_stride_in_bytes."
1959 )));
1960 }
1961 };
1962
1963 let width = usize::try_from(layout.width)
1964 .map_err(|_| Error::InvalidFrame(format!("Invalid image width: {}", layout.width)))?;
1965 let height = usize::try_from(layout.height)
1966 .map_err(|_| Error::InvalidFrame(format!("Invalid image height: {}", layout.height)))?;
1967
1968 if width == 0 || height == 0 {
1969 return Err(Error::InvalidFrame(format!(
1970 "Image dimensions must be positive, got {}x{}",
1971 layout.width, layout.height
1972 )));
1973 }
1974
1975 let active_row_bytes = width
1976 .checked_mul(ImagePixelFormat::BYTES_PER_PIXEL)
1977 .ok_or_else(|| {
1978 Error::InvalidFrame(format!(
1979 "Image row size overflow for width {} and {} bytes per pixel",
1980 width,
1981 ImagePixelFormat::BYTES_PER_PIXEL
1982 ))
1983 })?;
1984
1985 if line_stride < active_row_bytes {
1986 return Err(Error::InvalidFrame(format!(
1987 "Image line stride {line_stride} is smaller than active row size {active_row_bytes}"
1988 )));
1989 }
1990
1991 let expected_data_len = line_stride.checked_mul(height).ok_or_else(|| {
1992 Error::InvalidFrame(format!(
1993 "Image data length overflow: {line_stride} stride x {height} height"
1994 ))
1995 })?;
1996
1997 if expected_data_len != layout.data_len_bytes {
1998 return Err(Error::InvalidFrame(format!(
1999 "Image layout data length {} does not match line stride x height ({expected_data_len})",
2000 layout.data_len_bytes
2001 )));
2002 }
2003
2004 Ok(Self {
2005 data,
2006 width,
2007 height,
2008 line_stride,
2009 active_row_bytes,
2010 pixel_format,
2011 })
2012 }
2013
2014 fn png_rgba_input(&self) -> Result<(Cow<'a, [u8]>, u32, u32)> {
2015 let width = u32::try_from(self.width).map_err(|_| {
2016 Error::InvalidFrame(format!("PNG width {} exceeds u32 range", self.width))
2017 })?;
2018 let height = u32::try_from(self.height).map_err(|_| {
2019 Error::InvalidFrame(format!("PNG height {} exceeds u32 range", self.height))
2020 })?;
2021
2022 if self.pixel_format.can_borrow_tightly_packed_png()
2023 && self.line_stride == self.active_row_bytes
2024 {
2025 return Ok((Cow::Borrowed(self.data), width, height));
2026 }
2027
2028 let mut rgba = Vec::with_capacity(self.output_len(4)?);
2029
2030 if self.pixel_format.can_borrow_tightly_packed_png() {
2031 for row in self.active_rows() {
2032 rgba.extend_from_slice(row);
2033 }
2034 } else {
2035 for row in self.active_rows() {
2036 for pixel in row.chunks_exact(ImagePixelFormat::BYTES_PER_PIXEL) {
2037 rgba.extend_from_slice(&self.pixel_format.rgba_pixel(pixel));
2038 }
2039 }
2040 }
2041
2042 Ok((Cow::Owned(rgba), width, height))
2043 }
2044
2045 fn jpeg_rgb_input(&self, quality: u8) -> Result<(Vec<u8>, u16, u16)> {
2046 if !(1..=100).contains(&quality) {
2047 return Err(Error::InvalidFrame(format!(
2048 "JPEG quality must be in 1..=100, got {quality}"
2049 )));
2050 }
2051
2052 let width = u16::try_from(self.width).map_err(|_| {
2053 Error::InvalidFrame(format!(
2054 "JPEG width {} exceeds maximum supported value {}",
2055 self.width,
2056 u16::MAX
2057 ))
2058 })?;
2059 let height = u16::try_from(self.height).map_err(|_| {
2060 Error::InvalidFrame(format!(
2061 "JPEG height {} exceeds maximum supported value {}",
2062 self.height,
2063 u16::MAX
2064 ))
2065 })?;
2066
2067 let mut rgb = Vec::with_capacity(self.output_len(3)?);
2068 for row in self.active_rows() {
2069 for pixel in row.chunks_exact(ImagePixelFormat::BYTES_PER_PIXEL) {
2070 rgb.extend_from_slice(&self.pixel_format.rgb_pixel(pixel));
2071 }
2072 }
2073
2074 Ok((rgb, width, height))
2075 }
2076
2077 fn output_len(&self, channels: usize) -> Result<usize> {
2078 self.width
2079 .checked_mul(self.height)
2080 .and_then(|pixels| pixels.checked_mul(channels))
2081 .ok_or_else(|| {
2082 Error::InvalidFrame(format!(
2083 "Image output buffer size overflow: {}x{}x{}",
2084 self.width, self.height, channels
2085 ))
2086 })
2087 }
2088
2089 fn active_rows(&self) -> impl Iterator<Item = &'a [u8]> + '_ {
2090 (0..self.height).map(move |row| {
2091 let start = row * self.line_stride;
2092 let end = start + self.active_row_bytes;
2093 &self.data[start..end]
2094 })
2095 }
2096}
2097
2098#[derive(Debug, Clone, Copy)]
2109pub(crate) struct ValidatedVideoLayout {
2110 pub width: i32,
2112 pub height: i32,
2114 pub pixel_format: PixelFormat,
2116 pub data_len_bytes: usize,
2118 pub line_stride_or_size: LineStrideOrSize,
2122}
2123
2124impl ValidatedVideoLayout {
2125 pub(crate) fn new_uncompressed(
2126 pixel_format: PixelFormat,
2127 width: i32,
2128 height: i32,
2129 line_stride: Option<i32>,
2130 ) -> Result<Self> {
2131 validate_video_dimensions_for_format(pixel_format, width, height)?;
2132
2133 let width_usize = usize::try_from(width)
2134 .map_err(|_| Error::InvalidFrame(format!("Invalid width value: {width}")))?;
2135 let height_usize = usize::try_from(height)
2136 .map_err(|_| Error::InvalidFrame(format!("Invalid height value: {height}")))?;
2137
2138 let min_stride = min_video_line_stride_checked(pixel_format, width_usize)?;
2139 let line_stride_usize = match line_stride {
2140 Some(stride) => {
2141 if stride <= 0 {
2142 return Err(Error::InvalidFrame(format!(
2143 "Uncompressed video frame has invalid line_stride_in_bytes: {stride}"
2144 )));
2145 }
2146
2147 let stride_usize = usize::try_from(stride).map_err(|_| {
2148 Error::InvalidFrame(format!("Invalid line_stride_in_bytes value: {stride}"))
2149 })?;
2150
2151 if stride_usize < min_stride {
2152 return Err(Error::InvalidFrame(format!(
2153 "Video line_stride_in_bytes {stride} is smaller than minimum row size {min_stride} for {pixel_format:?} width {width}"
2154 )));
2155 }
2156
2157 stride_usize
2158 }
2159 None => min_stride,
2160 };
2161
2162 if pixel_format.info().is_planar_420() && !line_stride_usize.is_multiple_of(2) {
2163 return Err(Error::InvalidFrame(format!(
2164 "Planar 4:2:0 video frame has odd line_stride_in_bytes: {line_stride_usize}"
2165 )));
2166 }
2167
2168 let data_len_bytes =
2169 calculate_buffer_len_checked(pixel_format, line_stride_usize, height_usize)?;
2170 validate_video_data_len(data_len_bytes)?;
2171
2172 let line_stride_i32 = i32::try_from(line_stride_usize).map_err(|_| {
2173 Error::InvalidFrame(format!(
2174 "Video line stride {line_stride_usize} exceeds i32 range"
2175 ))
2176 })?;
2177
2178 Ok(Self {
2179 width,
2180 height,
2181 pixel_format,
2182 data_len_bytes,
2183 line_stride_or_size: LineStrideOrSize::LineStrideBytes(line_stride_i32),
2184 })
2185 }
2186}
2187
2188pub(crate) fn validate_video_layout(raw: &NDIlib_video_frame_v2_t) -> Result<ValidatedVideoLayout> {
2204 if raw.p_data.is_null() {
2205 return Err(Error::InvalidFrame(
2206 "Video frame has null data pointer".into(),
2207 ));
2208 }
2209
2210 validate_video_frame_metadata(raw.frame_rate_N, raw.frame_rate_D, raw.picture_aspect_ratio)?;
2211
2212 #[allow(clippy::unnecessary_cast)]
2213 ScanType::try_from(raw.frame_format_type as u32).map_err(|_| {
2214 Error::InvalidFrame(format!(
2215 "Unknown scan type: 0x{:08X}",
2216 raw.frame_format_type
2217 ))
2218 })?;
2219
2220 #[allow(clippy::unnecessary_cast)] let pixel_format = PixelFormat::try_from(raw.FourCC as u32).map_err(|_| {
2222 Error::InvalidFrame(format!("Unknown pixel format FourCC: 0x{:08X}", raw.FourCC))
2223 })?;
2224
2225 let line_stride = unsafe { raw.__bindgen_anon_1.line_stride_in_bytes };
2226 ValidatedVideoLayout::new_uncompressed(pixel_format, raw.xres, raw.yres, Some(line_stride))
2227}
2228
2229fn validate_video_dimensions_for_format(
2230 pixel_format: PixelFormat,
2231 width: i32,
2232 height: i32,
2233) -> Result<()> {
2234 validate_video_width_for_format(pixel_format, width)?;
2235 if height <= 0 {
2236 return Err(Error::InvalidFrame(format!(
2237 "Video frame has invalid height: {height}"
2238 )));
2239 }
2240
2241 if pixel_format.info().is_planar_420() && (width % 2 != 0 || height % 2 != 0) {
2242 return Err(Error::InvalidFrame(format!(
2243 "Planar 4:2:0 video frames require even dimensions, got {}x{}",
2244 width, height
2245 )));
2246 }
2247
2248 Ok(())
2249}
2250
2251fn validate_video_width_for_format(pixel_format: PixelFormat, width: i32) -> Result<()> {
2252 if width <= 0 {
2253 return Err(Error::InvalidFrame(format!(
2254 "Video frame has invalid width: {width}"
2255 )));
2256 }
2257
2258 if pixel_format.info().is_planar_420() && width % 2 != 0 {
2259 return Err(Error::InvalidFrame(format!(
2260 "Planar 4:2:0 video frames require even width, got {width}"
2261 )));
2262 }
2263
2264 Ok(())
2265}
2266
2267pub(crate) fn validate_video_frame_metadata(
2268 frame_rate_n: i32,
2269 frame_rate_d: i32,
2270 picture_aspect_ratio: f32,
2271) -> Result<()> {
2272 if frame_rate_n <= 0 {
2273 return Err(Error::InvalidFrame(format!(
2274 "Video frame has invalid frame rate numerator: {frame_rate_n}"
2275 )));
2276 }
2277 if frame_rate_d <= 0 {
2278 return Err(Error::InvalidFrame(format!(
2279 "Video frame has invalid frame rate denominator: {frame_rate_d}"
2280 )));
2281 }
2282 if !picture_aspect_ratio.is_finite() || picture_aspect_ratio <= 0.0 {
2283 return Err(Error::InvalidFrame(format!(
2284 "Video frame has invalid picture aspect ratio: {picture_aspect_ratio}"
2285 )));
2286 }
2287
2288 Ok(())
2289}
2290
2291fn validate_video_data_len(data_len_bytes: usize) -> Result<()> {
2292 if data_len_bytes == 0 {
2293 return Err(Error::InvalidFrame(
2294 "Video frame has zero calculated size".into(),
2295 ));
2296 }
2297
2298 if data_len_bytes > MAX_VIDEO_BYTES {
2299 return Err(Error::InvalidFrame(format!(
2300 "Video frame exceeds maximum size: {} bytes > {} bytes",
2301 data_len_bytes, MAX_VIDEO_BYTES
2302 )));
2303 }
2304
2305 Ok(())
2306}
2307
2308fn min_video_line_stride_checked(pixel_format: PixelFormat, width: usize) -> Result<usize> {
2309 width
2310 .checked_mul(pixel_format.info().bytes_per_pixel() as usize)
2311 .ok_or_else(|| {
2312 Error::InvalidFrame(format!(
2313 "Video line stride overflow for {:?} width {}",
2314 pixel_format, width
2315 ))
2316 })
2317}
2318
2319fn calculate_buffer_len_checked(
2321 pixel_format: PixelFormat,
2322 y_stride: usize,
2323 height: usize,
2324) -> Result<usize> {
2325 calculate_buffer_len_for_info_checked(pixel_format.info(), y_stride, height)
2326}
2327
2328fn calculate_buffer_len_for_info_checked(
2329 info: PixelFormatInfo,
2330 y_stride: usize,
2331 height: usize,
2332) -> Result<usize> {
2333 let y_size = y_stride.checked_mul(height).ok_or_else(|| {
2335 Error::InvalidFrame(format!(
2336 "Video buffer size overflow: {} stride × {} height",
2337 y_stride, height
2338 ))
2339 })?;
2340
2341 match info.category() {
2342 FormatCategory::Packed => Ok(y_size),
2343 FormatCategory::Planar420 => {
2344 if !height.is_multiple_of(2) || !y_stride.is_multiple_of(2) {
2345 return Err(Error::InvalidFrame(
2346 "Planar 4:2:0 video frames require even height and stride".into(),
2347 ));
2348 }
2349 let chroma_height = height / 2;
2352 let u_stride = y_stride / 2;
2353 let v_stride = y_stride / 2;
2354
2355 let u_size = u_stride
2356 .checked_mul(chroma_height)
2357 .ok_or_else(|| Error::InvalidFrame("Video U-plane size overflow".into()))?;
2358 let v_size = v_stride
2359 .checked_mul(chroma_height)
2360 .ok_or_else(|| Error::InvalidFrame("Video V-plane size overflow".into()))?;
2361
2362 let total = y_size
2363 .checked_add(u_size)
2364 .and_then(|s| s.checked_add(v_size))
2365 .ok_or_else(|| Error::InvalidFrame("Video total buffer size overflow".into()))?;
2366
2367 Ok(total)
2368 }
2369 FormatCategory::SemiPlanar420 => {
2370 if !height.is_multiple_of(2) {
2371 return Err(Error::InvalidFrame(
2372 "Semi-planar 4:2:0 video frames require even height".into(),
2373 ));
2374 }
2375 let chroma_height = height / 2;
2378 let uv_size = y_stride
2379 .checked_mul(chroma_height)
2380 .ok_or_else(|| Error::InvalidFrame("Video UV-plane size overflow".into()))?;
2381
2382 let total = y_size
2383 .checked_add(uv_size)
2384 .ok_or_else(|| Error::InvalidFrame("Video total buffer size overflow".into()))?;
2385
2386 Ok(total)
2387 }
2388 }
2389}
2390
2391#[derive(Debug, Clone, Copy)]
2398pub(crate) struct ValidatedAudioLayout {
2399 pub format: Option<AudioFormat>,
2401 pub sample_rate: i32,
2403 pub no_channels: usize,
2405 pub no_samples: usize,
2407 pub channel_stride_in_bytes: i32,
2409 pub channel_stride_samples: usize,
2411 pub sample_count: usize,
2415}
2416
2417impl ValidatedAudioLayout {
2418 pub(crate) fn is_empty(self) -> bool {
2419 self.sample_count == 0
2420 }
2421
2422 pub(crate) fn format(self) -> Option<AudioFormat> {
2423 self.format
2424 }
2425
2426 pub(crate) fn channel_range(self, channel: usize) -> Option<std::ops::Range<usize>> {
2427 if self.is_empty() || channel >= self.no_channels {
2428 return None;
2429 }
2430
2431 let start = channel.checked_mul(self.channel_stride_samples)?;
2432 let end = start.checked_add(self.no_samples)?;
2433
2434 (end <= self.sample_count).then_some(start..end)
2435 }
2436}
2437
2438fn validate_outbound_audio_layout(
2439 sample_rate: i32,
2440 no_channels: i32,
2441 no_samples: i32,
2442 format: AudioFormat,
2443) -> Result<ValidatedAudioLayout> {
2444 if sample_rate <= 0 {
2445 return Err(Error::InvalidFrame(format!(
2446 "Invalid sample rate: {sample_rate}"
2447 )));
2448 }
2449 if no_channels <= 0 {
2450 return Err(Error::InvalidFrame(format!(
2451 "Invalid number of channels: {no_channels}"
2452 )));
2453 }
2454 if no_samples <= 0 {
2455 return Err(Error::InvalidFrame(format!(
2456 "Invalid number of samples: {no_samples}"
2457 )));
2458 }
2459
2460 validate_audio_format(format.into())?;
2461
2462 let no_channels = usize::try_from(no_channels)
2463 .map_err(|_| Error::InvalidFrame(format!("Invalid no_channels value: {no_channels}")))?;
2464 let no_samples = usize::try_from(no_samples)
2465 .map_err(|_| Error::InvalidFrame(format!("Invalid no_samples value: {no_samples}")))?;
2466
2467 let channel_stride_bytes = no_samples
2468 .checked_mul(std::mem::size_of::<f32>())
2469 .ok_or_else(|| {
2470 Error::InvalidFrame(format!(
2471 "Audio channel stride overflow: {} samples × {} bytes",
2472 no_samples,
2473 std::mem::size_of::<f32>()
2474 ))
2475 })?;
2476 let channel_stride_in_bytes = i32::try_from(channel_stride_bytes).map_err(|_| {
2477 Error::InvalidFrame(format!(
2478 "Audio channel stride {channel_stride_bytes} exceeds i32 range"
2479 ))
2480 })?;
2481
2482 let sample_count = no_channels.checked_mul(no_samples).ok_or_else(|| {
2483 Error::InvalidFrame(format!(
2484 "Audio sample count overflow: {no_channels} channels × {no_samples} samples"
2485 ))
2486 })?;
2487 let byte_size = sample_count
2488 .checked_mul(std::mem::size_of::<f32>())
2489 .ok_or_else(|| {
2490 Error::InvalidFrame(format!(
2491 "Audio byte size overflow: {} samples × {} bytes",
2492 sample_count,
2493 std::mem::size_of::<f32>()
2494 ))
2495 })?;
2496
2497 if byte_size > MAX_AUDIO_BYTES {
2498 return Err(Error::InvalidFrame(format!(
2499 "Audio frame exceeds maximum size: {} bytes > {} bytes",
2500 byte_size, MAX_AUDIO_BYTES
2501 )));
2502 }
2503
2504 Ok(ValidatedAudioLayout {
2505 format: Some(format),
2506 sample_rate,
2507 no_channels,
2508 no_samples,
2509 channel_stride_in_bytes,
2510 channel_stride_samples: no_samples,
2511 sample_count,
2512 })
2513}
2514
2515pub(crate) fn validate_audio_layout(raw: &NDIlib_audio_frame_v3_t) -> Result<ValidatedAudioLayout> {
2532 validate_audio_layout_inner(raw, false)
2533}
2534
2535pub(crate) fn validate_audio_layout_allow_empty(
2538 raw: &NDIlib_audio_frame_v3_t,
2539) -> Result<ValidatedAudioLayout> {
2540 validate_audio_layout_inner(raw, true)
2541}
2542
2543fn validate_audio_layout_inner(
2544 raw: &NDIlib_audio_frame_v3_t,
2545 allow_empty: bool,
2546) -> Result<ValidatedAudioLayout> {
2547 if raw.no_samples == 0 {
2548 if allow_empty {
2549 return validate_empty_audio_layout(raw);
2550 }
2551
2552 return Err(Error::InvalidFrame("Invalid number of samples: 0".into()));
2553 }
2554
2555 if raw.p_data.is_null() {
2556 return Err(Error::InvalidFrame(
2557 "Audio frame has null data pointer".into(),
2558 ));
2559 }
2560
2561 if raw.sample_rate <= 0 {
2562 return Err(Error::InvalidFrame(format!(
2563 "Invalid sample rate: {}",
2564 raw.sample_rate
2565 )));
2566 }
2567
2568 if raw.no_channels <= 0 {
2569 return Err(Error::InvalidFrame(format!(
2570 "Invalid number of channels: {}",
2571 raw.no_channels
2572 )));
2573 }
2574
2575 if raw.no_samples <= 0 {
2576 return Err(Error::InvalidFrame(format!(
2577 "Invalid number of samples: {}",
2578 raw.no_samples
2579 )));
2580 }
2581
2582 let format = validate_audio_format(raw.FourCC)?;
2583 let channel_stride_in_bytes = unsafe { raw.__bindgen_anon_1.channel_stride_in_bytes };
2584 if channel_stride_in_bytes <= 0 {
2585 return Err(Error::InvalidFrame(format!(
2586 "Invalid channel_stride_in_bytes: {}",
2587 channel_stride_in_bytes
2588 )));
2589 }
2590
2591 let no_samples = usize::try_from(raw.no_samples).map_err(|_| {
2593 Error::InvalidFrame(format!("Invalid no_samples value: {}", raw.no_samples))
2594 })?;
2595
2596 let no_channels = usize::try_from(raw.no_channels).map_err(|_| {
2597 Error::InvalidFrame(format!("Invalid no_channels value: {}", raw.no_channels))
2598 })?;
2599
2600 let channel_stride_bytes = usize::try_from(channel_stride_in_bytes).map_err(|_| {
2601 Error::InvalidFrame(format!(
2602 "Invalid channel_stride_in_bytes value: {}",
2603 channel_stride_in_bytes
2604 ))
2605 })?;
2606
2607 if channel_stride_bytes % std::mem::size_of::<f32>() != 0 {
2608 return Err(Error::InvalidFrame(format!(
2609 "channel_stride_in_bytes must be a multiple of {}, got {}",
2610 std::mem::size_of::<f32>(),
2611 channel_stride_in_bytes
2612 )));
2613 }
2614
2615 let minimum_channel_stride = no_samples
2616 .checked_mul(std::mem::size_of::<f32>())
2617 .ok_or_else(|| {
2618 Error::InvalidFrame(format!(
2619 "Audio channel stride overflow: {} samples × {} bytes",
2620 no_samples,
2621 std::mem::size_of::<f32>()
2622 ))
2623 })?;
2624
2625 if channel_stride_bytes < minimum_channel_stride {
2626 return Err(Error::InvalidFrame(format!(
2627 "channel_stride_in_bytes {} is smaller than one channel of audio samples {}",
2628 channel_stride_in_bytes, minimum_channel_stride
2629 )));
2630 }
2631
2632 let channel_stride_samples = channel_stride_bytes / std::mem::size_of::<f32>();
2633 let last_channel_offset = no_channels
2634 .checked_sub(1)
2635 .and_then(|last| last.checked_mul(channel_stride_samples))
2636 .ok_or_else(|| {
2637 Error::InvalidFrame(format!(
2638 "Audio channel offset overflow: {} channels × {} stride samples",
2639 no_channels, channel_stride_samples
2640 ))
2641 })?;
2642
2643 let sample_count = last_channel_offset.checked_add(no_samples).ok_or_else(|| {
2644 Error::InvalidFrame(format!(
2645 "Audio backing sample count overflow: channel offset {} + {} samples",
2646 last_channel_offset, no_samples
2647 ))
2648 })?;
2649
2650 let byte_size = sample_count
2652 .checked_mul(std::mem::size_of::<f32>())
2653 .ok_or_else(|| {
2654 Error::InvalidFrame(format!(
2655 "Audio byte size overflow: {} samples × {} bytes",
2656 sample_count,
2657 std::mem::size_of::<f32>()
2658 ))
2659 })?;
2660
2661 if byte_size > MAX_AUDIO_BYTES {
2662 return Err(Error::InvalidFrame(format!(
2663 "Audio frame exceeds maximum size: {} bytes > {} bytes",
2664 byte_size, MAX_AUDIO_BYTES
2665 )));
2666 }
2667
2668 Ok(ValidatedAudioLayout {
2669 format: Some(format),
2670 sample_rate: raw.sample_rate,
2671 no_channels,
2672 no_samples,
2673 channel_stride_in_bytes,
2674 channel_stride_samples,
2675 sample_count,
2676 })
2677}
2678
2679fn validate_empty_audio_layout(raw: &NDIlib_audio_frame_v3_t) -> Result<ValidatedAudioLayout> {
2680 if !raw.p_data.is_null() {
2681 return Err(Error::InvalidFrame(
2682 "Zero-length audio frame has non-null data pointer".into(),
2683 ));
2684 }
2685
2686 if raw.sample_rate < 0 {
2687 return Err(Error::InvalidFrame(format!(
2688 "Invalid sample rate: {}",
2689 raw.sample_rate
2690 )));
2691 }
2692
2693 if raw.no_channels < 0 {
2694 return Err(Error::InvalidFrame(format!(
2695 "Invalid number of channels: {}",
2696 raw.no_channels
2697 )));
2698 }
2699
2700 let channel_stride_in_bytes = unsafe { raw.__bindgen_anon_1.channel_stride_in_bytes };
2701 if channel_stride_in_bytes != 0 {
2702 return Err(Error::InvalidFrame(format!(
2703 "Zero-length audio frame has non-zero channel_stride_in_bytes: {}",
2704 channel_stride_in_bytes
2705 )));
2706 }
2707
2708 let no_source = raw.sample_rate == 0 && raw.no_channels == 0;
2709 let query_format = raw.sample_rate > 0 && raw.no_channels > 0;
2710 if !no_source && !query_format {
2711 return Err(Error::InvalidFrame(format!(
2712 "Invalid zero-length audio query state: sample_rate={}, no_channels={}",
2713 raw.sample_rate, raw.no_channels
2714 )));
2715 }
2716
2717 let format = if no_source && raw.FourCC == 0 {
2718 None
2719 } else {
2720 Some(validate_audio_format(raw.FourCC)?)
2721 };
2722
2723 let no_channels = usize::try_from(raw.no_channels).map_err(|_| {
2724 Error::InvalidFrame(format!("Invalid no_channels value: {}", raw.no_channels))
2725 })?;
2726
2727 Ok(ValidatedAudioLayout {
2728 format,
2729 sample_rate: raw.sample_rate,
2730 no_channels,
2731 no_samples: 0,
2732 channel_stride_in_bytes: 0,
2733 channel_stride_samples: 0,
2734 sample_count: 0,
2735 })
2736}
2737
2738fn validate_audio_format(fourcc: NDIlib_FourCC_audio_type_e) -> Result<AudioFormat> {
2739 match fourcc {
2740 NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP => Ok(AudioFormat::FLTP),
2741 _ => Err(Error::InvalidFrame(format!(
2742 "Unknown audio format FourCC: 0x{:08X}",
2743 fourcc
2744 ))),
2745 }
2746}
2747
2748pub struct VideoRef<'a, S: FrameFree<RawFrame = NDIlib_video_frame_v2_t>> {
2785 guard: Guard<'a, S>,
2786 layout: ValidatedVideoLayout,
2789 metadata: ValidatedFrameMetadata,
2790}
2791
2792pub type VideoFrameRef<'rx> = VideoRef<'rx, VideoKind>;
2799
2800impl<'a, S: FrameFree<RawFrame = NDIlib_video_frame_v2_t>> VideoRef<'a, S> {
2801 pub(crate) unsafe fn new(guard: Guard<'a, S>) -> Result<Self> {
2817 let layout = validate_video_layout(guard.frame())?;
2818 let metadata = unsafe { validate_frame_metadata(guard.frame().p_metadata)? };
2819
2820 Ok(Self {
2821 guard,
2822 layout,
2823 metadata,
2824 })
2825 }
2826
2827 pub fn width(&self) -> i32 {
2829 self.guard.frame().xres
2830 }
2831
2832 pub fn height(&self) -> i32 {
2834 self.guard.frame().yres
2835 }
2836
2837 pub fn pixel_format(&self) -> PixelFormat {
2841 self.layout.pixel_format
2842 }
2843
2844 pub fn frame_rate_n(&self) -> i32 {
2846 self.guard.frame().frame_rate_N
2847 }
2848
2849 pub fn frame_rate_d(&self) -> i32 {
2851 self.guard.frame().frame_rate_D
2852 }
2853
2854 pub fn picture_aspect_ratio(&self) -> f32 {
2856 self.guard.frame().picture_aspect_ratio
2857 }
2858
2859 pub fn scan_type(&self) -> ScanType {
2863 #[allow(clippy::unnecessary_cast)]
2864 ScanType::try_from(self.guard.frame().frame_format_type as u32)
2865 .expect("VideoFrameRef validates scan type during construction")
2866 }
2867
2868 pub fn timecode(&self) -> i64 {
2870 self.guard.frame().timecode
2871 }
2872
2873 pub fn timestamp(&self) -> i64 {
2875 self.guard.frame().timestamp
2876 }
2877
2878 pub fn line_stride_or_size(&self) -> LineStrideOrSize {
2882 self.layout.line_stride_or_size
2883 }
2884
2885 pub fn metadata(&self) -> Option<&str> {
2887 unsafe { frame_metadata_str(self.guard.frame().p_metadata, self.metadata) }
2888 }
2889
2890 pub fn data(&self) -> &[u8] {
2904 unsafe { slice::from_raw_parts(self.guard.frame().p_data, self.layout.data_len_bytes) }
2909 }
2910
2911 pub fn to_owned(&self) -> Result<VideoFrame> {
2916 unsafe { VideoFrame::from_raw_validated(self.guard.frame(), self.layout, self.metadata) }
2917 }
2918}
2919
2920impl<'a, S: FrameFree<RawFrame = NDIlib_video_frame_v2_t>> fmt::Debug for VideoRef<'a, S> {
2921 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2922 f.debug_struct(S::REF_DEBUG_NAME)
2923 .field("width", &self.width())
2924 .field("height", &self.height())
2925 .field("pixel_format", &self.pixel_format())
2926 .field("frame_rate_n", &self.frame_rate_n())
2927 .field("frame_rate_d", &self.frame_rate_d())
2928 .field("picture_aspect_ratio", &self.picture_aspect_ratio())
2929 .field("scan_type", &self.scan_type())
2930 .field("timecode", &self.timecode())
2931 .field("data (bytes)", &self.data().len())
2932 .field("line_stride_or_size", &self.line_stride_or_size())
2933 .field("metadata", &self.metadata())
2934 .field("timestamp", &self.timestamp())
2935 .finish()
2936 }
2937}
2938
2939pub struct AudioRef<'a, S: FrameFree<RawFrame = NDIlib_audio_frame_v3_t>> {
2961 guard: Guard<'a, S>,
2962 layout: ValidatedAudioLayout,
2965 metadata: ValidatedFrameMetadata,
2966}
2967
2968pub type AudioFrameRef<'rx> = AudioRef<'rx, AudioKind>;
2977
2978impl<'a, S: FrameFree<RawFrame = NDIlib_audio_frame_v3_t>> AudioRef<'a, S> {
2979 pub(crate) unsafe fn from_validated_layout(
2988 guard: Guard<'a, S>,
2989 layout: ValidatedAudioLayout,
2990 ) -> Result<Self> {
2991 let metadata = unsafe { validate_frame_metadata(guard.frame().p_metadata)? };
2992
2993 Ok(Self {
2994 guard,
2995 layout,
2996 metadata,
2997 })
2998 }
2999
3000 pub fn sample_rate(&self) -> i32 {
3002 self.layout.sample_rate
3003 }
3004
3005 pub fn num_channels(&self) -> i32 {
3007 self.layout.no_channels as i32
3008 }
3009
3010 pub fn num_samples(&self) -> i32 {
3012 self.layout.no_samples as i32
3013 }
3014
3015 pub fn timecode(&self) -> i64 {
3017 self.guard.frame().timecode
3018 }
3019
3020 pub fn timestamp(&self) -> i64 {
3022 self.guard.frame().timestamp
3023 }
3024
3025 pub fn channel_stride_in_bytes(&self) -> i32 {
3027 self.layout.channel_stride_in_bytes
3028 }
3029
3030 pub fn metadata(&self) -> Option<&str> {
3032 unsafe { frame_metadata_str(self.guard.frame().p_metadata, self.metadata) }
3033 }
3034
3035 pub fn data(&self) -> &[f32] {
3047 if self.layout.is_empty() {
3048 return &[];
3049 }
3050
3051 unsafe {
3056 slice::from_raw_parts(
3057 self.guard.frame().p_data as *const f32,
3058 self.layout.sample_count,
3059 )
3060 }
3061 }
3062
3063 pub fn channel_data(&self, channel: usize) -> Option<&[f32]> {
3068 let range = self.layout.channel_range(channel)?;
3069 Some(&self.data()[range])
3070 }
3071}
3072
3073impl<'rx> AudioRef<'rx, AudioKind> {
3074 pub(crate) unsafe fn new(guard: RecvAudioGuard<'rx>) -> Result<Self> {
3090 let layout = validate_audio_layout(guard.frame())?;
3091 unsafe { Self::from_validated_layout(guard, layout) }
3092 }
3093
3094 pub fn format(&self) -> AudioFormat {
3098 self.layout
3099 .format()
3100 .expect("validate_audio_layout requires a concrete audio format")
3101 }
3102
3103 pub fn to_owned(&self) -> Result<AudioFrame> {
3108 AudioFrame::from_raw_validated(*self.guard.frame(), self.layout, self.metadata)
3109 }
3110}
3111
3112impl<'fs> AudioRef<'fs, FrameSyncAudioFree> {
3113 pub(crate) unsafe fn new(
3124 guard: Guard<'fs, FrameSyncAudioFree>,
3125 query_input: bool,
3126 ) -> Result<Self> {
3127 let layout = if query_input {
3128 if guard.frame().no_samples != 0 {
3129 return Err(Error::InvalidFrame(format!(
3130 "FrameSync audio query returned {} samples",
3131 guard.frame().no_samples
3132 )));
3133 }
3134 validate_audio_layout_allow_empty(guard.frame())?
3135 } else {
3136 validate_audio_layout(guard.frame())?
3137 };
3138 unsafe { Self::from_validated_layout(guard, layout) }
3139 }
3140
3141 pub fn is_empty(&self) -> bool {
3143 self.layout.is_empty()
3144 }
3145
3146 pub fn format(&self) -> Option<AudioFormat> {
3148 self.layout.format()
3149 }
3150
3151 pub fn to_owned(&self) -> Result<Option<AudioFrame>> {
3159 if self.layout.is_empty() {
3160 Ok(None)
3161 } else {
3162 AudioFrame::from_raw_validated(*self.guard.frame(), self.layout, self.metadata)
3163 .map(Some)
3164 }
3165 }
3166}
3167
3168impl<'a, S: FrameFree<RawFrame = NDIlib_audio_frame_v3_t>> fmt::Debug for AudioRef<'a, S> {
3169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3170 f.debug_struct(S::REF_DEBUG_NAME)
3171 .field("sample_rate", &self.sample_rate())
3172 .field("num_channels", &self.num_channels())
3173 .field("num_samples", &self.num_samples())
3174 .field("timecode", &self.timecode())
3175 .field("format", &self.layout.format())
3179 .field("data (samples)", &self.data().len())
3180 .field("channel_stride_in_bytes", &self.channel_stride_in_bytes())
3181 .field("metadata", &self.metadata())
3182 .field("timestamp", &self.timestamp())
3183 .finish()
3184 }
3185}
3186
3187pub struct MetadataFrameRef<'rx> {
3219 guard: RecvMetadataGuard<'rx>,
3220 layout: ValidatedMetadataLayout,
3223}
3224
3225impl<'rx> MetadataFrameRef<'rx> {
3226 pub(crate) unsafe fn new(guard: RecvMetadataGuard<'rx>) -> Result<Self> {
3233 let layout = validate_metadata_layout(guard.frame())?;
3234
3235 Ok(Self { guard, layout })
3236 }
3237
3238 pub fn timecode(&self) -> i64 {
3240 self.guard.frame().timecode
3241 }
3242
3243 pub fn data(&self) -> &str {
3248 let bytes = self.as_bytes();
3249 unsafe { str::from_utf8_unchecked(bytes) }
3252 }
3253
3254 pub fn as_bytes(&self) -> &[u8] {
3257 metadata_payload_bytes(self.guard.frame(), self.layout)
3258 }
3259
3260 pub fn to_owned(&self) -> MetadataFrame {
3265 unsafe { MetadataFrame::from_raw_validated(self.guard.frame(), self.layout) }
3266 }
3267}
3268
3269impl<'rx> fmt::Debug for MetadataFrameRef<'rx> {
3270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3271 f.debug_struct("MetadataFrameRef")
3272 .field("data", &self.data())
3273 .field("data (bytes)", &self.as_bytes().len())
3274 .field("timecode", &self.timecode())
3275 .finish()
3276 }
3277}
3278
3279#[cfg(test)]
3280mod tests {
3281 use super::*;
3282
3283 #[cfg(feature = "image-encoding")]
3284 fn image_test_frame(
3285 pixel_format: PixelFormat,
3286 width: i32,
3287 height: i32,
3288 line_stride: Option<i32>,
3289 data: Vec<u8>,
3290 ) -> VideoFrame {
3291 let layout =
3292 ValidatedVideoLayout::new_uncompressed(pixel_format, width, height, line_stride)
3293 .unwrap();
3294 assert_eq!(data.len(), layout.data_len_bytes);
3295
3296 VideoFrame {
3297 layout,
3298 frame_rate_n: 60,
3299 frame_rate_d: 1,
3300 picture_aspect_ratio: 16.0 / 9.0,
3301 scan_type: ScanType::Progressive,
3302 timecode: 0,
3303 data,
3304 metadata: None,
3305 timestamp: 0,
3306 }
3307 }
3308
3309 #[cfg(feature = "image-encoding")]
3310 fn decode_png_rgba(png_bytes: &[u8]) -> (u32, u32, Vec<u8>) {
3311 let decoder = png::Decoder::new(std::io::Cursor::new(png_bytes));
3312 let mut reader = decoder.read_info().unwrap();
3313 let output_size = reader.output_buffer_size().unwrap();
3314 let mut output = vec![0; output_size];
3315 let info = reader.next_frame(&mut output).unwrap();
3316 output.truncate(info.buffer_size());
3317
3318 assert_eq!(info.color_type, png::ColorType::Rgba);
3319 assert_eq!(info.bit_depth, png::BitDepth::Eight);
3320
3321 (info.width, info.height, output)
3322 }
3323
3324 #[cfg(feature = "image-encoding")]
3325 fn assert_jpeg_markers(jpeg_bytes: &[u8]) {
3326 assert!(jpeg_bytes.len() >= 4);
3327 assert_eq!(&jpeg_bytes[..2], &[0xFF, 0xD8]);
3328 assert_eq!(&jpeg_bytes[jpeg_bytes.len() - 2..], &[0xFF, 0xD9]);
3329 }
3330
3331 #[cfg(feature = "image-encoding")]
3332 #[test]
3333 fn test_encode_png_decodes_exact_supported_pixels() {
3334 let cases = [
3335 (
3336 PixelFormat::RGBA,
3337 vec![10, 20, 30, 40, 50, 60, 70, 80],
3338 vec![10, 20, 30, 40, 50, 60, 70, 80],
3339 ),
3340 (
3341 PixelFormat::BGRA,
3342 vec![30, 20, 10, 40, 70, 60, 50, 80],
3343 vec![10, 20, 30, 40, 50, 60, 70, 80],
3344 ),
3345 (
3346 PixelFormat::RGBX,
3347 vec![10, 20, 30, 0, 50, 60, 70, 99],
3348 vec![10, 20, 30, 255, 50, 60, 70, 255],
3349 ),
3350 (
3351 PixelFormat::BGRX,
3352 vec![30, 20, 10, 0, 70, 60, 50, 99],
3353 vec![10, 20, 30, 255, 50, 60, 70, 255],
3354 ),
3355 ];
3356
3357 for (pixel_format, data, expected) in cases {
3358 let frame = image_test_frame(pixel_format, 2, 1, None, data);
3359 let png = frame.encode_png().unwrap();
3360 let (width, height, decoded) = decode_png_rgba(&png);
3361
3362 assert_eq!((width, height), (2, 1), "{pixel_format:?}");
3363 assert_eq!(decoded, expected, "{pixel_format:?}");
3364 }
3365 }
3366
3367 #[cfg(feature = "image-encoding")]
3368 #[test]
3369 fn test_encode_png_skips_padded_rows() {
3370 let data = vec![
3371 1, 2, 3, 0, 4, 5, 6, 0, 200, 201, 202, 203, 7, 8, 9, 0, 10, 11, 12, 0, 204, 205, 206,
3372 207,
3373 ];
3374 let frame = image_test_frame(PixelFormat::RGBX, 2, 2, Some(12), data);
3375
3376 let png = frame.encode_png().unwrap();
3377 let (width, height, decoded) = decode_png_rgba(&png);
3378
3379 assert_eq!((width, height), (2, 2));
3380 assert_eq!(
3381 decoded,
3382 vec![1, 2, 3, 255, 4, 5, 6, 255, 7, 8, 9, 255, 10, 11, 12, 255,]
3383 );
3384 }
3385
3386 #[cfg(feature = "image-encoding")]
3387 #[test]
3388 fn test_encode_jpeg_accepts_supported_image_formats() {
3389 let cases = [
3390 (PixelFormat::RGBA, vec![10, 20, 30, 40]),
3391 (PixelFormat::BGRA, vec![30, 20, 10, 40]),
3392 (PixelFormat::RGBX, vec![10, 20, 30, 0]),
3393 (PixelFormat::BGRX, vec![30, 20, 10, 0]),
3394 ];
3395
3396 for (pixel_format, pixel) in cases {
3397 let data = pixel.repeat(16);
3398 let frame = image_test_frame(pixel_format, 4, 4, None, data);
3399 let jpeg = frame.encode_jpeg(90).unwrap();
3400 assert_jpeg_markers(&jpeg);
3401 }
3402 }
3403
3404 #[cfg(feature = "image-encoding")]
3405 #[test]
3406 fn test_encode_jpeg_skips_padded_rows() {
3407 let data = vec![
3408 3, 2, 1, 0, 6, 5, 4, 0, 250, 251, 252, 253, 9, 8, 7, 0, 12, 11, 10, 0, 254, 255, 0, 1,
3409 ];
3410 let frame = image_test_frame(PixelFormat::BGRX, 2, 2, Some(12), data);
3411
3412 let jpeg = frame.encode_jpeg(85).unwrap();
3413 assert_jpeg_markers(&jpeg);
3414 }
3415
3416 #[cfg(feature = "image-encoding")]
3417 #[test]
3418 fn test_encode_jpeg_rejects_invalid_quality() {
3419 let frame = image_test_frame(PixelFormat::RGBA, 2, 2, None, vec![0; 16]);
3420
3421 for quality in [0, 101, 255] {
3422 let err = frame.encode_jpeg(quality).unwrap_err();
3423 let err_msg = err.to_string();
3424 assert!(
3425 matches!(&err, Error::InvalidFrame(message) if message.contains("quality")),
3426 "unexpected error for quality {quality}: {err_msg}"
3427 );
3428 }
3429 }
3430
3431 #[cfg(feature = "image-encoding")]
3432 #[test]
3433 fn test_encode_jpeg_rejects_oversized_dimensions_before_casting() {
3434 let wide = image_test_frame(PixelFormat::RGBA, 65_536, 1, None, vec![0; 65_536 * 4]);
3435 let err = wide.encode_jpeg(85).unwrap_err();
3436 let err_msg = err.to_string();
3437 assert!(
3438 matches!(&err, Error::InvalidFrame(message) if message.contains("JPEG width")),
3439 "unexpected error: {err_msg}"
3440 );
3441
3442 let tall = image_test_frame(PixelFormat::RGBA, 1, 65_536, None, vec![0; 65_536 * 4]);
3443 let err = tall.encode_jpeg(85).unwrap_err();
3444 let err_msg = err.to_string();
3445 assert!(
3446 matches!(&err, Error::InvalidFrame(message) if message.contains("JPEG height")),
3447 "unexpected error: {err_msg}"
3448 );
3449 }
3450
3451 #[cfg(feature = "image-encoding")]
3452 #[test]
3453 fn test_encode_jpeg_rejects_unsupported_format() {
3454 let frame = image_test_frame(PixelFormat::UYVY, 2, 2, None, vec![0; 8]);
3455
3456 let err = frame.encode_jpeg(85).unwrap_err();
3457 let err_msg = err.to_string();
3458 assert!(
3459 matches!(&err, Error::InvalidFrame(message) if message.contains("Unsupported format")),
3460 "unexpected error: {err_msg}"
3461 );
3462 }
3463
3464 #[cfg(feature = "image-encoding")]
3465 #[test]
3466 fn test_png_input_borrows_only_tightly_packed_rgba() {
3467 let rgba = image_test_frame(PixelFormat::RGBA, 2, 1, None, vec![1, 2, 3, 4, 5, 6, 7, 8]);
3468 let source = ImagePixelSource::new(rgba.layout, rgba.data()).unwrap();
3469 let (pixels, _, _) = source.png_rgba_input().unwrap();
3470 assert!(matches!(pixels, Cow::Borrowed(_)));
3471
3472 let padded_rgba = image_test_frame(
3473 PixelFormat::RGBA,
3474 2,
3475 1,
3476 Some(12),
3477 vec![1, 2, 3, 4, 5, 6, 7, 8, 200, 201, 202, 203],
3478 );
3479 let source = ImagePixelSource::new(padded_rgba.layout, padded_rgba.data()).unwrap();
3480 let (pixels, _, _) = source.png_rgba_input().unwrap();
3481 assert!(matches!(pixels, Cow::Owned(_)));
3482
3483 let rgbx = image_test_frame(PixelFormat::RGBX, 2, 1, None, vec![1, 2, 3, 0, 5, 6, 7, 0]);
3484 let source = ImagePixelSource::new(rgbx.layout, rgbx.data()).unwrap();
3485 let (pixels, _, _) = source.png_rgba_input().unwrap();
3486 assert!(matches!(pixels, Cow::Owned(_)));
3487 }
3488
3489 #[cfg(feature = "image-encoding")]
3490 #[test]
3491 fn test_image_pixel_source_rejects_data_size_layout_and_bad_data_len() {
3492 let data_size_layout = ValidatedVideoLayout {
3493 width: 2,
3494 height: 1,
3495 pixel_format: PixelFormat::RGBA,
3496 data_len_bytes: 8,
3497 line_stride_or_size: LineStrideOrSize::DataSizeBytes(8),
3498 };
3499 let err = ImagePixelSource::new(data_size_layout, &[0; 8]).unwrap_err();
3500 let err_msg = err.to_string();
3501 assert!(
3502 matches!(&err, Error::InvalidFrame(message) if message.contains("data-size frame")),
3503 "unexpected error: {err_msg}"
3504 );
3505
3506 let layout = ValidatedVideoLayout::new_uncompressed(PixelFormat::RGBA, 2, 1, None).unwrap();
3507 let err = ImagePixelSource::new(layout, &[0; 7]).unwrap_err();
3508 let err_msg = err.to_string();
3509 assert!(
3510 matches!(&err, Error::InvalidFrame(message) if message.contains("Video data length")),
3511 "unexpected error: {err_msg}"
3512 );
3513 }
3514
3515 #[test]
3517 fn test_pixel_format_info_packed_rgb() {
3518 let formats = [
3519 PixelFormat::BGRA,
3520 PixelFormat::BGRX,
3521 PixelFormat::RGBA,
3522 PixelFormat::RGBX,
3523 ];
3524
3525 for fmt in formats {
3526 let info = fmt.info();
3527 assert_eq!(
3528 info.bytes_per_pixel(),
3529 4,
3530 "Format {:?} bytes per pixel",
3531 fmt
3532 );
3533 assert_eq!(
3534 info.category(),
3535 FormatCategory::Packed,
3536 "Format {:?} category",
3537 fmt
3538 );
3539 assert!(
3540 !info.is_planar_420(),
3541 "Format {:?} should not be planar",
3542 fmt
3543 );
3544
3545 let len = info.try_buffer_len(7680, 1080).unwrap();
3547 assert_eq!(len, 7680 * 1080, "Format {:?} even dimensions", fmt);
3548
3549 let len = info.try_buffer_len(7684, 1081).unwrap();
3551 assert_eq!(len, 7684 * 1081, "Format {:?} odd dimensions", fmt);
3552 }
3553 }
3554
3555 #[test]
3557 fn test_pixel_format_info_packed_yuv() {
3558 let info = PixelFormat::UYVY.info();
3560 assert_eq!(info.bytes_per_pixel(), 2);
3561 assert_eq!(info.category(), FormatCategory::Packed);
3562 let len = info.try_buffer_len(3840, 1080).unwrap();
3563 assert_eq!(len, 3840 * 1080);
3564
3565 let info = PixelFormat::UYVA.info();
3567 assert_eq!(info.bytes_per_pixel(), 3);
3568 assert_eq!(info.category(), FormatCategory::Packed);
3569 let len = info.try_buffer_len(5760, 1080).unwrap();
3570 assert_eq!(len, 5760 * 1080);
3571
3572 let info = PixelFormat::P216.info();
3574 assert_eq!(info.bytes_per_pixel(), 4);
3575 assert_eq!(info.category(), FormatCategory::Packed);
3576 let len = info.try_buffer_len(7680, 1080).unwrap();
3577 assert_eq!(len, 7680 * 1080);
3578
3579 let info = PixelFormat::PA16.info();
3580 assert_eq!(info.bytes_per_pixel(), 4);
3581 let len = info.try_buffer_len(7680, 1080).unwrap();
3582 assert_eq!(len, 7680 * 1080);
3583 }
3584
3585 #[test]
3587 fn test_pixel_format_info_planar_420_even() {
3588 let y_stride = 1920;
3594
3595 let info = PixelFormat::YV12.info();
3596 assert_eq!(info.bytes_per_pixel(), 1);
3597 assert_eq!(info.category(), FormatCategory::Planar420);
3598 assert!(info.is_planar_420());
3599 let len = info.try_buffer_len(y_stride, 1080).unwrap();
3600 assert_eq!(len, 3_110_400, "YV12 1920x1080");
3601
3602 let info = PixelFormat::I420.info();
3603 assert_eq!(info.category(), FormatCategory::Planar420);
3604 let len = info.try_buffer_len(y_stride, 1080).unwrap();
3605 assert_eq!(len, 3_110_400, "I420 1920x1080");
3606 }
3607
3608 #[test]
3610 fn test_pixel_format_info_planar_420_odd() {
3611 let y_stride = 1921;
3612
3613 assert!(PixelFormat::YV12
3614 .info()
3615 .try_buffer_len(y_stride, 1081)
3616 .is_err());
3617 assert!(PixelFormat::I420
3618 .info()
3619 .try_buffer_len(y_stride, 1081)
3620 .is_err());
3621 }
3622
3623 #[test]
3625 fn test_pixel_format_info_nv12_even() {
3626 let y_stride = 1920;
3631
3632 let info = PixelFormat::NV12.info();
3633 assert_eq!(info.bytes_per_pixel(), 1);
3634 assert_eq!(info.category(), FormatCategory::SemiPlanar420);
3635 assert!(info.is_planar_420());
3636 let len = info.try_buffer_len(y_stride, 1080).unwrap();
3637 assert_eq!(len, 3_110_400, "NV12 1920x1080");
3638 }
3639
3640 #[test]
3642 fn test_pixel_format_info_nv12_odd() {
3643 let y_stride = 1921;
3644 assert!(PixelFormat::NV12
3645 .info()
3646 .try_buffer_len(y_stride, 1081)
3647 .is_err());
3648 }
3649
3650 #[test]
3652 fn test_pixel_format_line_stride() {
3653 assert_eq!(PixelFormat::BGRA.try_line_stride(1920).unwrap(), 7680);
3655 assert_eq!(PixelFormat::BGRX.try_line_stride(1920).unwrap(), 7680);
3656 assert_eq!(PixelFormat::RGBA.try_line_stride(1920).unwrap(), 7680);
3657 assert_eq!(PixelFormat::RGBX.try_line_stride(1920).unwrap(), 7680);
3658
3659 assert_eq!(PixelFormat::UYVY.try_line_stride(1920).unwrap(), 3840);
3661
3662 assert_eq!(PixelFormat::UYVA.try_line_stride(1920).unwrap(), 5760);
3664
3665 assert_eq!(PixelFormat::P216.try_line_stride(1920).unwrap(), 7680);
3667 assert_eq!(PixelFormat::PA16.try_line_stride(1920).unwrap(), 7680);
3668
3669 assert_eq!(PixelFormat::YV12.try_line_stride(1920).unwrap(), 1920);
3671 assert_eq!(PixelFormat::I420.try_line_stride(1920).unwrap(), 1920);
3672 assert_eq!(PixelFormat::NV12.try_line_stride(1920).unwrap(), 1920);
3673 }
3674
3675 #[test]
3677 fn test_pixel_format_buffer_size() {
3678 assert_eq!(
3680 PixelFormat::BGRA.try_buffer_size(1920, 1080).unwrap(),
3681 8_294_400
3682 );
3683 assert_eq!(
3684 PixelFormat::RGBA.try_buffer_size(1920, 1080).unwrap(),
3685 8_294_400
3686 );
3687
3688 assert_eq!(
3690 PixelFormat::YV12.try_buffer_size(1920, 1080).unwrap(),
3691 3_110_400
3692 );
3693 assert_eq!(
3694 PixelFormat::I420.try_buffer_size(1920, 1080).unwrap(),
3695 3_110_400
3696 );
3697
3698 assert_eq!(
3700 PixelFormat::NV12.try_buffer_size(1920, 1080).unwrap(),
3701 3_110_400
3702 );
3703 }
3704
3705 #[test]
3707 fn test_pixel_format_info_is_planar_420() {
3708 assert!(PixelFormat::YV12.info().is_planar_420());
3709 assert!(PixelFormat::I420.info().is_planar_420());
3710 assert!(PixelFormat::NV12.info().is_planar_420());
3711
3712 assert!(!PixelFormat::BGRA.info().is_planar_420());
3713 assert!(!PixelFormat::RGBA.info().is_planar_420());
3714 assert!(!PixelFormat::UYVY.info().is_planar_420());
3715 assert!(!PixelFormat::UYVA.info().is_planar_420());
3716 }
3717
3718 #[test]
3720 fn test_videoframe_builder_planar_even() {
3721 let frame = VideoFrame::builder()
3722 .resolution(1920, 1080)
3723 .pixel_format(PixelFormat::NV12)
3724 .build()
3725 .expect("Builder should succeed");
3726
3727 assert_eq!(frame.width(), 1920);
3728 assert_eq!(frame.height(), 1080);
3729 assert_eq!(frame.pixel_format(), PixelFormat::NV12);
3730 assert_eq!(frame.data().len(), 3_110_400, "NV12 1920x1080 buffer size");
3731 }
3732
3733 #[test]
3735 fn test_videoframe_builder_planar_odd() {
3736 let result = VideoFrame::builder()
3737 .resolution(1921, 1081)
3738 .pixel_format(PixelFormat::I420)
3739 .build();
3740
3741 assert!(
3742 matches!(result, Err(Error::InvalidFrame(_))),
3743 "Planar 4:2:0 odd dimensions should be rejected"
3744 );
3745 }
3746
3747 #[test]
3749 fn test_videoframe_builder_packed() {
3750 let frame = VideoFrame::builder()
3751 .resolution(1920, 1080)
3752 .pixel_format(PixelFormat::BGRA)
3753 .build()
3754 .expect("Builder should succeed");
3755
3756 assert_eq!(frame.width(), 1920);
3757 assert_eq!(frame.height(), 1080);
3758 assert_eq!(frame.pixel_format(), PixelFormat::BGRA);
3759 assert_eq!(
3760 frame.data().len(),
3761 1920 * 1080 * 4,
3762 "BGRA buffer size unchanged"
3763 );
3764 }
3765
3766 #[test]
3768 fn test_videoframe_from_raw_nv12() {
3769 let width = 1920;
3771 let height = 1080;
3772 let y_stride = 1920;
3773 let expected_size = 3_110_400; let mut data = vec![0u8; expected_size];
3776 data[expected_size - 1] = 0xFF;
3778
3779 let c_frame = NDIlib_video_frame_v2_t {
3780 xres: width,
3781 yres: height,
3782 FourCC: PixelFormat::NV12.into(),
3783 frame_rate_N: 60,
3784 frame_rate_D: 1,
3785 picture_aspect_ratio: 16.0 / 9.0,
3786 frame_format_type: ScanType::Progressive.into(),
3787 timecode: 0,
3788 p_data: data.as_mut_ptr(),
3789 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
3790 line_stride_in_bytes: y_stride,
3791 },
3792 p_metadata: ptr::null(),
3793 timestamp: 0,
3794 };
3795
3796 let frame = unsafe { VideoFrame::from_raw(&c_frame) }.expect("from_raw should succeed");
3797
3798 assert_eq!(frame.width(), width);
3799 assert_eq!(frame.height(), height);
3800 assert_eq!(frame.pixel_format(), PixelFormat::NV12);
3801 assert_eq!(
3802 frame.data().len(),
3803 expected_size,
3804 "Should copy full Y+UV buffer"
3805 );
3806 assert_eq!(
3807 frame.data()[expected_size - 1],
3808 0xFF,
3809 "Last byte should be copied"
3810 );
3811 }
3812
3813 #[test]
3815 fn test_videoframe_from_raw_i420_odd() {
3816 let width = 1921;
3817 let height = 1081;
3818 let y_stride = 1921;
3819 let expected_size = 3_116_403; let mut data = vec![0u8; expected_size];
3822
3823 let c_frame = NDIlib_video_frame_v2_t {
3824 xres: width,
3825 yres: height,
3826 FourCC: PixelFormat::I420.into(),
3827 frame_rate_N: 30,
3828 frame_rate_D: 1,
3829 picture_aspect_ratio: 16.0 / 9.0,
3830 frame_format_type: ScanType::Progressive.into(),
3831 timecode: 0,
3832 p_data: data.as_mut_ptr(),
3833 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
3834 line_stride_in_bytes: y_stride,
3835 },
3836 p_metadata: ptr::null(),
3837 timestamp: 0,
3838 };
3839
3840 let result = unsafe { VideoFrame::from_raw(&c_frame) };
3841 assert!(
3842 matches!(result, Err(Error::InvalidFrame(_))),
3843 "Planar 4:2:0 odd dimensions should be rejected"
3844 );
3845 }
3846
3847 #[test]
3849 fn test_videoframe_from_raw_packed_regression() {
3850 let width = 1920;
3851 let height = 1080;
3852 let stride = 1920 * 4; let expected_size = (stride * height) as usize;
3854
3855 let mut data = vec![0u8; expected_size];
3856
3857 let c_frame = NDIlib_video_frame_v2_t {
3858 xres: width,
3859 yres: height,
3860 FourCC: PixelFormat::BGRA.into(),
3861 frame_rate_N: 60,
3862 frame_rate_D: 1,
3863 picture_aspect_ratio: 16.0 / 9.0,
3864 frame_format_type: ScanType::Progressive.into(),
3865 timecode: 0,
3866 p_data: data.as_mut_ptr(),
3867 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
3868 line_stride_in_bytes: stride,
3869 },
3870 p_metadata: ptr::null(),
3871 timestamp: 0,
3872 };
3873
3874 let frame = unsafe { VideoFrame::from_raw(&c_frame) }.expect("from_raw should succeed");
3875 assert_eq!(
3876 frame.data().len(),
3877 expected_size,
3878 "BGRA buffer size unchanged"
3879 );
3880 }
3881
3882 #[test]
3884 fn test_videoframeref_unknown_fourcc() {
3885 use crate::capture::{Guard, VideoKind};
3886
3887 let width = 1920;
3888 let height = 1080;
3889 let stride = 1920 * 4;
3890 let expected_size = (stride * height) as usize;
3891 let mut data = vec![0u8; expected_size];
3892
3893 #[allow(clippy::unnecessary_cast)]
3896 let c_frame = NDIlib_video_frame_v2_t {
3897 xres: width,
3898 yres: height,
3899 #[cfg(target_os = "windows")]
3900 FourCC: 0xDEADBEEFu32 as i32, #[cfg(not(target_os = "windows"))]
3902 FourCC: 0xDEADBEEF, frame_rate_N: 60,
3904 frame_rate_D: 1,
3905 picture_aspect_ratio: 16.0 / 9.0,
3906 frame_format_type: ScanType::Progressive.into(),
3907 timecode: 0,
3908 p_data: data.as_mut_ptr(),
3909 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
3910 line_stride_in_bytes: stride,
3911 },
3912 p_metadata: ptr::null(),
3913 timestamp: 0,
3914 };
3915
3916 let mock_instance = ptr::null_mut();
3918 let guard = unsafe { Guard::<VideoKind>::new(mock_instance, c_frame) };
3919
3920 let result = unsafe { VideoFrameRef::new(guard) };
3922 assert!(result.is_err(), "Should reject unknown FourCC");
3923
3924 if let Err(Error::InvalidFrame(ref msg)) = result {
3925 assert!(
3926 msg.contains("0xDEADBEEF"),
3927 "Error message should include FourCC: {}",
3928 msg
3929 );
3930 } else {
3931 panic!("Expected InvalidFrame error");
3932 }
3933
3934 std::mem::forget(result);
3936 }
3937
3938 #[test]
3940 fn test_videoframeref_known_fourcc() {
3941 use crate::capture::{Guard, VideoKind};
3942
3943 let width = 1920;
3944 let height = 1080;
3945 let stride = 1920 * 4;
3946 let expected_size = (stride * height) as usize;
3947 let mut data = vec![0u8; expected_size];
3948
3949 let c_frame = NDIlib_video_frame_v2_t {
3950 xres: width,
3951 yres: height,
3952 FourCC: PixelFormat::BGRA.into(),
3953 frame_rate_N: 60,
3954 frame_rate_D: 1,
3955 picture_aspect_ratio: 16.0 / 9.0,
3956 frame_format_type: ScanType::Progressive.into(),
3957 timecode: 0,
3958 p_data: data.as_mut_ptr(),
3959 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
3960 line_stride_in_bytes: stride,
3961 },
3962 p_metadata: ptr::null(),
3963 timestamp: 0,
3964 };
3965
3966 let mock_instance = ptr::null_mut();
3967 let guard = unsafe { Guard::<VideoKind>::new(mock_instance, c_frame) };
3968
3969 let frame_ref = unsafe { VideoFrameRef::new(guard) }.expect("Should accept BGRA FourCC");
3970 assert_eq!(
3971 frame_ref.pixel_format(),
3972 PixelFormat::BGRA,
3973 "Should store validated pixel format"
3974 );
3975
3976 std::mem::forget(frame_ref);
3978 }
3979
3980 #[test]
3982 fn test_audioframeref_unknown_fourcc() {
3983 use crate::recv_guard::RecvAudioGuard;
3984
3985 let num_samples = 1024;
3986 let num_channels = 2;
3987 let sample_count = (num_samples * num_channels) as usize;
3988 let mut data = vec![0.0f32; sample_count];
3989
3990 let c_frame = NDIlib_audio_frame_v3_t {
3992 sample_rate: 48000,
3993 no_channels: num_channels,
3994 no_samples: num_samples,
3995 timecode: 0,
3996 FourCC: 0xBADC0DE, p_data: data.as_mut_ptr() as *mut u8,
3998 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
3999 channel_stride_in_bytes: num_samples * 4,
4000 },
4001 p_metadata: ptr::null(),
4002 timestamp: 0,
4003 };
4004
4005 let mock_instance = ptr::null_mut();
4006 let guard = unsafe { RecvAudioGuard::new(mock_instance, c_frame) };
4007
4008 let result = unsafe { AudioFrameRef::new(guard) };
4009 assert!(result.is_err(), "Should reject unknown audio FourCC");
4010
4011 if let Err(Error::InvalidFrame(ref msg)) = result {
4012 assert!(
4013 msg.contains("0x0BADC0DE"),
4014 "Error message should include FourCC: {}",
4015 msg
4016 );
4017 } else {
4018 panic!("Expected InvalidFrame error");
4019 }
4020
4021 std::mem::forget(result);
4022 }
4023
4024 #[test]
4026 fn test_audioframeref_known_fourcc() {
4027 use crate::recv_guard::RecvAudioGuard;
4028
4029 let num_samples = 1024;
4030 let num_channels = 2;
4031 let sample_count = (num_samples * num_channels) as usize;
4032 let mut data = vec![0.0f32; sample_count];
4033
4034 let c_frame = NDIlib_audio_frame_v3_t {
4035 sample_rate: 48000,
4036 no_channels: num_channels,
4037 no_samples: num_samples,
4038 timecode: 0,
4039 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
4040 p_data: data.as_mut_ptr() as *mut u8,
4041 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
4042 channel_stride_in_bytes: num_samples * 4,
4043 },
4044 p_metadata: ptr::null(),
4045 timestamp: 0,
4046 };
4047
4048 let mock_instance = ptr::null_mut();
4049 let guard = unsafe { RecvAudioGuard::new(mock_instance, c_frame) };
4050
4051 let frame_ref = unsafe { AudioFrameRef::new(guard) }.expect("Should accept FLTP FourCC");
4052 assert_eq!(
4053 frame_ref.format(),
4054 AudioFormat::FLTP,
4055 "Should store validated audio format"
4056 );
4057
4058 std::mem::forget(frame_ref);
4059 }
4060
4061 #[test]
4063 fn test_videoframeref_data_uses_validated_format() {
4064 use crate::capture::{Guard, VideoKind};
4065
4066 let width = 1920;
4068 let height = 1080;
4069 let stride = 1920 * 4;
4070 let expected_size = (stride * height) as usize;
4071 let mut data = vec![0xAB_u8; expected_size];
4072
4073 let c_frame = NDIlib_video_frame_v2_t {
4074 xres: width,
4075 yres: height,
4076 FourCC: PixelFormat::BGRA.into(),
4077 frame_rate_N: 60,
4078 frame_rate_D: 1,
4079 picture_aspect_ratio: 16.0 / 9.0,
4080 frame_format_type: ScanType::Progressive.into(),
4081 timecode: 0,
4082 p_data: data.as_mut_ptr(),
4083 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
4084 line_stride_in_bytes: stride,
4085 },
4086 p_metadata: ptr::null(),
4087 timestamp: 0,
4088 };
4089
4090 let mock_instance = ptr::null_mut();
4091 let guard = unsafe { Guard::<VideoKind>::new(mock_instance, c_frame) };
4092 let frame_ref = unsafe { VideoFrameRef::new(guard) }.expect("Should create frame ref");
4093
4094 assert_eq!(
4096 frame_ref.data().len(),
4097 expected_size,
4098 "data() should use validated pixel format for size calculation"
4099 );
4100
4101 assert_eq!(
4103 frame_ref.line_stride_or_size(),
4104 LineStrideOrSize::LineStrideBytes(stride),
4105 "line_stride_or_size() should use validated format"
4106 );
4107
4108 std::mem::forget(frame_ref);
4109 }
4110
4111 #[test]
4116 fn test_receiver_and_framesync_video_refs_agree() {
4117 use crate::capture::{FrameSyncVideoFree, Guard, VideoKind};
4118
4119 let width = 16;
4120 let height = 8;
4121 let stride = width * 4;
4122 let len = (stride * height) as usize;
4123
4124 let mut recv_data: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
4127 let mut fs_data = recv_data.clone();
4128 let mut recv_meta = b"shared frame\0".to_vec();
4129 let mut fs_meta = recv_meta.clone();
4130
4131 let raw = |data: &mut [u8], meta: &mut [u8]| NDIlib_video_frame_v2_t {
4132 xres: width,
4133 yres: height,
4134 FourCC: PixelFormat::BGRA.into(),
4135 frame_rate_N: 30,
4136 frame_rate_D: 1,
4137 picture_aspect_ratio: 16.0 / 9.0,
4138 frame_format_type: ScanType::Interlaced.into(),
4141 timecode: 123,
4142 p_data: data.as_mut_ptr(),
4143 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
4144 line_stride_in_bytes: stride,
4145 },
4146 p_metadata: meta.as_mut_ptr().cast(),
4147 timestamp: 456,
4148 };
4149
4150 let recv_raw = raw(&mut recv_data, &mut recv_meta);
4151 let fs_raw = raw(&mut fs_data, &mut fs_meta);
4152
4153 let recv_guard = unsafe { Guard::<VideoKind>::new(ptr::null_mut(), recv_raw) };
4154 let recv = unsafe { VideoFrameRef::new(recv_guard) }.expect("receiver video ref");
4155 let fs_guard = unsafe { Guard::<FrameSyncVideoFree>::new(ptr::null_mut(), fs_raw) };
4156 let fs =
4157 unsafe { VideoRef::<FrameSyncVideoFree>::new(fs_guard) }.expect("framesync video ref");
4158
4159 assert_eq!(recv.width(), fs.width());
4160 assert_eq!(recv.height(), fs.height());
4161 assert_eq!(recv.pixel_format(), fs.pixel_format());
4162 assert_eq!(recv.frame_rate_n(), fs.frame_rate_n());
4163 assert_eq!(recv.frame_rate_d(), fs.frame_rate_d());
4164 assert_eq!(recv.picture_aspect_ratio(), fs.picture_aspect_ratio());
4165 assert_eq!(recv.scan_type(), fs.scan_type());
4166 assert_eq!(recv.scan_type(), ScanType::Interlaced);
4167 assert_eq!(recv.timecode(), fs.timecode());
4168 assert_eq!(recv.timestamp(), fs.timestamp());
4169 assert_eq!(recv.line_stride_or_size(), fs.line_stride_or_size());
4170 assert_eq!(recv.metadata(), fs.metadata());
4171 assert_eq!(recv.data(), fs.data());
4172
4173 std::mem::forget(recv);
4176 std::mem::forget(fs);
4177 }
4178
4179 #[test]
4183 fn test_video_ref_rejects_out_of_range_scan_type() {
4184 use crate::capture::{FrameSyncVideoFree, Guard, VideoKind};
4185
4186 let width = 16;
4187 let height = 8;
4188 let stride = width * 4;
4189 let len = (stride * height) as usize;
4190 let mut recv_data = vec![0u8; len];
4191 let mut fs_data = vec![0u8; len];
4192
4193 let raw = |data: &mut [u8]| NDIlib_video_frame_v2_t {
4194 xres: width,
4195 yres: height,
4196 FourCC: PixelFormat::BGRA.into(),
4197 frame_rate_N: 30,
4198 frame_rate_D: 1,
4199 picture_aspect_ratio: 16.0 / 9.0,
4200 frame_format_type: 0x0BAD_F00D, timecode: 0,
4202 p_data: data.as_mut_ptr(),
4203 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
4204 line_stride_in_bytes: stride,
4205 },
4206 p_metadata: ptr::null(),
4207 timestamp: 0,
4208 };
4209
4210 assert!(matches!(
4212 validate_video_layout(&raw(&mut recv_data)),
4213 Err(Error::InvalidFrame(_))
4214 ));
4215
4216 let recv_guard = unsafe { Guard::<VideoKind>::new(ptr::null_mut(), raw(&mut recv_data)) };
4218 assert!(matches!(
4219 unsafe { VideoFrameRef::new(recv_guard) },
4220 Err(Error::InvalidFrame(_))
4221 ));
4222
4223 let fs_guard =
4224 unsafe { Guard::<FrameSyncVideoFree>::new(ptr::null_mut(), raw(&mut fs_data)) };
4225 assert!(matches!(
4226 unsafe { VideoRef::<FrameSyncVideoFree>::new(fs_guard) },
4227 Err(Error::InvalidFrame(_))
4228 ));
4229 }
4230
4231 #[test]
4233 fn test_max_video_bytes_constant() {
4234 assert_eq!(MAX_VIDEO_BYTES, 100 * 1024 * 1024);
4236 }
4237
4238 #[test]
4240 fn test_max_audio_bytes_constant() {
4241 assert_eq!(MAX_AUDIO_BYTES, 64 * 1024 * 1024);
4243 }
4244
4245 #[test]
4247 fn test_max_metadata_bytes_constant() {
4248 assert_eq!(MAX_METADATA_BYTES, 4 * 1024 * 1024);
4249 }
4250
4251 fn metadata_raw_from_bytes(data: &mut [u8], length: i32) -> NDIlib_metadata_frame_t {
4252 NDIlib_metadata_frame_t {
4253 length,
4254 timecode: 12345,
4255 p_data: data.as_mut_ptr().cast::<c_char>(),
4256 }
4257 }
4258
4259 fn null_metadata_raw(length: i32) -> NDIlib_metadata_frame_t {
4260 NDIlib_metadata_frame_t {
4261 length,
4262 timecode: 12345,
4263 p_data: ptr::null_mut(),
4264 }
4265 }
4266
4267 #[test]
4268 fn test_validate_metadata_layout_accepts_empty_null_frame() {
4269 let raw = null_metadata_raw(0);
4270 let layout = validate_metadata_layout(&raw).expect("empty null frame is valid");
4271
4272 assert_eq!(
4273 layout,
4274 ValidatedMetadataLayout {
4275 len_with_nul: 0,
4276 text_len: 0,
4277 }
4278 );
4279 }
4280
4281 #[test]
4282 fn test_validate_metadata_layout_accepts_one_byte_empty_payload() {
4283 let mut data = vec![0u8];
4284 let raw = metadata_raw_from_bytes(&mut data, 1);
4285 let layout = validate_metadata_layout(&raw).expect("explicit empty payload is valid");
4286
4287 assert_eq!(layout.len_with_nul, 1);
4288 assert_eq!(layout.text_len, 0);
4289 }
4290
4291 #[test]
4292 fn test_validate_metadata_layout_accepts_utf8_with_bounded_length() {
4293 let mut data = "<ndi tally=\"preview\"/>".as_bytes().to_vec();
4294 data.push(0);
4295 let length = data.len() as i32;
4296 let raw = metadata_raw_from_bytes(&mut data, length);
4297 let layout = validate_metadata_layout(&raw).expect("valid UTF-8 metadata");
4298
4299 assert_eq!(layout.len_with_nul, data.len());
4300 assert_eq!(layout.text_len, data.len() - 1);
4301 }
4302
4303 #[test]
4304 fn test_validate_metadata_layout_ignores_bytes_after_length() {
4305 let mut data = b"ok\0\0\xFF".to_vec();
4306 let raw = metadata_raw_from_bytes(&mut data, 3);
4307 let layout = validate_metadata_layout(&raw).expect("extra bytes after length ignored");
4308
4309 assert_eq!(layout.text_len, 2);
4310 let owned = unsafe { MetadataFrame::from_raw_validated(&raw, layout) };
4311 assert_eq!(owned.data(), "ok");
4312 }
4313
4314 #[test]
4315 fn test_validate_metadata_layout_rejects_negative_length() {
4316 let raw = null_metadata_raw(-1);
4317
4318 assert!(matches!(
4319 validate_metadata_layout(&raw),
4320 Err(Error::InvalidFrame(_))
4321 ));
4322 }
4323
4324 #[test]
4325 fn test_validate_metadata_layout_rejects_lengthless_non_null_data() {
4326 let mut data = vec![0u8];
4327 let raw = metadata_raw_from_bytes(&mut data, 0);
4328
4329 assert!(matches!(
4330 validate_metadata_layout(&raw),
4331 Err(Error::InvalidFrame(_))
4332 ));
4333 }
4334
4335 #[test]
4336 fn test_validate_metadata_layout_rejects_nonzero_length_null_data() {
4337 let raw = null_metadata_raw(1);
4338
4339 assert!(matches!(
4340 validate_metadata_layout(&raw),
4341 Err(Error::InvalidFrame(_))
4342 ));
4343 }
4344
4345 #[test]
4346 fn test_validate_metadata_layout_rejects_missing_trailing_nul() {
4347 let mut data = b"abc".to_vec();
4348 let length = data.len() as i32;
4349 let raw = metadata_raw_from_bytes(&mut data, length);
4350
4351 assert!(matches!(
4352 validate_metadata_layout(&raw),
4353 Err(Error::InvalidFrame(_))
4354 ));
4355 }
4356
4357 #[test]
4358 fn test_validate_metadata_layout_rejects_interior_nul() {
4359 let mut data = b"a\0b\0".to_vec();
4360 let length = data.len() as i32;
4361 let raw = metadata_raw_from_bytes(&mut data, length);
4362
4363 assert!(matches!(
4364 validate_metadata_layout(&raw),
4365 Err(Error::InvalidFrame(_))
4366 ));
4367 }
4368
4369 #[test]
4370 fn test_validate_metadata_layout_rejects_oversized_length_before_reading() {
4371 let mut data = vec![0u8];
4372 let raw = metadata_raw_from_bytes(&mut data, (MAX_METADATA_BYTES + 1) as i32);
4373
4374 assert!(matches!(
4375 validate_metadata_layout(&raw),
4376 Err(Error::InvalidFrame(_))
4377 ));
4378 }
4379
4380 #[test]
4381 fn test_validate_metadata_layout_rejects_invalid_utf8() {
4382 let mut data = vec![0xFF, 0];
4383 let length = data.len() as i32;
4384 let raw = metadata_raw_from_bytes(&mut data, length);
4385
4386 assert!(matches!(
4387 validate_metadata_layout(&raw),
4388 Err(Error::InvalidUtf8(_))
4389 ));
4390 }
4391
4392 #[test]
4393 fn test_metadata_frame_constructor_and_accessors_preserve_text() {
4394 let frame = MetadataFrame::with_data("<ndi_product/>", 9876).unwrap();
4395
4396 assert_eq!(frame.data(), "<ndi_product/>");
4397 assert_eq!(frame.timecode(), 9876);
4398 assert_eq!(frame.clone().into_data(), "<ndi_product/>");
4399 }
4400
4401 #[test]
4402 fn test_metadata_frame_rejects_interior_nul_input() {
4403 assert!(matches!(
4404 MetadataFrame::with_data("bad\0metadata", 0),
4405 Err(Error::InvalidCString(_))
4406 ));
4407
4408 let mut frame = MetadataFrame::new();
4409 assert!(matches!(
4410 frame.set_data("bad\0metadata"),
4411 Err(Error::InvalidCString(_))
4412 ));
4413 }
4414
4415 #[test]
4416 fn test_metadata_frame_rejects_oversized_input() {
4417 let oversized = "x".repeat(MAX_METADATA_BYTES);
4418
4419 assert!(matches!(
4420 MetadataFrame::with_data(oversized, 0),
4421 Err(Error::InvalidFrame(_))
4422 ));
4423 }
4424
4425 #[test]
4426 fn test_metadata_frame_setters_preserve_invariants() {
4427 let mut frame = MetadataFrame::new();
4428
4429 frame.set_data("updated").unwrap();
4430 frame.set_timecode(42);
4431
4432 assert_eq!(frame.data(), "updated");
4433 assert_eq!(frame.timecode(), 42);
4434 assert_eq!(MetadataFrame::new().with_timecode(7).timecode(), 7);
4435 }
4436
4437 #[test]
4438 fn test_metadata_frame_from_raw_validated_copies_only_payload() {
4439 let mut data = b"copy-me\0\0\xFF".to_vec();
4440 let raw = metadata_raw_from_bytes(&mut data, 8);
4441 let layout = validate_metadata_layout(&raw).unwrap();
4442 let frame = unsafe { MetadataFrame::from_raw_validated(&raw, layout) };
4443
4444 assert_eq!(frame.data(), "copy-me");
4445 assert_eq!(frame.timecode(), 12345);
4446 }
4447
4448 #[test]
4449 fn test_metadata_frame_from_raw_reports_invalid_utf8() {
4450 let mut data = vec![0xFF, 0];
4451 let raw = metadata_raw_from_bytes(&mut data, 2);
4452
4453 assert!(matches!(
4454 unsafe { MetadataFrame::from_raw(&raw) },
4455 Err(Error::InvalidUtf8(_))
4456 ));
4457 }
4458
4459 #[test]
4460 fn test_metadata_frame_to_raw_includes_trailing_nul() {
4461 let frame = MetadataFrame::with_data("abc", 101).unwrap();
4462 let (c_data, raw) = frame.to_raw().unwrap();
4463
4464 assert_eq!(raw.length, 4);
4465 assert_eq!(raw.timecode, 101);
4466 assert_eq!(c_data.as_bytes_with_nul(), b"abc\0");
4467 }
4468
4469 #[test]
4470 fn test_empty_metadata_frame_to_raw_sends_explicit_nul() {
4471 let frame = MetadataFrame::new();
4472 let (c_data, raw) = frame.to_raw().unwrap();
4473
4474 assert_eq!(raw.length, 1);
4475 assert_eq!(c_data.as_bytes_with_nul(), b"\0");
4476 }
4477
4478 #[test]
4479 fn test_metadata_raw_length_conversion_is_checked() {
4480 assert!(metadata_len_to_i32(i32::MAX as usize).is_ok());
4481 assert!(matches!(
4482 metadata_len_to_i32(i32::MAX as usize + 1),
4483 Err(Error::InvalidFrame(_))
4484 ));
4485 }
4486
4487 #[test]
4488 fn test_metadata_frame_ref_uses_cached_layout() {
4489 use crate::capture::RecvMetadataGuard;
4490
4491 let mut data = b"zero-copy\0\0\xFF".to_vec();
4492 let raw = metadata_raw_from_bytes(&mut data, 10);
4493 let guard = unsafe { RecvMetadataGuard::new(ptr::null_mut(), raw) };
4494 let frame_ref = unsafe { MetadataFrameRef::new(guard) }.expect("valid metadata ref");
4495
4496 assert_eq!(frame_ref.data(), "zero-copy");
4497 assert_eq!(frame_ref.as_bytes(), b"zero-copy");
4498 assert_eq!(frame_ref.layout.text_len, 9);
4499
4500 let owned = frame_ref.to_owned();
4501 assert_eq!(owned.data(), "zero-copy");
4502 assert_eq!(owned.timecode(), 12345);
4503
4504 std::mem::forget(frame_ref);
4505 }
4506
4507 #[test]
4508 fn test_validate_frame_metadata_accepts_null_without_allocation_state() {
4509 let layout = unsafe { validate_frame_metadata(ptr::null()) }.expect("null is valid");
4510
4511 assert_eq!(
4512 layout,
4513 ValidatedFrameMetadata {
4514 len_with_nul: None,
4515 text_len: 0,
4516 }
4517 );
4518 }
4519
4520 #[test]
4521 fn test_validate_frame_metadata_accepts_explicit_empty() {
4522 let mut data = b"\0".to_vec();
4523 let layout = unsafe { validate_frame_metadata(data.as_mut_ptr().cast::<c_char>()) }
4524 .expect("explicit empty metadata is valid");
4525
4526 assert_eq!(layout.len_with_nul.unwrap().get(), 1);
4527 assert_eq!(layout.text_len, 0);
4528 assert_eq!(
4529 unsafe { frame_metadata_str(data.as_ptr().cast::<c_char>(), layout) },
4530 Some("")
4531 );
4532 }
4533
4534 #[test]
4535 fn test_validate_frame_metadata_accepts_utf8_and_ignores_after_first_nul() {
4536 let mut data = b"hello metadata\0\0\xFF".to_vec();
4537 let layout = unsafe { validate_frame_metadata(data.as_mut_ptr().cast::<c_char>()) }
4538 .expect("valid UTF-8 metadata");
4539
4540 assert_eq!(layout.text_len, "hello metadata".len());
4541 assert_eq!(
4542 unsafe { frame_metadata_str(data.as_ptr().cast::<c_char>(), layout) },
4543 Some("hello metadata")
4544 );
4545 }
4546
4547 #[test]
4548 fn test_validate_frame_metadata_accepts_max_boundary() {
4549 let mut data = vec![b'x'; MAX_METADATA_BYTES];
4550 data[MAX_METADATA_BYTES - 1] = 0;
4551
4552 let layout = unsafe { validate_frame_metadata(data.as_mut_ptr().cast::<c_char>()) }
4553 .expect("metadata exactly at cap including terminator is valid");
4554
4555 assert_eq!(layout.len_with_nul.unwrap().get(), MAX_METADATA_BYTES);
4556 assert_eq!(layout.text_len, MAX_METADATA_BYTES - 1);
4557 }
4558
4559 #[test]
4560 fn test_validate_frame_metadata_rejects_missing_nul_within_cap() {
4561 let mut data = vec![b'x'; MAX_METADATA_BYTES];
4562
4563 assert!(matches!(
4564 unsafe { validate_frame_metadata(data.as_mut_ptr().cast::<c_char>()) },
4565 Err(Error::InvalidFrame(_))
4566 ));
4567 }
4568
4569 #[test]
4570 fn test_validate_frame_metadata_rejects_invalid_utf8_before_nul() {
4571 let mut data = vec![0xFF, 0];
4572
4573 assert!(matches!(
4574 unsafe { validate_frame_metadata(data.as_mut_ptr().cast::<c_char>()) },
4575 Err(Error::InvalidUtf8(_))
4576 ));
4577 }
4578
4579 #[test]
4580 fn test_video_frame_ref_metadata_is_cached_and_owned_conversion_appends_nul() {
4581 use crate::capture::{Guard, VideoKind};
4582
4583 let width = 16;
4584 let height = 8;
4585 let stride = width * 4;
4586 let expected_len = (stride * height) as usize;
4587 let data = vec![0u8; expected_len];
4588 let mut metadata = b"cached\0\xFF".to_vec();
4589
4590 let raw = NDIlib_video_frame_v2_t {
4591 xres: width,
4592 yres: height,
4593 FourCC: PixelFormat::BGRA.into(),
4594 frame_rate_N: 60,
4595 frame_rate_D: 1,
4596 picture_aspect_ratio: 16.0 / 9.0,
4597 frame_format_type: ScanType::Progressive.into(),
4598 timecode: 123,
4599 p_data: data.as_ptr() as *mut u8,
4600 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
4601 line_stride_in_bytes: stride,
4602 },
4603 p_metadata: metadata.as_mut_ptr().cast::<c_char>(),
4604 timestamp: 456,
4605 };
4606
4607 let guard = unsafe { Guard::<VideoKind>::new(ptr::null_mut(), raw) };
4608 let frame_ref = unsafe { VideoFrameRef::new(guard) }.expect("valid video ref");
4609
4610 assert_eq!(frame_ref.metadata(), Some("cached"));
4611 metadata[6] = b'!';
4612 assert_eq!(frame_ref.metadata(), Some("cached"));
4613 assert!(format!("{frame_ref:?}").contains("metadata: Some(\"cached\")"));
4614
4615 let owned = frame_ref.to_owned().expect("owned conversion");
4616 assert_eq!(owned.metadata(), Some("cached"));
4617 assert_eq!(owned.data().len(), expected_len);
4618
4619 std::mem::forget(frame_ref);
4620 }
4621
4622 #[test]
4623 fn test_audio_frame_ref_metadata_is_text_and_owned_conversion_preserves_it() {
4624 use crate::capture::RecvAudioGuard;
4625
4626 let no_samples = 4;
4627 let no_channels = 2;
4628 let sample_count = (no_samples * no_channels) as usize;
4629 let data = vec![0.25f32; sample_count];
4630 let mut metadata = b"audio meta\0".to_vec();
4631
4632 let raw = NDIlib_audio_frame_v3_t {
4633 sample_rate: 48000,
4634 no_channels,
4635 no_samples,
4636 timecode: 123,
4637 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
4638 p_data: data.as_ptr() as *mut u8,
4639 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
4640 channel_stride_in_bytes: no_samples * 4,
4641 },
4642 p_metadata: metadata.as_mut_ptr().cast::<c_char>(),
4643 timestamp: 456,
4644 };
4645
4646 let guard = unsafe { RecvAudioGuard::new(ptr::null_mut(), raw) };
4647 let frame_ref = unsafe { AudioFrameRef::new(guard) }.expect("valid audio ref");
4648
4649 assert_eq!(frame_ref.metadata(), Some("audio meta"));
4650 let owned = frame_ref.to_owned().expect("owned conversion");
4651 assert_eq!(owned.metadata(), Some("audio meta"));
4652 assert_eq!(owned.data().len(), sample_count);
4653
4654 std::mem::forget(frame_ref);
4655 }
4656
4657 #[test]
4658 fn test_owned_video_from_raw_rejects_malformed_frame_metadata() {
4659 let width = 16;
4660 let height = 8;
4661 let stride = width * 4;
4662 let expected_len = (stride * height) as usize;
4663 let mut data = vec![0u8; expected_len];
4664 let mut metadata = vec![b'x'; MAX_METADATA_BYTES];
4665
4666 let raw = NDIlib_video_frame_v2_t {
4667 xres: width,
4668 yres: height,
4669 FourCC: PixelFormat::BGRA.into(),
4670 frame_rate_N: 60,
4671 frame_rate_D: 1,
4672 picture_aspect_ratio: 16.0 / 9.0,
4673 frame_format_type: ScanType::Progressive.into(),
4674 timecode: 0,
4675 p_data: data.as_mut_ptr(),
4676 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
4677 line_stride_in_bytes: stride,
4678 },
4679 p_metadata: metadata.as_mut_ptr().cast::<c_char>(),
4680 timestamp: 0,
4681 };
4682
4683 assert!(matches!(
4684 unsafe { VideoFrame::from_raw(&raw) },
4685 Err(Error::InvalidFrame(_))
4686 ));
4687
4688 metadata[0] = 0xFF;
4689 metadata[1] = 0;
4690 assert!(matches!(
4691 unsafe { VideoFrame::from_raw(&raw) },
4692 Err(Error::InvalidUtf8(_))
4693 ));
4694 }
4695
4696 #[test]
4697 fn test_owned_frame_metadata_builders_setters_and_raw_conversion() {
4698 let mut video = VideoFrame::builder().metadata("").build().unwrap();
4699 assert_eq!(video.metadata(), Some(""));
4700 let raw = video.to_raw();
4701 assert!(!raw.p_metadata.is_null());
4702 assert_eq!(
4703 unsafe { slice::from_raw_parts(raw.p_metadata.cast::<u8>(), 1) },
4704 b"\0"
4705 );
4706
4707 video.set_metadata(Some("video meta")).unwrap();
4708 assert_eq!(video.metadata(), Some("video meta"));
4709 let raw = video.to_raw();
4710 assert_eq!(
4711 unsafe { slice::from_raw_parts(raw.p_metadata.cast::<u8>(), 11) },
4712 b"video meta\0"
4713 );
4714
4715 video.set_metadata(Option::<String>::None).unwrap();
4716 assert!(video.metadata().is_none());
4717 assert!(video.to_raw().p_metadata.is_null());
4718
4719 let mut audio = AudioFrame::builder().metadata("").build().unwrap();
4720 assert_eq!(audio.metadata(), Some(""));
4721 assert!(!audio.to_raw().p_metadata.is_null());
4722 audio.set_metadata(Some("audio meta")).unwrap();
4723 assert_eq!(audio.metadata(), Some("audio meta"));
4724
4725 assert!(matches!(
4726 VideoFrame::builder().metadata("bad\0metadata").build(),
4727 Err(Error::InvalidCString(_))
4728 ));
4729 assert!(matches!(
4730 AudioFrame::builder().metadata("bad\0metadata").build(),
4731 Err(Error::InvalidCString(_))
4732 ));
4733 assert!(matches!(
4734 video.set_metadata(Some("bad\0metadata")),
4735 Err(Error::InvalidCString(_))
4736 ));
4737 assert!(matches!(
4738 audio.set_metadata(Some("bad\0metadata")),
4739 Err(Error::InvalidCString(_))
4740 ));
4741
4742 let oversized = "x".repeat(MAX_METADATA_BYTES);
4743 assert!(matches!(
4744 VideoFrame::builder().metadata(oversized.clone()).build(),
4745 Err(Error::InvalidFrame(_))
4746 ));
4747 assert!(matches!(
4748 AudioFrame::builder().metadata(oversized.clone()).build(),
4749 Err(Error::InvalidFrame(_))
4750 ));
4751 assert!(matches!(
4752 video.set_metadata(Some(oversized.clone())),
4753 Err(Error::InvalidFrame(_))
4754 ));
4755 assert!(matches!(
4756 audio.set_metadata(Some(oversized)),
4757 Err(Error::InvalidFrame(_))
4758 ));
4759 }
4760
4761 #[test]
4763 fn test_audio_overflow_checked_mul() {
4764 let no_samples = 1024;
4766 let no_channels = i32::MAX;
4767 let mut dummy_data = vec![0f32; 1024]; let raw = NDIlib_audio_frame_v3_t {
4770 sample_rate: 48000,
4771 no_channels,
4772 no_samples,
4773 timecode: 0,
4774 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
4775 p_data: dummy_data.as_mut_ptr() as *mut u8,
4776 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
4777 channel_stride_in_bytes: no_samples * 4,
4778 },
4779 p_metadata: ptr::null(),
4780 timestamp: 0,
4781 };
4782
4783 let result = AudioFrame::from_raw(raw);
4784 assert!(
4785 result.is_err(),
4786 "Should reject audio frame with sample count overflow or exceeding size limit"
4787 );
4788
4789 if let Err(Error::InvalidFrame(msg)) = result {
4790 assert!(
4792 msg.contains("overflow") || msg.contains("exceeds maximum size"),
4793 "Error message should mention overflow or size limit, got: {msg}"
4794 );
4795 } else {
4796 panic!("Expected InvalidFrame error");
4797 }
4798 }
4799
4800 #[test]
4802 fn test_audio_within_bounds_succeeds() {
4803 let sample_rate = 48000;
4805 let no_channels = 2;
4806 let no_samples = 1024;
4807 let sample_count = (no_samples * no_channels) as usize;
4808 let mut data = vec![0.5f32; sample_count];
4809
4810 let raw = NDIlib_audio_frame_v3_t {
4811 sample_rate,
4812 no_channels,
4813 no_samples,
4814 timecode: 12345,
4815 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
4816 p_data: data.as_mut_ptr() as *mut u8,
4817 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
4818 channel_stride_in_bytes: no_samples * 4,
4819 },
4820 p_metadata: ptr::null(),
4821 timestamp: 67890,
4822 };
4823
4824 let result = AudioFrame::from_raw(raw);
4825 assert!(
4826 result.is_ok(),
4827 "Should accept normal audio frame within bounds"
4828 );
4829
4830 let frame = result.unwrap();
4831 assert_eq!(frame.data().len(), sample_count);
4832 assert_eq!(frame.num_samples(), no_samples);
4833 assert_eq!(frame.num_channels(), no_channels);
4834 }
4835
4836 #[test]
4837 fn test_audio_builder_rejects_invalid_dimensions() {
4838 assert!(matches!(
4839 AudioFrame::builder().sample_rate(0).build(),
4840 Err(Error::InvalidFrame(_))
4841 ));
4842 assert!(matches!(
4843 AudioFrame::builder().channels(0).build(),
4844 Err(Error::InvalidFrame(_))
4845 ));
4846 assert!(matches!(
4847 AudioFrame::builder().samples(0).build(),
4848 Err(Error::InvalidFrame(_))
4849 ));
4850 assert!(matches!(
4851 AudioFrame::builder().samples(-1).build(),
4852 Err(Error::InvalidFrame(_))
4853 ));
4854 }
4855
4856 #[test]
4857 fn test_audio_builder_rejects_oversized_layout() {
4858 let samples = (MAX_AUDIO_BYTES / std::mem::size_of::<f32>()) as i32 + 1;
4859 let result = AudioFrame::builder().channels(1).samples(samples).build();
4860
4861 assert!(matches!(result, Err(Error::InvalidFrame(_))));
4862 }
4863
4864 #[test]
4865 fn test_video_builder_rejects_invalid_send_metadata() {
4866 assert!(matches!(
4867 VideoFrame::builder().frame_rate(0, 1).build(),
4868 Err(Error::InvalidFrame(_))
4869 ));
4870 assert!(matches!(
4871 VideoFrame::builder().frame_rate(30, 0).build(),
4872 Err(Error::InvalidFrame(_))
4873 ));
4874 assert!(matches!(
4875 VideoFrame::builder().aspect_ratio(f32::NAN).build(),
4876 Err(Error::InvalidFrame(_))
4877 ));
4878 assert!(matches!(
4879 VideoFrame::builder().aspect_ratio(0.0).build(),
4880 Err(Error::InvalidFrame(_))
4881 ));
4882 }
4883
4884 #[test]
4885 fn test_owned_frame_data_replacement_preserves_layout_size() {
4886 let mut video = VideoFrame::builder()
4887 .resolution(16, 16)
4888 .pixel_format(PixelFormat::BGRA)
4889 .build()
4890 .unwrap();
4891 assert!(video.replace_data(vec![0; video.data().len() - 1]).is_err());
4892
4893 let mut audio = AudioFrame::builder()
4894 .channels(2)
4895 .samples(16)
4896 .build()
4897 .unwrap();
4898 assert!(audio
4899 .replace_data(vec![0.0; audio.data().len() + 1])
4900 .is_err());
4901 }
4902
4903 #[test]
4905 fn test_video_uncompressed_uses_constant_cap() {
4906 let width = 7680;
4910 let height = 4320;
4911 let stride = width * 4;
4912 let expected_size = (stride * height) as usize;
4913 let mut data = vec![0u8; expected_size];
4914
4915 let c_frame = NDIlib_video_frame_v2_t {
4916 xres: width,
4917 yres: height,
4918 FourCC: PixelFormat::BGRA.into(),
4919 frame_rate_N: 60,
4920 frame_rate_D: 1,
4921 picture_aspect_ratio: 16.0 / 9.0,
4922 frame_format_type: ScanType::Progressive.into(),
4923 timecode: 0,
4924 p_data: data.as_mut_ptr(),
4925 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
4926 line_stride_in_bytes: stride,
4927 },
4928 p_metadata: ptr::null(),
4929 timestamp: 0,
4930 };
4931
4932 let result = unsafe { VideoFrame::from_raw(&c_frame) };
4933 assert!(
4934 result.is_err(),
4935 "Should reject uncompressed video frame exceeding MAX_VIDEO_BYTES"
4936 );
4937
4938 if let Err(Error::InvalidFrame(msg)) = result {
4939 assert!(
4940 msg.contains("exceeds maximum size"),
4941 "Error message should mention size limit, got: {msg}"
4942 );
4943 } else {
4944 panic!("Expected InvalidFrame error");
4945 }
4946 }
4947
4948 #[test]
4954 fn test_validate_video_layout_valid_uncompressed() {
4955 let width = 1920;
4956 let height = 1080;
4957 let stride = width * 4; let expected_size = (stride * height) as usize;
4959 let mut data = vec![0u8; expected_size];
4960
4961 let raw = NDIlib_video_frame_v2_t {
4962 xres: width,
4963 yres: height,
4964 FourCC: PixelFormat::BGRA.into(),
4965 frame_rate_N: 60,
4966 frame_rate_D: 1,
4967 picture_aspect_ratio: 16.0 / 9.0,
4968 frame_format_type: ScanType::Progressive.into(),
4969 timecode: 0,
4970 p_data: data.as_mut_ptr(),
4971 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
4972 line_stride_in_bytes: stride,
4973 },
4974 p_metadata: ptr::null(),
4975 timestamp: 0,
4976 };
4977
4978 let result = validate_video_layout(&raw);
4979 assert!(result.is_ok(), "Should validate valid uncompressed frame");
4980
4981 let layout = result.unwrap();
4982 assert_eq!(layout.pixel_format, PixelFormat::BGRA);
4983 assert_eq!(layout.data_len_bytes, expected_size);
4984 assert_eq!(
4985 layout.line_stride_or_size,
4986 LineStrideOrSize::LineStrideBytes(stride)
4987 );
4988 }
4989
4990 #[test]
4992 fn test_validate_video_layout_null_pointer() {
4993 let raw = NDIlib_video_frame_v2_t {
4994 xres: 1920,
4995 yres: 1080,
4996 FourCC: PixelFormat::BGRA.into(),
4997 frame_rate_N: 60,
4998 frame_rate_D: 1,
4999 picture_aspect_ratio: 16.0 / 9.0,
5000 frame_format_type: ScanType::Progressive.into(),
5001 timecode: 0,
5002 p_data: ptr::null_mut(),
5003 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
5004 line_stride_in_bytes: 7680,
5005 },
5006 p_metadata: ptr::null(),
5007 timestamp: 0,
5008 };
5009
5010 let result = validate_video_layout(&raw);
5011 assert!(result.is_err(), "Should reject null data pointer");
5012
5013 if let Err(Error::InvalidFrame(msg)) = result {
5014 assert!(
5015 msg.contains("null data pointer"),
5016 "Error should mention null pointer, got: {msg}"
5017 );
5018 } else {
5019 panic!("Expected InvalidFrame error");
5020 }
5021 }
5022
5023 #[test]
5025 fn test_validate_video_layout_invalid_stride() {
5026 let mut data = vec![0u8; 1024];
5027
5028 let raw = NDIlib_video_frame_v2_t {
5029 xres: 1920,
5030 yres: 1080,
5031 FourCC: PixelFormat::BGRA.into(),
5032 frame_rate_N: 60,
5033 frame_rate_D: 1,
5034 picture_aspect_ratio: 16.0 / 9.0,
5035 frame_format_type: ScanType::Progressive.into(),
5036 timecode: 0,
5037 p_data: data.as_mut_ptr(),
5038 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
5039 line_stride_in_bytes: 0, },
5041 p_metadata: ptr::null(),
5042 timestamp: 0,
5043 };
5044
5045 let result = validate_video_layout(&raw);
5046 assert!(result.is_err(), "Should reject zero line_stride");
5047
5048 if let Err(Error::InvalidFrame(msg)) = result {
5049 assert!(
5050 msg.contains("invalid line_stride_in_bytes"),
5051 "Error should mention invalid stride, got: {msg}"
5052 );
5053 } else {
5054 panic!("Expected InvalidFrame error");
5055 }
5056 }
5057
5058 #[test]
5060 fn test_validate_video_layout_rejects_short_stride() {
5061 let mut data = vec![0u8; 1024];
5062
5063 let raw = NDIlib_video_frame_v2_t {
5064 xres: 1920,
5065 yres: 1080,
5066 FourCC: PixelFormat::BGRA.into(),
5067 frame_rate_N: 60,
5068 frame_rate_D: 1,
5069 picture_aspect_ratio: 16.0 / 9.0,
5070 frame_format_type: ScanType::Progressive.into(),
5071 timecode: 0,
5072 p_data: data.as_mut_ptr(),
5073 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
5074 line_stride_in_bytes: 1919 * 4,
5075 },
5076 p_metadata: ptr::null(),
5077 timestamp: 0,
5078 };
5079
5080 let result = validate_video_layout(&raw);
5081 assert!(matches!(result, Err(Error::InvalidFrame(_))));
5082 }
5083
5084 #[test]
5086 fn test_validate_video_layout_rejects_planar_odd_dimensions() {
5087 let mut data = vec![0u8; 4096];
5088
5089 let raw = NDIlib_video_frame_v2_t {
5090 xres: 641,
5091 yres: 480,
5092 FourCC: PixelFormat::I420.into(),
5093 frame_rate_N: 60,
5094 frame_rate_D: 1,
5095 picture_aspect_ratio: 4.0 / 3.0,
5096 frame_format_type: ScanType::Progressive.into(),
5097 timecode: 0,
5098 p_data: data.as_mut_ptr(),
5099 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
5100 line_stride_in_bytes: 642,
5101 },
5102 p_metadata: ptr::null(),
5103 timestamp: 0,
5104 };
5105
5106 let result = validate_video_layout(&raw);
5107 assert!(matches!(result, Err(Error::InvalidFrame(_))));
5108 }
5109
5110 #[test]
5112 fn test_validate_video_layout_negative_dimensions() {
5113 let mut data = vec![0u8; 1024];
5114
5115 let raw = NDIlib_video_frame_v2_t {
5116 xres: 1920,
5117 yres: -1, FourCC: PixelFormat::BGRA.into(),
5119 frame_rate_N: 60,
5120 frame_rate_D: 1,
5121 picture_aspect_ratio: 16.0 / 9.0,
5122 frame_format_type: ScanType::Progressive.into(),
5123 timecode: 0,
5124 p_data: data.as_mut_ptr(),
5125 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
5126 line_stride_in_bytes: 7680,
5127 },
5128 p_metadata: ptr::null(),
5129 timestamp: 0,
5130 };
5131
5132 let result = validate_video_layout(&raw);
5133 assert!(result.is_err(), "Should reject negative height");
5134
5135 if let Err(Error::InvalidFrame(msg)) = result {
5136 assert!(
5137 msg.contains("invalid height"),
5138 "Error should mention invalid height, got: {msg}"
5139 );
5140 } else {
5141 panic!("Expected InvalidFrame error");
5142 }
5143 }
5144
5145 #[test]
5147 fn test_validate_video_layout_exceeds_max() {
5148 let width = 7680;
5151 let height = 4320;
5152 let stride = width * 4;
5153 let expected_size = (stride * height) as usize;
5154 let mut data = vec![0u8; expected_size];
5155
5156 let raw = NDIlib_video_frame_v2_t {
5157 xres: width,
5158 yres: height,
5159 FourCC: PixelFormat::BGRA.into(),
5160 frame_rate_N: 60,
5161 frame_rate_D: 1,
5162 picture_aspect_ratio: 16.0 / 9.0,
5163 frame_format_type: ScanType::Progressive.into(),
5164 timecode: 0,
5165 p_data: data.as_mut_ptr(),
5166 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
5167 line_stride_in_bytes: stride,
5168 },
5169 p_metadata: ptr::null(),
5170 timestamp: 0,
5171 };
5172
5173 let result = validate_video_layout(&raw);
5174 assert!(result.is_err(), "Should reject oversized frame");
5175
5176 if let Err(Error::InvalidFrame(msg)) = result {
5177 assert!(
5178 msg.contains("exceeds maximum size"),
5179 "Error should mention size limit, got: {msg}"
5180 );
5181 } else {
5182 panic!("Expected InvalidFrame error");
5183 }
5184 }
5185
5186 #[test]
5188 fn test_validate_audio_layout_valid() {
5189 let no_samples = 1024;
5190 let no_channels = 2;
5191 let sample_count = (no_samples * no_channels) as usize;
5192 let mut data = vec![0.0f32; sample_count];
5193
5194 let raw = NDIlib_audio_frame_v3_t {
5195 sample_rate: 48000,
5196 no_channels,
5197 no_samples,
5198 timecode: 0,
5199 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
5200 p_data: data.as_mut_ptr() as *mut u8,
5201 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
5202 channel_stride_in_bytes: no_samples * 4,
5203 },
5204 p_metadata: ptr::null(),
5205 timestamp: 0,
5206 };
5207
5208 let result = validate_audio_layout(&raw);
5209 assert!(result.is_ok(), "Should validate valid audio frame");
5210
5211 let layout = result.unwrap();
5212 assert_eq!(layout.format, Some(AudioFormat::FLTP));
5213 assert_eq!(layout.sample_count, sample_count);
5214 }
5215
5216 #[test]
5218 fn test_validate_audio_layout_strided_planar() {
5219 let no_samples = 4;
5220 let no_channels = 2;
5221 let stride_samples = 6;
5222 let backing_samples = (stride_samples + no_samples) as usize;
5223 let mut data = vec![0.0f32; backing_samples];
5224
5225 let raw = NDIlib_audio_frame_v3_t {
5226 sample_rate: 48000,
5227 no_channels,
5228 no_samples,
5229 timecode: 0,
5230 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
5231 p_data: data.as_mut_ptr() as *mut u8,
5232 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
5233 channel_stride_in_bytes: stride_samples * 4,
5234 },
5235 p_metadata: ptr::null(),
5236 timestamp: 0,
5237 };
5238
5239 let layout = validate_audio_layout(&raw).expect("strided FLTP should validate");
5240 assert_eq!(layout.sample_count, backing_samples);
5241 assert_eq!(layout.channel_stride_samples, stride_samples as usize);
5242 assert_eq!(layout.channel_range(1), Some(6..10));
5243 }
5244
5245 #[test]
5247 fn test_validate_audio_layout_empty_query_no_source() {
5248 let raw = NDIlib_audio_frame_v3_t::default();
5249
5250 let layout =
5251 validate_audio_layout_allow_empty(&raw).expect("all-zero query state should validate");
5252 assert!(layout.is_empty());
5253 assert_eq!(layout.format(), None);
5254 assert_eq!(layout.sample_count, 0);
5255 }
5256
5257 #[test]
5259 fn test_validate_audio_layout_empty_query_with_source_format() {
5260 let raw = NDIlib_audio_frame_v3_t {
5261 sample_rate: 48000,
5262 no_channels: 2,
5263 no_samples: 0,
5264 timecode: 0,
5265 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
5266 p_data: ptr::null_mut(),
5267 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
5268 channel_stride_in_bytes: 0,
5269 },
5270 p_metadata: ptr::null(),
5271 timestamp: 0,
5272 };
5273
5274 let layout = validate_audio_layout_allow_empty(&raw)
5275 .expect("query source format should validate without samples");
5276 assert!(layout.is_empty());
5277 assert_eq!(layout.format(), Some(AudioFormat::FLTP));
5278 assert_eq!(layout.sample_rate, 48000);
5279 assert_eq!(layout.no_channels, 2);
5280 }
5281
5282 #[test]
5284 fn test_validate_audio_layout_rejects_partial_empty_query() {
5285 let raw = NDIlib_audio_frame_v3_t {
5286 sample_rate: 48000,
5287 no_channels: 0,
5288 no_samples: 0,
5289 timecode: 0,
5290 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
5291 p_data: ptr::null_mut(),
5292 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
5293 channel_stride_in_bytes: 0,
5294 },
5295 p_metadata: ptr::null(),
5296 timestamp: 0,
5297 };
5298
5299 let result = validate_audio_layout_allow_empty(&raw);
5300 assert!(matches!(result, Err(Error::InvalidFrame(_))));
5301 }
5302
5303 #[test]
5305 fn test_validate_audio_layout_rejects_invalid_channel_stride() {
5306 let mut data = vec![0.0f32; 2048];
5307
5308 let raw = NDIlib_audio_frame_v3_t {
5309 sample_rate: 48000,
5310 no_channels: 2,
5311 no_samples: 1024,
5312 timecode: 0,
5313 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
5314 p_data: data.as_mut_ptr() as *mut u8,
5315 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
5316 channel_stride_in_bytes: 1024 * 4 - 4,
5317 },
5318 p_metadata: ptr::null(),
5319 timestamp: 0,
5320 };
5321
5322 let result = validate_audio_layout(&raw);
5323 assert!(matches!(result, Err(Error::InvalidFrame(_))));
5324 }
5325
5326 #[test]
5328 fn test_validate_audio_layout_null_pointer() {
5329 let raw = NDIlib_audio_frame_v3_t {
5330 sample_rate: 48000,
5331 no_channels: 2,
5332 no_samples: 1024,
5333 timecode: 0,
5334 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
5335 p_data: ptr::null_mut(),
5336 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
5337 channel_stride_in_bytes: 1024 * 4,
5338 },
5339 p_metadata: ptr::null(),
5340 timestamp: 0,
5341 };
5342
5343 let result = validate_audio_layout(&raw);
5344 assert!(result.is_err(), "Should reject null data pointer");
5345
5346 if let Err(Error::InvalidFrame(msg)) = result {
5347 assert!(
5348 msg.contains("null data pointer"),
5349 "Error should mention null pointer, got: {msg}"
5350 );
5351 } else {
5352 panic!("Expected InvalidFrame error");
5353 }
5354 }
5355
5356 #[test]
5358 fn test_validate_audio_layout_negative_samples() {
5359 let mut data = vec![0.0f32; 1024];
5360
5361 let raw = NDIlib_audio_frame_v3_t {
5362 sample_rate: 48000,
5363 no_channels: 2,
5364 no_samples: -1, timecode: 0,
5366 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
5367 p_data: data.as_mut_ptr() as *mut u8,
5368 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
5369 channel_stride_in_bytes: 1024 * 4,
5370 },
5371 p_metadata: ptr::null(),
5372 timestamp: 0,
5373 };
5374
5375 let result = validate_audio_layout(&raw);
5376 assert!(result.is_err(), "Should reject negative sample count");
5377
5378 if let Err(Error::InvalidFrame(msg)) = result {
5379 assert!(
5380 msg.contains("Invalid number of samples"),
5381 "Error should mention invalid samples, got: {msg}"
5382 );
5383 } else {
5384 panic!("Expected InvalidFrame error");
5385 }
5386 }
5387
5388 #[test]
5390 fn test_validate_audio_layout_overflow() {
5391 let mut data = vec![0.0f32; 1024];
5392
5393 let raw = NDIlib_audio_frame_v3_t {
5394 sample_rate: 48000,
5395 no_channels: i32::MAX,
5396 no_samples: 1024,
5397 timecode: 0,
5398 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
5399 p_data: data.as_mut_ptr() as *mut u8,
5400 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
5401 channel_stride_in_bytes: 1024 * 4,
5402 },
5403 p_metadata: ptr::null(),
5404 timestamp: 0,
5405 };
5406
5407 let result = validate_audio_layout(&raw);
5408 assert!(
5409 result.is_err(),
5410 "Should reject audio frame with overflow potential"
5411 );
5412
5413 if let Err(Error::InvalidFrame(msg)) = result {
5414 assert!(
5415 msg.contains("overflow") || msg.contains("exceeds maximum size"),
5416 "Error should mention overflow or size limit, got: {msg}"
5417 );
5418 } else {
5419 panic!("Expected InvalidFrame error");
5420 }
5421 }
5422
5423 #[test]
5425 fn test_calculate_buffer_len_checked_matches_public_helper() {
5426 let test_cases = [
5427 (PixelFormat::BGRA, 7680usize, 1080usize),
5428 (PixelFormat::UYVY, 3840usize, 1080usize),
5429 (PixelFormat::NV12, 1920usize, 1080usize),
5430 (PixelFormat::YV12, 1920usize, 1080usize),
5431 (PixelFormat::I420, 1920usize, 1080usize),
5432 ];
5433
5434 for (format, stride, height) in test_cases {
5435 let expected = format
5436 .info()
5437 .try_buffer_len(stride as i32, height as i32)
5438 .unwrap();
5439 let result = calculate_buffer_len_checked(format, stride, height);
5440
5441 assert!(
5442 result.is_ok(),
5443 "Should succeed for valid inputs: {:?}",
5444 format
5445 );
5446 assert_eq!(
5447 result.unwrap(),
5448 expected,
5449 "Checked calculation should match unchecked for {:?}",
5450 format
5451 );
5452 }
5453 }
5454
5455 #[test]
5457 fn test_video_frame_ref_uses_cached_length() {
5458 use crate::capture::{Guard, VideoKind};
5459
5460 let width = 1920;
5462 let height = 1080;
5463 let stride = width * 4;
5464 let expected_size = (stride * height) as usize;
5465 let data = vec![0u8; expected_size];
5466
5467 let raw = NDIlib_video_frame_v2_t {
5468 xres: width,
5469 yres: height,
5470 FourCC: PixelFormat::BGRA.into(),
5471 frame_rate_N: 60,
5472 frame_rate_D: 1,
5473 picture_aspect_ratio: 16.0 / 9.0,
5474 frame_format_type: ScanType::Progressive.into(),
5475 timecode: 12345,
5476 p_data: data.as_ptr() as *mut u8,
5477 __bindgen_anon_1: NDIlib_video_frame_v2_t__bindgen_ty_1 {
5478 line_stride_in_bytes: stride,
5479 },
5480 p_metadata: ptr::null(),
5481 timestamp: 67890,
5482 };
5483
5484 let guard = unsafe { Guard::<VideoKind>::new(ptr::null_mut(), raw) };
5487
5488 let frame_ref = unsafe { VideoFrameRef::new(guard) };
5490 assert!(frame_ref.is_ok(), "Should create valid VideoFrameRef");
5491
5492 let frame_ref = frame_ref.unwrap();
5493
5494 assert_eq!(
5496 frame_ref.data().len(),
5497 expected_size,
5498 "data() should return slice with cached length"
5499 );
5500 assert_eq!(
5501 frame_ref.layout.data_len_bytes, expected_size,
5502 "Cached data_len_bytes should match expected"
5503 );
5504
5505 std::mem::forget(frame_ref);
5507 }
5508
5509 #[test]
5511 fn test_audio_frame_ref_uses_cached_sample_count() {
5512 use crate::capture::RecvAudioGuard;
5513
5514 let no_samples = 1024;
5515 let no_channels = 2;
5516 let sample_count = (no_samples * no_channels) as usize;
5517 let data = vec![0.5f32; sample_count];
5518
5519 let raw = NDIlib_audio_frame_v3_t {
5520 sample_rate: 48000,
5521 no_channels,
5522 no_samples,
5523 timecode: 12345,
5524 FourCC: NDIlib_FourCC_audio_type_e_NDIlib_FourCC_audio_type_FLTP,
5525 p_data: data.as_ptr() as *mut u8,
5526 __bindgen_anon_1: NDIlib_audio_frame_v3_t__bindgen_ty_1 {
5527 channel_stride_in_bytes: no_samples * 4,
5528 },
5529 p_metadata: ptr::null(),
5530 timestamp: 67890,
5531 };
5532
5533 let guard = unsafe { RecvAudioGuard::new(ptr::null_mut(), raw) };
5535
5536 let frame_ref = unsafe { AudioFrameRef::new(guard) };
5538 assert!(frame_ref.is_ok(), "Should create valid AudioFrameRef");
5539
5540 let frame_ref = frame_ref.unwrap();
5541
5542 assert_eq!(
5544 frame_ref.data().len(),
5545 sample_count,
5546 "data() should return slice with cached sample count"
5547 );
5548 assert_eq!(
5549 frame_ref.layout.sample_count, sample_count,
5550 "Cached sample_count should match expected"
5551 );
5552
5553 std::mem::forget(frame_ref);
5555 }
5556}