mirror of
https://github.com/nestriness/warp.git
synced 2025-12-10 17:05:39 +02:00
Fuck Rust, this is getting way too complicated.
Fixed error handling in ``start``. Fixed more stuff in ``render``
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -14,3 +14,10 @@
|
||||
# already existing elements were commented out
|
||||
|
||||
#/target
|
||||
|
||||
|
||||
# Added by cargo
|
||||
#
|
||||
# already existing elements were commented out
|
||||
|
||||
#/target
|
||||
|
||||
2189
Cargo.lock
generated
Normal file
2189
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "warp"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1", features = ["backtrace"] }
|
||||
|
||||
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs"}
|
||||
moq-sink = { path = "gst-moq-sink" }
|
||||
54
gst-warp-sink/Cargo.lock → gst-moq-sink/Cargo.lock
generated
54
gst-warp-sink/Cargo.lock → gst-moq-sink/Cargo.lock
generated
@@ -1021,7 +1021,33 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "me"
|
||||
name = "memchr"
|
||||
version = "2.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
|
||||
dependencies = [
|
||||
"adler",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moq-sink"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
@@ -1048,32 +1074,6 @@ dependencies = [
|
||||
"webtransport-quinn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
|
||||
dependencies = [
|
||||
"adler",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moq-transport"
|
||||
version = "0.2.0"
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "me"
|
||||
name = "moq-sink"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
@@ -9,9 +9,8 @@ edition = "2021"
|
||||
anyhow = { version = "1", features = ["backtrace"] }
|
||||
|
||||
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs"}
|
||||
gst-app = { package = "gstreamer-app", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs"}
|
||||
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_18"] }
|
||||
gst-pbutils = { package = "gstreamer-pbutils", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_20"] }
|
||||
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
|
||||
gst-sys = { package = "gstreamer-sys", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
|
||||
chrono = "0.4.31"
|
||||
isobmff = { git = "https://github.com/LMinJae/isobmff-rs", version = "0.1.0" }
|
||||
bytes = "1.5.0"
|
||||
@@ -2,14 +2,15 @@ use gst::glib;
|
||||
use gst::glib::once_cell::sync::Lazy;
|
||||
use gst::prelude::*;
|
||||
use gst::subclass::prelude::*;
|
||||
use gst::ClockTime;
|
||||
use gst_base::subclass::prelude::BaseSinkImpl;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::relayurl::*;
|
||||
use crate::RUNTIME;
|
||||
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use moq_transport::cache::{broadcast, fragment, segment, track};
|
||||
use url::Url;
|
||||
@@ -106,11 +107,11 @@ struct Started {
|
||||
moof_atom: Option<Mp4Atom>,
|
||||
// Below members that track current fragment (moof, mdat).
|
||||
/// Minimum PTS in fragment.
|
||||
fragment_pts: ClockTime,
|
||||
fragment_pts: Option<ClockTime>,
|
||||
/// Minimum DTS in fragment.
|
||||
fragment_dts: ClockTime,
|
||||
fragment_dts: Option<ClockTime>,
|
||||
/// Maximum PTS + duration in fragment.
|
||||
fragment_max_pts_plus_duration: ClockTime,
|
||||
fragment_max_pts_plus_duration: Option<ClockTime>,
|
||||
/// Minimum offset in fragment.
|
||||
fragment_offset: Option<u64>,
|
||||
/// Maximum offset_end in fragment.
|
||||
@@ -126,9 +127,9 @@ impl Started {
|
||||
ftype_atom: None,
|
||||
moov_atom: None,
|
||||
moof_atom: None,
|
||||
fragment_pts: ClockTime::none(),
|
||||
fragment_dts: ClockTime::none(),
|
||||
fragment_max_pts_plus_duration: ClockTime::none(),
|
||||
fragment_pts: ClockTime::NONE,
|
||||
fragment_dts: ClockTime::NONE,
|
||||
fragment_max_pts_plus_duration: ClockTime::NONE,
|
||||
fragment_offset: None,
|
||||
fragment_offset_end: None,
|
||||
fragment_buffer_flags: gst::BufferFlags::DELTA_UNIT,
|
||||
@@ -185,7 +186,6 @@ static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||
)
|
||||
});
|
||||
|
||||
#[derive(Default, object_subclass)]
|
||||
pub struct MoqSink {
|
||||
state: Mutex<State>,
|
||||
url: Mutex<Option<Url>>,
|
||||
@@ -195,7 +195,7 @@ pub struct MoqSink {
|
||||
impl Default for MoqSink {
|
||||
fn default() -> Self {
|
||||
MoqSink {
|
||||
state: Mutex::new(None),
|
||||
state: Mutex::new(State::Stopped),
|
||||
url: Mutex::new(None),
|
||||
settings: Mutex::new(Settings::default()),
|
||||
}
|
||||
@@ -228,25 +228,6 @@ impl MoqSink {
|
||||
}
|
||||
};
|
||||
|
||||
//More complex but with error handling
|
||||
|
||||
// let relay_url = self
|
||||
// .url
|
||||
// .lock()
|
||||
// .map_err(|e| {
|
||||
// gst::error_msg!(
|
||||
// gst::ResourceError::Settings,
|
||||
// ["Failed to acquire URL lock: {}", e]
|
||||
// )
|
||||
// })?
|
||||
// .clone()
|
||||
// .ok_or_else(|| {
|
||||
// gst::error_msg!(
|
||||
// gst::ResourceError::Settings,
|
||||
// ["Cannot start without a URL being set"]
|
||||
// )
|
||||
// })?;
|
||||
|
||||
gst::trace!(
|
||||
CAT,
|
||||
imp: self,
|
||||
@@ -267,12 +248,34 @@ impl MoqSink {
|
||||
let mut roots = rustls::RootCertStore::empty();
|
||||
|
||||
// Add the platform's native root certificates.
|
||||
for cert in
|
||||
rustls_native_certs::load_native_certs().context("could not load platform certs")?
|
||||
{
|
||||
roots
|
||||
.add(&rustls::Certificate(cert.0))
|
||||
.context("failed to add root cert")?;
|
||||
let certs = {
|
||||
let c = rustls_native_certs::load_native_certs();
|
||||
match c {
|
||||
Ok(certs) => certs,
|
||||
Err(e) => {
|
||||
gst::error!(CAT,"Could not load platform certs : {}", e);
|
||||
|
||||
return gst::error_msg!(
|
||||
gst::ResourceError::Failed,
|
||||
["could not load platform certs"]
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for cert in certs {
|
||||
let res = roots.add(&rustls::Certificate(cert.0));
|
||||
match res {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
gst::error!(CAT, "Failed to add root cert : {}", e);
|
||||
|
||||
return gst::error_msg!(
|
||||
gst::ResourceError::Failed,
|
||||
["failed to add root cert"]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut tls_config = rustls::ClientConfig::builder()
|
||||
@@ -286,25 +289,71 @@ impl MoqSink {
|
||||
let arc_tls_config = std::sync::Arc::new(tls_config);
|
||||
let quinn_client_config = quinn::ClientConfig::new(arc_tls_config);
|
||||
|
||||
let mut endpoint =
|
||||
quinn::Endpoint::client(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0))?;
|
||||
let mut endpoint = {
|
||||
let endpoint = quinn::Endpoint::client(SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
|
||||
0,
|
||||
));
|
||||
|
||||
match endpoint {
|
||||
Ok(end) => end,
|
||||
Err(e) => {
|
||||
gst::error!(CAT, "Failed to set endpoint to [::]:0 : {}", e);
|
||||
|
||||
return gst::error_msg!(
|
||||
gst::ResourceError::Failed,
|
||||
["failed to set endpoint to [::]:0"]
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
endpoint.set_default_client_config(quinn_client_config);
|
||||
|
||||
let session = webtransport_quinn::connect(&endpoint, &relay_url)
|
||||
.await
|
||||
.context("failed to create WebTransport session")?;
|
||||
let session = {
|
||||
let session = webtransport_quinn::connect(&endpoint, &relay_url).await;
|
||||
|
||||
let session = moq_transport::session::Client::publisher(session, subscriber)
|
||||
.await
|
||||
.context("failed to create MoQ Transport session")?;
|
||||
match session {
|
||||
Ok(session) => session,
|
||||
Err(e) => {
|
||||
gst::error!(CAT, "Failed to create WebTransport session: {}", e);
|
||||
|
||||
session.run().await.context("session error")?;
|
||||
return gst::error_msg!(
|
||||
gst::ResourceError::Failed,
|
||||
["failed to create WebTransport session"]
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
let session = {
|
||||
let session = moq_transport::session::Client::publisher(session, subscriber).await;
|
||||
|
||||
match session {
|
||||
Ok(session) => session,
|
||||
Err(e) => {
|
||||
gst::error!(CAT, "Failed to create MoQ Transport session: {}", e);
|
||||
|
||||
return gst::error_msg!(
|
||||
gst::ResourceError::Failed,
|
||||
["failed to create MoQ Transport session"]
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = session.run().await {
|
||||
gst::error!(CAT, "Session error: {}", e);
|
||||
};
|
||||
|
||||
return gst::error_msg!(
|
||||
gst::ResourceError::Failed,
|
||||
["something is not working as intended on this thread"]
|
||||
);
|
||||
});
|
||||
|
||||
// Update the state to indicate the element has started
|
||||
*state = State::Started(Started::new(broadcast));
|
||||
*state = State::Started(Started::new(publisher));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -346,18 +395,14 @@ impl ObjectSubclass for MoqSink {
|
||||
type ParentType = gst_base::BaseSink;
|
||||
|
||||
type Interfaces = (gst::URIHandler,);
|
||||
|
||||
type Instance;
|
||||
|
||||
type Class;
|
||||
}
|
||||
|
||||
impl GstObjectImpl for WaylandDisplaySrc {}
|
||||
impl GstObjectImpl for MoqSink {}
|
||||
|
||||
impl ObjectImpl for MoqSink {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
self.obj().set_sync(false);
|
||||
self.obj();
|
||||
}
|
||||
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
@@ -443,8 +488,6 @@ impl ObjectImpl for MoqSink {
|
||||
}
|
||||
}
|
||||
|
||||
impl GstObjectImpl for MoqSink {}
|
||||
|
||||
impl ElementImpl for MoqSink {
|
||||
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||
@@ -496,11 +539,8 @@ impl BaseSinkImpl for MoqSink {
|
||||
fn start(&self) -> Result<(), gst::ErrorMessage> {
|
||||
self.start()
|
||||
}
|
||||
fn render(
|
||||
&self,
|
||||
element: &Self::Type,
|
||||
buffer: &gst::Buffer,
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
|
||||
fn render(&self, buffer: &gst::Buffer) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
if let State::Stopped = *self.state.lock().unwrap() {
|
||||
gst::element_imp_error!(self, gst::CoreError::Failed, ["Not started yet"]);
|
||||
return Err(gst::FlowError::Error);
|
||||
@@ -536,19 +576,26 @@ impl BaseSinkImpl for MoqSink {
|
||||
if started_state.fragment_dts.is_none() || started_state.fragment_dts > buffer.dts() {
|
||||
started_state.fragment_dts = buffer.dts();
|
||||
}
|
||||
let pts_plus_duration = buffer.pts() + buffer.duration();
|
||||
let pts = buffer.pts();
|
||||
let duration = buffer.duration();
|
||||
|
||||
let pts_plus_duration = match (pts, duration) {
|
||||
(Some(pts), Some(duration)) => Some(pts + duration),
|
||||
// Handle the case where one or both values are `None`
|
||||
_ => None,
|
||||
};
|
||||
if started_state.fragment_max_pts_plus_duration.is_none()
|
||||
|| started_state.fragment_max_pts_plus_duration < pts_plus_duration
|
||||
{
|
||||
started_state.fragment_max_pts_plus_duration = pts_plus_duration;
|
||||
}
|
||||
if buffer.offset() != gst::BUFFER_OFFSET_NONE
|
||||
if buffer.offset() != gst_sys::GST_BUFFER_OFFSET_NONE
|
||||
&& (started_state.fragment_offset.is_none()
|
||||
|| started_state.fragment_offset.unwrap() > buffer.offset())
|
||||
{
|
||||
started_state.fragment_offset = Some(buffer.offset());
|
||||
}
|
||||
if buffer.offset_end() != gst::BUFFER_OFFSET_NONE
|
||||
if buffer.offset_end() != gst_sys::GST_BUFFER_OFFSET_NONE
|
||||
&& (started_state.fragment_offset_end.is_none()
|
||||
|| started_state.fragment_offset_end.unwrap() < buffer.offset_end())
|
||||
{
|
||||
@@ -616,18 +663,31 @@ impl BaseSinkImpl for MoqSink {
|
||||
let buffer_ref = gst_buffer.get_mut().unwrap();
|
||||
buffer_ref.set_pts(started_state.fragment_pts);
|
||||
buffer_ref.set_dts(started_state.fragment_dts);
|
||||
let duration = started_state.fragment_max_pts_plus_duration
|
||||
- started_state.fragment_pts;
|
||||
// let duration =
|
||||
// started_state.fragment_max_pts_plus_duration.clone()
|
||||
// - started_state.fragment_pts.clone();
|
||||
|
||||
let pts_plus_duration =
|
||||
started_state.fragment_max_pts_plus_duration.clone();
|
||||
let fragment_pts = started_state.fragment_pts.clone();
|
||||
|
||||
let duration = match (pts_plus_duration, fragment_pts) {
|
||||
(Some(pts_plus_duration), Some(fragment_pts)) => {
|
||||
Some(pts_plus_duration - fragment_pts)
|
||||
}
|
||||
// Handle the case where one or both values are `None`
|
||||
_ => None,
|
||||
};
|
||||
buffer_ref.set_duration(duration);
|
||||
buffer_ref.set_offset(
|
||||
started_state
|
||||
.fragment_offset
|
||||
.unwrap_or(gst::BUFFER_OFFSET_NONE),
|
||||
.unwrap_or(gst_sys::GST_BUFFER_OFFSET_NONE),
|
||||
);
|
||||
buffer_ref.set_offset_end(
|
||||
started_state
|
||||
.fragment_offset_end
|
||||
.unwrap_or(gst::BUFFER_OFFSET_NONE),
|
||||
.unwrap_or(gst_sys::GST_BUFFER_OFFSET_NONE),
|
||||
);
|
||||
buffer_ref.set_flags(started_state.fragment_buffer_flags);
|
||||
let mut buffer_map = buffer_ref.map_writable().unwrap();
|
||||
@@ -652,29 +712,30 @@ impl BaseSinkImpl for MoqSink {
|
||||
//FIXME: Work on the Json here, instead of redoing it in a new method.
|
||||
}
|
||||
// Clear fragment variables.
|
||||
started_state.fragment_pts = ClockTime::none();
|
||||
started_state.fragment_dts = ClockTime::none();
|
||||
started_state.fragment_max_pts_plus_duration = ClockTime::none();
|
||||
started_state.fragment_pts = ClockTime::NONE;
|
||||
started_state.fragment_dts = ClockTime::NONE;
|
||||
started_state.fragment_max_pts_plus_duration = ClockTime::NONE;
|
||||
started_state.fragment_offset = None;
|
||||
started_state.fragment_offset_end = None;
|
||||
started_state.fragment_buffer_flags = gst::BufferFlags::DELTA_UNIT;
|
||||
started_state.fragment_buffer_flags =
|
||||
gst::BufferFlags::DELTA_UNIT;
|
||||
// Push new buffer.
|
||||
gst::log!(CAT, imp: self, "Pushing buffer {:?}", gst_buffer);
|
||||
}
|
||||
_ => {
|
||||
gst_warning!(CAT, obj: pad, "Received mdat without ftype, moov, or moof");
|
||||
gst::warning!(CAT, imp: self, "Received mdat without ftype, moov, or moof");
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
gst_warning!(CAT, obj: pad, "Unknown atom type {:?}", atom);
|
||||
gst::warning!(CAT, imp: self, "Unknown atom type {:?}", atom);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
gst_trace!(CAT, obj: element, "sink_chain: END: state={:?}", state);
|
||||
gst::trace!(CAT, imp: self, "sink_chain: END: state={:?}", started_state);
|
||||
|
||||
Ok(gst::FlowSuccess::Ok)
|
||||
}
|
||||
45
src/main.rs
45
src/main.rs
@@ -1,43 +1,16 @@
|
||||
use anyhow::Error;
|
||||
|
||||
use gst::prelude::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::Error;
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
gst::init()?;
|
||||
gstfmp4::plugin_register_static().expect("Failed to register fmp4 plugin");
|
||||
// gst::init()?;
|
||||
|
||||
let pipeline = gst::parse_launch("videotestsrc num-buffers=2500 ! timecodestamper ! video/x-raw,format=I420,width=1280,height=720,framerate=30/1 ! videoconvert ! queue ! x264enc tune=zerolatency key-int-max=30 ! mp4mux streamable=true fragment-duration=1 ! ! testsink name=sink ").unwrap().downcast::<gst::Pipeline>().unwrap();
|
||||
|
||||
pipeline.set_state(gst::State::Playing)?;
|
||||
|
||||
let bus = pipeline
|
||||
.bus()
|
||||
.expect("Pipeline without bus. Shouldn't happen!");
|
||||
|
||||
for msg in bus.iter_timed(gst::ClockTime::NONE) {
|
||||
use gst::MessageView;
|
||||
|
||||
match msg.view() {
|
||||
MessageView::Eos(..) => {
|
||||
println!("EOS");
|
||||
break;
|
||||
}
|
||||
MessageView::Error(err) => {
|
||||
pipeline.set_state(gst::State::Null)?;
|
||||
eprintln!(
|
||||
"Got error from {}: {} ({})",
|
||||
msg.src()
|
||||
.map(|s| String::from(s.path_string()))
|
||||
.unwrap_or_else(|| "None".into()),
|
||||
err.error(),
|
||||
err.debug().unwrap_or_else(|| "".into()),
|
||||
);
|
||||
break;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
pipeline.set_state(gst::State::Null)?;
|
||||
// gstfmp4::plugin_register_static()?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,542 +0,0 @@
|
||||
// Copyright (C) 2022 Mathieu Duponchelle <mathieu@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
|
||||
|
||||
// This creates a live HLS stream with one video playlist and two video playlists.
|
||||
// Basic trimming is implemented
|
||||
|
||||
use bytes::BytesMut;
|
||||
use gst::prelude::*;
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::io::Cursor;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::Error;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use isobmff::IO;
|
||||
use m3u8_rs::{
|
||||
AlternativeMedia, AlternativeMediaType, MasterPlaylist, MediaPlaylist, MediaSegment,
|
||||
VariantStream,
|
||||
};
|
||||
|
||||
struct State {
|
||||
video_streams: Vec<VideoStream>,
|
||||
audio_streams: Vec<AudioStream>,
|
||||
all_mimes: Vec<String>,
|
||||
path: PathBuf,
|
||||
wrote_manifest: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn maybe_write_manifest(&mut self) {
|
||||
if self.wrote_manifest {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.all_mimes.len() < self.video_streams.len() + self.audio_streams.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut all_mimes = self.all_mimes.clone();
|
||||
all_mimes.sort();
|
||||
all_mimes.dedup();
|
||||
|
||||
let playlist = MasterPlaylist {
|
||||
version: Some(7),
|
||||
variants: self
|
||||
.video_streams
|
||||
.iter()
|
||||
.map(|stream| {
|
||||
let mut path = PathBuf::new();
|
||||
|
||||
path.push(&stream.name);
|
||||
path.push("manifest.m3u8");
|
||||
|
||||
VariantStream {
|
||||
uri: path.as_path().display().to_string(),
|
||||
bandwidth: stream.bitrate,
|
||||
codecs: Some(all_mimes.join(",")),
|
||||
resolution: Some(m3u8_rs::Resolution {
|
||||
width: stream.width,
|
||||
height: stream.height,
|
||||
}),
|
||||
audio: Some("audio".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
alternatives: self
|
||||
.audio_streams
|
||||
.iter()
|
||||
.map(|stream| {
|
||||
let mut path = PathBuf::new();
|
||||
path.push(&stream.name);
|
||||
path.push("manifest.m3u8");
|
||||
|
||||
AlternativeMedia {
|
||||
media_type: AlternativeMediaType::Audio,
|
||||
uri: Some(path.as_path().display().to_string()),
|
||||
group_id: "audio".to_string(),
|
||||
language: Some(stream.lang.clone()),
|
||||
name: stream.name.clone(),
|
||||
default: stream.default,
|
||||
autoselect: stream.default,
|
||||
channels: Some("2".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
independent_segments: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
println!("Writing master manifest to {}", self.path.display());
|
||||
|
||||
let mut file = std::fs::File::create(&self.path).unwrap();
|
||||
playlist
|
||||
.write_to(&mut file)
|
||||
.expect("Failed to write master playlist");
|
||||
|
||||
self.wrote_manifest = true;
|
||||
}
|
||||
}
|
||||
|
||||
struct Segment {
|
||||
date_time: DateTime<Utc>,
|
||||
duration: gst::ClockTime,
|
||||
path: String,
|
||||
}
|
||||
|
||||
struct UnreffedSegment {
|
||||
removal_time: DateTime<Utc>,
|
||||
path: String,
|
||||
}
|
||||
|
||||
struct StreamState {
|
||||
path: PathBuf,
|
||||
segments: VecDeque<Segment>,
|
||||
trimmed_segments: VecDeque<UnreffedSegment>,
|
||||
start_date_time: Option<DateTime<Utc>>,
|
||||
start_time: Option<gst::ClockTime>,
|
||||
media_sequence: u64,
|
||||
segment_index: u32,
|
||||
}
|
||||
|
||||
struct VideoStream {
|
||||
name: String,
|
||||
bitrate: u64,
|
||||
width: u64,
|
||||
height: u64,
|
||||
}
|
||||
|
||||
struct AudioStream {
|
||||
name: String,
|
||||
lang: String,
|
||||
default: bool,
|
||||
wave: String,
|
||||
}
|
||||
|
||||
fn trim_segments(state: &mut StreamState) {
|
||||
// Arbitrary 5 segments window
|
||||
while state.segments.len() > 5 {
|
||||
let segment = state.segments.pop_front().unwrap();
|
||||
|
||||
state.media_sequence += 1;
|
||||
|
||||
state.trimmed_segments.push_back(UnreffedSegment {
|
||||
// HLS spec mandates that segments are removed from the filesystem no sooner
|
||||
// than the duration of the longest playlist + duration of the segment.
|
||||
// This is 15 seconds (12.5 + 2.5) in our case, we use 20 seconds to be on the
|
||||
// safe side
|
||||
removal_time: segment
|
||||
.date_time
|
||||
.checked_add_signed(Duration::seconds(20))
|
||||
.unwrap(),
|
||||
path: segment.path.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
while let Some(segment) = state.trimmed_segments.front() {
|
||||
if segment.removal_time < state.segments.front().unwrap().date_time {
|
||||
let segment = state.trimmed_segments.pop_front().unwrap();
|
||||
|
||||
let mut path = state.path.clone();
|
||||
path.push(segment.path);
|
||||
println!("Removing {}", path.display());
|
||||
std::fs::remove_file(path).expect("Failed to remove old segment");
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_manifest(state: &mut StreamState) {
|
||||
// Now write the manifest
|
||||
let mut path = state.path.clone();
|
||||
path.push("manifest.m3u8");
|
||||
|
||||
println!("writing manifest to {}", path.display());
|
||||
|
||||
trim_segments(state);
|
||||
|
||||
let playlist = MediaPlaylist {
|
||||
version: Some(7),
|
||||
target_duration: 2.5,
|
||||
media_sequence: state.media_sequence,
|
||||
segments: state
|
||||
.segments
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, segment)| MediaSegment {
|
||||
uri: segment.path.to_string(),
|
||||
duration: (segment.duration.nseconds() as f64
|
||||
/ gst::ClockTime::SECOND.nseconds() as f64) as f32,
|
||||
map: Some(m3u8_rs::Map {
|
||||
uri: "init.cmfi".into(),
|
||||
..Default::default()
|
||||
}),
|
||||
program_date_time: if idx == 0 {
|
||||
Some(segment.date_time.into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
end_list: false,
|
||||
playlist_type: None,
|
||||
i_frames_only: false,
|
||||
start: None,
|
||||
independent_segments: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut file = std::fs::File::create(path).unwrap();
|
||||
playlist
|
||||
.write_to(&mut file)
|
||||
.expect("Failed to write media playlist");
|
||||
}
|
||||
|
||||
fn setup_appsink(appsink: &gst_app::AppSink, name: &str, path: &Path, is_video: bool) {
|
||||
let mut path: PathBuf = path.into();
|
||||
path.push(name);
|
||||
|
||||
let state = Arc::new(Mutex::new(StreamState {
|
||||
segments: VecDeque::new(),
|
||||
trimmed_segments: VecDeque::new(),
|
||||
path,
|
||||
start_date_time: None,
|
||||
start_time: gst::ClockTime::NONE,
|
||||
media_sequence: 0,
|
||||
segment_index: 0,
|
||||
}));
|
||||
|
||||
appsink.set_callbacks(
|
||||
gst_app::AppSinkCallbacks::builder()
|
||||
.new_sample(move |sink| {
|
||||
let sample = sink.pull_sample().map_err(|_| gst::FlowError::Eos)?;
|
||||
// let mut state = state.lock().unwrap();
|
||||
let mut state = state.lock().unwrap();
|
||||
|
||||
// The muxer only outputs non-empty buffer lists
|
||||
let mut buffer_list = sample.buffer_list_owned().expect("no buffer list");
|
||||
assert!(!buffer_list.is_empty());
|
||||
|
||||
let mut first = buffer_list.get(0).unwrap();
|
||||
|
||||
// Each list contains a full segment, i.e. does not start with a DELTA_UNIT
|
||||
assert!(!first.flags().contains(gst::BufferFlags::DELTA_UNIT));
|
||||
|
||||
// If the buffer has the DISCONT and HEADER flag set then it contains the media
|
||||
// header, i.e. the `ftyp`, `moov` and other media boxes.
|
||||
//
|
||||
// This might be the initial header or the updated header at the end of the stream.
|
||||
if first
|
||||
.flags()
|
||||
.contains(gst::BufferFlags::DISCONT | gst::BufferFlags::HEADER)
|
||||
{
|
||||
// let mut path = state.path.clone();
|
||||
// std::fs::create_dir_all(&path).expect("failed to create directory");
|
||||
// path.push("init.cmfi");
|
||||
|
||||
// println!("writing header to {}", path.display());
|
||||
let map = first.map_readable().unwrap();
|
||||
let mut cursor = Cursor::new(&*map);
|
||||
|
||||
let header = mp4::BoxHeader::read(&mut cursor).unwrap();
|
||||
println!("header name {}", header.name);
|
||||
|
||||
match header.name {
|
||||
mp4::BoxType::MoofBox => {
|
||||
println!("writing manifest to moof");
|
||||
}
|
||||
mp4::BoxType::MdatBox => {
|
||||
println!("writing manifest to mdat");
|
||||
}
|
||||
|
||||
_ => {
|
||||
// Skip unknown atoms
|
||||
}
|
||||
}
|
||||
|
||||
drop(map);
|
||||
|
||||
// Remove the header from the buffer list
|
||||
buffer_list.make_mut().remove(0, 1);
|
||||
|
||||
// If the list is now empty then it only contained the media header and nothing
|
||||
// else.
|
||||
if buffer_list.is_empty() {
|
||||
return Ok(gst::FlowSuccess::Ok);
|
||||
}
|
||||
|
||||
// Otherwise get the next buffer and continue working with that.
|
||||
first = buffer_list.get(0).unwrap();
|
||||
}
|
||||
|
||||
// If the buffer only has the HEADER flag set then this is a segment header that is
|
||||
// followed by one or more actual media buffers.
|
||||
assert!(first.flags().contains(gst::BufferFlags::HEADER));
|
||||
|
||||
let map = first.map_readable().unwrap();
|
||||
let mut cursor = Cursor::new(&*map);
|
||||
|
||||
let header = mp4::BoxHeader::read(&mut cursor).unwrap();
|
||||
println!("header name 2 {}", header.name);
|
||||
|
||||
match header.name {
|
||||
mp4::BoxType::MoofBox => {
|
||||
println!("writing manifest to moof");
|
||||
}
|
||||
mp4::BoxType::MdatBox => {
|
||||
println!("writing manifest to mdat");
|
||||
}
|
||||
|
||||
_ => {
|
||||
// Skip unknown atoms
|
||||
}
|
||||
}
|
||||
|
||||
Ok(gst::FlowSuccess::Ok)
|
||||
})
|
||||
.eos(move |_sink| {
|
||||
unreachable!();
|
||||
})
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
|
||||
fn probe_encoder(state: Arc<Mutex<State>>, enc: gst::Element) {
|
||||
enc.static_pad("src").unwrap().add_probe(
|
||||
gst::PadProbeType::EVENT_DOWNSTREAM,
|
||||
move |_pad, info| {
|
||||
let Some(ev) = info.event() else {
|
||||
return gst::PadProbeReturn::Ok;
|
||||
};
|
||||
let gst::EventView::Caps(ev) = ev.view() else {
|
||||
return gst::PadProbeReturn::Ok;
|
||||
};
|
||||
|
||||
let mime = gst_pbutils::codec_utils_caps_get_mime_codec(ev.caps());
|
||||
|
||||
let mut state = state.lock().unwrap();
|
||||
state.all_mimes.push(mime.unwrap().into());
|
||||
state.maybe_write_manifest();
|
||||
|
||||
gst::PadProbeReturn::Remove
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
impl VideoStream {
|
||||
fn setup(
|
||||
&self,
|
||||
state: Arc<Mutex<State>>,
|
||||
pipeline: &gst::Pipeline,
|
||||
path: &Path,
|
||||
) -> Result<(), Error> {
|
||||
let src = gst::ElementFactory::make("videotestsrc")
|
||||
.property("is-live", true)
|
||||
.build()?;
|
||||
|
||||
let raw_capsfilter = gst::ElementFactory::make("capsfilter")
|
||||
.property(
|
||||
"caps",
|
||||
gst_video::VideoCapsBuilder::new()
|
||||
.format(gst_video::VideoFormat::I420)
|
||||
.width(self.width as i32)
|
||||
.height(self.height as i32)
|
||||
.framerate(30.into())
|
||||
.build(),
|
||||
)
|
||||
.build()?;
|
||||
let timeoverlay = gst::ElementFactory::make("timeoverlay").build()?;
|
||||
let enc = gst::ElementFactory::make("x264enc")
|
||||
.property("bframes", 0u32)
|
||||
.property("bitrate", self.bitrate as u32 / 1000u32)
|
||||
.property_from_str("tune", "zerolatency")
|
||||
.build()?;
|
||||
let h264_capsfilter = gst::ElementFactory::make("capsfilter")
|
||||
.property(
|
||||
"caps",
|
||||
gst::Caps::builder("video/x-h264")
|
||||
.field("profile", "main")
|
||||
.build(),
|
||||
)
|
||||
.build()?;
|
||||
let mux = gst::ElementFactory::make("cmafmux")
|
||||
.property("movie-timescale", 0)
|
||||
.property("fragment-duration", 1.mseconds())
|
||||
.build()?;
|
||||
let appsink = gst_app::AppSink::builder().buffer_list(true).build();
|
||||
|
||||
pipeline.add_many([
|
||||
&src,
|
||||
&raw_capsfilter,
|
||||
&timeoverlay,
|
||||
&enc,
|
||||
&h264_capsfilter,
|
||||
&mux,
|
||||
appsink.upcast_ref(),
|
||||
])?;
|
||||
|
||||
gst::Element::link_many([
|
||||
&src,
|
||||
&raw_capsfilter,
|
||||
&timeoverlay,
|
||||
&enc,
|
||||
&h264_capsfilter,
|
||||
&mux,
|
||||
appsink.upcast_ref(),
|
||||
])?;
|
||||
|
||||
probe_encoder(state, enc);
|
||||
|
||||
setup_appsink(&appsink, &self.name, path, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioStream {
|
||||
fn setup(
|
||||
&self,
|
||||
state: Arc<Mutex<State>>,
|
||||
pipeline: &gst::Pipeline,
|
||||
path: &Path,
|
||||
) -> Result<(), Error> {
|
||||
let src = gst::ElementFactory::make("audiotestsrc")
|
||||
.property("is-live", true)
|
||||
.property_from_str("wave", &self.wave)
|
||||
.build()?;
|
||||
let enc = gst::ElementFactory::make("avenc_aac").build()?;
|
||||
let mux = gst::ElementFactory::make("cmafmux")
|
||||
.property("fragment-duration", 1.mseconds())
|
||||
.property("movie-timescale", 0)
|
||||
.build()?;
|
||||
let appsink = gst_app::AppSink::builder().buffer_list(true).build();
|
||||
|
||||
pipeline.add_many([&src, &enc, &mux, appsink.upcast_ref()])?;
|
||||
|
||||
gst::Element::link_many([&src, &enc, &mux, appsink.upcast_ref()])?;
|
||||
|
||||
probe_encoder(state, enc);
|
||||
|
||||
setup_appsink(&appsink, &self.name, path, false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
gst::init()?;
|
||||
|
||||
gstfmp4::plugin_register_static()?;
|
||||
|
||||
let path = PathBuf::from("hls_live_stream");
|
||||
|
||||
let pipeline = gst::Pipeline::default();
|
||||
|
||||
std::fs::create_dir_all(&path).expect("failed to create directory");
|
||||
|
||||
let mut manifest_path = path.clone();
|
||||
manifest_path.push("manifest.m3u8");
|
||||
|
||||
let state = Arc::new(Mutex::new(State {
|
||||
video_streams: vec![VideoStream {
|
||||
name: "video_0".to_string(),
|
||||
bitrate: 2_048_000,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
}],
|
||||
audio_streams: vec![
|
||||
AudioStream {
|
||||
name: "audio_0".to_string(),
|
||||
lang: "eng".to_string(),
|
||||
default: true,
|
||||
wave: "sine".to_string(),
|
||||
},
|
||||
AudioStream {
|
||||
name: "audio_1".to_string(),
|
||||
lang: "fre".to_string(),
|
||||
default: false,
|
||||
wave: "white-noise".to_string(),
|
||||
},
|
||||
],
|
||||
all_mimes: vec![],
|
||||
path: manifest_path.clone(),
|
||||
wrote_manifest: false,
|
||||
}));
|
||||
|
||||
{
|
||||
let state_lock = state.lock().unwrap();
|
||||
|
||||
for stream in &state_lock.video_streams {
|
||||
stream.setup(state.clone(), &pipeline, &path)?;
|
||||
}
|
||||
|
||||
for stream in &state_lock.audio_streams {
|
||||
stream.setup(state.clone(), &pipeline, &path)?;
|
||||
}
|
||||
}
|
||||
|
||||
pipeline.set_state(gst::State::Playing)?;
|
||||
|
||||
let bus = pipeline
|
||||
.bus()
|
||||
.expect("Pipeline without bus. Shouldn't happen!");
|
||||
|
||||
for msg in bus.iter_timed(gst::ClockTime::NONE) {
|
||||
use gst::MessageView;
|
||||
|
||||
match msg.view() {
|
||||
MessageView::Eos(..) => {
|
||||
println!("EOS");
|
||||
break;
|
||||
}
|
||||
MessageView::Error(err) => {
|
||||
pipeline.set_state(gst::State::Null)?;
|
||||
eprintln!(
|
||||
"Got error from {}: {} ({})",
|
||||
msg.src()
|
||||
.map(|s| String::from(s.path_string()))
|
||||
.unwrap_or_else(|| "None".into()),
|
||||
err.error(),
|
||||
err.debug().unwrap_or_else(|| "".into()),
|
||||
);
|
||||
break;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
pipeline.set_state(gst::State::Null)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user