From f9a39b1138d77c48a54735c74a49ee5e28ab887d Mon Sep 17 00:00:00 2001 From: Guillaume Desmottes Date: Fri, 15 Oct 2021 16:57:55 +0200 Subject: [PATCH] add uriplaylistbin plugin uriplaylistbin plays a list of URIs sequentially, ensuring gapless transitions and proper streams synchronization. --- Cargo.toml | 2 + meson.build | 1 + utils/uriplaylistbin/Cargo.toml | 50 + utils/uriplaylistbin/build.rs | 3 + utils/uriplaylistbin/examples/playlist.rs | 137 ++ utils/uriplaylistbin/src/lib.rs | 28 + .../uriplaylistbin/src/uriplaylistbin/imp.rs | 1572 +++++++++++++++++ .../uriplaylistbin/src/uriplaylistbin/mod.rs | 30 + utils/uriplaylistbin/tests/sample.mkv | Bin 0 -> 58531 bytes utils/uriplaylistbin/tests/sample.ogg | Bin 0 -> 4618 bytes utils/uriplaylistbin/tests/uriplaylistbin.rs | 299 ++++ 11 files changed, 2122 insertions(+) create mode 100644 utils/uriplaylistbin/Cargo.toml create mode 100644 utils/uriplaylistbin/build.rs create mode 100644 utils/uriplaylistbin/examples/playlist.rs create mode 100644 utils/uriplaylistbin/src/lib.rs create mode 100644 utils/uriplaylistbin/src/uriplaylistbin/imp.rs create mode 100644 utils/uriplaylistbin/src/uriplaylistbin/mod.rs create mode 100644 utils/uriplaylistbin/tests/sample.mkv create mode 100644 utils/uriplaylistbin/tests/sample.ogg create mode 100644 utils/uriplaylistbin/tests/uriplaylistbin.rs diff --git a/Cargo.toml b/Cargo.toml index e2065554..796c6568 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "net/rusoto", "utils/fallbackswitch", "utils/togglerecord", + "utils/uriplaylistbin", "video/cdg", "video/closedcaption", "video/videofx", @@ -46,6 +47,7 @@ default-members = [ "net/rusoto", "utils/fallbackswitch", "utils/togglerecord", + "utils/uriplaylistbin", "video/cdg", "video/ffv1", "video/flavors", diff --git a/meson.build b/meson.build index 8dbbef34..772d714d 100644 --- a/meson.build +++ b/meson.build @@ -58,6 +58,7 @@ plugins = { # https://github.com/qnighy/libwebp-sys2-rs/issues/4 'gst-plugin-webp': 'libgstrswebp', 'gst-plugin-videofx': 'libgstvideofx', + 'gst-plugin-uriplaylistbin': 'libgsturiplaylistbin', } extra_env = {} diff --git a/utils/uriplaylistbin/Cargo.toml b/utils/uriplaylistbin/Cargo.toml new file mode 100644 index 00000000..1b102265 --- /dev/null +++ b/utils/uriplaylistbin/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "gst-plugin-uriplaylistbin" +version = "0.8.0" +authors = ["Guillaume Desmottes "] +repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs" +license = "MPL-2.0" +edition = "2018" +description = "Playlist Plugin" + +[dependencies] +gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_14"] } +once_cell = "1.0" +anyhow = "1" +crossbeam-channel = "0.5" + +[dev-dependencies] +gst-app = { package = "gstreamer-app", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_14"]} +structopt = "0.3" +url = "2.2" +more-asserts = "0.2" + +[lib] +name = "gsturiplaylistbin" +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[[example]] +name = "playlist" +path = "examples/playlist.rs" + +[build-dependencies] +gst-plugin-version-helper = { path="../../version-helper" } + +[features] +# GStreamer 1.14 is required for static linking +static = ["gst/v1_14"] +capi = [] + +[package.metadata.capi] +min_version = "0.8.0" + +[package.metadata.capi.header] +enabled = false + +[package.metadata.capi.library] +install_subdir = "gstreamer-1.0" +versioning = false + +[package.metadata.capi.pkg_config] +requires_private = "gstreamer-1.0, gobject-2.0, glib-2.0, gmodule-2.0" diff --git a/utils/uriplaylistbin/build.rs b/utils/uriplaylistbin/build.rs new file mode 100644 index 00000000..cda12e57 --- /dev/null +++ b/utils/uriplaylistbin/build.rs @@ -0,0 +1,3 @@ +fn main() { + gst_plugin_version_helper::info() +} diff --git a/utils/uriplaylistbin/examples/playlist.rs b/utils/uriplaylistbin/examples/playlist.rs new file mode 100644 index 00000000..ee81000b --- /dev/null +++ b/utils/uriplaylistbin/examples/playlist.rs @@ -0,0 +1,137 @@ +// Copyright (C) 2021 OneStream Live +// +// 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::{ + collections::HashMap, + path::Path, + sync::{Arc, Mutex}, +}; + +use gst::prelude::*; +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +#[structopt(name = "playlist", about = "An example of uriplaylistbin usage.")] +struct Opt { + #[structopt(default_value = "1", short = "i", long = "iterations")] + iterations: u32, + uris: Vec, +} + +fn create_pipeline(uris: Vec, iterations: u32) -> anyhow::Result { + let pipeline = gst::Pipeline::new(None); + let playlist = gst::ElementFactory::make("uriplaylistbin", None)?; + + pipeline.add(&playlist)?; + + playlist.set_property("uris", &uris); + playlist.set_property("iterations", &iterations); + + let sink_bins = Arc::new(Mutex::new(HashMap::new())); + let sink_bins_clone = sink_bins.clone(); + + let pipeline_weak = pipeline.downgrade(); + playlist.connect_pad_added(move |_playlist, src_pad| { + let pipeline = match pipeline_weak.upgrade() { + None => return, + Some(pipeline) => pipeline, + }; + let pad_name = src_pad.name(); + + let sink = if pad_name.starts_with("audio") { + gst::parse_bin_from_description("audioconvert ! audioresample ! autoaudiosink", true) + .unwrap() + } else if pad_name.starts_with("video") { + gst::parse_bin_from_description("videoconvert ! autovideosink", true).unwrap() + } else { + unimplemented!(); + }; + + pipeline.add(&sink).unwrap(); + sink.sync_state_with_parent().unwrap(); + + let sink_pad = sink.static_pad("sink").unwrap(); + src_pad.link(&sink_pad).unwrap(); + + sink_bins.lock().unwrap().insert(pad_name, sink); + }); + + let pipeline_weak = pipeline.downgrade(); + playlist.connect_pad_removed(move |_playlist, pad| { + let pipeline = match pipeline_weak.upgrade() { + None => return, + Some(pipeline) => pipeline, + }; + + // remove sink bin that was handling the pad + let sink_bins = sink_bins_clone.lock().unwrap(); + let sink = sink_bins.get(&pad.name()).unwrap(); + pipeline.remove(sink).unwrap(); + let _ = sink.set_state(gst::State::Null); + }); + + Ok(pipeline) +} + +fn main() -> anyhow::Result<()> { + gst::init().unwrap(); + gsturiplaylistbin::plugin_register_static().expect("Failed to register uriplaylistbin plugin"); + + let opt = Opt::from_args(); + if opt.uris.is_empty() { + anyhow::bail!("Need at least one URI to play"); + } + + let uris = opt + .uris + .into_iter() + .map(|uri| { + let p = Path::new(&uri); + match p.canonicalize() { + Ok(p) => format!("file://{}", p.to_str().unwrap().to_string()), + _ => uri, + } + }) + .collect(); + + { + let pipeline = create_pipeline(uris, opt.iterations)?; + + pipeline + .set_state(gst::State::Playing) + .expect("Unable to set the pipeline to the `Playing` state"); + + let bus = pipeline.bus().unwrap(); + for msg in bus.iter_timed(gst::ClockTime::NONE) { + use gst::MessageView; + match msg.view() { + MessageView::Error(err) => { + eprintln!( + "Error received from element {:?}: {}", + err.src().map(|s| s.path_string()), + err.error() + ); + eprintln!("Debugging information: {:?}", err.debug()); + break; + } + MessageView::Eos(..) => break, + _ => (), + } + } + + pipeline + .set_state(gst::State::Null) + .expect("Unable to set the pipeline to the `Null` state"); + } + + unsafe { + gst::deinit(); + } + + Ok(()) +} diff --git a/utils/uriplaylistbin/src/lib.rs b/utils/uriplaylistbin/src/lib.rs new file mode 100644 index 00000000..38c32e8e --- /dev/null +++ b/utils/uriplaylistbin/src/lib.rs @@ -0,0 +1,28 @@ +// Copyright (C) 2021 OneStream Live +// +// 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 gst::glib; + +mod uriplaylistbin; + +fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + uriplaylistbin::register(plugin)?; + Ok(()) +} + +gst::plugin_define!( + uriplaylistbin, + env!("CARGO_PKG_DESCRIPTION"), + plugin_init, + concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")), + "LGPL", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_REPOSITORY"), + env!("BUILD_REL_DATE") +); diff --git a/utils/uriplaylistbin/src/uriplaylistbin/imp.rs b/utils/uriplaylistbin/src/uriplaylistbin/imp.rs new file mode 100644 index 00000000..906cf524 --- /dev/null +++ b/utils/uriplaylistbin/src/uriplaylistbin/imp.rs @@ -0,0 +1,1572 @@ +// Copyright (C) 2021 OneStream Live +// +// 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::sync::Arc; +use std::sync::Mutex; + +use gst::glib; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst::{gst_debug, gst_error, gst_fixme, gst_info, gst_log, gst_warning}; + +use once_cell::sync::Lazy; + +static CAT: Lazy = Lazy::new(|| { + gst::DebugCategory::new( + "uriplaylistbin", + gst::DebugColorFlags::empty(), + Some("Uri Playlist Bin"), + ) +}); + +/// how many items are allowed to be prepared and waiting in the pipeline +const MAX_STREAMING_ITEMS: usize = 2; + +#[derive(Debug)] +enum PlaylistError { + PluginMissing { error: anyhow::Error }, + ItemFailed { error: anyhow::Error, item: Item }, +} + +impl std::fmt::Display for PlaylistError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PlaylistError::PluginMissing { error } => { + write!(f, "{}", error) + } + PlaylistError::ItemFailed { error, item } => { + write!(f, "{} (URI: {})", error, item.uri()) + } + } + } +} + +impl std::error::Error for PlaylistError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + PlaylistError::PluginMissing { error } | PlaylistError::ItemFailed { error, .. } => { + Some(error.as_ref()) + } + } + } +} + +/// Number of different streams currently handled by the element +#[derive(Debug, Clone, PartialEq)] +struct StreamsTopology { + audio: u32, + video: u32, + text: u32, +} + +impl Default for StreamsTopology { + fn default() -> Self { + Self { + audio: 0, + video: 0, + text: 0, + } + } +} + +impl StreamsTopology { + fn n_streams(&self) -> u32 { + self.audio + self.video + self.text + } +} + +impl<'a> From for StreamsTopology { + fn from(collection: gst::StreamCollection) -> Self { + let (mut audio, mut video, mut text) = (0, 0, 0); + for stream in collection.iter() { + match stream.stream_type() { + gst::StreamType::AUDIO => audio += 1, + gst::StreamType::VIDEO => video += 1, + gst::StreamType::TEXT => text += 1, + _ => {} + } + } + + Self { audio, video, text } + } +} + +#[derive(Debug, Clone)] +struct Settings { + uris: Vec, + iterations: u32, +} + +impl Default for Settings { + fn default() -> Self { + Self { + uris: vec![], + iterations: 1, + } + } +} + +struct State { + streamsynchronizer: gst::Element, + concat_audio: Vec, + concat_video: Vec, + concat_text: Vec, + + playlist: Playlist, + + /// the current number of streams handled by the element + streams_topology: StreamsTopology, + // true if the element stopped because of an error + errored: bool, + + // we have max one item in one of each of those states + waiting_for_stream_collection: Option, + waiting_for_ss_eos: Option, + waiting_for_pads: Option, + blocked: Option, + // multiple items can be streaming, `concat` elements will block them all but the active one + streaming: Vec, + // items which have been fully played, waiting to be cleaned up + done: Vec, +} + +impl State { + fn new(uris: Vec, iterations: u32, streamsynchronizer: gst::Element) -> Self { + Self { + concat_audio: vec![], + concat_video: vec![], + concat_text: vec![], + streamsynchronizer, + playlist: Playlist::new(uris, iterations), + streams_topology: StreamsTopology::default(), + errored: false, + waiting_for_stream_collection: None, + waiting_for_ss_eos: None, + waiting_for_pads: None, + blocked: None, + streaming: vec![], + done: vec![], + } + } + + /// Return the item whose decodebin is either `src` or an ancestor of `src` + fn find_item_from_src(&self, src: &gst::Object) -> Option { + // iterate in all the places we store `Item`, ordering does not matter + // as one decodebin element can be in only one Item. + let mut items = self + .waiting_for_stream_collection + .iter() + .chain(self.waiting_for_ss_eos.iter()) + .chain(self.waiting_for_pads.iter()) + .chain(self.blocked.iter()) + .chain(self.streaming.iter()) + .chain(self.done.iter()); + + items + .find(|item| { + let decodebin = item.uridecodebin(); + let from_decodebin = src == &decodebin; + let bin = decodebin.downcast_ref::().unwrap(); + from_decodebin || src.has_as_ancestor(bin) + }) + .cloned() + } + + fn unblock_item(&mut self, element: &super::UriPlaylistBin) { + if let Some(blocked) = self.blocked.take() { + let (messages, sender) = blocked.set_streaming(self.streams_topology.n_streams()); + + gst_log!( + CAT, + obj: element, + "send pending message of item #{} and unblock its pads", + blocked.index() + ); + + // send pending messages then unblock pads + for msg in messages { + let _ = element.post_message(msg); + } + let _ = sender.send(true); + + self.streaming.push(blocked); + } + } +} + +#[derive(Default)] +pub struct UriPlaylistBin { + settings: Mutex, + state: Mutex>, +} + +#[derive(Debug, Clone)] +enum ItemState { + /// Waiting to create a decodebin element + Pending, + /// Waiting to receive the stream collection from its decodebin element + WaitingForStreamCollection { uridecodebin: gst::Element }, + /// Waiting for streamsynchronizer to be eos on all its src pads. + /// Only used to block item whose streams topology is different from the one + /// currently handled by the element. In such case we need to wait for + /// streamsynchronizer to be flushed before adding/removing concat elements. + WaitingForStreamsynchronizerEos { + uridecodebin: gst::Element, + /// src pads from decodebin currently blocked + decodebin_pads: Vec, + /// number of streamsynchronizer src pads which are not eos yet + waiting_eos: u32, + stream_collection_msg: gst::Message, + // channel used to block pads flow until streamsynchronizer is eos + sender: crossbeam_channel::Sender, + receiver: crossbeam_channel::Receiver, + }, + /// Waiting that pads of all the streams have been created on decodebin. + /// Required to ensure that streams are plugged to concat in the playlist order. + WaitingForPads { + uridecodebin: gst::Element, + n_pads_pendings: u32, + stream_collection_msg: gst::Message, + /// concat sink pads which have been requested to handle this item + concat_sink_pads: Vec<(gst::Element, gst::Pad)>, + // channel used to block pad flow in the Blocked state + sender: crossbeam_channel::Sender, + receiver: crossbeam_channel::Receiver, + }, + /// Pads have been linked to `concat` elements but are blocked until the next item is linked to `concat` as well. + /// This is required to ensure gap-less transition between items. + Blocked { + uridecodebin: gst::Element, + stream_collection_msg: gst::Message, + stream_selected_msg: Option, + concat_sink_pads: Vec<(gst::Element, gst::Pad)>, + sender: crossbeam_channel::Sender, + receiver: crossbeam_channel::Receiver, + }, + /// Buffers are flowing + Streaming { + uridecodebin: gst::Element, + concat_sink_pads: Vec<(gst::Element, gst::Pad)>, + // number of pads which are not eos yet + waiting_eos: u32, + }, + /// Item has been fully streamed + Done { + uridecodebin: gst::Element, + concat_sink_pads: Vec<(gst::Element, gst::Pad)>, + }, +} + +#[derive(Debug, Clone)] +struct Item { + inner: Arc>, +} + +impl Item { + fn new(uri: String, index: usize) -> Self { + let inner = ItemInner { + uri, + index, + state: ItemState::Pending, + }; + + Self { + inner: Arc::new(Mutex::new(inner)), + } + } + + fn uri(&self) -> String { + let inner = self.inner.lock().unwrap(); + inner.uri.clone() + } + + fn index(&self) -> usize { + let inner = self.inner.lock().unwrap(); + inner.index + } + + fn uridecodebin(&self) -> gst::Element { + let inner = self.inner.lock().unwrap(); + + match &inner.state { + ItemState::WaitingForStreamCollection { uridecodebin } + | ItemState::WaitingForStreamsynchronizerEos { uridecodebin, .. } + | ItemState::WaitingForPads { uridecodebin, .. } + | ItemState::Blocked { uridecodebin, .. } + | ItemState::Streaming { uridecodebin, .. } + | ItemState::Done { uridecodebin, .. } => uridecodebin.clone(), + _ => unreachable!(), + } + } + + fn concat_sink_pads(&self) -> Vec<(gst::Element, gst::Pad)> { + let inner = self.inner.lock().unwrap(); + + match &inner.state { + ItemState::WaitingForPads { + concat_sink_pads, .. + } + | ItemState::Blocked { + concat_sink_pads, .. + } + | ItemState::Streaming { + concat_sink_pads, .. + } + | ItemState::Done { + concat_sink_pads, .. + } => concat_sink_pads.clone(), + _ => unreachable!(), + } + } + + fn dec_n_pads_pending(&self) -> u32 { + let mut inner = self.inner.lock().unwrap(); + + match &mut inner.state { + ItemState::WaitingForPads { + n_pads_pendings, .. + } => { + *n_pads_pendings -= 1; + *n_pads_pendings + } + _ => unreachable!(), + } + } + + fn receiver(&self) -> crossbeam_channel::Receiver { + let inner = self.inner.lock().unwrap(); + + match &inner.state { + ItemState::WaitingForPads { receiver, .. } => receiver.clone(), + ItemState::WaitingForStreamsynchronizerEos { receiver, .. } => receiver.clone(), + // receiver is no longer supposed to be accessed once in the `Blocked` state + _ => unreachable!(), + } + } + + fn add_blocked_pad(&self, pad: gst::Pad) { + let mut inner = self.inner.lock().unwrap(); + + match &mut inner.state { + ItemState::WaitingForStreamsynchronizerEos { decodebin_pads, .. } => { + decodebin_pads.push(pad); + } + _ => unreachable!(), + } + } + + /// decrement waiting_eos on a WaitingForStreamsynchronizeEos item, returns if all the streams are now eos or not + fn dec_waiting_eos_ss(&self) -> bool { + let mut inner = self.inner.lock().unwrap(); + + match &mut inner.state { + ItemState::WaitingForStreamsynchronizerEos { waiting_eos, .. } => { + *waiting_eos -= 1; + *waiting_eos == 0 + } + _ => unreachable!(), + } + } + + fn is_streaming(&self) -> bool { + let inner = self.inner.lock().unwrap(); + + matches!(&inner.state, ItemState::Streaming { .. }) + } + + /// queue the stream-selected message of a blocked item + fn add_stream_selected(&self, msg: gst::Message) { + let mut inner = self.inner.lock().unwrap(); + + match &mut inner.state { + ItemState::Blocked { + stream_selected_msg, + .. + } => { + *stream_selected_msg = Some(msg); + } + _ => unreachable!(), + } + } + + // decrement waiting_eos on a Streaming item, returns if all the streams are now eos or not + fn dec_waiting_eos(&self) -> bool { + let mut inner = self.inner.lock().unwrap(); + + match &mut inner.state { + ItemState::Streaming { waiting_eos, .. } => { + *waiting_eos -= 1; + *waiting_eos == 0 + } + _ => unreachable!(), + } + } + + fn add_concat_sink_pad(&self, concat: &gst::Element, sink_pad: &gst::Pad) { + let mut inner = self.inner.lock().unwrap(); + + match &mut inner.state { + ItemState::WaitingForPads { + concat_sink_pads, .. + } => { + concat_sink_pads.push((concat.clone(), sink_pad.clone())); + } + _ => unreachable!(), + } + } + + // change state methods + + // from the Pending state, called when starting to process the item + fn set_waiting_for_stream_collection(&self) -> Result<(), PlaylistError> { + let mut inner = self.inner.lock().unwrap(); + + let uridecodebin = gst::ElementFactory::make( + "uridecodebin3", + Some(&format!("playlist-decodebin-{}", inner.index)), + ) + .map_err(|e| PlaylistError::PluginMissing { error: e.into() })?; + uridecodebin.set_property("uri", &inner.uri); + + assert!(matches!(inner.state, ItemState::Pending)); + inner.state = ItemState::WaitingForStreamCollection { uridecodebin }; + + Ok(()) + } + + // from the WaitingForStreamCollection state, called when we received the item stream collection + // and its stream topology matches what is currently being processed by the element. + fn set_waiting_for_pads(&self, n_streams: u32, msg: gst::message::StreamCollection) { + let mut inner = self.inner.lock().unwrap(); + assert!(matches!( + inner.state, + ItemState::WaitingForStreamCollection { .. } + )); + + let (sender, receiver) = crossbeam_channel::unbounded::(); + + match &inner.state { + ItemState::WaitingForStreamCollection { uridecodebin } => { + inner.state = ItemState::WaitingForPads { + uridecodebin: uridecodebin.clone(), + n_pads_pendings: n_streams, + stream_collection_msg: msg.copy(), + concat_sink_pads: vec![], + sender, + receiver, + }; + } + _ => unreachable!(), + } + } + + // from the WaitingForStreamCollection state, called when we received the item stream collection + // but its stream topology does not match what is currently being processed by the element, + // having to wait until streamsynchronizer is flushed to internally reorganize the element. + fn set_waiting_for_ss_eos(&self, waiting_eos: u32, msg: gst::message::StreamCollection) { + let mut inner = self.inner.lock().unwrap(); + let (sender, receiver) = crossbeam_channel::unbounded::(); + + match &inner.state { + ItemState::WaitingForStreamCollection { uridecodebin } => { + inner.state = ItemState::WaitingForStreamsynchronizerEos { + uridecodebin: uridecodebin.clone(), + decodebin_pads: vec![], + waiting_eos, + // FIXME: save deep copy once https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/-/issues/363 is fixed + stream_collection_msg: msg.copy(), + sender, + receiver, + }; + } + _ => unreachable!(), + } + } + + // from the WaitingForStreamsynchronizerEos state, called when the streamsynchronizer has been flushed + // and the item can now be processed. + fn done_waiting_for_ss_eos( + &self, + ) -> ( + StreamsTopology, + Vec, + crossbeam_channel::Sender, + ) { + let mut inner = self.inner.lock().unwrap(); + + match &inner.state { + ItemState::WaitingForStreamsynchronizerEos { + uridecodebin, + decodebin_pads, + waiting_eos, + stream_collection_msg, + sender, + .. + } => { + assert_eq!(*waiting_eos, 0); + + let topology = match stream_collection_msg.view() { + gst::MessageView::StreamCollection(stream_collection_msg) => { + StreamsTopology::from(stream_collection_msg.stream_collection()) + } + _ => unreachable!(), + }; + let pending_pads = decodebin_pads.clone(); + let sender = sender.clone(); + + let (new_sender, new_receiver) = crossbeam_channel::unbounded::(); + + inner.state = ItemState::WaitingForPads { + uridecodebin: uridecodebin.clone(), + n_pads_pendings: topology.n_streams(), + stream_collection_msg: stream_collection_msg.copy(), + concat_sink_pads: vec![], + sender: new_sender, + receiver: new_receiver, + }; + + (topology, pending_pads, sender) + } + _ => unreachable!(), + } + } + + // from the WaitingForPads state, called when all the pads from decodebin have been added and connected to concat elements. + fn set_blocked(&self) { + let mut inner = self.inner.lock().unwrap(); + + match &mut inner.state { + ItemState::WaitingForPads { + uridecodebin, + sender, + receiver, + stream_collection_msg, + concat_sink_pads, + .. + } => { + inner.state = ItemState::Blocked { + uridecodebin: uridecodebin.clone(), + sender: sender.clone(), + receiver: receiver.clone(), + concat_sink_pads: concat_sink_pads.clone(), + stream_collection_msg: stream_collection_msg.copy(), + stream_selected_msg: None, + }; + } + _ => unreachable!(), + } + } + + // from the Blocked state, called when the item streaming threads can be unblocked. + // Return the queued messages from this item and the sender to unblock their pads + fn set_streaming( + &self, + n_streams: u32, + ) -> (Vec, crossbeam_channel::Sender) { + let mut inner = self.inner.lock().unwrap(); + + match &mut inner.state { + ItemState::Blocked { + uridecodebin, + sender, + stream_collection_msg, + stream_selected_msg, + concat_sink_pads, + .. + } => { + let mut messages = vec![stream_collection_msg.copy()]; + if let Some(msg) = stream_selected_msg { + messages.push(msg.copy()); + } + let sender = sender.clone(); + + inner.state = ItemState::Streaming { + uridecodebin: uridecodebin.clone(), + waiting_eos: n_streams, + concat_sink_pads: concat_sink_pads.clone(), + }; + + (messages, sender) + } + _ => unreachable!(), + } + } + + // from the Streaming state, called when the item has been fully processed and can be cleaned up + fn set_done(&self) { + let mut inner = self.inner.lock().unwrap(); + + match &mut inner.state { + ItemState::Streaming { + uridecodebin, + concat_sink_pads, + .. + } => { + inner.state = ItemState::Done { + uridecodebin: uridecodebin.clone(), + concat_sink_pads: concat_sink_pads.clone(), + }; + } + _ => unreachable!(), + } + } +} + +#[derive(Debug, Clone)] +struct ItemInner { + uri: String, + index: usize, + state: ItemState, +} + +struct Playlist { + items: Box + Send>, +} + +impl Playlist { + fn new(uris: Vec, iterations: u32) -> Self { + fn infinite_iter(uris: Vec) -> Box + Send + Sync> { + Box::new( + uris.into_iter() + .cycle() + .enumerate() + .map(|(index, uri)| Item::new(uri, index)), + ) + } + fn finite_iter( + uris: Vec, + iterations: u32, + ) -> Box + Send + Sync> { + let n = (iterations as usize) + .checked_mul(uris.len()) + .unwrap_or(usize::MAX); + + Box::new( + uris.into_iter() + .cycle() + .take(n) + .enumerate() + .map(|(index, uri)| Item::new(uri, index)), + ) + } + + let items = if iterations == 0 { + infinite_iter(uris) + } else { + finite_iter(uris, iterations) + }; + + Self { items } + } + + fn next(&mut self) -> Result, PlaylistError> { + let item = match self.items.next() { + None => return Ok(None), + Some(item) => item, + }; + + item.set_waiting_for_stream_collection()?; + + Ok(Some(item)) + } +} + +fn stream_type_from_pad_name(name: &str) -> anyhow::Result<(gst::StreamType, usize)> { + if let Some(index) = name.strip_prefix("audio_") { + Ok((gst::StreamType::AUDIO, index.parse().unwrap())) + } else if let Some(index) = name.strip_prefix("video_") { + Ok((gst::StreamType::VIDEO, index.parse().unwrap())) + } else if let Some(index) = name.strip_prefix("text_") { + Ok((gst::StreamType::TEXT, index.parse().unwrap())) + } else { + Err(anyhow::anyhow!("type of pad {} not supported", name)) + } +} + +#[glib::object_subclass] +impl ObjectSubclass for UriPlaylistBin { + const NAME: &'static str = "GstUriPlaylistBin"; + type Type = super::UriPlaylistBin; + type ParentType = gst::Bin; +} + +impl ObjectImpl for UriPlaylistBin { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![ + glib::ParamSpecBoxed::new( + "uris", + "URIs", + "URIs of the medias to play", + Vec::::static_type(), + glib::ParamFlags::READWRITE | gst::PARAM_FLAG_MUTABLE_READY, + ), + glib::ParamSpecUInt::new( + "iterations", + "Iterations", + "Number of time the playlist items should be played each (0 = unlimited)", + 0, + u32::MAX, + 1, + glib::ParamFlags::READWRITE | gst::PARAM_FLAG_MUTABLE_READY, + ), + ] + }); + + PROPERTIES.as_ref() + } + + fn set_property( + &self, + obj: &Self::Type, + _id: usize, + value: &glib::Value, + pspec: &glib::ParamSpec, + ) { + match pspec.name() { + "uris" => { + let mut settings = self.settings.lock().unwrap(); + let new_value = value.get().expect("type checked upstream"); + gst_info!( + CAT, + obj: obj, + "Changing uris from {:?} to {:?}", + settings.uris, + new_value, + ); + settings.uris = new_value; + } + "iterations" => { + let mut settings = self.settings.lock().unwrap(); + let new_value = value.get().expect("type checked upstream"); + gst_info!( + CAT, + obj: obj, + "Changing iterations from {:?} to {:?}", + settings.iterations, + new_value, + ); + settings.iterations = new_value; + } + _ => unimplemented!(), + } + } + + fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "uris" => { + let settings = self.settings.lock().unwrap(); + settings.uris.to_value() + } + "iterations" => { + let settings = self.settings.lock().unwrap(); + settings.iterations.to_value() + } + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + + obj.set_suppressed_flags(gst::ElementFlags::SOURCE | gst::ElementFlags::SINK); + obj.set_element_flags(gst::ElementFlags::SOURCE); + } +} + +impl GstObjectImpl for UriPlaylistBin {} + +impl BinImpl for UriPlaylistBin { + fn handle_message(&self, element: &Self::Type, msg: gst::Message) { + match msg.view() { + gst::MessageView::StreamCollection(stream_collection_msg) => { + if let Err(e) = self.handle_stream_collection(element, stream_collection_msg) { + self.failed(element, e); + } + // stream collection will be send when the item starts streaming + return; + } + gst::MessageView::StreamsSelected(stream_selected) => { + if !self.handle_stream_selected(element, stream_selected) { + return; + } + } + gst::MessageView::Error(error) => { + // find item which raised the error + let self_ = UriPlaylistBin::from_instance(element); + let mut state_guard = self_.state.lock().unwrap(); + let state = state_guard.as_mut().unwrap(); + + let src = error.src().unwrap(); + let item = state.find_item_from_src(&src); + + drop(state_guard); + + if let Some(item) = item { + // handle the error message so we can add the failing uri as error details + + self.failed( + element, + PlaylistError::ItemFailed { + error: anyhow::anyhow!( + "Error when processing item #{} ({}): {}", + item.index(), + item.uri(), + error.error().to_string() + ), + item, + }, + ); + return; + } + } + _ => (), + } + + self.parent_handle_message(element, msg) + } +} + +impl ElementImpl for UriPlaylistBin { + fn metadata() -> Option<&'static gst::subclass::ElementMetadata> { + static ELEMENT_METADATA: Lazy = Lazy::new(|| { + gst::subclass::ElementMetadata::new( + "Playlist Source", + "Generic/Source", + "Sequentially play uri streams", + "Guillaume Desmottes ", + ) + }); + + Some(&*ELEMENT_METADATA) + } + + fn pad_templates() -> &'static [gst::PadTemplate] { + static PAD_TEMPLATES: Lazy> = Lazy::new(|| { + let audio_src_pad_template = gst::PadTemplate::new( + "audio_%u", + gst::PadDirection::Src, + gst::PadPresence::Sometimes, + &gst::Caps::new_any(), + ) + .unwrap(); + + let video_src_pad_template = gst::PadTemplate::new( + "video_%u", + gst::PadDirection::Src, + gst::PadPresence::Sometimes, + &gst::Caps::new_any(), + ) + .unwrap(); + + let text_src_pad_template = gst::PadTemplate::new( + "text_%u", + gst::PadDirection::Src, + gst::PadPresence::Sometimes, + &gst::Caps::new_any(), + ) + .unwrap(); + + vec![ + audio_src_pad_template, + video_src_pad_template, + text_src_pad_template, + ] + }); + + PAD_TEMPLATES.as_ref() + } + + fn change_state( + &self, + element: &Self::Type, + transition: gst::StateChange, + ) -> Result { + if transition == gst::StateChange::NullToReady { + if let Err(e) = self.start(element) { + self.failed(element, e); + return Err(gst::StateChangeError); + } + } + + self.parent_change_state(element, transition) + } +} + +impl UriPlaylistBin { + fn start(&self, element: &super::UriPlaylistBin) -> Result<(), PlaylistError> { + gst_debug!(CAT, obj: element, "Starting"); + { + let mut state_guard = self.state.lock().unwrap(); + assert!(state_guard.is_none()); + + let streamsynchronizer = + gst::ElementFactory::make("streamsynchronizer", Some("playlist-streamsync")) + .map_err(|e| PlaylistError::PluginMissing { error: e.into() })?; + + element.add(&streamsynchronizer).unwrap(); + + let settings = self.settings.lock().unwrap(); + + *state_guard = Some(State::new( + settings.uris.clone(), + settings.iterations, + streamsynchronizer, + )); + } + + self.start_next_item(element)?; + + Ok(()) + } + + fn start_next_item(&self, element: &super::UriPlaylistBin) -> Result<(), PlaylistError> { + let mut state_guard = self.state.lock().unwrap(); + let state = state_guard.as_mut().unwrap(); + + // clean up done items, so uridecodebin elements and concat sink pads don't pile up in the pipeline + while let Some(done) = state.done.pop() { + let uridecodebin = done.uridecodebin(); + gst_log!(CAT, obj: element, "remove {} from bin", uridecodebin.name()); + + for (concat, sink_pad) in done.concat_sink_pads() { + // calling release_request_pad() while holding the pad stream lock would deadlock + concat.call_async(move |concat| { + concat.release_request_pad(&sink_pad); + }); + } + + // can't change state from the streaming thread + let uridecodebin_clone = uridecodebin.clone(); + element.call_async(move |_element| { + let _ = uridecodebin_clone.set_state(gst::State::Null); + }); + + element.remove(&uridecodebin).unwrap(); + } + + if state.waiting_for_stream_collection.is_some() + || state.waiting_for_pads.is_some() + || state.waiting_for_ss_eos.is_some() + { + // another item is being prepared + return Ok(()); + } + + let n_streaming = state.streaming.len(); + if n_streaming > MAX_STREAMING_ITEMS { + gst_log!( + CAT, + obj: element, + "Too many items streaming ({}), wait before starting the next one", + n_streaming + ); + + return Ok(()); + } + + let item = match state.playlist.next()? { + Some(item) => item, + None => { + gst_debug!(CAT, obj: element, "no more item to queue",); + + // unblock last item + state.unblock_item(element); + + return Ok(()); + } + }; + + gst_debug!( + CAT, + obj: element, + "start decoding item #{}: {}", + item.index(), + item.uri() + ); + + let uridecodebin = item.uridecodebin(); + + element.add(&uridecodebin).unwrap(); + + let element_weak = element.downgrade(); + let uridecodebin_clone = uridecodebin.clone(); + + let item_clone = item.clone(); + assert!(state.waiting_for_stream_collection.is_none()); + state.waiting_for_stream_collection = Some(item); + + uridecodebin.connect_pad_added(move |_uridecodebin, src_pad| { + let element = match element_weak.upgrade() { + Some(element) => element, + None => return, + }; + let self_ = UriPlaylistBin::from_instance(&element); + + let item = { + let mut state_guard = self_.state.lock().unwrap(); + let state = state_guard.as_mut().unwrap(); + state.waiting_for_ss_eos.as_ref().cloned() + }; + + if let Some(item) = item { + // block pad until streamsynchronizer is eos + let element_weak = element.downgrade(); + let receiver = item.receiver(); + + gst_debug!( + CAT, + obj: &element, + "Block pad {} until streamsynchronizer is flushed", + src_pad.name(), + ); + + src_pad.add_probe(gst::PadProbeType::BLOCK_DOWNSTREAM, move |pad, _info| { + let element = match element_weak.upgrade() { + Some(element) => element, + None => return gst::PadProbeReturn::Remove, + }; + let parent = pad.parent().unwrap(); + + let _ = receiver.recv(); + + gst_log!( + CAT, + obj: &element, + "pad {}:{} has been unblocked", + parent.name(), + pad.name() + ); + + gst::PadProbeReturn::Remove + }); + + item.add_blocked_pad(src_pad.clone()); + } else { + self_.process_decodebin_pad(src_pad); + } + }); + + drop(state_guard); + + uridecodebin_clone + .sync_state_with_parent() + .map_err(|e| PlaylistError::ItemFailed { + error: e.into(), + item: item_clone, + })?; + + Ok(()) + } + + fn handle_stream_collection( + &self, + element: &super::UriPlaylistBin, + stream_collection_msg: gst::message::StreamCollection, + ) -> Result<(), PlaylistError> { + let mut state_guard = self.state.lock().unwrap(); + let state = state_guard.as_mut().unwrap(); + let src = stream_collection_msg.src().unwrap(); + + if let Some(item) = state.waiting_for_stream_collection.clone() { + // check message is from the decodebin we are waiting for + let uridecodebin = item.uridecodebin(); + + if src.has_as_ancestor(&uridecodebin) { + let topology = StreamsTopology::from(stream_collection_msg.stream_collection()); + + gst_debug!( + CAT, + obj: element, + "got stream collection from {}: {:?}", + src.name(), + topology + ); + + if state.streams_topology.n_streams() == 0 { + state.streams_topology = topology.clone(); + } + + assert!(state.waiting_for_pads.is_none()); + + if state.streams_topology != topology { + gst_debug!( + CAT, + obj: element, "streams topoly changed ('{:?}' -> '{:?}'), waiting for streamsynchronize to be flushed", + state.streams_topology, topology); + item.set_waiting_for_ss_eos( + state.streams_topology.n_streams(), + stream_collection_msg, + ); + state.waiting_for_ss_eos = Some(item); + + // unblock previous item as we need it to be flushed out of streamsynchronizer + state.unblock_item(element); + } else { + item.set_waiting_for_pads(topology.n_streams(), stream_collection_msg); + state.waiting_for_pads = Some(item); + } + + state.waiting_for_stream_collection = None; + } + } + Ok(()) + } + + // return true if the message can be forwarded + fn handle_stream_selected( + &self, + element: &super::UriPlaylistBin, + stream_selected_msg: gst::message::StreamsSelected, + ) -> bool { + let mut state_guard = self.state.lock().unwrap(); + let state = state_guard.as_mut().unwrap(); + let src = stream_selected_msg.src().unwrap(); + + if let Some(item) = state.blocked.clone() { + let uridecodebin = item.uridecodebin(); + + if src.has_as_ancestor(&uridecodebin) { + // stream-selected message is from the blocked item, queue the message until it's unblocked + gst_debug!( + CAT, + obj: element, + "queue stream-selected message from {} as item is currently blocked", + src.name(), + ); + + item.add_stream_selected(stream_selected_msg.copy()); + false + } else { + true + } + } else { + true + } + } + + fn process_decodebin_pad(&self, src_pad: &gst::Pad) { + let element = self.instance(); + + let start_next = { + let mut state_guard = self.state.lock().unwrap(); + let state = state_guard.as_mut().unwrap(); + + if state.errored { + return; + } + + let item = state.waiting_for_pads.clone().unwrap(); + + // Parse the pad name to extract the stream type and its index. + // We could get the type from the Stream object from the StreamStart sticky event but we'd still have + // to parse the name for the index. + let pad_name = src_pad.name(); + let (stream_type, stream_index) = match stream_type_from_pad_name(&pad_name) { + Ok((stream_type, stream_index)) => (stream_type, stream_index), + Err(e) => { + gst_warning!(CAT, obj: &element, "Ignoring pad {}: {}", pad_name, e); + return; + } + }; + + let concat = match stream_type { + gst::StreamType::AUDIO => state.concat_audio.get(stream_index), + gst::StreamType::VIDEO => state.concat_video.get(stream_index), + gst::StreamType::TEXT => state.concat_text.get(stream_index), + _ => unreachable!(), // early return on unsupported streams above + }; + + let concat = match concat { + None => { + gst_debug!( + CAT, + obj: &element, + "stream {} from item #{}: creating concat element", + pad_name, + item.index() + ); + + let concat = match gst::ElementFactory::make( + "concat", + Some(&format!( + "playlist-concat-{}-{}", + stream_type.name(), + stream_index + )), + ) { + Ok(concat) => concat, + Err(_) => { + drop(state_guard); + self.failed( + &element, + PlaylistError::PluginMissing { + error: anyhow::anyhow!("element 'concat' missing"), + }, + ); + return; + } + }; + + // this is done by the streamsynchronizer element downstream + concat.set_property("adjust-base", false); + + element.add(&concat).unwrap(); + + concat.sync_state_with_parent().unwrap(); + + // link concat elements to streamsynchronizer + let concat_src = concat.static_pad("src").unwrap(); + let sync_sink = state + .streamsynchronizer + .request_pad_simple("sink_%u") + .unwrap(); + concat_src.link(&sync_sink).unwrap(); + + let element_weak = element.downgrade(); + + // add event probe on streamsynchronizer src pad. Will only be used when we are waiting for the + // streamsynchronizer to be flushed in order to handle streams topology changes. + let src_pad_name = sync_sink.name().to_string().replace("sink", "src"); + let sync_src = state.streamsynchronizer.static_pad(&src_pad_name).unwrap(); + sync_src.add_probe(gst::PadProbeType::EVENT_DOWNSTREAM, move |_pad, info| { + match info.data { + Some(gst::PadProbeData::Event(ref ev)) + if ev.type_() == gst::EventType::Eos => + { + let element = match element_weak.upgrade() { + Some(element) => element, + None => return gst::PadProbeReturn::Remove, + }; + let self_ = UriPlaylistBin::from_instance(&element); + + let item = { + let mut state_guard = self_.state.lock().unwrap(); + let state = state_guard.as_mut().unwrap(); + state.waiting_for_ss_eos.as_ref().cloned() + }; + + if let Some(item) = item { + if item.dec_waiting_eos_ss() { + gst_debug!(CAT, obj: &element, "streamsynchronizer has been flushed, reorganize pipeline to fit new streams topology and unblock item"); + self_.handle_topology_change(&element); + gst::PadProbeReturn::Drop + } else { + gst::PadProbeReturn::Drop + } + } else { + gst::PadProbeReturn::Pass + } + } + _ => gst::PadProbeReturn::Pass, + } + }); + + // ghost streamsynchronizer src pad + let sync_src_name = sync_sink.name().as_str().replace("sink", "src"); + let src = state.streamsynchronizer.static_pad(&sync_src_name).unwrap(); + let ghost = gst::GhostPad::with_target(Some(pad_name.as_str()), &src).unwrap(); + ghost.set_active(true).unwrap(); + + // proxy sticky events + src.sticky_events_foreach(|event| { + let _ = ghost.store_sticky_event(&event); + Ok(Some(event)) + }); + + unsafe { + ghost.set_event_function(|pad, parent, event| match event.view() { + gst::EventView::SelectStreams(_) => { + // TODO: handle select-streams event + let element = parent.unwrap(); + gst_fixme!( + CAT, + obj: element, + "select-streams event not supported ('{:?}')", + event + ); + false + } + _ => pad.event_default(parent, event), + }); + } + + element.add_pad(&ghost).unwrap(); + + match stream_type { + gst::StreamType::AUDIO => { + state.concat_audio.push(concat.clone()); + } + gst::StreamType::VIDEO => { + state.concat_video.push(concat.clone()); + } + gst::StreamType::TEXT => { + state.concat_text.push(concat.clone()); + } + _ => unreachable!(), // early return on unsupported streams above + } + + concat + } + Some(concat) => { + gst_debug!( + CAT, + obj: &element, + "stream {} from item #{}: re-using concat element {}", + pad_name, + item.index(), + concat.name() + ); + + concat.clone() + } + }; + + let sink_pad = concat.request_pad_simple("sink_%u").unwrap(); + src_pad.link(&sink_pad).unwrap(); + + item.add_concat_sink_pad(&concat, &sink_pad); + + // block pad until next item is reaching the `Blocked` state + let receiver = item.receiver(); + let element_weak = element.downgrade(); + let item_clone = item.clone(); + + sink_pad.add_probe(gst::PadProbeType::BLOCK_DOWNSTREAM, move |pad, info| { + let element = match element_weak.upgrade() { + Some(element) => element, + None => return gst::PadProbeReturn::Remove, + }; + let parent = pad.parent().unwrap(); + let item = &item_clone; + + if !item.is_streaming() { + // block pad until next item is ready + gst_log!( + CAT, + obj: &element, + "blocking pad {}:{} until next item is ready", + parent.name(), + pad.name() + ); + + let _ = receiver.recv(); + + gst_log!( + CAT, + obj: &element, + "pad {}:{} has been unblocked", + parent.name(), + pad.name() + ); + + gst::PadProbeReturn::Pass + } else { + match info.data { + Some(gst::PadProbeData::Event(ref ev)) + if ev.type_() == gst::EventType::Eos => + { + if item.dec_waiting_eos() { + // all the streams are eos, item is now done + gst_log!( + CAT, + obj: &element, + "all streams of item #{} are eos", + item.index() + ); + + let self_ = UriPlaylistBin::from_instance(&element); + { + let mut state_guard = self_.state.lock().unwrap(); + let state = state_guard.as_mut().unwrap(); + + let index = item.index(); + + let removed = state + .streaming + .iter() + .position(|i| i.index() == index) + .map(|e| state.streaming.remove(e)); + + if let Some(item) = removed { + item.set_done(); + state.done.push(item); + } + } + + if let Err(e) = self_.start_next_item(&element) { + self_.failed(&element, e); + } + } + + gst::PadProbeReturn::Remove + } + _ => gst::PadProbeReturn::Pass, + } + } + }); + + if item.dec_n_pads_pending() == 0 { + // we got all the pads + gst_debug!( + CAT, + obj: &element, + "got all the pads for item #{}", + item.index() + ); + + // all pads have been linked to concat, unblock previous item + state.unblock_item(&element); + + state.waiting_for_pads = None; + // block item until the next one is fully linked to concat + item.set_blocked(); + state.blocked = Some(item); + + true + } else { + false + } + }; + + if start_next { + gst_debug!( + CAT, + obj: &element, + "got all pending streams, queue next item" + ); + + if let Err(e) = self.start_next_item(&element) { + self.failed(&element, e); + } + } + } + + /// called when all previous items have been flushed from streamsynchronizer + /// and so the elements can reorganize itself to handle a pending changes in + /// streams topology. + fn handle_topology_change(&self, element: &super::UriPlaylistBin) { + let (pending_pads, sender) = { + let mut state_guard = self.state.lock().unwrap(); + let state = state_guard.as_mut().unwrap(); + + let item = state.waiting_for_ss_eos.take().unwrap(); + let (topology, pending_pads, sender) = item.done_waiting_for_ss_eos(); + state.waiting_for_pads = Some(item); + + // remove now useless concat elements, missing ones will be added when handling src pads from decodebin + + fn remove_useless_concat( + n_stream: usize, + concats: &mut Vec, + element: &super::UriPlaylistBin, + streamsynchronizer: &gst::Element, + ) { + while n_stream < concats.len() { + // need to remove concat elements + let concat = concats.pop().unwrap(); + gst_log!(CAT, obj: element, "remove {}", concat.name()); + + let concat_src = concat.static_pad("src").unwrap(); + let ss_sink = concat_src.peer().unwrap(); + + // unlink and remove sink pad from streamsynchronizer + concat_src.unlink(&ss_sink).unwrap(); + streamsynchronizer.release_request_pad(&ss_sink); + + // remove associated ghost pad + let src_pads = element.src_pads(); + let ghost = src_pads + .iter() + .find(|pad| { + let ghost = pad.downcast_ref::().unwrap(); + ghost.target().is_none() + }) + .unwrap(); + element.remove_pad(ghost).unwrap(); + + element.remove(&concat).unwrap(); + let _ = concat.set_state(gst::State::Null); + } + } + + remove_useless_concat( + topology.audio as usize, + &mut state.concat_audio, + element, + &state.streamsynchronizer, + ); + remove_useless_concat( + topology.video as usize, + &mut state.concat_video, + element, + &state.streamsynchronizer, + ); + remove_useless_concat( + topology.text as usize, + &mut state.concat_text, + element, + &state.streamsynchronizer, + ); + + state.streams_topology = topology; + + (pending_pads, sender) + }; + + // process decodebin src pads we already received and unblock them + for pad in pending_pads.iter() { + self.process_decodebin_pad(pad); + } + + let _ = sender.send(true); + } + + fn failed(&self, element: &super::UriPlaylistBin, error: PlaylistError) { + { + let mut state_guard = self.state.lock().unwrap(); + let state = state_guard.as_mut().unwrap(); + + if state.errored { + return; + } + state.errored = true; + + if let Some(blocked) = state.blocked.take() { + // unblock streaming thread + blocked.set_streaming(state.streams_topology.n_streams()); + } + } + let error_msg = error.to_string(); + gst_error!(CAT, obj: element, "{}", error_msg); + + match error { + PlaylistError::PluginMissing { .. } => { + gst::element_error!(element, gst::CoreError::MissingPlugin, [&error_msg]); + } + PlaylistError::ItemFailed { item, .. } => { + // remove failing uridecodebin + let uridecodebin = item.uridecodebin(); + uridecodebin.call_async(move |uridecodebin| { + let _ = uridecodebin.set_state(gst::State::Null); + }); + let _ = element.remove(&uridecodebin); + + let details = gst::Structure::builder("details"); + let details = details.field("uri", item.uri()); + + gst::element_error!( + element, + gst::LibraryError::Failed, + [&error_msg], + details: details.build() + ); + } + } + } +} diff --git a/utils/uriplaylistbin/src/uriplaylistbin/mod.rs b/utils/uriplaylistbin/src/uriplaylistbin/mod.rs new file mode 100644 index 00000000..a3fa7c16 --- /dev/null +++ b/utils/uriplaylistbin/src/uriplaylistbin/mod.rs @@ -0,0 +1,30 @@ +// Copyright (C) 2021 OneStream Live +// +// 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 gst::glib; +use gst::prelude::*; + +mod imp; + +glib::wrapper! { + pub struct UriPlaylistBin(ObjectSubclass) @extends gst::Bin, gst::Element, gst::Object; +} + +// GStreamer elements need to be thread-safe. For the private implementation this is automatically +// enforced but for the public wrapper type we need to specify this manually. +unsafe impl Send for UriPlaylistBin {} +unsafe impl Sync for UriPlaylistBin {} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "uriplaylistbin", + gst::Rank::None, + UriPlaylistBin::static_type(), + ) +} diff --git a/utils/uriplaylistbin/tests/sample.mkv b/utils/uriplaylistbin/tests/sample.mkv new file mode 100644 index 0000000000000000000000000000000000000000..331e063ba2720e80fd58943d4849b850a6c75ba3 GIT binary patch literal 58531 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

UHKtW3U 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 LdiQ*coZo)|t1UPi literal 0 HcmV?d00001 diff --git a/utils/uriplaylistbin/tests/sample.ogg b/utils/uriplaylistbin/tests/sample.ogg new file mode 100644 index 0000000000000000000000000000000000000000..fd79fe108bd8b124784a248c7598833b5d869e5d GIT binary patch literal 4618 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 literal 0 HcmV?d00001 diff --git a/utils/uriplaylistbin/tests/uriplaylistbin.rs b/utils/uriplaylistbin/tests/uriplaylistbin.rs new file mode 100644 index 00000000..e8e4842c --- /dev/null +++ b/utils/uriplaylistbin/tests/uriplaylistbin.rs @@ -0,0 +1,299 @@ +// Copyright (C) 2021 OneStream Live +// +// 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::PathBuf; + +use gst::prelude::*; +use gst::MessageView; +use more_asserts::assert_ge; + +struct TestMedia { + uri: String, + len: gst::ClockTime, +} + +fn file_name_to_uri(name: &str) -> String { + let input_path = { + let mut r = PathBuf::new(); + r.push(env!("CARGO_MANIFEST_DIR")); + r.push("tests"); + r.push(name); + r + }; + + let url = url::Url::from_file_path(&input_path).unwrap(); + url.to_string() +} + +impl TestMedia { + fn ogg() -> Self { + Self { + uri: file_name_to_uri("sample.ogg"), + len: gst::ClockTime::from_mseconds(510), + } + } + + fn mkv() -> Self { + Self { + uri: file_name_to_uri("sample.mkv"), + len: gst::ClockTime::from_mseconds(510), + } + } + + fn missing_file() -> Self { + Self { + uri: "file:///not-there.ogg".to_string(), + len: gst::ClockTime::from_mseconds(10), + } + } + + fn missing_http() -> Self { + Self { + uri: "http:///not-there.ogg".to_string(), + len: gst::ClockTime::from_mseconds(10), + } + } +} + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gsturiplaylistbin::plugin_register_static() + .expect("Failed to register uriplaylistbin plugin"); + }); +} + +fn test( + medias: Vec, + n_streams: u32, + iterations: u32, + check_streams: bool, +) -> Vec { + init(); + + let playlist_len = medias.len() * (iterations as usize); + + let pipeline = gst::Pipeline::new(None); + let playlist = gst::ElementFactory::make("uriplaylistbin", None).unwrap(); + let mq = gst::ElementFactory::make("multiqueue", None).unwrap(); + + pipeline.add_many(&[&playlist, &mq]).unwrap(); + + let total_len = gst::ClockTime::from_nseconds( + medias + .iter() + .map(|t| t.len.nseconds() * (iterations as u64)) + .sum(), + ); + + let uris: Vec = medias.iter().map(|t| t.uri.clone()).collect(); + + playlist.set_property("uris", &uris); + playlist.set_property("iterations", &iterations); + + let mq_clone = mq.clone(); + playlist.connect_pad_added(move |_playlist, src_pad| { + let mq_sink = mq_clone.request_pad_simple("sink_%u").unwrap(); + src_pad.link(&mq_sink).unwrap(); + }); + + let pipeline_weak = pipeline.downgrade(); + mq.connect_pad_added(move |_mq, pad| { + if pad.direction() != gst::PadDirection::Src { + return; + } + + let pipeline = match pipeline_weak.upgrade() { + Some(pipeline) => pipeline, + None => return, + }; + + let sink = gst::ElementFactory::make("fakesink", None).unwrap(); + pipeline.add(&sink).unwrap(); + sink.sync_state_with_parent().unwrap(); + + pad.link(&sink.static_pad("sink").unwrap()).unwrap(); + }); + + pipeline.set_state(gst::State::Playing).unwrap(); + + let bus = pipeline.bus().unwrap(); + let mut events = vec![]; + + loop { + let msg = bus.iter_timed(gst::ClockTime::NONE).next().unwrap(); + + match msg.view() { + MessageView::Error(_) | MessageView::Eos(..) => { + events.push(msg.clone()); + break; + } + // check stream related messages + MessageView::StreamCollection(_) | MessageView::StreamsSelected(_) => { + events.push(msg.clone()) + } + _ => {} + } + } + + // check we actually played all files and all streams + fn stream_end_ts(sink: &gst::Element) -> gst::ClockTime { + let sample: gst::Sample = sink.property("last-sample"); + let buffer = sample.buffer().unwrap(); + let pts = buffer.pts().unwrap(); + let segment = sample.segment().unwrap(); + let segment = segment.downcast_ref::().unwrap(); + let rt = segment.to_running_time(pts).unwrap(); + + rt + buffer.duration().unwrap() + } + + if check_streams { + // check all streams have been fully played + let mut n = 0; + for sink in pipeline.iterate_sinks() { + let sink = sink.unwrap(); + assert_ge!( + stream_end_ts(&sink), + total_len, + "{}: {} < {}", + sink.name(), + stream_end_ts(&sink), + total_len + ); + n += 1; + } + assert_eq!(n, n_streams); + + // check stream-collection and streams-selected message ordering + let mut events = events.clone().into_iter(); + + for _ in 0..playlist_len { + let decodebin = assert_stream_collection(events.next().unwrap(), n_streams as usize); + assert_eq!( + assert_stream_selected(events.next().unwrap(), n_streams as usize), + decodebin + ); + } + } + + pipeline.set_state(gst::State::Null).unwrap(); + + events +} + +fn assert_eos(msg: gst::Message) { + assert!(matches!(msg.view(), MessageView::Eos(_))); +} + +fn assert_error(msg: gst::Message, failing: TestMedia) { + match msg.view() { + MessageView::Error(err) => { + let details = err.details().unwrap(); + assert_eq!(details.get::<&str>("uri").unwrap(), failing.uri); + } + _ => { + panic!("last message is not an error"); + } + } +} + +fn assert_stream_collection(msg: gst::Message, n_streams: usize) -> gst::Object { + match msg.view() { + MessageView::StreamCollection(sc) => { + let collection = sc.stream_collection(); + assert_eq!(collection.len(), n_streams); + sc.src().unwrap() + } + _ => { + panic!("message is not a stream collection"); + } + } +} + +fn assert_stream_selected(msg: gst::Message, n_streams: usize) -> gst::Object { + match msg.view() { + MessageView::StreamsSelected(ss) => { + let collection = ss.stream_collection(); + assert_eq!(collection.len(), n_streams); + ss.src().unwrap() + } + _ => { + panic!("message is not stream selected"); + } + } +} + +#[test] +fn single_audio() { + let events = test(vec![TestMedia::ogg()], 1, 1, true).into_iter(); + assert_eos(events.last().unwrap()); +} + +#[test] +fn single_video() { + let events = test(vec![TestMedia::mkv()], 2, 1, true).into_iter(); + assert_eos(events.last().unwrap()); +} + +#[test] +fn multi_audio() { + let events = test( + vec![TestMedia::ogg(), TestMedia::ogg(), TestMedia::ogg()], + 1, + 1, + true, + ) + .into_iter(); + assert_eos(events.last().unwrap()); +} + +#[test] +fn multi_audio_video() { + let events = test(vec![TestMedia::mkv(), TestMedia::mkv()], 2, 1, true).into_iter(); + assert_eos(events.last().unwrap()); +} + +#[test] +fn iterations() { + let events = test(vec![TestMedia::mkv(), TestMedia::mkv()], 2, 2, true).into_iter(); + assert_eos(events.last().unwrap()); +} + +#[test] +fn nb_streams_increasing() { + let events = test(vec![TestMedia::ogg(), TestMedia::mkv()], 2, 1, false).into_iter(); + assert_eos(events.last().unwrap()); +} + +#[test] +fn missing_file() { + let events = test( + vec![TestMedia::ogg(), TestMedia::missing_file()], + 1, + 1, + false, + ) + .into_iter(); + assert_error(events.last().unwrap(), TestMedia::missing_file()); +} + +#[test] +fn missing_http() { + let events = test( + vec![TestMedia::ogg(), TestMedia::missing_http()], + 1, + 1, + false, + ) + .into_iter(); + assert_error(events.last().unwrap(), TestMedia::missing_http()); +}