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;
|
2022-03-18 10:30:23 -07:00
|
|
|
use bytes::Bytes;
|
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;
|
2022-03-18 10:30:23 -07:00
|
|
|
use retina::client::Demuxed;
|
2022-04-13 14:37:45 -07:00
|
|
|
use retina::codec::CodecItem;
|
2021-06-28 16:27:28 -07:00
|
|
|
use std::pin::Pin;
|
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-28 16:27:28 -07:00
|
|
|
static RETINA_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
|
|
|
|
|
2022-05-17 16:36:39 -07:00
|
|
|
pub struct Options {
|
|
|
|
pub session: retina::client::SessionOptions,
|
|
|
|
pub setup: retina::client::SetupOptions,
|
|
|
|
}
|
|
|
|
|
2022-03-18 10:30:23 -07:00
|
|
|
/// Opens a RTSP stream. This is a trait for test injection.
|
2021-06-07 14:36:53 -07:00
|
|
|
pub trait Opener: Send + Sync {
|
2022-03-18 10:30:23 -07:00
|
|
|
/// Opens the given RTSP URL.
|
|
|
|
///
|
|
|
|
/// Note: despite the blocking interface, this expects to be called from
|
2022-04-13 11:39:38 -07:00
|
|
|
/// the context of a multithreaded tokio runtime with IO and time enabled.
|
2022-05-17 16:36:39 -07:00
|
|
|
fn open(&self, label: String, url: Url, options: Options) -> Result<Box<dyn Stream>, Error>;
|
2021-06-07 14:36:53 -07:00
|
|
|
}
|
|
|
|
|
2022-03-18 10:30:23 -07:00
|
|
|
pub struct VideoFrame {
|
2021-06-07 14:36:53 -07:00
|
|
|
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,
|
2022-03-18 10:30:23 -07:00
|
|
|
pub data: Bytes,
|
2022-04-13 14:37:45 -07:00
|
|
|
|
|
|
|
pub new_video_sample_entry: bool,
|
2016-12-06 18:41:44 -08:00
|
|
|
}
|
|
|
|
|
2021-06-07 14:36:53 -07:00
|
|
|
pub trait Stream: Send {
|
2022-04-12 14:57:16 -07:00
|
|
|
fn tool(&self) -> Option<&retina::client::Tool>;
|
2022-04-13 14:37:45 -07:00
|
|
|
fn video_sample_entry(&self) -> &db::VideoSampleEntryToInsert;
|
2021-06-07 14:36:53 -07:00
|
|
|
fn next(&mut self) -> Result<VideoFrame, Error>;
|
2016-12-06 18:41:44 -08:00
|
|
|
}
|
|
|
|
|
2022-03-18 10:30:23 -07:00
|
|
|
pub struct RealOpener;
|
2016-12-06 18:41:44 -08:00
|
|
|
|
2022-03-18 10:30:23 -07:00
|
|
|
pub const OPENER: RealOpener = RealOpener;
|
2016-11-25 14:34:00 -08:00
|
|
|
|
2022-03-18 10:30:23 -07:00
|
|
|
impl Opener for RealOpener {
|
2022-04-13 11:39:38 -07:00
|
|
|
fn open(
|
2021-06-28 17:49:29 -07:00
|
|
|
&self,
|
|
|
|
label: String,
|
2022-03-18 10:30:23 -07:00
|
|
|
url: Url,
|
2022-05-17 16:36:39 -07:00
|
|
|
mut options: Options,
|
2022-04-13 14:37:45 -07:00
|
|
|
) -> Result<Box<dyn Stream>, Error> {
|
2022-05-17 16:36:39 -07:00
|
|
|
options.session = options
|
|
|
|
.session
|
|
|
|
.user_agent(format!("Moonfire NVR {}", env!("CARGO_PKG_VERSION")));
|
2022-04-13 11:39:38 -07:00
|
|
|
let rt_handle = tokio::runtime::Handle::current();
|
2022-04-13 14:37:45 -07:00
|
|
|
let (inner, first_frame) = rt_handle
|
2022-04-13 13:22:26 -07:00
|
|
|
.block_on(rt_handle.spawn(tokio::time::timeout(
|
|
|
|
RETINA_TIMEOUT,
|
|
|
|
RetinaStreamInner::play(label, url, options),
|
|
|
|
)))
|
|
|
|
.expect("RetinaStream::play task panicked, see earlier error")??;
|
2022-04-13 14:37:45 -07:00
|
|
|
Ok(Box::new(RetinaStream {
|
2022-04-13 13:22:26 -07:00
|
|
|
inner: Some(inner),
|
2022-04-13 11:39:38 -07:00
|
|
|
rt_handle,
|
2022-03-18 10:30:23 -07:00
|
|
|
first_frame: Some(first_frame),
|
2022-04-13 14:37:45 -07:00
|
|
|
}))
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
2021-06-07 14:36:53 -07:00
|
|
|
}
|
2016-11-25 14:34:00 -08:00
|
|
|
|
2022-04-13 13:22:26 -07:00
|
|
|
/// Real stream, implemented with the Retina library.
|
|
|
|
///
|
|
|
|
/// Retina is asynchronous and tokio-based where currently Moonfire expects
|
|
|
|
/// a synchronous stream interface. This blocks on the tokio operations.
|
|
|
|
///
|
|
|
|
/// Experimentally, it appears faster to have one thread hand-off per frame via
|
|
|
|
/// `handle.block_on(handle.spawn(...))` rather than the same without the
|
|
|
|
/// `handle.spawn(...)`. See
|
|
|
|
/// [#206](https://github.com/scottlamb/moonfire-nvr/issues/206).
|
2022-04-13 11:39:38 -07:00
|
|
|
struct RetinaStream {
|
2022-04-13 13:22:26 -07:00
|
|
|
/// The actual stream details used from within the tokio reactor.
|
|
|
|
///
|
|
|
|
/// Spawned tokio tasks must be `'static`, so ownership is passed to the
|
|
|
|
/// task, and then returned when it completes.
|
|
|
|
inner: Option<Box<RetinaStreamInner>>,
|
|
|
|
|
2022-04-13 11:39:38 -07:00
|
|
|
rt_handle: tokio::runtime::Handle,
|
2021-06-07 14:36:53 -07:00
|
|
|
|
2022-03-18 10:30:23 -07:00
|
|
|
/// The first frame, if not yet returned from `next`.
|
|
|
|
///
|
|
|
|
/// This frame is special because we sometimes need to fetch it as part of getting the video
|
|
|
|
/// parameters.
|
|
|
|
first_frame: Option<retina::codec::VideoFrame>,
|
2021-06-07 14:36:53 -07:00
|
|
|
}
|
|
|
|
|
2022-04-13 13:22:26 -07:00
|
|
|
struct RetinaStreamInner {
|
|
|
|
label: String,
|
|
|
|
session: Demuxed,
|
2022-04-13 14:37:45 -07:00
|
|
|
video_sample_entry: db::VideoSampleEntryToInsert,
|
2022-04-13 13:22:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
impl RetinaStreamInner {
|
2021-06-28 16:27:28 -07:00
|
|
|
/// Plays to first frame. No timeout; that's the caller's responsibility.
|
2021-06-07 14:36:53 -07:00
|
|
|
async fn play(
|
2022-04-13 13:22:26 -07:00
|
|
|
label: String,
|
2021-06-07 14:36:53 -07:00
|
|
|
url: Url,
|
2022-05-17 16:36:39 -07:00
|
|
|
options: Options,
|
2022-04-13 14:37:45 -07:00
|
|
|
) -> Result<(Box<Self>, retina::codec::VideoFrame), Error> {
|
2022-05-17 16:36:39 -07:00
|
|
|
let mut session = retina::client::Session::describe(url, options.session).await?;
|
2022-04-13 13:22:26 -07:00
|
|
|
log::debug!("connected to {:?}, tool {:?}", &label, session.tool());
|
2022-05-17 16:36:39 -07:00
|
|
|
let video_i = session
|
2021-06-07 14:36:53 -07:00
|
|
|
.streams()
|
|
|
|
.iter()
|
2022-05-17 16:36:39 -07:00
|
|
|
.position(|s| s.media() == "video" && s.encoding_name() == "h264")
|
2021-06-07 14:36:53 -07:00
|
|
|
.ok_or_else(|| format_err!("couldn't find H.264 video stream"))?;
|
2022-05-17 16:36:39 -07:00
|
|
|
session.setup(video_i, options.setup).await?;
|
2021-08-19 15:06:20 -07:00
|
|
|
let session = session.play(retina::client::PlayOptions::default()).await?;
|
2022-04-13 13:22:26 -07:00
|
|
|
let mut session = session.demuxed()?;
|
2021-06-28 16:27:28 -07:00
|
|
|
|
|
|
|
// First frame.
|
|
|
|
let first_frame = loop {
|
2022-04-13 13:22:26 -07:00
|
|
|
match Pin::new(&mut session).next().await {
|
2021-07-08 16:06:30 -07:00
|
|
|
None => bail!("stream closed before first frame"),
|
|
|
|
Some(Err(e)) => return Err(e.into()),
|
2022-05-17 16:36:39 -07:00
|
|
|
Some(Ok(CodecItem::VideoFrame(v))) => {
|
|
|
|
if v.is_random_access_point() {
|
2021-07-08 16:06:30 -07:00
|
|
|
break v;
|
|
|
|
}
|
2021-06-28 16:27:28 -07:00
|
|
|
}
|
2021-07-08 16:06:30 -07:00
|
|
|
Some(Ok(_)) => {}
|
2021-06-28 16:27:28 -07:00
|
|
|
}
|
|
|
|
};
|
2022-05-17 16:36:39 -07:00
|
|
|
let video_params = match session.streams()[video_i].parameters() {
|
|
|
|
Some(retina::codec::ParametersRef::Video(v)) => v.clone(),
|
|
|
|
Some(_) => unreachable!(),
|
|
|
|
None => bail!("couldn't find H.264 parameters"),
|
|
|
|
};
|
|
|
|
let video_sample_entry = h264::parse_extra_data(video_params.extra_data())?;
|
2022-04-13 14:37:45 -07:00
|
|
|
let self_ = Box::new(Self {
|
|
|
|
label,
|
|
|
|
session,
|
|
|
|
video_sample_entry,
|
|
|
|
});
|
|
|
|
Ok((self_, first_frame))
|
2021-06-07 14:36:53 -07:00
|
|
|
}
|
|
|
|
|
2022-03-18 10:30:23 -07:00
|
|
|
/// Fetches a non-initial frame.
|
|
|
|
async fn fetch_next_frame(
|
2022-04-13 13:22:26 -07:00
|
|
|
mut self: Box<Self>,
|
2022-05-17 16:36:39 -07:00
|
|
|
) -> Result<
|
|
|
|
(
|
|
|
|
Box<Self>,
|
|
|
|
retina::codec::VideoFrame,
|
|
|
|
Option<retina::codec::VideoParameters>,
|
|
|
|
),
|
|
|
|
Error,
|
|
|
|
> {
|
2022-03-18 10:30:23 -07:00
|
|
|
loop {
|
2022-04-13 13:22:26 -07:00
|
|
|
match Pin::new(&mut self.session).next().await.transpose()? {
|
2022-03-18 10:30:23 -07:00
|
|
|
None => bail!("end of stream"),
|
|
|
|
Some(CodecItem::VideoFrame(v)) => {
|
2022-05-17 16:36:39 -07:00
|
|
|
if v.loss() > 0 {
|
2022-03-18 10:30:23 -07:00
|
|
|
log::warn!(
|
|
|
|
"{}: lost {} RTP packets @ {}",
|
2022-04-13 13:22:26 -07:00
|
|
|
&self.label,
|
2022-05-17 16:36:39 -07:00
|
|
|
v.loss(),
|
2022-03-18 10:30:23 -07:00
|
|
|
v.start_ctx()
|
|
|
|
);
|
|
|
|
}
|
2022-05-17 16:36:39 -07:00
|
|
|
let p = if v.has_new_parameters() {
|
|
|
|
Some(match self.session.streams()[v.stream_id()].parameters() {
|
|
|
|
Some(retina::codec::ParametersRef::Video(v)) => v.clone(),
|
|
|
|
_ => unreachable!(),
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
|
|
|
return Ok((self, v, p));
|
2022-03-18 10:30:23 -07:00
|
|
|
}
|
|
|
|
Some(_) => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-06-07 14:36:53 -07:00
|
|
|
}
|
|
|
|
|
2022-04-13 11:39:38 -07:00
|
|
|
impl Stream for RetinaStream {
|
2022-04-12 14:57:16 -07:00
|
|
|
fn tool(&self) -> Option<&retina::client::Tool> {
|
2022-04-13 13:22:26 -07:00
|
|
|
self.inner.as_ref().unwrap().session.tool()
|
2022-04-12 14:57:16 -07:00
|
|
|
}
|
|
|
|
|
2022-04-13 14:37:45 -07:00
|
|
|
fn video_sample_entry(&self) -> &db::VideoSampleEntryToInsert {
|
|
|
|
&self.inner.as_ref().unwrap().video_sample_entry
|
|
|
|
}
|
|
|
|
|
2021-06-07 14:36:53 -07:00
|
|
|
fn next(&mut self) -> Result<VideoFrame, Error> {
|
2022-04-13 14:37:45 -07:00
|
|
|
let (frame, new_video_sample_entry) = self
|
|
|
|
.first_frame
|
|
|
|
.take()
|
|
|
|
.map(|f| Ok((f, false)))
|
|
|
|
.unwrap_or_else(move || {
|
|
|
|
let inner = self.inner.take().unwrap();
|
2022-05-17 16:36:39 -07:00
|
|
|
let (mut inner, frame, new_parameters) = self
|
2022-04-13 14:37:45 -07:00
|
|
|
.rt_handle
|
|
|
|
.block_on(self.rt_handle.spawn(tokio::time::timeout(
|
|
|
|
RETINA_TIMEOUT,
|
|
|
|
inner.fetch_next_frame(),
|
|
|
|
)))
|
|
|
|
.expect("fetch_next_frame task panicked, see earlier error")
|
|
|
|
.map_err(|_| format_err!("timeout getting next frame"))??;
|
|
|
|
let mut new_video_sample_entry = false;
|
2022-05-17 16:36:39 -07:00
|
|
|
if let Some(p) = new_parameters {
|
2022-04-13 14:37:45 -07:00
|
|
|
let video_sample_entry = h264::parse_extra_data(p.extra_data())?;
|
|
|
|
if video_sample_entry != inner.video_sample_entry {
|
|
|
|
log::debug!(
|
|
|
|
"{}: parameter change:\nold: {:?}\nnew: {:?}",
|
|
|
|
&inner.label,
|
|
|
|
&inner.video_sample_entry,
|
|
|
|
&video_sample_entry
|
|
|
|
);
|
|
|
|
inner.video_sample_entry = video_sample_entry;
|
|
|
|
new_video_sample_entry = true;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
self.inner = Some(inner);
|
|
|
|
Ok::<_, failure::Error>((frame, new_video_sample_entry))
|
|
|
|
})?;
|
2021-06-07 14:36:53 -07:00
|
|
|
Ok(VideoFrame {
|
2022-05-17 16:36:39 -07:00
|
|
|
pts: frame.timestamp().elapsed(),
|
2021-06-07 14:36:53 -07:00
|
|
|
duration: 0,
|
2022-05-17 16:36:39 -07:00
|
|
|
is_key: frame.is_random_access_point(),
|
|
|
|
data: frame.into_data().into(),
|
2022-04-13 14:37:45 -07:00
|
|
|
new_video_sample_entry,
|
2021-06-07 14:36:53 -07:00
|
|
|
})
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
}
|
2022-03-18 10:30:23 -07:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
pub mod testutil {
|
|
|
|
use super::*;
|
|
|
|
use std::convert::TryFrom;
|
|
|
|
use std::io::Cursor;
|
|
|
|
|
|
|
|
pub struct Mp4Stream {
|
|
|
|
reader: mp4::Mp4Reader<Cursor<Vec<u8>>>,
|
|
|
|
h264_track_id: u32,
|
|
|
|
next_sample_id: u32,
|
2022-04-13 14:37:45 -07:00
|
|
|
video_sample_entry: db::VideoSampleEntryToInsert,
|
2022-03-18 10:30:23 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Mp4Stream {
|
|
|
|
/// Opens a stream, with a return matching that expected by [`Opener`].
|
2022-04-13 14:37:45 -07:00
|
|
|
pub fn open(path: &str) -> Result<Self, Error> {
|
2022-03-18 10:30:23 -07:00
|
|
|
let f = std::fs::read(path)?;
|
|
|
|
let len = f.len();
|
|
|
|
let reader = mp4::Mp4Reader::read_header(Cursor::new(f), u64::try_from(len)?)?;
|
|
|
|
let h264_track = match reader
|
|
|
|
.tracks()
|
|
|
|
.values()
|
|
|
|
.find(|t| matches!(t.media_type(), Ok(mp4::MediaType::H264)))
|
|
|
|
{
|
|
|
|
None => bail!("expected a H.264 track"),
|
|
|
|
Some(t) => t,
|
|
|
|
};
|
2022-04-13 14:37:45 -07:00
|
|
|
let video_sample_entry = h264::parse_extra_data(&h264_track.extra_data()?[..])?;
|
2022-03-18 10:30:23 -07:00
|
|
|
let h264_track_id = h264_track.track_id();
|
|
|
|
let stream = Mp4Stream {
|
|
|
|
reader,
|
|
|
|
h264_track_id,
|
|
|
|
next_sample_id: 1,
|
2022-04-13 14:37:45 -07:00
|
|
|
video_sample_entry,
|
2022-03-18 10:30:23 -07:00
|
|
|
};
|
2022-04-13 14:37:45 -07:00
|
|
|
Ok(stream)
|
2022-03-18 10:30:23 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn duration(&self) -> u64 {
|
|
|
|
self.reader.moov.mvhd.duration
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the edit list from the H.264 stream, if any.
|
|
|
|
pub fn elst(&self) -> Option<&mp4::mp4box::elst::ElstBox> {
|
|
|
|
let h264_track = self.reader.tracks().get(&self.h264_track_id).unwrap();
|
|
|
|
h264_track
|
|
|
|
.trak
|
|
|
|
.edts
|
|
|
|
.as_ref()
|
|
|
|
.and_then(|edts| edts.elst.as_ref())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Stream for Mp4Stream {
|
2022-04-12 14:57:16 -07:00
|
|
|
fn tool(&self) -> Option<&retina::client::Tool> {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
2022-03-18 10:30:23 -07:00
|
|
|
fn next(&mut self) -> Result<VideoFrame, Error> {
|
|
|
|
let sample = self
|
|
|
|
.reader
|
|
|
|
.read_sample(self.h264_track_id, self.next_sample_id)?
|
|
|
|
.ok_or_else(|| format_err!("End of file"))?;
|
|
|
|
self.next_sample_id += 1;
|
|
|
|
Ok(VideoFrame {
|
|
|
|
pts: sample.start_time as i64,
|
|
|
|
duration: sample.duration as i32,
|
|
|
|
is_key: sample.is_sync,
|
|
|
|
data: sample.bytes,
|
2022-04-13 14:37:45 -07:00
|
|
|
new_video_sample_entry: false,
|
2022-03-18 10:30:23 -07:00
|
|
|
})
|
|
|
|
}
|
2022-04-13 14:37:45 -07:00
|
|
|
|
|
|
|
fn video_sample_entry(&self) -> &db::VideoSampleEntryToInsert {
|
|
|
|
&self.video_sample_entry
|
|
|
|
}
|
2022-03-18 10:30:23 -07:00
|
|
|
}
|
|
|
|
}
|