use std::ffi::CString;
use ffmpeg_next as ffmpeg;
use crate::Error;
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct Config {
pub device: Option<String>,
pub width: Option<u32>,
pub height: Option<u32>,
pub framerate: Option<u32>,
}
pub(crate) struct Camera {
input: ffmpeg::format::context::Input,
decoder: ffmpeg::decoder::Video,
stream_index: usize,
url: String,
framerate: Option<u32>,
}
impl Camera {
pub fn open(config: &Config) -> Result<Self, Error> {
ffmpeg::init()?;
ffmpeg::device::register_all();
let backend = Backend::current();
let url = backend.url(config.device.as_deref());
let input_format = find_input_format(backend.format_name)?;
let mut opts = ffmpeg::Dictionary::new();
if let (Some(w), Some(h)) = (config.width, config.height) {
opts.set("video_size", &format!("{w}x{h}"));
}
if let Some(fps) = config.framerate {
opts.set("framerate", &fps.to_string());
}
let ctx = ffmpeg::format::open_with(&url, &input_format, opts)?;
let input = match ctx {
ffmpeg::format::context::Context::Input(input) => input,
ffmpeg::format::context::Context::Output(_) => {
return Err(Error::NoVideoStream(url));
}
};
let stream = input
.streams()
.best(ffmpeg::media::Type::Video)
.ok_or_else(|| Error::NoVideoStream(url.clone()))?;
let stream_index = stream.index();
let framerate = stream_framerate(&stream);
let decoder = ffmpeg::codec::context::Context::from_parameters(stream.parameters())?
.decoder()
.video()?;
tracing::info!(
device = %url,
backend = backend.format_name,
width = decoder.width(),
height = decoder.height(),
framerate,
"opened camera"
);
Ok(Self {
input,
decoder,
stream_index,
url,
framerate,
})
}
pub fn width(&self) -> u32 {
self.decoder.width()
}
pub fn height(&self) -> u32 {
self.decoder.height()
}
pub fn framerate(&self) -> Option<u32> {
self.framerate
}
pub fn read(&mut self) -> Result<Option<ffmpeg::frame::Video>, Error> {
let mut frame = ffmpeg::frame::Video::empty();
loop {
match self.decoder.receive_frame(&mut frame) {
Ok(()) => return Ok(Some(frame)),
Err(ffmpeg::Error::Other { errno }) if errno == ffmpeg::util::error::EAGAIN => {}
Err(ffmpeg::Error::Eof) => return Ok(None),
Err(e) => return Err(e.into()),
}
let packet = {
let mut packets = self.input.packets();
loop {
match packets.next() {
Some((stream, packet)) if stream.index() == self.stream_index => break Some(packet),
Some(_) => continue,
None => break None,
}
}
};
match packet {
Some(packet) => self.decoder.send_packet(&packet)?,
None => {
self.decoder.send_eof()?;
return match self.decoder.receive_frame(&mut frame) {
Ok(()) => Ok(Some(frame)),
Err(ffmpeg::Error::Eof) => Ok(None),
Err(ffmpeg::Error::Other { errno }) if errno == ffmpeg::util::error::EAGAIN => Ok(None),
Err(e) => Err(e.into()),
};
}
}
}
}
pub fn device(&self) -> &str {
&self.url
}
}
struct Backend {
format_name: &'static str,
}
impl Backend {
#[cfg(target_os = "macos")]
fn current() -> Self {
Self {
format_name: "avfoundation",
}
}
#[cfg(target_os = "linux")]
fn current() -> Self {
Self { format_name: "v4l2" }
}
#[cfg(target_os = "windows")]
fn current() -> Self {
Self { format_name: "dshow" }
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
fn current() -> Self {
Self {
format_name: "avfoundation",
}
}
fn url(&self, device: Option<&str>) -> String {
match self.format_name {
"avfoundation" => {
let video = device.unwrap_or("default");
if video.contains(':') {
video.to_string()
} else {
format!("{video}:none")
}
}
"v4l2" => device.unwrap_or("/dev/video0").to_string(),
"dshow" => format!("video={}", device.unwrap_or("")),
_ => device.unwrap_or("default").to_string(),
}
}
}
fn stream_framerate(stream: &ffmpeg::format::stream::Stream) -> Option<u32> {
for rate in [stream.avg_frame_rate(), stream.rate()] {
let (num, den) = (rate.numerator(), rate.denominator());
if num > 0 && den > 0 {
let fps = (num as f64 / den as f64).round();
if fps >= 1.0 {
return Some(fps as u32);
}
}
}
None
}
fn find_input_format(name: &str) -> Result<ffmpeg::format::format::Format, Error> {
let cname = CString::new(name).expect("format name has no interior NUL");
let ptr = unsafe { ffmpeg::ffi::av_find_input_format(cname.as_ptr()) };
if ptr.is_null() {
return Err(match name {
"avfoundation" => Error::NoCaptureBackend("avfoundation"),
"v4l2" => Error::NoCaptureBackend("v4l2"),
"dshow" => Error::NoCaptureBackend("dshow"),
_ => Error::NoCaptureBackend("camera"),
});
}
let input = unsafe { ffmpeg::format::Input::wrap(ptr as *mut _) };
Ok(ffmpeg::format::format::Format::Input(input))
}