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, Muxer, 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 container: alpha2_media::Container,
54 pub color: ColorMetadata,
57 pub output_start: MediaTime,
60 pub audio: Option<PacketCopyAudio>,
65}
66
67#[derive(Copy, Clone, Debug, Eq, PartialEq)]
69pub struct PacketCopyAudio {
70 pub sample_rate: u32,
72 pub channels: u32,
74}
75
76#[derive(Clone, Debug)]
78pub struct PacketCopyPlan {
79 source: PathBuf,
80 video_stream: usize,
81 audio_stream: Option<usize>,
82 audio_refusal: Option<String>,
84 first_packet: u64,
86 last_packet: u64,
87 first_timestamps: (i64, i64),
90 origin: i64,
93 offset: i64,
95 video_time_base: AVRational,
96 contract: StreamContract,
99 decode_span: (i64, i64),
101}
102
103#[derive(Clone, Debug, Eq, PartialEq)]
105struct StreamContract {
106 codec_id: ffi::AVCodecID,
107 width: i32,
108 height: i32,
109 format: i32,
110 extradata: Vec<u8>,
112 time_base: (i32, i32),
113}
114
115#[derive(Clone, Debug)]
117pub struct PacketCopy {
118 segments: Vec<PacketCopyPlan>,
119}
120
121impl PacketCopy {
122 pub fn join(segments: Vec<PacketCopyPlan>) -> Result<Self, String> {
135 let first = segments
136 .first()
137 .ok_or_else(|| "a copy needs at least one run".to_owned())?;
138 if segments.len() > 1 && segments.iter().any(PacketCopyPlan::copies_audio) {
139 return Err("copied audio travels only with one whole file".to_owned());
140 }
141 for (index, pair) in segments.windows(2).enumerate() {
142 if pair[1].contract != first.contract {
143 return Err(format!(
144 "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",
145 index + 2
146 ));
147 }
148 if pair[1].decode_span.0 <= pair[0].decode_span.1 {
149 return Err(format!(
150 "at the join into run {}, decode times go back: the runs reorder pictures differently",
151 index + 2
152 ));
153 }
154 }
155 Ok(Self { segments })
156 }
157
158 #[must_use]
160 pub fn video_packets(&self) -> u64 {
161 self.segments
162 .iter()
163 .map(PacketCopyPlan::video_packets)
164 .sum()
165 }
166
167 #[must_use]
169 pub fn segments(&self) -> usize {
170 self.segments.len()
171 }
172
173 #[must_use]
175 pub fn copies_audio(&self) -> bool {
176 matches!(&self.segments[..], [only] if only.copies_audio())
177 }
178
179 #[must_use]
181 pub fn audio_copy_refusal(&self) -> Option<&str> {
182 self.segments
183 .first()
184 .and_then(PacketCopyPlan::audio_copy_refusal)
185 }
186}
187
188impl PacketCopyPlan {
189 #[must_use]
191 pub const fn video_packets(&self) -> u64 {
192 self.last_packet - self.first_packet + 1
193 }
194
195 #[must_use]
197 pub const fn copies_audio(&self) -> bool {
198 self.audio_stream.is_some()
199 }
200
201 #[must_use]
203 pub fn audio_copy_refusal(&self) -> Option<&str> {
204 self.audio_refusal.as_deref()
205 }
206}
207
208pub trait AudioBlockFeed {
210 fn next_block(&mut self) -> Result<Option<alpha2_media::AudioBlock>, String>;
216}
217
218pub enum CopyAudio<'a> {
220 None,
222 Source,
224 Encode {
226 config: crate::AudioStreamConfig,
228 feed: &'a mut dyn AudioBlockFeed,
230 },
231}
232
233#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
235pub struct PacketCopyReport {
236 pub video_packets: u64,
238 pub audio_packets: u64,
240 pub encoded_audio_frames: u64,
242 pub bytes: u64,
244}
245
246#[derive(Clone)]
248struct ScannedPacket {
249 pts: i64,
250 dts: i64,
251 idr: bool,
252 new_description: bool,
255}
256
257pub fn plan_packet_copy(request: &PacketCopyRequest<'_>) -> Result<PacketCopyPlan, String> {
266 PacketCopyPlanner::new(CancelToken::default()).plan(request)
267}
268
269pub struct PacketCopyPlanner {
272 cancel: CancelToken,
273 sources: std::collections::HashMap<PathBuf, SourceFacts>,
274}
275
276#[derive(Default)]
278struct SourceFacts {
279 probe: Option<Result<crate::ProbeResult, String>>,
280 video: Option<Result<VideoParameters, String>>,
281 scan: Option<Result<std::rc::Rc<Scan>, String>>,
282}
283
284#[derive(Clone)]
286struct VideoParameters {
287 codec_id: ffi::AVCodecID,
288 format: i32,
289 field_order: ffi::AVFieldOrder,
290 sample_aspect_ratio: AVRational,
291 extradata: Vec<u8>,
292 time_base: AVRational,
293 width: i32,
294 height: i32,
295 tags: (
296 ffi::AVColorPrimaries,
297 ffi::AVColorTransferCharacteristic,
298 ffi::AVColorSpace,
299 ffi::AVColorRange,
300 ),
301}
302
303impl PacketCopyPlanner {
304 #[must_use]
306 pub fn new(cancel: CancelToken) -> Self {
307 Self {
308 cancel,
309 sources: std::collections::HashMap::new(),
310 }
311 }
312
313 pub fn plan(&mut self, request: &PacketCopyRequest<'_>) -> Result<PacketCopyPlan, String> {
318 if request.frame_times.is_empty() {
319 return Err("the range has no frames".to_owned());
320 }
321 let probed = self.probe(request.source)?;
322 let (video_index, audio_stream) = check_metadata(&probed, request)?;
323 let parameters = self.video(request.source, video_index)?;
324 let (length_size, contract) = stream_framing(request, ¶meters)?;
325 let time_base = parameters.time_base;
326 let expected = request
327 .frame_times
328 .iter()
329 .map(|time| av_rescale_q(time.ticks(), tick_base(), time_base))
330 .collect::<Vec<_>>();
331 if expected
334 .iter()
335 .zip(request.frame_times)
336 .any(|(pts, time)| av_rescale_q(*pts, time_base, tick_base()) != time.ticks())
337 {
338 return Err(
339 "the export's frame times do not land on the source's time base".to_owned(),
340 );
341 }
342 let scanned = self.scan(
343 request.source,
344 video_index,
345 probed.selected_audio,
346 request.codec,
347 length_size,
348 )?;
349 plan_run(
350 request,
351 &scanned,
352 &expected,
353 video_index,
354 audio_stream,
355 contract,
356 )
357 }
358
359 fn facts(&mut self, source: &Path) -> &mut SourceFacts {
360 self.sources.entry(source.to_owned()).or_default()
361 }
362
363 fn probe(&mut self, source: &Path) -> Result<crate::ProbeResult, String> {
364 self.facts(source)
365 .probe
366 .get_or_insert_with(|| {
367 crate::probe(source).map_err(|error| format!("probe failed: {error}"))
368 })
369 .clone()
370 }
371
372 fn video(&mut self, source: &Path, index: usize) -> Result<VideoParameters, String> {
373 self.facts(source)
374 .video
375 .get_or_insert_with(|| video_parameters(source, index))
376 .clone()
377 }
378
379 fn scan(
380 &mut self,
381 source: &Path,
382 index: usize,
383 audio: Option<usize>,
384 codec: NativeVideoCodec,
385 length_size: usize,
386 ) -> Result<std::rc::Rc<Scan>, String> {
387 let cancel = self.cancel.clone();
388 self.facts(source)
389 .scan
390 .get_or_insert_with(|| {
391 scan(source, index, codec, length_size, audio, &cancel).map(std::rc::Rc::new)
392 })
393 .clone()
394 }
395}
396
397fn plan_run(
399 request: &PacketCopyRequest<'_>,
400 scanned: &Scan,
401 expected: &[i64],
402 video_index: usize,
403 audio_stream: Option<Result<usize, String>>,
404 contract: StreamContract,
405) -> Result<PacketCopyPlan, String> {
406 let time_base = AVRational {
407 num: contract.time_base.0,
408 den: contract.time_base.1,
409 };
410 let packets = &scanned.video;
411 let first = packets
412 .iter()
413 .position(|packet| packet.pts == expected[0])
414 .ok_or_else(|| "no source frame is at the range's first frame time".to_owned())?;
415 let last = first + expected.len() - 1;
416 let run = packets
417 .get(first..=last)
418 .ok_or_else(|| "the source ends before the range does".to_owned())?;
419 if !run[0].idr {
420 return Err(
421 "the range does not start at an IDR picture, so its first frames may refer to earlier ones"
422 .to_owned(),
423 );
424 }
425 if packets[1..=last]
428 .iter()
429 .any(|packet| packet.new_description)
430 {
431 return Err(
432 "the source changes its sample description (its parameter sets) before or inside the range, and a copy carries only the first"
433 .to_owned(),
434 );
435 }
436 let mut presented = run.iter().map(|packet| packet.pts).collect::<Vec<_>>();
437 presented.sort_unstable();
438 if presented != expected {
439 return Err(
440 "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"
441 .to_owned(),
442 );
443 }
444 let (audio_stream, audio_refusal) = match audio_stream {
447 Some(Ok(_)) if first != 0 || last + 1 != packets.len() => (
448 None,
449 Some("audio is copied only with the whole of its video".to_owned()),
450 ),
451 Some(Ok(index)) => match check_audio_span(scanned.audio, expected[0], time_base, request) {
452 Ok(()) => (Some(index), None),
453 Err(reason) => (None, Some(reason)),
454 },
455 Some(Err(reason)) => (None, Some(reason)),
456 None => (None, None),
457 };
458 let offset = av_rescale_q(request.output_start.ticks(), tick_base(), time_base);
459 let output_decode = |dts: i64| av_rescale_q(dts - expected[0] + offset, time_base, tick_base());
460 let decode_span = (
461 output_decode(run.iter().map(|packet| packet.dts).min().unwrap_or(0)),
462 output_decode(run.iter().map(|packet| packet.dts).max().unwrap_or(0)),
463 );
464 Ok(PacketCopyPlan {
465 source: request.source.to_owned(),
466 video_stream: video_index,
467 audio_stream,
468 audio_refusal,
469 first_packet: u64::try_from(first).unwrap_or(u64::MAX),
470 last_packet: u64::try_from(last).unwrap_or(u64::MAX),
471 first_timestamps: (run[0].pts, run[0].dts),
472 origin: expected[0],
473 offset,
474 video_time_base: time_base,
475 contract,
476 decode_span,
477 })
478}
479
480fn check_metadata(
483 probed: &crate::ProbeResult,
484 request: &PacketCopyRequest<'_>,
485) -> Result<(usize, Option<Result<usize, String>>), String> {
486 if !probed
487 .format
488 .split(',')
489 .any(|name| matches!(name, "mov" | "mp4"))
490 {
491 return Err(format!(
492 "the source is {}, and only MP4/MOV sources are copied",
493 probed.format
494 ));
495 }
496 if !request.container.offers_video(request.codec.model()) {
497 return Err(format!(
498 "the source's {} is not offered in {}",
499 request.codec.as_str(),
500 request.container
501 ));
502 }
503 let video_index = probed
504 .selected_video
505 .ok_or_else(|| "the source has no video stream".to_owned())?;
506 check_picture(
507 probed,
508 video_index,
509 (request.width, request.height),
510 request.color,
511 )?;
512 let audio_stream = request
513 .audio
514 .map(|audio| copyable_audio(probed, audio, request.container));
515
516 Ok((video_index, audio_stream))
517}
518
519fn check_picture(
522 probed: &crate::ProbeResult,
523 video_index: usize,
524 (width, height): (u32, u32),
525 color: ColorMetadata,
526) -> Result<(), String> {
527 let video = probed
528 .streams
529 .iter()
530 .find(|stream| stream.index == video_index)
531 .ok_or_else(|| "the source's video stream is missing".to_owned())?;
532 if video.dimensions != Some((width, height)) {
533 return Err(format!(
534 "the source is {:?}, not the {width}x{height} output",
535 video.dimensions
536 ));
537 }
538 if video.orientation.mirror {
539 return Err(
540 "the source mirrors the picture in its display matrix, which a render applies"
541 .to_owned(),
542 );
543 }
544 if video.orientation.rotation != alpha2_media::Rotation::Deg0 {
545 return Err("the source is rotated".to_owned());
546 }
547 let aspect = video.sample_aspect_ratio;
548 if aspect != alpha2_media::SampleAspectRatio::default() {
549 return Err(format!(
550 "the source shows {}:{} pixels, which a render makes square",
551 aspect.num, aspect.den
552 ));
553 }
554 if video.color != Some(color) {
555 return Err(format!(
556 "the source is tagged {:?}, and the output must be tagged {color:?}",
557 video.color
558 ));
559 }
560 Ok(())
561}
562
563fn check_carried_display(aspect: AVRational) -> Result<(), String> {
571 if aspect.num > 0 && aspect.den > 0 && aspect.num != aspect.den {
572 return Err(format!(
573 "the source's codec declares {}:{} pixels, which a copy keeps and its container overrides to square",
574 aspect.num, aspect.den
575 ));
576 }
577 Ok(())
578}
579
580fn check_sampling(
583 format: i32,
584 field_order: ffi::AVFieldOrder,
585 formats: &[ffi::AVPixelFormat],
586) -> Result<(), String> {
587 if !formats.contains(&format) {
588 return Err("the source is not 8-bit 4:2:0".to_owned());
589 }
590 if !matches!(
591 field_order,
592 ffi::AV_FIELD_PROGRESSIVE | ffi::AV_FIELD_UNKNOWN
593 ) {
594 return Err("the source is interlaced".to_owned());
595 }
596 Ok(())
597}
598
599pub fn check_yuv_transcode(
613 source: &Path,
614 width: u32,
615 height: u32,
616 encoding: YuvEncoding,
617) -> Result<(), String> {
618 let input =
619 crate::decode::open_input(source).map_err(|error| format!("probe failed: {error}"))?;
620 let probed = crate::decode::probe_context(&input, source)
621 .map_err(|error| format!("probe failed: {error}"))?;
622 let video_index = probed
623 .selected_video
624 .ok_or_else(|| "the source has no video stream".to_owned())?;
625 check_picture(&probed, video_index, (width, height), encoding.color)?;
626 let parameters = input.streams()[video_index].codecpar();
627 check_sampling(
628 parameters.format,
629 parameters.field_order,
630 &[ffi::AV_PIX_FMT_YUV420P, ffi::AV_PIX_FMT_NV12],
631 )?;
632 let siting =
633 crate::convert::decoded_chroma_location(parameters.codec_id, parameters.chroma_location);
634 if !same_chroma_siting(siting, encoding.chroma) {
635 return Err(format!(
636 "the source's chroma sits {siting:?}, and the output's must sit {:?}",
637 encoding.chroma
638 ));
639 }
640 Ok(())
641}
642
643#[must_use]
646pub fn same_chroma_siting(source: ChromaLocation, output: ChromaLocation) -> bool {
647 let centred = |location| {
648 if location == ChromaLocation::Unspecified {
649 ChromaLocation::Center
650 } else {
651 location
652 }
653 };
654 centred(source) == centred(output)
655}
656
657fn video_parameters(source: &Path, index: usize) -> Result<VideoParameters, String> {
659 let input = open(source)?;
660 let stream = input
661 .streams()
662 .get(index)
663 .ok_or_else(|| "the source's video stream is missing".to_owned())?;
664 let parameters = stream.codecpar();
665 Ok(VideoParameters {
666 codec_id: parameters.codec_id,
667 format: parameters.format,
668 field_order: parameters.field_order,
669 sample_aspect_ratio: parameters.sample_aspect_ratio,
670 extradata: alpha2_ffmpeg_bridge::codec_parameters_extradata(¶meters),
671 time_base: stream.time_base,
672 width: parameters.width,
673 height: parameters.height,
674 tags: (
675 parameters.color_primaries,
676 parameters.color_trc,
677 parameters.color_space,
678 parameters.color_range,
679 ),
680 })
681}
682
683fn stream_framing(
686 request: &PacketCopyRequest<'_>,
687 parameters: &VideoParameters,
688) -> Result<(usize, StreamContract), String> {
689 if parameters.codec_id != request.codec.codec_id() {
690 return Err(format!(
691 "the source's video is not {}",
692 request.codec.as_str()
693 ));
694 }
695 check_sampling(
696 parameters.format,
697 parameters.field_order,
698 &[ffi::AV_PIX_FMT_YUV420P],
699 )?;
700 check_carried_display(parameters.sample_aspect_ratio)?;
701 if parameters.tags != crate::convert::color_tags(request.color) {
704 return Err(format!(
705 "the source is tagged primaries {}, transfer {}, matrix {}, range {} (FFmpeg's numbers), and the output must be tagged {:?}",
706 parameters.tags.0,
707 parameters.tags.1,
708 parameters.tags.2,
709 parameters.tags.3,
710 request.color
711 ));
712 }
713 let length_size = nal_length_size(request.codec, ¶meters.extradata)?;
714 Ok((
715 length_size,
716 StreamContract {
717 codec_id: parameters.codec_id,
718 width: parameters.width,
719 height: parameters.height,
720 format: parameters.format,
721 extradata: parameters.extradata.clone(),
722 time_base: (parameters.time_base.num, parameters.time_base.den),
723 },
724 ))
725}
726
727fn copyable_audio(
729 probed: &crate::ProbeResult,
730 audio: PacketCopyAudio,
731 container: alpha2_media::Container,
732) -> Result<usize, String> {
733 let index = probed
734 .selected_audio
735 .ok_or_else(|| "the source has no audio stream to copy".to_owned())?;
736 let stream = probed
737 .streams
738 .iter()
739 .find(|stream| stream.index == index && stream.kind == StreamKind::Audio)
740 .ok_or_else(|| "the source's audio stream is missing".to_owned())?;
741 if stream.codec != "aac" {
742 return Err(format!("the source's audio is {}, not AAC", stream.codec));
743 }
744 if !container.offers_audio(alpha2_media::AudioCodec::Aac) {
745 return Err(format!("the source's aac is not offered in {container}"));
746 }
747 if stream.sample_rate != Some(audio.sample_rate) || stream.channels != Some(audio.channels) {
748 return Err(format!(
749 "the source's audio is {:?} Hz x {:?}, not the export's {} Hz x {}",
750 stream.sample_rate, stream.channels, audio.sample_rate, audio.channels
751 ));
752 }
753 Ok(index)
754}
755
756fn open(path: &Path) -> Result<DemuxInput, String> {
757 crate::decode::open_input(path).map_err(|error| format!("cannot open the source: {error}"))
758}
759
760pub(crate) fn nal_length_size(codec: NativeVideoCodec, extradata: &[u8]) -> Result<usize, String> {
764 let annex_b = || "the source's parameter sets are not in MP4 form".to_owned();
765 match codec {
766 NativeVideoCodec::H264 if extradata.len() >= 7 && extradata[0] == 1 => {
767 Ok(usize::from(extradata[4] & 0b11) + 1)
768 }
769 NativeVideoCodec::Hevc if extradata.len() >= 23 && extradata[0] == 1 => {
770 Ok(usize::from(extradata[21] & 0b11) + 1)
771 }
772 NativeVideoCodec::H264 | NativeVideoCodec::Hevc => Err(annex_b()),
773 other => Err(format!(
774 "{} is not copied; only H.264 and HEVC are",
775 other.as_str()
776 )),
777 }
778}
779
780pub(crate) fn is_idr(codec: NativeVideoCodec, length_size: usize, data: &[u8]) -> bool {
782 let mut at = 0_usize;
783 while at + length_size < data.len() {
784 let length = data[at..at + length_size]
785 .iter()
786 .fold(0_usize, |value, byte| (value << 8) | usize::from(*byte));
787 let Some(&header) = data.get(at + length_size) else {
788 return false;
789 };
790 let idr = match codec {
791 NativeVideoCodec::H264 => header & 0x1f == 5,
792 NativeVideoCodec::Hevc => matches!((header >> 1) & 0x3f, 19 | 20),
795 _ => false,
796 };
797 if idr {
798 return true;
799 }
800 at = at.saturating_add(length_size).saturating_add(length);
801 }
802 false
803}
804
805struct Scan {
807 video: Vec<ScannedPacket>,
809 audio: Option<AudioSpan>,
813}
814
815#[derive(Clone, Copy)]
817struct AudioSpan {
818 first: i64,
819 last: i64,
820 end: i64,
823 last_duration: i64,
824 time_base: AVRational,
825}
826
827fn scan(
831 path: &Path,
832 index: usize,
833 codec: NativeVideoCodec,
834 length_size: usize,
835 audio: Option<usize>,
836 cancel: &CancelToken,
837) -> Result<Scan, String> {
838 let mut input = open(path)?;
839 crate::decode::interrupt_on_cancel(&input, cancel);
840 input.keep_only(
841 &[Some(index), audio]
842 .into_iter()
843 .flatten()
844 .collect::<Vec<_>>(),
845 );
846 let audio_time_base =
847 audio.and_then(|audio| input.streams().get(audio).map(|stream| stream.time_base));
848 let mut packets = Vec::new();
849 let mut audio_span: Option<(i64, i64, i64, i64)> = None;
850 let cancelled = || "the export was cancelled while its copy was planned".to_owned();
851 loop {
852 if cancel.is_cancelled() {
853 return Err(cancelled());
854 }
855 let packet = match input.read_packet() {
856 Ok(Some(packet)) => packet,
857 Ok(None) => break,
858 Err(_) if cancel.is_cancelled() => return Err(cancelled()),
859 Err(error) => return Err(format!("cannot read the source: {error}")),
860 };
861 crate::recovery::report_decode_progress();
863 let stream = usize::try_from(packet.stream_index).ok();
864 if stream.is_some() && stream == audio {
865 for time in [packet.pts, packet.dts] {
866 if time != ffi::AV_NOPTS_VALUE {
867 let span = audio_span.get_or_insert((time, time, time, 0));
868 span.0 = span.0.min(time);
869 span.1 = span.1.max(time);
870 }
871 }
872 if packet.pts != ffi::AV_NOPTS_VALUE {
873 let end = packet.pts.saturating_add(packet.duration.max(0));
874 if let Some(span) = audio_span.as_mut()
875 && end >= span.2
876 {
877 span.2 = end;
878 span.3 = packet.duration.max(0);
879 }
880 }
881 continue;
882 }
883 if stream != Some(index) {
884 continue;
885 }
886 if packet.pts == ffi::AV_NOPTS_VALUE {
887 return Err("a source video packet has no presentation time".to_owned());
888 }
889 let key = packet.flags & i32::try_from(ffi::AV_PKT_FLAG_KEY).unwrap_or(0) != 0;
890 packets.push(ScannedPacket {
891 pts: packet.pts,
892 dts: if packet.dts == ffi::AV_NOPTS_VALUE {
893 packet.pts
894 } else {
895 packet.dts
896 },
897 idr: key
898 && is_idr(
899 codec,
900 length_size,
901 &alpha2_ffmpeg_bridge::packet_bytes(&packet),
902 ),
903 new_description: alpha2_ffmpeg_bridge::packet_has_side_data(
904 &packet,
905 ffi::AV_PKT_DATA_NEW_EXTRADATA,
906 ),
907 });
908 }
909 Ok(Scan {
910 video: packets,
911 audio: audio_span.zip(audio_time_base).map(
912 |((first, last, end, last_duration), time_base)| AudioSpan {
913 first,
914 last,
915 end,
916 last_duration,
917 time_base,
918 },
919 ),
920 })
921}
922
923const COPIED_AUDIO_SLACK: i64 = alpha2_time::TICKS_PER_SECOND;
927
928fn check_audio_span(
936 span: Option<AudioSpan>,
937 origin: i64,
938 video_time_base: AVRational,
939 request: &PacketCopyRequest<'_>,
940) -> Result<(), String> {
941 let Some(span) = span else {
942 return Ok(());
943 };
944 let out_of_range = || {
945 "the source's audio timestamps lie outside its video, so copying them would not play the render's mix".to_owned()
946 };
947 let output_start = request.output_start.ticks();
948 let video_ticks = request
949 .frame_times
950 .last()
951 .map_or(0, |last| last.ticks() - request.frame_times[0].ticks());
952 let earliest = output_start.saturating_sub(COPIED_AUDIO_SLACK);
953 let latest = output_start
954 .saturating_add(video_ticks)
955 .saturating_add(COPIED_AUDIO_SLACK);
956 let shift_by = av_rescale_q(
957 origin - av_rescale_q(output_start, tick_base(), video_time_base),
958 video_time_base,
959 span.time_base,
960 );
961 for time in [span.first, span.last] {
962 let moved = time.checked_sub(shift_by).ok_or_else(out_of_range)?;
963 let ticks = av_rescale_q(moved, span.time_base, tick_base());
964 if ticks == ffi::AV_NOPTS_VALUE || !(earliest..=latest).contains(&ticks) {
965 return Err(out_of_range());
966 }
967 }
968 let frames = i64::try_from(request.frame_times.len()).unwrap_or(i64::MAX);
971 let frame_ticks = if frames > 1 {
972 video_ticks / (frames - 1)
973 } else {
974 0
975 };
976 let video_end = output_start
977 .saturating_add(video_ticks)
978 .saturating_add(frame_ticks);
979 let end = av_rescale_q(
980 span.end.checked_sub(shift_by).ok_or_else(out_of_range)?,
981 span.time_base,
982 tick_base(),
983 );
984 let last_packet = av_rescale_q(span.last_duration, span.time_base, tick_base());
985 if end > video_end.saturating_add(last_packet) {
986 return Err(format!(
987 "the source's audio outlasts its video by {} ms, and a render mixes only the range",
988 (end - video_end) * 1_000 / alpha2_time::TICKS_PER_SECOND
989 ));
990 }
991 Ok(())
992}
993
994pub fn copy_packets(
1008 copy: &PacketCopy,
1009 output: &Path,
1010 container: alpha2_media::Container,
1011 audio: CopyAudio<'_>,
1012 cancel: &CancelToken,
1013 progress: &mut dyn FnMut(u64),
1014) -> Result<PacketCopyReport, EncodeError> {
1015 let first = ©.segments[0];
1016 let input = crate::decode::open_input(&first.source).map_err(encode_adapter_error)?;
1017 let video_stream = {
1018 let source = &input.streams()[first.video_stream];
1019 VideoStream {
1020 parameters: source.codecpar().clone(),
1021 time_base: source.time_base,
1022 copied: true,
1023 }
1024 };
1025 let audio_stream = match &audio {
1026 CopyAudio::None => None,
1027 CopyAudio::Source => {
1028 if !copy.copies_audio() {
1029 return Err(EncodeError::Adapter("the plan copies no audio".into()));
1030 }
1031 let index = first.audio_stream.unwrap_or(0);
1032 let source = &input.streams()[index];
1033 Some(AudioStream::Copy {
1034 parameters: source.codecpar().clone(),
1035 time_base: source.time_base,
1036 })
1037 }
1038 CopyAudio::Encode { config, .. } => Some(AudioStream::Encode(*config)),
1039 };
1040 drop(input);
1041 let copied_audio = matches!(audio, CopyAudio::Source);
1042 let mut writer = RunWriter {
1043 muxer: Muxer::open_streams(output, container, video_stream, audio_stream)?,
1044 mix: match audio {
1045 CopyAudio::Encode { feed, .. } => Some(MixWriter {
1046 feed,
1047 covered: 0,
1048 done: false,
1049 }),
1050 CopyAudio::None | CopyAudio::Source => None,
1051 },
1052 stages: crate::EncodeStageTimings::default(),
1053 report: PacketCopyReport::default(),
1054 };
1055 for segment in ©.segments {
1056 writer.write_run(segment, copied_audio, cancel, progress)?;
1057 }
1058 if writer.report.video_packets != copy.video_packets() {
1059 return Err(EncodeError::Adapter(format!(
1060 "the copy wrote {} video packets of the planned {}",
1061 writer.report.video_packets,
1062 copy.video_packets()
1063 )));
1064 }
1065 let RunWriter {
1066 mut muxer,
1067 mut mix,
1068 mut stages,
1069 mut report,
1070 } = writer;
1071 if let Some(mix) = mix.as_mut() {
1072 mix.write_until(i64::MAX, &mut muxer, &mut stages, &mut report)?;
1073 }
1074 muxer.finish_audio()?;
1075 muxer.write_ready_audio(true, &mut stages)?;
1076 muxer.close()?;
1077 Ok(report)
1078}
1079
1080struct RunWriter<'a> {
1082 muxer: Muxer,
1083 mix: Option<MixWriter<'a>>,
1084 stages: crate::EncodeStageTimings,
1085 report: PacketCopyReport,
1086}
1087
1088impl RunWriter<'_> {
1089 fn write_run(
1092 &mut self,
1093 run: &PacketCopyPlan,
1094 copied_audio: bool,
1095 cancel: &CancelToken,
1096 progress: &mut dyn FnMut(u64),
1097 ) -> Result<(), EncodeError> {
1098 let mut input = crate::decode::open_input(&run.source).map_err(encode_adapter_error)?;
1099 crate::decode::interrupt_on_cancel(&input, cancel);
1100 let audio_stream = copied_audio.then_some(run.audio_stream).flatten();
1101 input.keep_only(
1102 &[Some(run.video_stream), audio_stream]
1103 .into_iter()
1104 .flatten()
1105 .collect::<Vec<_>>(),
1106 );
1107 let shift_by = run.origin - run.offset;
1108 let mut video_position = if run.first_packet > 0 {
1111 let stream = i32::try_from(run.video_stream)
1112 .map_err(|_| EncodeError::Adapter("video stream index is out of range".into()))?;
1113 input
1114 .seek(
1115 stream,
1116 run.first_timestamps.1,
1117 i32::try_from(ffi::AVSEEK_FLAG_BACKWARD).unwrap_or(0),
1118 )
1119 .map_err(|error| {
1120 if cancel.is_cancelled() {
1121 EncodeError::Cancelled
1122 } else {
1123 encode_adapter_error_at("seeking to the run", error)
1124 }
1125 })?;
1126 None
1127 } else {
1128 Some(0_u64)
1129 };
1130 while let Some(mut packet) = input.read_packet().map_err(|error| {
1131 if cancel.is_cancelled() {
1132 EncodeError::Cancelled
1133 } else {
1134 encode_adapter_error(error)
1135 }
1136 })? {
1137 if cancel.is_cancelled() {
1138 return Err(EncodeError::Cancelled);
1139 }
1140 let stream = usize::try_from(packet.stream_index).ok();
1141 if stream == Some(run.video_stream) {
1142 let position = match video_position {
1143 Some(position) => position,
1144 None if (packet.pts, packet.dts) == run.first_timestamps => run.first_packet,
1145 None => continue,
1146 };
1147 video_position = Some(position + 1);
1148 if position < run.first_packet {
1149 continue;
1150 }
1151 if position > run.last_packet {
1152 break;
1154 }
1155 packet.set_pts(shift(packet.pts, shift_by)?);
1156 packet.set_dts(shift(packet.dts, shift_by)?);
1157 packet.set_pos(-1);
1158 if let Some(mix) = self.mix.as_mut() {
1159 let at = av_rescale_q(packet.dts, run.video_time_base, tick_base());
1160 mix.write_until(
1161 at.saturating_add(MIX_LEAD),
1162 &mut self.muxer,
1163 &mut self.stages,
1164 &mut self.report,
1165 )?;
1166 }
1167 self.report.bytes = self
1168 .report
1169 .bytes
1170 .saturating_add(u64::try_from(packet.size).unwrap_or(0));
1171 self.muxer.write_video(&mut packet, run.video_time_base)?;
1172 self.report.video_packets += 1;
1173 progress(self.report.video_packets);
1174 } else if stream.is_some() && stream == audio_stream {
1175 let source_base = input.streams()[run.audio_stream.unwrap_or(0)].time_base;
1176 let origin = av_rescale_q(shift_by, run.video_time_base, source_base);
1179 packet.set_pts(shift(packet.pts, origin)?);
1180 packet.set_dts(shift(packet.dts, origin)?);
1181 packet.set_pos(-1);
1182 self.report.bytes = self
1183 .report
1184 .bytes
1185 .saturating_add(u64::try_from(packet.size).unwrap_or(0));
1186 self.muxer.write_copied_audio(&mut packet)?;
1187 self.report.audio_packets += 1;
1188 }
1189 }
1190 Ok(())
1191 }
1192}
1193
1194const MIX_LEAD: i64 = alpha2_time::TICKS_PER_SECOND / 10;
1198
1199struct MixWriter<'a> {
1201 feed: &'a mut dyn AudioBlockFeed,
1202 covered: i64,
1204 done: bool,
1205}
1206
1207impl MixWriter<'_> {
1208 fn write_until(
1210 &mut self,
1211 target: i64,
1212 muxer: &mut Muxer,
1213 stages: &mut crate::EncodeStageTimings,
1214 report: &mut PacketCopyReport,
1215 ) -> Result<(), EncodeError> {
1216 while !self.done && self.covered < target {
1217 let Some(block) = self.feed.next_block().map_err(EncodeError::Adapter)? else {
1218 self.done = true;
1219 break;
1220 };
1221 let frames = block.channels.first().map_or(0, Vec::len);
1222 muxer.write_audio(&block, stages)?;
1223 report.encoded_audio_frames = report
1224 .encoded_audio_frames
1225 .saturating_add(u64::try_from(frames).unwrap_or(u64::MAX));
1226 self.covered = i64::try_from(report.encoded_audio_frames)
1227 .unwrap_or(i64::MAX)
1228 .saturating_mul(alpha2_time::TICKS_PER_SECOND)
1229 .div_euclid(i64::from(block.sample_rate.max(1)));
1230 }
1231 Ok(())
1232 }
1233}
1234
1235fn shift(value: i64, origin: i64) -> Result<i64, EncodeError> {
1239 if value == ffi::AV_NOPTS_VALUE {
1240 return Ok(value);
1241 }
1242 value
1243 .checked_sub(origin)
1244 .filter(|moved| *moved != ffi::AV_NOPTS_VALUE)
1245 .ok_or_else(|| {
1246 EncodeError::Adapter(format!(
1247 "a source packet's timestamp {value} cannot be moved by {origin}"
1248 ))
1249 })
1250}