use oxideav_core::{CodecId, MediaType, RuntimeContext, StreamInfo};
enum Storable {
Only(&'static [&'static str]),
AllBut(&'static [&'static str]),
}
impl Storable {
fn accepts(&self, codec: &str) -> bool {
match self {
Storable::Only(ids) => ids.contains(&codec),
Storable::AllBut(ids) => !ids.contains(&codec),
}
}
}
const DEFAULTS: &[(&str, MediaType, Storable, &str)] = &[
(
"webm",
MediaType::Video,
Storable::Only(&["vp8", "vp9", "av1"]),
"vp9",
),
(
"webm",
MediaType::Audio,
Storable::Only(&["opus", "vorbis"]),
"opus",
),
(
"matroska",
MediaType::Video,
Storable::AllBut(&["rawvideo"]),
"vp9",
),
(
"ogg",
MediaType::Audio,
Storable::Only(&["vorbis", "opus", "flac", "speex"]),
"vorbis",
),
(
"wav",
MediaType::Audio,
Storable::Only(&[
"pcm_u8",
"pcm_s16le",
"pcm_s24le",
"pcm_s32le",
"pcm_f32le",
"pcm_f64le",
]),
"pcm_s16le",
),
(
"y4m",
MediaType::Video,
Storable::Only(&["rawvideo"]),
"rawvideo",
),
];
pub fn track_codec(
ctx: &RuntimeContext,
container: &str,
kind: MediaType,
source_codec: &str,
) -> Option<Option<String>> {
let (_, _, storable, default) = DEFAULTS
.iter()
.find(|(c, k, _, _)| *c == container && *k == kind)?;
if storable.accepts(source_codec) {
return Some(None);
}
ctx.codecs
.has_encoder(&CodecId::new(*default))
.then(|| Some((*default).to_string()))
}
pub fn stream_codec(
ctx: &RuntimeContext,
container: &str,
output_codec: Option<&str>,
kind: MediaType,
source_codec: &str,
) -> Option<String> {
if let Some(codec) = output_codec {
let encodes_kind = ctx
.codecs
.implementations(&CodecId::new(codec))
.iter()
.any(|i| i.make_encoder.is_some() && i.caps.media_type == kind);
if encodes_kind {
return Some(codec.to_string());
}
}
track_codec(ctx, container, kind, source_codec).flatten()
}
pub fn plan_tracks(
ctx: &RuntimeContext,
container: &str,
streams: &[StreamInfo],
) -> Option<Vec<(MediaType, u32, Option<String>)>> {
let mut any_encode = false;
let mut ordinals: [u32; 2] = [0, 0];
let mut out = Vec::new();
for s in streams {
let kind = s.params.media_type;
let slot = match kind {
MediaType::Video => 0,
MediaType::Audio => 1,
_ => continue,
};
let ordinal = ordinals[slot];
ordinals[slot] += 1;
let codec = track_codec(ctx, container, kind, s.params.codec_id.as_str()).unwrap_or(None);
any_encode |= codec.is_some();
out.push((kind, ordinal, codec));
}
any_encode.then_some(out)
}
#[cfg(test)]
mod tests {
use super::*;
use oxideav_core::{CodecCapabilities, CodecInfo, CodecParameters, TimeBase};
fn ctx_with_encoders(ids: &[&str]) -> RuntimeContext {
let mut ctx = RuntimeContext::new();
for id in ids {
ctx.codecs.register(
CodecInfo::new(CodecId::new(*id))
.capabilities(CodecCapabilities::video(*id).with_encode())
.encoder(|_| Err(oxideav_core::Error::unsupported("stub"))),
);
}
ctx
}
fn stream(index: u32, kind: MediaType, codec: &str) -> StreamInfo {
let mut params = match kind {
MediaType::Audio => CodecParameters::audio(CodecId::new(codec)),
_ => CodecParameters::video(CodecId::new(codec)),
};
params.media_type = kind;
StreamInfo {
index,
time_base: TimeBase::new(1, 25),
duration: None,
start_time: Some(0),
params,
}
}
#[test]
fn webm_reencodes_what_it_cannot_store() {
let ctx = ctx_with_encoders(&["vp9", "opus"]);
let streams = [
stream(0, MediaType::Video, "rawvideo"),
stream(1, MediaType::Audio, "pcm_s16le"),
];
let plan = plan_tracks(&ctx, "webm", &streams).expect("needs encoding");
assert_eq!(
plan,
vec![
(MediaType::Video, 0, Some("vp9".to_string())),
(MediaType::Audio, 0, Some("opus".to_string())),
]
);
}
#[test]
fn storable_streams_keep_the_copy_job() {
let ctx = ctx_with_encoders(&["vp9", "opus"]);
let streams = [
stream(0, MediaType::Video, "vp9"),
stream(1, MediaType::Audio, "opus"),
];
assert_eq!(plan_tracks(&ctx, "webm", &streams), None);
let streams = [stream(0, MediaType::Video, "h264")];
assert_eq!(plan_tracks(&ctx, "matroska", &streams), None);
let streams = [stream(0, MediaType::Video, "rawvideo")];
assert_eq!(
plan_tracks(&ctx, "matroska", &streams),
Some(vec![(MediaType::Video, 0, Some("vp9".to_string()))])
);
}
fn ctx_with_audio_encoders(ids: &[&str]) -> RuntimeContext {
let mut ctx = RuntimeContext::new();
for id in ids {
ctx.codecs.register(
CodecInfo::new(CodecId::new(*id))
.capabilities(CodecCapabilities::audio(*id).with_encode())
.encoder(|_| Err(oxideav_core::Error::unsupported("stub"))),
);
}
ctx
}
#[test]
fn stream_codec_prefers_the_extension_codec_then_the_table() {
let ctx = ctx_with_audio_encoders(&["flac", "vorbis", "pcm_s16le"]);
assert_eq!(
stream_codec(&ctx, "flac", Some("flac"), MediaType::Audio, "aac"),
Some("flac".to_string())
);
assert_eq!(
stream_codec(&ctx, "flac", Some("flac"), MediaType::Video, "h264"),
None
);
assert_eq!(
stream_codec(&ctx, "ogg", None, MediaType::Audio, "aac"),
Some("vorbis".to_string())
);
assert_eq!(
stream_codec(&ctx, "ogg", None, MediaType::Audio, "opus"),
None
);
assert_eq!(
stream_codec(&ctx, "wav", None, MediaType::Audio, "flac"),
Some("pcm_s16le".to_string())
);
assert_eq!(
stream_codec(&ctx, "matroska", None, MediaType::Audio, "aac"),
None
);
}
#[test]
fn missing_default_encoder_or_unknown_container_is_no_entry() {
let ctx = ctx_with_encoders(&[]);
assert_eq!(
track_codec(&ctx, "webm", MediaType::Video, "rawvideo"),
None
);
let ctx = ctx_with_encoders(&["vp9"]);
assert_eq!(track_codec(&ctx, "mp4", MediaType::Video, "rawvideo"), None);
assert_eq!(
track_codec(&ctx, "webm", MediaType::Video, "vp9"),
Some(None)
);
}
}