Skip to main content

alpha2_native/
passthrough.rs

1//! Copying a source's compressed video packets into an export unchanged.
2//!
3//! The project side of the decision (one untouched clip covering the range)
4//! is the engine's ([`alpha2_engine::passthrough_plan`] in the host). This is
5//! the file side: whether the packets under that range decode, on their own,
6//! into exactly the frames rendering would show, and then the copy itself.
7//!
8//! The test is on timestamps, not on flags. The copy takes one contiguous run
9//! of video packets in decode order. It must start at an IDR access unit, so
10//! nothing in it refers to a picture before it (a keyframe flag alone does not
11//! promise that), and the presentation times in the run must be exactly the
12//! export's frame times, one packet each. A B picture that needs a reference
13//! outside the range puts that reference inside the run with a presentation
14//! time outside it, and the run is refused; so is a stream whose frames are off
15//! the export's grid, a variable rate, or a duplicate. What passes is a
16//! self-contained stretch of the stream whose decoded frames are the rendered
17//! ones, which is the only thing a copy may claim.
18//!
19//! Several such runs (clips cut one after another) join into one output
20//! ([`PacketCopy::join`]) when one MP4 sample description fits them all and
21//! their decode times keep increasing across each join; each run starts at an
22//! IDR picture, so a decoder restarts there.
23
24use std::path::{Path, PathBuf};
25
26use alpha2_ffmpeg_bridge::DemuxInput;
27use alpha2_media::{
28    CancelToken, ChromaLocation, ColorMetadata, EncodeError, StreamKind, YuvEncoding,
29};
30use alpha2_time::MediaTime;
31use rsmpeg::avutil::{AVRational, av_rescale_q};
32use rsmpeg::ffi;
33
34use crate::NativeVideoCodec;
35use crate::convert::{encode_adapter_error, encode_adapter_error_at, tick_base};
36use crate::mux::{AudioStream, Mp4Muxer, VideoStream};
37
38/// The stream an export copies from and what it must match.
39#[derive(Clone, Debug)]
40pub struct PacketCopyRequest<'a> {
41    /// The source file.
42    pub source: &'a Path,
43    /// Each output frame's source time, in order: what rendering would show.
44    pub frame_times: &'a [MediaTime],
45    /// The output size, which the stream must already have.
46    pub width: u32,
47    /// See `width`.
48    pub height: u32,
49    /// The codec the export asked for, which the stream must already be.
50    pub codec: NativeVideoCodec,
51    /// The color the output is tagged with when it renders; a copy keeps the
52    /// source's tags, so they must already say this.
53    pub color: ColorMetadata,
54    /// Where this run's first frame lands in the output: zero for a single
55    /// copy, a later segment's start in a join.
56    pub output_start: MediaTime,
57    /// The mix is the source's own audio, untouched: copy it too where that
58    /// can be exact, which is over the whole file. Where it cannot, the plan
59    /// says why ([`PacketCopyPlan::audio_copy_refusal`]) and the caller
60    /// encodes the mix instead; the video copy stands either way.
61    pub audio: Option<PacketCopyAudio>,
62}
63
64/// What a copied audio stream must match.
65#[derive(Copy, Clone, Debug, Eq, PartialEq)]
66pub struct PacketCopyAudio {
67    /// The project's sample rate.
68    pub sample_rate: u32,
69    /// The export's channel count.
70    pub channels: u32,
71}
72
73/// A copy that can be made, and what it copies.
74#[derive(Clone, Debug)]
75pub struct PacketCopyPlan {
76    source: PathBuf,
77    video_stream: usize,
78    audio_stream: Option<usize>,
79    /// Why audio the request offered to copy is not copied.
80    audio_refusal: Option<String>,
81    /// The copied run, as positions in the video stream's decode order.
82    first_packet: u64,
83    last_packet: u64,
84    /// The run's first packet's timestamps, in the video stream's time base:
85    /// what a writer that seeks to it recognizes it by.
86    first_timestamps: (i64, i64),
87    /// The first output frame's presentation time, in the video stream's
88    /// time base: it lands at `offset` in the output.
89    origin: i64,
90    /// `output_start`, in the video stream's time base.
91    offset: i64,
92    video_time_base: AVRational,
93    /// What an MP4 sample description records of the stream: runs that join
94    /// must agree on all of it.
95    contract: StreamContract,
96    /// The run's first and last decode times in the output, in ticks.
97    decode_span: (i64, i64),
98}
99
100/// The part of a video stream a single MP4 sample description fixes.
101#[derive(Clone, Debug, Eq, PartialEq)]
102struct StreamContract {
103    codec_id: ffi::AVCodecID,
104    width: i32,
105    height: i32,
106    format: i32,
107    /// The parameter sets.
108    extradata: Vec<u8>,
109    time_base: (i32, i32),
110}
111
112/// One or more copy runs written back to back into one output.
113#[derive(Clone, Debug)]
114pub struct PacketCopy {
115    segments: Vec<PacketCopyPlan>,
116}
117
118impl PacketCopy {
119    /// Joins runs, in output order, into one copy.
120    ///
121    /// Each run is already a self-contained stretch of its stream starting
122    /// at an IDR picture, so a decoder restarts at every join. What a join
123    /// adds is that one sample description must describe every run (the
124    /// same codec, size, pixel format, parameter sets, and time base), and
125    /// that decode times keep increasing across it, which fails when the
126    /// runs reorder pictures differently.
127    ///
128    /// # Errors
129    /// Names the join that breaks either, or a run that copies audio in a
130    /// join (copied audio travels only with one whole file).
131    pub fn join(segments: Vec<PacketCopyPlan>) -> Result<Self, String> {
132        let first = segments
133            .first()
134            .ok_or_else(|| "a copy needs at least one run".to_owned())?;
135        if segments.len() > 1 && segments.iter().any(PacketCopyPlan::copies_audio) {
136            return Err("copied audio travels only with one whole file".to_owned());
137        }
138        for (index, pair) in segments.windows(2).enumerate() {
139            if pair[1].contract != first.contract {
140                return Err(format!(
141                    "the source of run {} is not encoded like the first run's (codec, size, pixel format, parameter sets, or time base differ), and one MP4 sample description must fit both",
142                    index + 2
143                ));
144            }
145            if pair[1].decode_span.0 <= pair[0].decode_span.1 {
146                return Err(format!(
147                    "at the join into run {}, decode times go back: the runs reorder pictures differently",
148                    index + 2
149                ));
150            }
151        }
152        Ok(Self { segments })
153    }
154
155    /// Video packets the copy writes: one per output frame.
156    #[must_use]
157    pub fn video_packets(&self) -> u64 {
158        self.segments
159            .iter()
160            .map(PacketCopyPlan::video_packets)
161            .sum()
162    }
163
164    /// Runs joined.
165    #[must_use]
166    pub fn segments(&self) -> usize {
167        self.segments.len()
168    }
169
170    /// Whether the source's audio is copied too.
171    #[must_use]
172    pub fn copies_audio(&self) -> bool {
173        matches!(&self.segments[..], [only] if only.copies_audio())
174    }
175
176    /// Why the source's audio, which the request offered to copy, is not.
177    #[must_use]
178    pub fn audio_copy_refusal(&self) -> Option<&str> {
179        self.segments
180            .first()
181            .and_then(PacketCopyPlan::audio_copy_refusal)
182    }
183}
184
185impl PacketCopyPlan {
186    /// Video packets the copy writes: one per output frame.
187    #[must_use]
188    pub const fn video_packets(&self) -> u64 {
189        self.last_packet - self.first_packet + 1
190    }
191
192    /// Whether the source's audio is copied too.
193    #[must_use]
194    pub const fn copies_audio(&self) -> bool {
195        self.audio_stream.is_some()
196    }
197
198    /// Why the source's audio, which the request offered to copy, is not.
199    #[must_use]
200    pub fn audio_copy_refusal(&self) -> Option<&str> {
201        self.audio_refusal.as_deref()
202    }
203}
204
205/// The export's mixed audio, block by block, for a copy that encodes it.
206pub trait AudioBlockFeed {
207    /// The next block, starting where the previous one ended, at sample zero
208    /// of the output; `None` once the export's audio is complete.
209    ///
210    /// # Errors
211    /// Says why the mix could not be produced.
212    fn next_block(&mut self) -> Result<Option<alpha2_media::AudioBlock>, String>;
213}
214
215/// The audio a packet copy writes beside its video.
216pub enum CopyAudio<'a> {
217    /// None.
218    None,
219    /// The plan's copied audio packets; see [`PacketCopyPlan::copies_audio`].
220    Source,
221    /// The export's mix, encoded to AAC.
222    Encode {
223        /// The encoder's settings.
224        config: crate::AacStreamConfig,
225        /// Where the mix comes from.
226        feed: &'a mut dyn AudioBlockFeed,
227    },
228}
229
230/// What a copy wrote.
231#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
232pub struct PacketCopyReport {
233    /// Video packets written.
234    pub video_packets: u64,
235    /// Audio packets copied.
236    pub audio_packets: u64,
237    /// Audio sample frames encoded from the mix.
238    pub encoded_audio_frames: u64,
239    /// Payload bytes written, both streams.
240    pub bytes: u64,
241}
242
243/// One video packet as the scan saw it.
244#[derive(Clone)]
245struct ScannedPacket {
246    pts: i64,
247    dts: i64,
248    idr: bool,
249    /// The stream's sample description changes here
250    /// (`AV_PKT_DATA_NEW_EXTRADATA`).
251    new_description: bool,
252}
253
254/// Decides whether `request` can be served by copying packets.
255///
256/// A host planning several runs of one export uses one
257/// [`PacketCopyPlanner`], which reads each source once.
258///
259/// # Errors
260/// Names why not: the container, codec, size, color, or frame timing that a
261/// copy would not reproduce, or a run that does not start at an IDR picture.
262pub fn plan_packet_copy(request: &PacketCopyRequest<'_>) -> Result<PacketCopyPlan, String> {
263    PacketCopyPlanner::new(CancelToken::default()).plan(request)
264}
265
266/// Plans the runs of one export, reading each source file once however many
267/// runs it serves, and stopping when the export is cancelled.
268pub struct PacketCopyPlanner {
269    cancel: CancelToken,
270    sources: std::collections::HashMap<PathBuf, SourceFacts>,
271}
272
273/// What a plan learns of one source file, each part read when first needed.
274#[derive(Default)]
275struct SourceFacts {
276    probe: Option<Result<crate::ProbeResult, String>>,
277    video: Option<Result<VideoParameters, String>>,
278    scan: Option<Result<std::rc::Rc<Scan>, String>>,
279}
280
281/// The selected video stream's codec parameters a copy depends on.
282#[derive(Clone)]
283struct VideoParameters {
284    codec_id: ffi::AVCodecID,
285    format: i32,
286    field_order: ffi::AVFieldOrder,
287    sample_aspect_ratio: AVRational,
288    extradata: Vec<u8>,
289    time_base: AVRational,
290    width: i32,
291    height: i32,
292    tags: (
293        ffi::AVColorPrimaries,
294        ffi::AVColorTransferCharacteristic,
295        ffi::AVColorSpace,
296        ffi::AVColorRange,
297    ),
298}
299
300impl PacketCopyPlanner {
301    /// A planner that gives up as soon as `cancel` is set.
302    #[must_use]
303    pub fn new(cancel: CancelToken) -> Self {
304        Self {
305            cancel,
306            sources: std::collections::HashMap::new(),
307        }
308    }
309
310    /// [`plan_packet_copy`], from what this planner has read of the source.
311    ///
312    /// # Errors
313    /// As [`plan_packet_copy`], and a cancel.
314    pub fn plan(&mut self, request: &PacketCopyRequest<'_>) -> Result<PacketCopyPlan, String> {
315        if request.frame_times.is_empty() {
316            return Err("the range has no frames".to_owned());
317        }
318        let probed = self.probe(request.source)?;
319        let (video_index, audio_stream) = check_metadata(&probed, request)?;
320        let parameters = self.video(request.source, video_index)?;
321        let (length_size, contract) = stream_framing(request, &parameters)?;
322        let time_base = parameters.time_base;
323        let expected = request
324            .frame_times
325            .iter()
326            .map(|time| av_rescale_q(time.ticks(), tick_base(), time_base))
327            .collect::<Vec<_>>();
328        // Exact frame identity: the ticks each expected pts converts back to must be the frame time
329        // itself, or the grid does not land on the stream's time base.
330        if expected
331            .iter()
332            .zip(request.frame_times)
333            .any(|(pts, time)| av_rescale_q(*pts, time_base, tick_base()) != time.ticks())
334        {
335            return Err(
336                "the export's frame times do not land on the source's time base".to_owned(),
337            );
338        }
339        let scanned = self.scan(
340            request.source,
341            video_index,
342            probed.selected_audio,
343            request.codec,
344            length_size,
345        )?;
346        plan_run(
347            request,
348            &scanned,
349            &expected,
350            video_index,
351            audio_stream,
352            contract,
353        )
354    }
355
356    fn facts(&mut self, source: &Path) -> &mut SourceFacts {
357        self.sources.entry(source.to_owned()).or_default()
358    }
359
360    fn probe(&mut self, source: &Path) -> Result<crate::ProbeResult, String> {
361        self.facts(source)
362            .probe
363            .get_or_insert_with(|| {
364                crate::probe(source).map_err(|error| format!("probe failed: {error}"))
365            })
366            .clone()
367    }
368
369    fn video(&mut self, source: &Path, index: usize) -> Result<VideoParameters, String> {
370        self.facts(source)
371            .video
372            .get_or_insert_with(|| video_parameters(source, index))
373            .clone()
374    }
375
376    fn scan(
377        &mut self,
378        source: &Path,
379        index: usize,
380        audio: Option<usize>,
381        codec: NativeVideoCodec,
382        length_size: usize,
383    ) -> Result<std::rc::Rc<Scan>, String> {
384        let cancel = self.cancel.clone();
385        self.facts(source)
386            .scan
387            .get_or_insert_with(|| {
388                scan(source, index, codec, length_size, audio, &cancel).map(std::rc::Rc::new)
389            })
390            .clone()
391    }
392}
393
394/// The plan of one run, from its source's scan.
395fn plan_run(
396    request: &PacketCopyRequest<'_>,
397    scanned: &Scan,
398    expected: &[i64],
399    video_index: usize,
400    audio_stream: Option<Result<usize, String>>,
401    contract: StreamContract,
402) -> Result<PacketCopyPlan, String> {
403    let time_base = AVRational {
404        num: contract.time_base.0,
405        den: contract.time_base.1,
406    };
407    let packets = &scanned.video;
408    let first = packets
409        .iter()
410        .position(|packet| packet.pts == expected[0])
411        .ok_or_else(|| "no source frame is at the range's first frame time".to_owned())?;
412    let last = first + expected.len() - 1;
413    let run = packets
414        .get(first..=last)
415        .ok_or_else(|| "the source ends before the range does".to_owned())?;
416    if !run[0].idr {
417        return Err(
418            "the range does not start at an IDR picture, so its first frames may refer to earlier ones"
419                .to_owned(),
420        );
421    }
422    // The stream's parameter sets are its first sample description's; a run under another is
423    // decoded with the wrong ones.
424    if packets[1..=last]
425        .iter()
426        .any(|packet| packet.new_description)
427    {
428        return Err(
429            "the source changes its sample description (its parameter sets) before or inside the range, and a copy carries only the first"
430                .to_owned(),
431        );
432    }
433    let mut presented = run.iter().map(|packet| packet.pts).collect::<Vec<_>>();
434    presented.sort_unstable();
435    if presented != expected {
436        return Err(
437            "the packets under the range are not exactly its frames: the source is off the export's frame grid, or a picture in it depends on one outside it"
438                .to_owned(),
439        );
440    }
441    // Anywhere but the whole file the boundary audio packets straddle the range, and cutting them
442    // is not a copy.
443    let (audio_stream, audio_refusal) = match audio_stream {
444        Some(Ok(_)) if first != 0 || last + 1 != packets.len() => (
445            None,
446            Some("audio is copied only with the whole of its video".to_owned()),
447        ),
448        Some(Ok(index)) => match check_audio_span(scanned.audio, expected[0], time_base, request) {
449            Ok(()) => (Some(index), None),
450            Err(reason) => (None, Some(reason)),
451        },
452        Some(Err(reason)) => (None, Some(reason)),
453        None => (None, None),
454    };
455    let offset = av_rescale_q(request.output_start.ticks(), tick_base(), time_base);
456    let output_decode = |dts: i64| av_rescale_q(dts - expected[0] + offset, time_base, tick_base());
457    let decode_span = (
458        output_decode(run.iter().map(|packet| packet.dts).min().unwrap_or(0)),
459        output_decode(run.iter().map(|packet| packet.dts).max().unwrap_or(0)),
460    );
461    Ok(PacketCopyPlan {
462        source: request.source.to_owned(),
463        video_stream: video_index,
464        audio_stream,
465        audio_refusal,
466        first_packet: u64::try_from(first).unwrap_or(u64::MAX),
467        last_packet: u64::try_from(last).unwrap_or(u64::MAX),
468        first_timestamps: (run[0].pts, run[0].dts),
469        origin: expected[0],
470        offset,
471        video_time_base: time_base,
472        contract,
473        decode_span,
474    })
475}
476
477/// The container and stream metadata a copy keeps: the video stream, and
478/// the audio stream when the request copies audio.
479fn check_metadata(
480    probed: &crate::ProbeResult,
481    request: &PacketCopyRequest<'_>,
482) -> Result<(usize, Option<Result<usize, String>>), String> {
483    if !probed
484        .format
485        .split(',')
486        .any(|name| matches!(name, "mov" | "mp4"))
487    {
488        return Err(format!(
489            "the source is {}, and only MP4/MOV sources are copied",
490            probed.format
491        ));
492    }
493    let video_index = probed
494        .selected_video
495        .ok_or_else(|| "the source has no video stream".to_owned())?;
496    check_picture(
497        probed,
498        video_index,
499        (request.width, request.height),
500        request.color,
501    )?;
502    let audio_stream = request.audio.map(|audio| copyable_audio(probed, audio));
503
504    Ok((video_index, audio_stream))
505}
506
507/// Whether the video stream's pictures, shown as they are, already are the
508/// output's: its size, upright with square pixels, tagged with its color.
509fn check_picture(
510    probed: &crate::ProbeResult,
511    video_index: usize,
512    (width, height): (u32, u32),
513    color: ColorMetadata,
514) -> Result<(), String> {
515    let video = probed
516        .streams
517        .iter()
518        .find(|stream| stream.index == video_index)
519        .ok_or_else(|| "the source's video stream is missing".to_owned())?;
520    if video.dimensions != Some((width, height)) {
521        return Err(format!(
522            "the source is {:?}, not the {width}x{height} output",
523            video.dimensions
524        ));
525    }
526    if video.orientation.mirror {
527        return Err(
528            "the source mirrors the picture in its display matrix, which a render applies"
529                .to_owned(),
530        );
531    }
532    if video.orientation.rotation != alpha2_media::Rotation::Deg0 {
533        return Err("the source is rotated".to_owned());
534    }
535    let aspect = video.sample_aspect_ratio;
536    if aspect != alpha2_media::SampleAspectRatio::default() {
537        return Err(format!(
538            "the source shows {}:{} pixels, which a render makes square",
539            aspect.num, aspect.den
540        ));
541    }
542    if video.color != Some(color) {
543        return Err(format!(
544            "the source is tagged {:?}, and the output must be tagged {color:?}",
545            video.color
546        ));
547    }
548    Ok(())
549}
550
551/// Whether the codec parameters a copy carries into the output show its
552/// pictures as a render does. A render shows the pixel aspect ratio
553/// [`check_picture`] reads: the container's declaration, which overrides the
554/// codec's. A copy carries the codec's (in the bitstream, and in the `pasp`
555/// box the output then writes), so a codec ratio a container overrides to
556/// square pixels would show otherwise. A YUV transcode encodes its own
557/// parameters, so only a copy asks this.
558fn check_carried_display(aspect: AVRational) -> Result<(), String> {
559    if aspect.num > 0 && aspect.den > 0 && aspect.num != aspect.den {
560        return Err(format!(
561            "the source's codec declares {}:{} pixels, which a copy keeps and its container overrides to square",
562            aspect.num, aspect.den
563        ));
564    }
565    Ok(())
566}
567
568/// Whether the stream's pictures are progressive 8-bit 4:2:0 in one of
569/// `formats`.
570fn check_sampling(
571    format: i32,
572    field_order: ffi::AVFieldOrder,
573    formats: &[ffi::AVPixelFormat],
574) -> Result<(), String> {
575    if !formats.contains(&format) {
576        return Err("the source is not 8-bit 4:2:0".to_owned());
577    }
578    if !matches!(
579        field_order,
580        ffi::AV_FIELD_PROGRESSIVE | ffi::AV_FIELD_UNKNOWN
581    ) {
582        return Err("the source is interlaced".to_owned());
583    }
584    Ok(())
585}
586
587/// Decides whether `source`'s decoded pictures can become a
588/// `width`x`height` output's frames as they are, with no conversion: the
589/// file side of a YUV transcode, whose project side is the same untouched
590/// tiling a copy needs.
591///
592/// Unlike a copy, any container and codec qualify, and the frame grid is
593/// free: the decoder picks each frame's picture exactly as it does for a
594/// render, so only what the render would have changed (size, orientation,
595/// pixel shape, color, sampling, and where the chroma samples sit) decides.
596/// The output is encoded as `encoding`.
597///
598/// # Errors
599/// Names what the render would have changed.
600pub fn check_yuv_transcode(
601    source: &Path,
602    width: u32,
603    height: u32,
604    encoding: YuvEncoding,
605) -> Result<(), String> {
606    let input =
607        crate::decode::open_input(source).map_err(|error| format!("probe failed: {error}"))?;
608    let probed = crate::decode::probe_context(&input, source)
609        .map_err(|error| format!("probe failed: {error}"))?;
610    let video_index = probed
611        .selected_video
612        .ok_or_else(|| "the source has no video stream".to_owned())?;
613    check_picture(&probed, video_index, (width, height), encoding.color)?;
614    let parameters = input.streams()[video_index].codecpar();
615    check_sampling(
616        parameters.format,
617        parameters.field_order,
618        &[ffi::AV_PIX_FMT_YUV420P, ffi::AV_PIX_FMT_NV12],
619    )?;
620    let siting =
621        crate::convert::decoded_chroma_location(parameters.codec_id, parameters.chroma_location);
622    if !same_chroma_siting(siting, encoding.chroma) {
623        return Err(format!(
624            "the source's chroma sits {siting:?}, and the output's must sit {:?}",
625            encoding.chroma
626        ));
627    }
628    Ok(())
629}
630
631/// Whether chroma sited at `source` is where `output` puts it, reading an
632/// unspecified location as centred, as import does.
633#[must_use]
634pub fn same_chroma_siting(source: ChromaLocation, output: ChromaLocation) -> bool {
635    let centred = |location| {
636        if location == ChromaLocation::Unspecified {
637            ChromaLocation::Center
638        } else {
639            location
640        }
641    };
642    centred(source) == centred(output)
643}
644
645/// The video stream's codec parameters, read from the file.
646fn video_parameters(source: &Path, index: usize) -> Result<VideoParameters, String> {
647    let input = open(source)?;
648    let stream = input
649        .streams()
650        .get(index)
651        .ok_or_else(|| "the source's video stream is missing".to_owned())?;
652    let parameters = stream.codecpar();
653    Ok(VideoParameters {
654        codec_id: parameters.codec_id,
655        format: parameters.format,
656        field_order: parameters.field_order,
657        sample_aspect_ratio: parameters.sample_aspect_ratio,
658        extradata: alpha2_ffmpeg_bridge::codec_parameters_extradata(&parameters),
659        time_base: stream.time_base,
660        width: parameters.width,
661        height: parameters.height,
662        tags: (
663            parameters.color_primaries,
664            parameters.color_trc,
665            parameters.color_space,
666            parameters.color_range,
667        ),
668    })
669}
670
671/// The video stream's NAL framing and sample description, after the checks
672/// only the codec parameters answer.
673fn stream_framing(
674    request: &PacketCopyRequest<'_>,
675    parameters: &VideoParameters,
676) -> Result<(usize, StreamContract), String> {
677    if parameters.codec_id != request.codec.codec_id() {
678        return Err(format!(
679            "the source's video is not {}",
680            request.codec.as_str()
681        ));
682    }
683    check_sampling(
684        parameters.format,
685        parameters.field_order,
686        &[ffi::AV_PIX_FMT_YUV420P],
687    )?;
688    check_carried_display(parameters.sample_aspect_ratio)?;
689    // A copy keeps the stream's own tags, so they must be the output's exactly, not a summary that
690    // reads an unspecified one as BT.709.
691    if parameters.tags != crate::convert::color_tags(request.color) {
692        return Err(format!(
693            "the source is tagged primaries {}, transfer {}, matrix {}, range {} (FFmpeg's numbers), and the output must be tagged {:?}",
694            parameters.tags.0,
695            parameters.tags.1,
696            parameters.tags.2,
697            parameters.tags.3,
698            request.color
699        ));
700    }
701    let length_size = nal_length_size(request.codec, &parameters.extradata)?;
702    Ok((
703        length_size,
704        StreamContract {
705            codec_id: parameters.codec_id,
706            width: parameters.width,
707            height: parameters.height,
708            format: parameters.format,
709            extradata: parameters.extradata.clone(),
710            time_base: (parameters.time_base.num, parameters.time_base.den),
711        },
712    ))
713}
714
715/// The source's audio stream, when it is what the export would mix.
716fn copyable_audio(probed: &crate::ProbeResult, audio: PacketCopyAudio) -> Result<usize, String> {
717    let index = probed
718        .selected_audio
719        .ok_or_else(|| "the source has no audio stream to copy".to_owned())?;
720    let stream = probed
721        .streams
722        .iter()
723        .find(|stream| stream.index == index && stream.kind == StreamKind::Audio)
724        .ok_or_else(|| "the source's audio stream is missing".to_owned())?;
725    if stream.codec != "aac" {
726        return Err(format!("the source's audio is {}, not AAC", stream.codec));
727    }
728    if stream.sample_rate != Some(audio.sample_rate) || stream.channels != Some(audio.channels) {
729        return Err(format!(
730            "the source's audio is {:?} Hz x {:?}, not the export's {} Hz x {}",
731            stream.sample_rate, stream.channels, audio.sample_rate, audio.channels
732        ));
733    }
734    Ok(index)
735}
736
737fn open(path: &Path) -> Result<DemuxInput, String> {
738    crate::decode::open_input(path).map_err(|error| format!("cannot open the source: {error}"))
739}
740
741/// The byte width of each NAL unit's length prefix, from the sample
742/// description. Only length-prefixed (MP4) streams are copied: an Annex B
743/// stream would need its framing rewritten, which is not a copy.
744pub(crate) fn nal_length_size(codec: NativeVideoCodec, extradata: &[u8]) -> Result<usize, String> {
745    let annex_b = || "the source's parameter sets are not in MP4 form".to_owned();
746    match codec {
747        NativeVideoCodec::H264 if extradata.len() >= 7 && extradata[0] == 1 => {
748            Ok(usize::from(extradata[4] & 0b11) + 1)
749        }
750        NativeVideoCodec::Hevc if extradata.len() >= 23 && extradata[0] == 1 => {
751            Ok(usize::from(extradata[21] & 0b11) + 1)
752        }
753        NativeVideoCodec::H264 | NativeVideoCodec::Hevc => Err(annex_b()),
754        other => Err(format!(
755            "{} is not copied; only H.264 and HEVC are",
756            other.as_str()
757        )),
758    }
759}
760
761/// Whether a length-prefixed access unit holds an IDR picture.
762pub(crate) fn is_idr(codec: NativeVideoCodec, length_size: usize, data: &[u8]) -> bool {
763    let mut at = 0_usize;
764    while at + length_size < data.len() {
765        let length = data[at..at + length_size]
766            .iter()
767            .fold(0_usize, |value, byte| (value << 8) | usize::from(*byte));
768        let Some(&header) = data.get(at + length_size) else {
769            return false;
770        };
771        let idr = match codec {
772            NativeVideoCodec::H264 => header & 0x1f == 5,
773            // IDR_W_RADL and IDR_N_LP. A CRA may be followed by pictures that refer to the previous
774            // GOP, so it does not count.
775            NativeVideoCodec::Hevc => matches!((header >> 1) & 0x3f, 19 | 20),
776            _ => false,
777        };
778        if idr {
779            return true;
780        }
781        at = at.saturating_add(length_size).saturating_add(length);
782    }
783    false
784}
785
786/// What a plan reads of a source's packets.
787struct Scan {
788    /// Every video packet, in decode order.
789    video: Vec<ScannedPacket>,
790    /// The span of the audio packets a copy would carry, with their time
791    /// base; `None` when the source has no audio stream or no timestamped
792    /// audio packet.
793    audio: Option<AudioSpan>,
794}
795
796/// The timestamps a copy would carry of the source's audio.
797#[derive(Clone, Copy)]
798struct AudioSpan {
799    first: i64,
800    last: i64,
801    /// Where the last packet ends: the latest presentation time plus its
802    /// duration, and that duration.
803    end: i64,
804    last_duration: i64,
805    time_base: AVRational,
806}
807
808/// Reads the source through once: every packet of video stream `index`,
809/// and the timestamps of audio stream `audio`'s. Every other stream is
810/// skipped by the demuxer, and `cancel` stops it between packets.
811fn scan(
812    path: &Path,
813    index: usize,
814    codec: NativeVideoCodec,
815    length_size: usize,
816    audio: Option<usize>,
817    cancel: &CancelToken,
818) -> Result<Scan, String> {
819    let mut input = open(path)?;
820    crate::decode::interrupt_on_cancel(&input, cancel);
821    input.keep_only(
822        &[Some(index), audio]
823            .into_iter()
824            .flatten()
825            .collect::<Vec<_>>(),
826    );
827    let audio_time_base =
828        audio.and_then(|audio| input.streams().get(audio).map(|stream| stream.time_base));
829    let mut packets = Vec::new();
830    let mut audio_span: Option<(i64, i64, i64, i64)> = None;
831    let cancelled = || "the export was cancelled while its copy was planned".to_owned();
832    loop {
833        if cancel.is_cancelled() {
834            return Err(cancelled());
835        }
836        let packet = match input.read_packet() {
837            Ok(Some(packet)) => packet,
838            Ok(None) => break,
839            Err(_) if cancel.is_cancelled() => return Err(cancelled()),
840            Err(error) => return Err(format!("cannot read the source: {error}")),
841        };
842        // A watcher outside (a host's) sees the scan alive, not blocked, packet by packet.
843        crate::recovery::report_decode_progress();
844        let stream = usize::try_from(packet.stream_index).ok();
845        if stream.is_some() && stream == audio {
846            for time in [packet.pts, packet.dts] {
847                if time != ffi::AV_NOPTS_VALUE {
848                    let span = audio_span.get_or_insert((time, time, time, 0));
849                    span.0 = span.0.min(time);
850                    span.1 = span.1.max(time);
851                }
852            }
853            if packet.pts != ffi::AV_NOPTS_VALUE {
854                let end = packet.pts.saturating_add(packet.duration.max(0));
855                if let Some(span) = audio_span.as_mut()
856                    && end >= span.2
857                {
858                    span.2 = end;
859                    span.3 = packet.duration.max(0);
860                }
861            }
862            continue;
863        }
864        if stream != Some(index) {
865            continue;
866        }
867        if packet.pts == ffi::AV_NOPTS_VALUE {
868            return Err("a source video packet has no presentation time".to_owned());
869        }
870        let key = packet.flags & i32::try_from(ffi::AV_PKT_FLAG_KEY).unwrap_or(0) != 0;
871        packets.push(ScannedPacket {
872            pts: packet.pts,
873            dts: if packet.dts == ffi::AV_NOPTS_VALUE {
874                packet.pts
875            } else {
876                packet.dts
877            },
878            idr: key
879                && is_idr(
880                    codec,
881                    length_size,
882                    &alpha2_ffmpeg_bridge::packet_bytes(&packet),
883                ),
884            new_description: alpha2_ffmpeg_bridge::packet_has_side_data(
885                &packet,
886                ffi::AV_PKT_DATA_NEW_EXTRADATA,
887            ),
888        });
889    }
890    Ok(Scan {
891        video: packets,
892        audio: audio_span.zip(audio_time_base).map(
893            |((first, last, end, last_duration), time_base)| AudioSpan {
894                first,
895                last,
896                end,
897                last_duration,
898                time_base,
899            },
900        ),
901    })
902}
903
904/// How far outside the copied video's span copied audio may reach, in
905/// ticks: more than an AAC encoder's priming before it and the last video
906/// frame's duration after it, together.
907const COPIED_AUDIO_SLACK: i64 = alpha2_time::TICKS_PER_SECOND;
908
909/// Whether the audio a copy would carry lands where the render's mix does:
910/// every timestamp, moved by the shift that puts the video's first frame at
911/// its place in the output, within the output's video, give or take
912/// [`COPIED_AUDIO_SLACK`]. The timestamps are the file's own, unchecked by
913/// the demuxer (a fragment's `tfdt` is any 64-bit value), so the shift is
914/// checked too: audio that cannot be moved, or would play elsewhere, is not
915/// the mix.
916fn check_audio_span(
917    span: Option<AudioSpan>,
918    origin: i64,
919    video_time_base: AVRational,
920    request: &PacketCopyRequest<'_>,
921) -> Result<(), String> {
922    let Some(span) = span else {
923        return Ok(());
924    };
925    let out_of_range = || {
926        "the source's audio timestamps lie outside its video, so copying them would not play the render's mix".to_owned()
927    };
928    let output_start = request.output_start.ticks();
929    let video_ticks = request
930        .frame_times
931        .last()
932        .map_or(0, |last| last.ticks() - request.frame_times[0].ticks());
933    let earliest = output_start.saturating_sub(COPIED_AUDIO_SLACK);
934    let latest = output_start
935        .saturating_add(video_ticks)
936        .saturating_add(COPIED_AUDIO_SLACK);
937    let shift_by = av_rescale_q(
938        origin - av_rescale_q(output_start, tick_base(), video_time_base),
939        video_time_base,
940        span.time_base,
941    );
942    for time in [span.first, span.last] {
943        let moved = time.checked_sub(shift_by).ok_or_else(out_of_range)?;
944        let ticks = av_rescale_q(moved, span.time_base, tick_base());
945        if ticks == ffi::AV_NOPTS_VALUE || !(earliest..=latest).contains(&ticks) {
946            return Err(out_of_range());
947        }
948    }
949    // A render mixes exactly the range, so the copied audio may not run on past the last frame's
950    // end by more than its own last packet, which a frame cannot be cut from.
951    let frames = i64::try_from(request.frame_times.len()).unwrap_or(i64::MAX);
952    let frame_ticks = if frames > 1 {
953        video_ticks / (frames - 1)
954    } else {
955        0
956    };
957    let video_end = output_start
958        .saturating_add(video_ticks)
959        .saturating_add(frame_ticks);
960    let end = av_rescale_q(
961        span.end.checked_sub(shift_by).ok_or_else(out_of_range)?,
962        span.time_base,
963        tick_base(),
964    );
965    let last_packet = av_rescale_q(span.last_duration, span.time_base, tick_base());
966    if end > video_end.saturating_add(last_packet) {
967        return Err(format!(
968            "the source's audio outlasts its video by {} ms, and a render mixes only the range",
969            (end - video_end) * 1_000 / alpha2_time::TICKS_PER_SECOND
970        ));
971    }
972    Ok(())
973}
974
975/// Writes `copy`'s runs, back to back, into a new MP4 at `output`, with
976/// `audio`.
977///
978/// The output is transactional, as every export's is: it is written beside
979/// `output` and renamed into place only when complete, and a failure or a
980/// cancel removes it. Encoded audio is interleaved with the video as it is
981/// written, a little ahead of it, so neither stream piles up in the muxer.
982/// `progress` is called with the video packets written.
983///
984/// # Errors
985/// Returns an adapter failure for an existing output, a partial a running export holds, a read
986/// or write failure, a failed mix, audio the plan does not copy, or a cancel.
987pub fn copy_packets(
988    copy: &PacketCopy,
989    output: &Path,
990    audio: CopyAudio<'_>,
991    cancel: &CancelToken,
992    progress: &mut dyn FnMut(u64),
993) -> Result<PacketCopyReport, EncodeError> {
994    let first = &copy.segments[0];
995    let input = crate::decode::open_input(&first.source).map_err(encode_adapter_error)?;
996    let video_stream = {
997        let source = &input.streams()[first.video_stream];
998        VideoStream {
999            parameters: source.codecpar().clone(),
1000            time_base: source.time_base,
1001            copied: true,
1002        }
1003    };
1004    let audio_stream = match &audio {
1005        CopyAudio::None => None,
1006        CopyAudio::Source => {
1007            if !copy.copies_audio() {
1008                return Err(EncodeError::Adapter("the plan copies no audio".into()));
1009            }
1010            let index = first.audio_stream.unwrap_or(0);
1011            let source = &input.streams()[index];
1012            Some(AudioStream::Copy {
1013                parameters: source.codecpar().clone(),
1014                time_base: source.time_base,
1015            })
1016        }
1017        CopyAudio::Encode { config, .. } => Some(AudioStream::Encode(*config)),
1018    };
1019    drop(input);
1020    let copied_audio = matches!(audio, CopyAudio::Source);
1021    let mut writer = RunWriter {
1022        muxer: Mp4Muxer::open_streams(output, video_stream, audio_stream)?,
1023        mix: match audio {
1024            CopyAudio::Encode { feed, .. } => Some(MixWriter {
1025                feed,
1026                covered: 0,
1027                done: false,
1028            }),
1029            CopyAudio::None | CopyAudio::Source => None,
1030        },
1031        stages: crate::EncodeStageTimings::default(),
1032        report: PacketCopyReport::default(),
1033    };
1034    for segment in &copy.segments {
1035        writer.write_run(segment, copied_audio, cancel, progress)?;
1036    }
1037    if writer.report.video_packets != copy.video_packets() {
1038        return Err(EncodeError::Adapter(format!(
1039            "the copy wrote {} video packets of the planned {}",
1040            writer.report.video_packets,
1041            copy.video_packets()
1042        )));
1043    }
1044    let RunWriter {
1045        mut muxer,
1046        mut mix,
1047        mut stages,
1048        mut report,
1049    } = writer;
1050    if let Some(mix) = mix.as_mut() {
1051        mix.write_until(i64::MAX, &mut muxer, &mut stages, &mut report)?;
1052    }
1053    muxer.finish_audio()?;
1054    muxer.write_ready_audio(true, &mut stages)?;
1055    muxer.close()?;
1056    Ok(report)
1057}
1058
1059/// The output a copy writes its runs into, and the mix it interleaves.
1060struct RunWriter<'a> {
1061    muxer: Mp4Muxer,
1062    mix: Option<MixWriter<'a>>,
1063    stages: crate::EncodeStageTimings,
1064    report: PacketCopyReport,
1065}
1066
1067impl RunWriter<'_> {
1068    /// Copies one run's packets, and its source's audio packets when
1069    /// `copied_audio` (only ever for a single whole file).
1070    fn write_run(
1071        &mut self,
1072        run: &PacketCopyPlan,
1073        copied_audio: bool,
1074        cancel: &CancelToken,
1075        progress: &mut dyn FnMut(u64),
1076    ) -> Result<(), EncodeError> {
1077        let mut input = crate::decode::open_input(&run.source).map_err(encode_adapter_error)?;
1078        crate::decode::interrupt_on_cancel(&input, cancel);
1079        let audio_stream = copied_audio.then_some(run.audio_stream).flatten();
1080        input.keep_only(
1081            &[Some(run.video_stream), audio_stream]
1082                .into_iter()
1083                .flatten()
1084                .collect::<Vec<_>>(),
1085        );
1086        let shift_by = run.origin - run.offset;
1087        // A run after the first packet is sought to (its first packet is an indexed IDR). Copied
1088        // audio only travels with a whole file, which starts at the first packet.
1089        let mut video_position = if run.first_packet > 0 {
1090            let stream = i32::try_from(run.video_stream)
1091                .map_err(|_| EncodeError::Adapter("video stream index is out of range".into()))?;
1092            input
1093                .seek(
1094                    stream,
1095                    run.first_timestamps.1,
1096                    i32::try_from(ffi::AVSEEK_FLAG_BACKWARD).unwrap_or(0),
1097                )
1098                .map_err(|error| {
1099                    if cancel.is_cancelled() {
1100                        EncodeError::Cancelled
1101                    } else {
1102                        encode_adapter_error_at("seeking to the run", error)
1103                    }
1104                })?;
1105            None
1106        } else {
1107            Some(0_u64)
1108        };
1109        while let Some(mut packet) = input.read_packet().map_err(|error| {
1110            if cancel.is_cancelled() {
1111                EncodeError::Cancelled
1112            } else {
1113                encode_adapter_error(error)
1114            }
1115        })? {
1116            if cancel.is_cancelled() {
1117                return Err(EncodeError::Cancelled);
1118            }
1119            let stream = usize::try_from(packet.stream_index).ok();
1120            if stream == Some(run.video_stream) {
1121                let position = match video_position {
1122                    Some(position) => position,
1123                    None if (packet.pts, packet.dts) == run.first_timestamps => run.first_packet,
1124                    None => continue,
1125                };
1126                video_position = Some(position + 1);
1127                if position < run.first_packet {
1128                    continue;
1129                }
1130                if position > run.last_packet {
1131                    // Copied audio only travels with the whole video.
1132                    break;
1133                }
1134                packet.set_pts(shift(packet.pts, shift_by)?);
1135                packet.set_dts(shift(packet.dts, shift_by)?);
1136                packet.set_pos(-1);
1137                if let Some(mix) = self.mix.as_mut() {
1138                    let at = av_rescale_q(packet.dts, run.video_time_base, tick_base());
1139                    mix.write_until(
1140                        at.saturating_add(MIX_LEAD),
1141                        &mut self.muxer,
1142                        &mut self.stages,
1143                        &mut self.report,
1144                    )?;
1145                }
1146                self.report.bytes = self
1147                    .report
1148                    .bytes
1149                    .saturating_add(u64::try_from(packet.size).unwrap_or(0));
1150                self.muxer.write_video(&mut packet, run.video_time_base)?;
1151                self.report.video_packets += 1;
1152                progress(self.report.video_packets);
1153            } else if stream.is_some() && stream == audio_stream {
1154                let source_base = input.streams()[run.audio_stream.unwrap_or(0)].time_base;
1155                // The same zero as the video's; audio keeps its own offset from it, priming
1156                // included.
1157                let origin = av_rescale_q(shift_by, run.video_time_base, source_base);
1158                packet.set_pts(shift(packet.pts, origin)?);
1159                packet.set_dts(shift(packet.dts, origin)?);
1160                packet.set_pos(-1);
1161                self.report.bytes = self
1162                    .report
1163                    .bytes
1164                    .saturating_add(u64::try_from(packet.size).unwrap_or(0));
1165                self.muxer.write_copied_audio(&mut packet)?;
1166                self.report.audio_packets += 1;
1167            }
1168        }
1169        Ok(())
1170    }
1171}
1172
1173/// How far ahead of the video the encoded mix is written, in ticks: enough
1174/// that the muxer always has audio to interleave, little enough that neither
1175/// stream piles up in it.
1176const MIX_LEAD: i64 = alpha2_time::TICKS_PER_SECOND / 10;
1177
1178/// The export's mix, written into the muxer as the video advances.
1179struct MixWriter<'a> {
1180    feed: &'a mut dyn AudioBlockFeed,
1181    /// Output time the mix written so far reaches, in ticks.
1182    covered: i64,
1183    done: bool,
1184}
1185
1186impl MixWriter<'_> {
1187    /// Writes blocks until the mix reaches `target`, or ends.
1188    fn write_until(
1189        &mut self,
1190        target: i64,
1191        muxer: &mut Mp4Muxer,
1192        stages: &mut crate::EncodeStageTimings,
1193        report: &mut PacketCopyReport,
1194    ) -> Result<(), EncodeError> {
1195        while !self.done && self.covered < target {
1196            let Some(block) = self.feed.next_block().map_err(EncodeError::Adapter)? else {
1197                self.done = true;
1198                break;
1199            };
1200            let frames = block.channels.first().map_or(0, Vec::len);
1201            muxer.write_audio(&block, stages)?;
1202            report.encoded_audio_frames = report
1203                .encoded_audio_frames
1204                .saturating_add(u64::try_from(frames).unwrap_or(u64::MAX));
1205            self.covered = i64::try_from(report.encoded_audio_frames)
1206                .unwrap_or(i64::MAX)
1207                .saturating_mul(alpha2_time::TICKS_PER_SECOND)
1208                .div_euclid(i64::from(block.sample_rate.max(1)));
1209        }
1210        Ok(())
1211    }
1212}
1213
1214/// `value` moved back by `origin`, keeping a missing timestamp missing.
1215/// The plan checked the copied stream's timestamps; this still refuses one
1216/// that cannot be moved rather than overflowing.
1217fn shift(value: i64, origin: i64) -> Result<i64, EncodeError> {
1218    if value == ffi::AV_NOPTS_VALUE {
1219        return Ok(value);
1220    }
1221    value
1222        .checked_sub(origin)
1223        .filter(|moved| *moved != ffi::AV_NOPTS_VALUE)
1224        .ok_or_else(|| {
1225            EncodeError::Adapter(format!(
1226                "a source packet's timestamp {value} cannot be moved by {origin}"
1227            ))
1228        })
1229}