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