2021-10-28 13:07:39 -07:00
|
|
|
// This file is part of Moonfire NVR, a security camera network video recorder.
|
|
|
|
// Copyright (C) 2021 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
|
|
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
|
|
|
|
|
|
|
|
//! Live video websocket handling.
|
|
|
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
2023-07-09 22:04:17 -07:00
|
|
|
use base::{bail, err, Error};
|
2024-08-15 06:12:11 -07:00
|
|
|
use futures::SinkExt;
|
2023-02-15 07:04:50 -08:00
|
|
|
use http::header;
|
2024-08-15 06:12:11 -07:00
|
|
|
use tokio::sync::broadcast::error::RecvError;
|
2024-08-23 20:56:40 -07:00
|
|
|
use tokio_tungstenite::tungstenite;
|
2021-10-28 13:07:39 -07:00
|
|
|
use uuid::Uuid;
|
|
|
|
|
2023-02-15 07:04:50 -08:00
|
|
|
use crate::mp4;
|
2021-10-28 13:07:39 -07:00
|
|
|
|
2024-08-23 20:56:40 -07:00
|
|
|
use super::{websocket::WebSocketStream, Caller, Service};
|
2022-04-13 21:45:45 -07:00
|
|
|
|
2024-08-15 06:12:11 -07:00
|
|
|
/// Interval at which to send keepalives if there are no frames.
|
|
|
|
///
|
|
|
|
/// Chrome appears to time out WebSockets after 60 seconds of inactivity.
|
|
|
|
/// If the camera is disconnected or not sending frames, we'd like to keep
|
|
|
|
/// the connection open so everything will recover when the camera comes back.
|
|
|
|
const KEEPALIVE_AFTER_IDLE: tokio::time::Duration = tokio::time::Duration::from_secs(30);
|
|
|
|
|
2021-10-28 13:07:39 -07:00
|
|
|
impl Service {
|
2023-02-15 07:04:50 -08:00
|
|
|
pub(super) async fn stream_live_m4s(
|
2021-10-28 13:07:39 -07:00
|
|
|
self: Arc<Self>,
|
2024-08-23 20:56:40 -07:00
|
|
|
ws: &mut WebSocketStream,
|
2023-02-15 07:04:50 -08:00
|
|
|
caller: Result<Caller, Error>,
|
2021-10-28 13:07:39 -07:00
|
|
|
uuid: Uuid,
|
|
|
|
stream_type: db::StreamType,
|
2023-02-15 07:04:50 -08:00
|
|
|
) -> Result<(), Error> {
|
|
|
|
let caller = caller?;
|
2021-10-28 13:07:39 -07:00
|
|
|
if !caller.permissions.view_video {
|
2023-07-09 22:04:17 -07:00
|
|
|
bail!(PermissionDenied, msg("view_video required"));
|
2021-10-28 13:07:39 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
let stream_id;
|
|
|
|
let open_id;
|
2024-08-15 06:12:11 -07:00
|
|
|
let mut sub_rx = {
|
2021-10-28 13:07:39 -07:00
|
|
|
let mut db = self.db.lock();
|
|
|
|
open_id = match db.open {
|
|
|
|
None => {
|
2023-07-09 22:04:17 -07:00
|
|
|
bail!(
|
2021-10-28 13:07:39 -07:00
|
|
|
FailedPrecondition,
|
2023-07-09 22:04:17 -07:00
|
|
|
msg("database is read-only; there are no live streams"),
|
2021-10-28 13:07:39 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
Some(o) => o.id,
|
|
|
|
};
|
2023-02-15 07:04:50 -08:00
|
|
|
let camera = db
|
|
|
|
.get_camera(uuid)
|
2023-07-09 22:04:17 -07:00
|
|
|
.ok_or_else(|| err!(NotFound, msg("no such camera {uuid}")))?;
|
2023-02-15 07:04:50 -08:00
|
|
|
stream_id = camera.streams[stream_type.index()]
|
2023-07-09 22:04:17 -07:00
|
|
|
.ok_or_else(|| err!(NotFound, msg("no such stream {uuid}/{stream_type}")))?;
|
2024-08-15 06:12:11 -07:00
|
|
|
db.watch_live(stream_id).expect("stream_id refed by camera")
|
|
|
|
};
|
2021-10-28 13:07:39 -07:00
|
|
|
|
2024-08-15 06:12:11 -07:00
|
|
|
let mut keepalive = tokio::time::interval(KEEPALIVE_AFTER_IDLE);
|
|
|
|
keepalive.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
2021-10-28 13:07:39 -07:00
|
|
|
|
2024-08-15 06:12:11 -07:00
|
|
|
// On the first LiveFrame, send all the data from the previous key frame
|
|
|
|
// onward. Afterward, send a single (often non-key) frame at a time.
|
2021-10-28 13:07:39 -07:00
|
|
|
let mut start_at_key = true;
|
|
|
|
loop {
|
2024-08-15 06:12:11 -07:00
|
|
|
tokio::select! {
|
|
|
|
biased;
|
|
|
|
|
|
|
|
next = sub_rx.recv() => {
|
|
|
|
match next {
|
|
|
|
Ok(l) => {
|
|
|
|
keepalive.reset_after(KEEPALIVE_AFTER_IDLE);
|
|
|
|
if !self.stream_live_m4s_chunk(
|
|
|
|
open_id,
|
|
|
|
stream_id,
|
|
|
|
ws,
|
|
|
|
l,
|
|
|
|
start_at_key,
|
|
|
|
).await? {
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
start_at_key = false;
|
|
|
|
}
|
|
|
|
Err(RecvError::Closed) => {
|
|
|
|
bail!(Internal, msg("live stream closed unexpectedly"));
|
|
|
|
}
|
|
|
|
Err(RecvError::Lagged(frames)) => {
|
|
|
|
bail!(
|
|
|
|
ResourceExhausted,
|
|
|
|
msg("subscriber {frames} frames further behind than allowed; \
|
|
|
|
this typically indicates insufficient bandwidth"),
|
|
|
|
)
|
|
|
|
}
|
2023-02-15 07:04:50 -08:00
|
|
|
}
|
2021-10-28 13:07:39 -07:00
|
|
|
}
|
2024-08-15 06:12:11 -07:00
|
|
|
|
|
|
|
_ = keepalive.tick() => {
|
|
|
|
if ws.send(tungstenite::Message::Ping(Vec::new())).await.is_err() {
|
2023-02-15 07:04:50 -08:00
|
|
|
return Ok(());
|
|
|
|
}
|
2021-10-28 13:07:39 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-15 07:04:50 -08:00
|
|
|
/// Sends a single live segment chunk of a `live.m4s` stream, returning `Ok(false)` when
|
|
|
|
/// the connection is lost.
|
2021-10-28 13:07:39 -07:00
|
|
|
async fn stream_live_m4s_chunk(
|
|
|
|
&self,
|
|
|
|
open_id: u32,
|
|
|
|
stream_id: i32,
|
2024-08-23 20:56:40 -07:00
|
|
|
ws: &mut WebSocketStream,
|
2024-08-15 06:12:11 -07:00
|
|
|
live: db::LiveFrame,
|
2021-10-28 13:07:39 -07:00
|
|
|
start_at_key: bool,
|
2023-02-15 07:04:50 -08:00
|
|
|
) -> Result<bool, Error> {
|
2021-10-28 13:07:39 -07:00
|
|
|
let mut builder = mp4::FileBuilder::new(mp4::Type::MediaSegment);
|
|
|
|
let mut row = None;
|
|
|
|
{
|
|
|
|
let db = self.db.lock();
|
|
|
|
let mut rows = 0;
|
|
|
|
db.list_recordings_by_id(stream_id, live.recording..live.recording + 1, &mut |r| {
|
|
|
|
rows += 1;
|
2024-06-01 07:46:11 -07:00
|
|
|
builder.append(&db, &r, live.media_off_90k.clone(), start_at_key)?;
|
2021-10-28 13:07:39 -07:00
|
|
|
row = Some(r);
|
|
|
|
Ok(())
|
|
|
|
})?;
|
|
|
|
}
|
2023-07-09 22:04:17 -07:00
|
|
|
let row = row.ok_or_else(|| err!(Internal, msg("unable to find {live:?}")))?;
|
2021-10-28 13:07:39 -07:00
|
|
|
use http_serve::Entity;
|
|
|
|
let mp4 = builder.build(self.db.clone(), self.dirs_by_stream_id.clone())?;
|
|
|
|
let mut hdrs = header::HeaderMap::new();
|
|
|
|
mp4.add_headers(&mut hdrs);
|
|
|
|
let mime_type = hdrs.get(header::CONTENT_TYPE).unwrap();
|
|
|
|
let (prev_media_duration, prev_runs) = row.prev_media_duration_and_runs.unwrap();
|
|
|
|
let hdr = format!(
|
|
|
|
"Content-Type: {}\r\n\
|
|
|
|
X-Recording-Start: {}\r\n\
|
|
|
|
X-Recording-Id: {}.{}\r\n\
|
|
|
|
X-Media-Time-Range: {}-{}\r\n\
|
|
|
|
X-Prev-Media-Duration: {}\r\n\
|
|
|
|
X-Runs: {}\r\n\
|
|
|
|
X-Video-Sample-Entry-Id: {}\r\n\r\n",
|
|
|
|
mime_type.to_str().unwrap(),
|
|
|
|
row.start.0,
|
|
|
|
open_id,
|
|
|
|
live.recording,
|
|
|
|
live.media_off_90k.start,
|
|
|
|
live.media_off_90k.end,
|
|
|
|
prev_media_duration.0,
|
|
|
|
prev_runs + if row.run_offset == 0 { 1 } else { 0 },
|
|
|
|
&row.video_sample_entry_id
|
|
|
|
);
|
|
|
|
let mut v = hdr.into_bytes();
|
|
|
|
mp4.append_into_vec(&mut v).await?;
|
2023-02-15 07:04:50 -08:00
|
|
|
Ok(ws.send(tungstenite::Message::Binary(v)).await.is_ok())
|
2022-04-13 21:45:45 -07:00
|
|
|
}
|
|
|
|
}
|