WIP: ffmpeg example (#19)
Unfortunately, the images don't look right! I don't know yet what's wrong.
This commit is contained in:
parent
bb3baf0329
commit
c4b83f1eb9
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -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
133
Cargo.lock
generated
@ -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",
|
||||
|
@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
members = [".", "examples/client", "examples/webrtc-proxy"]
|
||||
members = [".", "examples/client", "examples/ffmpeg-decode", "examples/webrtc-proxy"]
|
||||
default-members = ["."]
|
||||
|
||||
[package]
|
||||
|
18
examples/ffmpeg-decode/Cargo.toml
Normal file
18
examples/ffmpeg-decode/Cargo.toml
Normal 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"
|
253
examples/ffmpeg-decode/src/main.rs
Normal file
253
examples/ffmpeg-decode/src/main.rs
Normal 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(())
|
||||
}
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user