use std::{collections::HashMap, ffi::OsStr, path::Path, process::Command, str::FromStr};
use crate::{
codec::Codec,
errors::{ExportError, FfmpegError},
ffmpeg::command_mutators::FfmpegPresets,
library::{
metadata::{self, RawMetadata},
track::Track,
},
};
use serde::{Deserialize, Serialize};
pub mod command_mutators;
pub mod loudnorm;
pub fn ffmpeg() -> Command {
let mut command = Command::new("ffmpeg");
command.args(["-hide_banner", "-loglevel", "error"]);
command
}
pub fn ffprobe(log_level: impl AsRef<str>) -> Command {
let mut command = Command::new("ffprobe");
command.args(["-hide_banner", "-loglevel", log_level.as_ref()]);
command
}
pub fn output_ffmpeg(mut command: Command) -> Result<String, FfmpegError> {
if command.get_program() != OsStr::new("ffmpeg") {
return Err(FfmpegError::FfmpegWrapperError(
"Attempted to access non-ffmpeg command: Expected ffmpeg wrapper".to_owned(),
));
}
let output = command.output()?;
if output.status.success() {
let ok = String::from_utf8_lossy(&output.stdout).to_string();
Ok(ok)
} else {
let err = String::from_utf8_lossy(&output.stderr).to_string();
Err(FfmpegError::FfmpegWrapperError(err))
}
}
pub fn output_ffmpeg_err(mut command: Command) -> Result<String, FfmpegError> {
if command.get_program() != OsStr::new("ffmpeg") {
return Err(FfmpegError::FfmpegWrapperError(
"Attempted to access non-ffmpeg command: Expected ffmpeg wrapper".to_owned(),
));
}
let output = command.output()?;
if output.status.success() {
let ok = String::from_utf8_lossy(&output.stderr).to_string();
Ok(ok)
} else {
let err = String::from_utf8_lossy(&output.stderr).to_string();
Err(FfmpegError::FfmpegWrapperError(err))
}
}
pub fn output_ffprobe(mut command: Command) -> Result<String, FfmpegError> {
if command.get_program() != OsStr::new("ffprobe") {
return Err(FfmpegError::FfprobeWrapperError(
"Attempted to access non-ffprobe command: Expected ffprobe wrapper".to_owned(),
));
}
let output = command.output()?;
if output.status.success() {
let ok = String::from_utf8_lossy(&output.stdout).to_string();
Ok(ok)
} else {
let err = String::from_utf8_lossy(&output.stderr).to_string();
Err(FfmpegError::FfprobeWrapperError(err))
}
}
pub fn ffprobe_media_container_json(path: impl AsRef<Path>) -> Result<String, FfmpegError> {
let path = path.as_ref();
let mut command = ffprobe("error");
command.args([
"-select_streams", "a:0",
"-show_entries", "stream=codec_name,sample_rate,channels,channel_layout,duration,bits_per_raw_sample:format=duration,bit_rate,format_name",
"-of", "json"
]);
command.arg(path);
output_ffprobe(command)
}
#[derive(Debug, PartialEq, PartialOrd, Clone, Serialize, Deserialize, Copy)]
pub struct Stream {
pub(crate) codec: Codec,
pub(crate) sample_rate: usize,
pub(crate) channels: isize,
pub(crate) channel_layout: Option<ChannelLayout>,
pub(crate) duration: f32,
}
impl Stream {
pub fn sample_rate(&self) -> usize {
self.sample_rate
}
pub fn duration(&self) -> f32 {
self.duration
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum ChannelLayout {
Mono,
Stereo,
TwoOne,
ThreeZero,
ThreeZeroBack,
FourZero,
Quad,
QuadSide,
ThreeOne,
FiveZero,
FiveZeroSide,
FourOne,
FiveOne,
FiveOneSide,
SixZero,
SixZeroFront,
ThreeOneTwo,
Hexagonal,
SixOne,
SixOneFront,
SevenZero,
SevenZeroFront,
SevenOne,
SevenOneWide,
SevenOneWideSide,
FiveOneTwo,
Octagonal,
Cube,
FiveOneFour,
SevenOneTwo,
SevenOneFour,
SevenTwoThree,
NineOneFour,
NineOneSix,
Hexadecagonal,
Binaural,
Downmix,
TwentytwoTwo,
}
impl FromStr for ChannelLayout {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"mono" => Ok(ChannelLayout::Mono),
"stereo" => Ok(ChannelLayout::Stereo),
"2.1" => Ok(ChannelLayout::TwoOne),
"3.0" => Ok(ChannelLayout::ThreeZero),
"3.0(back)" => Ok(ChannelLayout::ThreeZeroBack),
"4.0" => Ok(ChannelLayout::FourZero),
"quad" => Ok(ChannelLayout::Quad),
"quad(side)" => Ok(ChannelLayout::QuadSide),
"3.1" => Ok(ChannelLayout::ThreeOne),
"5.0" => Ok(ChannelLayout::FiveZero),
"5.0(side)" => Ok(ChannelLayout::FiveZeroSide),
"4.1" => Ok(ChannelLayout::FourOne),
"5.1" => Ok(ChannelLayout::FiveOne),
"5.1(side)" => Ok(ChannelLayout::FiveOneSide),
"6.0" => Ok(ChannelLayout::SixZero),
"6.0(front)" => Ok(ChannelLayout::SixZeroFront),
"3.1.2" => Ok(ChannelLayout::ThreeOneTwo),
"hexagonal" => Ok(ChannelLayout::Hexagonal),
"6.1" => Ok(ChannelLayout::SixOne),
"6.1(front)" => Ok(ChannelLayout::SixOneFront),
"7.0" => Ok(ChannelLayout::SevenZero),
"7.0(front)" => Ok(ChannelLayout::SevenZeroFront),
"7.1" => Ok(ChannelLayout::SevenOne),
"7.1(wide)" => Ok(ChannelLayout::SevenOneWide),
"7.1(wide-side)" => Ok(ChannelLayout::SevenOneWideSide),
"5.1.2" => Ok(ChannelLayout::FiveOneTwo),
"octagonal" => Ok(ChannelLayout::Octagonal),
"cube" => Ok(ChannelLayout::Cube),
"5.1.4" => Ok(ChannelLayout::FiveOneFour),
"7.1.2" => Ok(ChannelLayout::SevenOneTwo),
"7.1.4" => Ok(ChannelLayout::SevenOneFour),
"7.2.3" => Ok(ChannelLayout::SevenTwoThree),
"9.1.4" => Ok(ChannelLayout::NineOneFour),
"9.1.6" => Ok(ChannelLayout::NineOneSix),
"hexadecagonal" => Ok(ChannelLayout::Hexadecagonal),
"binaural" => Ok(ChannelLayout::Binaural),
"downmix" => Ok(ChannelLayout::Downmix),
"22.2" => Ok(ChannelLayout::TwentytwoTwo),
_ => Err(format!("Unknown channel layout: {s}")),
}
}
}
pub fn ffprobe_format_tags(path: impl AsRef<Path>) -> Result<RawMetadata, FfmpegError> {
#[derive(Deserialize)]
struct Root {
#[serde(default)]
format: Tags,
}
#[derive(Deserialize, Default)]
struct Tags {
#[serde(default)]
tags: HashMap<String, String>,
}
let mut command = ffprobe("error");
command.args(["-show_entries", "format_tags", "-print_format", "json"]);
command.arg(path.as_ref());
let json = output_ffprobe(command)?;
let root: Root = serde_json::from_str(&json)?;
let tags = root
.format
.tags
.into_iter()
.map(|(key, value)| (metadata::canonicalize_metadata_key(&key), value))
.collect();
Ok(tags)
}
pub fn export_set_metadata(track: &Track, export_to: impl AsRef<Path>) -> Result<(), ExportError> {
let export_to = export_to.as_ref();
let Some(lib_container) = track.lib_container() else {
return Err(FfmpegError::Other(
"Input track does not have a library file to export from".to_owned(),
)
.into());
};
let mut command = ffmpeg();
command.input_file(lib_container.path());
command.copy_all();
command.drop_metadata();
let metadata = track.metadata_key_values()?;
command.add_metadata_group(metadata.iter());
command.output_file(export_to);
output_ffmpeg(command)?;
Ok(())
}