// Copyright (C) 2021 Rafael Caricio // Copyright (C) 2023 Seungha Yang // // 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 crate::hlsbasesink::HlsBaseSinkImpl; use crate::hlssink3::HlsSink3PlaylistType; use crate::playlist::Playlist; use crate::HlsBaseSink; use gio::prelude::*; use gst::glib; use gst::prelude::*; use gst::subclass::prelude::*; use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment}; use once_cell::sync::Lazy; use std::sync::Mutex; const DEFAULT_TS_LOCATION: &str = "segment%05d.ts"; const DEFAULT_TARGET_DURATION: u32 = 15; const DEFAULT_PLAYLIST_TYPE: HlsSink3PlaylistType = HlsSink3PlaylistType::Unspecified; const DEFAULT_I_FRAMES_ONLY_PLAYLIST: bool = false; const DEFAULT_SEND_KEYFRAME_REQUESTS: bool = true; static CAT: Lazy = Lazy::new(|| { gst::DebugCategory::new("hlssink3", gst::DebugColorFlags::empty(), Some("HLS sink")) }); macro_rules! base_imp { ($i:expr) => { $i.obj().upcast_ref::().imp() }; } impl From for Option { fn from(pl_type: HlsSink3PlaylistType) -> Self { use HlsSink3PlaylistType::*; match pl_type { Unspecified => None, Event => Some(MediaPlaylistType::Event), Vod => Some(MediaPlaylistType::Vod), } } } impl From> for HlsSink3PlaylistType { fn from(inner_pl_type: Option<&MediaPlaylistType>) -> Self { use HlsSink3PlaylistType::*; match inner_pl_type { None | Some(MediaPlaylistType::Other(_)) => Unspecified, Some(MediaPlaylistType::Event) => Event, Some(MediaPlaylistType::Vod) => Vod, } } } struct HlsSink3Settings { location: String, target_duration: u32, playlist_type: Option, i_frames_only: bool, send_keyframe_requests: bool, splitmuxsink: gst::Element, giostreamsink: gst::Element, video_sink: bool, audio_sink: bool, } impl Default for HlsSink3Settings { fn default() -> Self { let muxer = gst::ElementFactory::make("mpegtsmux") .name("mpeg-ts_mux") .build() .expect("Could not make element mpegtsmux"); let giostreamsink = gst::ElementFactory::make("giostreamsink") .name("giostream_sink") .build() .expect("Could not make element giostreamsink"); let splitmuxsink = gst::ElementFactory::make("splitmuxsink") .name("split_mux_sink") .property("muxer", &muxer) .property("reset-muxer", false) .property("send-keyframe-requests", DEFAULT_SEND_KEYFRAME_REQUESTS) .property( "max-size-time", gst::ClockTime::from_seconds(DEFAULT_TARGET_DURATION as u64), ) .property("sink", &giostreamsink) .build() .expect("Could not make element splitmuxsink"); // giostreamsink doesn't let go of its stream until the element is finalized, which might // be too late for the calling application. Let's try to force it to close while tearing // down the pipeline. if giostreamsink.has_property("close-on-stop", Some(bool::static_type())) { giostreamsink.set_property("close-on-stop", true); } else { gst::warning!( CAT, "hlssink3 may sometimes fail to write out the final playlist update. This can be fixed by using giostreamsink from GStreamer 1.24 or later." ) } Self { location: String::from(DEFAULT_TS_LOCATION), target_duration: DEFAULT_TARGET_DURATION, playlist_type: None, send_keyframe_requests: DEFAULT_SEND_KEYFRAME_REQUESTS, i_frames_only: DEFAULT_I_FRAMES_ONLY_PLAYLIST, splitmuxsink, giostreamsink, video_sink: false, audio_sink: false, } } } #[derive(Default)] struct HlsSink3State { fragment_opened_at: Option, fragment_running_time: Option, current_segment_location: Option, } #[derive(Default)] pub struct HlsSink3 { settings: Mutex, state: Mutex, } #[glib::object_subclass] impl ObjectSubclass for HlsSink3 { const NAME: &'static str = "GstHlsSink3"; type Type = super::HlsSink3; type ParentType = HlsBaseSink; } impl ObjectImpl for HlsSink3 { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: Lazy> = Lazy::new(|| { vec![ glib::ParamSpecString::builder("location") .nick("File Location") .blurb("Location of the file to write") .default_value(Some(DEFAULT_TS_LOCATION)) .build(), glib::ParamSpecUInt::builder("target-duration") .nick("Target duration") .blurb("The target duration in seconds of a segment/file. (0 - disabled, useful for management of segment duration by the streaming server)") .default_value(DEFAULT_TARGET_DURATION) .build(), glib::ParamSpecEnum::builder_with_default("playlist-type", DEFAULT_PLAYLIST_TYPE) .nick("Playlist Type") .blurb("The type of the playlist to use. When VOD type is set, the playlist will be live until the pipeline ends execution.") .build(), glib::ParamSpecBoolean::builder("i-frames-only") .nick("I-Frames only playlist") .blurb("Each video segments is single iframe, So put EXT-X-I-FRAMES-ONLY tag in the playlist") .default_value(DEFAULT_I_FRAMES_ONLY_PLAYLIST) .build(), glib::ParamSpecBoolean::builder("send-keyframe-requests") .nick("Send Keyframe Requests") .blurb("Send keyframe requests to ensure correct fragmentation. If this is disabled then the input must have keyframes in regular intervals.") .default_value(DEFAULT_SEND_KEYFRAME_REQUESTS) .build(), ] }); PROPERTIES.as_ref() } fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { let mut settings = self.settings.lock().unwrap(); match pspec.name() { "location" => { settings.location = value .get::>() .expect("type checked upstream") .unwrap_or_else(|| DEFAULT_TS_LOCATION.into()); settings .splitmuxsink .set_property("location", &settings.location); } "target-duration" => { settings.target_duration = value.get().expect("type checked upstream"); settings.splitmuxsink.set_property( "max-size-time", gst::ClockTime::from_seconds(settings.target_duration as u64), ); } "playlist-type" => { settings.playlist_type = value .get::() .expect("type checked upstream") .into(); } "i-frames-only" => { settings.i_frames_only = value.get().expect("type checked upstream"); if settings.i_frames_only && settings.audio_sink { gst::element_error!( self.obj(), gst::StreamError::WrongType, ("Invalid configuration"), ["Audio not allowed for i-frames-only-stream"] ); } } "send-keyframe-requests" => { settings.send_keyframe_requests = value.get().expect("type checked upstream"); settings .splitmuxsink .set_property("send-keyframe-requests", settings.send_keyframe_requests); } _ => unimplemented!(), }; } fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { let settings = self.settings.lock().unwrap(); match pspec.name() { "location" => settings.location.to_value(), "target-duration" => settings.target_duration.to_value(), "playlist-type" => { let playlist_type: HlsSink3PlaylistType = settings.playlist_type.as_ref().into(); playlist_type.to_value() } "i-frames-only" => settings.i_frames_only.to_value(), "send-keyframe-requests" => settings.send_keyframe_requests.to_value(), _ => unimplemented!(), } } fn constructed(&self) { self.parent_constructed(); let obj = self.obj(); let settings = self.settings.lock().unwrap(); obj.add(&settings.splitmuxsink).unwrap(); settings .splitmuxsink .connect("format-location-full", false, { let imp_weak = self.downgrade(); move |args| { let Some(imp) = imp_weak.upgrade() else { return Some(None::.to_value()); }; let fragment_id = args[1].get::().unwrap(); gst::info!(CAT, imp: imp, "Got fragment-id: {}", fragment_id); let sample = args[2].get::().unwrap(); let buffer = sample.buffer(); let running_time = if let Some(buffer) = buffer { let segment = sample .segment() .expect("segment not available") .downcast_ref::() .expect("no time segment"); segment.to_running_time(buffer.pts().unwrap()) } else { gst::warning!( CAT, imp: imp, "buffer null for fragment-id: {}", fragment_id ); None }; match imp.on_format_location(fragment_id, running_time) { Ok(segment_location) => Some(segment_location.to_value()), Err(err) => { gst::error!(CAT, imp: imp, "on format-location handler: {}", err); Some("unknown_segment".to_value()) } } } }); } } impl GstObjectImpl for HlsSink3 {} impl ElementImpl for HlsSink3 { fn metadata() -> Option<&'static gst::subclass::ElementMetadata> { static ELEMENT_METADATA: Lazy = Lazy::new(|| { gst::subclass::ElementMetadata::new( "HTTP Live Streaming sink", "Sink/Muxer", "HTTP Live Streaming sink", "Alessandro Decina , \ Sebastian Dröge , \ Rafael Caricio ", ) }); Some(&*ELEMENT_METADATA) } fn pad_templates() -> &'static [gst::PadTemplate] { static PAD_TEMPLATES: Lazy> = Lazy::new(|| { let caps = gst::Caps::new_any(); let video_pad_template = gst::PadTemplate::new( "video", gst::PadDirection::Sink, gst::PadPresence::Request, &caps, ) .unwrap(); let caps = gst::Caps::new_any(); let audio_pad_template = gst::PadTemplate::new( "audio", gst::PadDirection::Sink, gst::PadPresence::Request, &caps, ) .unwrap(); vec![video_pad_template, audio_pad_template] }); PAD_TEMPLATES.as_ref() } fn change_state( &self, transition: gst::StateChange, ) -> Result { if transition == gst::StateChange::ReadyToPaused { let (target_duration, playlist_type, i_frames_only, segment_template) = { let settings = self.settings.lock().unwrap(); ( settings.target_duration, settings.playlist_type.clone(), settings.i_frames_only, settings.location.clone(), ) }; let playlist = self.start(target_duration, playlist_type, i_frames_only); base_imp!(self).open_playlist(playlist, segment_template); } self.parent_change_state(transition) } fn request_new_pad( &self, templ: &gst::PadTemplate, _name: Option<&str>, _caps: Option<&gst::Caps>, ) -> Option { let mut settings = self.settings.lock().unwrap(); match templ.name_template() { "audio" => { if settings.audio_sink { gst::debug!( CAT, imp: self, "requested_new_pad: audio pad is already set" ); return None; } if settings.i_frames_only { gst::element_error!( self.obj(), gst::StreamError::WrongType, ("Invalid configuration"), ["Audio not allowed for i-frames-only-stream"] ); return None; } let peer_pad = settings.splitmuxsink.request_pad_simple("audio_0").unwrap(); let sink_pad = gst::GhostPad::from_template_with_target(templ, &peer_pad).unwrap(); self.obj().add_pad(&sink_pad).unwrap(); sink_pad.set_active(true).unwrap(); settings.audio_sink = true; Some(sink_pad.upcast()) } "video" => { if settings.video_sink { gst::debug!( CAT, imp: self, "requested_new_pad: video pad is already set" ); return None; } let peer_pad = settings.splitmuxsink.request_pad_simple("video").unwrap(); let sink_pad = gst::GhostPad::from_template_with_target(templ, &peer_pad).unwrap(); self.obj().add_pad(&sink_pad).unwrap(); sink_pad.set_active(true).unwrap(); settings.video_sink = true; Some(sink_pad.upcast()) } other_name => { gst::debug!( CAT, imp: self, "requested_new_pad: name \"{}\" is not audio or video", other_name ); None } } } fn release_pad(&self, pad: &gst::Pad) { let mut settings = self.settings.lock().unwrap(); if !settings.audio_sink && !settings.video_sink { return; } let ghost_pad = pad.downcast_ref::().unwrap(); if let Some(peer) = ghost_pad.target() { settings.splitmuxsink.release_request_pad(&peer); } pad.set_active(false).unwrap(); self.obj().remove_pad(pad).unwrap(); if "audio" == ghost_pad.name() { settings.audio_sink = false; } else { settings.video_sink = false; } } } impl BinImpl for HlsSink3 { #[allow(clippy::single_match)] fn handle_message(&self, msg: gst::Message) { use gst::MessageView; match msg.view() { MessageView::Element(msg) => { let event_is_from_splitmuxsink = { let settings = self.settings.lock().unwrap(); msg.src() == Some(settings.splitmuxsink.upcast_ref()) }; if !event_is_from_splitmuxsink { return; } let s = msg.structure().unwrap(); match s.name().as_str() { "splitmuxsink-fragment-opened" => { if let Ok(new_fragment_opened_at) = s.get::("running-time") { let mut state = self.state.lock().unwrap(); state.fragment_opened_at = Some(new_fragment_opened_at); } } "splitmuxsink-fragment-closed" => { let s = msg.structure().unwrap(); if let Ok(fragment_closed_at) = s.get::("running-time") { self.on_fragment_closed(fragment_closed_at); } } _ => {} } } _ => self.parent_handle_message(msg), } } } impl HlsBaseSinkImpl for HlsSink3 {} impl HlsSink3 { fn start( &self, target_duration: u32, playlist_type: Option, i_frames_only: bool, ) -> Playlist { gst::info!(CAT, imp: self, "Starting"); let mut state = self.state.lock().unwrap(); *state = HlsSink3State::default(); let (turn_vod, playlist_type) = if playlist_type == Some(MediaPlaylistType::Vod) { (true, Some(MediaPlaylistType::Event)) } else { (false, playlist_type) }; let playlist = MediaPlaylist { version: if i_frames_only { Some(4) } else { Some(3) }, target_duration: target_duration as f32, playlist_type, i_frames_only, ..Default::default() }; Playlist::new(playlist, turn_vod, false) } fn on_format_location( &self, fragment_id: u32, running_time: Option, ) -> Result { gst::info!( CAT, imp: self, "Starting the formatting of the fragment-id: {}", fragment_id ); let (fragment_stream, segment_file_location) = base_imp!(self) .get_fragment_stream(fragment_id) .ok_or_else(|| String::from("Error while getting fragment stream"))?; let mut state = self.state.lock().unwrap(); state.current_segment_location = Some(segment_file_location.clone()); state.fragment_running_time = running_time; let settings = self.settings.lock().unwrap(); settings .giostreamsink .set_property("stream", &fragment_stream); gst::info!( CAT, imp: self, "New segment location: {:?}", state.current_segment_location.as_ref() ); Ok(segment_file_location) } fn on_fragment_closed(&self, closed_at: gst::ClockTime) { let mut state = self.state.lock().unwrap(); let location = match state.current_segment_location.take() { Some(location) => location, None => { gst::error!(CAT, imp: self, "Unknown segment location"); return; } }; let opened_at = match state.fragment_opened_at.take() { Some(opened_at) => opened_at, None => { gst::error!(CAT, imp: self, "Unknown segment duration"); return; } }; let duration = ((closed_at - opened_at).mseconds() as f32) / 1_000f32; let running_time = state.fragment_running_time; drop(state); let obj = self.obj(); let base_imp = obj.upcast_ref::().imp(); let uri = base_imp.get_segment_uri(&location); let _ = base_imp.add_segment( &location, running_time, MediaSegment { uri, duration, ..Default::default() }, ); } }