Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1366,6 +1366,13 @@ jobs:
--target-dir target/fallback
cargo test --locked --release -p coreav-cli --features ffmpeg-fallback,dav1d,libvpx,libopus,aac --test aac_worker \
--target-dir target/fallback
- name: Identity audio options run without whole-command fallback
env:
COREAV_REQUIRE_FFMPEG: '1'
COREAV_FFMPEG: ${{ runner.temp }}/coreav-oracle-bin/ffmpeg
run: |
cargo test --locked --release -p coreav-cli --features native-h264,aac,libopus \
--test ffmpeg_audio_options --target-dir target/fallback
- name: Copied video with AAC encoding preserves exact fallback
env:
COREAV_REQUIRE_FFMPEG: '1'
Expand Down
31 changes: 17 additions & 14 deletions crates/coreav-cli/src/ffmpeg/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1238,7 +1238,7 @@ pub fn parse(args: &[String]) -> Result<Command, Refusal> {
));
}
if let Some(option) = audio_encoders.first()
&& command.muxer != Muxer::Mp3
&& !matches!(command.muxer, Muxer::Mp3 | Muxer::Mp4 | Muxer::Ipod)
&& !command
.codecs
.iter()
Comment thread
capy-ai[bot] marked this conversation as resolved.
Resolved
Expand Down Expand Up @@ -1279,13 +1279,19 @@ pub fn parse(args: &[String]) -> Result<Command, Refusal> {
"it is not an option of native audio output",
));
}
} else if command.sample_rate.is_some() {
} else if command.sample_rate.is_some()
&& !matches!(command.muxer, Muxer::Mp4 | Muxer::Ipod | Muxer::Mp3)
&& command.muxer.matroska().is_none()
{
return Err(not_native(
"-ar",
None,
"it is not supported on this output",
));
} else if command.channels.is_some() {
} else if command.channels.is_some()
&& !matches!(command.muxer, Muxer::Mp4 | Muxer::Ipod | Muxer::Mp3)
&& command.muxer.matroska().is_none()
{
return Err(not_native(
"-ac",
None,
Expand Down Expand Up @@ -1973,14 +1979,15 @@ mod tests {
]
);
assert_eq!(c.encoding.audio_bitrate.as_deref(), Some("128k"));
assert!(matches!(
parse(&args("-i in.mkv -c:v libx264 -b:a 128k out.mp4")),
Err(Refusal::NotNative(_))
));
let c = parse(&args("-i in.mkv -c:v libx264 -b:a 128k out.mp4")).unwrap();
assert_eq!(c.encoding.audio_bitrate.as_deref(), Some("128k"));
}

#[test]
fn aac_and_its_bit_rate() {
let c = parse(&args("-i in.mkv -c:a aac -ar 44100 -ac 1 out.mp4")).unwrap();
assert_eq!(c.sample_rate, Some(44_100));
assert_eq!(c.channels, Some(1));
for line in [
"-i in.mkv -c:v libx264 -c:a aac -b:a 96k out.mp4",
"-i in.mkv -c:v libx264 -acodec aac -b:a 96k out.mp4",
Expand Down Expand Up @@ -2009,8 +2016,6 @@ mod tests {
("-aac_coder twoloop", "-aac_coder", None),
("-q:a 2", "-q:a", None),
("-vbr 3", "-vbr", None),
("-ar 44100", "-ar", None),
("-ac 1", "-ac", None),
("-af volume=2", "-af", None),
("-strict experimental", "-strict", None),
("-c:a libfdk_aac", "-c:a", Some("libfdk_aac")),
Expand Down Expand Up @@ -2055,11 +2060,9 @@ mod tests {
for (option, value) in [("-ar", "48000"), ("-ac", "1")] {
for codec in ["copy", "aac"] {
let line = format!("-i in.mp4 -c:a {codec} -vn {option} {value} out.mp4");
assert_eq!(
refused(&line),
Reason::Option(option.to_owned(), None),
"{line}"
);
let command = parse(&args(&line)).unwrap();
assert_eq!(command.sample_rate, (option == "-ar").then_some(48_000));
assert_eq!(command.channels, (option == "-ac").then_some(1));
}
}
assert!(parse(&args("-i in.mp4 -c copy -movflags +faststart out.mp4")).is_ok());
Expand Down
78 changes: 62 additions & 16 deletions crates/coreav-cli/src/ffmpeg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const ENCODE_LIMIT: std::time::Duration = std::time::Duration::from_secs(3600);
use crate::error::Failure;
use crate::fallback::NotNative;
use crate::ffprobe::args::Kind2;
use crate::ffprobe::model::Value;
use crate::ffprobe::model::{Unit, Value};
use crate::ffprobe::{Report, Stream, StreamKind};
use crate::output::{self, WriteFailure};
use crate::split::Pattern;
Expand Down Expand Up @@ -730,7 +730,7 @@ pub fn plan(
"pcm_s16le outside WAV or raw s16le output",
));
}
non_pcm_audio_options(command)?;
non_pcm_audio_options(command, &output, &decided)?;
if decided.contains(&Some(Codec::Flac)) {
return Err(Refusal::not_native("FLAC encoding outside FLAC output"));
}
Expand Down Expand Up @@ -1599,19 +1599,65 @@ fn select_sources(
Ok(selected)
}

fn non_pcm_audio_options(command: &Command) -> Result<(), Refusal> {
if command.muxer == Muxer::Mp4 {
for (option, given) in [
("-ar", command.sample_rate.is_some()),
("-ac", command.channels.is_some()),
] {
if given {
return Err(Refusal::NotNative(NotNative::option(
option,
None,
format!("'{option}' is not supported: it is not supported on this output"),
)));
}
fn non_pcm_audio_options(
command: &Command,
output: &[(usize, &Stream)],
decided: &[Option<Codec>],
) -> Result<(), Refusal> {
if command.encoding.audio_bitrate.is_some()
&& matches!(command.muxer, Muxer::Mp4 | Muxer::Ipod)
&& !command
.codecs
.iter()
.any(|(_, codec)| matches!(codec, Codec::Audio(_) | Codec::AviAudio(_)))
&& !output.iter().zip(decided).any(|((_, stream), codec)| {
stream.kind == StreamKind::Audio
&& *codec == Some(Codec::Audio(args::AudioEncoder::Aac))
})
{
return Err(Refusal::NotNative(NotNative::option(
"-b:a",
None,
"'-b:a' without an explicit audio encoder requires selected default AAC encoding"
.to_owned(),
)));
}
for (option, asked) in [
("-ar", command.sample_rate.map(i64::from)),
("-ac", command.channels.map(i64::from)),
] {
let Some(asked) = asked else { continue };
let mut audio = output
.iter()
.zip(decided)
.filter(|((_, stream), _)| stream.kind == StreamKind::Audio)
.peekable();
if audio.peek().is_none()
|| !audio.all(|((_, stream), codec)| {
if !matches!(codec, Some(Codec::Audio(_))) {
return false;
}
if option == "-ac" {
return int(stream, "channels") == asked;
}
if matches!(codec, Some(Codec::Audio(args::AudioEncoder::Libopus)))
&& asked != 48_000
{
return false;
}
stream.entries.iter().any(|(key, value)| {
*key == "sample_rate"
&& matches!(value, Value::Unit(rate, Unit::Hertz) if *rate == asked)
})
})
{
return Err(Refusal::NotNative(NotNative::option(
option,
None,
format!(
"'{option}' is not supported: native audio encoding requires unchanged sample rate and channels"
),
)));
}
}
Ok(())
Expand All @@ -1623,7 +1669,7 @@ fn plan_inputs(command: &Command, reports: &[Report]) -> Result<Job, Refusal> {
"multiple-input output other than MP4 with default metadata",
));
}
non_pcm_audio_options(command)?;
non_pcm_audio_options(command, &[], &[])?;
if command.maps.is_empty() {
return Err(Refusal::not_native(
"automatic stream selection across several inputs",
Expand Down
Loading
Loading