// Copyright (C) 2022 Sebastian Dröge // // This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at // . // // SPDX-License-Identifier: MPL-2.0 // use std::path::Path; use gst::prelude::*; use gst_pbutils::prelude::*; fn init() { use std::sync::Once; static INIT: Once = Once::new(); INIT.call_once(|| { gst::init().unwrap(); gstmp4::plugin_register_static().unwrap(); }); } struct Pipeline(gst::Pipeline); impl std::ops::Deref for Pipeline { type Target = gst::Pipeline; fn deref(&self) -> &Self::Target { &self.0 } } impl Drop for Pipeline { fn drop(&mut self) { let _ = self.0.set_state(gst::State::Null); } } impl Pipeline { fn into_completion(self) { self.set_state(gst::State::Playing) .expect("Unable to set the pipeline to the `Playing` state"); for msg in self.bus().unwrap().iter_timed(gst::ClockTime::NONE) { use gst::MessageView; match msg.view() { MessageView::Eos(..) => break, MessageView::Error(err) => { panic!( "Error from {:?}: {} ({:?})", err.src().map(|s| s.path_string()), err.error(), err.debug() ); } _ => (), } } self.set_state(gst::State::Null) .expect("Unable to set the pipeline to the `Null` state"); } } fn test_basic_with(video_enc: &str, audio_enc: &str, cb: impl FnOnce(&Path)) { let Ok(pipeline) = gst::parse::launch(&format!( "videotestsrc num-buffers=99 ! {video_enc} ! mux. \ audiotestsrc num-buffers=140 ! {audio_enc} ! mux. \ isomp4mux name=mux ! filesink name=sink" )) else { println!("could not build encoding pipeline"); return; }; let pipeline = Pipeline(pipeline.downcast::().unwrap()); let dir = tempfile::TempDir::new().unwrap(); let mut location = dir.path().to_owned(); location.push("test.mp4"); let sink = pipeline.by_name("sink").unwrap(); sink.set_property("location", location.to_str().expect("Non-UTF8 filename")); pipeline.into_completion(); cb(&location) } #[test] fn test_basic_x264_aac() { init(); test_basic_with("x264enc", "fdkaacenc", |location| { let discoverer = gst_pbutils::Discoverer::new(gst::ClockTime::from_seconds(5)) .expect("Failed to create discoverer"); let info = discoverer .discover_uri( url::Url::from_file_path(location) .expect("Failed to convert filename to URL") .as_str(), ) .expect("Failed to discover MP4 file"); assert_eq!(info.duration(), Some(gst::ClockTime::from_mseconds(3_300))); let audio_streams = info.audio_streams(); assert_eq!(audio_streams.len(), 1); let audio_stream = &audio_streams[0]; assert_eq!(audio_stream.channels(), 1); assert_eq!(audio_stream.sample_rate(), 44_100); let caps = audio_stream.caps().unwrap(); assert!( caps.can_intersect( &gst::Caps::builder("audio/mpeg") .any_features() .field("mpegversion", 4i32) .build() ), "Unexpected audio caps {caps:?}" ); let video_streams = info.video_streams(); assert_eq!(video_streams.len(), 1); let video_stream = &video_streams[0]; assert_eq!(video_stream.width(), 320); assert_eq!(video_stream.height(), 240); assert_eq!(video_stream.framerate(), gst::Fraction::new(30, 1)); assert_eq!(video_stream.par(), gst::Fraction::new(1, 1)); assert!(!video_stream.is_interlaced()); let caps = video_stream.caps().unwrap(); assert!( caps.can_intersect(&gst::Caps::builder("video/x-h264").any_features().build()), "Unexpected video caps {caps:?}" ); }) } #[test] fn test_roundtrip_vp9_flac() { init(); test_basic_with("vp9enc ! vp9parse", "flacenc ! flacparse", |location| { let Ok(pipeline) = gst::parse::launch( "filesrc name=src ! qtdemux name=demux \ demux.audio_0 ! queue ! flacdec ! fakesink \ demux.video_0 ! queue ! vp9dec ! fakesink", ) else { panic!("could not build decoding pipeline") }; let pipeline = Pipeline(pipeline.downcast::().unwrap()); pipeline .by_name("src") .unwrap() .set_property("location", location.display().to_string()); pipeline.into_completion(); }) }