diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a50d29e..228a42b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,8 @@ jobs: toolchain: ${{ matrix.rust }} override: true components: ${{ matrix.extra_components }} + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install libavcodec-dev libavformat-dev libavutil-dev libswscale-dev pkgconf - name: Build run: cargo build --all-features --all-targets --workspace - name: Test diff --git a/Cargo.lock b/Cargo.lock index c64e4ca..90dac5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,6 +183,25 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.59.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -306,6 +325,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.1", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -343,6 +371,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "clang-sys" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "2.34.0" @@ -865,6 +904,46 @@ dependencies = [ "subtle", ] +[[package]] +name = "ffmpeg-decode" +version = "0.0.0" +dependencies = [ + "anyhow", + "ffmpeg-next", + "futures", + "log", + "mylog", + "retina", + "structopt", + "tokio", + "url", +] + +[[package]] +name = "ffmpeg-next" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585e5eaf57eceaa199ba6f6a1f46bdad7992930bb7b45b40275d9445b5ba2bc8" +dependencies = [ + "bitflags", + "ffmpeg-sys-next", + "libc", +] + +[[package]] +name = "ffmpeg-sys-next" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba12dea33516e30c160ce557c7e43dd857276368eb1cd0eef4fce6529f2dee5" +dependencies = [ + "bindgen", + "cc", + "libc", + "num_cpus", + "pkg-config", + "vcpkg", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1034,6 +1113,12 @@ dependencies = [ "polyval 0.5.3", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "group" version = "0.11.0" @@ -1206,6 +1291,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "lexical-core" version = "0.7.6" @@ -1225,6 +1316,16 @@ version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +[[package]] +name = "libloading" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "linux-raw-sys" version = "0.0.46" @@ -1491,6 +1592,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "pem" version = "1.1.0" @@ -1549,6 +1656,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + [[package]] name = "plotters" version = "0.3.2" @@ -1879,6 +1992,12 @@ dependencies = [ "url", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.0" @@ -2112,6 +2231,12 @@ dependencies = [ "digest 0.10.3", ] +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -2516,6 +2641,12 @@ dependencies = [ "getrandom 0.2.7", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vec_map" version = "0.8.2" @@ -2784,7 +2915,7 @@ dependencies = [ [[package]] name = "webrtc-proxy" -version = "0.1.0" +version = "0.0.0" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index e4d1cd3..aae4a8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [".", "examples/client", "examples/webrtc-proxy"] +members = [".", "examples/client", "examples/ffmpeg-decode", "examples/webrtc-proxy"] default-members = ["."] [package] diff --git a/examples/ffmpeg-decode/Cargo.toml b/examples/ffmpeg-decode/Cargo.toml new file mode 100644 index 0000000..a45e97a --- /dev/null +++ b/examples/ffmpeg-decode/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "ffmpeg-decode" +version = "0.0.0" +edition = "2021" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.41" +ffmpeg-next = { version = "5.0.3", default-features=false, features = ["codec", "format", "software-scaling"] } +futures = "0.3.14" +log = "0.4.8" +mylog = { git = "https://github.com/scottlamb/mylog" } +retina = { path = "../../" } +structopt = "0.3.21" +tokio = { version = "1.5.0", features = ["macros", "rt-multi-thread", "signal"] } +url = "2.2.1" diff --git a/examples/ffmpeg-decode/src/main.rs b/examples/ffmpeg-decode/src/main.rs new file mode 100644 index 0000000..268cdbb --- /dev/null +++ b/examples/ffmpeg-decode/src/main.rs @@ -0,0 +1,253 @@ +// Copyright (C) 2022 Scott Lamb +// SPDX-License-Identifier: MIT OR Apache-2.0 + +extern crate ffmpeg_next as ffmpeg; + +use anyhow::{anyhow, bail, Error}; +use futures::StreamExt; +use log::{error, info}; +use retina::{ + client::SetupOptions, + codec::{CodecItem, ParametersRef, VideoFrame, VideoParameters}, +}; +use std::{fs::File, io::Write, str::FromStr, sync::Arc}; +use structopt::StructOpt; + +/// Decodes H.264 streams using ffmpeg, writing them into `frame.ppm` images. +#[derive(StructOpt)] +struct Opts { + /// `rtsp://` URL to connect to. + #[structopt(long, parse(try_from_str))] + url: url::Url, + + /// Username to send if the server requires authentication. + #[structopt(long)] + username: Option, + + /// Password; requires username. + #[structopt(long, requires = "username")] + password: Option, + + /// When to issue a `TEARDOWN` request: `auto`, `always`, or `never`. + #[structopt(default_value, long)] + teardown: retina::client::TeardownPolicy, + + /// The transport to use: `tcp` or `udp` (experimental). + #[structopt(default_value, long)] + transport: retina::client::Transport, +} + +fn init_logging() -> mylog::Handle { + let h = mylog::Builder::new() + .set_format( + ::std::env::var("MOONFIRE_FORMAT") + .map_err(|_| ()) + .and_then(|s| mylog::Format::from_str(&s)) + .unwrap_or(mylog::Format::Google), + ) + .set_spec(::std::env::var("MOONFIRE_LOG").as_deref().unwrap_or("info")) + .build(); + h.clone().install().unwrap(); + h +} + +#[tokio::main] +async fn main() { + let mut h = init_logging(); + if let Err(e) = { + let _a = h.async_scope(); + run().await + } { + error!("{}", e); + std::process::exit(1); + } +} + +struct H264Processor { + decoder: ffmpeg::codec::decoder::Video, + scaler: Option, + frame_i: u64, +} + +impl H264Processor { + fn new() -> Self { + let mut codec_opts = ffmpeg::Dictionary::new(); + codec_opts.set("is_avc", "1"); + let codec = ffmpeg::codec::decoder::find(ffmpeg::codec::Id::H264).unwrap(); + let decoder = ffmpeg::codec::decoder::Decoder(ffmpeg::codec::Context::new()) + .open_as_with(codec, codec_opts) + .unwrap() + .video() + .unwrap(); + Self { + decoder, + scaler: None, + frame_i: 0, + } + } + + fn handle_parameters(&mut self, p: &VideoParameters) -> Result<(), Error> { + let pkt = ffmpeg::codec::packet::Packet::borrow(p.extra_data()); + self.decoder.send_packet(&pkt)?; + + // ffmpeg doesn't appear to actually handle the parameters until the + // first full frame, so just note that the scaler needs to be + // (re)created. + self.scaler = None; + Ok(()) + } + + fn send_frame(&mut self, f: &VideoFrame) -> Result<(), Error> { + let pkt = ffmpeg::codec::packet::Packet::borrow(f.data()); + self.decoder.send_packet(&pkt)?; + self.receive_frames()?; + Ok(()) + } + + fn flush(&mut self) -> Result<(), Error> { + self.decoder.send_eof()?; + self.receive_frames()?; + Ok(()) + } + + fn receive_frames(&mut self) -> Result<(), Error> { + let mut decoded = ffmpeg::util::frame::video::Video::empty(); + loop { + match self.decoder.receive_frame(&mut decoded) { + Err(ffmpeg::Error::Other { + errno: ffmpeg::util::error::EAGAIN, + }) => { + // No complete frame available. + break; + } + Err(e) => bail!(e), + Ok(()) => {} + } + + // This frame writing logic lifted from ffmpeg-next's examples/dump-frames.rs. + let scaler = self.scaler.get_or_insert_with(|| { + info!( + "image parameters: {:?}, {}x{}", + self.decoder.format(), + self.decoder.width(), + self.decoder.height() + ); + ffmpeg::software::scaling::Context::get( + self.decoder.format(), + self.decoder.width(), + self.decoder.height(), + ffmpeg::format::Pixel::RGB24, + 320, + 240, + ffmpeg::software::scaling::Flags::BILINEAR, + ) + .unwrap() + }); + let mut scaled = ffmpeg::util::frame::video::Video::empty(); + scaler.run(&decoded, &mut scaled)?; + let filename = format!("frame{}.ppm", self.frame_i); + info!("writing {}", &filename); + let mut file = File::create(filename)?; + file.write_all( + format!("P6\n{} {}\n255\n", scaled.width(), scaled.height()).as_bytes(), + )?; + file.write_all(decoded.data(0))?; + self.frame_i += 1; + } + Ok(()) + } +} + +async fn run() -> Result<(), Error> { + let opts = Opts::from_args(); + ffmpeg::init().unwrap(); + ffmpeg::util::log::set_level(ffmpeg::util::log::Level::Trace); + let creds = match (opts.username, opts.password) { + (Some(username), password) => Some(retina::client::Credentials { + username, + password: password.unwrap_or_default(), + }), + (None, None) => None, + _ => unreachable!(), // structopt/clap enforce that password requires username. + }; + let stop_signal = tokio::signal::ctrl_c(); + tokio::pin!(stop_signal); + let upstream_session_group = Arc::new(retina::client::SessionGroup::default()); + let mut session = retina::client::Session::describe( + opts.url.clone(), + retina::client::SessionOptions::default() + .creds(creds) + .session_group(upstream_session_group.clone()) + .user_agent("Retina ffmpeg-decode example".to_owned()) + .teardown(opts.teardown), + ) + .await?; + + let video_stream_i = session + .streams() + .iter() + .position(|s| { + if s.media() == "video" { + if s.encoding_name() == "h264" { + log::info!("Using h264 video stream"); + return true; + } + log::info!( + "Ignoring {} video stream because it's unsupported", + s.encoding_name(), + ); + } + false + }) + .ok_or_else(|| anyhow!("No h264 video stream found"))?; + let mut processor = H264Processor::new(); + session + .setup( + video_stream_i, + SetupOptions::default().transport(opts.transport.clone()), + ) + .await?; + + let mut session = session + .play(retina::client::PlayOptions::default().ignore_zero_seq(true)) + .await? + .demuxed()?; + + if let Some(ParametersRef::Video(v)) = session.streams()[video_stream_i].parameters() { + processor.handle_parameters(v)?; + } + + loop { + tokio::select! { + item = session.next() => { + match item { + Some(Ok(CodecItem::VideoFrame(f))) => { + if f.has_new_parameters() { + let v = match session.streams()[video_stream_i].parameters() { + Some(ParametersRef::Video(v)) => v, + _ => unreachable!(), + }; + processor.handle_parameters(v)?; + } + processor.send_frame(&f)?; + }, + Some(Ok(_)) => {}, + Some(Err(e)) => { + return Err(anyhow!(e).context("RTSP failure")); + } + None => { + info!("EOF"); + break; + } + } + }, + _ = &mut stop_signal => { + info!("received ctrl-C"); + break; + }, + } + } + + processor.flush()?; + Ok(()) +} diff --git a/examples/webrtc-proxy/Cargo.toml b/examples/webrtc-proxy/Cargo.toml index db29849..c76bbba 100644 --- a/examples/webrtc-proxy/Cargo.toml +++ b/examples/webrtc-proxy/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "webrtc-proxy" -version = "0.1.0" +version = "0.0.0" edition = "2021" +publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html