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
14 changes: 14 additions & 0 deletions .github/workflows/h264-encoder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ on:
- crates/coreav-h264-encoder/**
- crates/coreav-cli/**
- crates/coreav-transcode/**
- crates/coreav-isobmff-write/**
- crates/coreav-frame/**
- scripts/native_h264_cli.py
- scripts/native_h264_matroska.py
- scripts/oracle_audio.py
- scripts/oracle-audio-args.txt
- scripts/tests/test_native_h264_cli.py
- scripts/track_boxes.py
- crates/coreav-kernels-h264/**
- crates/coreav-h264/**
Expand Down Expand Up @@ -42,8 +47,13 @@ on:
- crates/coreav-h264-encoder/**
- crates/coreav-cli/**
- crates/coreav-transcode/**
- crates/coreav-isobmff-write/**
- crates/coreav-frame/**
- scripts/native_h264_cli.py
- scripts/native_h264_matroska.py
- scripts/oracle_audio.py
- scripts/oracle-audio-args.txt
- scripts/tests/test_native_h264_cli.py
- scripts/track_boxes.py
- crates/coreav-kernels-h264/**
- crates/coreav-h264/**
Expand Down Expand Up @@ -110,6 +120,8 @@ jobs:
- name: Preserve base-build byte references
run: |
cd ../baseline
git rev-parse HEAD
test "$(git rev-parse HEAD)" = '${{ github.event.pull_request.base.sha || github.sha }}'
cargo build --locked --release -p coreav-cli
cp target/release/coreav "$RUNNER_TEMP/default-base"
cargo build --locked --release -p coreav-cli --features native-h264
Expand All @@ -134,6 +146,7 @@ jobs:
cargo test --locked --release -p coreav-cli --features native-h264-encoder,native-h264 --lib ffmpeg -- --nocapture
cargo test --locked --release -p coreav-cli --features native-h264-encoder,native-h264 --test docs_site --test strict_safe_limit --test ffprobe_limits --test native_h264_cli
cargo test --locked --release -p coreav-transcode --features h264-native --lib
cargo test --locked --release -p coreav-isobmff-write --lib mkv_output
cargo clippy --locked -p coreav-cli --features native-h264-encoder,native-h264 --all-targets -- -D warnings
cargo build --locked --release -p coreav-cli --features native-h264-encoder,native-h264
cargo build --locked --release -p coreav-h264-encoder --features full --example p
Expand All @@ -157,6 +170,7 @@ jobs:
--baseline-default "$RUNNER_TEMP/default-base" \
--baseline-external "$RUNNER_TEMP/external-base" \
--baseline-native "$RUNNER_TEMP/native-base" \
--baseline-revision "$(git -C ../baseline rev-parse HEAD)" \
--driver target/release/examples/p \
--ffmpeg "$RUNNER_TEMP/cli-oracles/ffmpeg" \
--output "$RUNNER_TEMP/native-cli"
Expand Down
20 changes: 19 additions & 1 deletion crates/coreav-cli/src/ffmpeg/native_h264.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fn candidate(args: &[String]) -> Option<(Command, NativeH264)> {
let (command, qp) = super::args::native_h264(args).ok()?;
let encoding = &command.encoding;
if !command.more_inputs.is_empty()
|| command.muxer != Muxer::Mp4
|| !matches!(command.muxer, Muxer::Mp4 | Muxer::Matroska)
|| !command.codecs.iter().any(|(spec, codec)| {
*codec == Codec::Libx264 && matches!(spec, Spec::Kind(Kind2::Video, None | Some(0)))
})
Expand Down Expand Up @@ -208,6 +208,24 @@ mod tests {
);
}

#[test]
fn matroska_selection_keeps_arguments_and_excludes_webm() {
for suffix in ["out.mp4", "out.mkv", "-f matroska out.bin"] {
let args: Vec<String> = format!("-i in.mp4 -an -c:v libx264 -crf 23 {suffix}")
.split_whitespace()
.map(str::to_owned)
.collect();
let before = args.clone();
assert!(candidate(&args).is_some(), "{suffix}");
assert_eq!(args, before);
}
let webm: Vec<String> = "-i in.mp4 -an -c:v libx264 out.webm"
.split_whitespace()
.map(str::to_owned)
.collect();
assert!(candidate(&webm).is_none());
}

#[test]
fn unsupported_controls_keep_the_existing_command() {
for option in [
Expand Down
101 changes: 98 additions & 3 deletions crates/coreav-isobmff-write/src/mkv_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

mod config;
mod source;
#[cfg(test)]
mod tests;

use crate::{Replacements, replace};
use coreav_isobmff::{Limits, Rational, Source};
Expand All @@ -21,6 +23,36 @@ pub struct OutputOptions {
pub streams: Option<Vec<usize>>,
}

/// Explicit constant cadence for one replaced AVC video stream.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct VideoCadence {
stream_index: usize,
default_duration_ns: u64,
}

impl VideoCadence {
/// Round a positive rate of at most 1,000 frames per second to nanoseconds.
pub fn new(stream_index: usize, fps_num: u32, fps_den: u32) -> Result<Self, Error> {
if fps_num == 0 || fps_den == 0 || u128::from(fps_num) > 1_000 * u128::from(fps_den) {
return Err(unsupported(
"video cadence needs a positive rate at most 1000 fps",
));
}
let numerator = u128::from(fps_den)
.checked_mul(1_000_000_000)
.and_then(|n| n.checked_add(u128::from(fps_num) / 2))
.ok_or_else(|| unsupported("video cadence exceeds nanosecond limits"))?;
let default_duration_ns = u64::try_from(numerator / u128::from(fps_num))
.ok()
.filter(|n| *n > 0)
.ok_or_else(|| unsupported("video cadence exceeds nanosecond limits"))?;
Ok(Self {
stream_index,
default_duration_ns,
})
}
}

/// One written track.
#[derive(Clone, Debug)]
pub struct OutputTrack {
Expand Down Expand Up @@ -111,11 +143,45 @@ pub fn copied_audio<S: Source>(source: &mut S, streams: &[usize]) -> Result<(),
/// Plans selected tracks and replacements, then streams their payloads to
/// the output. Media stays in its original source or encoder spool.
pub fn write_output<S: Source, R: Source, W: Write>(
source: S,
options: &OutputOptions,
limits: &Limits,
mkv_limits: &coreav_mkv::Limits,
replacements: Replacements<'_, R>,
out: W,
) -> Result<OutputSummary, Error> {
write_output_inner(source, options, limits, mkv_limits, replacements, None, out)
}

/// Writes one replaced AVC stream with caller-established constant cadence.
/// Copied ISO Opus follows its source edit rather than its informative pre-skip.
pub fn write_output_with_video_cadence<S: Source, R: Source, W: Write>(
source: S,
options: &OutputOptions,
limits: &Limits,
mkv_limits: &coreav_mkv::Limits,
replacements: Replacements<'_, R>,
cadence: VideoCadence,
out: W,
) -> Result<OutputSummary, Error> {
write_output_inner(
source,
options,
limits,
mkv_limits,
replacements,
Some(cadence),
out,
)
}

fn write_output_inner<S: Source, R: Source, W: Write>(
mut source: S,
options: &OutputOptions,
limits: &Limits,
mkv_limits: &coreav_mkv::Limits,
mut replacements: Replacements<'_, R>,
video_cadence: Option<VideoCadence>,
out: W,
) -> Result<OutputSummary, Error> {
if options
Expand All @@ -130,7 +196,18 @@ pub fn write_output<S: Source, R: Source, W: Write>(
limit: mkv_limits.max_retained_bytes,
};
if let Some(v) = &mut replacements.video {
replace::validate(v, limits).map_err(iso)?;
let codec = replace::validate(v, limits).map_err(iso)?;
if let Some(cadence) = video_cadence
&& (cadence.stream_index != v.stream_index
|| codec != replace::EntryCodec::H264
|| options.doc_type != DocType::Matroska)
{
return Err(unsupported(
"video cadence needs its selected AVC replacement in Matroska",
));
}
} else if video_cadence.is_some() {
return Err(unsupported("video cadence needs an AVC replacement"));
}
if let Some(a) = &mut replacements.audio {
replace::validate_audio(a, limits).map_err(iso)?;
Expand Down Expand Up @@ -163,7 +240,10 @@ pub fn write_output<S: Source, R: Source, W: Write>(
} else {
source::isobmff(
&mut source,
options,
source::IsoOptions {
output: options,
preserve_opus_edit: video_cadence.is_some(),
},
limits,
mkv_limits,
&replacement_headers,
Expand Down Expand Up @@ -205,7 +285,22 @@ pub fn write_output<S: Source, R: Source, W: Write>(
.checked_add(u64::from(sample.size))
.ok_or_else(overflow)?;
}
cadence(t);
if let Some(cadence) = video_cadence {
if t.packets.windows(2).any(|w| match w {
[a, b] => a.packet.pts_ns >= b.packet.pts_ns,
_ => true,
}) {
return Err(unsupported(
"video cadence needs increasing millisecond timestamps",
));
}
t.header.default_duration_ns = Some(cadence.default_duration_ns);
for packet in &mut t.packets {
packet.packet.duration_ns = None;
}
} else {
cadence(t);
}
t.prefix.clear();
t.replaced = true;
}
Expand Down
13 changes: 11 additions & 2 deletions crates/coreav-isobmff-write/src/mkv_output/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,24 @@ use super::{
};
use coreav_isobmff::{Allowlist, EditList, Limits, TrackKind};

pub(super) struct IsoOptions<'a> {
pub output: &'a OutputOptions,
pub preserve_opus_edit: bool,
}

pub(super) fn isobmff<S: Source>(
source: &mut S,
options: &OutputOptions,
options: IsoOptions<'_>,
limits: &Limits,
mkv_limits: &coreav_mkv::Limits,
replacements: &[(usize, Track)],
total: &mut usize,
metadata: &mut MetadataBudget,
) -> Result<Vec<InputTrack>, Error> {
let IsoOptions {
output: options,
preserve_opus_edit,
} = options;
let probe_limits = Limits {
max_retained_bytes: limits
.max_retained_bytes
Expand Down Expand Up @@ -121,7 +130,7 @@ pub(super) fn isobmff<S: Source>(
};
let mut trim =
u64::try_from(media).map_err(|_| unsupported("a negative media edit time"))?;
if replacement.is_none() && d.codec == coreav_isobmff::Codec::Opus {
if replacement.is_none() && d.codec == coreav_isobmff::Codec::Opus && !preserve_opus_edit {
let skip = header
.private
.get(10..12)
Expand Down
Loading
Loading