- Cargo.toml: merge duplicate [target.macos.deps] sections; move shiguredo_dav1d/svt_av1/video_toolbox into single block - lib.rs: dav1d + svt_av1 modules and re-exports guarded by cfg(target_os = "macos") instead of cfg(not(android)) - factory.rs: AV1 encoder/decoder paths split into macos (svt-av1/dav1d) and linux fallback (NotInitialized); update doc comments and tests - build-linux-docker.sh: build only wzp-relay + wzp-web (drops wzp-client which pulled in shiguredo crates); fix Docker copy step; add --deploy flag + deploy_relay(); fix branch auto-detection - build-tauri-android.sh: default to release build, arm64 only Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
270 lines
9.5 KiB
Rust
270 lines
9.5 KiB
Rust
//! Video encoder/decoder factory — dispatches by [`CodecId`] with platform-aware
|
|
//! HW → SW fallback.
|
|
|
|
use wzp_proto::CodecId;
|
|
|
|
use crate::decoder::VideoDecoder;
|
|
use crate::encoder::{VideoEncoder, VideoError};
|
|
|
|
/// Create a [`VideoEncoder`] for the given codec and platform.
|
|
///
|
|
/// **Encoder dispatch:**
|
|
/// - `H264Baseline` → `VideoToolboxEncoder` (macOS) / `MediaCodecEncoder` (Android)
|
|
/// - `H265Main` → `VideoToolboxHevcEncoder` (macOS) / `MediaCodecHevcEncoder` (Android)
|
|
/// - `Av1Main` → `SvtAv1Encoder` (macOS only — SW fallback)
|
|
///
|
|
/// Non-video codecs return [`VideoError::InvalidInput`].
|
|
pub fn create_video_encoder(
|
|
codec_id: CodecId,
|
|
width: u32,
|
|
height: u32,
|
|
bitrate_bps: u32,
|
|
) -> Result<Box<dyn VideoEncoder>, VideoError> {
|
|
match codec_id {
|
|
CodecId::H264Baseline => {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
Ok(Box::new(crate::videotoolbox::VideoToolboxEncoder::new(
|
|
width,
|
|
height,
|
|
bitrate_bps,
|
|
)?))
|
|
}
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
Ok(Box::new(crate::mediacodec::MediaCodecEncoder::new(
|
|
width,
|
|
height,
|
|
bitrate_bps,
|
|
)?))
|
|
}
|
|
#[cfg(not(any(target_os = "macos", target_os = "android")))]
|
|
{
|
|
let _ = (width, height, bitrate_bps);
|
|
Err(VideoError::NotInitialized)
|
|
}
|
|
}
|
|
CodecId::H265Main => {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
Ok(Box::new(crate::videotoolbox::VideoToolboxHevcEncoder::new(
|
|
width,
|
|
height,
|
|
bitrate_bps,
|
|
)?))
|
|
}
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
Ok(Box::new(crate::mediacodec::MediaCodecHevcEncoder::new(
|
|
width,
|
|
height,
|
|
bitrate_bps,
|
|
)?))
|
|
}
|
|
#[cfg(not(any(target_os = "macos", target_os = "android")))]
|
|
{
|
|
let _ = (width, height, bitrate_bps);
|
|
Err(VideoError::NotInitialized)
|
|
}
|
|
}
|
|
CodecId::Av1Main => {
|
|
// SVT-AV1 is the universal SW fallback for non-Android targets.
|
|
// On Android, MediaCodec AV1 (`video/av01`) is the only available
|
|
// path — shiguredo_svt_av1 does not build for aarch64-linux-android.
|
|
let _ = bitrate_bps; // SvtAv1Encoder currently hard-codes bitrate
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
let _ = (width, height);
|
|
#[allow(clippy::needless_return)]
|
|
return Err(VideoError::NotInitialized);
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
Ok(Box::new(crate::svt_av1::SvtAv1Encoder::new(width, height)?))
|
|
}
|
|
#[cfg(not(any(target_os = "macos", target_os = "android")))]
|
|
{
|
|
let _ = (width, height);
|
|
Err(VideoError::NotInitialized)
|
|
}
|
|
}
|
|
_ => Err(VideoError::InvalidInput("not a video codec".into())),
|
|
}
|
|
}
|
|
|
|
/// Create a [`VideoDecoder`] for the given codec and platform.
|
|
///
|
|
/// **Decoder dispatch:**
|
|
/// - `H264Baseline` → `VideoToolboxDecoder` (macOS) / `MediaCodecDecoder` (Android)
|
|
/// - `H265Main` → `VideoToolboxHevcDecoder` (macOS) / `MediaCodecHevcDecoder` (Android)
|
|
/// - `Av1Main` → `VideoToolboxAv1Decoder` (macOS M3+) → `Dav1dDecoder` (macOS SW fallback)
|
|
///
|
|
/// Non-video codecs return [`VideoError::InvalidInput`].
|
|
pub fn create_video_decoder(
|
|
codec_id: CodecId,
|
|
width: u32,
|
|
height: u32,
|
|
) -> Result<Box<dyn VideoDecoder>, VideoError> {
|
|
match codec_id {
|
|
CodecId::H264Baseline => {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
Ok(Box::new(crate::videotoolbox::VideoToolboxDecoder::new(
|
|
width, height,
|
|
)?))
|
|
}
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
Ok(Box::new(crate::mediacodec::MediaCodecDecoder::new(
|
|
width, height,
|
|
)?))
|
|
}
|
|
#[cfg(not(any(target_os = "macos", target_os = "android")))]
|
|
{
|
|
let _ = (width, height);
|
|
Err(VideoError::NotInitialized)
|
|
}
|
|
}
|
|
CodecId::H265Main => {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
Ok(Box::new(crate::videotoolbox::VideoToolboxHevcDecoder::new(
|
|
width, height,
|
|
)?))
|
|
}
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
Ok(Box::new(crate::mediacodec::MediaCodecHevcDecoder::new(
|
|
width, height,
|
|
)?))
|
|
}
|
|
#[cfg(not(any(target_os = "macos", target_os = "android")))]
|
|
{
|
|
let _ = (width, height);
|
|
Err(VideoError::NotInitialized)
|
|
}
|
|
}
|
|
CodecId::Av1Main => {
|
|
// Try platform HW decoders first, then fall back to dav1d on
|
|
// non-Android targets. On Android, MediaCodec is the only path —
|
|
// shiguredo_dav1d does not build for aarch64-linux-android.
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
if let Ok(dec) = crate::videotoolbox::VideoToolboxAv1Decoder::new(width, height) {
|
|
return Ok(Box::new(dec));
|
|
}
|
|
}
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
return crate::mediacodec::MediaCodecAv1Decoder::new(width, height)
|
|
.map(|d| Box::new(d) as Box<dyn VideoDecoder>);
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
Ok(Box::new(crate::dav1d::Dav1dDecoder::new()?))
|
|
}
|
|
#[cfg(not(any(target_os = "macos", target_os = "android")))]
|
|
{
|
|
let _ = (width, height);
|
|
Err(VideoError::NotInitialized)
|
|
}
|
|
}
|
|
_ => Err(VideoError::InvalidInput("not a video codec".into())),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn av1_encoder_factory_creates_svt_av1() {
|
|
let enc = create_video_encoder(CodecId::Av1Main, 640, 480, 2_000_000);
|
|
#[cfg(target_os = "macos")]
|
|
assert!(enc.is_ok(), "AV1 encoder factory should succeed on macOS");
|
|
#[cfg(not(target_os = "macos"))]
|
|
assert!(
|
|
matches!(enc, Err(VideoError::NotInitialized)),
|
|
"AV1 SW encoder is unavailable on Android/Linux (no shiguredo_svt_av1)"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn av1_decoder_factory_creates_decoder() {
|
|
let dec = create_video_decoder(CodecId::Av1Main, 640, 480);
|
|
#[cfg(target_os = "macos")]
|
|
assert!(dec.is_ok(), "AV1 decoder factory should succeed on macOS (dav1d fallback)");
|
|
#[cfg(not(target_os = "macos"))]
|
|
assert!(
|
|
matches!(dec, Err(VideoError::NotInitialized)),
|
|
"AV1 decoder unavailable on Android/Linux (no shiguredo_dav1d)"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn h264_encoder_factory_not_initialized_on_non_platform() {
|
|
#[cfg(not(any(target_os = "macos", target_os = "android")))]
|
|
{
|
|
let enc = create_video_encoder(CodecId::H264Baseline, 640, 480, 2_000_000);
|
|
assert!(matches!(enc, Err(VideoError::NotInitialized)));
|
|
}
|
|
#[cfg(any(target_os = "macos", target_os = "android"))]
|
|
{
|
|
// On supported platforms the factory succeeds.
|
|
let enc = create_video_encoder(CodecId::H264Baseline, 640, 480, 2_000_000);
|
|
assert!(enc.is_ok());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn h265_encoder_factory_not_initialized_on_non_platform() {
|
|
#[cfg(not(any(target_os = "macos", target_os = "android")))]
|
|
{
|
|
let enc = create_video_encoder(CodecId::H265Main, 640, 480, 2_000_000);
|
|
assert!(matches!(enc, Err(VideoError::NotInitialized)));
|
|
}
|
|
#[cfg(any(target_os = "macos", target_os = "android"))]
|
|
{
|
|
let enc = create_video_encoder(CodecId::H265Main, 640, 480, 2_000_000);
|
|
assert!(enc.is_ok());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn h264_decoder_factory_not_initialized_on_non_platform() {
|
|
#[cfg(not(any(target_os = "macos", target_os = "android")))]
|
|
{
|
|
let dec = create_video_decoder(CodecId::H264Baseline, 640, 480);
|
|
assert!(matches!(dec, Err(VideoError::NotInitialized)));
|
|
}
|
|
#[cfg(any(target_os = "macos", target_os = "android"))]
|
|
{
|
|
let dec = create_video_decoder(CodecId::H264Baseline, 640, 480);
|
|
assert!(dec.is_ok());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn h265_decoder_factory_not_initialized_on_non_platform() {
|
|
#[cfg(not(any(target_os = "macos", target_os = "android")))]
|
|
{
|
|
let dec = create_video_decoder(CodecId::H265Main, 640, 480);
|
|
assert!(matches!(dec, Err(VideoError::NotInitialized)));
|
|
}
|
|
#[cfg(any(target_os = "macos", target_os = "android"))]
|
|
{
|
|
let dec = create_video_decoder(CodecId::H265Main, 640, 480);
|
|
assert!(dec.is_ok());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn audio_codec_rejected_by_factory() {
|
|
let enc = create_video_encoder(CodecId::Opus24k, 640, 480, 2_000_000);
|
|
assert!(matches!(enc, Err(VideoError::InvalidInput(_))));
|
|
|
|
let dec = create_video_decoder(CodecId::Opus24k, 640, 480);
|
|
assert!(matches!(dec, Err(VideoError::InvalidInput(_))));
|
|
}
|
|
}
|