mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-11-09 21:49:46 -05:00
Merge branch 'master' into new-schema
This commit is contained in:
@@ -46,7 +46,7 @@ pub struct Args {
|
||||
trash_corrupt_rows: bool,
|
||||
}
|
||||
|
||||
pub fn run(args: &Args) -> Result<i32, Error> {
|
||||
pub fn run(args: Args) -> Result<i32, Error> {
|
||||
let (_db_dir, mut conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
|
||||
check::run(
|
||||
&mut conn,
|
||||
|
||||
@@ -197,6 +197,7 @@ fn press_test_inner(url: Url, username: String, password: String) -> Result<Stri
|
||||
username: if pass_creds { Some(username) } else { None },
|
||||
password: if pass_creds { Some(password) } else { None },
|
||||
transport: retina::client::Transport::Tcp,
|
||||
session_group: Default::default(),
|
||||
},
|
||||
)?;
|
||||
Ok(format!(
|
||||
|
||||
@@ -31,7 +31,7 @@ pub struct Args {
|
||||
db_dir: PathBuf,
|
||||
}
|
||||
|
||||
pub fn run(args: &Args) -> Result<i32, Error> {
|
||||
pub fn run(args: Args) -> Result<i32, Error> {
|
||||
let (_db_dir, conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
|
||||
let clocks = clock::RealClocks {};
|
||||
let db = Arc::new(db::Database::new(clocks, conn, true)?);
|
||||
|
||||
@@ -19,7 +19,7 @@ pub struct Args {
|
||||
db_dir: PathBuf,
|
||||
}
|
||||
|
||||
pub fn run(args: &Args) -> Result<i32, Error> {
|
||||
pub fn run(args: Args) -> Result<i32, Error> {
|
||||
let (_db_dir, mut conn) = super::open_conn(&args.db_dir, super::OpenMode::Create)?;
|
||||
|
||||
// Check if the database has already been initialized.
|
||||
|
||||
@@ -53,7 +53,7 @@ pub struct Args {
|
||||
username: String,
|
||||
}
|
||||
|
||||
pub fn run(args: &Args) -> Result<i32, Error> {
|
||||
pub fn run(args: Args) -> Result<i32, Error> {
|
||||
let clocks = clock::RealClocks {};
|
||||
let (_db_dir, conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
|
||||
let db = std::sync::Arc::new(db::Database::new(clocks, conn, true).unwrap());
|
||||
|
||||
@@ -28,7 +28,9 @@ enum OpenMode {
|
||||
/// The returned `dir::Fd` holds the lock and should be kept open as long as the `Connection` is.
|
||||
fn open_dir(db_dir: &Path, mode: OpenMode) -> Result<dir::Fd, Error> {
|
||||
let dir = dir::Fd::open(db_dir, mode == OpenMode::Create).map_err(|e| {
|
||||
e.context(if e == nix::Error::ENOENT {
|
||||
e.context(if mode == OpenMode::Create {
|
||||
format!("unable to create db dir {}", db_dir.display())
|
||||
} else if e == nix::Error::ENOENT {
|
||||
format!(
|
||||
"db dir {} not found; try running moonfire-nvr init",
|
||||
db_dir.display()
|
||||
@@ -79,3 +81,43 @@ fn open_conn(db_dir: &Path, mode: OpenMode) -> Result<(dir::Fd, rusqlite::Connec
|
||||
)?;
|
||||
Ok((dir, conn))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn open_dir_error_msg() {
|
||||
let tmpdir = tempfile::Builder::new()
|
||||
.prefix("moonfire-nvr-test")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let mut nonexistent_dir = tmpdir.path().to_path_buf();
|
||||
nonexistent_dir.push("nonexistent");
|
||||
let nonexistent_open = open_dir(&nonexistent_dir, OpenMode::ReadOnly).unwrap_err();
|
||||
assert!(
|
||||
nonexistent_open
|
||||
.to_string()
|
||||
.contains("try running moonfire-nvr init"),
|
||||
"unexpected error {}",
|
||||
&nonexistent_open
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_dir_error_msg() {
|
||||
let tmpdir = tempfile::Builder::new()
|
||||
.prefix("moonfire-nvr-test")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let mut nonexistent_dir = tmpdir.path().to_path_buf();
|
||||
nonexistent_dir.push("nonexistent");
|
||||
nonexistent_dir.push("db");
|
||||
let nonexistent_create = open_dir(&nonexistent_dir, OpenMode::Create).unwrap_err();
|
||||
assert!(
|
||||
nonexistent_create.to_string().contains("unable to create"),
|
||||
"unexpected error {}",
|
||||
&nonexistent_create
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,10 @@ use base::clock;
|
||||
use db::{dir, writer};
|
||||
use failure::{bail, Error, ResultExt};
|
||||
use fnv::FnvHashMap;
|
||||
use futures::future::FutureExt;
|
||||
use hyper::service::{make_service_fn, service_fn};
|
||||
use log::error;
|
||||
use log::{info, warn};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use structopt::StructOpt;
|
||||
@@ -150,9 +149,9 @@ fn resolve_zone() -> Result<String, Error> {
|
||||
}
|
||||
};
|
||||
|
||||
// If `TIMEZONE_PATH` is a file, use its contents as the zone name.
|
||||
// If `TIMEZONE_PATH` is a file, use its contents as the zone name, trimming whitespace.
|
||||
match ::std::fs::read_to_string(TIMEZONE_PATH) {
|
||||
Ok(z) => Ok(z),
|
||||
Ok(z) => Ok(z.trim().to_owned()),
|
||||
Err(e) => {
|
||||
bail!(
|
||||
"Unable to resolve timezone from TZ env, {}, or {}. Last error: {}",
|
||||
@@ -170,16 +169,55 @@ struct Syncer {
|
||||
join: thread::JoinHandle<()>,
|
||||
}
|
||||
|
||||
pub fn run(args: &Args) -> Result<i32, Error> {
|
||||
pub fn run(args: Args) -> Result<i32, Error> {
|
||||
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
||||
builder.enable_all();
|
||||
if let Some(worker_threads) = args.worker_threads {
|
||||
builder.worker_threads(worker_threads);
|
||||
}
|
||||
builder.build().unwrap().block_on(async_run(args))
|
||||
let rt = builder.build()?;
|
||||
let r = rt.block_on(async_run(args));
|
||||
|
||||
// tokio normally waits for all spawned tasks to complete, but:
|
||||
// * in the graceful shutdown path, we wait for specific tasks with logging.
|
||||
// * in the immediate shutdown path, we don't want to wait.
|
||||
rt.shutdown_background();
|
||||
|
||||
r
|
||||
}
|
||||
|
||||
async fn async_run(args: &Args) -> Result<i32, Error> {
|
||||
async fn async_run(args: Args) -> Result<i32, Error> {
|
||||
let (shutdown_tx, shutdown_rx) = base::shutdown::channel();
|
||||
let mut shutdown_tx = Some(shutdown_tx);
|
||||
|
||||
tokio::pin! {
|
||||
let int = signal(SignalKind::interrupt())?;
|
||||
let term = signal(SignalKind::terminate())?;
|
||||
let inner = inner(args, shutdown_rx);
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
_ = int.recv() => {
|
||||
info!("Received SIGINT; shutting down gracefully. \
|
||||
Send another SIGINT or SIGTERM to shut down immediately.");
|
||||
shutdown_tx.take();
|
||||
},
|
||||
_ = term.recv() => {
|
||||
info!("Received SIGTERM; shutting down gracefully. \
|
||||
Send another SIGINT or SIGTERM to shut down immediately.");
|
||||
shutdown_tx.take();
|
||||
},
|
||||
result = &mut inner => return result,
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
_ = int.recv() => bail!("immediate shutdown due to second signal (SIGINT)"),
|
||||
_ = term.recv() => bail!("immediate shutdown due to second singal (SIGTERM)"),
|
||||
result = &mut inner => result,
|
||||
}
|
||||
}
|
||||
|
||||
async fn inner(args: Args, shutdown_rx: base::shutdown::Receiver) -> Result<i32, Error> {
|
||||
let clocks = clock::RealClocks {};
|
||||
let (_db_dir, conn) = super::open_conn(
|
||||
&args.db_dir,
|
||||
@@ -214,8 +252,9 @@ async fn async_run(args: &Args) -> Result<i32, Error> {
|
||||
})?);
|
||||
|
||||
// Start a streamer for each stream.
|
||||
let shutdown_streamers = Arc::new(AtomicBool::new(false));
|
||||
let mut streamers = Vec::new();
|
||||
let mut session_groups_by_camera: FnvHashMap<i32, Arc<retina::client::SessionGroup>> =
|
||||
FnvHashMap::default();
|
||||
let syncers = if !args.read_only {
|
||||
let l = db.lock();
|
||||
let mut dirs = FnvHashMap::with_capacity_and_hasher(
|
||||
@@ -227,7 +266,7 @@ async fn async_run(args: &Args) -> Result<i32, Error> {
|
||||
db: &db,
|
||||
opener: args.rtsp_library.opener(),
|
||||
transport: args.rtsp_transport,
|
||||
shutdown: &shutdown_streamers,
|
||||
shutdown_rx: &shutdown_rx,
|
||||
};
|
||||
|
||||
// Get the directories that need syncers.
|
||||
@@ -253,7 +292,7 @@ async fn async_run(args: &Args) -> Result<i32, Error> {
|
||||
drop(l);
|
||||
let mut syncers = FnvHashMap::with_capacity_and_hasher(dirs.len(), Default::default());
|
||||
for (id, dir) in dirs.drain() {
|
||||
let (channel, join) = writer::start_syncer(db.clone(), id)?;
|
||||
let (channel, join) = writer::start_syncer(db.clone(), shutdown_rx.clone(), id)?;
|
||||
syncers.insert(id, Syncer { dir, channel, join });
|
||||
}
|
||||
|
||||
@@ -279,6 +318,10 @@ async fn async_run(args: &Args) -> Result<i32, Error> {
|
||||
};
|
||||
let rotate_offset_sec = streamer::ROTATE_INTERVAL_SEC * i as i64 / streams as i64;
|
||||
let syncer = syncers.get(&sample_file_dir_id).unwrap();
|
||||
let session_group = session_groups_by_camera
|
||||
.entry(camera.id)
|
||||
.or_default()
|
||||
.clone();
|
||||
let mut streamer = streamer::Streamer::new(
|
||||
&env,
|
||||
syncer.dir.clone(),
|
||||
@@ -286,6 +329,7 @@ async fn async_run(args: &Args) -> Result<i32, Error> {
|
||||
*id,
|
||||
camera,
|
||||
stream,
|
||||
session_group,
|
||||
rotate_offset_sec,
|
||||
streamer::ROTATE_INTERVAL_SEC,
|
||||
)?;
|
||||
@@ -319,39 +363,44 @@ async fn async_run(args: &Args) -> Result<i32, Error> {
|
||||
.with_context(|_| format!("unable to bind --http-addr={}", &args.http_addr))?
|
||||
.tcp_nodelay(true)
|
||||
.serve(make_svc);
|
||||
|
||||
let mut int = signal(SignalKind::interrupt())?;
|
||||
let mut term = signal(SignalKind::terminate())?;
|
||||
let shutdown = futures::future::select(Box::pin(int.recv()), Box::pin(term.recv()));
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = futures::channel::oneshot::channel();
|
||||
let server = server.with_graceful_shutdown(shutdown_rx.map(|_| ()));
|
||||
let server = server.with_graceful_shutdown(shutdown_rx.future());
|
||||
let server_handle = tokio::spawn(server);
|
||||
|
||||
info!("Ready to serve HTTP requests");
|
||||
shutdown.await;
|
||||
shutdown_tx.send(()).unwrap();
|
||||
let _ = shutdown_rx.as_future().await;
|
||||
|
||||
info!("Shutting down streamers.");
|
||||
shutdown_streamers.store(true, Ordering::SeqCst);
|
||||
for streamer in streamers.drain(..) {
|
||||
streamer.join().unwrap();
|
||||
}
|
||||
|
||||
if let Some(mut ss) = syncers {
|
||||
// The syncers shut down when all channels to them have been dropped.
|
||||
// The database maintains one; and `ss` holds one. Drop both.
|
||||
db.lock().clear_on_flush();
|
||||
for (_, s) in ss.drain() {
|
||||
drop(s.channel);
|
||||
s.join.join().unwrap();
|
||||
info!("Shutting down streamers and syncers.");
|
||||
tokio::task::spawn_blocking({
|
||||
let db = db.clone();
|
||||
move || {
|
||||
for streamer in streamers.drain(..) {
|
||||
streamer.join().unwrap();
|
||||
}
|
||||
if let Some(mut ss) = syncers {
|
||||
// The syncers shut down when all channels to them have been dropped.
|
||||
// The database maintains one; and `ss` holds one. Drop both.
|
||||
db.lock().clear_on_flush();
|
||||
for (_, s) in ss.drain() {
|
||||
drop(s.channel);
|
||||
s.join.join().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
db.lock().clear_watches();
|
||||
|
||||
info!("Waiting for HTTP requests to finish.");
|
||||
server_handle.await??;
|
||||
|
||||
info!("Waiting for TEARDOWN requests to complete.");
|
||||
for g in session_groups_by_camera.values() {
|
||||
if let Err(e) = g.await_teardown().await {
|
||||
error!("{}", e);
|
||||
}
|
||||
}
|
||||
|
||||
info!("Exiting.");
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ pub struct Args {
|
||||
arg: Vec<OsString>,
|
||||
}
|
||||
|
||||
pub fn run(args: &Args) -> Result<i32, Error> {
|
||||
pub fn run(args: Args) -> Result<i32, Error> {
|
||||
let mode = if args.read_only {
|
||||
OpenMode::ReadOnly
|
||||
} else {
|
||||
|
||||
@@ -17,7 +17,7 @@ pub struct Args {
|
||||
timestamps: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn run(args: &Args) -> Result<i32, Error> {
|
||||
pub fn run(args: Args) -> Result<i32, Error> {
|
||||
for timestamp in &args.timestamps {
|
||||
let t = db::recording::Time::parse(timestamp)?;
|
||||
println!("{} == {}", t, t.0);
|
||||
|
||||
@@ -40,7 +40,7 @@ pub struct Args {
|
||||
no_vacuum: bool,
|
||||
}
|
||||
|
||||
pub fn run(args: &Args) -> Result<i32, Error> {
|
||||
pub fn run(args: Args) -> Result<i32, Error> {
|
||||
let (_db_dir, mut conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
|
||||
|
||||
db::upgrade::run(
|
||||
|
||||
@@ -477,6 +477,9 @@ pub struct Recording {
|
||||
|
||||
#[serde(skip_serializing_if = "Not::not")]
|
||||
pub growing: bool,
|
||||
|
||||
#[serde(skip_serializing_if = "Not::not")]
|
||||
pub has_trailing_zero: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
||||
@@ -59,16 +59,16 @@ enum Args {
|
||||
}
|
||||
|
||||
impl Args {
|
||||
fn run(&self) -> Result<i32, failure::Error> {
|
||||
fn run(self) -> Result<i32, failure::Error> {
|
||||
match self {
|
||||
Args::Check(ref a) => cmds::check::run(a),
|
||||
Args::Config(ref a) => cmds::config::run(a),
|
||||
Args::Init(ref a) => cmds::init::run(a),
|
||||
Args::Login(ref a) => cmds::login::run(a),
|
||||
Args::Run(ref a) => cmds::run::run(a),
|
||||
Args::Sql(ref a) => cmds::sql::run(a),
|
||||
Args::Ts(ref a) => cmds::ts::run(a),
|
||||
Args::Upgrade(ref a) => cmds::upgrade::run(a),
|
||||
Args::Check(a) => cmds::check::run(a),
|
||||
Args::Config(a) => cmds::config::run(a),
|
||||
Args::Init(a) => cmds::init::run(a),
|
||||
Args::Login(a) => cmds::login::run(a),
|
||||
Args::Run(a) => cmds::run::run(a),
|
||||
Args::Sql(a) => cmds::sql::run(a),
|
||||
Args::Ts(a) => cmds::ts::run(a),
|
||||
Args::Upgrade(a) => cmds::upgrade::run(a),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2277,7 +2277,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_mp4_to_db(db: &TestDb<RealClocks>) {
|
||||
fn copy_mp4_to_db(db: &mut TestDb<RealClocks>) {
|
||||
let (extra_data, mut input) = stream::FFMPEG
|
||||
.open(
|
||||
"test".to_owned(),
|
||||
@@ -2322,7 +2322,13 @@ mod tests {
|
||||
};
|
||||
frame_time += recording::Duration(i64::from(pkt.duration));
|
||||
output
|
||||
.write(pkt.data, frame_time, pkt.pts, pkt.is_key)
|
||||
.write(
|
||||
&mut db.shutdown_rx,
|
||||
pkt.data,
|
||||
frame_time,
|
||||
pkt.pts,
|
||||
pkt.is_key,
|
||||
)
|
||||
.unwrap();
|
||||
end_pts = Some(pkt.pts + i64::from(pkt.duration));
|
||||
}
|
||||
@@ -2811,8 +2817,8 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_round_trip() {
|
||||
testutil::init();
|
||||
let db = TestDb::new(RealClocks {});
|
||||
copy_mp4_to_db(&db);
|
||||
let mut db = TestDb::new(RealClocks {});
|
||||
copy_mp4_to_db(&mut db);
|
||||
let mp4 = create_mp4_from_db(&db, 0, 0, false);
|
||||
traverse(mp4.clone()).await;
|
||||
let new_filename = write_mp4(&mp4, db.tmpdir.path()).await;
|
||||
@@ -2840,8 +2846,8 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_with_subtitles() {
|
||||
testutil::init();
|
||||
let db = TestDb::new(RealClocks {});
|
||||
copy_mp4_to_db(&db);
|
||||
let mut db = TestDb::new(RealClocks {});
|
||||
copy_mp4_to_db(&mut db);
|
||||
let mp4 = create_mp4_from_db(&db, 0, 0, true);
|
||||
traverse(mp4.clone()).await;
|
||||
let new_filename = write_mp4(&mp4, db.tmpdir.path()).await;
|
||||
@@ -2869,8 +2875,8 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_with_edit_list() {
|
||||
testutil::init();
|
||||
let db = TestDb::new(RealClocks {});
|
||||
copy_mp4_to_db(&db);
|
||||
let mut db = TestDb::new(RealClocks {});
|
||||
copy_mp4_to_db(&mut db);
|
||||
let mp4 = create_mp4_from_db(&db, 1, 0, false);
|
||||
traverse(mp4.clone()).await;
|
||||
let new_filename = write_mp4(&mp4, db.tmpdir.path()).await;
|
||||
@@ -2898,8 +2904,8 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_with_edit_list_and_subtitles() {
|
||||
testutil::init();
|
||||
let db = TestDb::new(RealClocks {});
|
||||
copy_mp4_to_db(&db);
|
||||
let mut db = TestDb::new(RealClocks {});
|
||||
copy_mp4_to_db(&mut db);
|
||||
let off = 2 * TIME_UNITS_PER_SEC;
|
||||
let mp4 = create_mp4_from_db(&db, i32::try_from(off).unwrap(), 0, true);
|
||||
traverse(mp4.clone()).await;
|
||||
@@ -2928,8 +2934,8 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_with_shorten() {
|
||||
testutil::init();
|
||||
let db = TestDb::new(RealClocks {});
|
||||
copy_mp4_to_db(&db);
|
||||
let mut db = TestDb::new(RealClocks {});
|
||||
copy_mp4_to_db(&mut db);
|
||||
let mp4 = create_mp4_from_db(&db, 0, 1, false);
|
||||
traverse(mp4.clone()).await;
|
||||
let new_filename = write_mp4(&mp4, db.tmpdir.path()).await;
|
||||
|
||||
@@ -15,6 +15,7 @@ use std::convert::TryFrom;
|
||||
use std::ffi::CString;
|
||||
use std::pin::Pin;
|
||||
use std::result::Result;
|
||||
use std::sync::Arc;
|
||||
use url::Url;
|
||||
|
||||
static START_FFMPEG: parking_lot::Once = parking_lot::Once::new();
|
||||
@@ -62,6 +63,7 @@ pub enum Source<'a> {
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
transport: Transport,
|
||||
session_group: Arc<retina::client::SessionGroup>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -73,6 +75,7 @@ pub enum Source {
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
transport: Transport,
|
||||
session_group: Arc<retina::client::SessionGroup>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -141,6 +144,7 @@ impl Opener for Ffmpeg {
|
||||
username,
|
||||
password,
|
||||
transport,
|
||||
..
|
||||
} => {
|
||||
let mut open_options = ffmpeg::avutil::Dictionary::new();
|
||||
open_options
|
||||
@@ -301,6 +305,7 @@ impl Opener for RetinaOpener {
|
||||
username,
|
||||
password,
|
||||
transport,
|
||||
session_group,
|
||||
} => (
|
||||
url,
|
||||
retina::client::SessionOptions::default()
|
||||
@@ -313,6 +318,7 @@ impl Opener for RetinaOpener {
|
||||
_ => bail!("must supply username when supplying password"),
|
||||
})
|
||||
.transport(transport)
|
||||
.session_group(session_group)
|
||||
.user_agent(format!("Moonfire NVR {}", env!("CARGO_PKG_VERSION"))),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@ use db::{dir, recording, writer, Camera, Database, Stream};
|
||||
use failure::{bail, format_err, Error};
|
||||
use log::{debug, info, trace, warn};
|
||||
use std::result::Result;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use url::Url;
|
||||
|
||||
@@ -22,7 +21,7 @@ where
|
||||
pub opener: &'a dyn stream::Opener,
|
||||
pub transport: retina::client::Transport,
|
||||
pub db: &'tmp Arc<Database<C>>,
|
||||
pub shutdown: &'tmp Arc<AtomicBool>,
|
||||
pub shutdown_rx: &'tmp base::shutdown::Receiver,
|
||||
}
|
||||
|
||||
/// Connects to a given RTSP stream and writes recordings to the database via [`writer::Writer`].
|
||||
@@ -31,7 +30,7 @@ pub struct Streamer<'a, C>
|
||||
where
|
||||
C: Clocks + Clone,
|
||||
{
|
||||
shutdown: Arc<AtomicBool>,
|
||||
shutdown_rx: base::shutdown::Receiver,
|
||||
|
||||
// State below is only used by the thread in Run.
|
||||
rotate_offset_sec: i64,
|
||||
@@ -42,6 +41,7 @@ where
|
||||
opener: &'a dyn stream::Opener,
|
||||
transport: retina::client::Transport,
|
||||
stream_id: i32,
|
||||
session_group: Arc<retina::client::SessionGroup>,
|
||||
short_name: String,
|
||||
url: Url,
|
||||
username: String,
|
||||
@@ -59,6 +59,7 @@ where
|
||||
stream_id: i32,
|
||||
c: &Camera,
|
||||
s: &Stream,
|
||||
session_group: Arc<retina::client::SessionGroup>,
|
||||
rotate_offset_sec: i64,
|
||||
rotate_interval_sec: i64,
|
||||
) -> Result<Self, Error> {
|
||||
@@ -71,7 +72,7 @@ where
|
||||
bail!("RTSP URL shouldn't include credentials");
|
||||
}
|
||||
Ok(Streamer {
|
||||
shutdown: env.shutdown.clone(),
|
||||
shutdown_rx: env.shutdown_rx.clone(),
|
||||
rotate_offset_sec,
|
||||
rotate_interval_sec,
|
||||
db: env.db.clone(),
|
||||
@@ -80,6 +81,7 @@ where
|
||||
opener: env.opener,
|
||||
transport: env.transport,
|
||||
stream_id,
|
||||
session_group,
|
||||
short_name: format!("{}-{}", c.short_name, s.type_.as_str()),
|
||||
url: url.clone(),
|
||||
username: c.config.username.clone(),
|
||||
@@ -95,7 +97,7 @@ where
|
||||
/// Note that when using Retina as the RTSP library, this must be called
|
||||
/// within a tokio runtime context; see [tokio::runtime::Handle].
|
||||
pub fn run(&mut self) {
|
||||
while !self.shutdown.load(Ordering::SeqCst) {
|
||||
while self.shutdown_rx.check().is_ok() {
|
||||
if let Err(e) = self.run_once() {
|
||||
let sleep_time = time::Duration::seconds(1);
|
||||
warn!(
|
||||
@@ -114,6 +116,31 @@ where
|
||||
info!("{}: Opening input: {}", self.short_name, self.url.as_str());
|
||||
let clocks = self.db.clocks();
|
||||
|
||||
let mut waited = false;
|
||||
loop {
|
||||
let status = self.session_group.stale_sessions();
|
||||
if let Some(max_expires) = status.max_expires {
|
||||
log::info!(
|
||||
"{}: waiting up to {:?} for TEARDOWN or expiration of {} stale sessions",
|
||||
&self.short_name,
|
||||
max_expires.saturating_duration_since(tokio::time::Instant::now()),
|
||||
status.num_sessions
|
||||
);
|
||||
tokio::runtime::Handle::current().block_on(async {
|
||||
tokio::select! {
|
||||
_ = self.session_group.await_stale_sessions(&status) => Ok(()),
|
||||
_ = self.shutdown_rx.as_future() => Err(base::shutdown::ShutdownError),
|
||||
}
|
||||
})?;
|
||||
waited = true;
|
||||
} else {
|
||||
if waited {
|
||||
log::info!("{}: done waiting; no more stale sessions", &self.short_name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let (extra_data, mut stream) = {
|
||||
let _t = TimerGuard::new(&clocks, || format!("opening {}", self.url.as_str()));
|
||||
self.opener.open(
|
||||
@@ -131,6 +158,7 @@ where
|
||||
Some(self.password.clone())
|
||||
},
|
||||
transport: self.transport,
|
||||
session_group: self.session_group.clone(),
|
||||
},
|
||||
)?
|
||||
};
|
||||
@@ -150,7 +178,7 @@ where
|
||||
self.stream_id,
|
||||
video_sample_entry_id,
|
||||
);
|
||||
while !self.shutdown.load(Ordering::SeqCst) {
|
||||
while self.shutdown_rx.check().is_ok() {
|
||||
let pkt = {
|
||||
let _t = TimerGuard::new(&clocks, || "getting next packet");
|
||||
stream.next()
|
||||
@@ -207,7 +235,13 @@ where
|
||||
}
|
||||
};
|
||||
let _t = TimerGuard::new(&clocks, || format!("writing {} bytes", pkt.data.len()));
|
||||
w.write(pkt.data, local_time, pkt.pts, pkt.is_key)?;
|
||||
w.write(
|
||||
&mut self.shutdown_rx,
|
||||
pkt.data,
|
||||
local_time,
|
||||
pkt.pts,
|
||||
pkt.is_key,
|
||||
)?;
|
||||
rotate = Some(r);
|
||||
}
|
||||
if rotate.is_some() {
|
||||
@@ -229,7 +263,6 @@ mod tests {
|
||||
use parking_lot::Mutex;
|
||||
use std::cmp;
|
||||
use std::convert::TryFrom;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use time;
|
||||
|
||||
@@ -305,7 +338,7 @@ mod tests {
|
||||
struct MockOpener {
|
||||
expected_url: url::Url,
|
||||
streams: Mutex<Vec<(h264::ExtraData, Box<dyn stream::Stream>)>>,
|
||||
shutdown: Arc<AtomicBool>,
|
||||
shutdown_tx: Mutex<Option<base::shutdown::Sender>>,
|
||||
}
|
||||
|
||||
impl stream::Opener for MockOpener {
|
||||
@@ -326,7 +359,7 @@ mod tests {
|
||||
}
|
||||
None => {
|
||||
trace!("MockOpener shutting down");
|
||||
self.shutdown.store(true, Ordering::SeqCst);
|
||||
self.shutdown_tx.lock().take();
|
||||
bail!("done")
|
||||
}
|
||||
}
|
||||
@@ -373,16 +406,17 @@ mod tests {
|
||||
stream.ts_offset = 123456; // starting pts of the input should be irrelevant
|
||||
stream.ts_offset_pkts_left = u32::max_value();
|
||||
stream.pkts_left = u32::max_value();
|
||||
let (shutdown_tx, shutdown_rx) = base::shutdown::channel();
|
||||
let opener = MockOpener {
|
||||
expected_url: url::Url::parse("rtsp://test-camera/main").unwrap(),
|
||||
streams: Mutex::new(vec![(extra_data, Box::new(stream))]),
|
||||
shutdown: Arc::new(AtomicBool::new(false)),
|
||||
shutdown_tx: Mutex::new(Some(shutdown_tx)),
|
||||
};
|
||||
let db = testutil::TestDb::new(clocks.clone());
|
||||
let env = super::Environment {
|
||||
opener: &opener,
|
||||
db: &db.db,
|
||||
shutdown: &opener.shutdown,
|
||||
shutdown_rx: &shutdown_rx,
|
||||
transport: retina::client::Transport::Tcp,
|
||||
};
|
||||
let mut stream;
|
||||
@@ -402,6 +436,7 @@ mod tests {
|
||||
testutil::TEST_STREAM_ID,
|
||||
camera,
|
||||
s,
|
||||
Arc::new(retina::client::SessionGroup::default()),
|
||||
0,
|
||||
3,
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ use core::borrow::Borrow;
|
||||
use core::str::FromStr;
|
||||
use db::dir::SampleFileDir;
|
||||
use db::{auth, recording};
|
||||
use failure::{bail, format_err, Error};
|
||||
use failure::{format_err, Error};
|
||||
use fnv::FnvHashMap;
|
||||
use futures::stream::StreamExt;
|
||||
use futures::{future::Either, sink::SinkExt};
|
||||
@@ -762,6 +762,7 @@ impl Service {
|
||||
video_samples: row.video_samples,
|
||||
video_sample_entry_id: row.video_sample_entry_id,
|
||||
growing: row.growing,
|
||||
has_trailing_zero: row.has_trailing_zero,
|
||||
});
|
||||
if !out
|
||||
.video_sample_entries
|
||||
@@ -856,7 +857,8 @@ impl Service {
|
||||
|
||||
if let Some(o) = s.open_id {
|
||||
if r.open_id != o {
|
||||
bail!(
|
||||
bail_t!(
|
||||
NotFound,
|
||||
"recording {} has open id {}, requested {}",
|
||||
r.id,
|
||||
r.open_id,
|
||||
@@ -868,9 +870,14 @@ impl Service {
|
||||
// Check for missing recordings.
|
||||
match prev {
|
||||
None if recording_id == s.ids.start => {}
|
||||
None => bail!("no such recording {}/{}", stream_id, s.ids.start),
|
||||
None => bail_t!(
|
||||
NotFound,
|
||||
"no such recording {}/{}",
|
||||
stream_id,
|
||||
s.ids.start
|
||||
),
|
||||
Some(id) if r.id.recording() != id + 1 => {
|
||||
bail!("no such recording {}/{}", stream_id, id + 1);
|
||||
bail_t!(NotFound, "no such recording {}/{}", stream_id, id + 1);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
@@ -909,8 +916,7 @@ impl Service {
|
||||
}
|
||||
cur_off += wd;
|
||||
Ok(())
|
||||
})
|
||||
.map_err(internal_server_err)?;
|
||||
})?;
|
||||
|
||||
// Check for missing recordings.
|
||||
match prev {
|
||||
@@ -1368,15 +1374,16 @@ impl<'a> StaticFileRequest<'a> {
|
||||
};
|
||||
let ext = &path[last_dot + 1..];
|
||||
let mime = match ext {
|
||||
"css" => "text/css",
|
||||
"html" => "text/html",
|
||||
"ico" => "image/x-icon",
|
||||
"js" | "map" => "text/javascript",
|
||||
"json" => "application/json",
|
||||
"png" => "image/png",
|
||||
"webmanifest" => "application/manifest+json",
|
||||
"svg" => "image/svg+xml",
|
||||
"txt" => "text/plain",
|
||||
"webmanifest" => "application/manifest+json",
|
||||
"woff2" => "font/woff2",
|
||||
"css" => "text/css",
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user