Skip to main content

oximedia_transcode/
codec_dispatch.rs

1// Copyright 2025 OxiMedia Contributors
2// Licensed under the Apache License, Version 2.0
3
4//! Codec encoder factory for MJPEG and APV intra-frame codecs.
5//!
6//! This module provides [`make_video_encoder`], which constructs a boxed
7//! [`oximedia_codec::VideoEncoder`] for a given [`CodecId`].  Currently
8//! dispatched codecs are:
9//!
10//! | `CodecId`       | Encoder              | Feature gate |
11//! |-----------------|----------------------|--------------|
12//! | `CodecId::Mjpeg`| `MjpegEncoder`       | `mjpeg`      |
13//! | `CodecId::Apv`  | `ApvEncoder`         | `apv`        |
14//!
15//! Callers that need other codecs (AV1, VP9, …) use the existing
16//! stream-copy path in `frame_pipeline`.
17
18use crate::{Result, TranscodeError};
19use oximedia_codec::traits::VideoEncoder;
20#[cfg(feature = "mjpeg")]
21use oximedia_codec::CodecError;
22use oximedia_core::CodecId;
23
24/// Parameters used to instantiate an intra-frame video encoder.
25#[derive(Debug, Clone)]
26pub struct VideoEncoderParams {
27    /// Frame width in pixels (must be > 0).
28    pub width: u32,
29    /// Frame height in pixels (must be > 0).
30    pub height: u32,
31    /// Quality/QP value.  Interpretation depends on the codec:
32    /// - MJPEG: JPEG quality 1-100 (higher = better).
33    /// - APV: quantisation parameter 0-63 (lower = better).
34    pub quality: u8,
35}
36
37impl VideoEncoderParams {
38    /// Create a new parameter set.
39    ///
40    /// # Errors
41    ///
42    /// Returns [`TranscodeError::InvalidInput`] if width or height is zero.
43    pub fn new(width: u32, height: u32, quality: u8) -> Result<Self> {
44        if width == 0 || height == 0 {
45            return Err(TranscodeError::InvalidInput(
46                "width and height must be non-zero".into(),
47            ));
48        }
49        Ok(Self {
50            width,
51            height,
52            quality,
53        })
54    }
55}
56
57/// Build a boxed [`VideoEncoder`] for the specified codec.
58///
59/// # Errors
60///
61/// - [`TranscodeError::Unsupported`] if `codec_id` is not MJPEG or APV.
62/// - [`TranscodeError::CodecError`] if the underlying encoder rejects the
63///   parameters.
64pub fn make_video_encoder(
65    codec_id: CodecId,
66    params: &VideoEncoderParams,
67) -> Result<Box<dyn VideoEncoder>> {
68    match codec_id {
69        CodecId::Mjpeg => make_mjpeg_encoder(params),
70        CodecId::Apv => make_apv_encoder(params),
71        other => Err(TranscodeError::Unsupported(format!(
72            "codec {other:?} is not handled by codec_dispatch; \
73             use the stream-copy pipeline for this codec"
74        ))),
75    }
76}
77
78// ─── MJPEG ───────────────────────────────────────────────────────────────────
79
80#[cfg(feature = "mjpeg")]
81fn make_mjpeg_encoder(params: &VideoEncoderParams) -> Result<Box<dyn VideoEncoder>> {
82    use oximedia_codec::{MjpegConfig, MjpegEncoder};
83
84    let config = MjpegConfig::new(params.width, params.height)
85        .map_err(|e| TranscodeError::CodecError(e.to_string()))?
86        .with_quality(params.quality);
87
88    let encoder = MjpegEncoder::new(config)
89        .map_err(|e: CodecError| TranscodeError::CodecError(e.to_string()))?;
90
91    Ok(Box::new(encoder))
92}
93
94#[cfg(not(feature = "mjpeg"))]
95fn make_mjpeg_encoder(_params: &VideoEncoderParams) -> Result<Box<dyn VideoEncoder>> {
96    Err(TranscodeError::Unsupported(
97        "MJPEG support requires the `mjpeg` feature of oximedia-codec".into(),
98    ))
99}
100
101// ─── APV ─────────────────────────────────────────────────────────────────────
102
103#[cfg(feature = "apv")]
104fn make_apv_encoder(params: &VideoEncoderParams) -> Result<Box<dyn VideoEncoder>> {
105    use oximedia_codec::{ApvConfig, ApvEncoder};
106
107    let config = ApvConfig::new(params.width, params.height)
108        .map_err(|e| TranscodeError::CodecError(e.to_string()))?
109        .with_qp(params.quality);
110
111    let encoder = ApvEncoder::new(config).map_err(|e| TranscodeError::CodecError(e.to_string()))?;
112
113    Ok(Box::new(encoder))
114}
115
116#[cfg(not(feature = "apv"))]
117fn make_apv_encoder(_params: &VideoEncoderParams) -> Result<Box<dyn VideoEncoder>> {
118    Err(TranscodeError::Unsupported(
119        "APV support requires the `apv` feature of oximedia-codec".into(),
120    ))
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_params_new_valid() {
129        let p = VideoEncoderParams::new(1920, 1080, 85);
130        assert!(p.is_ok());
131        let p = p.expect("valid params");
132        assert_eq!(p.width, 1920);
133        assert_eq!(p.height, 1080);
134        assert_eq!(p.quality, 85);
135    }
136
137    #[test]
138    fn test_params_zero_width() {
139        assert!(VideoEncoderParams::new(0, 1080, 85).is_err());
140    }
141
142    #[test]
143    fn test_params_zero_height() {
144        assert!(VideoEncoderParams::new(1920, 0, 85).is_err());
145    }
146
147    #[test]
148    fn test_unsupported_codec() {
149        let p = VideoEncoderParams::new(320, 240, 30).expect("valid");
150        let result = make_video_encoder(CodecId::Vp9, &p);
151        assert!(result.is_err());
152        // Extract the error without requiring Debug on the Ok variant.
153        if let Err(e) = result {
154            assert!(matches!(e, TranscodeError::Unsupported(_)));
155        }
156    }
157
158    #[cfg(feature = "mjpeg")]
159    #[test]
160    fn test_make_mjpeg_encoder() {
161        let p = VideoEncoderParams::new(320, 240, 85).expect("valid");
162        let enc = make_video_encoder(CodecId::Mjpeg, &p);
163        assert!(enc.is_ok(), "MJPEG encoder should build");
164        let enc = enc.expect("ok");
165        assert_eq!(enc.codec(), CodecId::Mjpeg);
166    }
167
168    #[cfg(feature = "apv")]
169    #[test]
170    fn test_make_apv_encoder() {
171        let p = VideoEncoderParams::new(320, 240, 22).expect("valid");
172        let enc = make_video_encoder(CodecId::Apv, &p);
173        assert!(enc.is_ok(), "APV encoder should build");
174        let enc = enc.expect("ok");
175        assert_eq!(enc.codec(), CodecId::Apv);
176    }
177
178    #[cfg(not(feature = "mjpeg"))]
179    #[test]
180    fn test_mjpeg_disabled() {
181        let p = VideoEncoderParams::new(320, 240, 85).expect("valid");
182        let result = make_video_encoder(CodecId::Mjpeg, &p);
183        assert!(matches!(result, Err(TranscodeError::Unsupported(_))));
184    }
185
186    #[cfg(not(feature = "apv"))]
187    #[test]
188    fn test_apv_disabled() {
189        let p = VideoEncoderParams::new(320, 240, 22).expect("valid");
190        let result = make_video_encoder(CodecId::Apv, &p);
191        assert!(matches!(result, Err(TranscodeError::Unsupported(_))));
192    }
193}