mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2025-02-27 18:16:20 +00:00
1347 lines
51 KiB
Rust
1347 lines
51 KiB
Rust
// Copyright (C) 2024, asymptotic.io
|
|
// Author: Sanchayan Maity <sanchayan@asymptotic.io>
|
|
//
|
|
// 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
|
|
// <https://mozilla.org/MPL/2.0/>.
|
|
//
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
/*
|
|
* Written on the same lines as the `hlssink3` tests.
|
|
*/
|
|
|
|
use gio::prelude::*;
|
|
use gst::prelude::*;
|
|
use gsthlsmultivariantsink::{HlsMultivariantSinkMuxerType, HlsMultivariantSinkPlaylistType};
|
|
use std::io::Write;
|
|
use std::str::FromStr;
|
|
use std::sync::mpsc::SyncSender;
|
|
use std::sync::{mpsc, Arc, LazyLock, Mutex};
|
|
use std::time::Duration;
|
|
use std::{collections::HashSet, hash::Hash};
|
|
|
|
fn is_playlist_events_eq<T>(a: &[T], b: &[T]) -> bool
|
|
where
|
|
T: Eq + Hash + std::fmt::Debug,
|
|
{
|
|
let a: HashSet<_> = a.iter().collect();
|
|
let b: HashSet<_> = b.iter().collect();
|
|
|
|
a == b
|
|
}
|
|
|
|
static CAT: LazyLock<gst::DebugCategory> = LazyLock::new(|| {
|
|
gst::DebugCategory::new(
|
|
"hlsmultivariantsink-test",
|
|
gst::DebugColorFlags::empty(),
|
|
Some("HLS sink test"),
|
|
)
|
|
});
|
|
|
|
macro_rules! try_create_element {
|
|
($l:expr, $n:expr) => {
|
|
match gst::ElementFactory::find($l) {
|
|
Some(factory) => Ok(factory.create().name($n).build().unwrap()),
|
|
None => {
|
|
eprintln!("Could not find {} ({}) plugin, skipping test", $l, $n);
|
|
return Err(());
|
|
}
|
|
}
|
|
};
|
|
($l:expr) => {{
|
|
let alias: String = format!("test_{}", $l);
|
|
try_create_element!($l, <std::string::String as AsRef<str>>::as_ref(&alias))
|
|
}};
|
|
}
|
|
|
|
fn init() {
|
|
use std::sync::Once;
|
|
static INIT: Once = Once::new();
|
|
|
|
INIT.call_once(|| {
|
|
gst::init().unwrap();
|
|
gstfmp4::plugin_register_static().expect("Need cmafmux for hlsmultivariantsink test");
|
|
gsthlssink3::plugin_register_static().expect("Need hlssink3 for hlsmultivariantsink test");
|
|
gsthlsmultivariantsink::plugin_register_static().expect("hlsmultivariantsink test");
|
|
});
|
|
}
|
|
|
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
|
enum HlsSinkEvent {
|
|
DeleteFragment(String),
|
|
GetMultivariantPlaylistStream(String),
|
|
GetFragmentStream(String),
|
|
GetInitStream(String),
|
|
GetPlaylistStream(String),
|
|
}
|
|
|
|
struct MemoryPlaylistFile {
|
|
handler: Arc<Mutex<String>>,
|
|
}
|
|
|
|
impl MemoryPlaylistFile {
|
|
fn clear_content(&self) {
|
|
let mut string = self.handler.lock().unwrap();
|
|
string.clear();
|
|
}
|
|
}
|
|
|
|
impl Write for MemoryPlaylistFile {
|
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
|
let value = std::str::from_utf8(buf).unwrap();
|
|
let mut string = self.handler.lock().unwrap();
|
|
string.push_str(value);
|
|
Ok(buf.len())
|
|
}
|
|
|
|
fn flush(&mut self) -> std::io::Result<()> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn video_bin(
|
|
width: u32,
|
|
height: u32,
|
|
fps: u32,
|
|
bitrate: u32,
|
|
iframe_only: bool,
|
|
h265: bool,
|
|
) -> Result<gst::Bin, ()> {
|
|
const BUFFER_NB: i32 = 62;
|
|
|
|
let bin = gst::Bin::new();
|
|
|
|
let videosrc = try_create_element!("videotestsrc")?;
|
|
let videoconvert = try_create_element!("videoconvert")?;
|
|
let videoscale = try_create_element!("videoscale")?;
|
|
let videorate = try_create_element!("videorate")?;
|
|
let capsfilter = try_create_element!("capsfilter")?;
|
|
let x26xenc = if h265 {
|
|
try_create_element!("x265enc")?
|
|
} else {
|
|
try_create_element!("x264enc")?
|
|
};
|
|
let h26xparse = if h265 {
|
|
try_create_element!("h265parse")?
|
|
} else {
|
|
try_create_element!("h264parse")?
|
|
};
|
|
let queue = try_create_element!("queue")?;
|
|
|
|
videosrc.set_property("is-live", true);
|
|
videosrc.set_property_from_str("pattern", "ball");
|
|
videosrc.set_property("num-buffers", BUFFER_NB);
|
|
|
|
let caps = gst::Caps::from_str(format!("video/x-raw,width={width},height={height},framerate={fps}/1,pixel-aspect-ratio=1/1,format=I420").as_str()).unwrap();
|
|
capsfilter.set_property("caps", caps);
|
|
|
|
x26xenc.set_property("bitrate", bitrate);
|
|
if iframe_only {
|
|
x26xenc.set_property("key-int-max", 1);
|
|
} else if h265 {
|
|
x26xenc.set_property("key-int-max", (fps * 2) as i32);
|
|
} else {
|
|
x26xenc.set_property("key-int-max", fps * 2);
|
|
}
|
|
|
|
bin.add(&videosrc).unwrap();
|
|
bin.add(&videoconvert).unwrap();
|
|
bin.add(&videoscale).unwrap();
|
|
bin.add(&videorate).unwrap();
|
|
bin.add(&capsfilter).unwrap();
|
|
bin.add(&x26xenc).unwrap();
|
|
bin.add(&h26xparse).unwrap();
|
|
bin.add(&queue).unwrap();
|
|
|
|
videosrc.link(&videoconvert).unwrap();
|
|
videoconvert.link(&videorate).unwrap();
|
|
videorate.link(&videoscale).unwrap();
|
|
videoscale.link(&capsfilter).unwrap();
|
|
capsfilter.link(&x26xenc).unwrap();
|
|
x26xenc.link(&h26xparse).unwrap();
|
|
h26xparse.link(&queue).unwrap();
|
|
|
|
let queue_srcpad = queue.static_pad("src").unwrap();
|
|
|
|
let srcpad = gst::GhostPad::with_target(&queue_srcpad).unwrap();
|
|
|
|
bin.add_pad(&srcpad).unwrap();
|
|
|
|
Ok(bin)
|
|
}
|
|
|
|
fn audio_bin(bitrate: i32) -> Result<gst::Bin, ()> {
|
|
const BUFFER_NB: i32 = 62;
|
|
|
|
let bin = gst::Bin::new();
|
|
|
|
let audiosrc = try_create_element!("audiotestsrc")?;
|
|
let audioconvert = try_create_element!("audioconvert")?;
|
|
let audiorate = try_create_element!("audiorate")?;
|
|
let capsfilter = try_create_element!("capsfilter")?;
|
|
let aacenc = try_create_element!("fdkaacenc")?;
|
|
let aacparse = try_create_element!("aacparse")?;
|
|
let queue = try_create_element!("queue")?;
|
|
|
|
let caps = gst::Caps::from_str("audio/x-raw,channels=2,rate=48000,format=S16LE").unwrap();
|
|
|
|
audiosrc.set_property("is-live", true);
|
|
audiosrc.set_property("num-buffers", BUFFER_NB);
|
|
capsfilter.set_property("caps", caps);
|
|
aacenc.set_property("bitrate", bitrate);
|
|
|
|
bin.add(&audiosrc).unwrap();
|
|
bin.add(&audioconvert).unwrap();
|
|
bin.add(&audiorate).unwrap();
|
|
bin.add(&capsfilter).unwrap();
|
|
bin.add(&aacenc).unwrap();
|
|
bin.add(&aacparse).unwrap();
|
|
bin.add(&queue).unwrap();
|
|
|
|
audiosrc.link(&audioconvert).unwrap();
|
|
audioconvert.link(&audiorate).unwrap();
|
|
audiorate.link(&capsfilter).unwrap();
|
|
capsfilter.link(&aacenc).unwrap();
|
|
aacenc.link(&aacparse).unwrap();
|
|
aacparse.link(&queue).unwrap();
|
|
|
|
let queue_srcpad = queue.static_pad("src").unwrap();
|
|
|
|
let srcpad = gst::GhostPad::with_target(&queue_srcpad).unwrap();
|
|
|
|
bin.add_pad(&srcpad).unwrap();
|
|
|
|
Ok(bin)
|
|
}
|
|
|
|
/*
|
|
* For the tests below the directory structure would be expected to be
|
|
* as follows.
|
|
*
|
|
* multivariant playlist will have the path /tmp/hlssink/multivariant.m3u8.
|
|
* Every other variant stream or alternate rendition will be relative to
|
|
* /tmp/hlssink.
|
|
*
|
|
# tmp → hlssink λ:
|
|
.
|
|
├── hi
|
|
├── hi-audio
|
|
├── low
|
|
├── low-audio
|
|
├── mid
|
|
├── mid-audio
|
|
*
|
|
*/
|
|
|
|
fn setup_signals(
|
|
hlssink: &gst::Element,
|
|
hls_events_sender: SyncSender<HlsSinkEvent>,
|
|
multivariant_playlist_content: Arc<Mutex<String>>,
|
|
playlist_content: Arc<Mutex<String>>,
|
|
muxer_type: HlsMultivariantSinkMuxerType,
|
|
) {
|
|
hlssink.connect("get-multivariant-playlist-stream", false, {
|
|
let hls_events_sender = hls_events_sender.clone();
|
|
let multivariant_playlist_content = multivariant_playlist_content.clone();
|
|
move |args| {
|
|
let location = args[1].get::<String>().expect("No location given");
|
|
|
|
gst::info!(CAT, "get-multivariant-playlist-stream: {}", location);
|
|
|
|
hls_events_sender
|
|
.try_send(HlsSinkEvent::GetMultivariantPlaylistStream(location))
|
|
.expect("Send multivariant playlist event");
|
|
|
|
let playlist = MemoryPlaylistFile {
|
|
handler: Arc::clone(&multivariant_playlist_content),
|
|
};
|
|
|
|
playlist.clear_content();
|
|
let output = gio::WriteOutputStream::new(playlist);
|
|
Some(output.to_value())
|
|
}
|
|
});
|
|
|
|
if muxer_type == HlsMultivariantSinkMuxerType::Cmaf {
|
|
hlssink.connect("get-init-stream", false, {
|
|
let hls_events_sender = hls_events_sender.clone();
|
|
move |args| {
|
|
let location = args[1].get::<String>().expect("No location given");
|
|
|
|
hls_events_sender
|
|
.try_send(HlsSinkEvent::GetInitStream(location))
|
|
.expect("Send init event");
|
|
|
|
let stream = gio::MemoryOutputStream::new_resizable();
|
|
Some(stream.to_value())
|
|
}
|
|
});
|
|
}
|
|
|
|
hlssink.connect("get-playlist-stream", false, {
|
|
let hls_events_sender = hls_events_sender.clone();
|
|
let playlist_content = playlist_content.clone();
|
|
move |args| {
|
|
let location = args[1].get::<String>().expect("No location given");
|
|
|
|
hls_events_sender
|
|
.try_send(HlsSinkEvent::GetPlaylistStream(location))
|
|
.expect("Send playlist event");
|
|
|
|
let playlist = MemoryPlaylistFile {
|
|
handler: Arc::clone(&playlist_content),
|
|
};
|
|
|
|
playlist.clear_content();
|
|
let output = gio::WriteOutputStream::new(playlist);
|
|
Some(output.to_value())
|
|
}
|
|
});
|
|
|
|
hlssink.connect("get-fragment-stream", false, {
|
|
let hls_events_sender = hls_events_sender.clone();
|
|
move |args| {
|
|
let location = args[1].get::<String>().expect("No location given");
|
|
|
|
hls_events_sender
|
|
.try_send(HlsSinkEvent::GetFragmentStream(location))
|
|
.expect("Send fragment event");
|
|
|
|
let stream = gio::MemoryOutputStream::new_resizable();
|
|
Some(stream.to_value())
|
|
}
|
|
});
|
|
|
|
hlssink.connect("delete-fragment", false, move |args| {
|
|
let location = args[1].get::<String>().expect("No location given");
|
|
hls_events_sender
|
|
.try_send(HlsSinkEvent::DeleteFragment(location))
|
|
.expect("Send delete fragment event");
|
|
Some(true.to_value())
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn hlsmultivariantsink_multiple_audio_rendition_multiple_video_variant() -> Result<(), ()> {
|
|
init();
|
|
|
|
let pipeline = gst::Pipeline::with_name("hlsmultivariantsink_pipeline");
|
|
|
|
let hlsmultivariantsink = gst::ElementFactory::make("hlsmultivariantsink")
|
|
.name("test_hlsmultivariantsink")
|
|
.property(
|
|
"multivariant-playlist-location",
|
|
"/tmp/hlssink/multivariant.m3u8",
|
|
)
|
|
.property("target-duration", 2u32)
|
|
.property("playlist-length", 2u32)
|
|
.property("max-files", 2u32)
|
|
.build()
|
|
.expect("Must be able to instantiate hlsmultivariantsink");
|
|
|
|
hlsmultivariantsink.set_property("playlist-type", HlsMultivariantSinkPlaylistType::Event);
|
|
let pl_type: HlsMultivariantSinkPlaylistType = hlsmultivariantsink.property("playlist-type");
|
|
assert_eq!(pl_type, HlsMultivariantSinkPlaylistType::Event);
|
|
|
|
hlsmultivariantsink.set_property_from_str("playlist-type", "unspecified");
|
|
|
|
let muxer_type: HlsMultivariantSinkMuxerType = hlsmultivariantsink.property("muxer-type");
|
|
assert_eq!(muxer_type, HlsMultivariantSinkMuxerType::Cmaf);
|
|
|
|
pipeline.add(&hlsmultivariantsink).unwrap();
|
|
|
|
let (hls_events_sender, hls_events_receiver) = mpsc::sync_channel(100);
|
|
let multivariant_playlist_content = Arc::new(Mutex::new(String::from("")));
|
|
let playlist_content = Arc::new(Mutex::new(String::from("")));
|
|
|
|
setup_signals(
|
|
&hlsmultivariantsink,
|
|
hls_events_sender.clone(),
|
|
multivariant_playlist_content.clone(),
|
|
playlist_content.clone(),
|
|
HlsMultivariantSinkMuxerType::Cmaf,
|
|
);
|
|
|
|
let audio_bin1 = audio_bin(256000).unwrap();
|
|
let audio_bin1_pad = audio_bin1.static_pad("src").unwrap();
|
|
let audio1_pad = hlsmultivariantsink.request_pad_simple("audio_%u").unwrap();
|
|
let r = gst::Structure::builder("audio1-rendition")
|
|
.field("media", "AUDIO")
|
|
.field("uri", "hi-audio/audio.m3u8")
|
|
.field("group_id", "aac")
|
|
.field("language", "en")
|
|
.field("name", "English")
|
|
.field("default", true)
|
|
.field("autoselect", false)
|
|
.build();
|
|
audio1_pad.set_property("alternate-rendition", r);
|
|
pipeline.add(&audio_bin1).unwrap();
|
|
audio_bin1_pad.link(&audio1_pad).unwrap();
|
|
|
|
let audio_bin2 = audio_bin(128000).unwrap();
|
|
let audio_bin2_pad = audio_bin2.static_pad("src").unwrap();
|
|
let audio2_pad = hlsmultivariantsink.request_pad_simple("audio_%u").unwrap();
|
|
let r = gst::Structure::builder("audio2-rendition")
|
|
.field("media", "AUDIO")
|
|
.field("uri", "mid-audio/audio.m3u8")
|
|
.field("group_id", "aac")
|
|
.field("language", "fr")
|
|
.field("name", "French")
|
|
.field("default", false)
|
|
.field("autoselect", false)
|
|
.build();
|
|
audio2_pad.set_property("alternate-rendition", r);
|
|
pipeline.add(&audio_bin2).unwrap();
|
|
audio_bin2_pad.link(&audio2_pad).unwrap();
|
|
|
|
let video_bin1 = video_bin(1920, 1080, 30, 2500, false, false).unwrap();
|
|
let video_bin1_pad = video_bin1.static_pad("src").unwrap();
|
|
let video1_pad = hlsmultivariantsink.request_pad_simple("video_%u").unwrap();
|
|
let v = gst::Structure::builder("video1-variant")
|
|
.field("uri", "hi/video.m3u8")
|
|
.field("audio", "aac")
|
|
.field("bandwidth", 2500)
|
|
.build();
|
|
video1_pad.set_property("variant", v);
|
|
pipeline.add(&video_bin1).unwrap();
|
|
video_bin1_pad.link(&video1_pad).unwrap();
|
|
|
|
let video_bin2 = video_bin(1280, 720, 30, 1500, false, false).unwrap();
|
|
let video_bin2_pad = video_bin2.static_pad("src").unwrap();
|
|
let video2_pad = hlsmultivariantsink.request_pad_simple("video_%u").unwrap();
|
|
let v = gst::Structure::builder("video2-variant")
|
|
.field("uri", "mid/video.m3u8")
|
|
.field("audio", "aac")
|
|
.field("bandwidth", 1500)
|
|
.build();
|
|
video2_pad.set_property("variant", v);
|
|
pipeline.add(&video_bin2).unwrap();
|
|
video_bin2_pad.link(&video2_pad).unwrap();
|
|
|
|
let video_bin3 = video_bin(640, 360, 24, 700, false, false).unwrap();
|
|
let video_bin3_pad = video_bin3.static_pad("src").unwrap();
|
|
let video3_pad = hlsmultivariantsink.request_pad_simple("video_%u").unwrap();
|
|
let v = gst::Structure::builder("video2-variant")
|
|
.field("uri", "low/video.m3u8")
|
|
.field("audio", "aac")
|
|
.field("bandwidth", 700)
|
|
.build();
|
|
video3_pad.set_property("variant", v);
|
|
pipeline.add(&video_bin3).unwrap();
|
|
video_bin3_pad.link(&video3_pad).unwrap();
|
|
|
|
pipeline.set_state(gst::State::Playing).unwrap();
|
|
|
|
let mut eos = false;
|
|
let bus = pipeline.bus().unwrap();
|
|
while let Some(msg) = bus.timed_pop(gst::ClockTime::NONE) {
|
|
use gst::MessageView;
|
|
match msg.view() {
|
|
MessageView::Eos(..) => {
|
|
eos = true;
|
|
break;
|
|
}
|
|
MessageView::Error(e) => gst::error!(CAT, "hlsmultivariantsink error: {}", e),
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
pipeline.debug_to_dot_file_with_ts(
|
|
gst::DebugGraphDetails::all(),
|
|
"multiple_audio_rendition_multiple_video_variant",
|
|
);
|
|
|
|
pipeline.set_state(gst::State::Null).unwrap();
|
|
assert!(eos);
|
|
|
|
let mut actual_events = Vec::new();
|
|
while let Ok(event) = hls_events_receiver.recv_timeout(Duration::from_millis(1)) {
|
|
actual_events.push(event);
|
|
}
|
|
let expected_events = {
|
|
use self::HlsSinkEvent::*;
|
|
vec![
|
|
GetMultivariantPlaylistStream("/tmp/hlssink/multivariant.m3u8".to_string()),
|
|
GetInitStream("/tmp/hlssink/hi/init00000.mp4".to_string()),
|
|
GetInitStream("/tmp/hlssink/mid/init00000.mp4".to_string()),
|
|
GetInitStream("/tmp/hlssink/low/init00000.mp4".to_string()),
|
|
GetInitStream("/tmp/hlssink/hi-audio/init00000.mp4".to_string()),
|
|
GetInitStream("/tmp/hlssink/mid-audio/init00000.mp4".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/hi/video.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/mid/video.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/low/video.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/hi-audio/audio.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/mid-audio/audio.m3u8".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi/segment00000.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi/segment00001.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/mid/segment00000.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/mid/segment00001.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/low/segment00000.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/low/segment00001.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi-audio/segment00000.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi-audio/segment00001.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/mid-audio/segment00000.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/mid-audio/segment00001.m4s".to_string()),
|
|
]
|
|
};
|
|
assert!(is_playlist_events_eq(&expected_events, &actual_events));
|
|
|
|
let contents = multivariant_playlist_content.lock().unwrap();
|
|
|
|
#[rustfmt::skip]
|
|
assert_eq!(
|
|
r###"#EXTM3U
|
|
#EXT-X-VERSION:4
|
|
#EXT-X-MEDIA:TYPE=AUDIO,URI="hi-audio/audio.m3u8",GROUP-ID="aac",LANGUAGE="en",NAME="English",DEFAULT=YES
|
|
#EXT-X-MEDIA:TYPE=AUDIO,URI="mid-audio/audio.m3u8",GROUP-ID="aac",LANGUAGE="fr",NAME="French"
|
|
#EXT-X-STREAM-INF:BANDWIDTH=2500,CODECS="avc1.64001E,avc1.64001F,avc1.640028,mp4a.40.2",AUDIO="aac"
|
|
hi/video.m3u8
|
|
#EXT-X-STREAM-INF:BANDWIDTH=1500,CODECS="avc1.64001E,avc1.64001F,avc1.640028,mp4a.40.2",AUDIO="aac"
|
|
mid/video.m3u8
|
|
#EXT-X-STREAM-INF:BANDWIDTH=700,CODECS="avc1.64001E,avc1.64001F,avc1.640028,mp4a.40.2",AUDIO="aac"
|
|
low/video.m3u8
|
|
"###,
|
|
contents.to_string()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn hlsmultivariantsink_multiple_audio_rendition_single_video_variant() -> Result<(), ()> {
|
|
init();
|
|
|
|
let pipeline = gst::Pipeline::with_name("hlsmultivariantsink_pipeline");
|
|
|
|
let hlsmultivariantsink = gst::ElementFactory::make("hlsmultivariantsink")
|
|
.name("test_hlsmultivariantsink")
|
|
.property(
|
|
"multivariant-playlist-location",
|
|
"/tmp/hlssink/multivariant.m3u8",
|
|
)
|
|
.property("target-duration", 2u32)
|
|
.property("playlist-length", 2u32)
|
|
.property("max-files", 2u32)
|
|
.build()
|
|
.expect("Must be able to instantiate hlsmultivariantsink");
|
|
|
|
hlsmultivariantsink.set_property("playlist-type", HlsMultivariantSinkPlaylistType::Event);
|
|
let pl_type: HlsMultivariantSinkPlaylistType = hlsmultivariantsink.property("playlist-type");
|
|
assert_eq!(pl_type, HlsMultivariantSinkPlaylistType::Event);
|
|
|
|
hlsmultivariantsink.set_property_from_str("playlist-type", "unspecified");
|
|
|
|
let muxer_type: HlsMultivariantSinkMuxerType = hlsmultivariantsink.property("muxer-type");
|
|
assert_eq!(muxer_type, HlsMultivariantSinkMuxerType::Cmaf);
|
|
|
|
pipeline.add(&hlsmultivariantsink).unwrap();
|
|
|
|
let (hls_events_sender, hls_events_receiver) = mpsc::sync_channel(100);
|
|
let multivariant_playlist_content = Arc::new(Mutex::new(String::from("")));
|
|
let playlist_content = Arc::new(Mutex::new(String::from("")));
|
|
|
|
setup_signals(
|
|
&hlsmultivariantsink,
|
|
hls_events_sender.clone(),
|
|
multivariant_playlist_content.clone(),
|
|
playlist_content.clone(),
|
|
HlsMultivariantSinkMuxerType::Cmaf,
|
|
);
|
|
|
|
let audio_bin1 = audio_bin(256000).unwrap();
|
|
let audio_bin1_pad = audio_bin1.static_pad("src").unwrap();
|
|
let audio1_pad = hlsmultivariantsink.request_pad_simple("audio_%u").unwrap();
|
|
let r = gst::Structure::builder("audio1-rendition")
|
|
.field("media", "AUDIO")
|
|
.field("uri", "hi-audio/audio.m3u8")
|
|
.field("group_id", "aac")
|
|
.field("language", "en")
|
|
.field("name", "English")
|
|
.field("default", true)
|
|
.field("autoselect", false)
|
|
.build();
|
|
audio1_pad.set_property("alternate-rendition", r);
|
|
pipeline.add(&audio_bin1).unwrap();
|
|
audio_bin1_pad.link(&audio1_pad).unwrap();
|
|
|
|
let audio_bin2 = audio_bin(128000).unwrap();
|
|
let audio_bin2_pad = audio_bin2.static_pad("src").unwrap();
|
|
let audio2_pad = hlsmultivariantsink.request_pad_simple("audio_%u").unwrap();
|
|
let r = gst::Structure::builder("audio2-rendition")
|
|
.field("media", "AUDIO")
|
|
.field("uri", "mid-audio/audio.m3u8")
|
|
.field("group_id", "aac")
|
|
.field("language", "fr")
|
|
.field("name", "French")
|
|
.field("default", false)
|
|
.field("autoselect", false)
|
|
.build();
|
|
audio2_pad.set_property("alternate-rendition", r);
|
|
pipeline.add(&audio_bin2).unwrap();
|
|
audio_bin2_pad.link(&audio2_pad).unwrap();
|
|
|
|
let video_bin1 = video_bin(1920, 1080, 30, 2500, false, false).unwrap();
|
|
let video_bin1_pad = video_bin1.static_pad("src").unwrap();
|
|
let video1_pad = hlsmultivariantsink.request_pad_simple("video_%u").unwrap();
|
|
let v = gst::Structure::builder("video1-variant")
|
|
.field("uri", "hi/video.m3u8")
|
|
.field("audio", "aac")
|
|
.field("bandwidth", 2500)
|
|
.build();
|
|
video1_pad.set_property("variant", v);
|
|
pipeline.add(&video_bin1).unwrap();
|
|
video_bin1_pad.link(&video1_pad).unwrap();
|
|
|
|
pipeline.set_state(gst::State::Playing).unwrap();
|
|
|
|
let mut eos = false;
|
|
let bus = pipeline.bus().unwrap();
|
|
while let Some(msg) = bus.timed_pop(gst::ClockTime::NONE) {
|
|
use gst::MessageView;
|
|
match msg.view() {
|
|
MessageView::Eos(..) => {
|
|
eos = true;
|
|
break;
|
|
}
|
|
MessageView::Error(e) => gst::error!(CAT, "hlsmultivariantsink error: {}", e),
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
pipeline.debug_to_dot_file_with_ts(
|
|
gst::DebugGraphDetails::all(),
|
|
"multiple_audio_rendition_single_video_variant",
|
|
);
|
|
|
|
pipeline.set_state(gst::State::Null).unwrap();
|
|
assert!(eos);
|
|
|
|
let mut actual_events = Vec::new();
|
|
while let Ok(event) = hls_events_receiver.recv_timeout(Duration::from_millis(1)) {
|
|
actual_events.push(event);
|
|
}
|
|
let expected_events = {
|
|
use self::HlsSinkEvent::*;
|
|
vec![
|
|
GetMultivariantPlaylistStream("/tmp/hlssink/multivariant.m3u8".to_string()),
|
|
GetInitStream("/tmp/hlssink/hi/init00000.mp4".to_string()),
|
|
GetInitStream("/tmp/hlssink/hi-audio/init00000.mp4".to_string()),
|
|
GetInitStream("/tmp/hlssink/mid-audio/init00000.mp4".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/hi/video.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/hi-audio/audio.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/mid-audio/audio.m3u8".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi/segment00000.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi/segment00001.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi-audio/segment00000.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi-audio/segment00001.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/mid-audio/segment00000.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/mid-audio/segment00001.m4s".to_string()),
|
|
]
|
|
};
|
|
assert!(is_playlist_events_eq(&expected_events, &actual_events));
|
|
|
|
let contents = multivariant_playlist_content.lock().unwrap();
|
|
|
|
#[rustfmt::skip]
|
|
assert_eq!(
|
|
r###"#EXTM3U
|
|
#EXT-X-VERSION:4
|
|
#EXT-X-MEDIA:TYPE=AUDIO,URI="hi-audio/audio.m3u8",GROUP-ID="aac",LANGUAGE="en",NAME="English",DEFAULT=YES
|
|
#EXT-X-MEDIA:TYPE=AUDIO,URI="mid-audio/audio.m3u8",GROUP-ID="aac",LANGUAGE="fr",NAME="French"
|
|
#EXT-X-STREAM-INF:BANDWIDTH=2500,CODECS="avc1.640028,mp4a.40.2",AUDIO="aac"
|
|
hi/video.m3u8
|
|
"###,
|
|
contents.to_string()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn hlsmultivariantsink_single_audio_rendition_multiple_video_variant() -> Result<(), ()> {
|
|
init();
|
|
|
|
let pipeline = gst::Pipeline::with_name("hlsmultivariantsink_pipeline");
|
|
|
|
let hlsmultivariantsink = gst::ElementFactory::make("hlsmultivariantsink")
|
|
.name("test_hlsmultivariantsink")
|
|
.property(
|
|
"multivariant-playlist-location",
|
|
"/tmp/hlssink/multivariant.m3u8",
|
|
)
|
|
.property("target-duration", 2u32)
|
|
.property("playlist-length", 2u32)
|
|
.property("max-files", 2u32)
|
|
.build()
|
|
.expect("Must be able to instantiate hlsmultivariantsink");
|
|
|
|
hlsmultivariantsink.set_property("playlist-type", HlsMultivariantSinkPlaylistType::Event);
|
|
let pl_type: HlsMultivariantSinkPlaylistType = hlsmultivariantsink.property("playlist-type");
|
|
assert_eq!(pl_type, HlsMultivariantSinkPlaylistType::Event);
|
|
|
|
hlsmultivariantsink.set_property_from_str("playlist-type", "unspecified");
|
|
|
|
let muxer_type: HlsMultivariantSinkMuxerType = hlsmultivariantsink.property("muxer-type");
|
|
assert_eq!(muxer_type, HlsMultivariantSinkMuxerType::Cmaf);
|
|
|
|
pipeline.add(&hlsmultivariantsink).unwrap();
|
|
|
|
let (hls_events_sender, hls_events_receiver) = mpsc::sync_channel(100);
|
|
let multivariant_playlist_content = Arc::new(Mutex::new(String::from("")));
|
|
let playlist_content = Arc::new(Mutex::new(String::from("")));
|
|
|
|
setup_signals(
|
|
&hlsmultivariantsink,
|
|
hls_events_sender.clone(),
|
|
multivariant_playlist_content.clone(),
|
|
playlist_content.clone(),
|
|
HlsMultivariantSinkMuxerType::Cmaf,
|
|
);
|
|
|
|
let audio_bin1 = audio_bin(256000).unwrap();
|
|
let audio_bin1_pad = audio_bin1.static_pad("src").unwrap();
|
|
let audio1_pad = hlsmultivariantsink.request_pad_simple("audio_%u").unwrap();
|
|
let r = gst::Structure::builder("audio1-rendition")
|
|
.field("media", "AUDIO")
|
|
.field("uri", "hi-audio/audio.m3u8")
|
|
.field("group_id", "aac")
|
|
.field("language", "en")
|
|
.field("name", "English")
|
|
.field("default", true)
|
|
.field("autoselect", false)
|
|
.build();
|
|
audio1_pad.set_property("alternate-rendition", r);
|
|
pipeline.add(&audio_bin1).unwrap();
|
|
audio_bin1_pad.link(&audio1_pad).unwrap();
|
|
|
|
let video_bin1 = video_bin(1920, 1080, 30, 2500, false, false).unwrap();
|
|
let video_bin1_pad = video_bin1.static_pad("src").unwrap();
|
|
let video1_pad = hlsmultivariantsink.request_pad_simple("video_%u").unwrap();
|
|
let v = gst::Structure::builder("video1-variant")
|
|
.field("uri", "hi/video.m3u8")
|
|
.field("audio", "aac")
|
|
.field("bandwidth", 2500)
|
|
.build();
|
|
video1_pad.set_property("variant", v);
|
|
pipeline.add(&video_bin1).unwrap();
|
|
video_bin1_pad.link(&video1_pad).unwrap();
|
|
|
|
let video_bin2 = video_bin(1280, 720, 30, 1500, false, false).unwrap();
|
|
let video_bin2_pad = video_bin2.static_pad("src").unwrap();
|
|
let video2_pad = hlsmultivariantsink.request_pad_simple("video_%u").unwrap();
|
|
let v = gst::Structure::builder("video2-variant")
|
|
.field("uri", "mid/video.m3u8")
|
|
.field("audio", "aac")
|
|
.field("bandwidth", 1500)
|
|
.build();
|
|
video2_pad.set_property("variant", v);
|
|
pipeline.add(&video_bin2).unwrap();
|
|
video_bin2_pad.link(&video2_pad).unwrap();
|
|
|
|
let video_bin3 = video_bin(640, 360, 24, 700, false, false).unwrap();
|
|
let video_bin3_pad = video_bin3.static_pad("src").unwrap();
|
|
let video3_pad = hlsmultivariantsink.request_pad_simple("video_%u").unwrap();
|
|
let v = gst::Structure::builder("video2-variant")
|
|
.field("uri", "low/video.m3u8")
|
|
.field("audio", "aac")
|
|
.field("bandwidth", 700)
|
|
.build();
|
|
video3_pad.set_property("variant", v);
|
|
pipeline.add(&video_bin3).unwrap();
|
|
video_bin3_pad.link(&video3_pad).unwrap();
|
|
|
|
pipeline.set_state(gst::State::Playing).unwrap();
|
|
|
|
let mut eos = false;
|
|
let bus = pipeline.bus().unwrap();
|
|
while let Some(msg) = bus.timed_pop(gst::ClockTime::NONE) {
|
|
use gst::MessageView;
|
|
match msg.view() {
|
|
MessageView::Eos(..) => {
|
|
eos = true;
|
|
break;
|
|
}
|
|
MessageView::Error(e) => gst::error!(CAT, "hlsmultivariantsink error: {}", e),
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
pipeline.debug_to_dot_file_with_ts(
|
|
gst::DebugGraphDetails::all(),
|
|
"single_audio_rendition_multiple_video_variant",
|
|
);
|
|
|
|
pipeline.set_state(gst::State::Null).unwrap();
|
|
assert!(eos);
|
|
|
|
let mut actual_events = Vec::new();
|
|
while let Ok(event) = hls_events_receiver.recv_timeout(Duration::from_millis(1)) {
|
|
actual_events.push(event);
|
|
}
|
|
let expected_events = {
|
|
use self::HlsSinkEvent::*;
|
|
vec![
|
|
GetMultivariantPlaylistStream("/tmp/hlssink/multivariant.m3u8".to_string()),
|
|
GetInitStream("/tmp/hlssink/hi/init00000.mp4".to_string()),
|
|
GetInitStream("/tmp/hlssink/mid/init00000.mp4".to_string()),
|
|
GetInitStream("/tmp/hlssink/low/init00000.mp4".to_string()),
|
|
GetInitStream("/tmp/hlssink/hi-audio/init00000.mp4".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/hi/video.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/mid/video.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/low/video.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/hi-audio/audio.m3u8".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi/segment00000.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi/segment00001.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/mid/segment00000.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/mid/segment00001.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/low/segment00000.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/low/segment00001.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi-audio/segment00000.m4s".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi-audio/segment00001.m4s".to_string()),
|
|
]
|
|
};
|
|
assert!(is_playlist_events_eq(&expected_events, &actual_events));
|
|
|
|
let contents = multivariant_playlist_content.lock().unwrap();
|
|
|
|
#[rustfmt::skip]
|
|
assert_eq!(
|
|
r###"#EXTM3U
|
|
#EXT-X-VERSION:4
|
|
#EXT-X-MEDIA:TYPE=AUDIO,URI="hi-audio/audio.m3u8",GROUP-ID="aac",LANGUAGE="en",NAME="English",DEFAULT=YES
|
|
#EXT-X-STREAM-INF:BANDWIDTH=2500,CODECS="avc1.64001E,avc1.64001F,avc1.640028,mp4a.40.2",AUDIO="aac"
|
|
hi/video.m3u8
|
|
#EXT-X-STREAM-INF:BANDWIDTH=1500,CODECS="avc1.64001E,avc1.64001F,avc1.640028,mp4a.40.2",AUDIO="aac"
|
|
mid/video.m3u8
|
|
#EXT-X-STREAM-INF:BANDWIDTH=700,CODECS="avc1.64001E,avc1.64001F,avc1.640028,mp4a.40.2",AUDIO="aac"
|
|
low/video.m3u8
|
|
"###,
|
|
contents.to_string()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn hlsmultivariantsink_multiple_audio_rendition_multiple_video_variant_with_mpegts(
|
|
) -> Result<(), ()> {
|
|
init();
|
|
|
|
let pipeline = gst::Pipeline::with_name("hlsmultivariantsink_pipeline");
|
|
|
|
let hlsmultivariantsink = gst::ElementFactory::make("hlsmultivariantsink")
|
|
.name("test_hlsmultivariantsink")
|
|
.property(
|
|
"multivariant-playlist-location",
|
|
"/tmp/hlssink/multivariant.m3u8",
|
|
)
|
|
.property("target-duration", 2u32)
|
|
.property("playlist-length", 2u32)
|
|
.property("max-files", 2u32)
|
|
.build()
|
|
.expect("Must be able to instantiate hlsmultivariantsink");
|
|
|
|
hlsmultivariantsink.set_property("muxer-type", HlsMultivariantSinkMuxerType::MpegTs);
|
|
let muxer_type: HlsMultivariantSinkMuxerType = hlsmultivariantsink.property("muxer-type");
|
|
assert_eq!(muxer_type, HlsMultivariantSinkMuxerType::MpegTs);
|
|
|
|
pipeline.add(&hlsmultivariantsink).unwrap();
|
|
|
|
let (hls_events_sender, hls_events_receiver) = mpsc::sync_channel(100);
|
|
let multivariant_playlist_content = Arc::new(Mutex::new(String::from("")));
|
|
let playlist_content = Arc::new(Mutex::new(String::from("")));
|
|
|
|
setup_signals(
|
|
&hlsmultivariantsink,
|
|
hls_events_sender.clone(),
|
|
multivariant_playlist_content.clone(),
|
|
playlist_content.clone(),
|
|
HlsMultivariantSinkMuxerType::MpegTs,
|
|
);
|
|
|
|
let audio_bin1 = audio_bin(256000).unwrap();
|
|
let audio_bin1_pad = audio_bin1.static_pad("src").unwrap();
|
|
let audio1_pad = hlsmultivariantsink.request_pad_simple("audio_%u").unwrap();
|
|
let r = gst::Structure::builder("audio1-rendition")
|
|
.field("media", "AUDIO")
|
|
.field("uri", "hi-audio/audio.m3u8")
|
|
.field("group_id", "aac")
|
|
.field("language", "en")
|
|
.field("name", "English")
|
|
.field("default", true)
|
|
.field("autoselect", false)
|
|
.build();
|
|
audio1_pad.set_property("alternate-rendition", r);
|
|
pipeline.add(&audio_bin1).unwrap();
|
|
audio_bin1_pad.link(&audio1_pad).unwrap();
|
|
|
|
let audio_bin2 = audio_bin(128000).unwrap();
|
|
let audio_bin2_pad = audio_bin2.static_pad("src").unwrap();
|
|
let audio2_pad = hlsmultivariantsink.request_pad_simple("audio_%u").unwrap();
|
|
let r = gst::Structure::builder("audio2-rendition")
|
|
.field("media", "AUDIO")
|
|
.field("uri", "mid-audio/audio.m3u8")
|
|
.field("group_id", "aac")
|
|
.field("language", "fr")
|
|
.field("name", "French")
|
|
.field("default", false)
|
|
.field("autoselect", false)
|
|
.build();
|
|
audio2_pad.set_property("alternate-rendition", r);
|
|
pipeline.add(&audio_bin2).unwrap();
|
|
audio_bin2_pad.link(&audio2_pad).unwrap();
|
|
|
|
let video_bin1 = video_bin(1920, 1080, 30, 2500, false, false).unwrap();
|
|
let video_bin1_pad = video_bin1.static_pad("src").unwrap();
|
|
let video1_pad = hlsmultivariantsink.request_pad_simple("video_%u").unwrap();
|
|
let v = gst::Structure::builder("video1-variant")
|
|
.field("uri", "hi/video.m3u8")
|
|
.field("audio", "aac")
|
|
.field("bandwidth", 2500)
|
|
.build();
|
|
video1_pad.set_property("variant", v);
|
|
pipeline.add(&video_bin1).unwrap();
|
|
video_bin1_pad.link(&video1_pad).unwrap();
|
|
|
|
let video_bin2 = video_bin(1280, 720, 30, 1500, false, false).unwrap();
|
|
let video_bin2_pad = video_bin2.static_pad("src").unwrap();
|
|
let video2_pad = hlsmultivariantsink.request_pad_simple("video_%u").unwrap();
|
|
let v = gst::Structure::builder("video2-variant")
|
|
.field("uri", "mid/video.m3u8")
|
|
.field("audio", "aac")
|
|
.field("bandwidth", 1500)
|
|
.build();
|
|
video2_pad.set_property("variant", v);
|
|
pipeline.add(&video_bin2).unwrap();
|
|
video_bin2_pad.link(&video2_pad).unwrap();
|
|
|
|
let video_bin3 = video_bin(640, 360, 24, 700, false, false).unwrap();
|
|
let video_bin3_pad = video_bin3.static_pad("src").unwrap();
|
|
let video3_pad = hlsmultivariantsink.request_pad_simple("video_%u").unwrap();
|
|
let v = gst::Structure::builder("video3-variant")
|
|
.field("uri", "low/video.m3u8")
|
|
.field("audio", "aac")
|
|
.field("bandwidth", 700)
|
|
.build();
|
|
video3_pad.set_property("variant", v);
|
|
pipeline.add(&video_bin3).unwrap();
|
|
video_bin3_pad.link(&video3_pad).unwrap();
|
|
|
|
pipeline.set_state(gst::State::Playing).unwrap();
|
|
|
|
let mut eos = false;
|
|
let bus = pipeline.bus().unwrap();
|
|
while let Some(msg) = bus.timed_pop(gst::ClockTime::NONE) {
|
|
use gst::MessageView;
|
|
match msg.view() {
|
|
MessageView::Eos(..) => {
|
|
eos = true;
|
|
break;
|
|
}
|
|
MessageView::Error(e) => gst::error!(CAT, "hlsmultivariantsink error: {}", e),
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
pipeline.debug_to_dot_file_with_ts(
|
|
gst::DebugGraphDetails::all(),
|
|
"multiple_audio_rendition_multiple_video_variant_with_mpegts",
|
|
);
|
|
|
|
pipeline.set_state(gst::State::Null).unwrap();
|
|
assert!(eos);
|
|
|
|
let mut actual_events = Vec::new();
|
|
while let Ok(event) = hls_events_receiver.recv_timeout(Duration::from_millis(1)) {
|
|
actual_events.push(event);
|
|
}
|
|
let expected_events = {
|
|
use self::HlsSinkEvent::*;
|
|
vec![
|
|
GetMultivariantPlaylistStream("/tmp/hlssink/multivariant.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/hi/video.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/mid/video.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/low/video.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/hi-audio/audio.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/mid-audio/audio.m3u8".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi-audio/segment00000.ts".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/mid-audio/segment00000.ts".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi/segment00000.ts".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/mid/segment00000.ts".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/low/segment00000.ts".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi/segment00001.ts".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/mid/segment00001.ts".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/low/segment00001.ts".to_string()),
|
|
]
|
|
};
|
|
assert!(is_playlist_events_eq(&expected_events, &actual_events));
|
|
|
|
let contents = multivariant_playlist_content.lock().unwrap();
|
|
|
|
#[rustfmt::skip]
|
|
assert_eq!(
|
|
r###"#EXTM3U
|
|
#EXT-X-VERSION:4
|
|
#EXT-X-MEDIA:TYPE=AUDIO,URI="hi-audio/audio.m3u8",GROUP-ID="aac",LANGUAGE="en",NAME="English",DEFAULT=YES
|
|
#EXT-X-MEDIA:TYPE=AUDIO,URI="mid-audio/audio.m3u8",GROUP-ID="aac",LANGUAGE="fr",NAME="French"
|
|
#EXT-X-STREAM-INF:BANDWIDTH=2500,CODECS="avc1.64001E,avc1.64001F,avc1.640028,mp4a.40.2",AUDIO="aac"
|
|
hi/video.m3u8
|
|
#EXT-X-STREAM-INF:BANDWIDTH=1500,CODECS="avc1.64001E,avc1.64001F,avc1.640028,mp4a.40.2",AUDIO="aac"
|
|
mid/video.m3u8
|
|
#EXT-X-STREAM-INF:BANDWIDTH=700,CODECS="avc1.64001E,avc1.64001F,avc1.640028,mp4a.40.2",AUDIO="aac"
|
|
low/video.m3u8
|
|
"###,
|
|
contents.to_string()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn hlsmultivariantsink_multiple_video_variant_with_mpegts_audio_video_muxed() -> Result<(), ()> {
|
|
init();
|
|
|
|
let pipeline = gst::Pipeline::with_name("hlsmultivariantsink_pipeline");
|
|
|
|
let hlsmultivariantsink = gst::ElementFactory::make("hlsmultivariantsink")
|
|
.name("test_hlsmultivariantsink")
|
|
.property(
|
|
"multivariant-playlist-location",
|
|
"/tmp/hlssink/multivariant.m3u8",
|
|
)
|
|
.property("target-duration", 2u32)
|
|
.property("playlist-length", 2u32)
|
|
.property("max-files", 2u32)
|
|
.build()
|
|
.expect("Must be able to instantiate hlsmultivariantsink");
|
|
|
|
hlsmultivariantsink.set_property("muxer-type", HlsMultivariantSinkMuxerType::MpegTs);
|
|
let muxer_type: HlsMultivariantSinkMuxerType = hlsmultivariantsink.property("muxer-type");
|
|
assert_eq!(muxer_type, HlsMultivariantSinkMuxerType::MpegTs);
|
|
|
|
pipeline.add(&hlsmultivariantsink).unwrap();
|
|
|
|
let (hls_events_sender, hls_events_receiver) = mpsc::sync_channel(20);
|
|
let multivariant_playlist_content = Arc::new(Mutex::new(String::from("")));
|
|
let playlist_content = Arc::new(Mutex::new(String::from("")));
|
|
|
|
setup_signals(
|
|
&hlsmultivariantsink,
|
|
hls_events_sender.clone(),
|
|
multivariant_playlist_content.clone(),
|
|
playlist_content.clone(),
|
|
HlsMultivariantSinkMuxerType::MpegTs,
|
|
);
|
|
|
|
let video_bin1 = video_bin(1920, 1080, 30, 2500, false, false).unwrap();
|
|
let video_bin1_pad = video_bin1.static_pad("src").unwrap();
|
|
let video1_pad = hlsmultivariantsink.request_pad_simple("video_%u").unwrap();
|
|
let v = gst::Structure::builder("video1-variant")
|
|
.field("uri", "hi/video.m3u8")
|
|
.field("bandwidth", 2500)
|
|
.field("codecs", "avc1,mp4a.40.2")
|
|
.build();
|
|
video1_pad.set_property("variant", v);
|
|
pipeline.add(&video_bin1).unwrap();
|
|
video_bin1_pad.link(&video1_pad).unwrap();
|
|
|
|
let video_bin2 = video_bin(1280, 720, 30, 1500, false, false).unwrap();
|
|
let video_bin2_pad = video_bin2.static_pad("src").unwrap();
|
|
let video2_pad = hlsmultivariantsink.request_pad_simple("video_%u").unwrap();
|
|
let v = gst::Structure::builder("video2-variant")
|
|
.field("uri", "mid/video.m3u8")
|
|
.field("bandwidth", 1500)
|
|
.field("codecs", "avc1,mp4a.40.2")
|
|
.build();
|
|
video2_pad.set_property("variant", v);
|
|
pipeline.add(&video_bin2).unwrap();
|
|
video_bin2_pad.link(&video2_pad).unwrap();
|
|
|
|
/*
|
|
* Note that the next two audio variants have the same URI as
|
|
* the above two video variants. In the MPEG-TS case, this will
|
|
* result in them being muxed.
|
|
*/
|
|
let audio_bin1 = audio_bin(256000).unwrap();
|
|
let audio_bin1_pad = audio_bin1.static_pad("src").unwrap();
|
|
let audio1_pad = hlsmultivariantsink.request_pad_simple("audio_%u").unwrap();
|
|
let v = gst::Structure::builder("audio1-variant")
|
|
.field("uri", "hi/video.m3u8")
|
|
.field("bandwidth", 256000)
|
|
.field("codecs", "avc1,mp4a.40.2")
|
|
.build();
|
|
audio1_pad.set_property("variant", v);
|
|
pipeline.add(&audio_bin1).unwrap();
|
|
audio_bin1_pad.link(&audio1_pad).unwrap();
|
|
|
|
let audio_bin2 = audio_bin(128000).unwrap();
|
|
let audio_bin2_pad = audio_bin2.static_pad("src").unwrap();
|
|
let audio2_pad = hlsmultivariantsink.request_pad_simple("audio_%u").unwrap();
|
|
let v = gst::Structure::builder("audio2-variant")
|
|
.field("uri", "mid/video.m3u8")
|
|
.field("bandwidth", 128000)
|
|
.field("codecs", "avc1,mp4a.40.2")
|
|
.build();
|
|
audio2_pad.set_property("variant", v);
|
|
pipeline.add(&audio_bin2).unwrap();
|
|
audio_bin2_pad.link(&audio2_pad).unwrap();
|
|
|
|
/* Audio only variant, not muxed with video */
|
|
let audio_bin3 = audio_bin(64000).unwrap();
|
|
let audio_bin3_pad = audio_bin3.static_pad("src").unwrap();
|
|
let audio3_pad = hlsmultivariantsink.request_pad_simple("audio_%u").unwrap();
|
|
let v = gst::Structure::builder("audio3-variant")
|
|
.field("uri", "low-audio/audio-only.m3u8")
|
|
.field("bandwidth", 64000)
|
|
.field("codecs", "mp4a.40.2")
|
|
.build();
|
|
audio3_pad.set_property("variant", v);
|
|
pipeline.add(&audio_bin3).unwrap();
|
|
audio_bin3_pad.link(&audio3_pad).unwrap();
|
|
|
|
pipeline.set_state(gst::State::Playing).unwrap();
|
|
|
|
let mut eos = false;
|
|
let bus = pipeline.bus().unwrap();
|
|
while let Some(msg) = bus.timed_pop(gst::ClockTime::NONE) {
|
|
use gst::MessageView;
|
|
match msg.view() {
|
|
MessageView::Eos(..) => {
|
|
eos = true;
|
|
break;
|
|
}
|
|
MessageView::Error(e) => gst::error!(CAT, "hlsmultivariantsink error: {}", e),
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
pipeline.debug_to_dot_file_with_ts(
|
|
gst::DebugGraphDetails::all(),
|
|
"multiple_video_variant_with_mpegts_audio_video_muxed",
|
|
);
|
|
|
|
pipeline.set_state(gst::State::Null).unwrap();
|
|
assert!(eos);
|
|
|
|
let mut actual_events = Vec::new();
|
|
while let Ok(event) = hls_events_receiver.recv_timeout(Duration::from_millis(1)) {
|
|
actual_events.push(event);
|
|
}
|
|
let expected_events = {
|
|
use self::HlsSinkEvent::*;
|
|
vec![
|
|
GetMultivariantPlaylistStream("/tmp/hlssink/multivariant.m3u8".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi/segment00000.ts".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/mid/segment00000.ts".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/mid/segment00001.ts".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi/segment00001.ts".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/low-audio/segment00000.ts".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/hi/video.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/mid/video.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/low-audio/audio-only.m3u8".to_string()),
|
|
]
|
|
};
|
|
assert!(is_playlist_events_eq(&expected_events, &actual_events));
|
|
|
|
let contents = multivariant_playlist_content.lock().unwrap();
|
|
|
|
#[rustfmt::skip]
|
|
assert_eq!(
|
|
r###"#EXTM3U
|
|
#EXT-X-VERSION:4
|
|
#EXT-X-STREAM-INF:BANDWIDTH=2500,CODECS="avc1.640028,mp4a.40.2"
|
|
hi/video.m3u8
|
|
#EXT-X-STREAM-INF:BANDWIDTH=1500,CODECS="avc1.64001F,mp4a.40.2"
|
|
mid/video.m3u8
|
|
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.2"
|
|
low-audio/audio-only.m3u8
|
|
"###,
|
|
contents.to_string()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn hlsmultivariantsink_multiple_audio_rendition_multiple_video_variant_with_mpegts_h265(
|
|
) -> Result<(), ()> {
|
|
init();
|
|
|
|
// Skip the test if `x265enc` is not available which is the case on CI
|
|
if gst::ElementFactory::make("x265enc").build().is_err() {
|
|
gst::info!(CAT, "Skipping test for MPEG-TS with H265");
|
|
return Ok(());
|
|
} else {
|
|
gst::info!(CAT, "Testing for MPEG-TS with H265");
|
|
}
|
|
|
|
let pipeline = gst::Pipeline::with_name("hlsmultivariantsink_pipeline");
|
|
|
|
let hlsmultivariantsink = gst::ElementFactory::make("hlsmultivariantsink")
|
|
.name("test_hlsmultivariantsink")
|
|
.property(
|
|
"multivariant-playlist-location",
|
|
"/tmp/hlssink/multivariant.m3u8",
|
|
)
|
|
.property("target-duration", 2u32)
|
|
.property("playlist-length", 2u32)
|
|
.property("max-files", 2u32)
|
|
.build()
|
|
.expect("Must be able to instantiate hlsmultivariantsink");
|
|
|
|
hlsmultivariantsink.set_property("muxer-type", HlsMultivariantSinkMuxerType::MpegTs);
|
|
let muxer_type: HlsMultivariantSinkMuxerType = hlsmultivariantsink.property("muxer-type");
|
|
assert_eq!(muxer_type, HlsMultivariantSinkMuxerType::MpegTs);
|
|
|
|
pipeline.add(&hlsmultivariantsink).unwrap();
|
|
|
|
let (hls_events_sender, hls_events_receiver) = mpsc::sync_channel(100);
|
|
let multivariant_playlist_content = Arc::new(Mutex::new(String::from("")));
|
|
let playlist_content = Arc::new(Mutex::new(String::from("")));
|
|
|
|
setup_signals(
|
|
&hlsmultivariantsink,
|
|
hls_events_sender.clone(),
|
|
multivariant_playlist_content.clone(),
|
|
playlist_content.clone(),
|
|
HlsMultivariantSinkMuxerType::MpegTs,
|
|
);
|
|
|
|
let audio_bin1 = audio_bin(256000).unwrap();
|
|
let audio_bin1_pad = audio_bin1.static_pad("src").unwrap();
|
|
let audio1_pad = hlsmultivariantsink.request_pad_simple("audio_%u").unwrap();
|
|
let r = gst::Structure::builder("audio1-rendition")
|
|
.field("media", "AUDIO")
|
|
.field("uri", "hi-audio/audio.m3u8")
|
|
.field("group_id", "aac")
|
|
.field("language", "en")
|
|
.field("name", "English")
|
|
.field("default", true)
|
|
.field("autoselect", false)
|
|
.build();
|
|
audio1_pad.set_property("alternate-rendition", r);
|
|
pipeline.add(&audio_bin1).unwrap();
|
|
audio_bin1_pad.link(&audio1_pad).unwrap();
|
|
|
|
let audio_bin2 = audio_bin(128000).unwrap();
|
|
let audio_bin2_pad = audio_bin2.static_pad("src").unwrap();
|
|
let audio2_pad = hlsmultivariantsink.request_pad_simple("audio_%u").unwrap();
|
|
let r = gst::Structure::builder("audio2-rendition")
|
|
.field("media", "AUDIO")
|
|
.field("uri", "mid-audio/audio.m3u8")
|
|
.field("group_id", "aac")
|
|
.field("language", "fr")
|
|
.field("name", "French")
|
|
.field("default", false)
|
|
.field("autoselect", false)
|
|
.build();
|
|
audio2_pad.set_property("alternate-rendition", r);
|
|
pipeline.add(&audio_bin2).unwrap();
|
|
audio_bin2_pad.link(&audio2_pad).unwrap();
|
|
|
|
let video_bin1 = video_bin(1920, 1080, 30, 2500, false, true).unwrap();
|
|
let video_bin1_pad = video_bin1.static_pad("src").unwrap();
|
|
let video1_pad = hlsmultivariantsink.request_pad_simple("video_%u").unwrap();
|
|
let v = gst::Structure::builder("video1-variant")
|
|
.field("uri", "hi/video.m3u8")
|
|
.field("audio", "aac")
|
|
.field("bandwidth", 2500)
|
|
.build();
|
|
video1_pad.set_property("variant", v);
|
|
pipeline.add(&video_bin1).unwrap();
|
|
video_bin1_pad.link(&video1_pad).unwrap();
|
|
|
|
let video_bin2 = video_bin(1280, 720, 30, 1500, false, true).unwrap();
|
|
let video_bin2_pad = video_bin2.static_pad("src").unwrap();
|
|
let video2_pad = hlsmultivariantsink.request_pad_simple("video_%u").unwrap();
|
|
let v = gst::Structure::builder("video2-variant")
|
|
.field("uri", "mid/video.m3u8")
|
|
.field("audio", "aac")
|
|
.field("bandwidth", 1500)
|
|
.build();
|
|
video2_pad.set_property("variant", v);
|
|
pipeline.add(&video_bin2).unwrap();
|
|
video_bin2_pad.link(&video2_pad).unwrap();
|
|
|
|
let video_bin3 = video_bin(640, 360, 24, 700, false, true).unwrap();
|
|
let video_bin3_pad = video_bin3.static_pad("src").unwrap();
|
|
let video3_pad = hlsmultivariantsink.request_pad_simple("video_%u").unwrap();
|
|
let v = gst::Structure::builder("video3-variant")
|
|
.field("uri", "low/video.m3u8")
|
|
.field("audio", "aac")
|
|
.field("bandwidth", 700)
|
|
.build();
|
|
video3_pad.set_property("variant", v);
|
|
pipeline.add(&video_bin3).unwrap();
|
|
video_bin3_pad.link(&video3_pad).unwrap();
|
|
|
|
pipeline.set_state(gst::State::Playing).unwrap();
|
|
|
|
let mut eos = false;
|
|
let bus = pipeline.bus().unwrap();
|
|
while let Some(msg) = bus.timed_pop(gst::ClockTime::NONE) {
|
|
use gst::MessageView;
|
|
match msg.view() {
|
|
MessageView::Eos(..) => {
|
|
eos = true;
|
|
break;
|
|
}
|
|
MessageView::Error(e) => gst::error!(CAT, "hlsmultivariantsink error: {}", e),
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
pipeline.debug_to_dot_file_with_ts(
|
|
gst::DebugGraphDetails::all(),
|
|
"multiple_audio_rendition_multiple_video_variant_with_mpegts",
|
|
);
|
|
|
|
pipeline.set_state(gst::State::Null).unwrap();
|
|
assert!(eos);
|
|
|
|
let mut actual_events = Vec::new();
|
|
while let Ok(event) = hls_events_receiver.recv_timeout(Duration::from_millis(1)) {
|
|
actual_events.push(event);
|
|
}
|
|
let expected_events = {
|
|
use self::HlsSinkEvent::*;
|
|
vec![
|
|
GetMultivariantPlaylistStream("/tmp/hlssink/multivariant.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/hi/video.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/mid/video.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/low/video.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/hi-audio/audio.m3u8".to_string()),
|
|
GetPlaylistStream("/tmp/hlssink/mid-audio/audio.m3u8".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi-audio/segment00000.ts".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/mid-audio/segment00000.ts".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi/segment00000.ts".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/mid/segment00000.ts".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/low/segment00000.ts".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/hi/segment00001.ts".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/mid/segment00001.ts".to_string()),
|
|
GetFragmentStream("/tmp/hlssink/low/segment00001.ts".to_string()),
|
|
]
|
|
};
|
|
assert!(is_playlist_events_eq(&expected_events, &actual_events));
|
|
|
|
let contents = multivariant_playlist_content.lock().unwrap();
|
|
|
|
#[rustfmt::skip]
|
|
assert_eq!(
|
|
r###"#EXTM3U
|
|
#EXT-X-VERSION:4
|
|
#EXT-X-MEDIA:TYPE=AUDIO,URI="hi-audio/audio.m3u8",GROUP-ID="aac",LANGUAGE="en",NAME="English",DEFAULT=YES
|
|
#EXT-X-MEDIA:TYPE=AUDIO,URI="mid-audio/audio.m3u8",GROUP-ID="aac",LANGUAGE="fr",NAME="French"
|
|
#EXT-X-STREAM-INF:BANDWIDTH=2500,CODECS="hvc1.1.6.L120.90,hvc1.1.6.L63.90,hvc1.1.6.L93.90,mp4a.40.2",AUDIO="aac"
|
|
hi/video.m3u8
|
|
#EXT-X-STREAM-INF:BANDWIDTH=1500,CODECS="hvc1.1.6.L120.90,hvc1.1.6.L63.90,hvc1.1.6.L93.90,mp4a.40.2",AUDIO="aac"
|
|
mid/video.m3u8
|
|
#EXT-X-STREAM-INF:BANDWIDTH=700,CODECS="hvc1.1.6.L120.90,hvc1.1.6.L63.90,hvc1.1.6.L93.90,mp4a.40.2",AUDIO="aac"
|
|
low/video.m3u8
|
|
"###,
|
|
contents.to_string()
|
|
);
|
|
|
|
Ok(())
|
|
}
|