2020-03-01 22:53:41 -08:00
|
|
|
// This file is part of Moonfire NVR, a security camera network video recorder.
|
2021-02-17 13:28:48 -08:00
|
|
|
// Copyright (C) 2016 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
|
|
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
|
2016-11-25 14:34:00 -08:00
|
|
|
|
2018-12-28 12:21:49 -06:00
|
|
|
use crate::h264;
|
2020-11-23 00:23:03 -08:00
|
|
|
use cstr::cstr;
|
2021-02-16 22:15:54 -08:00
|
|
|
use failure::{bail, Error};
|
2018-12-28 21:53:29 -06:00
|
|
|
use ffmpeg;
|
|
|
|
use lazy_static::lazy_static;
|
2020-08-14 21:21:59 -07:00
|
|
|
use log::{debug, warn};
|
2020-03-28 00:59:25 -07:00
|
|
|
use std::convert::TryFrom;
|
2019-07-04 23:22:45 -05:00
|
|
|
use std::ffi::CString;
|
2016-11-25 14:34:00 -08:00
|
|
|
use std::result::Result;
|
|
|
|
|
2019-07-24 21:52:55 -07:00
|
|
|
static START: parking_lot::Once = parking_lot::Once::new();
|
2016-11-25 14:34:00 -08:00
|
|
|
|
2016-12-06 18:41:44 -08:00
|
|
|
lazy_static! {
|
|
|
|
pub static ref FFMPEG: Ffmpeg = Ffmpeg::new();
|
|
|
|
}
|
|
|
|
|
|
|
|
pub enum Source<'a> {
|
2019-02-13 22:34:19 -08:00
|
|
|
/// A filename, for testing.
|
2016-11-25 14:34:00 -08:00
|
|
|
#[cfg(test)]
|
2019-02-13 22:34:19 -08:00
|
|
|
File(&'a str),
|
2016-11-25 14:34:00 -08:00
|
|
|
|
2019-02-13 22:34:19 -08:00
|
|
|
/// An RTSP stream, for production use.
|
2021-02-16 22:15:54 -08:00
|
|
|
Rtsp { url: &'a str, redacted_url: &'a str },
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
|
2021-02-16 22:15:54 -08:00
|
|
|
pub trait Opener<S: Stream>: Sync {
|
2016-12-06 18:41:44 -08:00
|
|
|
fn open(&self, src: Source) -> Result<S, Error>;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub trait Stream {
|
2020-04-13 23:03:49 -07:00
|
|
|
fn get_video_codecpar(&self) -> ffmpeg::avcodec::InputCodecParameters<'_>;
|
2016-12-06 18:41:44 -08:00
|
|
|
fn get_extra_data(&self) -> Result<h264::ExtraData, Error>;
|
2020-03-28 00:59:25 -07:00
|
|
|
fn get_next<'p>(&'p mut self) -> Result<ffmpeg::avcodec::Packet<'p>, ffmpeg::Error>;
|
2016-12-06 18:41:44 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
pub struct Ffmpeg {}
|
|
|
|
|
|
|
|
impl Ffmpeg {
|
|
|
|
fn new() -> Ffmpeg {
|
2021-02-16 22:15:54 -08:00
|
|
|
START.call_once(|| {
|
|
|
|
ffmpeg::Ffmpeg::new();
|
|
|
|
});
|
|
|
|
Ffmpeg {}
|
2016-12-06 18:41:44 -08:00
|
|
|
}
|
|
|
|
}
|
2016-11-25 14:34:00 -08:00
|
|
|
|
2016-12-06 18:41:44 -08:00
|
|
|
impl Opener<FfmpegStream> for Ffmpeg {
|
|
|
|
fn open(&self, src: Source) -> Result<FfmpegStream, Error> {
|
2020-03-28 00:59:25 -07:00
|
|
|
use ffmpeg::avformat::InputFormatContext;
|
2020-08-14 21:21:59 -07:00
|
|
|
let mut input = match src {
|
2016-11-25 14:34:00 -08:00
|
|
|
#[cfg(test)]
|
2017-10-09 21:44:48 -07:00
|
|
|
Source::File(filename) => {
|
2020-03-28 00:59:25 -07:00
|
|
|
let mut open_options = ffmpeg::avutil::Dictionary::new();
|
2017-10-09 21:44:48 -07:00
|
|
|
|
|
|
|
// Work around https://github.com/scottlamb/moonfire-nvr/issues/10
|
2021-02-16 22:15:54 -08:00
|
|
|
open_options
|
|
|
|
.set(cstr!("advanced_editlist"), cstr!("false"))
|
|
|
|
.unwrap();
|
2017-10-09 21:44:48 -07:00
|
|
|
let url = format!("file:{}", filename);
|
2021-02-16 22:15:54 -08:00
|
|
|
let i = InputFormatContext::open(
|
|
|
|
&CString::new(url.clone()).unwrap(),
|
|
|
|
&mut open_options,
|
|
|
|
)?;
|
2017-10-09 21:44:48 -07:00
|
|
|
if !open_options.empty() {
|
2021-02-16 22:15:54 -08:00
|
|
|
warn!(
|
|
|
|
"While opening URL {}, some options were not understood: {}",
|
|
|
|
url, open_options
|
|
|
|
);
|
2017-10-09 21:44:48 -07:00
|
|
|
}
|
2020-08-14 21:21:59 -07:00
|
|
|
i
|
2017-10-09 21:44:48 -07:00
|
|
|
}
|
2021-02-16 22:15:54 -08:00
|
|
|
Source::Rtsp { url, redacted_url } => {
|
2020-03-28 00:59:25 -07:00
|
|
|
let mut open_options = ffmpeg::avutil::Dictionary::new();
|
2021-02-16 22:15:54 -08:00
|
|
|
open_options
|
|
|
|
.set(cstr!("rtsp_transport"), cstr!("tcp"))
|
|
|
|
.unwrap();
|
|
|
|
open_options
|
|
|
|
.set(cstr!("user-agent"), cstr!("moonfire-nvr"))
|
|
|
|
.unwrap();
|
2020-08-14 21:21:59 -07:00
|
|
|
|
2017-09-20 21:06:06 -07:00
|
|
|
// 10-second socket timeout, in microseconds.
|
2021-02-16 22:15:54 -08:00
|
|
|
open_options
|
|
|
|
.set(cstr!("stimeout"), cstr!("10000000"))
|
|
|
|
.unwrap();
|
2019-02-11 22:58:09 -08:00
|
|
|
|
2020-08-14 21:21:59 -07:00
|
|
|
// Without this option, the first packet has an incorrect pts.
|
|
|
|
// https://trac.ffmpeg.org/ticket/5018
|
2021-02-16 22:15:54 -08:00
|
|
|
open_options
|
|
|
|
.set(cstr!("fflags"), cstr!("nobuffer"))
|
|
|
|
.unwrap();
|
2020-08-14 21:21:59 -07:00
|
|
|
|
2019-02-11 22:58:09 -08:00
|
|
|
// Moonfire NVR currently only supports video, so receiving audio is wasteful.
|
|
|
|
// It also triggers <https://github.com/scottlamb/moonfire-nvr/issues/36>.
|
2021-02-16 22:15:54 -08:00
|
|
|
open_options
|
|
|
|
.set(cstr!("allowed_media_types"), cstr!("video"))
|
|
|
|
.unwrap();
|
2019-02-11 22:58:09 -08:00
|
|
|
|
2017-09-20 21:06:06 -07:00
|
|
|
let i = InputFormatContext::open(&CString::new(url).unwrap(), &mut open_options)?;
|
|
|
|
if !open_options.empty() {
|
2021-02-16 22:15:54 -08:00
|
|
|
warn!(
|
|
|
|
"While opening URL {}, some options were not understood: {}",
|
|
|
|
redacted_url, open_options
|
|
|
|
);
|
2017-09-20 21:06:06 -07:00
|
|
|
}
|
2020-08-14 21:21:59 -07:00
|
|
|
i
|
2021-02-16 22:15:54 -08:00
|
|
|
}
|
2016-11-25 14:34:00 -08:00
|
|
|
};
|
|
|
|
|
2017-09-20 21:06:06 -07:00
|
|
|
input.find_stream_info()?;
|
|
|
|
|
2016-11-25 14:34:00 -08:00
|
|
|
// Find the video stream.
|
|
|
|
let mut video_i = None;
|
2017-09-20 21:06:06 -07:00
|
|
|
{
|
|
|
|
let s = input.streams();
|
2021-02-16 22:15:54 -08:00
|
|
|
for i in 0..s.len() {
|
2019-12-29 08:35:39 -06:00
|
|
|
if s.get(i).codecpar().codec_type().is_video() {
|
2017-09-20 21:06:06 -07:00
|
|
|
debug!("Video stream index is {}", i);
|
|
|
|
video_i = Some(i);
|
|
|
|
break;
|
|
|
|
}
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
let video_i = match video_i {
|
|
|
|
Some(i) => i,
|
2018-02-20 22:46:14 -08:00
|
|
|
None => bail!("no video stream"),
|
2016-11-25 14:34:00 -08:00
|
|
|
};
|
|
|
|
|
2021-02-16 22:15:54 -08:00
|
|
|
Ok(FfmpegStream { input, video_i })
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-06 18:41:44 -08:00
|
|
|
pub struct FfmpegStream {
|
2020-04-13 23:03:49 -07:00
|
|
|
input: ffmpeg::avformat::InputFormatContext<'static>,
|
2016-11-25 14:34:00 -08:00
|
|
|
video_i: usize,
|
|
|
|
}
|
|
|
|
|
2016-12-06 18:41:44 -08:00
|
|
|
impl Stream for FfmpegStream {
|
2020-04-13 23:03:49 -07:00
|
|
|
fn get_video_codecpar(&self) -> ffmpeg::avcodec::InputCodecParameters {
|
|
|
|
self.input.streams().get(self.video_i).codecpar()
|
|
|
|
}
|
|
|
|
|
2016-12-06 18:41:44 -08:00
|
|
|
fn get_extra_data(&self) -> Result<h264::ExtraData, Error> {
|
2017-09-20 21:06:06 -07:00
|
|
|
let video = self.input.streams().get(self.video_i);
|
|
|
|
let tb = video.time_base();
|
|
|
|
if tb.num != 1 || tb.den != 90000 {
|
2021-02-16 22:15:54 -08:00
|
|
|
bail!(
|
|
|
|
"video stream has timebase {}/{}; expected 1/90000",
|
|
|
|
tb.num,
|
|
|
|
tb.den
|
|
|
|
);
|
2017-09-20 21:06:06 -07:00
|
|
|
}
|
2019-12-29 08:35:39 -06:00
|
|
|
let codec = video.codecpar();
|
2017-09-20 21:06:06 -07:00
|
|
|
let codec_id = codec.codec_id();
|
|
|
|
if !codec_id.is_h264() {
|
2018-02-20 22:46:14 -08:00
|
|
|
bail!("stream's video codec {:?} is not h264", codec_id);
|
2017-09-20 21:06:06 -07:00
|
|
|
}
|
2020-03-28 00:59:25 -07:00
|
|
|
let dims = codec.dims();
|
2021-02-16 22:15:54 -08:00
|
|
|
h264::ExtraData::parse(
|
|
|
|
codec.extradata(),
|
|
|
|
u16::try_from(dims.width)?,
|
|
|
|
u16::try_from(dims.height)?,
|
|
|
|
)
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
|
2020-03-28 00:59:25 -07:00
|
|
|
fn get_next<'i>(&'i mut self) -> Result<ffmpeg::avcodec::Packet<'i>, ffmpeg::Error> {
|
2016-11-25 14:34:00 -08:00
|
|
|
loop {
|
2017-09-20 21:06:06 -07:00
|
|
|
let p = self.input.read_frame()?;
|
|
|
|
if p.stream_index() == self.video_i {
|
|
|
|
return Ok(p);
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|