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;
|
2021-06-07 14:36:53 -07:00
|
|
|
use retina::codec::{CodecItem, VideoParameters};
|
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-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.
|
|
|
|
fn open(
|
2022-03-18 10:30:23 -07:00
|
|
|
&self,
|
|
|
|
label: String,
|
|
|
|
url: Url,
|
|
|
|
options: retina::client::SessionOptions,
|
2022-04-13 11:39:38 -07:00
|
|
|
) -> Result<(db::VideoSampleEntryToInsert, 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,
|
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>;
|
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,
|
|
|
|
options: retina::client::SessionOptions,
|
2022-04-13 11:39:38 -07:00
|
|
|
) -> Result<(db::VideoSampleEntryToInsert, Box<dyn Stream>), Error> {
|
2022-03-18 10:30:23 -07:00
|
|
|
let options = options.user_agent(format!("Moonfire NVR {}", env!("CARGO_PKG_VERSION")));
|
2022-04-13 11:39:38 -07:00
|
|
|
let rt_handle = tokio::runtime::Handle::current();
|
|
|
|
let (session, video_params, first_frame) = rt_handle.block_on(tokio::time::timeout(
|
2022-03-18 10:30:23 -07:00
|
|
|
RETINA_TIMEOUT,
|
2022-04-12 14:57:16 -07:00
|
|
|
RetinaStream::play(&label, url, options),
|
2022-03-18 10:30:23 -07:00
|
|
|
))??;
|
|
|
|
let extra_data = h264::parse_extra_data(video_params.extra_data())?;
|
|
|
|
let stream = Box::new(RetinaStream {
|
|
|
|
label,
|
|
|
|
session,
|
2022-04-13 11:39:38 -07:00
|
|
|
rt_handle,
|
2022-03-18 10:30:23 -07:00
|
|
|
first_frame: Some(first_frame),
|
2021-06-07 14:36:53 -07:00
|
|
|
});
|
|
|
|
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
|
|
|
|
2022-04-13 11:39:38 -07:00
|
|
|
struct RetinaStream {
|
2022-03-18 10:30:23 -07:00
|
|
|
label: String,
|
|
|
|
session: Pin<Box<Demuxed>>,
|
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 11:39:38 -07:00
|
|
|
impl RetinaStream {
|
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-12 14:57:16 -07:00
|
|
|
label: &str,
|
2021-06-07 14:36:53 -07:00
|
|
|
url: Url,
|
2021-08-31 08:10:50 -07:00
|
|
|
options: retina::client::SessionOptions,
|
2021-06-28 16:27:28 -07:00
|
|
|
) -> Result<
|
|
|
|
(
|
2021-07-08 16:06:30 -07:00
|
|
|
Pin<Box<retina::client::Demuxed>>,
|
|
|
|
Box<VideoParameters>,
|
2021-06-28 16:27:28 -07:00
|
|
|
retina::codec::VideoFrame,
|
|
|
|
),
|
|
|
|
Error,
|
|
|
|
> {
|
2021-08-31 08:10:50 -07:00
|
|
|
let mut session = retina::client::Session::describe(url, options).await?;
|
2022-04-12 14:57:16 -07:00
|
|
|
log::debug!("connected to {:?}, tool {:?}", label, session.tool());
|
2021-06-28 16:27:28 -07:00
|
|
|
let (video_i, mut video_params) = session
|
2021-06-07 14:36:53 -07:00
|
|
|
.streams()
|
|
|
|
.iter()
|
|
|
|
.enumerate()
|
2022-01-23 22:58:56 +03:00
|
|
|
.find_map(|(i, s)| {
|
|
|
|
if s.media == "video" && s.encoding_name == "h264" {
|
|
|
|
Some((
|
|
|
|
i,
|
|
|
|
s.parameters().and_then(|p| match p {
|
|
|
|
retina::codec::Parameters::Video(v) => Some(Box::new(v.clone())),
|
|
|
|
_ => None,
|
|
|
|
}),
|
|
|
|
))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
2021-06-07 14:36:53 -07:00
|
|
|
})
|
|
|
|
.ok_or_else(|| format_err!("couldn't find H.264 video stream"))?;
|
|
|
|
session.setup(video_i).await?;
|
2021-08-19 15:06:20 -07:00
|
|
|
let session = session.play(retina::client::PlayOptions::default()).await?;
|
2021-06-28 16:27:28 -07:00
|
|
|
let mut session = Box::pin(session.demuxed()?);
|
|
|
|
|
|
|
|
// First frame.
|
|
|
|
let first_frame = loop {
|
2021-07-08 16:06:30 -07:00
|
|
|
match session.next().await {
|
|
|
|
None => bail!("stream closed before first frame"),
|
|
|
|
Some(Err(e)) => return Err(e.into()),
|
|
|
|
Some(Ok(CodecItem::VideoFrame(mut v))) => {
|
|
|
|
if let Some(v) = v.new_parameters.take() {
|
2022-01-23 22:58:56 +03:00
|
|
|
video_params = Some(v);
|
2021-07-08 16:06:30 -07:00
|
|
|
}
|
|
|
|
if v.is_random_access_point {
|
|
|
|
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-01-23 22:58:56 +03:00
|
|
|
Ok((
|
|
|
|
session,
|
|
|
|
video_params.ok_or_else(|| format_err!("couldn't find H.264 parameters"))?,
|
|
|
|
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(
|
|
|
|
label: &str,
|
|
|
|
mut session: Pin<&mut Demuxed>,
|
|
|
|
) -> Result<retina::codec::VideoFrame, Error> {
|
|
|
|
loop {
|
|
|
|
match session.next().await.transpose()? {
|
|
|
|
None => bail!("end of stream"),
|
|
|
|
Some(CodecItem::VideoFrame(v)) => {
|
|
|
|
if let Some(p) = v.new_parameters {
|
|
|
|
// TODO: we could start a new recording without dropping the connection.
|
|
|
|
bail!("parameter change: {:?}", p);
|
|
|
|
}
|
|
|
|
if v.loss > 0 {
|
|
|
|
log::warn!(
|
|
|
|
"{}: lost {} RTP packets @ {}",
|
|
|
|
&label,
|
|
|
|
v.loss,
|
|
|
|
v.start_ctx()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return Ok(v);
|
|
|
|
}
|
|
|
|
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> {
|
|
|
|
Pin::into_inner(self.session.as_ref()).tool()
|
|
|
|
}
|
|
|
|
|
2021-06-07 14:36:53 -07:00
|
|
|
fn next(&mut self) -> Result<VideoFrame, Error> {
|
2022-03-18 10:30:23 -07:00
|
|
|
let frame = self.first_frame.take().map(Ok).unwrap_or_else(|| {
|
2022-04-13 11:39:38 -07:00
|
|
|
self.rt_handle
|
2022-03-18 10:30:23 -07:00
|
|
|
.block_on(tokio::time::timeout(
|
|
|
|
RETINA_TIMEOUT,
|
|
|
|
RetinaStream::fetch_next_frame(&self.label, self.session.as_mut()),
|
|
|
|
))
|
|
|
|
.map_err(|_| format_err!("timeout getting next frame"))?
|
|
|
|
})?;
|
2021-06-07 14:36:53 -07:00
|
|
|
Ok(VideoFrame {
|
|
|
|
pts: frame.timestamp.elapsed(),
|
|
|
|
duration: 0,
|
|
|
|
is_key: frame.is_random_access_point,
|
2022-03-18 10:30:23 -07:00
|
|
|
data: frame.into_data(),
|
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,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Mp4Stream {
|
|
|
|
/// Opens a stream, with a return matching that expected by [`Opener`].
|
|
|
|
pub fn open(path: &str) -> Result<(db::VideoSampleEntryToInsert, Self), Error> {
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
let extra_data = h264::parse_extra_data(&h264_track.extra_data()?[..])?;
|
|
|
|
let h264_track_id = h264_track.track_id();
|
|
|
|
let stream = Mp4Stream {
|
|
|
|
reader,
|
|
|
|
h264_track_id,
|
|
|
|
next_sample_id: 1,
|
|
|
|
};
|
|
|
|
Ok((extra_data, stream))
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|