1use alloc::string::String;
42use alloc::vec::Vec;
43
44use crate::caps::{
45 AudioFormat, ByteStreamEncoding, Caps, ClosedCaptionFormat, Dim, Interlace, Rate,
46 RawVideoFormat, SubPictureFormat, TensorDType, TensorLayout, TensorShape, TextFormat,
47 VideoCodec,
48};
49use crate::frame::{Frame, FrameTiming, PipelinePacket};
50use crate::memory::{MemoryDomain, SystemSlice};
51use crate::meta::FrameMetaSet;
52use crate::segment::Segment;
53use crate::tensor::MAX_TENSOR_RANK;
54
55pub const WIRE_VERSION: u8 = 1;
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum WireError {
63 Truncated,
65 BadTag,
69 UnsupportedDomain,
72}
73
74const PKT_CAPS_CHANGED: u8 = 0;
77const PKT_DATA_FRAME: u8 = 1;
78const PKT_EOS: u8 = 2;
79const PKT_FLUSH: u8 = 3;
80const PKT_SEGMENT: u8 = 4;
81
82const DOMAIN_SYSTEM: u8 = 0;
83
84#[cfg_attr(not(feature = "metadata"), allow(dead_code))]
85const META_ANALYTICS: u8 = 0;
86#[cfg_attr(not(feature = "metadata"), allow(dead_code))]
87const META_BLOB: u8 = 1;
88#[cfg_attr(not(feature = "metadata"), allow(dead_code))]
89const META_CAPTION: u8 = 2;
90#[cfg_attr(not(feature = "metadata"), allow(dead_code))]
91const META_HDR_STATIC: u8 = 3;
92#[cfg_attr(not(feature = "metadata"), allow(dead_code))]
93const META_TIMECODE: u8 = 4;
94
95struct Writer {
98 buf: Vec<u8>,
99}
100
101impl Writer {
102 fn new() -> Self {
103 Writer { buf: Vec::new() }
104 }
105 fn u8(&mut self, v: u8) {
106 self.buf.push(v);
107 }
108 fn u32(&mut self, v: u32) {
109 self.buf.extend_from_slice(&v.to_le_bytes());
110 }
111 fn u64(&mut self, v: u64) {
112 self.buf.extend_from_slice(&v.to_le_bytes());
113 }
114 fn bool(&mut self, v: bool) {
115 self.u8(v as u8);
116 }
117 #[cfg_attr(not(feature = "metadata"), allow(dead_code))]
119 fn f32(&mut self, v: f32) {
120 self.u32(v.to_bits());
121 }
122 fn f64(&mut self, v: f64) {
123 self.u64(v.to_bits());
124 }
125 fn bytes(&mut self, b: &[u8]) {
127 self.u32(b.len() as u32);
128 self.buf.extend_from_slice(b);
129 }
130 #[cfg_attr(not(feature = "metadata"), allow(dead_code))]
132 fn str(&mut self, s: &str) {
133 self.bytes(s.as_bytes());
134 }
135}
136
137struct Reader<'a> {
140 buf: &'a [u8],
141 pos: usize,
142}
143
144impl<'a> Reader<'a> {
145 fn new(buf: &'a [u8]) -> Self {
146 Reader { buf, pos: 0 }
147 }
148 fn take(&mut self, n: usize) -> Result<&'a [u8], WireError> {
149 let end = self.pos.checked_add(n).ok_or(WireError::Truncated)?;
150 let slice = self.buf.get(self.pos..end).ok_or(WireError::Truncated)?;
151 self.pos = end;
152 Ok(slice)
153 }
154 fn u8(&mut self) -> Result<u8, WireError> {
155 Ok(self.take(1)?[0])
156 }
157 fn u32(&mut self) -> Result<u32, WireError> {
158 let b = self.take(4)?;
159 Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
160 }
161 fn u64(&mut self) -> Result<u64, WireError> {
162 let b = self.take(8)?;
163 Ok(u64::from_le_bytes([
164 b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
165 ]))
166 }
167 fn bool(&mut self) -> Result<bool, WireError> {
168 Ok(self.u8()? != 0)
169 }
170 #[cfg_attr(not(feature = "metadata"), allow(dead_code))]
172 fn f32(&mut self) -> Result<f32, WireError> {
173 Ok(f32::from_bits(self.u32()?))
174 }
175 fn f64(&mut self) -> Result<f64, WireError> {
176 Ok(f64::from_bits(self.u64()?))
177 }
178 fn bytes(&mut self) -> Result<Vec<u8>, WireError> {
179 let len = self.u32()? as usize;
180 Ok(self.take(len)?.to_vec())
181 }
182 #[cfg_attr(not(feature = "metadata"), allow(dead_code))]
184 fn str(&mut self) -> Result<String, WireError> {
185 String::from_utf8(self.bytes()?).map_err(|_| WireError::BadTag)
186 }
187}
188
189fn video_codec_to_u8(c: VideoCodec) -> u8 {
192 match c {
193 VideoCodec::H264 => 0,
194 VideoCodec::H265 => 1,
195 VideoCodec::Av1 => 2,
196 VideoCodec::Vp8 => 3,
197 VideoCodec::Vp9 => 4,
198 VideoCodec::Mjpeg => 5,
199 VideoCodec::Mpeg4Part2 => 6,
200 VideoCodec::JpegXs => 7,
201 VideoCodec::SorensonH263 => 8,
202 VideoCodec::Vp6 { alpha: false } => 9,
203 VideoCodec::Vp6 { alpha: true } => 10,
204 VideoCodec::Mpeg2 => 11,
205 VideoCodec::Png => 12,
206 VideoCodec::WebP => 13,
207 VideoCodec::Vc1 => 14,
208 VideoCodec::Pnm => 15,
209 }
210}
211fn video_codec_from_u8(v: u8) -> Result<VideoCodec, WireError> {
212 Ok(match v {
213 0 => VideoCodec::H264,
214 1 => VideoCodec::H265,
215 2 => VideoCodec::Av1,
216 3 => VideoCodec::Vp8,
217 4 => VideoCodec::Vp9,
218 5 => VideoCodec::Mjpeg,
219 6 => VideoCodec::Mpeg4Part2,
220 7 => VideoCodec::JpegXs,
221 8 => VideoCodec::SorensonH263,
222 9 => VideoCodec::Vp6 { alpha: false },
223 10 => VideoCodec::Vp6 { alpha: true },
224 11 => VideoCodec::Mpeg2,
225 12 => VideoCodec::Png,
226 13 => VideoCodec::WebP,
227 14 => VideoCodec::Vc1,
228 15 => VideoCodec::Pnm,
229 _ => return Err(WireError::BadTag),
230 })
231}
232
233pub fn raw_format_to_u8(f: RawVideoFormat) -> u8 {
237 match f {
238 RawVideoFormat::Nv12 => 0,
239 RawVideoFormat::I420 => 1,
240 RawVideoFormat::Rgba8 => 2,
241 RawVideoFormat::Bgra8 => 3,
242 RawVideoFormat::Yuyv => 4,
243 RawVideoFormat::I420p10 => 5,
244 RawVideoFormat::I420p12 => 6,
245 RawVideoFormat::I422 => 7,
246 RawVideoFormat::I422p10 => 8,
247 RawVideoFormat::I422p12 => 9,
248 RawVideoFormat::I444 => 10,
249 RawVideoFormat::I444p10 => 11,
250 RawVideoFormat::I444p12 => 12,
251 RawVideoFormat::P010 => 13,
252 RawVideoFormat::Rgb8 => 14,
253 }
254}
255pub fn raw_format_from_u8(v: u8) -> Result<RawVideoFormat, WireError> {
258 Ok(match v {
259 0 => RawVideoFormat::Nv12,
260 1 => RawVideoFormat::I420,
261 2 => RawVideoFormat::Rgba8,
262 3 => RawVideoFormat::Bgra8,
263 4 => RawVideoFormat::Yuyv,
264 5 => RawVideoFormat::I420p10,
265 6 => RawVideoFormat::I420p12,
266 7 => RawVideoFormat::I422,
267 8 => RawVideoFormat::I422p10,
268 9 => RawVideoFormat::I422p12,
269 10 => RawVideoFormat::I444,
270 11 => RawVideoFormat::I444p10,
271 12 => RawVideoFormat::I444p12,
272 13 => RawVideoFormat::P010,
273 14 => RawVideoFormat::Rgb8,
274 _ => return Err(WireError::BadTag),
275 })
276}
277
278fn audio_format_to_u8(f: AudioFormat) -> u8 {
279 match f {
280 AudioFormat::Aac => 0,
281 AudioFormat::Opus => 1,
282 AudioFormat::PcmS16Le => 2,
283 AudioFormat::PcmF32Le => 3,
284 AudioFormat::PcmS24Le => 4,
285 AudioFormat::Mulaw => 5,
286 AudioFormat::Alaw => 6,
287 AudioFormat::ImaAdpcm => 7,
288 AudioFormat::Mp2 => 8,
289 AudioFormat::Ac3 => 9,
290 AudioFormat::Flac => 10,
291 AudioFormat::Vorbis => 11,
292 AudioFormat::Mp3 => 12,
293 AudioFormat::Speex => 13,
294 AudioFormat::PcmS32Le => 14,
295 AudioFormat::PcmU8 => 15,
296 }
297}
298fn audio_format_from_u8(v: u8) -> Result<AudioFormat, WireError> {
299 Ok(match v {
300 0 => AudioFormat::Aac,
301 1 => AudioFormat::Opus,
302 2 => AudioFormat::PcmS16Le,
303 3 => AudioFormat::PcmF32Le,
304 4 => AudioFormat::PcmS24Le,
305 5 => AudioFormat::Mulaw,
306 6 => AudioFormat::Alaw,
307 7 => AudioFormat::ImaAdpcm,
308 8 => AudioFormat::Mp2,
309 9 => AudioFormat::Ac3,
310 10 => AudioFormat::Flac,
311 11 => AudioFormat::Vorbis,
312 12 => AudioFormat::Mp3,
313 13 => AudioFormat::Speex,
314 14 => AudioFormat::PcmS32Le,
315 15 => AudioFormat::PcmU8,
316 _ => return Err(WireError::BadTag),
317 })
318}
319
320fn bytestream_to_u8(e: ByteStreamEncoding) -> u8 {
321 match e {
322 ByteStreamEncoding::MpegTs => 0,
323 ByteStreamEncoding::Matroska => 1,
324 ByteStreamEncoding::Ogg => 2,
325 ByteStreamEncoding::Flv => 3,
326 ByteStreamEncoding::IsoBmff => 4,
327 ByteStreamEncoding::Mp4 => 5,
328 ByteStreamEncoding::Ivf => 6,
329 ByteStreamEncoding::MpegPs => 7,
330 ByteStreamEncoding::Wav => 8,
331 ByteStreamEncoding::Aiff => 18,
332 ByteStreamEncoding::Au => 19,
333 ByteStreamEncoding::Avi => 9,
334 ByteStreamEncoding::Y4m => 10,
335 ByteStreamEncoding::Multipart => 11,
336 ByteStreamEncoding::Raw => 12,
337 ByteStreamEncoding::Rtp => 13,
338 ByteStreamEncoding::Srtp => 14,
339 ByteStreamEncoding::Rtcp => 15,
340 ByteStreamEncoding::Srtcp => 16,
341 ByteStreamEncoding::Dtls => 17,
342 }
343}
344fn bytestream_from_u8(v: u8) -> Result<ByteStreamEncoding, WireError> {
345 Ok(match v {
346 0 => ByteStreamEncoding::MpegTs,
347 1 => ByteStreamEncoding::Matroska,
348 2 => ByteStreamEncoding::Ogg,
349 3 => ByteStreamEncoding::Flv,
350 4 => ByteStreamEncoding::IsoBmff,
351 5 => ByteStreamEncoding::Mp4,
352 6 => ByteStreamEncoding::Ivf,
353 7 => ByteStreamEncoding::MpegPs,
354 8 => ByteStreamEncoding::Wav,
355 9 => ByteStreamEncoding::Avi,
356 18 => ByteStreamEncoding::Aiff,
357 19 => ByteStreamEncoding::Au,
358 10 => ByteStreamEncoding::Y4m,
359 11 => ByteStreamEncoding::Multipart,
360 12 => ByteStreamEncoding::Raw,
361 13 => ByteStreamEncoding::Rtp,
362 14 => ByteStreamEncoding::Srtp,
363 15 => ByteStreamEncoding::Rtcp,
364 16 => ByteStreamEncoding::Srtcp,
365 17 => ByteStreamEncoding::Dtls,
366 _ => return Err(WireError::BadTag),
367 })
368}
369
370fn text_format_to_u8(f: TextFormat) -> u8 {
371 match f {
372 TextFormat::Utf8 => 0,
373 TextFormat::PangoMarkup => 1,
374 TextFormat::Srt => 2,
375 TextFormat::WebVtt => 3,
376 TextFormat::Ssa => 4,
377 TextFormat::Ttml => 5,
378 TextFormat::Teletext => 6,
379 }
380}
381fn text_format_from_u8(v: u8) -> Result<TextFormat, WireError> {
382 Ok(match v {
383 0 => TextFormat::Utf8,
384 1 => TextFormat::PangoMarkup,
385 2 => TextFormat::Srt,
386 3 => TextFormat::WebVtt,
387 4 => TextFormat::Ssa,
388 5 => TextFormat::Ttml,
389 6 => TextFormat::Teletext,
390 _ => return Err(WireError::BadTag),
391 })
392}
393
394fn cc_format_to_u8(f: ClosedCaptionFormat) -> u8 {
395 match f {
396 ClosedCaptionFormat::Cea608 => 0,
397 ClosedCaptionFormat::Cea708 => 1,
398 ClosedCaptionFormat::Cea608Raw => 2,
399 ClosedCaptionFormat::Cea608S334 => 3,
400 ClosedCaptionFormat::Cea708Cdp => 4,
401 }
402}
403fn cc_format_from_u8(v: u8) -> Result<ClosedCaptionFormat, WireError> {
404 Ok(match v {
405 0 => ClosedCaptionFormat::Cea608,
406 1 => ClosedCaptionFormat::Cea708,
407 2 => ClosedCaptionFormat::Cea608Raw,
408 3 => ClosedCaptionFormat::Cea608S334,
409 4 => ClosedCaptionFormat::Cea708Cdp,
410 _ => return Err(WireError::BadTag),
411 })
412}
413
414fn subpicture_format_to_u8(f: SubPictureFormat) -> u8 {
415 match f {
416 SubPictureFormat::VobSub => 0,
417 SubPictureFormat::DvbSub => 1,
418 SubPictureFormat::Pgs => 2,
419 }
420}
421fn subpicture_format_from_u8(v: u8) -> Result<SubPictureFormat, WireError> {
422 Ok(match v {
423 0 => SubPictureFormat::VobSub,
424 1 => SubPictureFormat::DvbSub,
425 2 => SubPictureFormat::Pgs,
426 _ => return Err(WireError::BadTag),
427 })
428}
429
430fn dtype_to_u8(d: TensorDType) -> u8 {
431 match d {
432 TensorDType::F16 => 0,
433 TensorDType::F32 => 1,
434 TensorDType::I8 => 2,
435 TensorDType::U8 => 3,
436 }
437}
438fn dtype_from_u8(v: u8) -> Result<TensorDType, WireError> {
439 Ok(match v {
440 0 => TensorDType::F16,
441 1 => TensorDType::F32,
442 2 => TensorDType::I8,
443 3 => TensorDType::U8,
444 _ => return Err(WireError::BadTag),
445 })
446}
447
448fn layout_to_u8(l: TensorLayout) -> u8 {
449 match l {
450 TensorLayout::Nchw => 0,
451 TensorLayout::Nhwc => 1,
452 }
453}
454fn layout_from_u8(v: u8) -> Result<TensorLayout, WireError> {
455 Ok(match v {
456 0 => TensorLayout::Nchw,
457 1 => TensorLayout::Nhwc,
458 _ => return Err(WireError::BadTag),
459 })
460}
461
462fn put_dim(w: &mut Writer, d: &Dim) {
465 match d {
466 Dim::Any => w.u8(0),
467 Dim::Range { min, max } => {
468 w.u8(1);
469 w.u32(*min);
470 w.u32(*max);
471 }
472 Dim::Fixed(v) => {
473 w.u8(2);
474 w.u32(*v);
475 }
476 }
477}
478fn get_dim(r: &mut Reader) -> Result<Dim, WireError> {
479 Ok(match r.u8()? {
480 0 => Dim::Any,
481 1 => Dim::Range {
482 min: r.u32()?,
483 max: r.u32()?,
484 },
485 2 => Dim::Fixed(r.u32()?),
486 _ => return Err(WireError::BadTag),
487 })
488}
489
490fn put_rate(w: &mut Writer, rt: &Rate) {
491 match rt {
492 Rate::Any => w.u8(0),
493 Rate::Range { min_q16, max_q16 } => {
494 w.u8(1);
495 w.u32(*min_q16);
496 w.u32(*max_q16);
497 }
498 Rate::Fixed(v) => {
499 w.u8(2);
500 w.u32(*v);
501 }
502 }
503}
504fn get_rate(r: &mut Reader) -> Result<Rate, WireError> {
505 Ok(match r.u8()? {
506 0 => Rate::Any,
507 1 => Rate::Range {
508 min_q16: r.u32()?,
509 max_q16: r.u32()?,
510 },
511 2 => Rate::Fixed(r.u32()?),
512 _ => return Err(WireError::BadTag),
513 })
514}
515
516fn interlace_to_u8(i: Interlace) -> u8 {
517 match i {
518 Interlace::Any => 0,
519 Interlace::Progressive => 1,
520 Interlace::Interleaved => 2,
521 }
522}
523fn interlace_from_u8(v: u8) -> Result<Interlace, WireError> {
524 Ok(match v {
525 0 => Interlace::Any,
526 1 => Interlace::Progressive,
527 2 => Interlace::Interleaved,
528 _ => return Err(WireError::BadTag),
529 })
530}
531
532fn put_caps(w: &mut Writer, c: &Caps) {
535 match c {
536 Caps::CompressedVideo {
537 codec,
538 width,
539 height,
540 framerate,
541 } => {
542 w.u8(0);
543 w.u8(video_codec_to_u8(*codec));
544 put_dim(w, width);
545 put_dim(w, height);
546 put_rate(w, framerate);
547 }
548 Caps::RawVideo {
549 format,
550 width,
551 height,
552 framerate,
553 interlace,
554 } => {
555 w.u8(1);
556 w.u8(raw_format_to_u8(*format));
557 put_dim(w, width);
558 put_dim(w, height);
559 put_rate(w, framerate);
560 w.u8(interlace_to_u8(*interlace));
561 }
562 Caps::Audio {
563 format,
564 channels,
565 sample_rate,
566 } => {
567 w.u8(2);
568 w.u8(audio_format_to_u8(*format));
569 w.u8(*channels);
570 w.u32(*sample_rate);
571 }
572 Caps::Tensor {
573 dtype,
574 shape,
575 layout,
576 } => {
577 w.u8(3);
578 w.u8(dtype_to_u8(*dtype));
579 w.u32(shape.dims().len() as u32);
580 for d in shape.dims() {
581 w.u32(*d);
582 }
583 w.u8(layout_to_u8(*layout));
584 }
585 Caps::ByteStream { encoding } => {
586 w.u8(4);
587 w.u8(bytestream_to_u8(*encoding));
588 }
589 Caps::Text { format } => {
590 w.u8(5);
591 w.u8(text_format_to_u8(*format));
592 }
593 Caps::Klv => w.u8(6),
594 Caps::ClosedCaption { format } => {
595 w.u8(7);
596 w.u8(cc_format_to_u8(*format));
597 }
598 Caps::SubPicture { format } => {
599 w.u8(8);
600 w.u8(subpicture_format_to_u8(*format));
601 }
602 }
603}
604
605fn get_caps(r: &mut Reader) -> Result<Caps, WireError> {
606 Ok(match r.u8()? {
607 0 => Caps::CompressedVideo {
608 codec: video_codec_from_u8(r.u8()?)?,
609 width: get_dim(r)?,
610 height: get_dim(r)?,
611 framerate: get_rate(r)?,
612 },
613 1 => Caps::RawVideo {
614 format: raw_format_from_u8(r.u8()?)?,
615 width: get_dim(r)?,
616 height: get_dim(r)?,
617 framerate: get_rate(r)?,
618 interlace: interlace_from_u8(r.u8()?)?,
619 },
620 2 => Caps::Audio {
621 format: audio_format_from_u8(r.u8()?)?,
622 channels: r.u8()?,
623 sample_rate: r.u32()?,
624 },
625 3 => {
626 let dtype = dtype_from_u8(r.u8()?)?;
627 let n = r.u32()? as usize;
631 let mut dims = [0u32; MAX_TENSOR_RANK];
632 let slots = dims.get_mut(..n).ok_or(WireError::BadTag)?;
633 for d in slots.iter_mut() {
634 *d = r.u32()?;
635 }
636 let layout = layout_from_u8(r.u8()?)?;
637 let shape = TensorShape::from_slice(&dims[..n]).ok_or(WireError::BadTag)?;
638 Caps::Tensor {
639 dtype,
640 shape,
641 layout,
642 }
643 }
644 4 => Caps::ByteStream {
645 encoding: bytestream_from_u8(r.u8()?)?,
646 },
647 5 => Caps::Text {
648 format: text_format_from_u8(r.u8()?)?,
649 },
650 6 => Caps::Klv,
651 7 => Caps::ClosedCaption {
652 format: cc_format_from_u8(r.u8()?)?,
653 },
654 8 => Caps::SubPicture {
655 format: subpicture_format_from_u8(r.u8()?)?,
656 },
657 _ => return Err(WireError::BadTag),
658 })
659}
660
661fn put_timing(w: &mut Writer, t: &FrameTiming) {
664 w.u64(t.pts_ns);
665 w.u64(t.dts_ns);
666 w.u64(t.duration_ns);
667 w.u64(t.capture_ns);
668 w.u64(t.arrival_ns);
669 w.bool(t.keyframe);
670}
671fn get_timing(r: &mut Reader) -> Result<FrameTiming, WireError> {
672 Ok(FrameTiming {
673 pts_ns: r.u64()?,
674 dts_ns: r.u64()?,
675 duration_ns: r.u64()?,
676 capture_ns: r.u64()?,
677 arrival_ns: r.u64()?,
678 keyframe: r.bool()?,
679 })
680}
681
682fn put_segment(w: &mut Writer, s: &Segment) {
685 w.f64(s.rate);
686 w.f64(s.applied_rate);
687 w.u64(s.base);
688 w.u64(s.start);
689 match s.stop {
690 Some(v) => {
691 w.bool(true);
692 w.u64(v);
693 }
694 None => w.bool(false),
695 }
696 w.u64(s.time);
697 w.u64(s.position);
698 w.bool(s.key_units_only);
699}
700fn get_segment(r: &mut Reader) -> Result<Segment, WireError> {
701 let rate = r.f64()?;
702 let applied_rate = r.f64()?;
703 let base = r.u64()?;
704 let start = r.u64()?;
705 let stop = if r.bool()? { Some(r.u64()?) } else { None };
706 let time = r.u64()?;
707 let position = r.u64()?;
708 let key_units_only = r.bool()?;
709 Ok(Segment {
710 rate,
711 applied_rate,
712 base,
713 start,
714 stop,
715 time,
716 position,
717 key_units_only,
718 })
719}
720
721fn put_domain(w: &mut Writer, d: &MemoryDomain) -> Result<(), WireError> {
724 match d {
725 MemoryDomain::System(s) => {
726 w.u8(DOMAIN_SYSTEM);
727 w.bytes(s.as_slice());
728 Ok(())
729 }
730 MemoryDomain::SystemView(v) => {
734 w.u8(DOMAIN_SYSTEM);
735 let dense = v.materialize();
736 w.bytes(&dense);
737 Ok(())
738 }
739 _ => Err(WireError::UnsupportedDomain),
741 }
742}
743fn get_domain(r: &mut Reader) -> Result<MemoryDomain, WireError> {
744 match r.u8()? {
745 DOMAIN_SYSTEM => {
746 let bytes = r.bytes()?;
747 Ok(MemoryDomain::System(SystemSlice::from_boxed(
748 bytes.into_boxed_slice(),
749 )))
750 }
751 _ => Err(WireError::BadTag),
752 }
753}
754
755#[cfg(feature = "metadata")]
758fn put_meta(w: &mut Writer, meta: &FrameMetaSet) {
759 use crate::meta::{
760 AnalyticsMeta, AnalyticsNode, BlobMeta, CaptionMeta, HdrStaticMeta, TimecodeMeta,
761 };
762
763 let analytics = meta.get::<AnalyticsMeta>();
764 let blob = meta.get::<BlobMeta>();
765 let caption = meta.get::<CaptionMeta>();
766 let hdr = meta.get::<HdrStaticMeta>();
767 let timecode = meta.get::<TimecodeMeta>();
768 let count = analytics.is_some() as u8
769 + blob.is_some() as u8
770 + caption.is_some() as u8
771 + hdr.is_some() as u8
772 + timecode.is_some() as u8;
773 w.u8(count);
774
775 if let Some(a) = analytics {
776 w.u8(META_ANALYTICS);
777 w.u32(a.nodes.len() as u32);
778 for node in &a.nodes {
779 match node {
780 AnalyticsNode::Detection(d) => {
781 w.u8(0);
782 w.f32(d.bbox.x);
783 w.f32(d.bbox.y);
784 w.f32(d.bbox.w);
785 w.f32(d.bbox.h);
786 w.u32(d.label);
787 w.f32(d.confidence);
788 }
789 AnalyticsNode::Classification(c) => {
790 w.u8(1);
791 w.u32(c.label);
792 w.f32(c.confidence);
793 }
794 AnalyticsNode::Tracking(t) => {
795 w.u8(2);
796 w.u64(t.object_id);
797 }
798 AnalyticsNode::Segmentation(s) => {
799 w.u8(3);
800 w.f32(s.bbox.x);
801 w.f32(s.bbox.y);
802 w.f32(s.bbox.w);
803 w.f32(s.bbox.h);
804 w.u32(s.label);
805 w.f32(s.confidence);
806 w.u32(s.mask.width());
807 w.u32(s.mask.height());
808 w.u32(s.mask.stride());
809 w.bytes(s.mask.data());
810 }
811 AnalyticsNode::Roi(r) => {
812 w.u8(4);
813 w.f32(r.bbox.x);
814 w.f32(r.bbox.y);
815 w.f32(r.bbox.w);
816 w.f32(r.bbox.h);
817 w.u32(r.id);
818 w.u32(r.label);
819 }
820 }
821 }
822 w.u32(a.relations.len() as u32);
823 for rel in &a.relations {
824 w.u32(rel.from as u32);
825 w.u32(rel.to as u32);
826 w.u8(relation_kind_to_u8(rel.kind));
827 }
828 }
829
830 if let Some(b) = blob {
831 w.u8(META_BLOB);
832 w.u32(b.blobs.len() as u32);
833 for blob in &b.blobs {
834 w.str(&blob.header);
835 w.bytes(&blob.payload);
836 }
837 }
838
839 if let Some(c) = caption {
840 w.u8(META_CAPTION);
841 w.u32(c.triples.len() as u32);
842 for t in &c.triples {
843 w.u8(t.cc_type);
844 w.u8(t.b0);
845 w.u8(t.b1);
846 }
847 }
848
849 if let Some(h) = hdr {
850 w.u8(META_HDR_STATIC);
851 match &h.mastering {
852 Some(m) => {
853 w.bool(true);
854 for p in &m.display_primaries {
855 w.f32(p.x);
856 w.f32(p.y);
857 }
858 w.f32(m.white_point.x);
859 w.f32(m.white_point.y);
860 w.f32(m.max_luminance);
861 w.f32(m.min_luminance);
862 }
863 None => w.bool(false),
864 }
865 put_opt_u16(w, h.max_content_light_level);
866 put_opt_u16(w, h.max_frame_average_light_level);
867 }
868
869 if let Some(t) = timecode {
870 w.u8(META_TIMECODE);
871 w.u8(t.hours);
872 w.u8(t.minutes);
873 w.u8(t.seconds);
874 w.u8(t.frames);
875 w.bool(t.drop_frame);
876 w.bool(t.framerate_q16.is_some());
877 w.u32(t.framerate_q16.unwrap_or(0));
878 }
879}
880
881#[cfg(feature = "metadata")]
884fn put_opt_u16(w: &mut Writer, v: Option<u16>) {
885 w.bool(v.is_some());
886 w.u32(v.unwrap_or(0) as u32);
887}
888
889#[cfg(feature = "metadata")]
890fn get_opt_u16(r: &mut Reader) -> Result<Option<u16>, WireError> {
891 let present = r.bool()?;
892 let v = u16::try_from(r.u32()?).map_err(|_| WireError::BadTag)?;
893 Ok(present.then_some(v))
894}
895
896#[cfg(not(feature = "metadata"))]
897fn put_meta(w: &mut Writer, _meta: &FrameMetaSet) {
898 w.u8(0);
900}
901
902#[cfg(feature = "metadata")]
903fn relation_kind_to_u8(k: crate::meta::RelationKind) -> u8 {
904 use crate::meta::RelationKind;
905 match k {
906 RelationKind::Classifies => 0,
907 RelationKind::Tracks => 1,
908 RelationKind::Contains => 2,
909 }
910}
911
912#[cfg(feature = "metadata")]
913fn relation_kind_from_u8(v: u8) -> Result<crate::meta::RelationKind, WireError> {
914 use crate::meta::RelationKind;
915 Ok(match v {
916 0 => RelationKind::Classifies,
917 1 => RelationKind::Tracks,
918 2 => RelationKind::Contains,
919 _ => return Err(WireError::BadTag),
920 })
921}
922
923#[cfg(feature = "metadata")]
924fn get_meta(r: &mut Reader) -> Result<FrameMetaSet, WireError> {
925 use crate::meta::{
926 AnalyticsMeta, AnalyticsNode, BBox, Blob, BlobMeta, CaptionMeta, CaptionTriple,
927 Chromaticity, Classification, HdrStaticMeta, Mask, MasteringDisplay, ObjectDetection,
928 Relation, Roi, Segmentation, TimecodeMeta, Tracking,
929 };
930
931 let count = r.u8()?;
932 let mut set = FrameMetaSet::new();
933 for _ in 0..count {
934 match r.u8()? {
935 META_ANALYTICS => {
936 let mut a = AnalyticsMeta::new();
937 let n = r.u32()? as usize;
938 for _ in 0..n {
939 let node = match r.u8()? {
940 0 => AnalyticsNode::Detection(ObjectDetection {
941 bbox: BBox {
942 x: r.f32()?,
943 y: r.f32()?,
944 w: r.f32()?,
945 h: r.f32()?,
946 },
947 label: r.u32()?,
948 confidence: r.f32()?,
949 }),
950 1 => AnalyticsNode::Classification(Classification {
951 label: r.u32()?,
952 confidence: r.f32()?,
953 }),
954 2 => AnalyticsNode::Tracking(Tracking {
955 object_id: r.u64()?,
956 }),
957 3 => {
958 let bbox = BBox {
959 x: r.f32()?,
960 y: r.f32()?,
961 w: r.f32()?,
962 h: r.f32()?,
963 };
964 let label = r.u32()?;
965 let confidence = r.f32()?;
966 let (width, height, stride) = (r.u32()?, r.u32()?, r.u32()?);
967 let mask = Mask::new(width, height, stride, r.bytes()?)
972 .ok_or(WireError::BadTag)?;
973 AnalyticsNode::Segmentation(Segmentation {
974 bbox,
975 label,
976 confidence,
977 mask,
978 })
979 }
980 4 => AnalyticsNode::Roi(Roi {
981 bbox: BBox {
982 x: r.f32()?,
983 y: r.f32()?,
984 w: r.f32()?,
985 h: r.f32()?,
986 },
987 id: r.u32()?,
988 label: r.u32()?,
989 }),
990 _ => return Err(WireError::BadTag),
991 };
992 a.nodes.push(node);
993 }
994 let m = r.u32()? as usize;
995 for _ in 0..m {
996 a.relations.push(Relation {
997 from: r.u32()? as usize,
998 to: r.u32()? as usize,
999 kind: relation_kind_from_u8(r.u8()?)?,
1000 });
1001 }
1002 set.attach(a);
1003 }
1004 META_BLOB => {
1005 let mut b = BlobMeta::new();
1006 let n = r.u32()? as usize;
1007 for _ in 0..n {
1008 b.blobs.push(Blob {
1009 header: r.str()?,
1010 payload: r.bytes()?,
1011 });
1012 }
1013 set.attach(b);
1014 }
1015 META_CAPTION => {
1016 let mut c = CaptionMeta::new();
1017 let n = r.u32()? as usize;
1018 for _ in 0..n {
1019 c.push(CaptionTriple {
1020 cc_type: r.u8()?,
1021 b0: r.u8()?,
1022 b1: r.u8()?,
1023 });
1024 }
1025 set.attach(c);
1026 }
1027 META_HDR_STATIC => {
1028 let mastering = if r.bool()? {
1029 let mut primaries = [Chromaticity { x: 0.0, y: 0.0 }; 3];
1030 for p in &mut primaries {
1031 p.x = r.f32()?;
1032 p.y = r.f32()?;
1033 }
1034 Some(MasteringDisplay {
1035 display_primaries: primaries,
1036 white_point: Chromaticity {
1037 x: r.f32()?,
1038 y: r.f32()?,
1039 },
1040 max_luminance: r.f32()?,
1041 min_luminance: r.f32()?,
1042 })
1043 } else {
1044 None
1045 };
1046 set.attach(HdrStaticMeta {
1047 mastering,
1048 max_content_light_level: get_opt_u16(r)?,
1049 max_frame_average_light_level: get_opt_u16(r)?,
1050 });
1051 }
1052 META_TIMECODE => {
1053 let tc = TimecodeMeta {
1054 hours: r.u8()?,
1055 minutes: r.u8()?,
1056 seconds: r.u8()?,
1057 frames: r.u8()?,
1058 drop_frame: r.bool()?,
1059 framerate_q16: {
1060 let present = r.bool()?;
1061 let v = r.u32()?;
1062 present.then_some(v)
1063 },
1064 };
1065 set.attach(tc);
1066 }
1067 _ => return Err(WireError::BadTag),
1068 }
1069 }
1070 Ok(set)
1071}
1072
1073#[cfg(not(feature = "metadata"))]
1074fn get_meta(r: &mut Reader) -> Result<FrameMetaSet, WireError> {
1075 let _count = r.u8()?;
1081 Ok(FrameMetaSet::new())
1082}
1083
1084pub fn encode_packet(packet: &PipelinePacket) -> Result<Vec<u8>, WireError> {
1093 let mut w = Writer::new();
1094 w.u8(WIRE_VERSION);
1095 match packet {
1096 PipelinePacket::CapsChanged(caps) => {
1097 w.u8(PKT_CAPS_CHANGED);
1098 put_caps(&mut w, caps);
1099 }
1100 PipelinePacket::DataFrame(frame) => {
1101 w.u8(PKT_DATA_FRAME);
1102 put_timing(&mut w, &frame.timing);
1103 w.u64(frame.sequence);
1104 put_domain(&mut w, &frame.domain)?;
1105 put_meta(&mut w, &frame.meta);
1106 }
1107 PipelinePacket::Eos => w.u8(PKT_EOS),
1108 PipelinePacket::Flush => w.u8(PKT_FLUSH),
1109 PipelinePacket::Segment(seg) => {
1110 w.u8(PKT_SEGMENT);
1111 put_segment(&mut w, seg);
1112 }
1113 PipelinePacket::Tick => return Err(WireError::BadTag),
1118 }
1119 Ok(w.buf)
1120}
1121
1122pub fn decode_packet(bytes: &[u8]) -> Result<PipelinePacket, WireError> {
1128 let mut r = Reader::new(bytes);
1129 if r.u8()? != WIRE_VERSION {
1130 return Err(WireError::BadTag);
1131 }
1132 Ok(match r.u8()? {
1133 PKT_CAPS_CHANGED => PipelinePacket::CapsChanged(get_caps(&mut r)?),
1134 PKT_DATA_FRAME => {
1135 let timing = get_timing(&mut r)?;
1136 let sequence = r.u64()?;
1137 let domain = get_domain(&mut r)?;
1138 let meta = get_meta(&mut r)?;
1139 PipelinePacket::DataFrame(Frame {
1140 domain,
1141 timing,
1142 sequence,
1143 meta,
1144 })
1145 }
1146 PKT_EOS => PipelinePacket::Eos,
1147 PKT_FLUSH => PipelinePacket::Flush,
1148 PKT_SEGMENT => PipelinePacket::Segment(get_segment(&mut r)?),
1149 _ => return Err(WireError::BadTag),
1150 })
1151}
1152
1153pub const RECORD_LENGTH_PREFIX_BYTES: usize = 4;
1158
1159pub fn record_length_prefix(
1165 payload_len: usize,
1166) -> Result<[u8; RECORD_LENGTH_PREFIX_BYTES], WireError> {
1167 let len = u32::try_from(payload_len).map_err(|_| WireError::UnsupportedDomain)?;
1168 Ok(len.to_le_bytes())
1169}
1170
1171pub fn read_records(buf: &[u8]) -> Result<Vec<PipelinePacket>, WireError> {
1175 let mut out = Vec::new();
1176 let mut i = 0usize;
1177 while i + RECORD_LENGTH_PREFIX_BYTES <= buf.len() {
1178 let len = u32::from_le_bytes([buf[i], buf[i + 1], buf[i + 2], buf[i + 3]]) as usize;
1179 let start = i + RECORD_LENGTH_PREFIX_BYTES;
1180 let end = match start.checked_add(len) {
1181 Some(e) if e <= buf.len() => e,
1182 _ => break, };
1184 out.push(decode_packet(&buf[start..end])?);
1185 i = end;
1186 }
1187 Ok(out)
1188}
1189
1190#[cfg(test)]
1191mod tests {
1192 use super::*;
1193 use alloc::boxed::Box;
1194
1195 fn roundtrip(p: &PipelinePacket) -> PipelinePacket {
1196 let bytes = encode_packet(p).expect("encode");
1197 decode_packet(&bytes).expect("decode")
1198 }
1199
1200 #[test]
1201 fn every_codec_tag_round_trips() {
1202 let video = [
1205 VideoCodec::H264,
1206 VideoCodec::H265,
1207 VideoCodec::Av1,
1208 VideoCodec::Vp8,
1209 VideoCodec::Vp9,
1210 VideoCodec::Mjpeg,
1211 VideoCodec::Mpeg4Part2,
1212 VideoCodec::JpegXs,
1213 VideoCodec::SorensonH263,
1214 VideoCodec::Vp6 { alpha: false },
1215 VideoCodec::Vp6 { alpha: true },
1216 VideoCodec::Mpeg2,
1217 VideoCodec::Png,
1218 VideoCodec::WebP,
1219 VideoCodec::Vc1,
1220 VideoCodec::Pnm,
1221 ];
1222 for c in video {
1223 assert_eq!(video_codec_from_u8(video_codec_to_u8(c)), Ok(c));
1224 }
1225 let audio = [
1226 AudioFormat::Aac,
1227 AudioFormat::Opus,
1228 AudioFormat::Mp2,
1229 AudioFormat::Mp3,
1230 AudioFormat::Speex,
1231 AudioFormat::Ac3,
1232 AudioFormat::Flac,
1233 AudioFormat::Vorbis,
1234 AudioFormat::PcmS16Le,
1235 AudioFormat::PcmF32Le,
1236 AudioFormat::PcmS24Le,
1237 AudioFormat::PcmS32Le,
1238 AudioFormat::PcmU8,
1239 AudioFormat::Mulaw,
1240 AudioFormat::Alaw,
1241 AudioFormat::ImaAdpcm,
1242 ];
1243 for f in audio {
1244 assert_eq!(audio_format_from_u8(audio_format_to_u8(f)), Ok(f));
1245 }
1246 }
1247
1248 #[test]
1249 fn packet_bytestream_tags_round_trip() {
1250 for encoding in [
1251 ByteStreamEncoding::Rtp,
1252 ByteStreamEncoding::Srtp,
1253 ByteStreamEncoding::Rtcp,
1254 ByteStreamEncoding::Srtcp,
1255 ByteStreamEncoding::Dtls,
1256 ByteStreamEncoding::Aiff,
1257 ByteStreamEncoding::Au,
1258 ] {
1259 assert_eq!(bytestream_from_u8(bytestream_to_u8(encoding)), Ok(encoding));
1260 }
1261 }
1262
1263 #[test]
1264 fn caps_changed_round_trips_every_variant() {
1265 let cases = [
1266 Caps::CompressedVideo {
1267 codec: VideoCodec::H265,
1268 width: Dim::Fixed(1920),
1269 height: Dim::Range {
1270 min: 480,
1271 max: 1080,
1272 },
1273 framerate: Rate::Fixed(30 << 16),
1274 },
1275 Caps::RawVideo {
1276 format: RawVideoFormat::Nv12,
1277 width: Dim::Fixed(640),
1278 height: Dim::Fixed(480),
1279 framerate: Rate::Any,
1280 interlace: crate::Interlace::Any,
1281 },
1282 Caps::Audio {
1283 format: AudioFormat::Opus,
1284 channels: 2,
1285 sample_rate: 48_000,
1286 },
1287 Caps::Tensor {
1288 dtype: TensorDType::F32,
1289 shape: TensorShape::new([1, 3, 224, 224]),
1290 layout: TensorLayout::Nchw,
1291 },
1292 Caps::ByteStream {
1293 encoding: ByteStreamEncoding::MpegTs,
1294 },
1295 Caps::Text {
1296 format: TextFormat::WebVtt,
1297 },
1298 Caps::ClosedCaption {
1299 format: ClosedCaptionFormat::Cea708,
1300 },
1301 ];
1302 for caps in cases {
1303 let p = PipelinePacket::CapsChanged(caps.clone());
1304 match roundtrip(&p) {
1305 PipelinePacket::CapsChanged(got) => assert_eq!(got, caps),
1306 other => panic!("expected CapsChanged, got {other:?}"),
1307 }
1308 }
1309 }
1310
1311 #[test]
1312 fn tensor_caps_rank_beyond_max_rejected() {
1313 let mut w = Writer::new();
1317 w.u8(3); w.u8(dtype_to_u8(TensorDType::F32));
1319 let n = (MAX_TENSOR_RANK + 1) as u32;
1320 w.u32(n);
1321 for _ in 0..n {
1322 w.u32(1);
1323 }
1324 w.u8(layout_to_u8(TensorLayout::Nchw));
1325 let mut r = Reader::new(&w.buf);
1326 assert_eq!(get_caps(&mut r), Err(WireError::BadTag));
1327 }
1328
1329 #[test]
1330 fn mpeg4_part2_codec_round_trips() {
1331 let caps = Caps::CompressedVideo {
1332 codec: VideoCodec::Mpeg4Part2,
1333 width: Dim::Fixed(720),
1334 height: Dim::Fixed(576),
1335 framerate: Rate::Fixed(25 << 16),
1336 };
1337 match roundtrip(&PipelinePacket::CapsChanged(caps.clone())) {
1338 PipelinePacket::CapsChanged(got) => assert_eq!(got, caps),
1339 other => panic!("expected CapsChanged, got {other:?}"),
1340 }
1341 assert_eq!(video_codec_to_u8(VideoCodec::Mpeg4Part2), 6);
1343 }
1344
1345 #[test]
1346 fn data_frame_round_trips_bytes_timing_and_sequence() {
1347 let bytes: Vec<u8> = (0u8..=200).collect();
1348 let timing = FrameTiming {
1349 pts_ns: 1_000,
1350 dts_ns: 900,
1351 duration_ns: 33,
1352 capture_ns: 7,
1353 arrival_ns: 42,
1354 keyframe: true,
1355 };
1356 let frame = Frame {
1357 domain: MemoryDomain::System(SystemSlice::from_boxed(bytes.clone().into_boxed_slice())),
1358 timing,
1359 sequence: 12_345,
1360 meta: FrameMetaSet::new(),
1361 };
1362 match roundtrip(&PipelinePacket::DataFrame(frame)) {
1363 PipelinePacket::DataFrame(got) => {
1364 assert_eq!(got.sequence, 12_345);
1365 assert_eq!(got.timing, timing);
1366 match got.domain {
1367 MemoryDomain::System(s) => assert_eq!(s.as_slice(), &bytes[..]),
1368 other => panic!("expected System, got {other:?}"),
1369 }
1370 }
1371 other => panic!("expected DataFrame, got {other:?}"),
1372 }
1373 }
1374
1375 #[test]
1376 fn control_packets_round_trip() {
1377 assert!(matches!(
1378 roundtrip(&PipelinePacket::Eos),
1379 PipelinePacket::Eos
1380 ));
1381 assert!(matches!(
1382 roundtrip(&PipelinePacket::Flush),
1383 PipelinePacket::Flush
1384 ));
1385 let seg = Segment {
1386 rate: 2.0,
1387 applied_rate: 1.0,
1388 base: 5,
1389 start: 1_000,
1390 stop: Some(9_000),
1391 time: 1_000,
1392 position: 3_000,
1393 key_units_only: true,
1394 };
1395 match roundtrip(&PipelinePacket::Segment(seg)) {
1396 PipelinePacket::Segment(got) => assert_eq!(got, seg),
1397 other => panic!("expected Segment, got {other:?}"),
1398 }
1399 }
1400
1401 #[test]
1402 fn device_domain_cannot_be_serialized() {
1403 let dmabuf = unsafe { crate::memory::OwnedDmaBuf::from_raw(-1, 0, 0) };
1410 let frame = Frame::new(MemoryDomain::DmaBuf(dmabuf), FrameTiming::default(), 0);
1411 assert_eq!(
1412 encode_packet(&PipelinePacket::DataFrame(frame)),
1413 Err(WireError::UnsupportedDomain)
1414 );
1415 }
1416
1417 #[test]
1418 fn truncated_and_bad_version_are_rejected() {
1419 assert!(matches!(decode_packet(&[]), Err(WireError::Truncated)));
1420 assert!(matches!(
1422 decode_packet(&[WIRE_VERSION + 1, PKT_EOS]),
1423 Err(WireError::BadTag)
1424 ));
1425 assert!(matches!(
1427 decode_packet(&[WIRE_VERSION, 250]),
1428 Err(WireError::BadTag)
1429 ));
1430 let mut bytes = encode_packet(&PipelinePacket::CapsChanged(Caps::Text {
1432 format: TextFormat::Utf8,
1433 }))
1434 .unwrap();
1435 bytes.pop();
1436 assert!(matches!(decode_packet(&bytes), Err(WireError::Truncated)));
1437 }
1438
1439 #[test]
1440 fn system_view_frame_materializes_to_system_bytes() {
1441 use crate::memory::SystemView;
1442 use crate::tensor::TensorView;
1443 let backing: alloc::sync::Arc<[u8]> = Box::<[u8]>::from([1u8, 2, 3, 4, 5, 6, 7, 8]).into();
1446 let view = TensorView::contiguous(TensorDType::U8, &[8]);
1447 let frame = Frame::new(
1448 MemoryDomain::SystemView(SystemView::new(backing, view)),
1449 FrameTiming::default(),
1450 1,
1451 );
1452 match roundtrip(&PipelinePacket::DataFrame(frame)) {
1453 PipelinePacket::DataFrame(got) => match got.domain {
1454 MemoryDomain::System(s) => assert_eq!(s.as_slice(), &[1, 2, 3, 4, 5, 6, 7, 8]),
1455 other => panic!("SystemView should decode as System, got {other:?}"),
1456 },
1457 other => panic!("expected DataFrame, got {other:?}"),
1458 }
1459 }
1460
1461 #[cfg(feature = "metadata")]
1462 #[test]
1463 fn analytics_and_blob_metadata_round_trip() {
1464 use crate::meta::{
1465 AnalyticsMeta, AnalyticsNode, BBox, BlobMeta, Classification, ObjectDetection,
1466 RelationKind,
1467 };
1468 let mut analytics = AnalyticsMeta::new();
1469 let d = analytics.add_detection(ObjectDetection {
1470 bbox: BBox {
1471 x: 0.1,
1472 y: 0.2,
1473 w: 0.3,
1474 h: 0.4,
1475 },
1476 label: 7,
1477 confidence: 0.9,
1478 });
1479 let c = analytics.push(AnalyticsNode::Classification(Classification {
1480 label: 42,
1481 confidence: 0.7,
1482 }));
1483 analytics.relate(d, c, RelationKind::Classifies);
1484
1485 let mut blob = BlobMeta::new();
1486 blob.push("embedding", alloc::vec![1, 2, 3, 4]);
1487
1488 let mut meta = FrameMetaSet::new();
1489 meta.attach(analytics.clone());
1490 meta.attach(blob.clone());
1491
1492 let frame = Frame {
1493 domain: MemoryDomain::System(SystemSlice::from_boxed(Box::new([9u8; 16]))),
1494 timing: FrameTiming::default(),
1495 sequence: 0,
1496 meta,
1497 };
1498 match roundtrip(&PipelinePacket::DataFrame(frame)) {
1499 PipelinePacket::DataFrame(got) => {
1500 let a = got.meta.get::<AnalyticsMeta>().expect("analytics survived");
1501 assert_eq!(a.nodes, analytics.nodes);
1502 assert_eq!(a.relations, analytics.relations);
1503 let b = got.meta.get::<BlobMeta>().expect("blob survived");
1504 assert_eq!(b, &blob);
1505 }
1506 other => panic!("expected DataFrame, got {other:?}"),
1507 }
1508 }
1509
1510 #[cfg(feature = "metadata")]
1511 #[test]
1512 fn segmentation_and_roi_nodes_round_trip() {
1513 use crate::meta::{AnalyticsMeta, AnalyticsNode, BBox, Mask, Roi, Segmentation};
1514 let bbox = BBox {
1515 x: 0.25,
1516 y: 0.5,
1517 w: 0.1,
1518 h: 0.2,
1519 };
1520 let mask = Mask::new(3, 2, 4, alloc::vec![10, 20, 30, 0, 40, 50, 60, 0])
1522 .expect("mask fits its data");
1523 let mut analytics = AnalyticsMeta::new();
1524 analytics.push(AnalyticsNode::Segmentation(Segmentation {
1525 bbox,
1526 label: 3,
1527 confidence: 0.75,
1528 mask,
1529 }));
1530 analytics.push(AnalyticsNode::Roi(Roi {
1531 bbox,
1532 id: 9,
1533 label: 4,
1534 }));
1535
1536 let mut meta = FrameMetaSet::new();
1537 meta.attach(analytics.clone());
1538 let frame = Frame {
1539 domain: MemoryDomain::System(SystemSlice::from_boxed(Box::new([0u8; 4]))),
1540 timing: FrameTiming::default(),
1541 sequence: 0,
1542 meta,
1543 };
1544 match roundtrip(&PipelinePacket::DataFrame(frame)) {
1545 PipelinePacket::DataFrame(got) => {
1546 let a = got.meta.get::<AnalyticsMeta>().expect("analytics survived");
1547 assert_eq!(a.nodes, analytics.nodes);
1548 let seg = a.segmentations().next().expect("segmentation node");
1549 assert_eq!(seg.mask.sample(2, 1), Some(60));
1550 assert_eq!(seg.mask.sample(3, 0), None, "outside the mask width");
1551 assert_eq!(a.rois().next().expect("roi node").id, 9);
1552 }
1553 other => panic!("expected DataFrame, got {other:?}"),
1554 }
1555 }
1556
1557 #[cfg(feature = "metadata")]
1558 #[test]
1559 fn a_mask_whose_geometry_overruns_its_bytes_is_rejected() {
1560 use crate::meta::Mask;
1561 assert!(
1562 Mask::new(4, 4, 4, alloc::vec![0; 15]).is_none(),
1563 "short data"
1564 );
1565 assert!(
1566 Mask::new(8, 2, 4, alloc::vec![0; 64]).is_none(),
1567 "stride < width"
1568 );
1569 assert!(
1570 Mask::new(u32::MAX, u32::MAX, u32::MAX, alloc::vec![0; 8]).is_none(),
1571 "the row product must not overflow into a valid-looking size"
1572 );
1573 }
1574
1575 #[cfg(feature = "metadata")]
1576 #[test]
1577 fn caption_metadata_round_trips() {
1578 use crate::meta::{CaptionMeta, CaptionTriple};
1579 let mut captions = CaptionMeta::new();
1580 captions.push(CaptionTriple {
1581 cc_type: 0,
1582 b0: 0x94,
1583 b1: 0xAE,
1584 });
1585 captions.push(CaptionTriple {
1586 cc_type: 3,
1587 b0: 0x01,
1588 b1: 0xFF,
1589 });
1590
1591 let mut meta = FrameMetaSet::new();
1592 meta.attach(captions.clone());
1593 let frame = Frame {
1594 domain: MemoryDomain::System(SystemSlice::from_boxed(Box::new([0u8; 4]))),
1595 timing: FrameTiming::default(),
1596 sequence: 3,
1597 meta,
1598 };
1599 match roundtrip(&PipelinePacket::DataFrame(frame)) {
1600 PipelinePacket::DataFrame(got) => {
1601 let c = got.meta.get::<CaptionMeta>().expect("captions survived");
1602 assert_eq!(c, &captions);
1603 }
1604 other => panic!("expected DataFrame, got {other:?}"),
1605 }
1606 }
1607
1608 #[cfg(feature = "metadata")]
1609 #[test]
1610 fn hdr_static_metadata_round_trips() {
1611 use crate::meta::{Chromaticity, HdrStaticMeta, MasteringDisplay};
1612 let xy = |x, y| Chromaticity { x, y };
1613 let hdr = HdrStaticMeta {
1614 mastering: Some(MasteringDisplay {
1615 display_primaries: [xy(0.708, 0.292), xy(0.170, 0.797), xy(0.131, 0.046)],
1616 white_point: xy(0.3127, 0.3290),
1617 max_luminance: 1000.0,
1618 min_luminance: 0.005,
1619 }),
1620 max_content_light_level: Some(1200),
1621 max_frame_average_light_level: Some(300),
1622 };
1623
1624 let mut meta = FrameMetaSet::new();
1625 meta.attach(hdr);
1626 let frame = Frame {
1627 domain: MemoryDomain::System(SystemSlice::from_boxed(Box::new([0u8; 4]))),
1628 timing: FrameTiming::default(),
1629 sequence: 0,
1630 meta,
1631 };
1632 match roundtrip(&PipelinePacket::DataFrame(frame)) {
1633 PipelinePacket::DataFrame(got) => {
1634 let h = got.meta.get::<HdrStaticMeta>().expect("hdr survived");
1635 assert_eq!(h, &hdr);
1636 }
1637 other => panic!("expected DataFrame, got {other:?}"),
1638 }
1639 }
1640
1641 #[cfg(feature = "metadata")]
1642 #[test]
1643 fn hdr_static_metadata_round_trips_without_a_mastering_display() {
1644 use crate::meta::HdrStaticMeta;
1647 let hdr = HdrStaticMeta {
1648 mastering: None,
1649 max_content_light_level: Some(400),
1650 max_frame_average_light_level: None,
1651 };
1652 let mut meta = FrameMetaSet::new();
1653 meta.attach(hdr);
1654 let frame = Frame {
1655 domain: MemoryDomain::System(SystemSlice::from_boxed(Box::new([0u8; 4]))),
1656 timing: FrameTiming::default(),
1657 sequence: 0,
1658 meta,
1659 };
1660 match roundtrip(&PipelinePacket::DataFrame(frame)) {
1661 PipelinePacket::DataFrame(got) => {
1662 assert_eq!(got.meta.get::<HdrStaticMeta>(), Some(&hdr));
1663 }
1664 other => panic!("expected DataFrame, got {other:?}"),
1665 }
1666 }
1667
1668 #[cfg(feature = "metadata")]
1669 #[test]
1670 fn timecode_metadata_round_trips() {
1671 use crate::meta::TimecodeMeta;
1672 let tc = TimecodeMeta {
1673 hours: 10,
1674 minutes: 59,
1675 seconds: 58,
1676 frames: 29,
1677 drop_frame: true,
1678 framerate_q16: Some(1_965_691), };
1680 let mut meta = FrameMetaSet::new();
1681 meta.attach(tc);
1682 let frame = Frame {
1683 domain: MemoryDomain::System(SystemSlice::from_boxed(Box::new([0u8; 4]))),
1684 timing: FrameTiming::default(),
1685 sequence: 0,
1686 meta,
1687 };
1688 match roundtrip(&PipelinePacket::DataFrame(frame)) {
1689 PipelinePacket::DataFrame(got) => {
1690 assert_eq!(got.meta.get::<TimecodeMeta>(), Some(&tc));
1691 }
1692 other => panic!("expected DataFrame, got {other:?}"),
1693 }
1694 }
1695}