diff --git a/Cargo.lock b/Cargo.lock index 574ff62..d86b415 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,6 +131,45 @@ dependencies = [ "winapi", ] +[[package]] +name = "clap" +version = "3.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aad2534fad53df1cc12519c5cda696dd3e20e6118a027e24054aea14a0bdcbe" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "lazy_static", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3aab4734e083b809aaf5794e14e756d1c798d2c69c7f7de7a09a2f5214993c1" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "189ddd3b5d32a70b35e7686054371742a937b0d99128e76dde6340210e966669" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "color-name" version = "1.1.0" @@ -254,6 +293,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + [[package]] name = "futures-channel" version = "0.3.21" @@ -500,6 +549,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + [[package]] name = "heck" version = "0.4.0" @@ -524,6 +579,17 @@ dependencies = [ "quick-error", ] +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "image" version = "0.23.14" @@ -562,6 +628,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +[[package]] +name = "indexmap" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "jpeg-decoder" version = "0.1.22" @@ -592,6 +668,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + [[package]] name = "memchr" version = "2.4.1" @@ -732,12 +814,24 @@ dependencies = [ "paste", ] +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" + [[package]] name = "paste" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + [[package]] name = "pin-project-lite" version = "0.2.8" @@ -983,6 +1077,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3ff2f71c82567c565ba4b3009a9350a96a7269eaa4001ebedae926230bc2254" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.91" @@ -1016,6 +1116,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + [[package]] name = "thiserror" version = "1.0.30" @@ -1047,6 +1153,21 @@ dependencies = [ "weezl", ] +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + [[package]] name = "toml" version = "0.5.8" @@ -1072,12 +1193,39 @@ dependencies = [ "strength_reduce", ] +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-xid" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + [[package]] name = "version-compare" version = "0.1.0" @@ -1094,6 +1242,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "webvttconverter" version = "0.1.0" dependencies = [ + "clap", "ctrlc", "eyre", "glib", @@ -1103,6 +1252,7 @@ dependencies = [ "log", "pretty_env_logger", "signal-hook", + "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 22a25f5..b18e8ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,5 +15,7 @@ signal-hook = "0.3.13" #axum = "0.4.5" #tower = "0.4.12" #tower-http = { version = "0.2.2", features = ["add-extension"] } +clap = { version = "3" , features = ["derive"] } +url = "2" log = "0.4.14" pretty_env_logger = "0.4.0" diff --git a/src/main.rs b/src/main.rs index 48a2e95..fd741d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,13 @@ use glib::translate::ToGlibPtr; use gst::prelude::*; -use log::{debug, info}; +use log::{debug, info, trace}; use std::fs::File; use std::io::Write; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; +use clap::Parser; +use url::Url; fn send_splice(element: &gst::Element, gst_sit: C) where @@ -49,6 +51,30 @@ impl EventId { } } +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Configuration { + /// HLS input source of the stream + #[clap(short='s', long)] + hls_source_url: String, + + /// RTP destination for the stream + #[clap(short='d', long)] + rtp_destination_url: String, + + /// Image of the frame used as reference to trigger the SCTE35 event + #[clap(short='i', long)] + slate_image_path: String, + + /// The SCTE35 stream PID + #[clap(short='p', long, default_value="500")] + scte_pid: u32, + + /// The duration of the SCTE35 Splice event in seconds + #[clap(short='d', long)] + scte_duration_secs: u64, +} + fn main() -> eyre::Result<()> { pretty_env_logger::init_timed(); gst::init()?; @@ -57,14 +83,15 @@ fn main() -> eyre::Result<()> { gst_mpegts::gst_mpegts_initialize(); } - // ! filesink sync=true location=video.ts + let conf: Configuration = Configuration::parse(); + let pipeline = gst::parse_launch( r#" - urisourcebin uri=https://plutolive-msl.akamaized.net/hls/live/2008623/defy/master.m3u8 ! tsdemux name=demux ! queue ! h264parse ! tee name=v + urisourcebin name=source ! tsdemux name=demux ! queue ! h264parse ! tee name=v - v. ! queue ! mpegtsmux name=mux scte-35-pid=500 scte-35-null-interval=450000 ! rtpmp2tpay ! udpsink sync=true host=54.225.215.79 port=5000 - v. ! queue ! decodebin ! videoconvert ! imgcmp name=imgcmp location=/Users/rafaelcaricio/Downloads/defy-AD-SLATE-APRIL3022.jpeg ! autovideosink + v. ! queue ! mpegtsmux name=mux scte-35-null-interval=450000 ! rtpmp2tpay ! udpsink name=rtpsink sync=true + v. ! queue ! decodebin ! videoconvert ! imgcmp name=imgcmp ! autovideosink demux. ! queue ! aacparse ! mux. "#, @@ -74,6 +101,28 @@ fn main() -> eyre::Result<()> { info!("Starting pipeline..."); + // Set the HLS source URL + let source = pipeline.by_name("source").unwrap(); + source.set_property("uri", conf.hls_source_url); + + // Set SCTE35 stream PID + let mux = pipeline.by_name("mux").unwrap(); + mux.set_property("scte-35-pid", conf.scte_pid); + + // Set the frame we are searching for + let imgcmp = pipeline.by_name("imgcmp").unwrap(); + imgcmp.set_property("location", conf.slate_image_path); + + // Set the RTP destination + let rtp_url = Url::parse(&conf.rtp_destination_url).expect("Valid URL in format rtp://:"); + let rtp_sink = pipeline.by_name("rtpsink").unwrap(); + rtp_sink.set_properties(&[ + ("host", &rtp_url.host_str().unwrap().to_string()), + ("port", &(rtp_url.port().unwrap() as i32)), + // In order to make sure the slate image is not event visible, we delay 1 second + ("ts-offset", &(gst::ClockTime::from_seconds(1).nseconds() as i64)), + ]); + let context = glib::MainContext::default(); let main_loop = glib::MainLoop::new(Some(&context), false); @@ -83,10 +132,12 @@ fn main() -> eyre::Result<()> { bus.add_watch({ let event_counter = EventId::default(); let ad_running = Arc::new(AtomicBool::new(false)); + let ad_duration = Duration::from_secs(conf.scte_duration_secs); + let main_loop = main_loop.clone(); let pipeline_weak = pipeline.downgrade(); - let imgcmp_weak = pipeline.by_name("imgcmp").unwrap().downgrade(); - let muxer_weak = pipeline.by_name("mux").unwrap().downgrade(); + let imgcmp_weak = imgcmp.downgrade(); + let muxer_weak = mux.downgrade(); move |_, msg| { use gst::MessageView; @@ -121,7 +172,7 @@ fn main() -> eyre::Result<()> { imgcmp_weak.upgrade(), muxer_weak.upgrade(), ) { - info!("Element Message: {:?}", elem_msg); + trace!("Element Message: {:?}", elem_msg); if elem_msg.src().map(|e| e == imgcmp).unwrap_or(false) && elem_msg.message().has_name("image-detected") && !ad_running.load(Ordering::Relaxed) @@ -130,24 +181,21 @@ fn main() -> eyre::Result<()> { // is, so we use the pipeline running time to base our timing calculations let now = pipeline.current_running_time().unwrap(); - // How much ahead should the ad be inserted, we say 0 seconds in the future (immediate) - let ahead = gst::ClockTime::from_seconds(0); - // Trigger the Splice Out event in the SCTE-35 stream - send_splice_out(&muxer, event_counter.next(), now + ahead); + send_splice_out(&muxer, event_counter.next(), now); ad_running.store(true, Ordering::Relaxed); info!("Ad started.."); // Now we add a timed call for the duration of the ad from now to indicate via // splice in that the stream can go back to normal programming. - glib::timeout_add(Duration::from_secs(30), { + glib::timeout_add(ad_duration, { let muxer_weak = muxer.downgrade(); let event_counter = event_counter.clone(); let ad_running = Arc::clone(&ad_running); move || { if let Some(muxer) = muxer_weak.upgrade() { let now = muxer.current_running_time().unwrap(); - send_splice_in(&muxer, event_counter.next(), now + ahead); + send_splice_in(&muxer, event_counter.next(), now); ad_running.store(false, Ordering::Relaxed); info!("Ad ended!") }