1#![allow(clippy::needless_range_loop)]
3
4pub mod alpha;
76pub mod bitstream;
77pub mod dct;
78pub mod decoder;
79pub mod encoder;
80pub mod entropy;
81pub mod frame;
82pub mod quant;
83pub mod slice;
84
85use oxideav_core::{CodecCapabilities, CodecId, CodecTag, PixelFormat};
86use oxideav_core::{CodecInfo, CodecRegistry, RuntimeContext};
87
88pub const CODEC_ID_STR: &str = "prores";
90
91pub const PRORES_FOURCCS: [&[u8; 4]; 6] = [b"apco", b"apcs", b"apcn", b"apch", b"ap4h", b"ap4x"];
104
105pub const PRORES_RAW_FOURCCS: [&[u8; 4]; 2] = [b"aprn", b"aprh"];
124
125pub fn is_prores_raw_fourcc(fourcc: &[u8; 4]) -> bool {
133 let mut upper = [0u8; 4];
134 for i in 0..4 {
135 upper[i] = fourcc[i].to_ascii_uppercase();
136 }
137 matches!(&upper, b"APRN" | b"APRH")
138}
139
140pub fn codec_id_for_fourcc(fourcc: &[u8; 4]) -> Option<CodecId> {
143 let mut upper = [0u8; 4];
144 for i in 0..4 {
145 upper[i] = fourcc[i].to_ascii_uppercase();
146 }
147 match &upper {
148 b"APCO" | b"APCS" | b"APCN" | b"APCH" | b"AP4H" | b"AP4X" => {
149 Some(CodecId::new(CODEC_ID_STR))
150 }
151 _ => None,
152 }
153}
154
155pub fn profile_for_fourcc(fourcc: &[u8; 4]) -> Option<frame::Profile> {
157 let mut upper = [0u8; 4];
158 for i in 0..4 {
159 upper[i] = fourcc[i].to_ascii_uppercase();
160 }
161 Some(match &upper {
162 b"APCO" => frame::Profile::Proxy,
163 b"APCS" => frame::Profile::Lt,
164 b"APCN" => frame::Profile::Standard,
165 b"APCH" => frame::Profile::Hq,
166 b"AP4H" => frame::Profile::Prores4444,
167 b"AP4X" => frame::Profile::Prores4444Xq,
168 _ => return None,
169 })
170}
171
172pub fn fourcc_for_profile(profile: frame::Profile) -> &'static [u8; 4] {
189 profile.fourcc()
190}
191
192pub fn register_codecs(reg: &mut CodecRegistry) {
195 let caps = CodecCapabilities::video("prores_sw")
196 .with_lossy(true)
197 .with_intra_only(true)
198 .with_pixel_format(PixelFormat::Yuv422P)
199 .with_pixel_format(PixelFormat::Yuv444P)
200 .with_pixel_format(PixelFormat::Yuv422P10Le)
201 .with_pixel_format(PixelFormat::Yuv444P10Le)
202 .with_pixel_format(PixelFormat::Yuv422P12Le)
203 .with_pixel_format(PixelFormat::Yuv444P12Le)
204 .with_pixel_format(PixelFormat::Yuv422P16Le)
205 .with_pixel_format(PixelFormat::Yuv444P16Le)
206 .with_pixel_format(PixelFormat::Yuva422P)
207 .with_pixel_format(PixelFormat::Yuva444P)
208 .with_pixel_format(PixelFormat::Yuva422P10Le)
209 .with_pixel_format(PixelFormat::Yuva444P10Le)
210 .with_pixel_format(PixelFormat::Yuva422P12Le)
211 .with_pixel_format(PixelFormat::Yuva444P12Le)
212 .with_pixel_format(PixelFormat::Yuva422P16Le)
213 .with_pixel_format(PixelFormat::Yuva444P16Le);
214 reg.register(
215 CodecInfo::new(CodecId::new(CODEC_ID_STR))
216 .capabilities(caps)
217 .decoder(decoder::make_decoder)
218 .encoder(encoder::make_encoder)
219 .tags([
220 CodecTag::fourcc(b"APCO"),
221 CodecTag::fourcc(b"APCS"),
222 CodecTag::fourcc(b"APCN"),
223 CodecTag::fourcc(b"APCH"),
224 CodecTag::fourcc(b"AP4H"),
225 CodecTag::fourcc(b"AP4X"),
226 ]),
227 );
228}
229
230pub fn register(ctx: &mut RuntimeContext) {
237 register_codecs(&mut ctx.codecs);
238}
239
240oxideav_core::register!("prores", register);
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use oxideav_core::frame::VideoPlane;
246 use oxideav_core::{CodecId, CodecParameters, Frame, MediaType, PixelFormat, VideoFrame};
247
248 fn synthetic_gradient(width: u32, height: u32) -> VideoFrame {
252 let w = width as usize;
253 let h = height as usize;
254 let cw = w / 2;
255 let mut y = vec![0u8; w * h];
256 let mut cb = vec![0u8; cw * h];
257 let mut cr = vec![0u8; cw * h];
258 for j in 0..h {
259 for i in 0..w {
260 y[j * w + i] = ((i + j) * 255 / (w + h)).min(255) as u8;
261 }
262 }
263 for j in 0..h {
264 for i in 0..cw {
265 cb[j * cw + i] = (128 + ((i as i32 - cw as i32 / 2) * 2).clamp(-64, 64)) as u8;
266 cr[j * cw + i] = (128 + ((j as i32 - h as i32 / 2) * 2).clamp(-64, 64)) as u8;
267 }
268 }
269 let _ = width;
270 let _ = height;
271 VideoFrame {
272 pts: Some(0),
273 planes: vec![
274 VideoPlane { stride: w, data: y },
275 VideoPlane {
276 stride: cw,
277 data: cb,
278 },
279 VideoPlane {
280 stride: cw,
281 data: cr,
282 },
283 ],
284 }
285 }
286
287 fn psnr(orig: &[u8], decoded: &[u8]) -> f64 {
288 assert_eq!(orig.len(), decoded.len());
289 let mut mse = 0.0f64;
290 for (a, b) in orig.iter().zip(decoded.iter()) {
291 let d = *a as f64 - *b as f64;
292 mse += d * d;
293 }
294 mse /= orig.len() as f64;
295 if mse == 0.0 {
296 return 120.0;
297 }
298 10.0 * (255.0f64 * 255.0 / mse).log10()
299 }
300
301 #[test]
302 fn rdd36_encoder_decoder_roundtrip_psnr() {
303 let width = 64u32;
304 let height = 48u32;
305 let original = synthetic_gradient(width, height);
306
307 let mut enc_params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
308 enc_params.media_type = MediaType::Video;
309 enc_params.width = Some(width);
310 enc_params.height = Some(height);
311 enc_params.pixel_format = Some(PixelFormat::Yuv422P);
312
313 let mut reg = oxideav_core::CodecRegistry::new();
314 register_codecs(&mut reg);
315 let mut encoder = reg.first_encoder(&enc_params).expect("make_encoder");
316 encoder
317 .send_frame(&Frame::Video(original.clone()))
318 .expect("send_frame");
319 let pkt = encoder.receive_packet().expect("receive_packet");
320
321 let dec_params = enc_params.clone();
322 let mut decoder = reg.first_decoder(&dec_params).expect("make_decoder");
323 decoder.send_packet(&pkt).expect("send_packet");
324 let frame = decoder.receive_frame().expect("receive_frame");
325 let decoded = match frame {
326 Frame::Video(v) => v,
327 _ => panic!("expected video frame"),
328 };
329
330 assert_eq!(decoded.planes.len(), 3);
331 for (i, (o, d)) in original
332 .planes
333 .iter()
334 .zip(decoded.planes.iter())
335 .enumerate()
336 {
337 assert_eq!(o.data.len(), d.data.len(), "plane {i} size mismatch");
338 let p = psnr(&o.data, &d.data);
339 assert!(p > 30.0, "plane {i} PSNR too low: {p:.2} dB (want > 30)");
340 eprintln!("plane {i} PSNR = {p:.2} dB");
341 }
342 }
343
344 #[test]
345 fn registry_caps_advertise_every_supported_pixel_format() {
346 let mut reg = oxideav_core::CodecRegistry::new();
351 register_codecs(&mut reg);
352 let impls = reg.implementations(&CodecId::new(CODEC_ID_STR));
353 assert_eq!(impls.len(), 1);
354 let advertised = &impls[0].caps.accepted_pixel_formats;
355 let expected = [
356 PixelFormat::Yuv422P,
357 PixelFormat::Yuv444P,
358 PixelFormat::Yuv422P10Le,
359 PixelFormat::Yuv444P10Le,
360 PixelFormat::Yuv422P12Le,
361 PixelFormat::Yuv444P12Le,
362 PixelFormat::Yuv422P16Le,
363 PixelFormat::Yuv444P16Le,
364 PixelFormat::Yuva422P,
365 PixelFormat::Yuva444P,
366 PixelFormat::Yuva422P10Le,
367 PixelFormat::Yuva444P10Le,
368 PixelFormat::Yuva422P12Le,
369 PixelFormat::Yuva444P12Le,
370 PixelFormat::Yuva422P16Le,
371 PixelFormat::Yuva444P16Le,
372 ];
373 assert_eq!(advertised.len(), expected.len());
374 for pf in expected {
375 assert!(
376 advertised.contains(&pf),
377 "capabilities must advertise {pf:?}"
378 );
379 let mut p = CodecParameters::video(CodecId::new(CODEC_ID_STR));
381 p.media_type = MediaType::Video;
382 p.width = Some(64);
383 p.height = Some(48);
384 p.pixel_format = Some(pf);
385 assert!(reg.first_decoder(&p).is_ok(), "decoder for {pf:?}");
386 assert!(reg.first_encoder(&p).is_ok(), "encoder for {pf:?}");
387 }
388 }
389
390 #[test]
391 fn decoder_registered() {
392 let mut reg = oxideav_core::CodecRegistry::new();
393 register_codecs(&mut reg);
394 assert!(reg.has_decoder(&CodecId::new(CODEC_ID_STR)));
395 assert!(reg.has_encoder(&CodecId::new(CODEC_ID_STR)));
396 }
397
398 #[test]
399 fn register_via_runtime_context_installs_codec_factory() {
400 let mut ctx = oxideav_core::RuntimeContext::new();
401 register(&mut ctx);
402 assert!(ctx.codecs.has_decoder(&CodecId::new(CODEC_ID_STR)));
403 assert!(ctx.codecs.has_encoder(&CodecId::new(CODEC_ID_STR)));
404 }
405
406 #[test]
407 fn codec_id_for_fourcc_maps_all_six() {
408 for fc in PRORES_FOURCCS {
409 assert_eq!(codec_id_for_fourcc(fc), Some(CodecId::new(CODEC_ID_STR)));
410 }
411 }
412
413 #[test]
414 fn codec_id_for_fourcc_is_case_insensitive() {
415 assert_eq!(
416 codec_id_for_fourcc(b"APCH"),
417 Some(CodecId::new(CODEC_ID_STR))
418 );
419 assert_eq!(
420 codec_id_for_fourcc(b"apch"),
421 Some(CodecId::new(CODEC_ID_STR))
422 );
423 assert_eq!(
424 codec_id_for_fourcc(b"ApCh"),
425 Some(CodecId::new(CODEC_ID_STR))
426 );
427 }
428
429 #[test]
430 fn codec_id_for_fourcc_rejects_non_prores() {
431 assert_eq!(codec_id_for_fourcc(b"avc1"), None);
432 assert_eq!(codec_id_for_fourcc(b"hvc1"), None);
433 assert_eq!(codec_id_for_fourcc(b"mp4v"), None);
434 assert_eq!(codec_id_for_fourcc(b"alac"), None);
435 assert_eq!(codec_id_for_fourcc(b"av01"), None);
436 }
437
438 #[test]
439 fn prores_raw_fourcc_does_not_resolve_to_standard_prores() {
440 for fc in PRORES_RAW_FOURCCS {
444 assert_eq!(codec_id_for_fourcc(fc), None, "raw fourcc {fc:?}");
445 assert_eq!(profile_for_fourcc(fc), None, "raw fourcc {fc:?}");
446 }
447 }
448
449 #[test]
450 fn is_prores_raw_fourcc_detects_aprn_aprh_case_insensitive() {
451 for fc in PRORES_RAW_FOURCCS {
452 assert!(is_prores_raw_fourcc(fc), "lower {fc:?}");
453 let mut up = *fc;
454 up.make_ascii_uppercase();
455 assert!(is_prores_raw_fourcc(&up), "upper {up:?}");
456 }
457 assert!(is_prores_raw_fourcc(b"ApRh"));
458 for fc in PRORES_FOURCCS {
460 assert!(!is_prores_raw_fourcc(fc), "standard {fc:?}");
461 }
462 assert!(!is_prores_raw_fourcc(b"avc1"));
464 assert!(!is_prores_raw_fourcc(b"av01"));
465 assert!(!is_prores_raw_fourcc(b"aprx"));
468 }
469
470 #[test]
471 fn decode_packet_rejects_prores_raw_marker_with_unsupported() {
472 let mut raw = Vec::new();
476 raw.extend_from_slice(&16u32.to_be_bytes()); raw.extend_from_slice(b"aprh"); raw.extend_from_slice(&[0u8; 8]); let err = decoder::decode_packet(&raw, None).expect_err("must reject ProRes RAW");
480 let msg = err.to_string();
481 assert!(
482 msg.contains("ProRes RAW"),
483 "error should name ProRes RAW, got: {msg}"
484 );
485
486 let mut other = Vec::new();
489 other.extend_from_slice(&16u32.to_be_bytes());
490 other.extend_from_slice(b"junk");
491 other.extend_from_slice(&[0u8; 8]);
492 let err2 = decoder::decode_packet(&other, None).expect_err("must reject non-ProRes");
493 assert!(
494 !err2.to_string().contains("ProRes RAW"),
495 "non-ProRes bytes should not be reported as ProRes RAW"
496 );
497 }
498
499 #[test]
500 fn profile_for_fourcc_roundtrips_via_profile_fourcc() {
501 for p in [
502 frame::Profile::Proxy,
503 frame::Profile::Lt,
504 frame::Profile::Standard,
505 frame::Profile::Hq,
506 frame::Profile::Prores4444,
507 frame::Profile::Prores4444Xq,
508 ] {
509 let fc = p.fourcc();
510 assert_eq!(profile_for_fourcc(fc), Some(p), "fourcc roundtrip");
511 let mut up = *fc;
512 up.make_ascii_uppercase();
513 assert_eq!(profile_for_fourcc(&up), Some(p));
514 }
515 assert_eq!(profile_for_fourcc(b"mp4v"), None);
516 }
517
518 #[test]
519 fn fourcc_for_profile_inverts_profile_for_fourcc() {
520 for p in [
523 frame::Profile::Proxy,
524 frame::Profile::Lt,
525 frame::Profile::Standard,
526 frame::Profile::Hq,
527 frame::Profile::Prores4444,
528 frame::Profile::Prores4444Xq,
529 ] {
530 let fc = fourcc_for_profile(p);
531 assert_eq!(fc, p.fourcc(), "canonical fourcc for {p:?}");
534 assert!(
535 PRORES_FOURCCS.contains(&fc),
536 "{fc:?} must be one of the six carriage FourCCs"
537 );
538 assert_eq!(
539 profile_for_fourcc(fc),
540 Some(p),
541 "fourcc_for_profile -> profile_for_fourcc round-trip for {p:?}"
542 );
543 }
544 }
545
546 #[test]
547 fn fourcc_for_profile_returns_lowercase_canonical_bytes() {
548 assert_eq!(fourcc_for_profile(frame::Profile::Proxy), b"apco");
551 assert_eq!(fourcc_for_profile(frame::Profile::Lt), b"apcs");
552 assert_eq!(fourcc_for_profile(frame::Profile::Standard), b"apcn");
553 assert_eq!(fourcc_for_profile(frame::Profile::Hq), b"apch");
554 assert_eq!(fourcc_for_profile(frame::Profile::Prores4444), b"ap4h");
555 assert_eq!(fourcc_for_profile(frame::Profile::Prores4444Xq), b"ap4x");
556 }
557
558 #[test]
559 fn registry_recognizes_prores_fourcc_tags() {
560 use oxideav_core::stream::{CodecResolver, ProbeContext};
561 use oxideav_core::CodecTag;
562 let mut reg = oxideav_core::CodecRegistry::new();
563 register_codecs(&mut reg);
564 for fc in PRORES_FOURCCS {
565 let tag = CodecTag::fourcc(fc);
566 let ctx = ProbeContext::new(&tag);
567 let id = reg.resolve_tag(&ctx).expect("resolve_tag");
568 assert_eq!(id, CodecId::new(CODEC_ID_STR), "fourcc {fc:?}");
569 }
570 }
571
572 fn synthetic_gradient_444(width: u32, height: u32) -> VideoFrame {
573 let w = width as usize;
574 let h = height as usize;
575 let mut y = vec![0u8; w * h];
576 let mut cb = vec![0u8; w * h];
577 let mut cr = vec![0u8; w * h];
578 for j in 0..h {
579 for i in 0..w {
580 y[j * w + i] = ((i + j) * 255 / (w + h)).min(255) as u8;
581 cb[j * w + i] = (128 + ((i as i32 - w as i32 / 2) * 2).clamp(-64, 64)) as u8;
582 cr[j * w + i] = (128 + ((j as i32 - h as i32 / 2) * 2).clamp(-64, 64)) as u8;
583 }
584 }
585 VideoFrame {
586 pts: Some(0),
587 planes: vec![
588 VideoPlane { stride: w, data: y },
589 VideoPlane {
590 stride: w,
591 data: cb,
592 },
593 VideoPlane {
594 stride: w,
595 data: cr,
596 },
597 ],
598 }
599 }
600
601 #[test]
602 fn rdd36_encoder_decoder_roundtrip_psnr_4444() {
603 let width = 64u32;
604 let height = 48u32;
605 let original = synthetic_gradient_444(width, height);
606
607 let mut enc_params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
608 enc_params.media_type = MediaType::Video;
609 enc_params.width = Some(width);
610 enc_params.height = Some(height);
611 enc_params.pixel_format = Some(PixelFormat::Yuv444P);
612
613 let mut reg = oxideav_core::CodecRegistry::new();
614 register_codecs(&mut reg);
615 let mut encoder = reg.first_encoder(&enc_params).expect("make_encoder");
616 encoder
617 .send_frame(&Frame::Video(original.clone()))
618 .expect("send_frame");
619 let pkt = encoder.receive_packet().expect("receive_packet");
620
621 let dec_params = enc_params.clone();
622 let mut decoder = reg.first_decoder(&dec_params).expect("make_decoder");
623 decoder.send_packet(&pkt).expect("send_packet");
624 let frame = decoder.receive_frame().expect("receive_frame");
625 let decoded = match frame {
626 Frame::Video(v) => v,
627 _ => panic!("expected video frame"),
628 };
629
630 assert_eq!(decoded.planes.len(), 3);
631 for (i, (o, d)) in original
632 .planes
633 .iter()
634 .zip(decoded.planes.iter())
635 .enumerate()
636 {
637 assert_eq!(o.data.len(), d.data.len(), "plane {i} size mismatch");
638 let p = psnr(&o.data, &d.data);
639 assert!(
640 p > 30.0,
641 "4444 plane {i} PSNR too low: {p:.2} dB (want > 30)"
642 );
643 eprintln!("4444 plane {i} PSNR = {p:.2} dB");
644 }
645 }
646}