1use oxideav_core::{
57 CodecId, CodecParameters, CodecResolver, Error, MediaType, Packet, PixelFormat, Rational,
58 Result, StreamInfo, TimeBase,
59};
60use oxideav_core::{ContainerRegistry, Demuxer, Muxer, ProbeData, ReadSeek, WriteSeek};
61use std::io::{Read, Write};
62
63pub(crate) const MAGIC: &[u8; 10] = b"YUV4MPEG2 ";
65
66pub fn register(reg: &mut ContainerRegistry) {
67 reg.register_demuxer("y4m", open_demuxer);
68 reg.register_muxer("y4m", open_muxer);
69 reg.register_extension("y4m", "y4m");
70 reg.register_extension("yuv4mpeg", "y4m");
71 reg.register_probe("y4m", probe);
72}
73
74fn probe(p: &ProbeData) -> u8 {
76 if p.buf.len() < MAGIC.len() {
77 return 0;
78 }
79 if &p.buf[..MAGIC.len()] == MAGIC.as_slice() {
80 100
81 } else {
82 0
83 }
84}
85
86#[derive(Clone, Debug)]
90struct Y4mHeader {
91 width: u32,
92 height: u32,
93 frame_rate: Rational,
94 pixel_aspect: Rational,
95 interlace: u8,
96 colorspace: String,
97 x_params: Vec<(String, String)>,
100}
101
102fn pixel_format_for_colorspace(c: &str) -> Option<(PixelFormat, u32, u32, u32)> {
106 match c {
108 "420" | "420jpeg" | "420mpeg2" | "420paldv" => Some((PixelFormat::Yuv420P, 1, 1, 1)),
113 "422" => Some((PixelFormat::Yuv422P, 1, 0, 1)),
114 "444" => Some((PixelFormat::Yuv444P, 0, 0, 1)),
115 "mono" => Some((PixelFormat::Gray8, 0, 0, 1)),
116
117 "420p10" => Some((PixelFormat::Yuv420P10Le, 1, 1, 2)),
119 "422p10" => Some((PixelFormat::Yuv422P10Le, 1, 0, 2)),
120 "444p10" => Some((PixelFormat::Yuv444P10Le, 0, 0, 2)),
121 "420p12" => Some((PixelFormat::Yuv420P12Le, 1, 1, 2)),
122 "422p12" => Some((PixelFormat::Yuv422P12Le, 1, 0, 2)),
123 "444p12" => Some((PixelFormat::Yuv444P12Le, 0, 0, 2)),
124 "monop10" | "monop12" | "monop16" => Some((PixelFormat::Gray16Le, 0, 0, 2)),
125 _ => None,
126 }
127}
128
129fn colorspace_for_pixel_format(pf: PixelFormat) -> Result<&'static str> {
134 Ok(match pf {
135 PixelFormat::Yuv420P => "420mpeg2",
136 PixelFormat::YuvJ420P => "420jpeg",
137 PixelFormat::Yuv422P | PixelFormat::YuvJ422P => "422",
138 PixelFormat::Yuv444P | PixelFormat::YuvJ444P => "444",
139 PixelFormat::Gray8 => "mono",
140 PixelFormat::Yuv420P10Le => "420p10",
141 PixelFormat::Yuv422P10Le => "422p10",
142 PixelFormat::Yuv444P10Le => "444p10",
143 PixelFormat::Yuv420P12Le => "420p12",
144 PixelFormat::Yuv422P12Le => "422p12",
145 PixelFormat::Yuv444P12Le => "444p12",
146 PixelFormat::Gray16Le => "monop16",
147 other => {
148 return Err(Error::unsupported(format!(
149 "y4m muxer: pixel format {:?} cannot be expressed as a Y4M colorspace",
150 other
151 )));
152 }
153 })
154}
155
156fn read_line(input: &mut dyn ReadSeek, cap: usize) -> Result<Vec<u8>> {
161 let mut buf = Vec::with_capacity(64);
162 let mut byte = [0u8; 1];
163 loop {
164 match input.read(&mut byte) {
165 Ok(0) => {
166 if buf.is_empty() {
167 return Err(Error::Eof);
168 }
169 return Err(Error::invalid("y4m: unterminated header/FRAME line"));
170 }
171 Ok(_) => {}
172 Err(e) => return Err(Error::from(e)),
173 }
174 if byte[0] == b'\n' {
175 return Ok(buf);
176 }
177 if buf.len() >= cap {
178 return Err(Error::invalid("y4m: header/FRAME line exceeded sanity cap"));
179 }
180 buf.push(byte[0]);
181 }
182}
183
184fn parse_ratio(s: &str, ctx: &str) -> Result<Rational> {
187 let mut it = s.splitn(2, ':');
188 let num = it
189 .next()
190 .ok_or_else(|| Error::invalid(format!("y4m: malformed {ctx} '{s}'")))?
191 .parse::<i64>()
192 .map_err(|_| Error::invalid(format!("y4m: non-numeric {ctx} '{s}'")))?;
193 let den = it
194 .next()
195 .ok_or_else(|| Error::invalid(format!("y4m: malformed {ctx} '{s}'")))?
196 .parse::<i64>()
197 .map_err(|_| Error::invalid(format!("y4m: non-numeric {ctx} '{s}'")))?;
198 if num < 0 || den < 0 {
199 return Err(Error::invalid(format!("y4m: negative {ctx} '{s}'")));
200 }
201 Ok(Rational::new(num, den))
202}
203
204fn parse_header(line: &[u8]) -> Result<Y4mHeader> {
207 if line.len() < MAGIC.len() || &line[..MAGIC.len()] != MAGIC.as_slice() {
208 return Err(Error::invalid("y4m: missing YUV4MPEG2 magic"));
209 }
210 let body = std::str::from_utf8(&line[MAGIC.len()..])
214 .map_err(|_| Error::invalid("y4m: non-ASCII header"))?;
215
216 let mut width: Option<u32> = None;
217 let mut height: Option<u32> = None;
218 let mut frame_rate: Option<Rational> = None;
219 let mut pixel_aspect: Option<Rational> = None;
220 let mut interlace: u8 = b'p';
221 let mut colorspace: Option<String> = None;
222 let mut x_params: Vec<(String, String)> = Vec::new();
223
224 for tok in body.split(' ') {
225 if tok.is_empty() {
226 continue;
227 }
228 let (tag, val) = (tok.as_bytes()[0], &tok[1..]);
229 match tag {
230 b'W' => {
231 width = Some(
232 val.parse()
233 .map_err(|_| Error::invalid(format!("y4m: bad width '{val}'")))?,
234 );
235 }
236 b'H' => {
237 height = Some(
238 val.parse()
239 .map_err(|_| Error::invalid(format!("y4m: bad height '{val}'")))?,
240 );
241 }
242 b'F' => frame_rate = Some(parse_ratio(val, "frame rate")?),
243 b'A' => pixel_aspect = Some(parse_ratio(val, "pixel aspect")?),
244 b'I' => {
245 interlace = *val.as_bytes().first().unwrap_or(&b'p');
246 if !matches!(interlace, b'p' | b't' | b'b' | b'm' | b'?') {
247 return Err(Error::invalid(format!(
248 "y4m: unknown interlace mode '{val}'"
249 )));
250 }
251 }
252 b'C' => colorspace = Some(val.to_string()),
253 b'X' => {
254 let (k, v) = match val.find('=') {
257 Some(i) => (&val[..i], &val[i + 1..]),
258 None => (val, ""),
259 };
260 x_params.push((format!("X{k}"), v.to_string()));
261 }
262 _ => {
263 return Err(Error::invalid(format!(
264 "y4m: unknown header tag '{}' in '{tok}'",
265 tag as char
266 )));
267 }
268 }
269 }
270
271 let width = width.ok_or_else(|| Error::invalid("y4m: header missing W (width)"))?;
272 let height = height.ok_or_else(|| Error::invalid("y4m: header missing H (height)"))?;
273 if width == 0 || height == 0 {
274 return Err(Error::invalid("y4m: zero width or height"));
275 }
276 let frame_rate = frame_rate.unwrap_or(Rational::new(25, 1));
277 let pixel_aspect = pixel_aspect.unwrap_or(Rational::new(0, 0));
278 let colorspace = colorspace.unwrap_or_else(|| "420jpeg".to_string());
280
281 Ok(Y4mHeader {
282 width,
283 height,
284 frame_rate,
285 pixel_aspect,
286 interlace,
287 colorspace,
288 x_params,
289 })
290}
291
292fn frame_size(hdr: &Y4mHeader) -> Result<usize> {
294 let (_, hss, vss, bps) = pixel_format_for_colorspace(&hdr.colorspace).ok_or_else(|| {
295 Error::unsupported(format!(
296 "y4m: colorspace '{}' not supported",
297 hdr.colorspace
298 ))
299 })?;
300 let w = hdr.width as usize;
301 let h = hdr.height as usize;
302 let bps = bps as usize;
303 let y_bytes = w * h * bps;
304 if hdr.colorspace.starts_with("mono") {
305 return Ok(y_bytes);
306 }
307 let cw = w >> hss;
308 let ch = h >> vss;
309 let chroma = cw * ch * bps;
310 Ok(y_bytes + 2 * chroma)
311}
312
313const LINE_CAP: usize = 16 * 1024;
319
320fn open_demuxer(
321 mut input: Box<dyn ReadSeek>,
322 _codecs: &dyn CodecResolver,
323) -> Result<Box<dyn Demuxer>> {
324 let line = read_line(&mut *input, LINE_CAP)?;
325 let hdr = parse_header(&line)?;
326 let pf_info = pixel_format_for_colorspace(&hdr.colorspace).ok_or_else(|| {
327 Error::unsupported(format!(
328 "y4m: colorspace '{}' not supported",
329 hdr.colorspace
330 ))
331 })?;
332 let (pix_fmt, _hss, _vss, _bps) = pf_info;
333 let fsize = frame_size(&hdr)?;
334
335 let time_base = if hdr.frame_rate.num > 0 && hdr.frame_rate.den > 0 {
337 TimeBase::new(hdr.frame_rate.den, hdr.frame_rate.num)
338 } else {
339 TimeBase::new(1, 25)
340 };
341
342 let mut params = CodecParameters::video(CodecId::new("rawvideo"));
343 params.width = Some(hdr.width);
344 params.height = Some(hdr.height);
345 params.pixel_format = Some(pix_fmt);
346 params.frame_rate = Some(hdr.frame_rate);
347
348 let mut metadata: Vec<(String, String)> = Vec::with_capacity(4 + hdr.x_params.len());
351 metadata.push(("colorspace".into(), hdr.colorspace.clone()));
352 metadata.push(("interlace".into(), (hdr.interlace as char).to_string()));
353 metadata.push((
354 "pixel_aspect".into(),
355 format!("{}:{}", hdr.pixel_aspect.num, hdr.pixel_aspect.den),
356 ));
357 metadata.push((
358 "frame_rate".into(),
359 format!("{}:{}", hdr.frame_rate.num, hdr.frame_rate.den),
360 ));
361 metadata.extend(hdr.x_params);
362
363 let stream = StreamInfo {
364 index: 0,
365 time_base,
366 duration: None,
367 start_time: Some(0),
368 params,
369 };
370
371 Ok(Box::new(Y4mDemuxer {
372 input,
373 streams: vec![stream],
374 frame_size: fsize,
375 frames_emitted: 0,
376 metadata,
377 }))
378}
379
380struct Y4mDemuxer {
381 input: Box<dyn ReadSeek>,
382 streams: Vec<StreamInfo>,
383 frame_size: usize,
384 frames_emitted: i64,
385 metadata: Vec<(String, String)>,
386}
387
388impl Demuxer for Y4mDemuxer {
389 fn format_name(&self) -> &str {
390 "y4m"
391 }
392
393 fn streams(&self) -> &[StreamInfo] {
394 &self.streams
395 }
396
397 fn next_packet(&mut self) -> Result<Packet> {
398 let line = match read_line(&mut *self.input, LINE_CAP) {
401 Ok(v) => v,
402 Err(Error::Eof) => return Err(Error::Eof),
403 Err(e) => return Err(e),
404 };
405 if line.len() < 5 || &line[..5] != b"FRAME" {
406 return Err(Error::invalid(
407 "y4m: expected 'FRAME' marker between frames",
408 ));
409 }
410 let mut buf = vec![0u8; self.frame_size];
416 self.input.read_exact(&mut buf)?;
417 let stream = &self.streams[0];
418 let pts = self.frames_emitted;
419 self.frames_emitted += 1;
420
421 let mut pkt = Packet::new(0, stream.time_base, buf);
422 pkt.pts = Some(pts);
423 pkt.dts = Some(pts);
424 pkt.duration = Some(1);
425 pkt.flags.keyframe = true;
426 Ok(pkt)
427 }
428
429 fn metadata(&self) -> &[(String, String)] {
430 &self.metadata
431 }
432}
433
434fn open_muxer(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
437 if streams.len() != 1 {
438 return Err(Error::unsupported("y4m supports exactly one video stream"));
439 }
440 let s = &streams[0];
441 if s.params.media_type != MediaType::Video {
442 return Err(Error::invalid("y4m stream must be video"));
443 }
444 let width = s
445 .params
446 .width
447 .ok_or_else(|| Error::invalid("y4m muxer: missing width"))?;
448 let height = s
449 .params
450 .height
451 .ok_or_else(|| Error::invalid("y4m muxer: missing height"))?;
452 let pix_fmt = s
453 .params
454 .pixel_format
455 .ok_or_else(|| Error::invalid("y4m muxer: missing pixel_format"))?;
456 let colorspace = colorspace_for_pixel_format(pix_fmt)?;
457 let frame_rate = s.params.frame_rate.unwrap_or(Rational::new(25, 1));
460 if frame_rate.num <= 0 || frame_rate.den <= 0 {
461 return Err(Error::invalid("y4m muxer: invalid frame_rate"));
462 }
463
464 let (_, hss, vss, bps) = pixel_format_for_colorspace(colorspace).ok_or_else(|| {
465 Error::other(format!(
466 "y4m muxer: internal colorspace lookup failed for '{colorspace}'"
467 ))
468 })?;
469 let w = width as usize;
470 let h = height as usize;
471 let bps = bps as usize;
472 let chroma = if colorspace.starts_with("mono") {
473 0
474 } else {
475 let cw = w >> hss;
476 let ch = h >> vss;
477 cw * ch * bps
478 };
479 let frame_size = w * h * bps + 2 * chroma;
480
481 Ok(Box::new(Y4mMuxer {
482 output,
483 width,
484 height,
485 frame_rate,
486 colorspace,
487 frame_size,
488 header_written: false,
489 trailer_written: false,
490 }))
491}
492
493struct Y4mMuxer {
494 output: Box<dyn WriteSeek>,
495 width: u32,
496 height: u32,
497 frame_rate: Rational,
498 colorspace: &'static str,
499 frame_size: usize,
500 header_written: bool,
501 trailer_written: bool,
502}
503
504impl Muxer for Y4mMuxer {
505 fn format_name(&self) -> &str {
506 "y4m"
507 }
508
509 fn write_header(&mut self) -> Result<()> {
510 if self.header_written {
511 return Err(Error::other("y4m header already written"));
512 }
513 let line = format!(
515 "YUV4MPEG2 W{w} H{h} F{fn_}:{fd} Ip A0:0 C{cs}\n",
516 w = self.width,
517 h = self.height,
518 fn_ = self.frame_rate.num,
519 fd = self.frame_rate.den,
520 cs = self.colorspace,
521 );
522 self.output.write_all(line.as_bytes())?;
523 self.header_written = true;
524 Ok(())
525 }
526
527 fn write_packet(&mut self, packet: &Packet) -> Result<()> {
528 if !self.header_written {
529 return Err(Error::other("y4m muxer: write_header not called"));
530 }
531 if packet.data.len() != self.frame_size {
532 return Err(Error::invalid(format!(
533 "y4m muxer: frame size mismatch (got {} bytes, expected {})",
534 packet.data.len(),
535 self.frame_size
536 )));
537 }
538 self.output.write_all(b"FRAME\n")?;
539 self.output.write_all(&packet.data)?;
540 Ok(())
541 }
542
543 fn write_trailer(&mut self) -> Result<()> {
544 if self.trailer_written {
545 return Ok(());
546 }
547 self.output.flush()?;
548 self.trailer_written = true;
549 Ok(())
550 }
551}
552
553#[cfg(test)]
556mod tests {
557 use super::*;
558 use oxideav_core::NullCodecResolver;
559
560 fn build_minimal_y4m_8x8(num_frames: usize) -> Vec<u8> {
563 let header = b"YUV4MPEG2 W8 H8 F30:1 Ip A1:1 C420mpeg2\n";
564 let frame_payload_len = 8 * 8 + 4 * 4 + 4 * 4;
565 let mut out = Vec::new();
566 out.extend_from_slice(header);
567 for f in 0..num_frames {
568 out.extend_from_slice(b"FRAME\n");
569 for i in 0..frame_payload_len {
570 out.push(((i + f * 13) & 0xff) as u8);
571 }
572 }
573 out
574 }
575
576 #[test]
577 fn probe_detects_magic() {
578 let probe_data = ProbeData {
579 buf: b"YUV4MPEG2 W16 H16 F25:1\n",
580 ext: None,
581 };
582 assert_eq!(probe(&probe_data), 100);
583 let probe_bad = ProbeData {
584 buf: b"NOTAMAGIC ",
585 ext: None,
586 };
587 assert_eq!(probe(&probe_bad), 0);
588 }
589
590 #[test]
591 fn parse_header_roundtrip_basic() {
592 let line = b"YUV4MPEG2 W64 H48 F30000:1001 Ip A1:1 C420jpeg XYSCSS=420JPEG";
593 let hdr = parse_header(line).unwrap();
594 assert_eq!(hdr.width, 64);
595 assert_eq!(hdr.height, 48);
596 assert_eq!(hdr.frame_rate, Rational::new(30000, 1001));
597 assert_eq!(hdr.pixel_aspect, Rational::new(1, 1));
598 assert_eq!(hdr.interlace, b'p');
599 assert_eq!(hdr.colorspace, "420jpeg");
600 assert_eq!(hdr.x_params.len(), 1);
601 assert_eq!(
602 hdr.x_params[0],
603 ("XYSCSS".to_string(), "420JPEG".to_string())
604 );
605 }
606
607 #[test]
608 fn parse_header_rejects_missing_dimensions() {
609 let no_w = b"YUV4MPEG2 H48 F25:1";
610 assert!(parse_header(no_w).is_err());
611 let no_h = b"YUV4MPEG2 W48 F25:1";
612 assert!(parse_header(no_h).is_err());
613 }
614
615 #[test]
616 fn frame_size_420_versus_422_versus_444() {
617 let mut hdr = Y4mHeader {
618 width: 16,
619 height: 16,
620 frame_rate: Rational::new(25, 1),
621 pixel_aspect: Rational::new(1, 1),
622 interlace: b'p',
623 colorspace: "420jpeg".to_string(),
624 x_params: Vec::new(),
625 };
626 assert_eq!(frame_size(&hdr).unwrap(), 16 * 16 + 8 * 8 + 8 * 8);
627 hdr.colorspace = "422".to_string();
628 assert_eq!(frame_size(&hdr).unwrap(), 16 * 16 + 8 * 16 + 8 * 16);
629 hdr.colorspace = "444".to_string();
630 assert_eq!(frame_size(&hdr).unwrap(), 16 * 16 * 3);
631 hdr.colorspace = "mono".to_string();
632 assert_eq!(frame_size(&hdr).unwrap(), 16 * 16);
633 hdr.colorspace = "420p10".to_string();
634 assert_eq!(frame_size(&hdr).unwrap(), (16 * 16 + 8 * 8 + 8 * 8) * 2);
635 }
636
637 #[test]
638 fn demux_minimal_y4m_two_frames() {
639 let bytes = build_minimal_y4m_8x8(2);
640 let cur = std::io::Cursor::new(bytes);
641 let mut dmx = open_demuxer(Box::new(cur), &NullCodecResolver).unwrap();
642 assert_eq!(dmx.format_name(), "y4m");
643 assert_eq!(dmx.streams().len(), 1);
644 let s = &dmx.streams()[0];
645 assert_eq!(s.params.codec_id, CodecId::new("rawvideo"));
646 assert_eq!(s.params.width, Some(8));
647 assert_eq!(s.params.height, Some(8));
648 assert_eq!(s.params.pixel_format, Some(PixelFormat::Yuv420P));
649 assert_eq!(s.params.frame_rate, Some(Rational::new(30, 1)));
650
651 let p1 = dmx.next_packet().unwrap();
652 assert_eq!(p1.pts, Some(0));
653 assert_eq!(p1.data.len(), 8 * 8 + 4 * 4 + 4 * 4);
654 let p2 = dmx.next_packet().unwrap();
655 assert_eq!(p2.pts, Some(1));
656 assert!(matches!(dmx.next_packet(), Err(Error::Eof)));
657 }
658
659 #[test]
660 fn demux_preserves_x_params_in_metadata() {
661 let header = b"YUV4MPEG2 W8 H8 F25:1 Ip A1:1 C420jpeg XCOLORRANGE=LIMITED\n";
663 let mut bytes = Vec::new();
664 bytes.extend_from_slice(header);
665 bytes.extend_from_slice(b"FRAME\n");
666 bytes.resize(bytes.len() + 8 * 8 + 4 * 4 + 4 * 4, 0u8);
667 let cur = std::io::Cursor::new(bytes);
668 let dmx = open_demuxer(Box::new(cur), &NullCodecResolver).unwrap();
669 let md = dmx.metadata();
670 assert!(md.iter().any(|(k, v)| k == "XCOLORRANGE" && v == "LIMITED"));
671 assert!(md.iter().any(|(k, v)| k == "colorspace" && v == "420jpeg"));
672 }
673
674 #[test]
675 fn round_trip_8x8_yuv420p() {
676 let frames: Vec<Vec<u8>> = (0..4)
679 .map(|f| {
680 let mut v = Vec::with_capacity(8 * 8 + 4 * 4 + 4 * 4);
681 for y in 0..8 {
682 for x in 0..8 {
683 v.push((((x + y) * 7 + f * 11) & 0xff) as u8);
684 }
685 }
686 for u in 0..4 {
687 for x in 0..4 {
688 v.push(((u * 13 + x * 19 + f * 23) & 0xff) as u8);
689 }
690 }
691 for vv in 0..4 {
692 for x in 0..4 {
693 v.push(((vv * 17 + x * 29 + f * 5) & 0xff) as u8);
694 }
695 }
696 v
697 })
698 .collect();
699
700 let mut params = CodecParameters::video(CodecId::new("rawvideo"));
701 params.width = Some(8);
702 params.height = Some(8);
703 params.pixel_format = Some(PixelFormat::Yuv420P);
704 params.frame_rate = Some(Rational::new(30, 1));
705 let stream = StreamInfo {
706 index: 0,
707 time_base: TimeBase::new(1, 30),
708 duration: None,
709 start_time: Some(0),
710 params,
711 };
712
713 let tmp = std::env::temp_dir().join("oxideav-basic-y4m-unit-420.y4m");
717 let _ = std::fs::remove_file(&tmp);
718 {
719 let f = std::fs::File::create(&tmp).unwrap();
720 let ws: Box<dyn WriteSeek> = Box::new(f);
721 let mut mux = open_muxer(ws, std::slice::from_ref(&stream)).unwrap();
722 mux.write_header().unwrap();
723 for f in &frames {
724 let pkt = Packet::new(0, stream.time_base, f.clone());
725 mux.write_packet(&pkt).unwrap();
726 }
727 mux.write_trailer().unwrap();
728 }
729
730 let muxed = std::fs::read(&tmp).unwrap();
731 assert!(muxed.starts_with(b"YUV4MPEG2 W8 H8 F30:1 Ip A0:0 C420mpeg2\n"));
734
735 let cur = std::io::Cursor::new(muxed);
737 let mut dmx = open_demuxer(Box::new(cur), &NullCodecResolver).unwrap();
738 for (i, want) in frames.iter().enumerate() {
739 let p = dmx.next_packet().unwrap();
740 assert_eq!(p.pts, Some(i as i64));
741 assert_eq!(p.data, *want, "frame {i} payload mismatch");
742 }
743 assert!(matches!(dmx.next_packet(), Err(Error::Eof)));
744 let _ = std::fs::remove_file(&tmp);
745 }
746
747 #[test]
748 fn round_trip_16x16_yuv444p() {
749 let mut frame = Vec::with_capacity(16 * 16 * 3);
750 for plane in 0..3 {
751 for y in 0..16 {
752 for x in 0..16 {
753 frame.push(((plane * 31 + y * 7 + x * 11) & 0xff) as u8);
754 }
755 }
756 }
757 let mut params = CodecParameters::video(CodecId::new("rawvideo"));
758 params.width = Some(16);
759 params.height = Some(16);
760 params.pixel_format = Some(PixelFormat::Yuv444P);
761 params.frame_rate = Some(Rational::new(24, 1));
762 let stream = StreamInfo {
763 index: 0,
764 time_base: TimeBase::new(1, 24),
765 duration: None,
766 start_time: Some(0),
767 params,
768 };
769
770 let tmp = std::env::temp_dir().join("oxideav-basic-y4m-unit-444.y4m");
771 let _ = std::fs::remove_file(&tmp);
772 {
773 let f = std::fs::File::create(&tmp).unwrap();
774 let ws: Box<dyn WriteSeek> = Box::new(f);
775 let mut mux = open_muxer(ws, std::slice::from_ref(&stream)).unwrap();
776 mux.write_header().unwrap();
777 mux.write_packet(&Packet::new(0, stream.time_base, frame.clone()))
778 .unwrap();
779 mux.write_trailer().unwrap();
780 }
781 let muxed = std::fs::read(&tmp).unwrap();
782 assert!(muxed.starts_with(b"YUV4MPEG2 W16 H16 F24:1 Ip A0:0 C444\n"));
783
784 let cur = std::io::Cursor::new(muxed);
785 let mut dmx = open_demuxer(Box::new(cur), &NullCodecResolver).unwrap();
786 let p = dmx.next_packet().unwrap();
787 assert_eq!(p.data, frame);
788 let _ = std::fs::remove_file(&tmp);
789 }
790
791 #[test]
792 fn muxer_rejects_wrong_frame_size() {
793 let mut params = CodecParameters::video(CodecId::new("rawvideo"));
794 params.width = Some(8);
795 params.height = Some(8);
796 params.pixel_format = Some(PixelFormat::Yuv420P);
797 params.frame_rate = Some(Rational::new(25, 1));
798 let stream = StreamInfo {
799 index: 0,
800 time_base: TimeBase::new(1, 25),
801 duration: None,
802 start_time: Some(0),
803 params,
804 };
805 let tmp = std::env::temp_dir().join("oxideav-basic-y4m-unit-bad.y4m");
806 let _ = std::fs::remove_file(&tmp);
807 let f = std::fs::File::create(&tmp).unwrap();
808 let ws: Box<dyn WriteSeek> = Box::new(f);
809 let mut mux = open_muxer(ws, std::slice::from_ref(&stream)).unwrap();
810 mux.write_header().unwrap();
811 let pkt = Packet::new(0, stream.time_base, vec![0u8; 100]);
812 assert!(mux.write_packet(&pkt).is_err());
813 let _ = std::fs::remove_file(&tmp);
814 }
815}