1use std::{
2 path::Path,
3 sync::{Arc, Mutex},
4};
5
6use crate::pp_log::{PpLog, pp_error};
7use ffmpeg_next as ffmpeg;
8use thiserror::Error as ThisError;
9
10use crate::{
11 buffer::MediaBuffer,
12 contract::{InputContract, MediaKind, PortContract},
13 control::{ControlMsg, SeekRejectReason},
14 element::{Element, ElementType, Sink, element_pp_log},
15 error::Result,
16};
17
18#[derive(Debug, ThisError)]
21pub enum FileMuxerError {
22 #[error("FileMuxer stream sinks only accept Packet or Eos buffers, got {0}")]
24 UnsupportedBuffer(&'static str),
25
26 #[error("ffmpeg error: {0}")]
28 Ffmpeg(#[from] ffmpeg::Error),
29}
30
31struct PendingStream {
37 name: Arc<str>,
38 input_time_base: ffmpeg::Rational,
39 kind: Option<MediaKind>,
42}
43
44pub struct FileMuxer {
96 output: ffmpeg::format::context::Output,
97 streams: Vec<PendingStream>,
98}
99
100impl FileMuxer {
101 pub fn create(path: impl AsRef<Path>) -> Result<Self> {
104 let output = ffmpeg::format::output(&path).map_err(FileMuxerError::from)?;
105 Ok(Self {
106 output,
107 streams: Vec::new(),
108 })
109 }
110
111 pub fn add_stream(
125 &mut self,
126 name: impl Into<String>,
127 parameters: ffmpeg::codec::Parameters,
128 time_base: ffmpeg::Rational,
129 ) -> Result<()> {
130 let mut stream = self
131 .output
132 .add_stream(parameters.id())
133 .map_err(FileMuxerError::from)?;
134 let kind = MediaKind::packet_for(parameters.medium());
135 stream.set_time_base(time_base);
136 stream.set_parameters(parameters);
137 self.streams.push(PendingStream {
138 name: name.into().into(),
139 input_time_base: time_base,
140 kind,
141 });
142 Ok(())
143 }
144
145 pub fn open(mut self) -> Result<Vec<Box<dyn Sink>>> {
174 self.output.write_header().map_err(FileMuxerError::from)?;
175 let total = self.streams.len();
176 let shared = Arc::new(FileMuxerShared {
177 state: Mutex::new(MuxerState {
178 output: self.output,
179 done: 0,
180 finished: false,
181 }),
182 total,
183 });
184 Ok(self
185 .streams
186 .into_iter()
187 .enumerate()
188 .map(|(index, stream)| -> Box<dyn Sink> {
189 Box::new(FileMuxerStreamSink {
190 pp_log: element_pp_log(ElementType::FileMuxer, &stream.name, None),
191 name: stream.name,
192 shared: shared.clone(),
193 stream_index: index,
194 input_time_base: stream.input_time_base,
195 kind: stream.kind,
196 done: false,
197 })
198 })
199 .collect())
200 }
201}
202
203struct MuxerState {
204 output: ffmpeg::format::context::Output,
205 done: usize,
210 finished: bool,
216}
217
218struct FileMuxerShared {
223 state: Mutex<MuxerState>,
224 total: usize,
225}
226
227impl FileMuxerShared {
228 fn write_packet(
229 &self,
230 stream_index: usize,
231 input_time_base: ffmpeg::Rational,
232 packet: &ffmpeg::Packet,
233 ) -> Result<()> {
234 let mut state = self.state.lock().unwrap();
235 if state.finished {
236 return Ok(());
237 }
238 let mut packet = packet.clone();
243 let output_time_base = state
244 .output
245 .stream(stream_index)
246 .expect("stream was added in FileMuxer::add_stream")
247 .time_base();
248 packet.rescale_ts(input_time_base, output_time_base);
249 packet.set_stream(stream_index);
250 packet.set_position(-1);
251 packet
252 .write_interleaved(&mut state.output)
253 .map_err(FileMuxerError::from)?;
254 Ok(())
255 }
256
257 fn finish_track(&self) -> Result<()> {
260 let mut state = self.state.lock().unwrap();
261 state.done += 1;
262 if state.finished || state.done < self.total {
263 return Ok(());
264 }
265 state.finished = true;
266 state.output.write_trailer().map_err(FileMuxerError::from)?;
267 Ok(())
268 }
269}
270
271pub struct FileMuxerStreamSink {
276 pp_log: PpLog,
277 name: Arc<str>,
278 shared: Arc<FileMuxerShared>,
279 stream_index: usize,
280 input_time_base: ffmpeg::Rational,
281 kind: Option<MediaKind>,
284 done: bool,
289}
290
291impl FileMuxerStreamSink {
292 fn finish(&mut self) -> Result<()> {
293 if self.done {
294 return Ok(());
295 }
296 self.done = true;
297 self.shared
298 .finish_track()
299 .inspect_err(|error| pp_error!(self, "write_trailer failed: {error}"))
300 }
301}
302
303impl Element for FileMuxerStreamSink {
304 fn name(&self) -> Arc<str> {
305 self.name.clone()
306 }
307
308 fn element_type(&self) -> ElementType {
309 ElementType::FileMuxer
310 }
311
312 fn pp_log(&self) -> &PpLog {
313 &self.pp_log
314 }
315
316 fn pp_log_mut(&mut self) -> &mut PpLog {
317 &mut self.pp_log
318 }
319}
320
321impl Sink for FileMuxerStreamSink {
322 fn input_contract(&self) -> InputContract {
327 match self.kind {
328 Some(kind) => InputContract::Fixed(PortContract::packet(kind)),
329 None => InputContract::Unknown,
330 }
331 }
332
333 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
334 match buf {
335 MediaBuffer::Packet(packet) => self
336 .shared
337 .write_packet(self.stream_index, self.input_time_base, &packet)
338 .inspect_err(|error| pp_error!(self, "write_interleaved failed: {error}")),
339 MediaBuffer::Eos => self.finish(),
340 other => Err(FileMuxerError::UnsupportedBuffer(other.kind()).into()),
341 }
342 }
343
344 fn control(&mut self, msg: ControlMsg) -> Result<()> {
345 if let ControlMsg::CheckSeek(context) = &msg {
346 context.reject(
347 self.element_type(),
348 self.name(),
349 SeekRejectReason::ElementNotSeekable,
350 );
351 }
352 if msg == ControlMsg::Stop {
357 self.finish()?;
358 }
359 Ok(())
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use crate::control::{SeekCheckContext, SeekRejectReason};
367 use crate::element::Source;
368 use crate::elements::{AudioCodec, SwAudioEncoder, SwAudioEncoderOptions};
369
370 fn open_aac_encoder(sample_rate: u32, channels: u16) -> SwAudioEncoder {
371 SwAudioEncoder::new(
372 "encoder",
373 SwAudioEncoderOptions {
374 codec: AudioCodec::Aac,
375 sample_rate,
376 channels,
377 time_base: ffmpeg::Rational::new(1, sample_rate as i32),
378 bit_rate: 64_000,
379 },
380 )
381 .expect("aac encoder must be available")
382 }
383
384 fn open_opus_encoder(sample_rate: u32, channels: u16) -> Option<SwAudioEncoder> {
387 SwAudioEncoder::new(
388 "encoder",
389 SwAudioEncoderOptions {
390 codec: AudioCodec::Opus,
391 sample_rate,
392 channels,
393 time_base: ffmpeg::Rational::new(1, sample_rate as i32),
394 bit_rate: 64_000,
395 },
396 )
397 .ok()
398 }
399
400 fn silent_frame(
401 sample_rate: u32,
402 channels: u16,
403 samples: usize,
404 pts: i64,
405 ) -> ffmpeg::frame::Audio {
406 let mut frame = ffmpeg::frame::Audio::new(
407 ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Packed),
408 samples,
409 ffmpeg::ChannelLayout::default(channels as i32),
410 );
411 frame.set_rate(sample_rate);
412 frame.set_pts(Some(pts));
413 frame.data_mut(0).fill(0);
417 frame
418 }
419
420 #[test]
421 fn seek_check_rejects_a_muxer_track() {
422 let encoder = open_aac_encoder(48000, 1);
423 let path = std::env::temp_dir().join(format!(
424 "file_muxer_seek_check_test_{}.mp4",
425 std::process::id()
426 ));
427 let mut muxer = FileMuxer::create(&path).expect("create muxer");
428 muxer
429 .add_stream(
430 "audio",
431 encoder.parameters(),
432 ffmpeg::Rational::new(1, 48000),
433 )
434 .expect("add stream");
435 let mut sink = muxer.open().expect("open muxer").pop().unwrap();
436 let context = Arc::new(SeekCheckContext::new());
437
438 sink.control(ControlMsg::CheckSeek(Arc::clone(&context)))
439 .expect("check control");
440
441 let error = context.result().expect_err("muxer must reject seek");
442 assert_eq!(error.rejections().len(), 1);
443 assert_eq!(
444 error.rejections()[0].reason,
445 SeekRejectReason::ElementNotSeekable
446 );
447 sink.control(ControlMsg::Stop).expect("finalize muxer");
448 std::fs::remove_file(path).ok();
449 }
450
451 #[test]
455 fn single_track_still_produces_a_playable_file() {
456 let mut encoder = open_aac_encoder(48000, 1);
457
458 let dir = std::env::temp_dir();
459 let path = dir.join(format!("file_muxer_single_test_{}.mp4", std::process::id()));
460
461 let mut muxer = FileMuxer::create(&path).expect("the muxer must open");
462 muxer
463 .add_stream(
464 "audio",
465 encoder.parameters(),
466 ffmpeg::Rational::new(1, 48000),
467 )
468 .expect("add_stream must succeed");
469 let mut sinks = muxer.open().expect("open must write the header");
470 assert_eq!(sinks.len(), 1);
471 encoder.src_pads()[0].link(sinks.pop().unwrap());
472
473 for tick in 0..20i64 {
474 encoder
475 .consume(MediaBuffer::Audio(Arc::new(silent_frame(
476 48000,
477 1,
478 960,
479 tick * 960,
480 ))))
481 .expect("consume must succeed");
482 }
483 encoder
484 .consume(MediaBuffer::Eos)
485 .expect("eos must flush cleanly");
486 drop(encoder);
487
488 let input = ffmpeg::format::input(&path).expect("muxed file must be readable back");
489 assert_eq!(input.streams().count(), 1);
490 std::fs::remove_file(&path).ok();
491 }
492
493 #[test]
504 fn dropping_every_sink_without_eos_or_stop_still_releases_the_file() {
505 let encoder = open_aac_encoder(48000, 1);
506
507 let dir = std::env::temp_dir();
508 let path = dir.join(format!("file_muxer_drop_test_{}.mp4", std::process::id()));
509
510 let mut muxer = FileMuxer::create(&path).expect("the muxer must open");
511 muxer
512 .add_stream(
513 "audio",
514 encoder.parameters(),
515 ffmpeg::Rational::new(1, 48000),
516 )
517 .expect("add_stream must succeed");
518 let sinks = muxer.open().expect("open must write the header");
519
520 drop(sinks);
522 drop(encoder);
523
524 std::fs::remove_file(&path)
525 .expect("file handle must be released once every sink is dropped");
526 }
527
528 #[test]
537 fn muxes_two_independent_tracks_without_finalizing_early() {
538 let mut encoder_a = open_aac_encoder(48000, 2);
539 let mut encoder_b = open_aac_encoder(44100, 1);
540
541 let dir = std::env::temp_dir();
542 let path = dir.join(format!("file_muxer_multi_test_{}.mp4", std::process::id()));
543
544 let mut muxer = FileMuxer::create(&path).expect("the muxer must open");
545 muxer
546 .add_stream("a", encoder_a.parameters(), ffmpeg::Rational::new(1, 48000))
547 .expect("add_stream a");
548 muxer
549 .add_stream("b", encoder_b.parameters(), ffmpeg::Rational::new(1, 44100))
550 .expect("add_stream b");
551 let mut sinks = muxer.open().expect("open must write the header");
552 assert_eq!(sinks.len(), 2);
553 let sink_b = sinks.pop().unwrap();
554 let sink_a = sinks.pop().unwrap();
555 encoder_a.src_pads()[0].link(sink_a);
556 encoder_b.src_pads()[0].link(sink_b);
557
558 for tick in 0..10i64 {
559 encoder_a
560 .consume(MediaBuffer::Audio(Arc::new(silent_frame(
561 48000,
562 2,
563 960,
564 tick * 960,
565 ))))
566 .expect("consume must succeed");
567 }
568 encoder_a
571 .consume(MediaBuffer::Eos)
572 .expect("eos must flush cleanly");
573
574 for tick in 0..10i64 {
575 encoder_b
576 .consume(MediaBuffer::Audio(Arc::new(silent_frame(
577 44100,
578 1,
579 882,
580 tick * 882,
581 ))))
582 .expect("consume must succeed");
583 }
584 encoder_b
585 .consume(MediaBuffer::Eos)
586 .expect("eos must flush cleanly");
587
588 drop(encoder_a);
589 drop(encoder_b);
590
591 let mut input = ffmpeg::format::input(&path).expect("muxed file must be readable back");
592 assert_eq!(input.streams().count(), 2, "expected two tracks");
593
594 let mut counts = [0usize; 2];
595 let mut packet = ffmpeg::Packet::empty();
596 while packet.read(&mut input).is_ok() {
597 counts[packet.stream()] += 1;
598 packet = ffmpeg::Packet::empty();
599 }
600 assert!(counts[0] > 0, "track a has no packets: {counts:?}");
601 assert!(
602 counts[1] > 0,
603 "track b has no packets: {counts:?} — trailer was written before track b finished"
604 );
605 std::fs::remove_file(&path).ok();
606 }
607
608 #[test]
623 fn the_container_follows_the_path_extension() {
624 let mut encoder = open_aac_encoder(48000, 1);
625
626 let dir = std::env::temp_dir();
627 let path = dir.join(format!("muxer_container_test_{}.mkv", std::process::id()));
628 let _ = std::fs::remove_file(&path);
629
630 let mut muxer = FileMuxer::create(&path).expect("the muxer must open a .mkv path");
631 muxer
632 .add_stream(
633 "audio",
634 encoder.parameters(),
635 ffmpeg::Rational::new(1, 48000),
636 )
637 .expect("add_stream must succeed");
638 let mut sinks = muxer.open().expect("open must write the header");
639 encoder.src_pads()[0].link(sinks.pop().expect("exactly one stream was added"));
640
641 for tick in 0..20i64 {
642 encoder
643 .consume(MediaBuffer::Audio(Arc::new(silent_frame(
644 48000,
645 1,
646 960,
647 tick * 960,
648 ))))
649 .expect("consume must succeed");
650 }
651 encoder
652 .consume(MediaBuffer::Eos)
653 .expect("eos must flush cleanly");
654 drop(encoder);
655
656 let input = ffmpeg::format::input(&path).expect("the file must be readable");
657 let format = input.format();
658 assert!(
659 format.name().contains("matroska"),
660 "a .mkv path should have produced Matroska, got {:?}",
661 format.name()
662 );
663 assert_eq!(input.streams().count(), 1);
664 std::fs::remove_file(&path).ok();
665 }
666
667 #[test]
679 fn a_video_track_carries_its_extradata_into_a_matroska_header() {
680 use crate::elements::{SwEncoder, SwEncoderOptions, VideoCodec};
681
682 let time_base = ffmpeg::Rational::new(1, 30);
683 let options = |codec| SwEncoderOptions {
684 codec,
685 width: 320,
686 height: 180,
687 time_base,
688 frame_rate: ffmpeg::Rational::new(30, 1),
689 bit_rate: 400_000,
690 gop_size: 30,
691 max_b_frames: None,
692 };
693 let Some(encoder) = [VideoCodec::OpenH264, VideoCodec::H264]
696 .into_iter()
697 .find_map(|codec| SwEncoder::new("video", options(codec)).ok())
698 else {
699 eprintln!("skipping: this FFmpeg build has no libopenh264 or libx264");
700 return;
701 };
702
703 let dir = std::env::temp_dir();
704 let path = dir.join(format!("file_muxer_mkv_video_{}.mkv", std::process::id()));
705 let _ = std::fs::remove_file(&path);
706
707 let mut muxer = FileMuxer::create(&path).expect("the muxer must open");
708 muxer
709 .add_stream("video", encoder.parameters(), time_base)
710 .expect("add_stream must succeed");
711 let sinks = muxer
717 .open()
718 .expect("Matroska must accept a video track's header");
719 drop(sinks);
720 std::fs::remove_file(&path).ok();
721 }
722
723 #[test]
739 fn opus_carries_its_own_header_into_matroska() {
740 let Some(mut encoder) = open_opus_encoder(48_000, 2) else {
742 eprintln!("skipping: this FFmpeg build has no libopus");
743 return;
744 };
745
746 let dir = std::env::temp_dir();
747 let path = dir.join(format!("file_muxer_opus_mkv_{}.mkv", std::process::id()));
748 let _ = std::fs::remove_file(&path);
749
750 let mut muxer = FileMuxer::create(&path).expect("the muxer must open");
751 muxer
752 .add_stream(
753 "audio",
754 encoder.parameters(),
755 ffmpeg::Rational::new(1, 48_000),
756 )
757 .expect("add_stream must succeed");
758 let mut sinks = muxer
759 .open()
760 .expect("Matroska must accept an Opus track's header");
761 encoder.src_pads()[0].link(sinks.pop().expect("exactly one stream was added"));
762
763 for tick in 0..20i64 {
764 encoder
765 .consume(MediaBuffer::Audio(Arc::new(silent_frame(
766 48_000,
767 2,
768 960,
769 tick * 960,
770 ))))
771 .expect("consume must succeed");
772 }
773 encoder
774 .consume(MediaBuffer::Eos)
775 .expect("eos must flush cleanly");
776 drop(encoder);
777
778 let input = ffmpeg::format::input(&path).expect("the file must be readable");
779 assert!(
780 input.format().name().contains("matroska"),
781 "got {:?}",
782 input.format().name()
783 );
784 let stream = input
785 .streams()
786 .best(ffmpeg::media::Type::Audio)
787 .expect("the file must hold the audio track it was given");
788 assert_eq!(
789 stream.parameters().id(),
790 ffmpeg::codec::Id::OPUS,
791 "the track must read back as Opus, not as whatever the header defaulted to"
792 );
793 std::fs::remove_file(&path).ok();
794 }
795
796 #[test]
809 fn a_reordered_stream_keeps_its_decode_order_through_the_muxer() {
810 use crate::elements::FileDemuxer;
811 use crate::pipeline::Pipeline;
812
813 let fixture = crate::test_support::synthesize_reordered("reordered", 3.0);
814 let source = fixture.path.to_string_lossy().into_owned();
815
816 let arriving = timestamps(&source);
820 assert!(
821 arriving.iter().any(|(dts, pts)| dts != pts),
822 "the fixture carries no reordering to preserve: {:?}",
823 &arriving[..arriving.len().min(8)]
824 );
825
826 let path = std::env::temp_dir().join("media-pp-reordered-remux.mp4");
827 let _ = std::fs::remove_file(&path);
828 let (demuxer, streams) = FileDemuxer::open("demuxer", &source).expect("open the fixture");
829 let video = streams
830 .iter()
831 .find(|stream| stream.kind == ffmpeg::media::Type::Video)
832 .expect("the fixture has video")
833 .index;
834 let parameters = demuxer.stream_parameters(video).expect("video parameters");
835 let time_base = demuxer.stream_time_base(video).expect("video time base");
836
837 let mut muxer = FileMuxer::create(&path).expect("create the remux");
838 muxer
839 .add_stream("video", parameters, time_base)
840 .expect("add the video stream");
841 let sink = muxer
842 .open()
843 .expect("write the header")
844 .pop()
845 .expect("one stream was added");
846
847 let pipeline = Pipeline::new("remux", demuxer, move |source, context| {
848 let branch = context.branch().to(sink)?;
849 context.attach(source, video, branch)?;
850 Ok(())
851 })
852 .expect("wire the remux");
853 pipeline.run().expect("run the remux");
854 for event in pipeline.bus().iter() {
855 if matches!(event, crate::bus::BusEvent::Eos { .. }) {
856 break;
857 }
858 }
859 pipeline.stop();
860
861 let written = timestamps(&path.to_string_lossy());
862 assert_eq!(
863 written.len(),
864 arriving.len(),
865 "every packet that came in has to come out"
866 );
867 assert!(
868 written.iter().any(|(dts, pts)| dts != pts),
869 "the reordering did not survive being written"
870 );
871 assert!(
872 written.windows(2).all(|pair| pair[0].0 <= pair[1].0),
873 "decode order is what `dts` is for and it has to keep rising: {:?}",
874 &written[..written.len().min(8)]
875 );
876 assert_eq!(
877 written, arriving,
878 "a remux copies packets; it does not restamp them"
879 );
880 std::fs::remove_file(&path).ok();
881 }
882
883 fn timestamps(path: &str) -> Vec<(i64, i64)> {
886 let mut input = ffmpeg::format::input(path).expect("the file opens");
887 let video = input
888 .streams()
889 .find(|stream| stream.parameters().medium() == ffmpeg::media::Type::Video)
890 .expect("it has video")
891 .index();
892 input
893 .packets()
894 .filter(|(stream, _)| stream.index() == video)
895 .filter_map(|(_, packet)| Some((packet.dts()?, packet.pts()?)))
896 .collect()
897 }
898
899 #[derive(Debug, Clone, Copy)]
901 struct Shape {
902 video_frames: usize,
903 video_seconds: f64,
904 audio_seconds: f64,
905 video_start: f64,
906 audio_start: f64,
907 }
908
909 impl Shape {
910 fn video_fps(self) -> f64 {
911 self.video_frames as f64 / self.video_seconds
912 }
913 }
914
915 fn shape_of(path: &str) -> Shape {
917 let input = ffmpeg::format::input(path).expect("the file opens");
918 let stream = |medium| {
919 input
920 .streams()
921 .find(|stream| stream.parameters().medium() == medium)
922 .unwrap_or_else(|| panic!("{path} carries no {medium:?}"))
923 };
924 let seconds = |stream: &ffmpeg::format::stream::Stream<'_>| {
925 stream.duration() as f64 * f64::from(stream.time_base())
926 };
927 let start = |stream: &ffmpeg::format::stream::Stream<'_>| {
928 stream.start_time() as f64 * f64::from(stream.time_base())
929 };
930 let video = stream(ffmpeg::media::Type::Video);
931 let audio = stream(ffmpeg::media::Type::Audio);
932 Shape {
933 video_frames: video.frames() as usize,
934 video_seconds: seconds(&video),
935 audio_seconds: seconds(&audio),
936 video_start: start(&video),
937 audio_start: start(&audio),
938 }
939 }
940
941 #[test]
956 fn a_transcoded_file_keeps_the_shape_of_what_went_in() {
957 use crate::elements::{FileDemuxer, SwDecoder, SwEncoder, SwEncoderOptions, VideoCodec};
958 use crate::pipeline::Pipeline;
959
960 let fixture = crate::test_support::synthesize("transcode-shape", 4.0, 44_100);
961 let source_path = fixture.path.to_string_lossy().into_owned();
962 let arriving = shape_of(&source_path);
963
964 let path = std::env::temp_dir().join("media-pp-transcode-shape.mp4");
965 let _ = std::fs::remove_file(&path);
966
967 let (demuxer, streams) = FileDemuxer::open("demuxer", &source_path).expect("open");
968 let index = |medium| {
969 streams
970 .iter()
971 .find(|stream| stream.kind == medium)
972 .unwrap_or_else(|| panic!("the fixture carries no {medium:?}"))
973 .index
974 };
975 let video = index(ffmpeg::media::Type::Video);
976 let audio = index(ffmpeg::media::Type::Audio);
977 let video_decoder = SwDecoder::new(
978 "video-decoder",
979 demuxer.stream_parameters(video).expect("video parameters"),
980 )
981 .expect("open the video decoder");
982 let audio_decoder = SwDecoder::new(
983 "audio-decoder",
984 demuxer.stream_parameters(audio).expect("audio parameters"),
985 )
986 .expect("open the audio decoder");
987
988 let width = 320;
991 let height = 240;
992 let video_time_base = demuxer.stream_time_base(video).expect("video time base");
998 let video_encoder = SwEncoder::new(
999 "video-encoder",
1000 SwEncoderOptions {
1001 codec: VideoCodec::OpenH264,
1002 width,
1003 height,
1004 time_base: video_time_base,
1005 frame_rate: ffmpeg::Rational::new(30, 1),
1006 bit_rate: 800_000,
1007 gop_size: 30,
1008 max_b_frames: None,
1009 },
1010 )
1011 .expect("open the video encoder");
1012 let audio_encoder = open_aac_encoder(48_000, 2);
1018
1019 let mut muxer = FileMuxer::create(&path).expect("create the output");
1020 muxer
1021 .add_stream("video", video_encoder.parameters(), video_time_base)
1022 .expect("add the video stream");
1023 muxer
1024 .add_stream(
1025 "audio",
1026 audio_encoder.parameters(),
1027 audio_encoder.time_base(),
1028 )
1029 .expect("add the audio stream");
1030 let mut sinks = muxer.open().expect("write the header");
1031 let audio_sink = sinks.pop().expect("audio was added second");
1032 let video_sink = sinks.pop().expect("video was added first");
1033
1034 let scaler = crate::elements::SwScaler::new(
1035 "to-yuv",
1036 ffmpeg::format::Pixel::YUV420P,
1037 width,
1038 height,
1039 ffmpeg::software::scaling::Flags::BILINEAR,
1040 );
1041
1042 let pipeline = Pipeline::new("transcode", demuxer, move |source, context| {
1043 let picture = context
1044 .branch()
1045 .pipe(video_decoder)
1046 .pipe(scaler)
1047 .pipe(video_encoder)
1048 .to(video_sink)?;
1049 context.attach(source, video, picture)?;
1050 let sound = context
1051 .branch()
1052 .pipe(audio_decoder)
1053 .pipe(audio_encoder)
1054 .to(audio_sink)?;
1055 context.attach(source, audio, sound)?;
1056 Ok(())
1057 })
1058 .expect("wire the transcode");
1059 pipeline.run().expect("run the transcode");
1060 for event in pipeline.bus().iter() {
1061 if matches!(event, crate::bus::BusEvent::Eos { .. }) {
1062 break;
1063 }
1064 }
1065 pipeline.stop();
1066
1067 let written = shape_of(&path.to_string_lossy());
1068 eprintln!("SHAPE in : {arriving:?} fps={:.3}", arriving.video_fps());
1069 eprintln!("SHAPE out: {written:?} fps={:.3}", written.video_fps());
1070
1071 assert!(
1072 (written.video_fps() - arriving.video_fps()).abs() < 0.5,
1073 "the picture came out at a different rate: {:.3} in, {:.3} out",
1074 arriving.video_fps(),
1075 written.video_fps()
1076 );
1077 assert!(
1078 (written.video_seconds - arriving.video_seconds).abs() < 0.25,
1079 "the picture came out a different length: {:.3}s in, {:.3}s out",
1080 arriving.video_seconds,
1081 written.video_seconds
1082 );
1083 assert!(
1084 (written.audio_seconds - written.video_seconds).abs() < 0.25,
1085 "the sound and the picture came out different lengths: \
1086 {:.3}s of sound against {:.3}s of picture",
1087 written.audio_seconds,
1088 written.video_seconds
1089 );
1090 assert!(
1091 (written.audio_start - written.video_start).abs()
1092 <= (arriving.audio_start - arriving.video_start).abs() + 0.05,
1093 "the two tracks no longer start where they did: \
1094 in {:.3}s/{:.3}s, out {:.3}s/{:.3}s",
1095 arriving.video_start,
1096 arriving.audio_start,
1097 written.video_start,
1098 written.audio_start
1099 );
1100 std::fs::remove_file(&path).ok();
1101 }
1102}