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-08-31 08:10:50 -07:00
|
|
|
use retina::client::{Credentials, Transport};
|
2021-06-07 14:36:53 -07:00
|
|
|
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;
|
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-07 14:36:53 -07:00
|
|
|
static START_FFMPEG: parking_lot::Once = parking_lot::Once::new();
|
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);
|
|
|
|
|
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>,
|
2021-08-31 08:10:50 -07:00
|
|
|
transport: Transport,
|
2021-06-07 14:36:53 -07:00
|
|
|
},
|
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>,
|
2021-08-31 08:10:50 -07:00
|
|
|
transport: Transport,
|
2021-06-07 14:36:53 -07:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
pub trait Opener: Send + Sync {
|
2021-06-28 17:49:29 -07:00
|
|
|
fn open(&self, label: String, src: Source)
|
|
|
|
-> Result<(h264::ExtraData, Box<dyn Stream>), Error>;
|
2021-06-07 14:36:53 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2021-06-28 17:49:29 -07:00
|
|
|
fn open(
|
|
|
|
&self,
|
|
|
|
label: String,
|
|
|
|
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!(
|
2021-06-28 17:49:29 -07:00
|
|
|
"{}: While opening URL {}, some options were not understood: {}",
|
|
|
|
&label, url, open_options
|
2021-02-16 22:15:54 -08:00
|
|
|
);
|
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,
|
2021-08-31 08:10:50 -07:00
|
|
|
transport,
|
2021-06-07 14:36:53 -07:00
|
|
|
} => {
|
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
|
2021-08-31 08:10:50 -07:00
|
|
|
.set(
|
|
|
|
cstr!("rtsp_transport"),
|
|
|
|
match transport {
|
|
|
|
Transport::Tcp => cstr!("tcp"),
|
|
|
|
Transport::Udp => cstr!("udp"),
|
|
|
|
},
|
|
|
|
)
|
2021-02-16 22:15:54 -08:00
|
|
|
.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!(
|
2021-06-28 17:49:29 -07:00
|
|
|
"{}: While opening URL {}, some options were not understood: {}",
|
|
|
|
&label, 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 {
|
2021-06-28 17:49:29 -07:00
|
|
|
fn open(
|
|
|
|
&self,
|
|
|
|
label: String,
|
|
|
|
src: Source,
|
|
|
|
) -> Result<(h264::ExtraData, Box<dyn Stream>), Error> {
|
2021-06-07 14:36:53 -07:00
|
|
|
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();
|
2021-08-31 08:10:50 -07:00
|
|
|
let (url, options) = match src {
|
2021-06-07 14:36:53 -07:00
|
|
|
#[cfg(test)]
|
|
|
|
Source::File(_) => bail!("Retina doesn't support .mp4 files"),
|
|
|
|
Source::Rtsp {
|
|
|
|
url,
|
|
|
|
username,
|
|
|
|
password,
|
2021-08-31 08:10:50 -07:00
|
|
|
transport,
|
|
|
|
} => (
|
|
|
|
url,
|
|
|
|
retina::client::SessionOptions::default()
|
|
|
|
.creds(match (username, password) {
|
|
|
|
(None, None) => None,
|
|
|
|
(Some(username), password) => Some(Credentials {
|
|
|
|
username,
|
|
|
|
password: password.unwrap_or_default(),
|
|
|
|
}),
|
|
|
|
_ => bail!("must supply username when supplying password"),
|
|
|
|
})
|
|
|
|
.transport(transport)
|
|
|
|
.user_agent(format!("Moonfire NVR {}", env!("CARGO_PKG_VERSION"))),
|
|
|
|
),
|
2021-06-07 14:36:53 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
handle.spawn(async move {
|
2021-08-31 08:10:50 -07:00
|
|
|
let r = tokio::time::timeout(RETINA_TIMEOUT, RetinaOpener::play(url, options)).await;
|
2021-06-28 16:27:28 -07:00
|
|
|
let (mut session, video_params, first_frame) =
|
|
|
|
match r.unwrap_or_else(|_| Err(format_err!("timeout opening stream"))) {
|
|
|
|
Err(e) => {
|
2021-06-07 14:36:53 -07:00
|
|
|
let _ = startup_tx.send(Err(e));
|
|
|
|
return;
|
|
|
|
}
|
2021-06-28 16:27:28 -07:00
|
|
|
Ok((s, p, f)) => (s, p, f),
|
|
|
|
};
|
|
|
|
if startup_tx.send(Ok(video_params)).is_err() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if frame_tx.send(Ok(first_frame)).await.is_err() {
|
|
|
|
return;
|
2021-06-07 14:36:53 -07:00
|
|
|
}
|
|
|
|
|
2021-06-28 16:27:28 -07:00
|
|
|
// Read following frames.
|
|
|
|
let mut deadline = tokio::time::Instant::now() + RETINA_TIMEOUT;
|
|
|
|
loop {
|
2021-07-08 16:06:30 -07:00
|
|
|
match tokio::time::timeout_at(deadline, session.next()).await {
|
|
|
|
Err(_) => {
|
|
|
|
let _ = frame_tx
|
|
|
|
.send(Err(format_err!("timeout getting next frame")))
|
|
|
|
.await;
|
2021-06-07 14:36:53 -07:00
|
|
|
return;
|
|
|
|
}
|
2021-07-08 16:06:30 -07:00
|
|
|
Ok(Some(Err(e))) => {
|
|
|
|
let _ = frame_tx.send(Err(e.into())).await;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
Ok(None) => break,
|
|
|
|
Ok(Some(Ok(CodecItem::VideoFrame(v)))) => {
|
|
|
|
if let Some(p) = v.new_parameters {
|
|
|
|
// TODO: we could start a new recording without dropping the connection.
|
|
|
|
let _ = frame_tx.send(Err(format_err!("parameter; change: {:?}", p)));
|
|
|
|
return;
|
|
|
|
}
|
2021-06-28 16:27:28 -07:00
|
|
|
deadline = tokio::time::Instant::now() + RETINA_TIMEOUT;
|
2021-06-07 14:36:53 -07:00
|
|
|
if v.loss > 0 {
|
2021-06-28 17:49:29 -07:00
|
|
|
log::warn!(
|
2021-08-31 08:59:33 -07:00
|
|
|
"{}: lost {} RTP packets @ {}",
|
2021-06-28 17:49:29 -07:00
|
|
|
&label,
|
|
|
|
v.loss,
|
|
|
|
v.start_ctx()
|
|
|
|
);
|
2021-06-07 14:36:53 -07:00
|
|
|
}
|
|
|
|
if frame_tx.send(Ok(v)).await.is_err() {
|
|
|
|
return; // other end died.
|
|
|
|
}
|
|
|
|
}
|
2021-07-08 16:06:30 -07:00
|
|
|
Ok(Some(Ok(_))) => {}
|
2021-06-07 14:36:53 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
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 {
|
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(
|
|
|
|
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?;
|
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()
|
|
|
|
.find_map(|(i, s)| match s.parameters() {
|
2021-07-08 16:06:30 -07:00
|
|
|
Some(retina::codec::Parameters::Video(v)) => Some((i, Box::new(v.clone()))),
|
2021-06-07 14:36:53 -07:00
|
|
|
_ => None,
|
|
|
|
})
|
|
|
|
.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() {
|
|
|
|
video_params = v;
|
|
|
|
}
|
|
|
|
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
|
|
|
}
|
|
|
|
};
|
|
|
|
Ok((session, video_params, first_frame))
|
2021-06-07 14:36:53 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
}
|