use crate::error::OpenOutputError;
use std::collections::HashMap;
use std::ffi::CString;
#[derive(Debug, Clone)]
pub struct StreamMap {
pub(crate) linklabel: String,
pub(crate) copy: bool,
pub(crate) codec: Option<String>,
pub(crate) codec_opts: Vec<(String, String)>,
}
impl StreamMap {
pub fn new(spec: impl Into<String>) -> Self {
Self {
linklabel: spec.into(),
copy: false,
codec: None,
codec_opts: Vec::new(),
}
}
pub fn codec(mut self, name: impl Into<String>) -> Self {
self.codec = Some(name.into());
self
}
pub fn codec_opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.codec_opts.push((key.into(), value.into()));
self
}
pub(crate) fn resolve_for_bind(&self) -> crate::error::Result<ResolvedStreamMap> {
let (copy, codec) = match self.codec.as_deref() {
Some("copy") => (true, None),
Some(name) => {
if self.copy {
return Err(OpenOutputError::StreamMapCopyConflict {
spec: self.linklabel.clone(),
what: "a per-map codec",
}
.into());
}
(false, Some(name.to_string()))
}
None => (self.copy, None),
};
let codec_opts = if self.codec_opts.is_empty() {
None
} else if copy {
return Err(OpenOutputError::StreamMapCopyConflict {
spec: self.linklabel.clone(),
what: "per-map codec options",
}
.into());
} else {
let mut map = HashMap::with_capacity(self.codec_opts.len());
for (key, value) in &self.codec_opts {
map.insert(CString::new(key.as_str())?, CString::new(value.as_str())?);
}
Some(map)
};
Ok(ResolvedStreamMap {
copy,
codec,
codec_opts,
})
}
}
impl<T: Into<String>> From<T> for StreamMap {
fn from(linklabel: T) -> Self {
Self::new(linklabel)
}
}
#[derive(Debug)]
pub(crate) struct ResolvedStreamMap {
pub(crate) copy: bool,
pub(crate) codec: Option<String>,
pub(crate) codec_opts: Option<HashMap<CString, CString>>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
fn cstr(s: &str) -> CString {
CString::new(s).unwrap()
}
#[test]
fn new_stores_spec_without_overrides() {
let map = StreamMap::new("0:v");
assert_eq!(map.linklabel, "0:v");
assert!(!map.copy);
assert_eq!(map.codec, None);
assert!(map.codec_opts.is_empty());
}
#[test]
fn blanket_from_matches_new() {
let from_str: StreamMap = "0:a?".into();
let from_string: StreamMap = String::from("0:a?").into();
for map in [from_str, from_string] {
assert_eq!(map.linklabel, "0:a?");
assert!(!map.copy && map.codec.is_none() && map.codec_opts.is_empty());
}
}
#[test]
fn codec_copy_normalizes_to_the_copy_flag() {
let resolved = StreamMap::new("0:a").codec("copy").resolve_for_bind().unwrap();
assert!(resolved.copy);
assert_eq!(resolved.codec, None);
assert!(resolved.codec_opts.is_none());
}
#[test]
fn copy_flag_with_codec_is_a_typed_conflict() {
let mut map = StreamMap::new("0:v").codec("libx264");
map.copy = true; match map.resolve_for_bind() {
Err(Error::OpenOutput(OpenOutputError::StreamMapCopyConflict { spec, what })) => {
assert_eq!(spec, "0:v");
assert_eq!(what, "a per-map codec");
}
other => panic!("expected StreamMapCopyConflict, got {other:?}"),
}
}
#[test]
fn copy_with_codec_opts_is_a_typed_conflict() {
let mut map = StreamMap::new("0:a").codec_opt("b", "320k");
map.copy = true;
match map.resolve_for_bind() {
Err(Error::OpenOutput(OpenOutputError::StreamMapCopyConflict { spec, what })) => {
assert_eq!(spec, "0:a");
assert_eq!(what, "per-map codec options");
}
other => panic!("expected StreamMapCopyConflict, got {other:?}"),
}
}
#[test]
fn codec_copy_with_codec_opts_is_a_typed_conflict() {
let map = StreamMap::new("0:a").codec("copy").codec_opt("b", "320k");
assert!(matches!(
map.resolve_for_bind(),
Err(Error::OpenOutput(
OpenOutputError::StreamMapCopyConflict { .. }
))
));
}
#[test]
fn later_codec_call_replaces_the_earlier_one() {
let resolved = StreamMap::new("0:v")
.codec("copy")
.codec("libx264")
.resolve_for_bind()
.unwrap();
assert!(!resolved.copy);
assert_eq!(resolved.codec.as_deref(), Some("libx264"));
}
#[test]
fn duplicate_option_keys_keep_the_last_value() {
let resolved = StreamMap::new("0:v")
.codec("mpeg4")
.codec_opt("b", "1M")
.codec_opt("b", "4M")
.resolve_for_bind()
.unwrap();
let opts = resolved.codec_opts.unwrap();
assert_eq!(opts.len(), 1);
assert_eq!(opts.get(&cstr("b")), Some(&cstr("4M")));
}
#[test]
fn empty_option_list_resolves_to_none() {
let resolved = StreamMap::new("0:v").codec("mpeg4").resolve_for_bind().unwrap();
assert!(resolved.codec_opts.is_none());
}
#[test]
fn interior_nul_in_option_is_a_typed_error() {
let map = StreamMap::new("0:v").codec_opt("b\0ad", "1M");
assert!(map.resolve_for_bind().is_err());
}
#[test]
fn add_stream_map_with_copy_forces_the_copy_flag() {
use crate::core::context::output::Output;
let output = Output::from("out.mkv").add_stream_map_with_copy(StreamMap::new("0:a"));
assert!(output.stream_map_specs[0].copy);
let output = Output::from("out.mkv").add_stream_map_with_copy("0:a?");
assert!(output.stream_map_specs[0].copy);
assert_eq!(output.stream_map_specs[0].linklabel, "0:a?");
}
#[test]
fn add_stream_map_accepts_str_string_and_stream_map() {
use crate::core::context::output::Output;
let output = Output::from("out.mkv")
.add_stream_map("0:v")
.add_stream_map(String::from("0:a:0"))
.add_stream_map(StreamMap::new("0:a:1").codec("flac"));
assert_eq!(output.stream_map_specs.len(), 3);
assert_eq!(output.stream_map_specs[2].codec.as_deref(), Some("flac"));
}
}