WIP: ffmpeg example (#19)

Unfortunately, the images don't look right! I don't know yet what's
wrong.
This commit is contained in:
Scott Lamb 2022-07-19 12:17:45 -07:00
parent bb3baf0329
commit c4b83f1eb9
6 changed files with 408 additions and 3 deletions

View File

@ -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

133
Cargo.lock generated
View File

@ -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",

View File

@ -1,5 +1,5 @@
[workspace]
members = [".", "examples/client", "examples/webrtc-proxy"]
members = [".", "examples/client", "examples/ffmpeg-decode", "examples/webrtc-proxy"]
default-members = ["."]
[package]

View File

@ -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"

View File

@ -0,0 +1,253 @@
// Copyright (C) 2022 Scott Lamb <slamb@slamb.org>
// 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<i>.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<String>,
/// Password; requires username.
#[structopt(long, requires = "username")]
password: Option<String>,
/// 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<ffmpeg::software::scaling::Context>,
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(())
}

View File

@ -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