1use 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#[derive(Clone, Debug)]
40pub struct PacketCopyRequest<'a> {
41 pub source: &'a Path,
43 pub frame_times: &'a [MediaTime],
45 pub width: u32,
47 pub height: u32,
49 pub codec: NativeVideoCodec,
51 pub color: ColorMetadata,
54 pub output_start: MediaTime,
57 pub audio: Option<PacketCopyAudio>,
62}
63
64#[derive(Copy, Clone, Debug, Eq, PartialEq)]
66pub struct PacketCopyAudio {
67 pub sample_rate: u32,
69 pub channels: u32,
71}
72
73#[derive(Clone, Debug)]
75pub struct PacketCopyPlan {
76 source: PathBuf,
77 video_stream: usize,
78 audio_stream: Option<usize>,
79 audio_refusal: Option<String>,
81 first_packet: u64,
83 last_packet: u64,
84 first_timestamps: (i64, i64),
87 origin: i64,
90 offset: i64,
92 video_time_base: AVRational,
93 contract: StreamContract,
96 decode_span: (i64, i64),
98}
99
100#[derive(Clone, Debug, Eq, PartialEq)]
102struct StreamContract {
103 codec_id: ffi::AVCodecID,
104 width: i32,
105 height: i32,
106 format: i32,
107 extradata: Vec<u8>,
109 time_base: (i32, i32),
110}
111
112#[derive(Clone, Debug)]
114pub struct PacketCopy {
115 segments: Vec<PacketCopyPlan>,
116}
117
118impl PacketCopy {
119 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 #[must_use]
157 pub fn video_packets(&self) -> u64 {
158 self.segments
159 .iter()
160 .map(PacketCopyPlan::video_packets)
161 .sum()
162 }
163
164 #[must_use]
166 pub fn segments(&self) -> usize {
167 self.segments.len()
168 }
169
170 #[must_use]
172 pub fn copies_audio(&self) -> bool {
173 matches!(&self.segments[..], [only] if only.copies_audio())
174 }
175
176 #[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 #[must_use]
188 pub const fn video_packets(&self) -> u64 {
189 self.last_packet - self.first_packet + 1
190 }
191
192 #[must_use]
194 pub const fn copies_audio(&self) -> bool {
195 self.audio_stream.is_some()
196 }
197
198 #[must_use]
200 pub fn audio_copy_refusal(&self) -> Option<&str> {
201 self.audio_refusal.as_deref()
202 }
203}
204
205pub trait AudioBlockFeed {
207 fn next_block(&mut self) -> Result<Option<alpha2_media::AudioBlock>, String>;
213}
214
215pub enum CopyAudio<'a> {
217 None,
219 Source,
221 Encode {
223 config: crate::AacStreamConfig,
225 feed: &'a mut dyn AudioBlockFeed,
227 },
228}
229
230#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
232pub struct PacketCopyReport {
233 pub video_packets: u64,
235 pub audio_packets: u64,
237 pub encoded_audio_frames: u64,
239 pub bytes: u64,
241}
242
243#[derive(Clone)]
245struct ScannedPacket {
246 pts: i64,
247 dts: i64,
248 idr: bool,
249 new_description: bool,
252}
253
254pub fn plan_packet_copy(request: &PacketCopyRequest<'_>) -> Result<PacketCopyPlan, String> {
263 PacketCopyPlanner::new(CancelToken::default()).plan(request)
264}
265
266pub struct PacketCopyPlanner {
269 cancel: CancelToken,
270 sources: std::collections::HashMap<PathBuf, SourceFacts>,
271}
272
273#[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#[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 #[must_use]
303 pub fn new(cancel: CancelToken) -> Self {
304 Self {
305 cancel,
306 sources: std::collections::HashMap::new(),
307 }
308 }
309
310 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, ¶meters)?;
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 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
394fn 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 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 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
477fn 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
507fn 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
551fn 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
568fn 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
587pub 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#[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
645fn 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(¶meters),
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
671fn 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 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, ¶meters.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
715fn 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
741pub(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
761pub(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 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
786struct Scan {
788 video: Vec<ScannedPacket>,
790 audio: Option<AudioSpan>,
794}
795
796#[derive(Clone, Copy)]
798struct AudioSpan {
799 first: i64,
800 last: i64,
801 end: i64,
804 last_duration: i64,
805 time_base: AVRational,
806}
807
808fn 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 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
904const COPIED_AUDIO_SLACK: i64 = alpha2_time::TICKS_PER_SECOND;
908
909fn 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 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
975pub 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 = ©.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 ©.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
1059struct RunWriter<'a> {
1061 muxer: Mp4Muxer,
1062 mix: Option<MixWriter<'a>>,
1063 stages: crate::EncodeStageTimings,
1064 report: PacketCopyReport,
1065}
1066
1067impl RunWriter<'_> {
1068 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 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 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 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
1173const MIX_LEAD: i64 = alpha2_time::TICKS_PER_SECOND / 10;
1177
1178struct MixWriter<'a> {
1180 feed: &'a mut dyn AudioBlockFeed,
1181 covered: i64,
1183 done: bool,
1184}
1185
1186impl MixWriter<'_> {
1187 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
1214fn 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}