mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2024-09-27 14:30:27 +00:00
582 lines
20 KiB
Rust
582 lines
20 KiB
Rust
|
// Copyright (C) 2021 Rafael Caricio <rafael@caricio.com>
|
||
|
// Copyright (C) 2023 Seungha Yang <seungha@centricular.com>
|
||
|
//
|
||
|
// 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
|
||
|
|
||
|
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<gst::DebugCategory> = Lazy::new(|| {
|
||
|
gst::DebugCategory::new("hlssink3", gst::DebugColorFlags::empty(), Some("HLS sink"))
|
||
|
});
|
||
|
|
||
|
macro_rules! base_imp {
|
||
|
($i:expr) => {
|
||
|
$i.obj().upcast_ref::<HlsBaseSink>().imp()
|
||
|
};
|
||
|
}
|
||
|
|
||
|
impl From<HlsSink3PlaylistType> for Option<MediaPlaylistType> {
|
||
|
fn from(pl_type: HlsSink3PlaylistType) -> Self {
|
||
|
use HlsSink3PlaylistType::*;
|
||
|
match pl_type {
|
||
|
Unspecified => None,
|
||
|
Event => Some(MediaPlaylistType::Event),
|
||
|
Vod => Some(MediaPlaylistType::Vod),
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl From<Option<&MediaPlaylistType>> 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<MediaPlaylistType>,
|
||
|
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<gst::ClockTime>,
|
||
|
fragment_running_time: Option<gst::ClockTime>,
|
||
|
current_segment_location: Option<String>,
|
||
|
}
|
||
|
|
||
|
#[derive(Default)]
|
||
|
pub struct HlsSink3 {
|
||
|
settings: Mutex<HlsSink3Settings>,
|
||
|
state: Mutex<HlsSink3State>,
|
||
|
}
|
||
|
|
||
|
#[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<Vec<glib::ParamSpec>> = 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::<Option<String>>()
|
||
|
.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::<HlsSink3PlaylistType>()
|
||
|
.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::<String>.to_value());
|
||
|
};
|
||
|
let fragment_id = args[1].get::<u32>().unwrap();
|
||
|
gst::info!(CAT, imp: imp, "Got fragment-id: {}", fragment_id);
|
||
|
|
||
|
let sample = args[2].get::<gst::Sample>().unwrap();
|
||
|
let buffer = sample.buffer();
|
||
|
let running_time = if let Some(buffer) = buffer {
|
||
|
let segment = sample
|
||
|
.segment()
|
||
|
.expect("segment not available")
|
||
|
.downcast_ref::<gst::ClockTime>()
|
||
|
.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<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||
|
gst::subclass::ElementMetadata::new(
|
||
|
"HTTP Live Streaming sink",
|
||
|
"Sink/Muxer",
|
||
|
"HTTP Live Streaming sink",
|
||
|
"Alessandro Decina <alessandro.d@gmail.com>, \
|
||
|
Sebastian Dröge <sebastian@centricular.com>, \
|
||
|
Rafael Caricio <rafael@caricio.com>",
|
||
|
)
|
||
|
});
|
||
|
|
||
|
Some(&*ELEMENT_METADATA)
|
||
|
}
|
||
|
|
||
|
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||
|
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = 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<gst::StateChangeSuccess, gst::StateChangeError> {
|
||
|
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<gst::Pad> {
|
||
|
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::<gst::GhostPad>().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::<gst::ClockTime>("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::<gst::ClockTime>("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<MediaPlaylistType>,
|
||
|
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<gst::ClockTime>,
|
||
|
) -> Result<String, String> {
|
||
|
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::<HlsBaseSink>().imp();
|
||
|
let uri = base_imp.get_segment_uri(&location);
|
||
|
let _ = base_imp.add_segment(
|
||
|
&location,
|
||
|
running_time,
|
||
|
MediaSegment {
|
||
|
uri,
|
||
|
duration,
|
||
|
..Default::default()
|
||
|
},
|
||
|
);
|
||
|
}
|
||
|
}
|