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-06-07 14:36:53 -07:00
|
|
|
use failure::format_err;
|
2021-02-16 22:15:54 -08:00
|
|
|
use failure::{bail, Error};
|
2021-06-07 14:36:53 -07:00
|
|
|
use futures::StreamExt;
|
2018-12-28 21:53:29 -06:00
|
|
|
use lazy_static::lazy_static;
|
2021-06-04 23:28:48 -07:00
|
|
|
use log::warn;
|
2021-06-07 14:36:53 -07:00
|
|
|
use retina::client::{Credentials, Playing, Session};
|
|
|
|
use retina::codec::{CodecItem, VideoParameters};
|
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;
|
2021-06-07 14:36:53 -07:00
|
|
|
use url::Url;
|
2016-11-25 14:34:00 -08:00
|
|
|
|
2021-06-07 14:36:53 -07:00
|
|
|
static START_FFMPEG: 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();
|
|
|
|
}
|
|
|
|
|
2021-06-07 14:36:53 -07:00
|
|
|
pub enum RtspLibrary {
|
|
|
|
Ffmpeg,
|
|
|
|
Retina,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl std::str::FromStr for RtspLibrary {
|
|
|
|
type Err = Error;
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
Ok(match s {
|
|
|
|
"ffmpeg" => RtspLibrary::Ffmpeg,
|
|
|
|
"retina" => RtspLibrary::Retina,
|
|
|
|
_ => bail!("unknown RTSP library {:?}", s),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl RtspLibrary {
|
|
|
|
pub fn opener(&self) -> &'static dyn Opener {
|
|
|
|
match self {
|
|
|
|
RtspLibrary::Ffmpeg => &*FFMPEG,
|
|
|
|
RtspLibrary::Retina => &RETINA,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
2016-12-06 18:41:44 -08:00
|
|
|
pub enum Source<'a> {
|
2019-02-13 22:34:19 -08:00
|
|
|
/// A filename, for testing.
|
|
|
|
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-06-07 14:36:53 -07:00
|
|
|
Rtsp {
|
|
|
|
url: Url,
|
|
|
|
username: Option<String>,
|
|
|
|
password: Option<String>,
|
|
|
|
},
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
|
2021-06-07 14:36:53 -07:00
|
|
|
#[cfg(not(test))]
|
|
|
|
pub enum Source {
|
|
|
|
/// An RTSP stream, for production use.
|
|
|
|
Rtsp {
|
|
|
|
url: Url,
|
|
|
|
username: Option<String>,
|
|
|
|
password: Option<String>,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
pub trait Opener: Send + Sync {
|
|
|
|
fn open(&self, src: Source) -> Result<(h264::ExtraData, Box<dyn Stream>), Error>;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct VideoFrame<'a> {
|
|
|
|
pub pts: i64,
|
|
|
|
|
|
|
|
/// An estimate of the duration of the frame, or zero.
|
|
|
|
/// This can be deceptive and is only used by some testing code.
|
|
|
|
pub duration: i32,
|
|
|
|
|
|
|
|
pub is_key: bool,
|
|
|
|
pub data: &'a [u8],
|
2016-12-06 18:41:44 -08:00
|
|
|
}
|
|
|
|
|
2021-06-07 14:36:53 -07:00
|
|
|
pub trait Stream: Send {
|
|
|
|
fn next(&mut self) -> Result<VideoFrame, Error>;
|
2016-12-06 18:41:44 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
pub struct Ffmpeg {}
|
|
|
|
|
|
|
|
impl Ffmpeg {
|
|
|
|
fn new() -> Ffmpeg {
|
2021-06-07 14:36:53 -07:00
|
|
|
START_FFMPEG.call_once(|| {
|
2021-02-16 22:15:54 -08:00
|
|
|
ffmpeg::Ffmpeg::new();
|
|
|
|
});
|
|
|
|
Ffmpeg {}
|
2016-12-06 18:41:44 -08:00
|
|
|
}
|
|
|
|
}
|
2016-11-25 14:34:00 -08:00
|
|
|
|
2021-06-07 14:36:53 -07:00
|
|
|
impl Opener for Ffmpeg {
|
|
|
|
fn open(&self, src: Source) -> Result<(h264::ExtraData, Box<dyn Stream>), 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-06-07 14:36:53 -07:00
|
|
|
Source::Rtsp {
|
|
|
|
url,
|
|
|
|
username,
|
|
|
|
password,
|
|
|
|
} => {
|
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
|
|
|
|
2021-06-07 14:36:53 -07:00
|
|
|
let mut url_with_credentials = url.clone();
|
|
|
|
if let Some(u) = username.as_deref() {
|
|
|
|
url_with_credentials
|
|
|
|
.set_username(u)
|
|
|
|
.map_err(|_| format_err!("unable to set username on url {}", url))?;
|
|
|
|
}
|
|
|
|
url_with_credentials
|
|
|
|
.set_password(password.as_deref())
|
|
|
|
.map_err(|_| format_err!("unable to set password on url {}", url))?;
|
|
|
|
let i = InputFormatContext::open(
|
|
|
|
&CString::new(url_with_credentials.as_str())?,
|
|
|
|
&mut open_options,
|
|
|
|
)?;
|
2017-09-20 21:06:06 -07:00
|
|
|
if !open_options.empty() {
|
2021-02-16 22:15:54 -08:00
|
|
|
warn!(
|
|
|
|
"While opening URL {}, some options were not understood: {}",
|
2021-06-07 14:36:53 -07:00
|
|
|
url, open_options
|
2021-02-16 22:15:54 -08:00
|
|
|
);
|
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
|
|
|
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-06-07 14:36:53 -07:00
|
|
|
let video = input.streams().get(video_i);
|
|
|
|
let codec = video.codecpar();
|
|
|
|
let codec_id = codec.codec_id();
|
|
|
|
if !codec_id.is_h264() {
|
|
|
|
bail!("stream's video codec {:?} is not h264", codec_id);
|
|
|
|
}
|
2017-09-20 21:06:06 -07:00
|
|
|
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
|
|
|
}
|
2020-03-28 00:59:25 -07:00
|
|
|
let dims = codec.dims();
|
2021-06-07 14:36:53 -07:00
|
|
|
let extra_data = h264::ExtraData::parse(
|
2021-02-16 22:15:54 -08:00
|
|
|
codec.extradata(),
|
|
|
|
u16::try_from(dims.width)?,
|
|
|
|
u16::try_from(dims.height)?,
|
2021-06-07 14:36:53 -07:00
|
|
|
)?;
|
|
|
|
let need_transform = extra_data.need_transform;
|
|
|
|
let stream = Box::new(FfmpegStream {
|
|
|
|
input,
|
|
|
|
video_i,
|
|
|
|
data: Vec::new(),
|
|
|
|
need_transform,
|
|
|
|
});
|
|
|
|
Ok((extra_data, stream))
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
2021-06-07 14:36:53 -07:00
|
|
|
}
|
2016-11-25 14:34:00 -08:00
|
|
|
|
2021-06-07 14:36:53 -07:00
|
|
|
struct FfmpegStream {
|
|
|
|
input: ffmpeg::avformat::InputFormatContext<'static>,
|
|
|
|
video_i: usize,
|
|
|
|
data: Vec<u8>,
|
|
|
|
need_transform: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Stream for FfmpegStream {
|
|
|
|
fn next(&mut self) -> Result<VideoFrame, Error> {
|
|
|
|
let pkt = loop {
|
|
|
|
let pkt = self.input.read_frame()?;
|
|
|
|
if pkt.stream_index() == self.video_i {
|
|
|
|
break pkt;
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
2021-06-07 14:36:53 -07:00
|
|
|
};
|
|
|
|
let data = pkt
|
|
|
|
.data()
|
|
|
|
.ok_or_else(|| format_err!("packet with no data"))?;
|
|
|
|
if self.need_transform {
|
|
|
|
h264::transform_sample_data(data, &mut self.data)?;
|
|
|
|
} else {
|
|
|
|
// This copy isn't strictly necessary, but this path is only taken in testing anyway.
|
|
|
|
self.data.clear();
|
|
|
|
self.data.extend_from_slice(data);
|
|
|
|
}
|
|
|
|
let pts = pkt.pts().ok_or_else(|| format_err!("packet with no pts"))?;
|
|
|
|
Ok(VideoFrame {
|
|
|
|
pts,
|
|
|
|
is_key: pkt.is_key(),
|
|
|
|
duration: pkt.duration(),
|
|
|
|
data: &self.data,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct RetinaOpener {}
|
|
|
|
|
|
|
|
pub const RETINA: RetinaOpener = RetinaOpener {};
|
|
|
|
|
|
|
|
impl Opener for RetinaOpener {
|
|
|
|
fn open(&self, src: Source) -> Result<(h264::ExtraData, Box<dyn Stream>), Error> {
|
|
|
|
let (startup_tx, startup_rx) = tokio::sync::oneshot::channel();
|
|
|
|
let (frame_tx, frame_rx) = tokio::sync::mpsc::channel(1);
|
|
|
|
let handle = tokio::runtime::Handle::current();
|
|
|
|
let (url, username, password) = match src {
|
|
|
|
#[cfg(test)]
|
|
|
|
Source::File(_) => bail!("Retina doesn't support .mp4 files"),
|
|
|
|
Source::Rtsp {
|
|
|
|
url,
|
|
|
|
username,
|
|
|
|
password,
|
|
|
|
} => (url, username, password),
|
|
|
|
};
|
|
|
|
let creds = match (username, password) {
|
|
|
|
(None, None) => None,
|
|
|
|
(Some(username), Some(password)) => Some(Credentials { username, password }),
|
|
|
|
_ => bail!("expected username and password together"),
|
|
|
|
};
|
|
|
|
|
|
|
|
// TODO: connection timeout.
|
|
|
|
handle.spawn(async move {
|
|
|
|
let (session, mut video_params) = match RetinaOpener::play(url, creds).await {
|
|
|
|
Err(e) => {
|
|
|
|
let _ = startup_tx.send(Err(e));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
Ok((s, v)) => (s, v),
|
|
|
|
};
|
|
|
|
let session = match session.demuxed() {
|
|
|
|
Ok(s) => s,
|
|
|
|
Err(e) => {
|
|
|
|
let _ = startup_tx.send(Err(e));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
tokio::pin!(session);
|
|
|
|
|
|
|
|
// First frame.
|
|
|
|
loop {
|
|
|
|
match session.next().await {
|
|
|
|
Some(Err(e)) => {
|
|
|
|
let _ = startup_tx.send(Err(e));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
Some(Ok(CodecItem::VideoFrame(mut v))) => {
|
|
|
|
if let Some(v) = v.new_parameters.take() {
|
|
|
|
video_params = v;
|
|
|
|
}
|
|
|
|
if v.is_random_access_point {
|
|
|
|
if startup_tx.send(Ok(video_params)).is_err() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if frame_tx.send(Ok(v)).await.is_err() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Some(Ok(_)) => {}
|
|
|
|
None => {
|
|
|
|
let _ =
|
|
|
|
startup_tx.send(Err(format_err!("stream closed before first frame")));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Following frames.
|
|
|
|
let mut need_key_frame = false;
|
|
|
|
while let Some(item) = session.next().await {
|
|
|
|
match item {
|
|
|
|
Err(e) => {
|
|
|
|
let _ = frame_tx.send(Err(e)).await;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
Ok(CodecItem::VideoFrame(v)) => {
|
|
|
|
if v.loss > 0 {
|
|
|
|
if !v.is_random_access_point {
|
|
|
|
log::info!(
|
|
|
|
"lost {} RTP packets; waiting for next key frame @ {:?}",
|
|
|
|
v.loss,
|
|
|
|
v.start_ctx()
|
|
|
|
);
|
|
|
|
need_key_frame = true;
|
|
|
|
continue;
|
|
|
|
} else {
|
|
|
|
log::info!(
|
|
|
|
"lost {} RTP packets; already have key frame @ {:?}",
|
|
|
|
v.loss,
|
|
|
|
v.start_ctx()
|
|
|
|
);
|
|
|
|
need_key_frame = false;
|
|
|
|
}
|
|
|
|
} else if need_key_frame && !v.is_random_access_point {
|
|
|
|
continue;
|
|
|
|
} else if need_key_frame {
|
|
|
|
log::info!("recovering from loss with key frame @ {:?}", v.start_ctx());
|
|
|
|
need_key_frame = false;
|
|
|
|
}
|
|
|
|
if frame_tx.send(Ok(v)).await.is_err() {
|
|
|
|
return; // other end died.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
let video_params = handle.block_on(startup_rx)??;
|
|
|
|
let dims = video_params.pixel_dimensions();
|
|
|
|
let extra_data = h264::ExtraData::parse(
|
|
|
|
video_params.extra_data(),
|
|
|
|
u16::try_from(dims.0)?,
|
|
|
|
u16::try_from(dims.1)?,
|
|
|
|
)?;
|
|
|
|
let stream = Box::new(RetinaStream {
|
|
|
|
frame_rx,
|
2021-06-28 14:25:02 -07:00
|
|
|
frame: None,
|
2021-06-07 14:36:53 -07:00
|
|
|
});
|
|
|
|
Ok((extra_data, stream))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl RetinaOpener {
|
|
|
|
async fn play(
|
|
|
|
url: Url,
|
|
|
|
creds: Option<Credentials>,
|
|
|
|
) -> Result<(Session<Playing>, VideoParameters), Error> {
|
|
|
|
let mut session = retina::client::Session::describe(url, creds).await?;
|
|
|
|
let (video_i, video_params) = session
|
|
|
|
.streams()
|
|
|
|
.iter()
|
|
|
|
.enumerate()
|
|
|
|
.find_map(|(i, s)| match s.parameters() {
|
|
|
|
Some(retina::codec::Parameters::Video(v)) => Some((i, v.clone())),
|
|
|
|
_ => None,
|
|
|
|
})
|
|
|
|
.ok_or_else(|| format_err!("couldn't find H.264 video stream"))?;
|
|
|
|
session.setup(video_i).await?;
|
2021-06-28 14:26:36 -07:00
|
|
|
let session = session.play(retina::client::PlayPolicy::default()).await?;
|
2021-06-07 14:36:53 -07:00
|
|
|
Ok((session, video_params))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct RetinaStream {
|
|
|
|
frame_rx: tokio::sync::mpsc::Receiver<Result<retina::codec::VideoFrame, Error>>,
|
2021-06-28 14:25:02 -07:00
|
|
|
frame: Option<retina::codec::VideoFrame>,
|
2021-06-07 14:36:53 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Stream for RetinaStream {
|
|
|
|
fn next(&mut self) -> Result<VideoFrame, Error> {
|
2021-06-28 14:25:02 -07:00
|
|
|
// TODO: use Option::insert after bumping MSRV to 1.53.
|
|
|
|
self.frame = Some(
|
|
|
|
self.frame_rx
|
|
|
|
.blocking_recv()
|
|
|
|
.ok_or_else(|| format_err!("stream ended"))??,
|
|
|
|
);
|
|
|
|
let frame = self.frame.as_ref().unwrap();
|
2021-06-07 14:36:53 -07:00
|
|
|
Ok(VideoFrame {
|
|
|
|
pts: frame.timestamp.elapsed(),
|
|
|
|
duration: 0,
|
|
|
|
is_key: frame.is_random_access_point,
|
2021-06-28 14:25:02 -07:00
|
|
|
data: &frame.data()[..],
|
2021-06-07 14:36:53 -07:00
|
|
|
})
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
}
|