mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2026-11-06 16:07:50 +00:00
202 lines
7.5 KiB
Rust
202 lines
7.5 KiB
Rust
// This file is part of Moonfire NVR, a security camera network video recorder.
|
|
// Copyright (C) 2026 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
|
|
|
|
//! Sample entry management (video and, eventually, audio).
|
|
//!
|
|
//! Sample entries store parameters needed to decode the stream, such as width
|
|
//! and height. There are typically a small number of these entries over the
|
|
//! lifetime of the system, and thus they're all kept in RAM, forever. When
|
|
//! new ones are added, they're flushed to the database lazily, allowing
|
|
//! streamers to work without ever waiting for the database. They are never
|
|
//! removed.
|
|
|
|
use std::{collections::VecDeque, sync::Arc};
|
|
|
|
use base::{bail, err, Error, FastHashMap};
|
|
use derive_more::Debug;
|
|
use pretty_hex::PrettyHex as _;
|
|
use rusqlite::{named_params, params};
|
|
|
|
const INSERT_VIDEO_SAMPLE_ENTRY_SQL: &str = r#"
|
|
insert into video_sample_entry (id, width, height, pasp_h_spacing, pasp_v_spacing,
|
|
rfc6381_codec, data)
|
|
values (:id, :width, :height, :pasp_h_spacing, :pasp_v_spacing,
|
|
:rfc6381_codec, :data)
|
|
"#;
|
|
|
|
pub type Handle = Arc<base::Mutex<State, 3>>;
|
|
|
|
pub struct State {
|
|
video_sample_entries_by_id: FastHashMap<i32, Arc<(i32, Video)>>,
|
|
next_video_sample_entry_id: i32,
|
|
|
|
/// These entries have not yet been committed to the database. They must be
|
|
/// committed in the same transaction as any recordings which reference
|
|
/// them.
|
|
video_sample_entries_to_flush: VecDeque<Arc<(i32, Video)>>,
|
|
}
|
|
|
|
/// Entries returned by [`State::get_entries_to_flush`].
|
|
pub(crate) struct EntriesToFlush {
|
|
video: Vec<Arc<(i32, Video)>>,
|
|
}
|
|
|
|
impl EntriesToFlush {
|
|
pub fn perform_inserts(&self, tx: &rusqlite::Transaction) -> Result<(), Error> {
|
|
let mut stmt = tx.prepare_cached(INSERT_VIDEO_SAMPLE_ENTRY_SQL)?;
|
|
for entry in &self.video {
|
|
let (id, ref entry) = **entry;
|
|
stmt.execute(named_params! {
|
|
":id": id,
|
|
":width": entry.width,
|
|
":height": entry.height,
|
|
":pasp_h_spacing": entry.pasp_h_spacing,
|
|
":pasp_v_spacing": entry.pasp_v_spacing,
|
|
":rfc6381_codec": &entry.rfc6381_codec,
|
|
":data": &entry.data,
|
|
})
|
|
.map_err(|e| err!(e, msg("Unable to insert {entry:#?}")).build())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Reports that these entries have been committed to the database.
|
|
pub fn post_flush(&self, state: &mut State) {
|
|
#[cfg(debug_assertions)]
|
|
itertools::assert_equal(
|
|
self.video.iter().map(|e| e.0),
|
|
state
|
|
.video_sample_entries_to_flush
|
|
.iter()
|
|
.take(self.video.len())
|
|
.map(|e| e.0),
|
|
);
|
|
state
|
|
.video_sample_entries_to_flush
|
|
.drain(..self.video.len());
|
|
}
|
|
}
|
|
|
|
impl State {
|
|
pub(crate) fn load(conn: &rusqlite::Connection) -> Result<Self, Error> {
|
|
let mut stmt = conn.prepare(
|
|
r#"
|
|
select
|
|
id,
|
|
width,
|
|
height,
|
|
pasp_h_spacing,
|
|
pasp_v_spacing,
|
|
rfc6381_codec,
|
|
data
|
|
from
|
|
video_sample_entry
|
|
order by id
|
|
"#,
|
|
)?;
|
|
let mut rows = stmt.query(params![])?;
|
|
let mut video_sample_entries_by_id = FastHashMap::default();
|
|
let mut next_video_sample_entry_id = 1;
|
|
while let Some(row) = rows.next()? {
|
|
let id = row.get(0)?;
|
|
let e = Video {
|
|
width: row.get(1)?,
|
|
height: row.get(2)?,
|
|
pasp_h_spacing: row.get(3)?,
|
|
pasp_v_spacing: row.get(4)?,
|
|
rfc6381_codec: row.get(5)?,
|
|
data: row.get(6)?,
|
|
};
|
|
video_sample_entries_by_id.insert(id, Arc::new((id, e)));
|
|
next_video_sample_entry_id = id + 1;
|
|
}
|
|
Ok(Self {
|
|
video_sample_entries_by_id,
|
|
next_video_sample_entry_id,
|
|
video_sample_entries_to_flush: VecDeque::new(),
|
|
})
|
|
}
|
|
|
|
/// Inserts a new video sample entry into the in-RAM database if it's not already present.
|
|
///
|
|
/// Errors only in the case that an entry with the same data exists but different other parameters, which is an error.
|
|
pub fn insert_video(&mut self, entry: Video) -> Result<i32, Error> {
|
|
// Check if it already exists.
|
|
// There shouldn't be too many entries, so it's fine to enumerate everything.
|
|
for value in self.video_sample_entries_by_id.values() {
|
|
let (id, ref v) = **value;
|
|
if v.data == entry.data {
|
|
// The other fields are derived from data, so differences indicate a bug.
|
|
if v.width != entry.width
|
|
|| v.height != entry.height
|
|
|| v.pasp_h_spacing != entry.pasp_h_spacing
|
|
|| v.pasp_v_spacing != entry.pasp_v_spacing
|
|
// XXX: this should also compare rfc6381_codec, but there are a pair of old bugs preventing it.
|
|
// (1) prior to dad664c2, rfc codec was always lowercase, when it should be uppercase.
|
|
// (2) in [dad664c2, b4836f3a) rfc codec was always `avc1.4d401e`!!!
|
|
// || !v.rfc6381_codec.eq_ignore_ascii_case(&entry.rfc6381_codec)
|
|
{
|
|
bail!(
|
|
Internal,
|
|
msg("video_sample_entry id {id}: existing entry {v:?}, new {entry:?}"),
|
|
);
|
|
}
|
|
return Ok(id);
|
|
}
|
|
}
|
|
|
|
let id = self.next_video_sample_entry_id;
|
|
self.next_video_sample_entry_id += 1;
|
|
let arc = Arc::new((id, entry));
|
|
self.video_sample_entries_by_id.insert(id, arc.clone());
|
|
self.video_sample_entries_to_flush.push_back(arc);
|
|
Ok(id)
|
|
}
|
|
|
|
pub fn get_video(&self, id: i32) -> Option<Arc<(i32, Video)>> {
|
|
self.video_sample_entries_by_id.get(&id).cloned()
|
|
}
|
|
|
|
/// Gets entries to flush.
|
|
///
|
|
/// The larger database is expected to perform the following sequence:
|
|
///
|
|
/// 1. (under state lock) get_entries_to_flush()
|
|
/// 2. (without state lock) EntriesToFlush::perform_inserts()
|
|
/// 3. (under state lock) EntriesToFlush::post_flush()
|
|
///
|
|
/// It's an error to have two `EntriesToFlush` instances active at the same
|
|
/// time. `EntriesToFlush::post_flush` assumes that the entries it knows
|
|
/// about are a prefix of the entries that exist in the `State` at time of
|
|
/// call.
|
|
pub(crate) fn get_entries_to_flush(&self) -> EntriesToFlush {
|
|
EntriesToFlush {
|
|
video: self.video_sample_entries_to_flush.iter().cloned().collect(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A concrete box derived from a ISO/IEC 14496-12 section 8.5.2 VisualSampleEntry box. Describes
|
|
/// the codec, width, height, etc.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct Video {
|
|
#[debug("{}", self.data.hex_dump())]
|
|
pub data: Vec<u8>,
|
|
pub rfc6381_codec: String,
|
|
pub width: u16,
|
|
pub height: u16,
|
|
pub pasp_h_spacing: u16,
|
|
pub pasp_v_spacing: u16,
|
|
}
|
|
|
|
impl Video {
|
|
/// Returns the aspect ratio in minimized form.
|
|
pub fn aspect(&self) -> num_rational::Ratio<u32> {
|
|
num_rational::Ratio::new(
|
|
u32::from(self.width) * u32::from(self.pasp_h_spacing),
|
|
u32::from(self.height) * u32::from(self.pasp_v_spacing),
|
|
)
|
|
}
|
|
}
|