mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2024-12-25 14:45:54 -05:00
Rust rewrite
I should have submitted/pushed more incrementally but just played with it on my computer as I was learning the language. The new Rust version more or less matches the functionality of the current C++ version, although there are many caveats listed below. Upgrade notes: when moving from the C++ version, I recommend dropping and recreating the "recording_cover" index in SQLite3 to pick up the addition of the "video_sync_samples" column: $ sudo systemctl stop moonfire-nvr $ sudo -u moonfire-nvr sqlite3 /var/lib/moonfire-nvr/db/db sqlite> drop index recording_cover; sqlite3> create index ...rest of command as in schema.sql...; sqlite3> ^D Some known visible differences from the C++ version: * .mp4 generation queries SQLite3 differently. Before it would just get all video indexes in a single query. Now it leads with a query that should be satisfiable by the covering index (assuming the index has been recreated as noted above), then queries individual recording's indexes as needed to fill a LRU cache. I believe this is roughly similar speed for the initial hit (which generates the moov part of the file) and significantly faster when seeking. I would have done it a while ago with the C++ version but didn't want to track down a lru cache library. It was easier to find with Rust. * On startup, the Rust version cleans up old reserved files. This is as in the design; the C++ version was just missing this code. * The .html recording list output is a little different. It's in ascending order, with the most current segment shorten than an hour rather than the oldest. This is less ergonomic, but it was easy. I could fix it or just wait to obsolete it with some fancier JavaScript UI. * commandline argument parsing and logging have changed formats due to different underlying libraries. * The JSON output isn't quite right (matching the spec / C++ implementation) yet. Additional caveats: * I haven't done any proof-reading of prep.sh + install instructions. * There's a lot of code quality work to do: adding (back) comments and test coverage, developing a good Rust style. * The ffmpeg foreign function interface is particularly sketchy. I'd eventually like to switch to something based on autogenerated bindings. I'd also like to use pure Rust code where practical, but once I do on-NVR motion detection I'll need to existing C/C++ libraries for speed (H.264 decoding + OpenCL-based analysis).
This commit is contained in:
parent
0f75e4f94a
commit
0a7535536d
17
.gitignore
vendored
17
.gitignore
vendored
@ -1,13 +1,6 @@
|
||||
*.swp
|
||||
build
|
||||
debug
|
||||
release
|
||||
obj-*
|
||||
cameras.sql
|
||||
debian/files
|
||||
debian/moonfire-nvr.debhelper.log
|
||||
debian/moonfire-nvr.postinst.debhelper
|
||||
debian/moonfire-nvr.postrm.debhelper
|
||||
debian/moonfire-nvr.prerm.debhelper
|
||||
debian/moonfire-nvr.substvars
|
||||
debian/moonfire-nvr/
|
||||
Cargo.lock
|
||||
.project
|
||||
.settings
|
||||
*.swp
|
||||
target
|
||||
|
154
CMakeLists.txt
154
CMakeLists.txt
@ -1,154 +0,0 @@
|
||||
# This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
# Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the
|
||||
# OpenSSL library under certain conditions as described in each
|
||||
# individual source file, and distribute linked combinations including
|
||||
# the two.
|
||||
#
|
||||
# You must obey the GNU General Public License in all respects for all
|
||||
# of the code used other than OpenSSL. If you modify file(s) with this
|
||||
# exception, you may extend this exception to your version of the
|
||||
# file(s), but you are not obligated to do so. If you do not wish to do
|
||||
# so, delete this exception statement from your version. If you delete
|
||||
# this exception statement from all source files in the program, then
|
||||
# also delete it here.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# CMakeLists.txt: top-level definitions for building Moonfire NVR.
|
||||
|
||||
cmake_minimum_required(VERSION 3.0.2)
|
||||
project(moonfire-nvr)
|
||||
|
||||
if(CMAKE_VERSION VERSION_LESS "3.1")
|
||||
set(CMAKE_CXX_FLAGS "--std=c++11 ${CMAKE_CXX_FLAGS}")
|
||||
else()
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_FLAGS "-Wall -Werror -pedantic-errors -ggdb ${CMAKE_CXX_FLAGS}")
|
||||
|
||||
option(LTO "Use link-time optimization" ON)
|
||||
option(FPROFILE_GENERATE "Compile executable to generate usage data" OFF)
|
||||
option(FPROFILE_USE "Compile executable using generated usage data" OFF)
|
||||
|
||||
if(LTO)
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -flto")
|
||||
set(CMAKE_AR "gcc-ar")
|
||||
set(CMAKE_RANLIB "gcc-ranlib")
|
||||
set(CMAKE_LD "gcc-ld")
|
||||
endif()
|
||||
|
||||
if(PROFILE_GENERATE)
|
||||
set(CMAKE_CXX_FLAGS "-fprofile-generate ${CMAKE_CXX_FLAGS}")
|
||||
endif()
|
||||
if(PROFILE_USE)
|
||||
set(CMAKE_CXX_FLAGS "-fprofile-use -fprofile-correction -Wno-error=coverage-mismatch ${CMAKE_CXX_FLAGS}")
|
||||
endif()
|
||||
|
||||
#
|
||||
# Dependencies.
|
||||
#
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
# https://gflags.github.io/gflags/#cmake mentions a cmake module, but at
|
||||
# least on Ubuntu 15.10, libgflags-dev does not include it. There's no
|
||||
# pkgconfig either. Do this by hand.
|
||||
find_library(GFLAGS_LIBRARIES gflags)
|
||||
find_library(RE2_LIBRARIES re2)
|
||||
find_library(PROFILER_LIBRARIES profiler)
|
||||
|
||||
# https://cmake.org/cmake/help/v3.0/module/FindPkgConfig.html
|
||||
find_package(PkgConfig)
|
||||
pkg_check_modules(FFMPEG REQUIRED libavutil libavcodec libavformat)
|
||||
pkg_check_modules(LIBEVENT REQUIRED libevent>=2.1)
|
||||
pkg_check_modules(JSONCPP REQUIRED jsoncpp)
|
||||
pkg_check_modules(GLOG REQUIRED libglog)
|
||||
pkg_check_modules(OPENSSL REQUIRED libcrypto)
|
||||
pkg_check_modules(SQLITE REQUIRED sqlite3)
|
||||
pkg_check_modules(UUID REQUIRED uuid)
|
||||
|
||||
# Check if ffmpeg support "stimeout".
|
||||
set(CMAKE_REQUIRED_INCLUDES ${FFMPEG_INCLUDES})
|
||||
set(CMAKE_REQUIRED_LIBRARIES ${FFMPEG_LIBRARIES})
|
||||
include(CheckCSourceRuns)
|
||||
check_c_source_runs([[
|
||||
#include <stdlib.h>
|
||||
#include <libavutil/opt.h>
|
||||
#include <libavformat/avformat.h>
|
||||
int main(int argc, char **argv) {
|
||||
av_register_all();
|
||||
AVInputFormat *input = av_find_input_format("rtsp");
|
||||
const AVClass *klass = input->priv_class;
|
||||
const AVOption *opt =
|
||||
av_opt_find2(&klass, "stimeout", NULL, 0, AV_OPT_SEARCH_FAKE_OBJ, NULL);
|
||||
return (opt != NULL) ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
]] HAVE_STIMEOUT)
|
||||
if(NOT HAVE_STIMEOUT)
|
||||
message(WARNING [[
|
||||
Your libavformat library lacks support for the "stimeout" rtsp option.
|
||||
Moonfire NVR will not be able to detect network partitions or retry.
|
||||
Consider installing a recent ffmpeg, from source if necessary.
|
||||
]])
|
||||
else()
|
||||
message(STATUS "libavformat library has support for \"stimeout\" - good.")
|
||||
endif()
|
||||
|
||||
enable_testing()
|
||||
|
||||
# http://www.kaizou.org/2014/11/gtest-cmake/
|
||||
include(ExternalProject)
|
||||
ExternalProject_Add(
|
||||
GTestProject
|
||||
URL "https://github.com/google/googletest/archive/release-1.8.0.tar.gz"
|
||||
URL_HASH "SHA1=e7e646a6204638fe8e87e165292b8dd9cd4c36ed"
|
||||
INSTALL_COMMAND "")
|
||||
ExternalProject_Get_Property(GTestProject source_dir binary_dir)
|
||||
set(GTest_INCLUDE_DIR ${source_dir}/googletest/include)
|
||||
add_library(GTest STATIC IMPORTED)
|
||||
add_dependencies(GTest GTestProject)
|
||||
set_target_properties(GTest PROPERTIES
|
||||
IMPORTED_LOCATION "${binary_dir}/googlemock/gtest/${CMAKE_STATIC_LIBRARY_PREFIX}gtest${CMAKE_STATIC_LIBRARY_SUFFIX}"
|
||||
IMPORTED_LINK_INTERFACE_LIBRARIES "${CMAKE_THREAD_LIBS_INIT}")
|
||||
set(GMock_INCLUDE_DIR ${source_dir}/googlemock/include)
|
||||
add_library(GMock STATIC IMPORTED)
|
||||
add_dependencies(GMock GTestProject)
|
||||
set_target_properties(GMock PROPERTIES
|
||||
IMPORTED_LOCATION "${binary_dir}/googlemock/${CMAKE_STATIC_LIBRARY_PREFIX}gmock${CMAKE_STATIC_LIBRARY_SUFFIX}"
|
||||
IMPORTED_LINK_INTERFACE_LIBRARIES "${CMAKE_THREAD_LIBS_INIT}")
|
||||
|
||||
ExternalProject_Add(
|
||||
GBenchmarkProject
|
||||
URL "https://github.com/google/benchmark/archive/v1.0.0.tar.gz"
|
||||
URL_HASH "SHA1=4f778985dce02d2e63262e6f388a24b595254a93"
|
||||
CMAKE_ARGS
|
||||
-DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
|
||||
INSTALL_COMMAND "")
|
||||
ExternalProject_Get_Property(GBenchmarkProject source_dir binary_dir)
|
||||
set(GBenchmark_INCLUDE_DIR ${source_dir}/include)
|
||||
add_library(GBenchmark STATIC IMPORTED)
|
||||
add_dependencies(GBenchmark GBenchmarkProject)
|
||||
set_target_properties(GBenchmark PROPERTIES
|
||||
IMPORTED_LOCATION "${binary_dir}/src/${CMAKE_STATIC_LIBRARY_PREFIX}benchmark${CMAKE_STATIC_LIBRARY_SUFFIX}"
|
||||
IMPORTED_LINK_INTERFACE_LIBRARIES "${CMAKE_THREAD_LIBS_INIT}")
|
||||
|
||||
#
|
||||
# Subdirectories.
|
||||
#
|
||||
|
||||
add_subdirectory(src)
|
55
Cargo.toml
Normal file
55
Cargo.toml
Normal file
@ -0,0 +1,55 @@
|
||||
[package]
|
||||
name = "moonfire-nvr"
|
||||
version = "0.1.0"
|
||||
authors = ["Scott Lamb <slamb@slamb.org>"]
|
||||
|
||||
[dependencies]
|
||||
byteorder = "0.5"
|
||||
clippy = "0.0.103"
|
||||
chan = "0.1"
|
||||
chan-signal = "0.1"
|
||||
docopt = "0.6"
|
||||
fnv = "1.0"
|
||||
hyper = "0.9"
|
||||
lazy_static = "0.2"
|
||||
libc = "0.2"
|
||||
log = { version = "0.3", features = ["release_max_level_info"] }
|
||||
lru-cache = "0.1"
|
||||
memmap = "0.3"
|
||||
mime = "0.2"
|
||||
openssl = "0.8"
|
||||
regex = "0.1"
|
||||
rusqlite = "0.7"
|
||||
rustc-serialize = "0.3"
|
||||
serde = "0.8"
|
||||
serde_json = "0.8"
|
||||
serde_derive = "0.8"
|
||||
slog = "1.2"
|
||||
slog-envlogger = "0.5"
|
||||
slog-stdlog = "1.1"
|
||||
slog-term = "1.3"
|
||||
smallvec = "0.2"
|
||||
time = "0.1"
|
||||
url = "1.2"
|
||||
uuid = { version = "0.3", features = ["serde", "v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempdir = "0.3"
|
||||
|
||||
[dependencies.ffmpeg]
|
||||
version = "0.2.0-alpha.1"
|
||||
default-features = false
|
||||
features = ["codec", "format"]
|
||||
|
||||
[dependencies.ffmpeg-sys]
|
||||
version = "2.8"
|
||||
default-features = false
|
||||
features = ["avcodec"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
debug = true
|
||||
|
||||
[profile.bench]
|
||||
lto = true
|
||||
debug = true
|
117
README.md
117
README.md
@ -60,59 +60,51 @@ edge version from the command line via git:
|
||||
# Building from source
|
||||
|
||||
There are no binary packages of Moonfire NVR available yet, so it must be built
|
||||
from source. It requires several packages to build:
|
||||
from source.
|
||||
|
||||
* [CMake](https://cmake.org/) version 3.1.0 or higher.
|
||||
* a C++11 compiler, such as [gcc](https://gcc.gnu.org/) 4.7 or higher.
|
||||
* [ffmpeg](http://ffmpeg.org/), including `libavutil`,
|
||||
The `rust` branch contains a rewrite into the [Rust Programming
|
||||
Language](https://www.rust-lang.org/en-US/). In the long term, I expect this
|
||||
will result in a more secure, full-featured, easy-to-install software. In the
|
||||
short term, there will be growing pains. Rust is a new programming language.
|
||||
Moonfire NVR's primary author is new to Rust. And Moonfire NVR is a young
|
||||
project.
|
||||
|
||||
You will need the following C libraries installed:
|
||||
|
||||
* [ffmpeg](http://ffmpeg.org/) version 2.x, including `libavutil`,
|
||||
`libavcodec` (to inspect H.264 frames), and `libavformat` (to connect to RTSP
|
||||
servers and write `.mp4` files). Note ffmpeg versions older than 55.1.101,
|
||||
along with all versions of the competing project [libav](http://libav.org),
|
||||
does not support socket timeouts for RTSP. For reliable reconnections on
|
||||
error, it's strongly recommended to use ffmpeg >= 55.1.101.
|
||||
* [libevent](http://libevent.org/) 2.1, for the built-in HTTP server.
|
||||
(This might be replaced with the more full-featured
|
||||
[nghttp2](https://github.com/tatsuhiro-t/nghttp2) in the future.)
|
||||
Unfortunately, the libevent 2.0 bundled with current Debian releases is
|
||||
unsuitable.
|
||||
* [gflags](http://gflags.github.io/gflags/), for command line flag parsing.
|
||||
* [glog](https://github.com/google/glog), for debug logging.
|
||||
* [gperftools](https://github.com/gperftools/gperftools), for debugging.
|
||||
* [googletest](https://github.com/google/googletest), for automated testing.
|
||||
This will be automatically downloaded during the build process, so it's
|
||||
not necessary to install it beforehand.
|
||||
* [re2](https://github.com/google/re2), for parsing with regular expressions.
|
||||
* libuuid from [util-linux](https://en.wikipedia.org/wiki/Util-linux).
|
||||
servers and write `.mp4` files).
|
||||
|
||||
Note ffmpeg 3.x isn't supported yet by the Rust `ffmpeg` crate; see
|
||||
[rust-ffmpeg/issues/64](https://github.com/meh/rust-ffmpeg/issues/64).
|
||||
|
||||
Additionally, ffmpeg library versions older than 55.1.101, along with
|
||||
55.1.101, along with all versions of the competing project
|
||||
[libav](http://libav.org), don't not support socket timeouts for RTSP. For
|
||||
reliable reconnections on error, it's strongly recommended to use ffmpeg
|
||||
library versions >= 55.1.101.
|
||||
|
||||
* [SQLite3](https://www.sqlite.org/).
|
||||
|
||||
On Ubuntu 15.10 or Raspbian Jessie, the following command will install most
|
||||
pre-requisites (see also the `Build-Depends` field in `debian/control`):
|
||||
On Ubuntu 16.04.1 LTS or Raspbian Jessie, the following command will install
|
||||
all non-Rust dependencies:
|
||||
|
||||
$ sudo apt-get install \
|
||||
build-essential \
|
||||
cmake \
|
||||
libavcodec-dev \
|
||||
libavformat-dev \
|
||||
libavutil-dev \
|
||||
libgflags-dev \
|
||||
libgoogle-glog-dev \
|
||||
libgoogle-perftools-dev \
|
||||
libjsoncpp-dev \
|
||||
libre2-dev \
|
||||
sqlite3 \
|
||||
libsqlite3-dev \
|
||||
pkgconf \
|
||||
uuid-runtime \
|
||||
uuid-dev
|
||||
|
||||
libevent 2.1 will have to be installed from source. In the future, this
|
||||
dependency may be replaced or support may be added for automatically building
|
||||
libevent in-tree to avoid the inconvenience.
|
||||
uuid-runtime
|
||||
|
||||
uuid-runtime is only necessary if you wish to use the uuid command to generate
|
||||
uuids for your cameras (see below). If you obtain them elsewhere, you can skip this
|
||||
package.
|
||||
|
||||
Next, you need a nightly version of Rust and Cargo. The easiest way to install
|
||||
them is by following the instructions at [rustup.rs](https://www.rustup.rs/).
|
||||
|
||||
You can continue to follow the build/install instructions below for a manual
|
||||
build and install, or alternatively you can run the prep script called `prep.sh`.
|
||||
|
||||
@ -121,11 +113,7 @@ build and install, or alternatively you can run the prep script called `prep.sh`
|
||||
|
||||
The script will take the following command line options, should you need them:
|
||||
|
||||
* `-E`: Forcibly purge all existing libevent packages. You would only do this
|
||||
if there is some apparent conflict (see remarks about building libevent
|
||||
from source).
|
||||
* `-f`: Force a build even if the binary appears to be installed. This can be useful
|
||||
on repeat builds.
|
||||
* `-D`: Skip database initialization.
|
||||
* `-S`: Skip updating and installing dependencies through apt-get. This too can be
|
||||
useful on repeated builds.
|
||||
|
||||
@ -148,17 +136,9 @@ For instructions, you can skip to "[Camera configuration and hard disk mounting]
|
||||
|
||||
Once prerequisites are installed, Moonfire NVR can be built as follows:
|
||||
|
||||
$ mkdir release
|
||||
$ cd release
|
||||
$ cmake -DCMAKE_BUILD_TYPE=Release ..
|
||||
$ make
|
||||
$ sudo make install
|
||||
|
||||
Alternatively, if you do have a sufficiently new apt-installed libevent
|
||||
installed, you may be able to prepare a `.deb` package:
|
||||
|
||||
$ sudo apt-get install devscripts dh-systemd
|
||||
$ debuild -us -uc
|
||||
$ RUST_TEST_THREADS=1 cargo test
|
||||
$ cargo build --release
|
||||
$ sudo install -m 755 target/release/moonfire-nvr /usr/local/bin
|
||||
|
||||
# Further configuration
|
||||
|
||||
@ -279,9 +259,10 @@ been done for you. If not, Create
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/moonfire-nvr \
|
||||
--sample_file_dir=/var/lib/moonfire-nvr/sample \
|
||||
--db_dir=/var/lib/moonfire-nvr/db \
|
||||
--http_port=8080
|
||||
--sample-file-dir=/var/lib/moonfire-nvr/sample \
|
||||
--db-dir=/var/lib/moonfire-nvr/db \
|
||||
--http-addr=0.0.0.0:8080
|
||||
Environment=RUST_LOG=info
|
||||
Type=simple
|
||||
User=moonfire-nvr
|
||||
Nice=-20
|
||||
@ -310,10 +291,16 @@ and `systemctl` may be of particular interest.
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
While Moonfire NVR is running, logs will be written to `/tmp/moonfire-nvr.INFO`.
|
||||
Also available will be `/tmp/moonfire-nvr.WARNING` and `/tmp/moonfire-nvr.ERROR`.
|
||||
These latter to contain only warning or more serious messages, respectively only
|
||||
error messages.
|
||||
While Moonfire NVR is running, logs will be written to stdout. The `RUST_LOG`
|
||||
environmental variable controls the log level; `RUST_LOG=info` is recommended.
|
||||
If running through systemd, try `sudo journalctl --unit moonfire-nvr` to view
|
||||
the logs.
|
||||
|
||||
If Moonfire NVR crashes with a `SIGSEGV`, the problem is likely an
|
||||
incompatible version of the C `ffmpeg` libraries; use the latest 2.x release
|
||||
instead. This is one of the Rust growing pains mentioned above. While most
|
||||
code written in Rust is "safe", the foreign function interface is not only
|
||||
unsafe but currently error-prone.
|
||||
|
||||
# <a name="help"></a> Getting help and getting involved
|
||||
|
||||
@ -322,17 +309,11 @@ Please email the
|
||||
mailing list with questions, bug reports, feature requests, or just to say
|
||||
you love/hate the software and why.
|
||||
|
||||
I'd welcome help with testing, development (in C++, JavaScript, and HTML), user
|
||||
interface/graphic design, and documentation. Please email the mailing list
|
||||
if interested. Patches are welcome, but I encourage you to discuss large
|
||||
I'd welcome help with testing, development (in Rust, JavaScript, and HTML),
|
||||
user interface/graphic design, and documentation. Please email the mailing
|
||||
list if interested. Patches are welcome, but I encourage you to discuss large
|
||||
changes on the mailing list first to save effort.
|
||||
|
||||
C++ code should be written using C++11 features, should follow the [Google C++
|
||||
style guide](https://google.github.io/styleguide/cppguide.html) for
|
||||
consistency, and should be automatically tested where practical. But don't
|
||||
worry about this too much; I'm much happier to work with you to refine a rough
|
||||
draft patch than never see your contribution at all!
|
||||
|
||||
# License
|
||||
|
||||
This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
|
5
debian/changelog
vendored
5
debian/changelog
vendored
@ -1,5 +0,0 @@
|
||||
moonfire-nvr (0.1.0) UNRELEASED; urgency=medium
|
||||
|
||||
* Initial release.
|
||||
|
||||
-- Scott Lamb <slamb@slamb.org> Fri, 1 Jan 2016 21:00:00 -0800
|
1
debian/compat
vendored
1
debian/compat
vendored
@ -1 +0,0 @@
|
||||
9
|
22
debian/control
vendored
22
debian/control
vendored
@ -1,22 +0,0 @@
|
||||
Source: moonfire-nvr
|
||||
Maintainer: Scott Lamb <slamb@slamb.org>
|
||||
Section: video
|
||||
Priority: optional
|
||||
Standards-Version: 3.9.6.1
|
||||
Build-Depends: cmake,
|
||||
libavcodec-dev,
|
||||
libavformat-dev,
|
||||
libavutil-dev,
|
||||
libgflags-dev,
|
||||
libgoogle-glog-dev,
|
||||
libgoogle-perftools-dev,
|
||||
libjsoncpp-dev,
|
||||
libre2-dev,
|
||||
libsqlite3-dev,
|
||||
pkgconf,
|
||||
uuid-dev
|
||||
Package: moonfire-nvr
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}, adduser, sqlite3, uuid-runtime
|
||||
Description: security camera network video recorder
|
||||
moonfire-nvr records video files from IP security cameras.
|
43
debian/copyright
vendored
43
debian/copyright
vendored
@ -1,43 +0,0 @@
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: Moonfire NVR
|
||||
Upstream-Contact: Scott Lamb <slamb@slamb.org>
|
||||
Source: http://github.com/scottlamb/moonfire-nvr
|
||||
|
||||
Files: *
|
||||
Copyright: 2016 Scott Lamb
|
||||
License: GPL-3+ with OpenSSL exception
|
||||
|
||||
License: GPL-3+ with OpenSSL exception
|
||||
This program is free software: you can redistribute it
|
||||
and/or modify it under the terms of the GNU General Public
|
||||
License as published by the Free Software Foundation,
|
||||
either version 3 of the License, or (at your option) any
|
||||
later version.
|
||||
.
|
||||
In addition, as a special exception, the copyright holders give
|
||||
permission to link the code of portions of this program with the
|
||||
OpenSSL library under certain conditions as described in each
|
||||
individual source file, and distribute linked combinations including
|
||||
the two.
|
||||
.
|
||||
You must obey the GNU General Public License in all respects for all
|
||||
of the code used other than OpenSSL. If you modify file(s) with this
|
||||
exception, you may extend this exception to your version of the
|
||||
file(s), but you are not obligated to do so. If you do not wish to do
|
||||
so, delete this exception statement from your version. If you delete
|
||||
this exception statement from all source files in the program, then
|
||||
also delete it here.
|
||||
.
|
||||
This program is distributed in the hope that it will be
|
||||
useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
PURPOSE. See the GNU General Public License for more
|
||||
details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public
|
||||
License along with this program. If not, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
.
|
||||
On Debian systems, the full text of the GNU General Public
|
||||
License version 3 can be found in the file
|
||||
`/usr/share/common-licenses/GPL-3'.
|
8
debian/moonfire-nvr.postinst
vendored
8
debian/moonfire-nvr.postinst
vendored
@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
adduser --system moonfire-nvr
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
16
debian/moonfire-nvr.service
vendored
16
debian/moonfire-nvr.service
vendored
@ -1,16 +0,0 @@
|
||||
[Unit]
|
||||
Description=Moonfire NVR
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/moonfire-nvr
|
||||
Type=simple
|
||||
User=moonfire-nvr
|
||||
Nice=-20
|
||||
Restart=on-abnormal
|
||||
CPUAccounting=true
|
||||
MemoryAccounting=true
|
||||
BlockIOAccounting=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
3
debian/rules
vendored
3
debian/rules
vendored
@ -1,3 +0,0 @@
|
||||
#!/usr/bin/make -f
|
||||
%:
|
||||
dh $@ --with=systemd
|
1
debian/source/format
vendored
1
debian/source/format
vendored
@ -1 +0,0 @@
|
||||
3.0 (native)
|
48
prep.sh
48
prep.sh
@ -34,7 +34,7 @@
|
||||
# Script to prepare for moonfire-nvr operations
|
||||
#
|
||||
# Command line options:
|
||||
# -f: Force clean build, even if binary already installed
|
||||
# -D: Skip database initialization
|
||||
# -S: Skip apt-get update and install
|
||||
#
|
||||
|
||||
@ -112,14 +112,10 @@ SERVICE_BIN="${SERVICE_BIN:-/usr/local/bin/moonfire-nvr}"
|
||||
|
||||
# Process command line options
|
||||
#
|
||||
while getopts ":DEfS" opt; do
|
||||
while getopts ":DS" opt; do
|
||||
case $opt in
|
||||
D) SKIP_DB=1
|
||||
;;
|
||||
E) PURGE_LIBEVENT=1
|
||||
;;
|
||||
f) FORCE_BUILD=1
|
||||
;;
|
||||
S) SKIP_APT=1
|
||||
;;
|
||||
:)
|
||||
@ -133,45 +129,34 @@ while getopts ":DEfS" opt; do
|
||||
esac
|
||||
done
|
||||
|
||||
# Setup all packages we need
|
||||
# Setup all apt packages we need
|
||||
#
|
||||
echo 'Preparing and downloading packages we need...'; echo
|
||||
if [ "${SKIP_APT:-0}" != 1 ]; then
|
||||
sudo apt-get update
|
||||
[ "${PURGE_LIBEVENT:-0}" == 1 ] && sudo apt-get --purge remove libevent-*
|
||||
sudo apt-get install \
|
||||
build-essential \
|
||||
cmake \
|
||||
libavcodec-dev \
|
||||
libavformat-dev \
|
||||
libavutil-dev \
|
||||
libgflags-dev \
|
||||
libgoogle-glog-dev \
|
||||
libgoogle-perftools-dev \
|
||||
libjsoncpp-dev \
|
||||
libre2-dev \
|
||||
libssl-dev \
|
||||
sqlite3 \
|
||||
libsqlite3-dev \
|
||||
pkgconf \
|
||||
uuid-runtime \
|
||||
uuid-dev
|
||||
uuid-runtime
|
||||
fi
|
||||
|
||||
# Check if binary is installed. Setup for build if it is not
|
||||
#
|
||||
if [ ! -x "${SERVICE_BIN}" ]; then
|
||||
echo "Binary not installed, building..."; echo
|
||||
FORCE_BUILD=1
|
||||
fi
|
||||
|
||||
# Build if requested
|
||||
#
|
||||
if [ "${FORCE_BUILD:-0}" -eq 1 ]; then
|
||||
# Remove previous build, if any
|
||||
[ -d release ] && rm -fr release 2>/dev/null
|
||||
mkdir release; cd release
|
||||
cmake -DCMAKE_BUILD_TYPE=Release .. && make && sudo make install
|
||||
if ! cargo --version; then
|
||||
echo "cargo not installed/working."
|
||||
echo "Install a nightly Rust (see http://rustup.us) first."
|
||||
echo
|
||||
exit 1
|
||||
fi
|
||||
RUST_TEST_THREADS=1 cargo test
|
||||
cargo build --release
|
||||
sudo install -m 755 target/release/moonfire-nvr ${SERVICE_BIN}
|
||||
if [ -x "${SERVICE_BIN}" ]; then
|
||||
echo "Binary installed..."; echo
|
||||
else
|
||||
@ -233,9 +218,10 @@ After=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=${SERVICE_BIN} \\
|
||||
--sample_file_dir=${SAMPLES_PATH} \\
|
||||
--db_dir=${DB_DIR} \\
|
||||
--http_port=${NVR_PORT}
|
||||
--sample-file-dir=${SAMPLES_PATH} \\
|
||||
--db-dir=${DB_DIR} \\
|
||||
--http-addr=0.0.0.0:${NVR_PORT}
|
||||
Environment=RUST_LOG=info
|
||||
Type=simple
|
||||
User=${NVR_USER}
|
||||
Nice=-20
|
||||
|
@ -1,101 +0,0 @@
|
||||
# Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the
|
||||
# OpenSSL library under certain conditions as described in each
|
||||
# individual source file, and distribute linked combinations including
|
||||
# the two.
|
||||
#
|
||||
# You must obey the GNU General Public License in all respects for all
|
||||
# of the code used other than OpenSSL. If you modify file(s) with this
|
||||
# exception, you may extend this exception to your version of the
|
||||
# file(s), but you are not obligated to do so. If you do not wish to do
|
||||
# so, delete this exception statement from your version. If you delete
|
||||
# this exception statement from all source files in the program, then
|
||||
# also delete it here.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
include_directories(${CMAKE_CURRENT_BINARY_DIR})
|
||||
include_directories(${JSONCPP_INCLUDE_DIRS})
|
||||
|
||||
set(MOONFIRE_DEPS
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
${FFMPEG_LIBRARIES}
|
||||
${GFLAGS_LIBRARIES}
|
||||
${GLOG_LIBRARIES}
|
||||
${JSONCPP_LIBRARIES}
|
||||
${LIBEVENT_LIBRARIES}
|
||||
${OPENSSL_LIBRARIES}
|
||||
${PROFILER_LIBRARIES}
|
||||
${RE2_LIBRARIES}
|
||||
${SQLITE_LIBRARIES}
|
||||
${UUID_LIBRARIES})
|
||||
|
||||
set(MOONFIRE_NVR_SRCS
|
||||
coding.cc
|
||||
crypto.cc
|
||||
ffmpeg.cc
|
||||
filesystem.cc
|
||||
h264.cc
|
||||
http.cc
|
||||
moonfire-db.cc
|
||||
moonfire-nvr.cc
|
||||
mp4.cc
|
||||
profiler.cc
|
||||
recording.cc
|
||||
sqlite.cc
|
||||
string.cc
|
||||
time.cc
|
||||
uuid.cc
|
||||
web.cc)
|
||||
|
||||
link_directories(${LIBEVENT_LIBRARY_DIRS})
|
||||
|
||||
add_library(moonfire-nvr-lib ${MOONFIRE_NVR_SRCS} ${PROTO_SRCS} ${PROTO_HDRS})
|
||||
target_link_libraries(moonfire-nvr-lib ${MOONFIRE_DEPS})
|
||||
|
||||
add_executable(moonfire-nvr moonfire-nvr-main.cc)
|
||||
target_link_libraries(moonfire-nvr moonfire-nvr-lib)
|
||||
install_programs(/bin FILES moonfire-nvr)
|
||||
|
||||
# Tests.
|
||||
include_directories(${GTest_INCLUDE_DIR})
|
||||
include_directories(${GMock_INCLUDE_DIR})
|
||||
include_directories(${GBenchmark_INCLUDE_DIR})
|
||||
|
||||
set(MOONFIRE_NVR_TESTS
|
||||
coding
|
||||
crypto
|
||||
h264
|
||||
http
|
||||
moonfire-db
|
||||
moonfire-nvr
|
||||
mp4
|
||||
recording
|
||||
sqlite
|
||||
string)
|
||||
|
||||
foreach(test ${MOONFIRE_NVR_TESTS})
|
||||
add_executable(${test}-test ${test}-test.cc testutil.cc)
|
||||
target_link_libraries(${test}-test GTest GMock moonfire-nvr-lib)
|
||||
add_test(NAME ${test}-test
|
||||
COMMAND ${test}-test
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
endforeach(test)
|
||||
|
||||
foreach(bench recording)
|
||||
add_executable(${bench}-bench ${bench}-bench.cc testutil.cc)
|
||||
target_link_libraries(${bench}-bench GTest GMock GBenchmark moonfire-nvr-lib)
|
||||
endforeach(bench)
|
@ -1,141 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// coding-test.cc: tests of the coding.h interface.
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
#include <glog/logging.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "coding.h"
|
||||
|
||||
DECLARE_bool(alsologtostderr);
|
||||
|
||||
namespace moonfire_nvr {
|
||||
namespace {
|
||||
|
||||
TEST(VarintTest, Simple) {
|
||||
// Encode.
|
||||
std::string foo;
|
||||
AppendVar32(UINT32_C(1), &foo);
|
||||
EXPECT_EQ("\x01", foo);
|
||||
AppendVar32(UINT32_C(300), &foo);
|
||||
EXPECT_EQ("\x01\xac\x02", foo);
|
||||
|
||||
// Decode.
|
||||
re2::StringPiece p(foo);
|
||||
uint32_t out;
|
||||
std::string error_message;
|
||||
EXPECT_TRUE(DecodeVar32(&p, &out, &error_message));
|
||||
EXPECT_EQ(UINT32_C(1), out);
|
||||
EXPECT_TRUE(DecodeVar32(&p, &out, &error_message));
|
||||
EXPECT_EQ(UINT32_C(300), out);
|
||||
EXPECT_EQ(0, p.size());
|
||||
}
|
||||
|
||||
TEST(VarintTest, AllDecodeSizes) {
|
||||
std::string error_message;
|
||||
const uint32_t kToDecode[]{
|
||||
1,
|
||||
1 | (2 << 7),
|
||||
1 | (2 << 7) | (3 << 14),
|
||||
1 | (2 << 7) | (3 << 14) | (4 << 21),
|
||||
1 | (2 << 7) | (3 << 14) | (4 << 21) | (5 << 28),
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(kToDecode) / sizeof(kToDecode[0]); ++i) {
|
||||
auto in = kToDecode[i];
|
||||
std::string foo;
|
||||
AppendVar32(in, &foo);
|
||||
ASSERT_EQ(i + 1, foo.size());
|
||||
re2::StringPiece p(foo);
|
||||
uint32_t out;
|
||||
|
||||
// Slow path: last bytes of the buffer.
|
||||
DecodeVar32(&p, &out, &error_message);
|
||||
EXPECT_EQ(in, out) << "i: " << i;
|
||||
EXPECT_EQ(0, p.size()) << "i: " << i;
|
||||
|
||||
// Fast path: plenty of bytes in the buffer.
|
||||
foo.append(4, 0);
|
||||
p = foo;
|
||||
DecodeVar32(&p, &out, &error_message);
|
||||
EXPECT_EQ(in, out);
|
||||
EXPECT_EQ(4, p.size());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(VarintTest, DecodeErrors) {
|
||||
re2::StringPiece empty;
|
||||
uint32_t out;
|
||||
std::string error_message;
|
||||
|
||||
for (auto input :
|
||||
{re2::StringPiece("", 0), re2::StringPiece("\x80", 1),
|
||||
re2::StringPiece("\x80\x80", 2), re2::StringPiece("\x80\x80\x80", 3),
|
||||
re2::StringPiece("\x80\x80\x80\x80", 4)}) {
|
||||
EXPECT_FALSE(DecodeVar32(&input, &out, &error_message)) << "input: "
|
||||
<< input;
|
||||
EXPECT_EQ("buffer underrun", error_message);
|
||||
}
|
||||
|
||||
re2::StringPiece too_big("\x80\x80\x80\x80\x10", 5);
|
||||
EXPECT_FALSE(DecodeVar32(&too_big, &out, &error_message));
|
||||
EXPECT_EQ("integer overflow", error_message);
|
||||
}
|
||||
|
||||
TEST(ZigzagTest, Encode) {
|
||||
EXPECT_EQ(UINT32_C(0), Zigzag32(INT32_C(0)));
|
||||
EXPECT_EQ(UINT32_C(1), Zigzag32(INT32_C(-1)));
|
||||
EXPECT_EQ(UINT32_C(2), Zigzag32(INT32_C(1)));
|
||||
EXPECT_EQ(UINT32_C(3), Zigzag32(INT32_C(-2)));
|
||||
EXPECT_EQ(UINT32_C(4294967294), Zigzag32(INT32_C(2147483647)));
|
||||
EXPECT_EQ(UINT32_C(4294967295), Zigzag32(INT32_C(-2147483648)));
|
||||
}
|
||||
|
||||
TEST(ZigzagTest, Decode) {
|
||||
EXPECT_EQ(INT32_C(0), Unzigzag32(UINT32_C(0)));
|
||||
EXPECT_EQ(INT32_C(-1), Unzigzag32(UINT32_C(1)));
|
||||
EXPECT_EQ(INT32_C(1), Unzigzag32(UINT32_C(2)));
|
||||
EXPECT_EQ(INT32_C(-2), Unzigzag32(UINT32_C(3)));
|
||||
EXPECT_EQ(INT32_C(2147483647), Unzigzag32(UINT32_C(4294967294)));
|
||||
EXPECT_EQ(INT32_C(-2147483648), Unzigzag32(UINT32_C(4294967295)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
FLAGS_alsologtostderr = true;
|
||||
google::ParseCommandLineFlags(&argc, &argv, true);
|
||||
testing::InitGoogleTest(&argc, argv);
|
||||
google::InitGoogleLogging(argv[0]);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
109
src/coding.cc
109
src/coding.cc
@ -1,109 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// coding.cc: see coding.h.
|
||||
|
||||
#include "coding.h"
|
||||
#include "common.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
namespace internal {
|
||||
|
||||
void AppendVar32Slow(uint32_t in, std::string *out) {
|
||||
while (true) {
|
||||
uint8_t next_byte = in & 0x7F;
|
||||
in >>= 7;
|
||||
if (in == 0) {
|
||||
out->push_back(next_byte);
|
||||
return;
|
||||
}
|
||||
out->push_back(next_byte | 0x80);
|
||||
}
|
||||
}
|
||||
|
||||
bool DecodeVar32Slow(re2::StringPiece *in, uint32_t *out_p,
|
||||
std::string *error_message) {
|
||||
// The fast path is inlined; this function is called only when
|
||||
// byte 0 is present and >= 0x80.
|
||||
size_t left = in->size() - 1;
|
||||
auto p = reinterpret_cast<uint8_t const *>(in->data());
|
||||
uint32_t v = uint32_t(p[0] & 0x7f);
|
||||
size_t size = 1;
|
||||
|
||||
// Aid branch prediction in two ways:
|
||||
// * have a faster path which doesn't check for buffer underrun on every
|
||||
// byte if there's plenty of bytes left or the last byte is not continued.
|
||||
// * fully unroll the loop
|
||||
if (left >= 4 || (p[left] & 0x80) == 0) {
|
||||
v |= uint32_t(p[size] & 0x7f) << 7;
|
||||
if (p[size++] & 0x80) {
|
||||
v |= uint32_t(p[size] & 0x7f) << 14;
|
||||
if (p[size++] & 0x80) {
|
||||
v |= uint32_t(p[size] & 0x7f) << 21;
|
||||
if (p[size++] & 0x80) {
|
||||
if (UNLIKELY(p[size] & 0xf0)) {
|
||||
*error_message = "integer overflow";
|
||||
return false;
|
||||
}
|
||||
v |= uint32_t(p[size++] & 0x7f) << 28;
|
||||
}
|
||||
}
|
||||
}
|
||||
*out_p = v;
|
||||
in->remove_prefix(size);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Slowest path.
|
||||
if (LIKELY(left)) {
|
||||
v |= uint32_t(p[size] & 0x7f) << 7;
|
||||
if (p[size++] & 0x80 && --left > 0) {
|
||||
v |= uint32_t(p[size] & 0x7f) << 14;
|
||||
if (p[size++] & 0x80 && --left > 0) {
|
||||
v |= uint32_t(p[size] & 0x7f) << 21;
|
||||
if (p[size++] & 0x80) {
|
||||
--left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (UNLIKELY(left == 0 && p[size - 1] & 0x80)) {
|
||||
*error_message = "buffer underrun";
|
||||
return false;
|
||||
}
|
||||
*out_p = v;
|
||||
in->remove_prefix(size);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
} // namespace moonfire_nvr
|
169
src/coding.h
169
src/coding.h
@ -1,169 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// coding.h: Binary encoding/decoding.
|
||||
|
||||
#ifndef MOONFIRE_NVR_CODING_H
|
||||
#define MOONFIRE_NVR_CODING_H
|
||||
|
||||
#include <endian.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <re2/stringpiece.h>
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
namespace internal {
|
||||
|
||||
void AppendVar32Slow(uint32_t in, std::string *out);
|
||||
bool DecodeVar32Slow(re2::StringPiece *in, uint32_t *out,
|
||||
std::string *error_message);
|
||||
|
||||
} // namespace internal
|
||||
|
||||
// Endianness conversion.
|
||||
|
||||
#if __BYTE_ORDER == __LITTLE_ENDIAN
|
||||
// XXX: __builtin_bswap64 doesn't compile on gcc 5.2.1 with an error about a
|
||||
// narrowing conversion?!? Doing this by hand...
|
||||
constexpr uint64_t ToNetworkU64(uint64_t in) {
|
||||
return ((in & UINT64_C(0xFF00000000000000)) >> 56) |
|
||||
((in & UINT64_C(0x00FF000000000000)) >> 40) |
|
||||
((in & UINT64_C(0x0000FF0000000000)) >> 24) |
|
||||
((in & UINT64_C(0x000000FF00000000)) >> 8) |
|
||||
((in & UINT64_C(0x00000000FF000000)) << 8) |
|
||||
((in & UINT64_C(0x0000000000FF0000)) << 24) |
|
||||
((in & UINT64_C(0x000000000000FF00)) << 40) |
|
||||
((in & UINT64_C(0x00000000000000FF)) << 56);
|
||||
}
|
||||
constexpr int64_t ToNetwork64(int64_t in) {
|
||||
return static_cast<int64_t>(ToNetworkU64(static_cast<uint64_t>(in)));
|
||||
}
|
||||
constexpr uint32_t ToNetworkU32(uint32_t in) {
|
||||
return ((in & UINT32_C(0xFF000000)) >> 24) |
|
||||
((in & UINT32_C(0x00FF0000)) >> 8) |
|
||||
((in & UINT32_C(0x0000FF00)) << 8) |
|
||||
((in & UINT32_C(0x000000FF)) << 24);
|
||||
}
|
||||
constexpr int32_t ToNetwork32(int32_t in) {
|
||||
return static_cast<int32_t>(ToNetworkU32(static_cast<uint32_t>(in)));
|
||||
}
|
||||
constexpr uint16_t ToNetworkU16(uint16_t in) {
|
||||
return ((in & UINT32_C(0xFF00)) >> 8) | ((in & UINT32_C(0x00FF)) << 8);
|
||||
}
|
||||
constexpr int16_t ToNetwork16(int16_t in) {
|
||||
return static_cast<int16_t>(ToNetworkU16(static_cast<uint16_t>(in)));
|
||||
}
|
||||
#elif __BYTE_ORDER == __BIG_ENDIAN
|
||||
constexpr uint64_t ToNetworkU64(uint64_t in) { return in; }
|
||||
constexpr int64_t ToNetwork64(int64_t in) { return in; }
|
||||
constexpr uint32_t ToNetworkU32(uint32_t in) { return in; }
|
||||
constexpr int32_t ToNetwork32(int32_t in) { return in; }
|
||||
constexpr uint16_t ToNetworkU16(uint16_t in) { return in; }
|
||||
constexpr int16_t ToNetwork16(int16_t in) { return in; }
|
||||
#else
|
||||
#error Unknown byte order.
|
||||
#endif
|
||||
|
||||
// Varint encoding, as in
|
||||
// https://developers.google.com/protocol-buffers/docs/encoding#varints
|
||||
|
||||
inline void AppendVar32(uint32_t in, std::string *out) {
|
||||
if (in < UINT32_C(1) << 7) {
|
||||
out->push_back(static_cast<char>(in));
|
||||
} else {
|
||||
internal::AppendVar32Slow(in, out);
|
||||
}
|
||||
}
|
||||
|
||||
// Decode the first varint from |in|, saving it to |out| and advancing |in|.
|
||||
// Returns error if |in| does not hold a complete varint or on integer overflow.
|
||||
inline bool DecodeVar32(re2::StringPiece *in, uint32_t *out,
|
||||
std::string *error_message) {
|
||||
if (in->size() == 0) {
|
||||
*error_message = "buffer underrun";
|
||||
return false;
|
||||
}
|
||||
auto first_byte = static_cast<uint8_t>(*in->data());
|
||||
if (first_byte < 0x80) {
|
||||
in->remove_prefix(1);
|
||||
*out = first_byte;
|
||||
return true;
|
||||
} else {
|
||||
return internal::DecodeVar32Slow(in, out, error_message);
|
||||
}
|
||||
}
|
||||
|
||||
// Zigzag encoding for signed integers, as in
|
||||
// https://developers.google.com/protocol-buffers/docs/encoding#types
|
||||
// Use the low bit to indicate signedness (1 = negative, 0 = non-negative).
|
||||
inline uint32_t Zigzag32(int32_t in) {
|
||||
return static_cast<uint32_t>(in << 1) ^ (in >> 31);
|
||||
}
|
||||
|
||||
inline int32_t Unzigzag32(uint32_t in) {
|
||||
return (in >> 1) ^ -static_cast<int32_t>(in & 1);
|
||||
}
|
||||
|
||||
inline void AppendU16(uint16_t in, std::string *out) {
|
||||
uint16_t net = ToNetworkU16(in);
|
||||
out->append(reinterpret_cast<const char *>(&net), sizeof(uint16_t));
|
||||
}
|
||||
|
||||
inline void Append16(int16_t in, std::string *out) {
|
||||
int16_t net = ToNetwork16(in);
|
||||
out->append(reinterpret_cast<const char *>(&net), sizeof(int16_t));
|
||||
}
|
||||
|
||||
inline void AppendU32(uint32_t in, std::string *out) {
|
||||
uint32_t net = ToNetworkU32(in);
|
||||
out->append(reinterpret_cast<const char *>(&net), sizeof(uint32_t));
|
||||
}
|
||||
|
||||
inline void Append32(int32_t in, std::string *out) {
|
||||
int32_t net = ToNetwork32(in);
|
||||
out->append(reinterpret_cast<const char *>(&net), sizeof(int32_t));
|
||||
}
|
||||
|
||||
inline void AppendU64(uint64_t in, std::string *out) {
|
||||
uint64_t net = ToNetworkU64(in);
|
||||
out->append(reinterpret_cast<const char *>(&net), sizeof(uint64_t));
|
||||
}
|
||||
|
||||
inline void Append64(int64_t in, std::string *out) {
|
||||
int64_t net = ToNetwork64(in);
|
||||
out->append(reinterpret_cast<const char *>(&net), sizeof(int64_t));
|
||||
}
|
||||
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
#endif // MOONFIRE_NVR_CODING_H
|
@ -1,67 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// crypto-test.cc: tests of the crypto.h interface.
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
#include <glog/logging.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "crypto.h"
|
||||
#include "string.h"
|
||||
|
||||
DECLARE_bool(alsologtostderr);
|
||||
|
||||
namespace moonfire_nvr {
|
||||
namespace {
|
||||
|
||||
TEST(DigestTest, Sha1) {
|
||||
auto sha1 = Digest::SHA1();
|
||||
EXPECT_EQ("da39a3ee5e6b4b0d3255bfef95601890afd80709",
|
||||
ToHex(sha1->Finalize()));
|
||||
|
||||
sha1 = Digest::SHA1();
|
||||
sha1->Update("hello");
|
||||
sha1->Update(" world");
|
||||
EXPECT_EQ("2aae6c35c94fcfb415dbe95f408b9ce91ee846ed",
|
||||
ToHex(sha1->Finalize()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
FLAGS_alsologtostderr = true;
|
||||
google::ParseCommandLineFlags(&argc, &argv, true);
|
||||
testing::InitGoogleTest(&argc, argv);
|
||||
google::InitGoogleLogging(argv[0]);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// crypto.cc: see crypto.h.
|
||||
|
||||
#include "crypto.h"
|
||||
|
||||
#include <glog/logging.h>
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
std::unique_ptr<Digest> Digest::SHA1() {
|
||||
std::unique_ptr<Digest> d(new Digest);
|
||||
CHECK_EQ(1, EVP_DigestInit_ex(d->ctx_, EVP_sha1(), nullptr));
|
||||
return d;
|
||||
}
|
||||
|
||||
Digest::Digest() { ctx_ = CHECK_NOTNULL(EVP_MD_CTX_create()); }
|
||||
|
||||
Digest::~Digest() { EVP_MD_CTX_destroy(ctx_); }
|
||||
|
||||
void Digest::Update(re2::StringPiece data) {
|
||||
CHECK_EQ(1, EVP_DigestUpdate(ctx_, data.data(), data.size()));
|
||||
}
|
||||
|
||||
std::string Digest::Finalize() {
|
||||
std::string out;
|
||||
out.resize(EVP_MD_CTX_size(ctx_));
|
||||
auto *p = reinterpret_cast<unsigned char *>(&out[0]);
|
||||
CHECK_EQ(1, EVP_DigestFinal_ex(ctx_, p, nullptr));
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace moonfire_nvr
|
63
src/crypto.h
63
src/crypto.h
@ -1,63 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// crypto.h: cryptographic functions.
|
||||
|
||||
#ifndef MOONFIRE_NVR_CRYPTO_H
|
||||
#define MOONFIRE_NVR_CRYPTO_H
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <openssl/evp.h>
|
||||
#include <re2/stringpiece.h>
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
class Digest {
|
||||
public:
|
||||
static std::unique_ptr<Digest> SHA1();
|
||||
~Digest();
|
||||
|
||||
// PRE: Finalize() has not been called.
|
||||
void Update(re2::StringPiece data);
|
||||
|
||||
// PRE: Finalize() has not been called.
|
||||
std::string Finalize();
|
||||
|
||||
private:
|
||||
Digest();
|
||||
Digest(const Digest &) = delete;
|
||||
void operator=(const Digest &) = delete;
|
||||
EVP_MD_CTX *ctx_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
#endif // MOONFIRE_NVR_CRYPTO_H
|
400
src/dir.rs
Normal file
400
src/dir.rs
Normal file
@ -0,0 +1,400 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Sample file directory management.
|
||||
//!
|
||||
//! This includes opening files for serving, rotating away old
|
||||
//! files, and syncing new files to disk.
|
||||
|
||||
use db;
|
||||
use libc;
|
||||
use recording;
|
||||
use error::Error;
|
||||
use std::ffi;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::mem;
|
||||
use std::os::unix::io::FromRawFd;
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// A sample file directory. This is currently a singleton in production. (Maybe in the future
|
||||
/// Moonfire will be extended to support multiple directories on different spindles.)
|
||||
///
|
||||
/// If the directory is used for writing, the `start_syncer` function should be called to start
|
||||
/// a background thread. This thread manages deleting files and writing new files. It synces the
|
||||
/// directory and commits these operations to the database in the correct order to maintain the
|
||||
/// invariants described in `design/schema.md`.
|
||||
pub struct SampleFileDir {
|
||||
db: Arc<db::Database>,
|
||||
|
||||
/// The open file descriptor for the directory. The worker uses it to create files and sync the
|
||||
/// directory. Other threads use it to open sample files for reading during video serving.
|
||||
fd: Fd,
|
||||
|
||||
// Lock order: don't acquire mutable.lock() while holding db.lock().
|
||||
mutable: Mutex<SharedMutableState>,
|
||||
}
|
||||
|
||||
/// A file descriptor associated with a directory (not necessarily the sample file dir).
|
||||
pub struct Fd(libc::c_int);
|
||||
|
||||
impl Drop for Fd {
|
||||
fn drop(&mut self) {
|
||||
if unsafe { libc::close(self.0) } < 0 {
|
||||
let e = io::Error::last_os_error();
|
||||
warn!("Unable to close sample file dir: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Fd {
|
||||
/// Opens the given path as a directory.
|
||||
pub fn open(path: &str) -> Result<Fd, io::Error> {
|
||||
let cstring = ffi::CString::new(path)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
|
||||
let fd = unsafe { libc::open(cstring.as_ptr(), libc::O_DIRECTORY | libc::O_RDONLY, 0) };
|
||||
if fd < 0 {
|
||||
return Err(io::Error::last_os_error().into());
|
||||
}
|
||||
Ok(Fd(fd))
|
||||
}
|
||||
|
||||
/// Locks the directory with the specified `flock` operation.
|
||||
pub fn lock(&self, operation: libc::c_int) -> Result<(), io::Error> {
|
||||
let ret = unsafe { libc::flock(self.0, operation) };
|
||||
if ret < 0 {
|
||||
return Err(io::Error::last_os_error().into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SampleFileDir {
|
||||
pub fn new(path: &str, db: Arc<db::Database>) -> Result<Arc<SampleFileDir>, Error> {
|
||||
let fd = Fd::open(path)?;
|
||||
Ok(Arc::new(SampleFileDir{
|
||||
db: db,
|
||||
fd: fd,
|
||||
mutable: Mutex::new(SharedMutableState{
|
||||
next_uuid: None,
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Opens the given sample file for reading.
|
||||
pub fn open_sample_file(&self, uuid: Uuid) -> Result<fs::File, io::Error> {
|
||||
self.open_int(uuid, libc::O_RDONLY, 0)
|
||||
}
|
||||
|
||||
/// Creates a new writer.
|
||||
/// Note this doesn't wait for previous rotation to complete; it's assumed the sample file
|
||||
/// directory has sufficient space for a couple recordings per camera in addition to the
|
||||
/// cameras' total `retain_bytes`.
|
||||
pub fn create_writer(&self, start: recording::Time, local_start: recording::Time,
|
||||
camera_id: i32, video_sample_entry_id: i32)
|
||||
-> Result<recording::Writer, Error> {
|
||||
// Grab the next uuid. Typically one is cached—a sync has usually completed since the last
|
||||
// writer was created, and syncs ensure `next_uuid` is filled while performing their
|
||||
// transaction. But if not, perform an extra database transaction to reserve a new one.
|
||||
let uuid = match self.mutable.lock().unwrap().next_uuid.take() {
|
||||
Some(u) => u,
|
||||
None => {
|
||||
info!("Committing extra transaction because there's no cached uuid");
|
||||
let mut db = self.db.lock();
|
||||
let mut tx = db.tx()?;
|
||||
let u = tx.reserve_sample_file()?;
|
||||
tx.commit()?;
|
||||
u
|
||||
},
|
||||
};
|
||||
|
||||
let f = match self.open_int(uuid, libc::O_WRONLY | libc::O_EXCL | libc::O_CREAT, 0o600) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
self.mutable.lock().unwrap().next_uuid = Some(uuid);
|
||||
return Err(e.into());
|
||||
},
|
||||
};
|
||||
recording::Writer::open(f, uuid, start, local_start, camera_id, video_sample_entry_id)
|
||||
}
|
||||
|
||||
/// Opens a sample file within this directory with the given flags and (if creating) mode.
|
||||
fn open_int(&self, uuid: Uuid, flags: libc::c_int, mode: libc::c_int)
|
||||
-> Result<fs::File, io::Error> {
|
||||
let p = SampleFileDir::get_rel_pathname(uuid);
|
||||
let fd = unsafe { libc::openat(self.fd.0, p.as_ptr(), flags, mode) };
|
||||
if fd < 0 {
|
||||
return Err(io::Error::last_os_error())
|
||||
}
|
||||
unsafe { Ok(fs::File::from_raw_fd(fd)) }
|
||||
}
|
||||
|
||||
/// Gets a pathname for a sample file suitable for passing to open or unlink.
|
||||
fn get_rel_pathname(uuid: Uuid) -> [libc::c_char; 37] {
|
||||
let mut buf = [0u8; 37];
|
||||
write!(&mut buf[..36], "{}", uuid.hyphenated()).expect("can't format uuid to pathname buf");
|
||||
|
||||
// libc::c_char seems to be i8 on some platforms (Linux/arm) and u8 on others (Linux/amd64).
|
||||
// Transmute, suppressing the warning that happens on platforms in which it's already u8.
|
||||
#[allow(useless_transmute)]
|
||||
unsafe { mem::transmute::<[u8; 37], [libc::c_char; 37]>(buf) }
|
||||
}
|
||||
|
||||
/// Unlinks the given sample file within this directory.
|
||||
fn unlink(fd: &Fd, uuid: Uuid) -> Result<(), io::Error> {
|
||||
let p = SampleFileDir::get_rel_pathname(uuid);
|
||||
let res = unsafe { libc::unlinkat(fd.0, p.as_ptr(), 0) };
|
||||
if res < 0 {
|
||||
return Err(io::Error::last_os_error())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Syncs the directory itself.
|
||||
fn sync(&self) -> Result<(), io::Error> {
|
||||
let res = unsafe { libc::fsync(self.fd.0) };
|
||||
if res < 0 {
|
||||
return Err(io::Error::last_os_error())
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// State shared between users of the `SampleFileDirectory` struct and the syncer.
|
||||
struct SharedMutableState {
|
||||
next_uuid: Option<Uuid>,
|
||||
}
|
||||
|
||||
/// A command sent to the syncer. These correspond to methods in the `SyncerChannel` struct.
|
||||
enum SyncerCommand {
|
||||
AsyncSaveWriter(db::RecordingToInsert, fs::File),
|
||||
|
||||
#[cfg(test)]
|
||||
Flush(mpsc::SyncSender<()>),
|
||||
}
|
||||
|
||||
/// A channel which can be used to send commands to the syncer.
|
||||
/// Can be cloned to allow multiple threads to send commands.
|
||||
#[derive(Clone)]
|
||||
pub struct SyncerChannel(mpsc::Sender<SyncerCommand>);
|
||||
|
||||
/// State of the worker thread.
|
||||
struct SyncerState {
|
||||
dir: Arc<SampleFileDir>,
|
||||
to_unlink: Vec<Uuid>,
|
||||
to_mark_deleted: Vec<Uuid>,
|
||||
cmds: mpsc::Receiver<SyncerCommand>,
|
||||
}
|
||||
|
||||
/// Starts a syncer for the given sample file directory.
|
||||
/// There should be only one syncer per directory, or 0 if operating in read-only mode.
|
||||
/// This function will perform the initial rotation synchronously, so that it is finished before
|
||||
/// file writing starts. Afterward the syncing happens in a background thread.
|
||||
///
|
||||
/// Returns a `SyncerChannel` which can be used to send commands (and can be cloned freely) and
|
||||
/// a `JoinHandle` for the syncer thread. At program shutdown, all `SyncerChannel` clones should be
|
||||
/// removed and then the handle joined to allow all recordings to be persisted.
|
||||
pub fn start_syncer(dir: Arc<SampleFileDir>)
|
||||
-> Result<(SyncerChannel, thread::JoinHandle<()>), Error> {
|
||||
let to_unlink = dir.db.lock().list_reserved_sample_files()?;
|
||||
let (snd, rcv) = mpsc::channel();
|
||||
let mut state = SyncerState {
|
||||
dir: dir,
|
||||
to_unlink: to_unlink,
|
||||
to_mark_deleted: Vec::new(),
|
||||
cmds: rcv,
|
||||
};
|
||||
state.initial_rotation()?;
|
||||
Ok((SyncerChannel(snd),
|
||||
thread::Builder::new().name("syncer".into()).spawn(move || state.run()).unwrap()))
|
||||
}
|
||||
|
||||
impl SyncerChannel {
|
||||
/// Asynchronously syncs the given writer, closes it, records it into the database, and
|
||||
/// starts rotation.
|
||||
pub fn async_save_writer(&self, w: recording::Writer) -> Result<(), Error> {
|
||||
let (recording, f) = w.close()?;
|
||||
self.0.send(SyncerCommand::AsyncSaveWriter(recording, f)).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// For testing: flushes the syncer, waiting for all currently-queued commands to complete.
|
||||
#[cfg(test)]
|
||||
pub fn flush(&self) {
|
||||
let (snd, rcv) = mpsc::sync_channel(0);
|
||||
self.0.send(SyncerCommand::Flush(snd)).unwrap();
|
||||
rcv.recv().unwrap_err(); // syncer should just drop the channel, closing it.
|
||||
}
|
||||
}
|
||||
|
||||
impl SyncerState {
|
||||
fn run(&mut self) {
|
||||
loop {
|
||||
match self.cmds.recv() {
|
||||
Err(_) => return, // all senders have closed the channel; shutdown
|
||||
Ok(SyncerCommand::AsyncSaveWriter(recording, f)) => self.save_writer(recording, f),
|
||||
|
||||
#[cfg(test)]
|
||||
Ok(SyncerCommand::Flush(_)) => {}, // just drop the supplied sender, closing it.
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotates files for all cameras and deletes stale reserved uuids from previous runs.
|
||||
fn initial_rotation(&mut self) -> Result<(), Error> {
|
||||
let mut to_delete = Vec::new();
|
||||
{
|
||||
let mut db = self.dir.db.lock();
|
||||
for (camera_id, camera) in db.cameras_by_id() {
|
||||
self.get_rows_to_delete(&db, *camera_id, camera, 0, &mut to_delete)?;
|
||||
}
|
||||
let mut tx = db.tx()?;
|
||||
tx.delete_recordings(&to_delete)?;
|
||||
tx.commit()?;
|
||||
}
|
||||
for row in to_delete {
|
||||
self.to_unlink.push(row.uuid);
|
||||
}
|
||||
self.try_unlink();
|
||||
if !self.to_unlink.is_empty() {
|
||||
return Err(Error::new(format!("failed to unlink {} sample files",
|
||||
self.to_unlink.len())));
|
||||
}
|
||||
self.dir.sync()?;
|
||||
{
|
||||
let mut db = self.dir.db.lock();
|
||||
let mut tx = db.tx()?;
|
||||
tx.mark_sample_files_deleted(&self.to_mark_deleted)?;
|
||||
tx.commit()?;
|
||||
}
|
||||
self.to_mark_deleted.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Saves the given writer and causes rotation to happen.
|
||||
/// Note that part of rotation is deferred for the next cycle (saved writing or program startup)
|
||||
/// so that there can be only one dir sync and database transaction per save.
|
||||
fn save_writer(&mut self, recording: db::RecordingToInsert, f: fs::File) {
|
||||
if let Err(e) = self.save_writer_helper(&recording, f) {
|
||||
error!("camera {}: will discard recording {} due to error while saving: {}",
|
||||
recording.camera_id, recording.sample_file_uuid, e);
|
||||
self.to_unlink.push(recording.sample_file_uuid);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal helper for `save_writer`. This is separated out so that the question-mark operator
|
||||
/// can be used in the many error paths.
|
||||
fn save_writer_helper(&mut self, recording: &db::RecordingToInsert, f: fs::File)
|
||||
-> Result<(), Error> {
|
||||
self.try_unlink();
|
||||
if !self.to_unlink.is_empty() {
|
||||
return Err(Error::new(format!("failed to unlink {} files.", self.to_unlink.len())));
|
||||
}
|
||||
f.sync_all()?;
|
||||
self.dir.sync()?;
|
||||
|
||||
let mut to_delete = Vec::new();
|
||||
let mut l = self.dir.mutable.lock().unwrap();
|
||||
let mut db = self.dir.db.lock();
|
||||
let mut new_next_uuid = l.next_uuid;
|
||||
{
|
||||
let camera =
|
||||
db.cameras_by_id().get(&recording.camera_id)
|
||||
.ok_or_else(|| Error::new(format!("no such camera {}", recording.camera_id)))?;
|
||||
self.get_rows_to_delete(&db, recording.camera_id, camera,
|
||||
recording.sample_file_bytes as i64, &mut to_delete)?;
|
||||
}
|
||||
let mut tx = db.tx()?;
|
||||
tx.mark_sample_files_deleted(&self.to_mark_deleted)?;
|
||||
tx.delete_recordings(&to_delete)?;
|
||||
if new_next_uuid.is_none() {
|
||||
new_next_uuid = Some(tx.reserve_sample_file()?);
|
||||
}
|
||||
tx.insert_recording(recording)?;
|
||||
tx.commit()?;
|
||||
l.next_uuid = new_next_uuid;
|
||||
|
||||
self.to_mark_deleted.clear();
|
||||
self.to_unlink.extend(to_delete.iter().map(|row| row.uuid));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets rows to delete to bring a camera's disk usage within bounds.
|
||||
fn get_rows_to_delete(&self, db: &MutexGuard<db::LockedDatabase>, camera_id: i32,
|
||||
camera: &db::Camera, extra_bytes_needed: i64,
|
||||
to_delete: &mut Vec<db::ListOldestSampleFilesRow>) -> Result<(), Error> {
|
||||
let bytes_needed = camera.sample_file_bytes + extra_bytes_needed - camera.retain_bytes;
|
||||
let mut bytes_to_delete = 0;
|
||||
if bytes_needed <= 0 {
|
||||
debug!("{}: have remaining quota of {}", camera.short_name, -bytes_needed);
|
||||
return Ok(());
|
||||
}
|
||||
let mut n = 0;
|
||||
db.list_oldest_sample_files(camera_id, |row| {
|
||||
bytes_to_delete += row.sample_file_bytes as i64;
|
||||
to_delete.push(row);
|
||||
n += 1;
|
||||
bytes_needed > bytes_to_delete // continue as long as more deletions are needed.
|
||||
})?;
|
||||
if bytes_needed > bytes_to_delete {
|
||||
return Err(Error::new(format!("{}: couldn't find enough files to delete: {} left.",
|
||||
camera.short_name, bytes_needed)));
|
||||
}
|
||||
info!("{}: deleting {} bytes in {} recordings ({} bytes needed)",
|
||||
camera.short_name, bytes_to_delete, n, bytes_needed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to unlink all the uuids in `self.to_unlink`. Any which can't be unlinked will
|
||||
/// be retained in the vec.
|
||||
fn try_unlink(&mut self) {
|
||||
let to_mark_deleted = &mut self.to_mark_deleted;
|
||||
let fd = &self.dir.fd;
|
||||
self.to_unlink.retain(|uuid| {
|
||||
if let Err(e) = SampleFileDir::unlink(fd, *uuid) {
|
||||
if e.kind() == io::ErrorKind::NotFound {
|
||||
warn!("dir: Sample file {} already deleted!", uuid.hyphenated());
|
||||
to_mark_deleted.push(*uuid);
|
||||
false
|
||||
} else {
|
||||
warn!("dir: Unable to unlink {}: {}", uuid.hyphenated(), e);
|
||||
true
|
||||
}
|
||||
} else {
|
||||
to_mark_deleted.push(*uuid);
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
145
src/error.rs
Normal file
145
src/error.rs
Normal file
@ -0,0 +1,145 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
extern crate rusqlite;
|
||||
extern crate time;
|
||||
extern crate uuid;
|
||||
|
||||
use core::ops::Deref;
|
||||
use core::num;
|
||||
use ffmpeg;
|
||||
use openssl::error::ErrorStack;
|
||||
use serde_json;
|
||||
use std::boxed::Box;
|
||||
use std::convert::From;
|
||||
use std::error;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::result;
|
||||
use std::string::String;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error {
|
||||
pub description: String,
|
||||
pub cause: Option<Box<error::Error + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new(description: String) -> Self {
|
||||
Error{description: description, cause: None }
|
||||
}
|
||||
|
||||
// Returns a function to pass to std::result::Result::map_err which annotates the given error
|
||||
// with a prefix.
|
||||
pub fn annotator(prefix: &'static str) -> impl Fn(Error) -> Error {
|
||||
move |e| { Error{description: format!("{}: {}", prefix, e.description), cause: e.cause} }
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for Error {
|
||||
fn description(&self) -> &str { &self.description }
|
||||
fn cause(&self) -> Option<&error::Error> {
|
||||
match self.cause {
|
||||
Some(ref b) => Some(b.deref()),
|
||||
None => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> {
|
||||
write!(f, "Error: {}", self.description)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(slamb): isn't there a "<? implements error::Error>" or some such?
|
||||
|
||||
impl From<rusqlite::Error> for Error {
|
||||
fn from(err: rusqlite::Error) -> Self {
|
||||
use std::error::{Error as E};
|
||||
Error{description: String::from(err.description()),
|
||||
cause: Some(Box::new(err))}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Self {
|
||||
use std::error::{Error as E};
|
||||
Error{description: String::from(err.description()),
|
||||
cause: Some(Box::new(err))}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<time::ParseError> for Error {
|
||||
fn from(err: time::ParseError) -> Self {
|
||||
use std::error::{Error as E};
|
||||
Error{description: String::from(err.description()),
|
||||
cause: Some(Box::new(err))}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<num::ParseIntError> for Error {
|
||||
fn from(err: num::ParseIntError) -> Self {
|
||||
use std::error::{Error as E};
|
||||
Error{description: err.description().to_owned(),
|
||||
cause: Some(Box::new(err))}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
use std::error::{Error as E};
|
||||
Error{description: format!("{} ({})", err.description(), err),
|
||||
cause: Some(Box::new(err))}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ffmpeg::Error> for Error {
|
||||
fn from(err: ffmpeg::Error) -> Self {
|
||||
use std::error::{Error as E};
|
||||
Error{description: format!("{} ({})", err.description(), err),
|
||||
cause: Some(Box::new(err))}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<uuid::ParseError> for Error {
|
||||
fn from(_: uuid::ParseError) -> Self {
|
||||
Error{description: String::from("UUID parse error"),
|
||||
cause: None}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ErrorStack> for Error {
|
||||
fn from(_: ErrorStack) -> Self {
|
||||
Error{description: String::from("openssl error"), cause: None}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = result::Result<T, Error>;
|
316
src/ffmpeg.cc
316
src/ffmpeg.cc
@ -1,316 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// ffmpeg.cc: See ffmpeg.h for description.
|
||||
|
||||
#include "ffmpeg.h"
|
||||
|
||||
#include <mutex>
|
||||
|
||||
extern "C" {
|
||||
#include <libavutil/buffer.h>
|
||||
#include <libavutil/mathematics.h>
|
||||
#include <libavutil/version.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavcodec/version.h>
|
||||
#include <libavformat/version.h>
|
||||
} // extern "C"
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
|
||||
#include "string.h"
|
||||
|
||||
// libav lacks this ffmpeg constant.
|
||||
#ifndef AV_ERROR_MAX_STRING_SIZE
|
||||
#define AV_ERROR_MAX_STRING_SIZE 64
|
||||
#endif
|
||||
|
||||
DEFINE_int32(avlevel, AV_LOG_INFO,
|
||||
"maximum logging level for ffmpeg/libav; "
|
||||
"higher levels will be ignored.");
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string AvError2Str(re2::StringPiece function, int err) {
|
||||
char str[AV_ERROR_MAX_STRING_SIZE];
|
||||
if (av_strerror(err, str, sizeof(str)) == 0) {
|
||||
return StrCat(function, ": ", str);
|
||||
}
|
||||
return StrCat(function, ": unknown error ", err);
|
||||
}
|
||||
|
||||
struct Dictionary {
|
||||
Dictionary() {}
|
||||
Dictionary(const Dictionary &) = delete;
|
||||
Dictionary &operator=(const Dictionary &) = delete;
|
||||
~Dictionary() { av_dict_free(&dict); }
|
||||
|
||||
bool Set(const char *key, const char *value, std::string *error_message) {
|
||||
int ret = av_dict_set(&dict, key, value, 0);
|
||||
if (ret < 0) {
|
||||
*error_message = AvError2Str("av_dict_set", ret);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool size() const { return av_dict_count(dict); }
|
||||
|
||||
AVDictionary *dict = nullptr;
|
||||
};
|
||||
|
||||
google::LogSeverity GlogLevelFromAvLevel(int avlevel) {
|
||||
if (avlevel >= AV_LOG_INFO) {
|
||||
return google::GLOG_INFO;
|
||||
} else if (avlevel >= AV_LOG_WARNING) {
|
||||
return google::GLOG_WARNING;
|
||||
} else if (avlevel > AV_LOG_PANIC) {
|
||||
return google::GLOG_ERROR;
|
||||
} else {
|
||||
return google::GLOG_FATAL;
|
||||
}
|
||||
}
|
||||
|
||||
void AvLogCallback(void *avcl, int avlevel, const char *fmt, va_list vl) {
|
||||
if (avlevel > FLAGS_avlevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// google::LogMessage expects a "file" and "line" to be prefixed to the
|
||||
// log message, like so:
|
||||
//
|
||||
// W1210 11:00:32.224936 28739 ffmpeg_rtsp:0] Estimating duration ...
|
||||
// ^file ^line
|
||||
//
|
||||
// Normally this is filled in via the __FILE__ and __LINE__
|
||||
// C preprocessor macros. In this case, try to fill in something useful
|
||||
// based on the information ffmpeg supplies.
|
||||
std::string file("ffmpeg");
|
||||
if (avcl != nullptr) {
|
||||
auto *avclass = *reinterpret_cast<AVClass **>(avcl);
|
||||
file.push_back('_');
|
||||
file.append(avclass->item_name(avcl));
|
||||
}
|
||||
char line[512];
|
||||
vsnprintf(line, sizeof(line), fmt, vl);
|
||||
google::LogSeverity glog_level = GlogLevelFromAvLevel(avlevel);
|
||||
google::LogMessage(file.c_str(), 0, glog_level).stream() << line;
|
||||
}
|
||||
|
||||
int AvLockCallback(void **mutex, enum AVLockOp op) {
|
||||
auto typed_mutex = reinterpret_cast<std::mutex **>(mutex);
|
||||
switch (op) {
|
||||
case AV_LOCK_CREATE:
|
||||
LOG_IF(DFATAL, *typed_mutex != nullptr)
|
||||
<< "creating mutex over existing value.";
|
||||
*typed_mutex = new std::mutex;
|
||||
break;
|
||||
case AV_LOCK_DESTROY:
|
||||
delete *typed_mutex;
|
||||
*typed_mutex = nullptr;
|
||||
break;
|
||||
case AV_LOCK_OBTAIN:
|
||||
(*typed_mutex)->lock();
|
||||
break;
|
||||
case AV_LOCK_RELEASE:
|
||||
(*typed_mutex)->unlock();
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string StringifyVersion(int version_int) {
|
||||
return StrCat((version_int >> 16) & 0xFF, ".", (version_int >> 8) & 0xFF, ".",
|
||||
(version_int)&0xFF);
|
||||
}
|
||||
|
||||
void LogVersion(const char *library_name, int compiled_version,
|
||||
int running_version, const char *configuration) {
|
||||
LOG(INFO) << library_name << ": compiled with version "
|
||||
<< StringifyVersion(compiled_version) << ", running with version "
|
||||
<< StringifyVersion(running_version)
|
||||
<< ", configuration: " << configuration;
|
||||
}
|
||||
|
||||
class RealInputVideoPacketStream : public InputVideoPacketStream {
|
||||
public:
|
||||
RealInputVideoPacketStream() {
|
||||
ctx_ = CHECK_NOTNULL(avformat_alloc_context());
|
||||
}
|
||||
|
||||
RealInputVideoPacketStream(const RealInputVideoPacketStream &) = delete;
|
||||
RealInputVideoPacketStream &operator=(const RealInputVideoPacketStream &) =
|
||||
delete;
|
||||
|
||||
~RealInputVideoPacketStream() final {
|
||||
avformat_close_input(&ctx_);
|
||||
avformat_free_context(ctx_);
|
||||
}
|
||||
|
||||
bool GetNext(VideoPacket *pkt, std::string *error_message) final {
|
||||
while (true) {
|
||||
av_packet_unref(pkt->pkt());
|
||||
int ret = av_read_frame(ctx_, pkt->pkt());
|
||||
if (ret != 0) {
|
||||
if (ret == AVERROR_EOF) {
|
||||
error_message->clear();
|
||||
} else {
|
||||
*error_message = AvError2Str("av_read_frame", ret);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (pkt->pkt()->stream_index != stream_index_) {
|
||||
VLOG(3) << "Ignoring packet for stream " << pkt->pkt()->stream_index
|
||||
<< "; only interested in " << stream_index_;
|
||||
continue;
|
||||
}
|
||||
VLOG(3) << "Read packet with pts=" << pkt->pkt()->pts
|
||||
<< ", dts=" << pkt->pkt()->dts << ", key=" << pkt->is_key();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const AVStream *stream() const final { return ctx_->streams[stream_index_]; }
|
||||
|
||||
private:
|
||||
friend class RealVideoSource;
|
||||
int64_t min_next_pts_ = std::numeric_limits<int64_t>::min();
|
||||
int64_t min_next_dts_ = std::numeric_limits<int64_t>::min();
|
||||
AVFormatContext *ctx_ = nullptr; // owned.
|
||||
int stream_index_ = -1;
|
||||
};
|
||||
|
||||
class RealVideoSource : public VideoSource {
|
||||
public:
|
||||
RealVideoSource() {
|
||||
CHECK_GE(0, av_lockmgr_register(&AvLockCallback));
|
||||
av_log_set_callback(&AvLogCallback);
|
||||
av_register_all();
|
||||
avformat_network_init();
|
||||
LogVersion("avutil", LIBAVUTIL_VERSION_INT, avutil_version(),
|
||||
avutil_configuration());
|
||||
LogVersion("avformat", LIBAVFORMAT_VERSION_INT, avformat_version(),
|
||||
avformat_configuration());
|
||||
LogVersion("avcodec", LIBAVCODEC_VERSION_INT, avcodec_version(),
|
||||
avcodec_configuration());
|
||||
}
|
||||
|
||||
std::unique_ptr<InputVideoPacketStream> OpenRtsp(
|
||||
const std::string &url, std::string *error_message) final {
|
||||
std::unique_ptr<InputVideoPacketStream> stream;
|
||||
Dictionary open_options;
|
||||
if (!open_options.Set("rtsp_transport", "tcp", error_message) ||
|
||||
// https://trac.ffmpeg.org/ticket/5018 workaround attempt.
|
||||
!open_options.Set("probesize", "262144", error_message) ||
|
||||
!open_options.Set("user-agent", "moonfire-nvr", error_message) ||
|
||||
// 10-second socket timeout, in microseconds.
|
||||
!open_options.Set("stimeout", "10000000", error_message)) {
|
||||
return stream;
|
||||
}
|
||||
|
||||
stream = OpenCommon(url, &open_options.dict, error_message);
|
||||
if (stream == nullptr) {
|
||||
return stream;
|
||||
}
|
||||
|
||||
// Discard the first packet.
|
||||
LOG(INFO) << "Discarding the first packet to work around "
|
||||
"https://trac.ffmpeg.org/ticket/5018";
|
||||
VideoPacket dummy;
|
||||
if (!stream->GetNext(&dummy, error_message)) {
|
||||
stream.reset();
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
std::unique_ptr<InputVideoPacketStream> OpenFile(
|
||||
const std::string &filename, std::string *error_message) final {
|
||||
AVDictionary *open_options = nullptr;
|
||||
return OpenCommon(filename, &open_options, error_message);
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<InputVideoPacketStream> OpenCommon(
|
||||
const std::string &source, AVDictionary **dict,
|
||||
std::string *error_message) {
|
||||
std::unique_ptr<RealInputVideoPacketStream> stream(
|
||||
new RealInputVideoPacketStream);
|
||||
|
||||
int ret = avformat_open_input(&stream->ctx_, source.c_str(), nullptr, dict);
|
||||
if (ret != 0) {
|
||||
*error_message = AvError2Str("avformat_open_input", ret);
|
||||
return std::unique_ptr<InputVideoPacketStream>();
|
||||
}
|
||||
|
||||
if (av_dict_count(*dict) != 0) {
|
||||
std::vector<std::string> ignored;
|
||||
AVDictionaryEntry *ent = nullptr;
|
||||
while ((ent = av_dict_get(*dict, "", ent, AV_DICT_IGNORE_SUFFIX)) !=
|
||||
nullptr) {
|
||||
ignored.push_back(StrCat(ent->key, "=", ent->value));
|
||||
}
|
||||
LOG(WARNING) << "avformat_open_input ignored " << ignored.size()
|
||||
<< " options: " << Join(ignored, ", ");
|
||||
}
|
||||
|
||||
ret = avformat_find_stream_info(stream->ctx_, nullptr);
|
||||
if (ret < 0) {
|
||||
*error_message = AvError2Str("avformat_find_stream_info", ret);
|
||||
return std::unique_ptr<InputVideoPacketStream>();
|
||||
}
|
||||
|
||||
// Find the video stream.
|
||||
for (unsigned int i = 0; i < stream->ctx_->nb_streams; ++i) {
|
||||
if (stream->ctx_->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
|
||||
VLOG(1) << "Video stream index is " << i;
|
||||
stream->stream_index_ = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (stream->stream() == nullptr) {
|
||||
*error_message = StrCat("no video stream");
|
||||
return std::unique_ptr<InputVideoPacketStream>();
|
||||
}
|
||||
|
||||
return std::unique_ptr<InputVideoPacketStream>(stream.release());
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
VideoSource *GetRealVideoSource() {
|
||||
static auto *real_video_source = new RealVideoSource; // never deleted.
|
||||
return real_video_source;
|
||||
}
|
||||
|
||||
} // namespace moonfire_nvr
|
149
src/ffmpeg.h
149
src/ffmpeg.h
@ -1,149 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// ffmpeg.h: ffmpeg (or libav) wrappers for operations needed by moonfire_nvr.
|
||||
// This is not a general-purpose wrapper. It makes assumptions about the
|
||||
// data we will be operated on and the desired operations, such as:
|
||||
//
|
||||
// * The input should contain no "B" frames (bi-directionally predicted
|
||||
// pictures) and thus input frames should be strictly in order of ascending
|
||||
// PTS as well as DTS.
|
||||
//
|
||||
// * Only video frames are of interest.
|
||||
|
||||
#ifndef MOONFIRE_NVR_FFMPEG_H
|
||||
#define MOONFIRE_NVR_FFMPEG_H
|
||||
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <glog/logging.h>
|
||||
#include <re2/stringpiece.h>
|
||||
|
||||
extern "C" {
|
||||
#include <libavformat/avformat.h>
|
||||
} // extern "C"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
// An encoded video packet.
|
||||
class VideoPacket {
|
||||
public:
|
||||
VideoPacket() { av_init_packet(&pkt_); }
|
||||
VideoPacket(const VideoPacket &) = delete;
|
||||
VideoPacket &operator=(const VideoPacket &) = delete;
|
||||
~VideoPacket() { av_packet_unref(&pkt_); }
|
||||
|
||||
// Returns iff this packet represents a key frame.
|
||||
//
|
||||
// (A key frame is one that can be decoded without previous frames.)
|
||||
//
|
||||
// PRE: this packet is valid, as if it has been filled by
|
||||
// InputVideoPacketStream::Next.
|
||||
bool is_key() const { return (pkt_.flags & AV_PKT_FLAG_KEY) != 0; }
|
||||
|
||||
int64_t pts() const { return pkt_.pts; }
|
||||
|
||||
AVPacket *pkt() { return &pkt_; }
|
||||
const AVPacket *pkt() const { return &pkt_; }
|
||||
|
||||
re2::StringPiece data() {
|
||||
return re2::StringPiece(reinterpret_cast<const char *>(pkt_.data),
|
||||
pkt_.size);
|
||||
}
|
||||
|
||||
private:
|
||||
AVPacket pkt_;
|
||||
};
|
||||
|
||||
// An input stream of (still-encoded) video packets.
|
||||
class InputVideoPacketStream {
|
||||
public:
|
||||
InputVideoPacketStream() {}
|
||||
InputVideoPacketStream(const InputVideoPacketStream &) = delete;
|
||||
InputVideoPacketStream &operator=(const InputVideoPacketStream &) = delete;
|
||||
|
||||
// Closes the stream.
|
||||
virtual ~InputVideoPacketStream() {}
|
||||
|
||||
// Get the next packet.
|
||||
//
|
||||
// Returns true iff one is available, false on EOF or failure.
|
||||
// |error_message| will be filled on failure, empty on EOF.
|
||||
//
|
||||
// PRE: the stream is healthy: there was no prior Close() call or GetNext()
|
||||
// failure.
|
||||
virtual bool GetNext(VideoPacket *pkt, std::string *error_message) = 0;
|
||||
|
||||
// Returns the video stream.
|
||||
virtual const AVStream *stream() const = 0;
|
||||
|
||||
re2::StringPiece extradata() const {
|
||||
return re2::StringPiece(
|
||||
reinterpret_cast<const char *>(stream()->codec->extradata),
|
||||
stream()->codec->extradata_size);
|
||||
}
|
||||
};
|
||||
|
||||
// A class which opens streams.
|
||||
// There's one of these for proudction use; see GetRealVideoSource().
|
||||
// It's an abstract class for testability.
|
||||
class VideoSource {
|
||||
public:
|
||||
virtual ~VideoSource() {}
|
||||
|
||||
// Open the given RTSP URL, accessing the first video stream.
|
||||
//
|
||||
// The RTSP URL will be opened with TCP and a hardcoded socket timeout.
|
||||
//
|
||||
// The first frame will be automatically discarded as a bug workaround.
|
||||
// https://trac.ffmpeg.org/ticket/5018
|
||||
//
|
||||
// Returns success, filling |error_message| on failure.
|
||||
//
|
||||
// PRE: closed.
|
||||
virtual std::unique_ptr<InputVideoPacketStream> OpenRtsp(
|
||||
const std::string &url, std::string *error_message) = 0;
|
||||
|
||||
// Open the given video file, accessing the first video stream.
|
||||
//
|
||||
// Returns the stream. On failure, returns nullptr and fills
|
||||
// |error_message|.
|
||||
virtual std::unique_ptr<InputVideoPacketStream> OpenFile(
|
||||
const std::string &filename, std::string *error_message) = 0;
|
||||
};
|
||||
|
||||
// Returns a VideoSource for production use, which will never be deleted.
|
||||
VideoSource *GetRealVideoSource();
|
||||
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
#endif // MOONFIRE_NVR_FFMPEG_H
|
@ -1,222 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// filesystem.cc: See filesystem.h.
|
||||
|
||||
#include "filesystem.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/queue.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/event.h>
|
||||
#include <event2/keyvalq_struct.h>
|
||||
#include <event2/http.h>
|
||||
#include <gperftools/profiler.h>
|
||||
#include <glog/logging.h>
|
||||
|
||||
#include "string.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
namespace {
|
||||
|
||||
class RealFile : public File {
|
||||
public:
|
||||
RealFile(re2::StringPiece name, int fd)
|
||||
: name_(name.data(), name.size()), fd_(fd) {}
|
||||
RealFile(const RealFile &) = delete;
|
||||
void operator=(const RealFile &) = delete;
|
||||
|
||||
~RealFile() final { Close(); }
|
||||
|
||||
const std::string &name() const { return name_; }
|
||||
|
||||
int Access(const char *path, int mode, int flags) final {
|
||||
return faccessat(fd_, path, mode, flags) < 0 ? errno : 0;
|
||||
}
|
||||
|
||||
int Close() final {
|
||||
if (fd_ < 0) {
|
||||
return 0;
|
||||
}
|
||||
int ret;
|
||||
while ((ret = close(fd_)) != 0 && errno == EINTR)
|
||||
;
|
||||
if (ret != 0) {
|
||||
return errno;
|
||||
}
|
||||
fd_ = -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Lock(int operation) final {
|
||||
return (flock(fd_, operation) < 0) ? errno : 0;
|
||||
}
|
||||
|
||||
int Open(const char *path, int flags, int *fd) final {
|
||||
return Open(path, flags, 0, fd);
|
||||
}
|
||||
|
||||
int Open(const char *path, int flags, std::unique_ptr<File> *f) final {
|
||||
return Open(path, flags, 0, f);
|
||||
}
|
||||
|
||||
int Open(const char *path, int flags, mode_t mode, int *fd) final {
|
||||
int ret = openat(fd_, path, flags, mode);
|
||||
if (ret < 0) {
|
||||
return errno;
|
||||
}
|
||||
*fd = ret;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Open(const char *path, int flags, mode_t mode,
|
||||
std::unique_ptr<File> *f) final {
|
||||
int ret = openat(fd_, path, flags, mode);
|
||||
if (ret < 0) {
|
||||
return errno;
|
||||
}
|
||||
f->reset(new RealFile(StrCat(name_, "/", path), ret));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Read(void *buf, size_t size, size_t *bytes_read) final {
|
||||
ssize_t ret;
|
||||
while ((ret = read(fd_, buf, size)) == -1 && errno == EINTR)
|
||||
;
|
||||
if (ret < 0) {
|
||||
return errno;
|
||||
}
|
||||
*bytes_read = static_cast<size_t>(ret);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Stat(struct stat *buf) final { return (fstat(fd_, buf) < 0) ? errno : 0; }
|
||||
|
||||
int Sync() final { return (fsync(fd_) < 0) ? errno : 0; }
|
||||
|
||||
int Truncate(off_t length) final {
|
||||
return (ftruncate(fd_, length) < 0) ? errno : 0;
|
||||
}
|
||||
|
||||
int Unlink(const char *pathname) {
|
||||
return unlinkat(fd_, pathname, 0) != 0 ? errno : 0;
|
||||
}
|
||||
|
||||
int Write(re2::StringPiece data, size_t *bytes_written) final {
|
||||
ssize_t ret;
|
||||
while ((ret = write(fd_, data.data(), data.size())) == -1 && errno == EINTR)
|
||||
;
|
||||
if (ret < 0) {
|
||||
return errno;
|
||||
}
|
||||
*bytes_written = static_cast<size_t>(ret);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
int fd_ = -1;
|
||||
};
|
||||
|
||||
class RealFilesystem : public Filesystem {
|
||||
public:
|
||||
bool DirForEach(const char *dir_path,
|
||||
std::function<IterationControl(const dirent *)> fn,
|
||||
std::string *error_message) final {
|
||||
DIR *owned_dir = opendir(dir_path);
|
||||
if (owned_dir == nullptr) {
|
||||
int err = errno;
|
||||
*error_message =
|
||||
StrCat("Unable to examine ", dir_path, ": ", strerror(err));
|
||||
return false;
|
||||
}
|
||||
struct dirent *ent;
|
||||
while (errno = 0, (ent = readdir(owned_dir)) != nullptr) {
|
||||
if (fn(ent) == IterationControl::kBreak) {
|
||||
closedir(owned_dir);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
int err = errno;
|
||||
closedir(owned_dir);
|
||||
if (err != 0) {
|
||||
*error_message = StrCat("readdir failed: ", strerror(err));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int Open(const char *path, int flags, std::unique_ptr<File> *f) final {
|
||||
return Open(path, flags, 0, f);
|
||||
}
|
||||
|
||||
int Open(const char *path, int flags, mode_t mode,
|
||||
std::unique_ptr<File> *f) final {
|
||||
int ret = open(path, flags, mode);
|
||||
if (ret < 0) {
|
||||
return errno;
|
||||
}
|
||||
f->reset(new RealFile(path, ret));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Mkdir(const char *path, mode_t mode) final {
|
||||
return (mkdir(path, mode) < 0) ? errno : 0;
|
||||
}
|
||||
|
||||
int Rmdir(const char *path) final { return (rmdir(path) < 0) ? errno : 0; }
|
||||
|
||||
int Stat(const char *path, struct stat *buf) final {
|
||||
return (stat(path, buf) < 0) ? errno : 0;
|
||||
}
|
||||
|
||||
int Unlink(const char *path) final { return (unlink(path) < 0) ? errno : 0; }
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Filesystem *GetRealFilesystem() {
|
||||
static Filesystem *real_filesystem = new RealFilesystem;
|
||||
return real_filesystem;
|
||||
}
|
||||
|
||||
} // namespace moonfire_nvr
|
140
src/filesystem.h
140
src/filesystem.h
@ -1,140 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// filesystem.h: helpers for dealing with the local filesystem.
|
||||
|
||||
#ifndef MOONFIRE_NVR_FILESYSTEM_H
|
||||
#define MOONFIRE_NVR_FILESYSTEM_H
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdarg.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/http.h>
|
||||
#include <glog/logging.h>
|
||||
#include <re2/stringpiece.h>
|
||||
|
||||
#include "common.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
// Represents an open file descriptor. All methods but Close() are thread-safe.
|
||||
class File {
|
||||
public:
|
||||
// Close the file, ignoring the result.
|
||||
virtual ~File() {}
|
||||
|
||||
// A name for the file (typically assigned at open time).
|
||||
virtual const std::string &name() const = 0;
|
||||
|
||||
// faccessat(), returning 0 on success or errno>0 on failure.
|
||||
virtual int Access(const char *path, int mode, int flags) = 0;
|
||||
|
||||
// Close the file, returning 0 on success or errno>0 on failure.
|
||||
// Already closed is considered a success.
|
||||
virtual int Close() = 0;
|
||||
|
||||
// flock(), returning 0 on success or errno>0 on failure.
|
||||
virtual int Lock(int operation) = 0;
|
||||
|
||||
// openat(), returning 0 on success or errno>0 on failure.
|
||||
virtual int Open(const char *path, int flags, int *fd) = 0;
|
||||
virtual int Open(const char *path, int flags, std::unique_ptr<File> *f) = 0;
|
||||
virtual int Open(const char *path, int flags, mode_t mode, int *fd) = 0;
|
||||
virtual int Open(const char *path, int flags, mode_t mode,
|
||||
std::unique_ptr<File> *f) = 0;
|
||||
|
||||
// read(), returning 0 on success or errno>0 on failure.
|
||||
// On success, |bytes_read| will be updated.
|
||||
virtual int Read(void *buf, size_t count, size_t *bytes_read) = 0;
|
||||
|
||||
// fstat(), returning 0 on success or errno>0 on failure.
|
||||
virtual int Stat(struct stat *buf) = 0;
|
||||
|
||||
// fsync(), returning 0 on success or errno>0 on failure.
|
||||
virtual int Sync() = 0;
|
||||
|
||||
// ftruncate(), returning 0 on success or errno>0 on failure.
|
||||
virtual int Truncate(off_t length) = 0;
|
||||
|
||||
// unlink() the specified file, returning 0 on success or errno>0 on failure.
|
||||
virtual int Unlink(const char *path) = 0;
|
||||
|
||||
// Write to the file, returning 0 on success or errno>0 on failure.
|
||||
// On success, |bytes_written| will be updated.
|
||||
virtual int Write(re2::StringPiece data, size_t *bytes_written) = 0;
|
||||
};
|
||||
|
||||
// Interface to the local filesystem. There's typically one per program,
|
||||
// but it's an abstract class for testability. Thread-safe.
|
||||
class Filesystem {
|
||||
public:
|
||||
virtual ~Filesystem() {}
|
||||
|
||||
// Execute |fn| for each directory entry in |dir_path|, stopping early
|
||||
// (successfully) if the callback returns IterationControl::kBreak.
|
||||
//
|
||||
// On success, returns true.
|
||||
// On failure, returns false and updates |error_msg|.
|
||||
virtual bool DirForEach(const char *dir_path,
|
||||
std::function<IterationControl(const dirent *)> fn,
|
||||
std::string *error_msg) = 0;
|
||||
|
||||
// open() the specified path, returning 0 on success or errno>0 on failure.
|
||||
// On success, |f| is populated with an open file.
|
||||
virtual int Open(const char *path, int flags, std::unique_ptr<File> *f) = 0;
|
||||
virtual int Open(const char *path, int flags, mode_t mode,
|
||||
std::unique_ptr<File> *f) = 0;
|
||||
|
||||
// mkdir() the specified path, returning 0 on success or errno>0 on failure.
|
||||
virtual int Mkdir(const char *path, mode_t mode) = 0;
|
||||
|
||||
// rmdir() the specified path, returning 0 on success or errno>0 on failure.
|
||||
virtual int Rmdir(const char *path) = 0;
|
||||
|
||||
// stat() the specified path, returning 0 on success or errno>0 on failure.
|
||||
virtual int Stat(const char *path, struct stat *buf) = 0;
|
||||
|
||||
// unlink() the specified file, returning 0 on success or errno>0 on failure.
|
||||
virtual int Unlink(const char *path) = 0;
|
||||
};
|
||||
|
||||
// Get the (singleton) real filesystem, which is never deleted.
|
||||
Filesystem *GetRealFilesystem();
|
||||
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
#endif // MOONFIRE_NVR_FILESYSTEM_H
|
153
src/h264-test.cc
153
src/h264-test.cc
@ -1,153 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// h264-test.cc: tests of the h264.h interface.
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
#include <glog/logging.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "h264.h"
|
||||
#include "string.h"
|
||||
|
||||
DECLARE_bool(alsologtostderr);
|
||||
|
||||
namespace moonfire_nvr {
|
||||
namespace {
|
||||
|
||||
const uint8_t kAnnexBTestInput[] = {
|
||||
0x00, 0x00, 0x00, 0x01, 0x67, 0x4d, 0x00, 0x1f, 0x9a, 0x66, 0x02, 0x80,
|
||||
0x2d, 0xff, 0x35, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0xfa, 0x00, 0x00,
|
||||
0x1d, 0x4c, 0x01, 0x00, 0x00, 0x00, 0x01, 0x68, 0xee, 0x3c, 0x80};
|
||||
|
||||
const uint8_t kAvcDecoderConfigTestInput[] = {
|
||||
0x01, 0x4d, 0x00, 0x1f, 0xff, 0xe1, 0x00, 0x17, 0x67, 0x4d,
|
||||
0x00, 0x1f, 0x9a, 0x66, 0x02, 0x80, 0x2d, 0xff, 0x35, 0x01,
|
||||
0x01, 0x01, 0x40, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x1d, 0x4c,
|
||||
0x01, 0x01, 0x00, 0x04, 0x68, 0xee, 0x3c, 0x80};
|
||||
|
||||
const char kTestOutput[] =
|
||||
"00 00 00 84 61 76 63 31 00 00 00 00 00 00 00 01 "
|
||||
"00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 "
|
||||
"05 00 02 d0 00 48 00 00 00 48 00 00 00 00 00 00 "
|
||||
"00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 "
|
||||
"00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 "
|
||||
"00 00 00 18 ff ff 00 00 00 2e 61 76 63 43 01 4d "
|
||||
"00 1f ff e1 00 17 67 4d 00 1f 9a 66 02 80 2d ff "
|
||||
"35 01 01 01 40 00 00 fa 00 00 1d 4c 01 01 00 04 "
|
||||
"68 ee 3c 80";
|
||||
|
||||
TEST(H264Test, DecodeOnly) {
|
||||
std::vector<std::string> nal_units_hexed;
|
||||
re2::StringPiece test_input(reinterpret_cast<const char *>(kAnnexBTestInput),
|
||||
sizeof(kAnnexBTestInput));
|
||||
internal::NalUnitFunction fn = [&nal_units_hexed](re2::StringPiece nal_unit) {
|
||||
nal_units_hexed.push_back(ToHex(nal_unit, true));
|
||||
return IterationControl::kContinue;
|
||||
};
|
||||
std::string error_message;
|
||||
ASSERT_TRUE(internal::DecodeH264AnnexB(test_input, fn, &error_message))
|
||||
<< error_message;
|
||||
EXPECT_THAT(nal_units_hexed,
|
||||
testing::ElementsAre("67 4d 00 1f 9a 66 02 80 2d ff 35 01 01 01 "
|
||||
"40 00 00 fa 00 00 1d 4c 01",
|
||||
"68 ee 3c 80"));
|
||||
}
|
||||
|
||||
TEST(H264Test, SampleEntryFromAnnexBExtraData) {
|
||||
re2::StringPiece test_input(reinterpret_cast<const char *>(kAnnexBTestInput),
|
||||
sizeof(kAnnexBTestInput));
|
||||
std::string sample_entry;
|
||||
std::string error_message;
|
||||
bool need_transform;
|
||||
ASSERT_TRUE(ParseExtraData(test_input, 1280, 720, &sample_entry,
|
||||
&need_transform, &error_message))
|
||||
<< error_message;
|
||||
|
||||
EXPECT_EQ(kTestOutput, ToHex(sample_entry, true));
|
||||
EXPECT_TRUE(need_transform);
|
||||
}
|
||||
|
||||
TEST(H264Test, SampleEntryFromAvcDecoderConfigExtraData) {
|
||||
re2::StringPiece test_input(
|
||||
reinterpret_cast<const char *>(kAvcDecoderConfigTestInput),
|
||||
sizeof(kAvcDecoderConfigTestInput));
|
||||
std::string sample_entry;
|
||||
std::string error_message;
|
||||
bool need_transform;
|
||||
ASSERT_TRUE(ParseExtraData(test_input, 1280, 720, &sample_entry,
|
||||
&need_transform, &error_message))
|
||||
<< error_message;
|
||||
|
||||
EXPECT_EQ(kTestOutput, ToHex(sample_entry, true));
|
||||
EXPECT_FALSE(need_transform);
|
||||
}
|
||||
|
||||
TEST(H264Test, TransformSampleEntry) {
|
||||
const uint8_t kInput[] = {
|
||||
0x00, 0x00, 0x00, 0x01, 0x67, 0x4d, 0x00, 0x1f, 0x9a, 0x66,
|
||||
0x02, 0x80, 0x2d, 0xff, 0x35, 0x01, 0x01, 0x01, 0x40, 0x00,
|
||||
0x00, 0xfa, 0x00, 0x00, 0x1d, 0x4c, 0x01,
|
||||
|
||||
0x00, 0x00, 0x00, 0x01, 0x68, 0xee, 0x3c, 0x80,
|
||||
|
||||
0x00, 0x00, 0x00, 0x01, 0x06, 0x06, 0x01, 0xc4, 0x80,
|
||||
|
||||
0x00, 0x00, 0x00, 0x01, 0x65, 0x88, 0x80, 0x10, 0x00, 0x08,
|
||||
0x7f, 0x00, 0x5d, 0x27, 0xb5, 0xc1, 0xff, 0x8c, 0xd6, 0x35,
|
||||
// (truncated)
|
||||
};
|
||||
const char kExpectedOutput[] =
|
||||
"00 00 00 17 "
|
||||
"67 4d 00 1f 9a 66 02 80 2d ff 35 01 01 01 40 00 00 fa 00 00 1d 4c 01 "
|
||||
"00 00 00 04 68 ee 3c 80 "
|
||||
"00 00 00 05 06 06 01 c4 80 "
|
||||
"00 00 00 10 "
|
||||
"65 88 80 10 00 08 7f 00 5d 27 b5 c1 ff 8c d6 35";
|
||||
re2::StringPiece input(reinterpret_cast<const char *>(kInput),
|
||||
sizeof(kInput));
|
||||
std::string out;
|
||||
std::string error_message;
|
||||
ASSERT_TRUE(TransformSampleData(input, &out, &error_message))
|
||||
<< error_message;
|
||||
EXPECT_EQ(kExpectedOutput, ToHex(out, true));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
FLAGS_alsologtostderr = true;
|
||||
google::ParseCommandLineFlags(&argc, &argv, true);
|
||||
testing::InitGoogleTest(&argc, argv);
|
||||
google::InitGoogleLogging(argv[0]);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
251
src/h264.cc
251
src/h264.cc
@ -1,251 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// h264.cc: see h264.h.
|
||||
|
||||
#include "h264.h"
|
||||
|
||||
#include <re2/re2.h>
|
||||
|
||||
#include "coding.h"
|
||||
#include "string.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
namespace {
|
||||
|
||||
// See ISO/IEC 14496-10 table 7-1 - NAL unit type codes, syntax element
|
||||
// categories, and NAL unit type classes.
|
||||
const int kNalUnitSeqParameterSet = 7;
|
||||
const int kNalUnitPicParameterSet = 8;
|
||||
|
||||
const uint8_t kNalUnitTypeMask = 0x1F; // bottom 5 bits of first byte of unit.
|
||||
|
||||
// Parse sequence parameter set and picture parameter set from ffmpeg's
|
||||
// "extra_data".
|
||||
bool ParseAnnexBExtraData(re2::StringPiece extradata, re2::StringPiece *sps,
|
||||
re2::StringPiece *pps, std::string *error_message) {
|
||||
bool ok = true;
|
||||
internal::NalUnitFunction fn = [&](re2::StringPiece nal_unit) {
|
||||
// See ISO/IEC 14496-10 section 7.3.1, which defines nal_unit.
|
||||
uint8_t nal_type = nal_unit[0] & kNalUnitTypeMask;
|
||||
switch (nal_type) {
|
||||
case kNalUnitSeqParameterSet:
|
||||
*sps = nal_unit;
|
||||
break;
|
||||
case kNalUnitPicParameterSet:
|
||||
*pps = nal_unit;
|
||||
break;
|
||||
default:
|
||||
*error_message =
|
||||
StrCat("Expected only SPS and PPS; got type ", nal_type);
|
||||
ok = false;
|
||||
return IterationControl::kBreak;
|
||||
}
|
||||
return IterationControl::kContinue;
|
||||
};
|
||||
if (!internal::DecodeH264AnnexB(extradata, fn, error_message) || !ok) {
|
||||
return false;
|
||||
}
|
||||
if (sps->empty() || pps->empty()) {
|
||||
*error_message = "SPS and PPS must be specified.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace internal {
|
||||
|
||||
// See ISO/IEC 14496-10 section B.2: Byte stream NAL unit decoding process.
|
||||
// This is a relatively simple, unoptimized implementation.
|
||||
bool DecodeH264AnnexB(re2::StringPiece data, NalUnitFunction process_nal_unit,
|
||||
std::string *error_message) {
|
||||
static const RE2 kStartCode("(\\x00{2,}\\x01)");
|
||||
|
||||
if (!RE2::Consume(&data, kStartCode)) {
|
||||
*error_message = StrCat("stream does not start with Annex B start code: ",
|
||||
ToHex(data, true));
|
||||
return false;
|
||||
}
|
||||
|
||||
while (!data.empty()) {
|
||||
// Now at the start of a NAL unit. Find the end.
|
||||
re2::StringPiece next_start;
|
||||
re2::StringPiece this_nal = data;
|
||||
if (RE2::FindAndConsume(&data, kStartCode, &next_start)) {
|
||||
// It ends where another start code is found.
|
||||
this_nal = re2::StringPiece(this_nal.data(),
|
||||
next_start.data() - this_nal.data());
|
||||
} else {
|
||||
// It ends at the end of |data|. |this_nal| is already correct.
|
||||
// Set |data| to be empty so the while loop exits after this iteration.
|
||||
data = re2::StringPiece();
|
||||
}
|
||||
|
||||
if (this_nal.empty()) {
|
||||
*error_message = "NAL unit can't be empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (process_nal_unit(this_nal) == IterationControl::kBreak) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
bool ParseExtraData(re2::StringPiece extradata, uint16_t width, uint16_t height,
|
||||
std::string *sample_entry, bool *need_transform,
|
||||
std::string *error_message) {
|
||||
uint32_t avcc_len;
|
||||
re2::StringPiece sps;
|
||||
re2::StringPiece pps;
|
||||
if (extradata.starts_with(re2::StringPiece("\x00\x00\x00\x01", 4)) ||
|
||||
extradata.starts_with(re2::StringPiece("\x00\x00\x01", 3))) {
|
||||
// ffmpeg supplied "extradata" in Annex B format.
|
||||
if (!ParseAnnexBExtraData(extradata, &sps, &pps, error_message)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This magic value is checked at the end.
|
||||
avcc_len = 19 + sps.size() + pps.size();
|
||||
*need_transform = true;
|
||||
} else {
|
||||
// Assume "extradata" holds an AVCDecoderConfiguration.
|
||||
avcc_len = 8 + extradata.size();
|
||||
*need_transform = false;
|
||||
}
|
||||
|
||||
// This magic value is also checked at the end.
|
||||
uint32_t avc1_len = 86 + avcc_len;
|
||||
|
||||
sample_entry->clear();
|
||||
sample_entry->reserve(avc1_len);
|
||||
|
||||
// This is a concatenation of the following boxes/classes.
|
||||
// SampleEntry, ISO/IEC 14496-10 section 8.5.2.
|
||||
uint32_t avc1_len_pos = sample_entry->size();
|
||||
AppendU32(avc1_len, sample_entry); // length
|
||||
sample_entry->append("avc1"); // type
|
||||
sample_entry->append(6, '\x00'); // reserved
|
||||
AppendU16(1, sample_entry); // data_reference_index = 1
|
||||
|
||||
// VisualSampleEntry, ISO/IEC 14496-12 section 12.1.3.
|
||||
sample_entry->append(16, '\x00'); // pre_defined + reserved
|
||||
AppendU16(width, sample_entry);
|
||||
AppendU16(height, sample_entry);
|
||||
AppendU32(UINT32_C(0x00480000), sample_entry); // horizresolution
|
||||
AppendU32(UINT32_C(0x00480000), sample_entry); // vertresolution
|
||||
AppendU32(0, sample_entry); // reserved
|
||||
AppendU16(1, sample_entry); // frame count
|
||||
sample_entry->append(32, '\x00'); // compressorname
|
||||
AppendU16(0x0018, sample_entry); // depth
|
||||
Append16(-1, sample_entry); // pre_defined
|
||||
|
||||
// AVCSampleEntry, ISO/IEC 14496-15 section 5.3.4.1.
|
||||
// AVCConfigurationBox, ISO/IEC 14496-15 section 5.3.4.1.
|
||||
uint32_t avcc_len_pos = sample_entry->size();
|
||||
AppendU32(avcc_len, sample_entry); // length
|
||||
sample_entry->append("avcC"); // type
|
||||
|
||||
if (!sps.empty() && !pps.empty()) {
|
||||
// Create the AVCDecoderConfiguration, ISO/IEC 14496-15 section 5.2.4.1.
|
||||
// The beginning of the AVCDecoderConfiguration takes a few values from
|
||||
// the SPS (ISO/IEC 14496-10 section 7.3.2.1.1). One caveat: that section
|
||||
// defines the syntax in terms of RBSP, not NAL. The difference is the
|
||||
// escaping of 00 00 01 and 00 00 02; see notes about
|
||||
// "emulation_prevention_three_byte" in ISO/IEC 14496-10 section 7.4.
|
||||
// It looks like 00 is not a valid value of profile_idc, so this distinction
|
||||
// shouldn't be relevant here. And ffmpeg seems to ignore it.
|
||||
sample_entry->push_back(1); // configurationVersion
|
||||
sample_entry->push_back(sps[1]); // profile_idc -> AVCProfileIndication
|
||||
sample_entry->push_back(
|
||||
sps[2]); // ...misc bits... -> profile_compatibility
|
||||
sample_entry->push_back(sps[3]); // level_idc -> AVCLevelIndication
|
||||
|
||||
// Hardcode lengthSizeMinusOne to 3, matching TransformSampleData's 4-byte
|
||||
// lengths.
|
||||
sample_entry->push_back(static_cast<char>(0xff));
|
||||
|
||||
// Only support one SPS and PPS.
|
||||
// ffmpeg's ff_isom_write_avcc has the same limitation, so it's probably
|
||||
// fine. This next byte is a reserved 0b111 + a 5-bit # of SPSs (1).
|
||||
sample_entry->push_back(static_cast<char>(0xe1));
|
||||
AppendU16(sps.size(), sample_entry);
|
||||
sample_entry->append(sps.data(), sps.size());
|
||||
sample_entry->push_back(1); // # of PPSs.
|
||||
AppendU16(pps.size(), sample_entry);
|
||||
sample_entry->append(pps.data(), pps.size());
|
||||
|
||||
if (sample_entry->size() - avcc_len_pos != avcc_len) {
|
||||
*error_message = StrCat(
|
||||
"internal error: anticipated AVCConfigurationBox length ", avcc_len,
|
||||
", but was actually ", sample_entry->size() - avcc_len_pos,
|
||||
"; sps length ", sps.size(), ", pps length ", pps.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
} else {
|
||||
sample_entry->append(extradata.data(), extradata.size());
|
||||
}
|
||||
|
||||
if (sample_entry->size() - avc1_len_pos != avc1_len) {
|
||||
*error_message =
|
||||
StrCat("internal error: anticipated AVCSampleEntry length ", avc1_len,
|
||||
", but was actually ", sample_entry->size() - avc1_len_pos,
|
||||
"; sps length ", sps.size(), ", pps length ", pps.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TransformSampleData(re2::StringPiece annexb_sample,
|
||||
std::string *avc_sample, std::string *error_message) {
|
||||
// See AVCParameterSamples, ISO/IEC 14496-15 section 5.3.2.
|
||||
avc_sample->clear();
|
||||
auto fn = [&](re2::StringPiece nal_unit) {
|
||||
// 4-byte length; this must be in sync with ParseExtraData's
|
||||
// lengthSizeMinusOne == 3.
|
||||
AppendU32(nal_unit.size(), avc_sample);
|
||||
avc_sample->append(nal_unit.data(), nal_unit.size());
|
||||
return IterationControl::kContinue;
|
||||
};
|
||||
if (!internal::DecodeH264AnnexB(annexb_sample, fn, error_message)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace moonfire_nvr
|
84
src/h264.h
84
src/h264.h
@ -1,84 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// h264.h: H.264 decoding. For the most part, Moonfire NVR does not try to
|
||||
// understand the video codec. However, H.264 has two byte stream encodings:
|
||||
// ISO/IEC 14496-10 Annex B, and ISO/IEC 14496-15 AVC access units.
|
||||
// When streaming from RTSP, ffmpeg supplies the former. We need the latter
|
||||
// to stick into .mp4 files. This file manages the conversion, both for
|
||||
// the ffmpeg "extra data" (which should become the ISO/IEC 14496-15
|
||||
// section 5.2.4.1 AVCDecoderConfigurationRecord) and the actual samples.
|
||||
//
|
||||
// ffmpeg of course has logic to do the same thing, but unfortunately it is
|
||||
// not exposed except through ffmpeg's own generated .mp4 file. Extracting
|
||||
// just this part of their .mp4 files would be more trouble than it's worth.
|
||||
|
||||
#ifndef MOONFIRE_NVR_H264_H
|
||||
#define MOONFIRE_NVR_H264_H
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include <re2/stringpiece.h>
|
||||
|
||||
#include "common.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
namespace internal {
|
||||
|
||||
using NalUnitFunction =
|
||||
std::function<IterationControl(re2::StringPiece nal_unit)>;
|
||||
|
||||
// Decode a H.264 Annex B byte stream into NAL units.
|
||||
// For GetH264SampleEntry; exposed for testing.
|
||||
// Calls |process_nal_unit| for each NAL unit in the byte stream.
|
||||
//
|
||||
// Note: this won't spot all invalid byte streams. For example, several 0x00s
|
||||
// not followed by a 0x01 will just be considered part of a NAL unit rather
|
||||
// than proof of an invalid stream.
|
||||
bool DecodeH264AnnexB(re2::StringPiece data, NalUnitFunction process_nal_unit,
|
||||
std::string *error_message);
|
||||
|
||||
} // namespace
|
||||
|
||||
// Gets a H.264 sample entry (AVCSampleEntry, which extends
|
||||
// VisualSampleEntry), given the "extradata", width, and height supplied by
|
||||
// ffmpeg.
|
||||
bool ParseExtraData(re2::StringPiece extradata, uint16_t width, uint16_t height,
|
||||
std::string *sample_entry, bool *need_transform,
|
||||
std::string *error_message);
|
||||
|
||||
bool TransformSampleData(re2::StringPiece annexb_sample,
|
||||
std::string *avc_sample, std::string *error_message);
|
||||
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
#endif // MOONFIRE_NVR_H264_H
|
349
src/h264.rs
Normal file
349
src/h264.rs
Normal file
@ -0,0 +1,349 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! H.264 decoding
|
||||
//!
|
||||
//! For the most part, Moonfire NVR does not try to understand the video codec. However, H.264 has
|
||||
//! two byte stream encodings: ISO/IEC 14496-10 Annex B, and ISO/IEC 14496-15 AVC access units.
|
||||
//! When streaming from RTSP, ffmpeg supplies the former. We need the latter to stick into `.mp4`
|
||||
//! files. This file manages the conversion, both for the ffmpeg "extra data" (which should become
|
||||
//! the ISO/IEC 14496-15 section 5.2.4.1 `AVCDecoderConfigurationRecord`) and the actual samples.
|
||||
//!
|
||||
//! ffmpeg of course has logic to do the same thing, but unfortunately it is not exposed except
|
||||
//! through ffmpeg's own generated `.mp4` file. Extracting just this part of their `.mp4` files
|
||||
//! would be more trouble than it's worth.
|
||||
|
||||
use byteorder::{BigEndian, WriteBytesExt};
|
||||
use error::{Error, Result};
|
||||
use regex::bytes::Regex;
|
||||
|
||||
// See ISO/IEC 14496-10 table 7-1 - NAL unit type codes, syntax element categories, and NAL unit
|
||||
// type classes.
|
||||
const NAL_UNIT_SEQ_PARAMETER_SET: u8 = 7;
|
||||
const NAL_UNIT_PIC_PARAMETER_SET: u8 = 8;
|
||||
|
||||
const NAL_UNIT_TYPE_MASK: u8 = 0x1F; // bottom 5 bits of first byte of unit.
|
||||
|
||||
/// Decodes a H.264 Annex B byte stream into NAL units. Calls `f` for each NAL unit in the byte
|
||||
/// stream. Aborts if `f` returns error.
|
||||
///
|
||||
/// See ISO/IEC 14496-10 section B.2: Byte stream NAL unit decoding process.
|
||||
/// This is a relatively simple, unoptimized implementation.
|
||||
///
|
||||
/// TODO: detect invalid byte streams. For example, several 0x00s not followed by a 0x01, a stream
|
||||
/// stream not starting with 0x00 0x00 0x00 0x01, or an empty NAL unit.
|
||||
fn decode_h264_annex_b<'a, F>(data: &'a [u8], mut f: F) -> Result<()>
|
||||
where F: FnMut(&'a [u8]) -> Result<()> {
|
||||
lazy_static! {
|
||||
static ref START_CODE: Regex = Regex::new(r"(\x00{2,}\x01)").unwrap();
|
||||
}
|
||||
for unit in START_CODE.split(data) {
|
||||
if !unit.is_empty() {
|
||||
f(unit)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parses Annex B extra data, returning a tuple holding the `sps` and `pps` substrings.
|
||||
fn parse_annex_b_extra_data(data: &[u8]) -> Result<(&[u8], &[u8])> {
|
||||
let mut sps = None;
|
||||
let mut pps = None;
|
||||
decode_h264_annex_b(data, |unit| {
|
||||
let nal_type = (unit[0] as u8) & NAL_UNIT_TYPE_MASK;
|
||||
match nal_type {
|
||||
NAL_UNIT_SEQ_PARAMETER_SET => { sps = Some(unit); },
|
||||
NAL_UNIT_PIC_PARAMETER_SET => { pps = Some(unit); },
|
||||
_ => { return Err(Error::new(format!("Expected SPS and PPS; got type {}", nal_type))); }
|
||||
};
|
||||
Ok(())
|
||||
})?;
|
||||
match (sps, pps) {
|
||||
(Some(s), Some(p)) => Ok((s, p)),
|
||||
_ => Err(Error::new("SPS and PPS must be specified".to_owned())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed representation of ffmpeg's "extradata".
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct ExtraData {
|
||||
pub sample_entry: Vec<u8>,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
|
||||
/// True iff sample data should be transformed from Annex B format to AVC format via a call to
|
||||
/// `transform_sample_data`. (The assumption is that if the extra data was in Annex B format,
|
||||
/// the sample data is also.)
|
||||
pub need_transform: bool,
|
||||
}
|
||||
|
||||
impl ExtraData {
|
||||
/// Parses "extradata" from ffmpeg. This data may be in either Annex B format or AVC format.
|
||||
pub fn parse(extradata: &[u8], width: u16, height: u16) -> Result<ExtraData> {
|
||||
let mut sps_and_pps = None;
|
||||
let need_transform;
|
||||
let avcc_len = if extradata.starts_with(b"\x00\x00\x00\x01") ||
|
||||
extradata.starts_with(b"\x00\x00\x01") {
|
||||
// ffmpeg supplied "extradata" in Annex B format.
|
||||
let (s, p) = parse_annex_b_extra_data(extradata)?;
|
||||
sps_and_pps = Some((s, p));
|
||||
need_transform = true;
|
||||
|
||||
// This magic value is checked at the end of the function;
|
||||
// unit tests confirm its accuracy.
|
||||
19 + s.len() + p.len()
|
||||
} else {
|
||||
// Assume "extradata" holds an AVCDecoderConfiguration.
|
||||
need_transform = false;
|
||||
8 + extradata.len()
|
||||
};
|
||||
let sps_and_pps = sps_and_pps;
|
||||
let need_transform = need_transform;
|
||||
|
||||
// This magic value is also checked at the end.
|
||||
let avc1_len = 86 + avcc_len;
|
||||
|
||||
let mut sample_entry = Vec::with_capacity(avc1_len);
|
||||
|
||||
// This is a concatenation of the following boxes/classes.
|
||||
|
||||
// SampleEntry, ISO/IEC 14496-10 section 8.5.2.
|
||||
let avc1_len_pos = sample_entry.len();
|
||||
sample_entry.write_u32::<BigEndian>(avc1_len as u32)?; // length
|
||||
// type + reserved + data_reference_index = 1
|
||||
sample_entry.extend_from_slice(b"avc1\x00\x00\x00\x00\x00\x00\x00\x01");
|
||||
|
||||
// VisualSampleEntry, ISO/IEC 14496-12 section 12.1.3.
|
||||
sample_entry.extend_from_slice(&[0; 16]); // pre-defined + reserved
|
||||
sample_entry.write_u16::<BigEndian>(width)?;
|
||||
sample_entry.write_u16::<BigEndian>(height)?;
|
||||
sample_entry.extend_from_slice(&[
|
||||
0x00, 0x48, 0x00, 0x00, // horizresolution
|
||||
0x00, 0x48, 0x00, 0x00, // vertresolution
|
||||
0x00, 0x00, 0x00, 0x00, // reserved
|
||||
0x00, 0x01, // frame count
|
||||
0x00, 0x00, 0x00, 0x00, // compressorname
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x18, 0xff, 0xff, // depth + pre_defined
|
||||
]);
|
||||
|
||||
// AVCSampleEntry, ISO/IEC 14496-15 section 5.3.4.1.
|
||||
// AVCConfigurationBox, ISO/IEC 14496-15 section 5.3.4.1.
|
||||
let avcc_len_pos = sample_entry.len();
|
||||
sample_entry.write_u32::<BigEndian>(avcc_len as u32)?; // length
|
||||
sample_entry.extend_from_slice(b"avcC");
|
||||
|
||||
let avc_decoder_config_len = if let Some((sps, pps)) = sps_and_pps {
|
||||
let before = sample_entry.len();
|
||||
|
||||
// Create the AVCDecoderConfiguration, ISO/IEC 14496-15 section 5.2.4.1.
|
||||
// The beginning of the AVCDecoderConfiguration takes a few values from
|
||||
// the SPS (ISO/IEC 14496-10 section 7.3.2.1.1). One caveat: that section
|
||||
// defines the syntax in terms of RBSP, not NAL. The difference is the
|
||||
// escaping of 00 00 01 and 00 00 02; see notes about
|
||||
// "emulation_prevention_three_byte" in ISO/IEC 14496-10 section 7.4.
|
||||
// It looks like 00 is not a valid value of profile_idc, so this distinction
|
||||
// shouldn't be relevant here. And ffmpeg seems to ignore it.
|
||||
sample_entry.push(1); // configurationVersion
|
||||
sample_entry.push(sps[1]); // profile_idc . AVCProfileIndication
|
||||
sample_entry.push(sps[2]); // ...misc bits... . profile_compatibility
|
||||
sample_entry.push(sps[3]); // level_idc . AVCLevelIndication
|
||||
|
||||
// Hardcode lengthSizeMinusOne to 3, matching TransformSampleData's 4-byte
|
||||
// lengths.
|
||||
sample_entry.push(0xff);
|
||||
|
||||
// Only support one SPS and PPS.
|
||||
// ffmpeg's ff_isom_write_avcc has the same limitation, so it's probably
|
||||
// fine. This next byte is a reserved 0b111 + a 5-bit # of SPSs (1).
|
||||
sample_entry.push(0xe1);
|
||||
sample_entry.write_u16::<BigEndian>(sps.len() as u16)?;
|
||||
sample_entry.extend_from_slice(sps);
|
||||
sample_entry.push(1); // # of PPSs.
|
||||
sample_entry.write_u16::<BigEndian>(pps.len() as u16)?;
|
||||
sample_entry.extend_from_slice(pps);
|
||||
|
||||
if sample_entry.len() - avcc_len_pos != avcc_len {
|
||||
return Err(Error::new(format!("internal error: anticipated AVCConfigurationBox \
|
||||
length {}, but was actually {}; sps length \
|
||||
{}, pps length {}",
|
||||
avcc_len, sample_entry.len() - avcc_len_pos,
|
||||
sps.len(), pps.len())));
|
||||
}
|
||||
sample_entry.len() - before
|
||||
} else {
|
||||
sample_entry.extend_from_slice(extradata);
|
||||
extradata.len()
|
||||
};
|
||||
|
||||
if sample_entry.len() - avc1_len_pos != avc1_len {
|
||||
return Err(Error::new(format!("internal error: anticipated AVCSampleEntry length \
|
||||
{}, but was actually {}; AVCDecoderConfiguration \
|
||||
length {}", avc1_len, sample_entry.len() - avc1_len_pos,
|
||||
avc_decoder_config_len)));
|
||||
}
|
||||
Ok(ExtraData{
|
||||
sample_entry: sample_entry,
|
||||
width: width,
|
||||
height: height,
|
||||
need_transform: need_transform,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Transforms sample data from Annex B format to AVC format. Should be called on samples iff
|
||||
/// `ExtraData::need_transform` is true. Uses an out parameter `avc_sample` rather than a return
|
||||
/// so that memory allocations can be reused from sample to sample.
|
||||
pub fn transform_sample_data(annexb_sample: &[u8], avc_sample: &mut Vec<u8>) -> Result<()> {
|
||||
// See AVCParameterSamples, ISO/IEC 14496-15 section 5.3.2.
|
||||
avc_sample.clear();
|
||||
|
||||
// The output will be about as long as the input. Annex B stop codes require at least three
|
||||
// bytes; many seem to be four. The output lengths are exactly four.
|
||||
avc_sample.reserve(annexb_sample.len() + 4);
|
||||
decode_h264_annex_b(annexb_sample, |unit| {
|
||||
// 4-byte length; this must match ParseExtraData's lengthSizeMinusOne == 3.
|
||||
avc_sample.write_u32::<BigEndian>(unit.len() as u32)?; // length
|
||||
avc_sample.extend_from_slice(unit);
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
const ANNEX_B_TEST_INPUT: [u8; 35] = [
|
||||
0x00, 0x00, 0x00, 0x01, 0x67, 0x4d, 0x00, 0x1f,
|
||||
0x9a, 0x66, 0x02, 0x80, 0x2d, 0xff, 0x35, 0x01,
|
||||
0x01, 0x01, 0x40, 0x00, 0x00, 0xfa, 0x00, 0x00,
|
||||
0x1d, 0x4c, 0x01, 0x00, 0x00, 0x00, 0x01, 0x68,
|
||||
0xee, 0x3c, 0x80,
|
||||
];
|
||||
|
||||
const AVC_DECODER_CONFIG_TEST_INPUT: [u8; 38] = [
|
||||
0x01, 0x4d, 0x00, 0x1f, 0xff, 0xe1, 0x00, 0x17,
|
||||
0x67, 0x4d, 0x00, 0x1f, 0x9a, 0x66, 0x02, 0x80,
|
||||
0x2d, 0xff, 0x35, 0x01, 0x01, 0x01, 0x40, 0x00,
|
||||
0x00, 0xfa, 0x00, 0x00, 0x1d, 0x4c, 0x01, 0x01,
|
||||
0x00, 0x04, 0x68, 0xee, 0x3c, 0x80,
|
||||
];
|
||||
|
||||
const TEST_OUTPUT: [u8; 132] = [
|
||||
0x00, 0x00, 0x00, 0x84, 0x61, 0x76, 0x63, 0x31,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x05, 0x00, 0x02, 0xd0, 0x00, 0x48, 0x00, 0x00,
|
||||
0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x18, 0xff, 0xff, 0x00, 0x00,
|
||||
0x00, 0x2e, 0x61, 0x76, 0x63, 0x43, 0x01, 0x4d,
|
||||
0x00, 0x1f, 0xff, 0xe1, 0x00, 0x17, 0x67, 0x4d,
|
||||
0x00, 0x1f, 0x9a, 0x66, 0x02, 0x80, 0x2d, 0xff,
|
||||
0x35, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0xfa,
|
||||
0x00, 0x00, 0x1d, 0x4c, 0x01, 0x01, 0x00, 0x04,
|
||||
0x68, 0xee, 0x3c, 0x80,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn test_decode() {
|
||||
let data = &ANNEX_B_TEST_INPUT;
|
||||
let mut pieces = Vec::new();
|
||||
super::decode_h264_annex_b(data, |p| {
|
||||
pieces.push(p);
|
||||
Ok(())
|
||||
}).unwrap();
|
||||
assert_eq!(&pieces, &[&data[4 .. 27], &data[31 ..]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_entry_from_avc_decoder_config() {
|
||||
let e = super::ExtraData::parse(&AVC_DECODER_CONFIG_TEST_INPUT, 1280, 720).unwrap();
|
||||
assert_eq!(&e.sample_entry[..], &TEST_OUTPUT[..]);
|
||||
assert_eq!(e.width, 1280);
|
||||
assert_eq!(e.height, 720);
|
||||
assert_eq!(e.need_transform, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_entry_from_annex_b() {
|
||||
let e = super::ExtraData::parse(&ANNEX_B_TEST_INPUT, 1280, 720).unwrap();
|
||||
assert_eq!(e.width, 1280);
|
||||
assert_eq!(e.height, 720);
|
||||
assert_eq!(e.need_transform, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_sample_data() {
|
||||
const INPUT: [u8; 64] = [
|
||||
0x00, 0x00, 0x00, 0x01, 0x67, 0x4d, 0x00, 0x1f,
|
||||
0x9a, 0x66, 0x02, 0x80, 0x2d, 0xff, 0x35, 0x01,
|
||||
0x01, 0x01, 0x40, 0x00, 0x00, 0xfa, 0x00, 0x00,
|
||||
0x1d, 0x4c, 0x01,
|
||||
|
||||
0x00, 0x00, 0x00, 0x01, 0x68, 0xee, 0x3c, 0x80,
|
||||
|
||||
0x00, 0x00, 0x00, 0x01, 0x06, 0x06, 0x01, 0xc4,
|
||||
0x80,
|
||||
|
||||
0x00, 0x00, 0x00, 0x01, 0x65, 0x88, 0x80, 0x10,
|
||||
0x00, 0x08, 0x7f, 0x00, 0x5d, 0x27, 0xb5, 0xc1,
|
||||
0xff, 0x8c, 0xd6, 0x35,
|
||||
// (truncated)
|
||||
];
|
||||
const EXPECTED_OUTPUT: [u8; 64] = [
|
||||
0x00, 0x00, 0x00, 0x17, 0x67, 0x4d, 0x00, 0x1f,
|
||||
0x9a, 0x66, 0x02, 0x80, 0x2d, 0xff, 0x35, 0x01,
|
||||
0x01, 0x01, 0x40, 0x00, 0x00, 0xfa, 0x00, 0x00,
|
||||
0x1d, 0x4c, 0x01,
|
||||
|
||||
0x00, 0x00, 0x00, 0x04, 0x68, 0xee, 0x3c, 0x80,
|
||||
|
||||
0x00, 0x00, 0x00, 0x05, 0x06, 0x06, 0x01, 0xc4,
|
||||
0x80,
|
||||
|
||||
0x00, 0x00, 0x00, 0x10, 0x65, 0x88, 0x80, 0x10,
|
||||
0x00, 0x08, 0x7f, 0x00, 0x5d, 0x27, 0xb5, 0xc1,
|
||||
0xff, 0x8c, 0xd6, 0x35,
|
||||
];
|
||||
let mut out = Vec::new();
|
||||
super::transform_sample_data(&INPUT, &mut out).unwrap();
|
||||
assert_eq!(&out[..], &EXPECTED_OUTPUT[..]);
|
||||
}
|
||||
}
|
295
src/http-test.cc
295
src/http-test.cc
@ -1,295 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// util_test.cc: tests of the util.h interface.
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
#include <glog/logging.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "http.h"
|
||||
#include "string.h"
|
||||
#include "testutil.h"
|
||||
|
||||
DECLARE_bool(alsologtostderr);
|
||||
|
||||
using moonfire_nvr::internal::ParseRangeHeader;
|
||||
using moonfire_nvr::internal::RangeHeaderType;
|
||||
|
||||
using testing::_;
|
||||
using testing::AnyNumber;
|
||||
using testing::DoAll;
|
||||
using testing::Return;
|
||||
using testing::SetArgPointee;
|
||||
|
||||
namespace moonfire_nvr {
|
||||
namespace {
|
||||
|
||||
class MockFileSlice : public FileSlice {
|
||||
public:
|
||||
MOCK_CONST_METHOD0(size, int64_t());
|
||||
MOCK_CONST_METHOD3(AddRange, int64_t(ByteRange, EvBuffer *, std::string *));
|
||||
};
|
||||
|
||||
TEST(EvBufferTest, AddFileTest) {
|
||||
std::string dir = PrepareTempDirOrDie("http");
|
||||
std::string foo_filename = StrCat(dir, "/foo");
|
||||
WriteFileOrDie(foo_filename, "foo");
|
||||
|
||||
int in_fd = open(foo_filename.c_str(), O_RDONLY);
|
||||
PCHECK(in_fd >= 0) << "open: " << foo_filename;
|
||||
std::string error_message;
|
||||
|
||||
// Ensure adding the whole file succeeds.
|
||||
EvBuffer buf1;
|
||||
ASSERT_TRUE(buf1.AddFile(in_fd, 0, 3, &error_message)) << error_message;
|
||||
in_fd = -1;
|
||||
EXPECT_EQ(3u, evbuffer_get_length(buf1.get()));
|
||||
|
||||
// Ensure adding an empty region succeeds.
|
||||
EvBuffer buf2;
|
||||
ASSERT_TRUE(buf2.AddFile(in_fd, 0, 0, &error_message)) << error_message;
|
||||
EXPECT_EQ(0u, evbuffer_get_length(buf2.get()));
|
||||
|
||||
// Ensure adding part of a file after another string succeeds.
|
||||
in_fd = open(foo_filename.c_str(), O_RDONLY);
|
||||
EvBuffer buf3;
|
||||
buf3.Add("1234");
|
||||
ASSERT_TRUE(buf3.AddFile(in_fd, 1, 2, &error_message)) << error_message;
|
||||
auto size3 = evbuffer_get_length(buf3.get());
|
||||
EXPECT_EQ(6u, size3);
|
||||
std::string buf3_contents = std::string(
|
||||
reinterpret_cast<const char *>(evbuffer_pullup(buf3.get(), size3)),
|
||||
size3);
|
||||
EXPECT_EQ("1234oo", buf3_contents);
|
||||
}
|
||||
|
||||
class FileSlicesTest : public testing::Test {
|
||||
protected:
|
||||
void Init(int flags) {
|
||||
EXPECT_CALL(a_, size()).Times(AnyNumber()).WillRepeatedly(Return(5));
|
||||
EXPECT_CALL(b_, size()).Times(AnyNumber()).WillRepeatedly(Return(13));
|
||||
EXPECT_CALL(c_, size()).Times(AnyNumber()).WillRepeatedly(Return(7));
|
||||
EXPECT_CALL(d_, size()).Times(AnyNumber()).WillRepeatedly(Return(17));
|
||||
EXPECT_CALL(e_, size()).Times(AnyNumber()).WillRepeatedly(Return(19));
|
||||
|
||||
slices_.Append(&a_, flags);
|
||||
slices_.Append(&b_, flags);
|
||||
slices_.Append(&c_, flags);
|
||||
slices_.Append(&d_, flags);
|
||||
slices_.Append(&e_, flags);
|
||||
}
|
||||
|
||||
FileSlices slices_;
|
||||
testing::StrictMock<MockFileSlice> a_;
|
||||
testing::StrictMock<MockFileSlice> b_;
|
||||
testing::StrictMock<MockFileSlice> c_;
|
||||
testing::StrictMock<MockFileSlice> d_;
|
||||
testing::StrictMock<MockFileSlice> e_;
|
||||
};
|
||||
|
||||
TEST_F(FileSlicesTest, Size) {
|
||||
Init(0);
|
||||
EXPECT_EQ(5 + 13 + 7 + 17 + 19, slices_.size());
|
||||
}
|
||||
|
||||
TEST_F(FileSlicesTest, ExactSlice) {
|
||||
// Exactly slice b.
|
||||
Init(0);
|
||||
std::string error_message;
|
||||
EXPECT_CALL(b_, AddRange(ByteRange(0, 13), _, _)).WillOnce(Return(13));
|
||||
EXPECT_EQ(13, slices_.AddRange(ByteRange(5, 18), nullptr, &error_message))
|
||||
<< error_message;
|
||||
}
|
||||
|
||||
TEST_F(FileSlicesTest, Offset) {
|
||||
// Part of slice b, all of slice c, and part of slice d.
|
||||
Init(0);
|
||||
std::string error_message;
|
||||
EXPECT_CALL(b_, AddRange(ByteRange(12, 13), _, _)).WillOnce(Return(1));
|
||||
EXPECT_CALL(c_, AddRange(ByteRange(0, 7), _, _)).WillOnce(Return(7));
|
||||
EXPECT_CALL(d_, AddRange(ByteRange(0, 1), _, _)).WillOnce(Return(1));
|
||||
EXPECT_EQ(9, slices_.AddRange(ByteRange(17, 26), nullptr, &error_message))
|
||||
<< error_message;
|
||||
}
|
||||
|
||||
TEST_F(FileSlicesTest, Everything) {
|
||||
Init(0);
|
||||
std::string error_message;
|
||||
EXPECT_CALL(a_, AddRange(ByteRange(0, 5), _, _)).WillOnce(Return(5));
|
||||
EXPECT_CALL(b_, AddRange(ByteRange(0, 13), _, _)).WillOnce(Return(13));
|
||||
EXPECT_CALL(c_, AddRange(ByteRange(0, 7), _, _)).WillOnce(Return(7));
|
||||
EXPECT_CALL(d_, AddRange(ByteRange(0, 17), _, _)).WillOnce(Return(17));
|
||||
EXPECT_CALL(e_, AddRange(ByteRange(0, 19), _, _)).WillOnce(Return(19));
|
||||
EXPECT_EQ(61, slices_.AddRange(ByteRange(0, 61), nullptr, &error_message))
|
||||
<< error_message;
|
||||
}
|
||||
|
||||
TEST_F(FileSlicesTest, Lazy) {
|
||||
Init(FileSlices::kLazy);
|
||||
std::string error_message;
|
||||
EXPECT_CALL(a_, AddRange(ByteRange(0, 5), _, _)).WillOnce(Return(5));
|
||||
EXPECT_EQ(5, slices_.AddRange(ByteRange(0, 61), nullptr, &error_message))
|
||||
<< error_message;
|
||||
EXPECT_CALL(b_, AddRange(ByteRange(0, 13), _, _)).WillOnce(Return(13));
|
||||
EXPECT_EQ(13, slices_.AddRange(ByteRange(5, 61), nullptr, &error_message))
|
||||
<< error_message;
|
||||
EXPECT_CALL(c_, AddRange(ByteRange(0, 7), _, _)).WillOnce(Return(7));
|
||||
EXPECT_EQ(7, slices_.AddRange(ByteRange(18, 61), nullptr, &error_message))
|
||||
<< error_message;
|
||||
EXPECT_CALL(d_, AddRange(ByteRange(0, 17), _, _)).WillOnce(Return(17));
|
||||
EXPECT_EQ(17, slices_.AddRange(ByteRange(25, 61), nullptr, &error_message))
|
||||
<< error_message;
|
||||
EXPECT_CALL(e_, AddRange(ByteRange(0, 19), _, _)).WillOnce(Return(19));
|
||||
EXPECT_EQ(19, slices_.AddRange(ByteRange(42, 61), nullptr, &error_message))
|
||||
<< error_message;
|
||||
}
|
||||
|
||||
TEST_F(FileSlicesTest, SliceWithPartialReturn) {
|
||||
Init(0);
|
||||
std::string error_message;
|
||||
EXPECT_CALL(a_, AddRange(ByteRange(0, 5), _, _)).WillOnce(Return(5));
|
||||
EXPECT_CALL(b_, AddRange(ByteRange(0, 13), _, _)).WillOnce(Return(1));
|
||||
EXPECT_EQ(6, slices_.AddRange(ByteRange(0, 61), nullptr, &error_message))
|
||||
<< error_message;
|
||||
}
|
||||
|
||||
TEST_F(FileSlicesTest, PropagateError) {
|
||||
Init(0);
|
||||
std::string error_message;
|
||||
EXPECT_CALL(a_, AddRange(ByteRange(0, 5), _, _)).WillOnce(Return(5));
|
||||
EXPECT_CALL(b_, AddRange(ByteRange(0, 13), _, _))
|
||||
.WillRepeatedly(DoAll(SetArgPointee<2>("asdf"), Return(-1)));
|
||||
EXPECT_EQ(5, slices_.AddRange(ByteRange(0, 61), nullptr, &error_message));
|
||||
EXPECT_EQ(-1, slices_.AddRange(ByteRange(5, 61), nullptr, &error_message));
|
||||
EXPECT_EQ("asdf", error_message);
|
||||
}
|
||||
|
||||
// Test the specific examples enumerated in RFC 2616 section 14.35.1.
|
||||
TEST(RangeHeaderTest, Rfc_2616_Section_14_35_1) {
|
||||
std::vector<ByteRange> ranges;
|
||||
EXPECT_EQ(RangeHeaderType::kSatisfiable,
|
||||
ParseRangeHeader("bytes=0-499", 10000, &ranges));
|
||||
EXPECT_THAT(ranges, testing::ElementsAre(ByteRange(0, 500)));
|
||||
|
||||
EXPECT_EQ(RangeHeaderType::kSatisfiable,
|
||||
ParseRangeHeader("bytes=500-999", 10000, &ranges));
|
||||
EXPECT_THAT(ranges, testing::ElementsAre(ByteRange(500, 1000)));
|
||||
|
||||
EXPECT_EQ(RangeHeaderType::kSatisfiable,
|
||||
ParseRangeHeader("bytes=-500", 10000, &ranges));
|
||||
EXPECT_THAT(ranges, testing::ElementsAre(ByteRange(9500, 10000)));
|
||||
|
||||
EXPECT_EQ(RangeHeaderType::kSatisfiable,
|
||||
ParseRangeHeader("bytes=9500-", 10000, &ranges));
|
||||
EXPECT_THAT(ranges, testing::ElementsAre(ByteRange(9500, 10000)));
|
||||
|
||||
EXPECT_EQ(RangeHeaderType::kSatisfiable,
|
||||
ParseRangeHeader("bytes=0-0,-1", 10000, &ranges));
|
||||
EXPECT_THAT(ranges,
|
||||
testing::ElementsAre(ByteRange(0, 1), ByteRange(9999, 10000)));
|
||||
|
||||
// Non-canonical ranges. Possibly the point of these is that the adjacent
|
||||
// and overlapping ranges are supposed to be coalesced into one? I'm not
|
||||
// going to do that for now...just trying to get something working...
|
||||
EXPECT_EQ(RangeHeaderType::kSatisfiable,
|
||||
ParseRangeHeader("bytes=500-600,601-999", 10000, &ranges));
|
||||
EXPECT_THAT(ranges,
|
||||
testing::ElementsAre(ByteRange(500, 601), ByteRange(601, 1000)));
|
||||
EXPECT_EQ(RangeHeaderType::kSatisfiable,
|
||||
ParseRangeHeader("bytes=500-700,601-999", 10000, &ranges));
|
||||
EXPECT_THAT(ranges,
|
||||
testing::ElementsAre(ByteRange(500, 701), ByteRange(601, 1000)));
|
||||
}
|
||||
|
||||
TEST(RangeHeaderTest, Satisfiability) {
|
||||
std::vector<ByteRange> ranges;
|
||||
EXPECT_EQ(RangeHeaderType::kNotSatisfiable,
|
||||
ParseRangeHeader("bytes=10000-", 10000, &ranges));
|
||||
EXPECT_EQ(RangeHeaderType::kSatisfiable,
|
||||
ParseRangeHeader("bytes=0-499,10000-", 10000, &ranges));
|
||||
EXPECT_THAT(ranges, testing::ElementsAre(ByteRange(0, 500)));
|
||||
EXPECT_EQ(RangeHeaderType::kNotSatisfiable,
|
||||
ParseRangeHeader("bytes=-1", 0, &ranges));
|
||||
EXPECT_EQ(RangeHeaderType::kNotSatisfiable,
|
||||
ParseRangeHeader("bytes=0-0", 0, &ranges));
|
||||
EXPECT_EQ(RangeHeaderType::kNotSatisfiable,
|
||||
ParseRangeHeader("bytes=0-", 0, &ranges));
|
||||
EXPECT_EQ(RangeHeaderType::kSatisfiable,
|
||||
ParseRangeHeader("bytes=0-0", 1, &ranges));
|
||||
EXPECT_THAT(ranges, testing::ElementsAre(ByteRange(0, 1)));
|
||||
EXPECT_EQ(RangeHeaderType::kSatisfiable,
|
||||
ParseRangeHeader("bytes=0-", 1, &ranges));
|
||||
EXPECT_THAT(ranges, testing::ElementsAre(ByteRange(0, 1)));
|
||||
}
|
||||
|
||||
TEST(RangeHeaderTest, AbsentOrInvalid) {
|
||||
std::vector<ByteRange> ranges;
|
||||
EXPECT_EQ(RangeHeaderType::kAbsentOrInvalid,
|
||||
ParseRangeHeader(nullptr, 10000, &ranges));
|
||||
EXPECT_EQ(RangeHeaderType::kAbsentOrInvalid,
|
||||
ParseRangeHeader("", 10000, &ranges));
|
||||
EXPECT_EQ(RangeHeaderType::kAbsentOrInvalid,
|
||||
ParseRangeHeader("foo=0-499", 10000, &ranges));
|
||||
EXPECT_EQ(RangeHeaderType::kAbsentOrInvalid,
|
||||
ParseRangeHeader("foo=0-499", 10000, &ranges));
|
||||
EXPECT_EQ(RangeHeaderType::kAbsentOrInvalid,
|
||||
ParseRangeHeader("bytes=499-0", 10000, &ranges));
|
||||
EXPECT_EQ(RangeHeaderType::kAbsentOrInvalid,
|
||||
ParseRangeHeader("bytes=", 10000, &ranges));
|
||||
EXPECT_EQ(RangeHeaderType::kAbsentOrInvalid,
|
||||
ParseRangeHeader("bytes=,", 10000, &ranges));
|
||||
EXPECT_EQ(RangeHeaderType::kAbsentOrInvalid,
|
||||
ParseRangeHeader("bytes=-", 10000, &ranges));
|
||||
}
|
||||
|
||||
// TODO: test HttpServe itself!
|
||||
// Currently the testing is manual. Three important cases:
|
||||
// * HTTP request succeeds
|
||||
// * client aborts (as in hitting ctrl-C in curl during a long request)
|
||||
// * the VirtualFile returns error (say, by chmod u-r on the file backing
|
||||
// a RealFileSlice)
|
||||
|
||||
} // namespace
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
FLAGS_alsologtostderr = true;
|
||||
google::ParseCommandLineFlags(&argc, &argv, true);
|
||||
testing::InitGoogleTest(&argc, argv);
|
||||
google::InitGoogleLogging(argv[0]);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
414
src/http.cc
414
src/http.cc
@ -1,414 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// http.cc: See http.h.
|
||||
|
||||
#include "http.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/event.h>
|
||||
#include <event2/keyvalq_struct.h>
|
||||
#include <event2/http.h>
|
||||
#include <glog/logging.h>
|
||||
|
||||
#include "string.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
namespace {
|
||||
|
||||
// An HttpServe call still in progress.
|
||||
struct ServeInProgress {
|
||||
ByteRange left;
|
||||
int64_t sent_bytes = 0;
|
||||
std::shared_ptr<VirtualFile> file;
|
||||
evhttp_request *req = nullptr;
|
||||
};
|
||||
|
||||
void ServeCloseCallback(evhttp_connection *con, void *arg) {
|
||||
std::unique_ptr<ServeInProgress> serve(
|
||||
reinterpret_cast<ServeInProgress *>(arg));
|
||||
LOG(INFO) << serve->req << ": received client abort after sending "
|
||||
<< serve->sent_bytes << " bytes; there were " << serve->left.size()
|
||||
<< " bytes left.";
|
||||
|
||||
// The call to cancel will guarantee ServeChunkCallback is not called again.
|
||||
evhttp_cancel_request(serve->req);
|
||||
}
|
||||
|
||||
void ServeChunkCallback(evhttp_connection *con, void *arg) {
|
||||
std::unique_ptr<ServeInProgress> serve(
|
||||
reinterpret_cast<ServeInProgress *>(arg));
|
||||
|
||||
if (serve->left.size() == 0) {
|
||||
LOG(INFO) << serve->req << ": done; sent " << serve->sent_bytes
|
||||
<< " bytes.";
|
||||
evhttp_connection_set_closecb(con, nullptr, nullptr);
|
||||
evhttp_send_reply_end(serve->req);
|
||||
return;
|
||||
}
|
||||
|
||||
// Serve more data.
|
||||
EvBuffer buf;
|
||||
std::string error_message;
|
||||
int64_t added = serve->file->AddRange(serve->left, &buf, &error_message);
|
||||
if (added <= 0) {
|
||||
// Order is important here: evhttp_cancel_request immediately calls the
|
||||
// close callback, so remove it first to avoid double-freeing |serve|.
|
||||
evhttp_connection_set_closecb(con, nullptr, nullptr);
|
||||
evhttp_cancel_request(serve->req);
|
||||
LOG(ERROR) << serve->req << ": Failed to serve request after sending "
|
||||
<< serve->sent_bytes << " bytes (" << serve->left.size()
|
||||
<< " bytes left): " << error_message;
|
||||
return;
|
||||
}
|
||||
|
||||
serve->sent_bytes += added;
|
||||
serve->left.begin += added;
|
||||
VLOG(1) << serve->req << ": sending " << added << " bytes (more) data; still "
|
||||
<< serve->left.size() << " bytes left";
|
||||
evhttp_send_reply_chunk_with_cb(serve->req, buf.get(), &ServeChunkCallback,
|
||||
serve.get());
|
||||
evhttp_send_reply_chunk(serve->req, buf.get());
|
||||
serve.release();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace internal {
|
||||
|
||||
RangeHeaderType ParseRangeHeader(const char *inptr, int64_t size,
|
||||
std::vector<ByteRange> *ranges) {
|
||||
if (inptr == nullptr) {
|
||||
return RangeHeaderType::kAbsentOrInvalid; // absent.
|
||||
}
|
||||
if (strncmp(inptr, "bytes=", strlen("bytes=")) != 0) {
|
||||
return RangeHeaderType::kAbsentOrInvalid; // invalid syntax.
|
||||
}
|
||||
inptr += strlen("bytes=");
|
||||
ranges->clear();
|
||||
int n_ranges = 0;
|
||||
while (*inptr != 0) { // have more byte-range-sets.
|
||||
++n_ranges;
|
||||
ByteRange r;
|
||||
|
||||
// Parse a number.
|
||||
const char *endptr;
|
||||
int64_t value;
|
||||
if (!strto64(inptr, 10, &endptr, &value)) {
|
||||
return RangeHeaderType::kAbsentOrInvalid; // invalid syntax.
|
||||
}
|
||||
|
||||
if (value < 0) { // just parsed suffix-byte-range-spec.
|
||||
r.begin = std::max(size + value, INT64_C(0));
|
||||
r.end = size;
|
||||
if (r.begin < r.end) { // satisfiable.
|
||||
ranges->emplace_back(std::move(r));
|
||||
}
|
||||
inptr = endptr;
|
||||
|
||||
} else { // just parsed start of byte-range-spec.
|
||||
if (*endptr != '-') {
|
||||
return RangeHeaderType::kAbsentOrInvalid;
|
||||
}
|
||||
r.begin = value;
|
||||
inptr = endptr + 1; // move past the '-'.
|
||||
if (*inptr == ',' || *inptr == 0) { // no end specified; use EOF.
|
||||
r.end = size;
|
||||
} else { // explicit range.
|
||||
if (!strto64(inptr, 10, &endptr, &value) || value < r.begin) {
|
||||
return RangeHeaderType::kAbsentOrInvalid; // invalid syntax.
|
||||
}
|
||||
inptr = endptr;
|
||||
r.end = std::min(size, value + 1); // note inclusive->exclusive.
|
||||
}
|
||||
if (r.begin < size) {
|
||||
ranges->emplace_back(std::move(r)); // satisfiable.
|
||||
}
|
||||
}
|
||||
|
||||
if (*inptr == ',') {
|
||||
inptr++;
|
||||
if (*inptr == 0) {
|
||||
return RangeHeaderType::kAbsentOrInvalid; // invalid syntax.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (n_ranges == 0) { // must be at least one range.
|
||||
return RangeHeaderType::kAbsentOrInvalid;
|
||||
}
|
||||
|
||||
return ranges->empty() ? RangeHeaderType::kNotSatisfiable
|
||||
: RangeHeaderType::kSatisfiable;
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
bool EvBuffer::AddFile(int fd, ev_off_t offset, ev_off_t length,
|
||||
std::string *error_message) {
|
||||
if (length == 0) {
|
||||
// evbuffer_add_file fails in this trivial case, at least when using mmap.
|
||||
// Just return true since there's nothing to be done.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (evbuffer_get_length(buf_) > 0) {
|
||||
// Work around https://github.com/libevent/libevent/issues/306 by using a
|
||||
// fresh buffer for evbuffer_add_file.
|
||||
EvBuffer fresh_buffer;
|
||||
if (!fresh_buffer.AddFile(fd, offset, length, error_message)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Crash if evbuffer_add_buffer fails, because the ownership of |fd| has
|
||||
// already been transferred, and it's too confusing to support some
|
||||
// failures in which the caller still owns |fd| and some in which it does
|
||||
// not.
|
||||
CHECK_EQ(0, evbuffer_add_buffer(buf_, fresh_buffer.buf_))
|
||||
<< "evbuffer_add_buffer failed: " << strerror(errno);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (evbuffer_add_file(buf_, fd, offset, length) != 0) {
|
||||
int err = errno;
|
||||
*error_message = StrCat("evbuffer_add_file failed with offset ", offset,
|
||||
", length ", length, ": ", strerror(err));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void RealFileSlice::Init(File *dir, re2::StringPiece filename,
|
||||
ByteRange range) {
|
||||
dir_ = dir;
|
||||
filename_ = filename.as_string();
|
||||
range_ = range;
|
||||
}
|
||||
|
||||
int64_t RealFileSlice::AddRange(ByteRange range, EvBuffer *buf,
|
||||
std::string *error_message) const {
|
||||
int fd;
|
||||
int ret = dir_->Open(filename_.c_str(), O_RDONLY, &fd);
|
||||
if (ret != 0) {
|
||||
*error_message = StrCat("open ", filename_, ": ", strerror(ret));
|
||||
return -1;
|
||||
}
|
||||
if (!buf->AddFile(fd, range_.begin + range.begin, range.size(),
|
||||
error_message)) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
// |buf| now owns |fd|.
|
||||
return range.size();
|
||||
}
|
||||
|
||||
int64_t FillerFileSlice::AddRange(ByteRange range, EvBuffer *buf,
|
||||
std::string *error_message) const {
|
||||
std::unique_ptr<std::string> s(new std::string);
|
||||
s->reserve(size_);
|
||||
if (!fn_(s.get(), error_message)) {
|
||||
return 0;
|
||||
}
|
||||
if (s->size() != size_) {
|
||||
*error_message = StrCat("Expected filled slice to be ", size_,
|
||||
" bytes; got ", s->size(), " bytes.");
|
||||
return 0;
|
||||
}
|
||||
std::string *unowned_s = s.release();
|
||||
buf->AddReference(unowned_s->data() + range.begin,
|
||||
range.size(), [](const void *, size_t, void *s) {
|
||||
delete reinterpret_cast<std::string *>(s);
|
||||
}, unowned_s);
|
||||
return range.size();
|
||||
}
|
||||
|
||||
int64_t StringPieceSlice::AddRange(ByteRange range, EvBuffer *buf,
|
||||
std::string *error_message) const {
|
||||
buf->AddReference(piece_.data() + range.begin, range.size(), nullptr,
|
||||
nullptr);
|
||||
return range.size();
|
||||
}
|
||||
|
||||
int64_t FileSlices::AddRange(ByteRange range, EvBuffer *buf,
|
||||
std::string *error_message) const {
|
||||
if (range.begin < 0 || range.begin > range.end || range.end > size_) {
|
||||
*error_message = StrCat("Range ", range.DebugString(),
|
||||
" not valid for file of size ", size_);
|
||||
return false;
|
||||
}
|
||||
int64_t total_bytes_added = 0;
|
||||
auto it = std::upper_bound(slices_.begin(), slices_.end(), range.begin,
|
||||
[](int64_t begin, const SliceInfo &info) {
|
||||
return begin < info.range.end;
|
||||
});
|
||||
for (; it != slices_.end() && range.end > it->range.begin; ++it) {
|
||||
if (total_bytes_added > 0 && (it->flags & kLazy) != 0) {
|
||||
VLOG(1) << "early return of " << total_bytes_added << "/" << range.size()
|
||||
<< " bytes from FileSlices " << this << " because slice "
|
||||
<< it->slice << " is lazy.";
|
||||
break;
|
||||
}
|
||||
ByteRange mapped(
|
||||
std::max(INT64_C(0), range.begin - it->range.begin),
|
||||
std::min(range.end - it->range.begin, it->range.end - it->range.begin));
|
||||
int64_t slice_bytes_added = it->slice->AddRange(mapped, buf, error_message);
|
||||
total_bytes_added += slice_bytes_added > 0 ? slice_bytes_added : 0;
|
||||
if (slice_bytes_added < 0 && total_bytes_added == 0) {
|
||||
LOG(WARNING) << "early return of " << total_bytes_added << "/"
|
||||
<< range.size() << " bytes from FileSlices " << this
|
||||
<< " due to slice " << it->slice
|
||||
<< " returning error: " << *error_message;
|
||||
return -1;
|
||||
} else if (slice_bytes_added < mapped.size()) {
|
||||
LOG(INFO) << "early return of " << total_bytes_added << "/"
|
||||
<< range.size() << " bytes from FileSlices " << this
|
||||
<< " due to slice " << it->slice << " returning "
|
||||
<< slice_bytes_added << "/" << mapped.size()
|
||||
<< " bytes. error_message (maybe populated): "
|
||||
<< *error_message;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return total_bytes_added;
|
||||
}
|
||||
|
||||
void HttpSendError(evhttp_request *req, int http_err, const std::string &prefix,
|
||||
int posix_err) {
|
||||
evhttp_send_error(req, http_err,
|
||||
EscapeHtml(prefix + strerror(posix_err)).c_str());
|
||||
}
|
||||
|
||||
void HttpServe(const std::shared_ptr<VirtualFile> &file, evhttp_request *req) {
|
||||
// We could support HEAD, but there's probably no need.
|
||||
if (evhttp_request_get_command(req) != EVHTTP_REQ_GET) {
|
||||
return evhttp_send_error(req, HTTP_BADMETHOD, "only GET allowed");
|
||||
}
|
||||
|
||||
const struct evkeyvalq *in_hdrs = evhttp_request_get_input_headers(req);
|
||||
struct evkeyvalq *out_hdrs = evhttp_request_get_output_headers(req);
|
||||
|
||||
// Construct a Last-Modified: header.
|
||||
time_t last_modified = file->last_modified();
|
||||
struct tm last_modified_tm;
|
||||
if (gmtime_r(&last_modified, &last_modified_tm) == 0) {
|
||||
return HttpSendError(req, HTTP_INTERNAL, "gmtime_r failed: ", errno);
|
||||
}
|
||||
char last_modified_str[50];
|
||||
if (strftime(last_modified_str, sizeof(last_modified_str),
|
||||
"%a, %d %b %Y %H:%M:%S GMT", &last_modified_tm) == 0) {
|
||||
return HttpSendError(req, HTTP_INTERNAL, "strftime failed: ", errno);
|
||||
}
|
||||
std::string etag = file->etag();
|
||||
|
||||
// Ignore the "Range:" header if "If-Range:" specifies an incorrect etag.
|
||||
const char *if_range = evhttp_find_header(in_hdrs, "If-Range");
|
||||
const char *range_hdr = evhttp_find_header(in_hdrs, "Range");
|
||||
if (if_range != nullptr && etag != if_range) {
|
||||
LOG(INFO) << req << ": Ignoring Range: because If-Range: is stale.";
|
||||
range_hdr = nullptr;
|
||||
}
|
||||
|
||||
EvBuffer buf;
|
||||
std::vector<ByteRange> ranges;
|
||||
auto range_type =
|
||||
internal::ParseRangeHeader(range_hdr, file->size(), &ranges);
|
||||
std::string error_message;
|
||||
int http_status;
|
||||
const char *http_status_str;
|
||||
ByteRange left;
|
||||
switch (range_type) {
|
||||
case internal::RangeHeaderType::kNotSatisfiable: {
|
||||
std::string range_hdr = StrCat("bytes */", file->size());
|
||||
evhttp_add_header(out_hdrs, "Content-Range", range_hdr.c_str());
|
||||
http_status = 416;
|
||||
http_status_str = "Range Not Satisfiable";
|
||||
LOG(INFO) << req
|
||||
<< ": Replying to non-satisfiable range request: " << range_hdr;
|
||||
break;
|
||||
}
|
||||
|
||||
case internal::RangeHeaderType::kSatisfiable:
|
||||
// We only support the simpler single-range case for now.
|
||||
// A multi-range request just serves the whole file via the fallthrough.
|
||||
if (ranges.size() == 1) {
|
||||
std::string range_hdr = StrCat("bytes ", ranges[0].begin, "-",
|
||||
ranges[0].end - 1, "/", file->size());
|
||||
left = ranges[0];
|
||||
evhttp_add_header(out_hdrs, "Content-Range", range_hdr.c_str());
|
||||
http_status = 206;
|
||||
http_status_str = "Partial Content";
|
||||
LOG(INFO) << req << ": URI " << evhttp_request_get_uri(req)
|
||||
<< ": client requested byte range " << left
|
||||
<< " (total file size " << file->size() << ")";
|
||||
break;
|
||||
}
|
||||
// FALLTHROUGH
|
||||
|
||||
case internal::RangeHeaderType::kAbsentOrInvalid: {
|
||||
left = ByteRange(0, file->size());
|
||||
LOG(INFO) << req << ": URI " << evhttp_request_get_uri(req)
|
||||
<< ": Client requested whole file of size " << file->size();
|
||||
http_status = HTTP_OK;
|
||||
http_status_str = "OK";
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
LOG(FATAL) << "unexpected range_type: " << static_cast<int>(range_type);
|
||||
}
|
||||
|
||||
// Successful reply started; add common headers and send.
|
||||
evhttp_add_header(out_hdrs, "Content-Length", StrCat(left.size()).c_str());
|
||||
evhttp_add_header(out_hdrs, "Content-Type", file->mime_type().c_str());
|
||||
evhttp_add_header(out_hdrs, "Accept-Ranges", "bytes");
|
||||
evhttp_add_header(out_hdrs, "Last-Modified", last_modified_str);
|
||||
evhttp_add_header(out_hdrs, "ETag", etag.c_str());
|
||||
evhttp_send_reply_start(req, http_status, http_status_str);
|
||||
|
||||
ServeInProgress *serve = new ServeInProgress;
|
||||
serve->file = file;
|
||||
serve->left = left;
|
||||
serve->req = req;
|
||||
evhttp_connection *con = evhttp_request_get_connection(req);
|
||||
evhttp_connection_set_closecb(con, &ServeCloseCallback, serve);
|
||||
return ServeChunkCallback(con, serve);
|
||||
}
|
||||
|
||||
} // namespace moonfire_nvr
|
306
src/http.h
306
src/http.h
@ -1,306 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// http.h: classes for HTTP serving. In particular, there are helpers for
|
||||
// serving HTTP byte range requests with libevent.
|
||||
|
||||
#ifndef MOONFIRE_NVR_HTTP_H
|
||||
#define MOONFIRE_NVR_HTTP_H
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdarg.h>
|
||||
#include <sys/queue.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/keyvalq_struct.h>
|
||||
#include <event2/http.h>
|
||||
#include <glog/logging.h>
|
||||
#include <re2/stringpiece.h>
|
||||
|
||||
#include "filesystem.h"
|
||||
#include "string.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
// Single-use object to represent a set of HTTP query parameters.
|
||||
class QueryParameters {
|
||||
public:
|
||||
// Parse parameters from the given URI.
|
||||
// Caller should check ok() afterward.
|
||||
QueryParameters(const char *uri) {
|
||||
TAILQ_INIT(&me_);
|
||||
ok_ = evhttp_parse_query(uri, &me_) == 0;
|
||||
}
|
||||
QueryParameters(const QueryParameters &) = delete;
|
||||
void operator=(const QueryParameters &) = delete;
|
||||
|
||||
~QueryParameters() { evhttp_clear_headers(&me_); }
|
||||
|
||||
bool ok() const { return ok_; }
|
||||
const char *Get(const char *param) const {
|
||||
return evhttp_find_header(&me_, param);
|
||||
}
|
||||
|
||||
private:
|
||||
struct evkeyvalq me_;
|
||||
bool ok_ = false;
|
||||
};
|
||||
|
||||
// Wrapped version of libevent's "struct evbuffer" which uses RAII and simply
|
||||
// aborts the process if allocations fail. (Moonfire NVR is intended to run on
|
||||
// Linux systems with the default vm.overcommit_memory=0, so there's probably
|
||||
// no point in trying to gracefully recover from a condition that's unlikely
|
||||
// to ever happen.)
|
||||
class EvBuffer {
|
||||
public:
|
||||
EvBuffer() { buf_ = CHECK_NOTNULL(evbuffer_new()); }
|
||||
EvBuffer(const EvBuffer &) = delete;
|
||||
EvBuffer &operator=(const EvBuffer &) = delete;
|
||||
~EvBuffer() { evbuffer_free(buf_); }
|
||||
|
||||
struct evbuffer *get() {
|
||||
return buf_;
|
||||
}
|
||||
|
||||
void Add(const re2::StringPiece &s) {
|
||||
CHECK_EQ(0, evbuffer_add(buf_, s.data(), s.size()));
|
||||
}
|
||||
|
||||
void AddPrintf(const char *fmt, ...) __attribute__((format(printf, 2, 3))) {
|
||||
va_list argp;
|
||||
va_start(argp, fmt);
|
||||
CHECK_LE(0, evbuffer_add_vprintf(buf_, fmt, argp));
|
||||
va_end(argp);
|
||||
}
|
||||
|
||||
// Delegates to evbuffer_add_file.
|
||||
// On success, |fd| will be closed by libevent. On failure, it remains open.
|
||||
bool AddFile(int fd, ev_off_t offset, ev_off_t length,
|
||||
std::string *error_message);
|
||||
|
||||
void AddReference(const void *data, size_t datlen,
|
||||
evbuffer_ref_cleanup_cb cleanupfn, void *cleanupfn_arg) {
|
||||
CHECK_EQ(
|
||||
0, evbuffer_add_reference(buf_, data, datlen, cleanupfn, cleanupfn_arg))
|
||||
<< strerror(errno);
|
||||
}
|
||||
|
||||
private:
|
||||
struct evbuffer *buf_;
|
||||
};
|
||||
|
||||
struct ByteRange {
|
||||
ByteRange() {}
|
||||
ByteRange(int64_t begin, int64_t end) : begin(begin), end(end) {}
|
||||
int64_t begin = 0;
|
||||
int64_t end = 0; // exclusive.
|
||||
int64_t size() const { return end - begin; }
|
||||
bool operator==(const ByteRange &o) const {
|
||||
return begin == o.begin && end == o.end;
|
||||
}
|
||||
std::string DebugString() const { return StrCat("[", begin, ", ", end, ")"); }
|
||||
};
|
||||
|
||||
inline std::ostream &operator<<(std::ostream &out, const ByteRange &range) {
|
||||
return out << range.DebugString();
|
||||
}
|
||||
|
||||
// Helper for sending HTTP errors based on POSIX error returns.
|
||||
void HttpSendError(evhttp_request *req, int http_err, const std::string &prefix,
|
||||
int posix_errno);
|
||||
|
||||
class FileSlice {
|
||||
public:
|
||||
virtual ~FileSlice() {}
|
||||
|
||||
virtual int64_t size() const = 0;
|
||||
|
||||
// Add some to all of the given non-empty |range| to |buf|.
|
||||
// Returns the number of bytes added, or < 0 on error.
|
||||
// On error, |error_message| should be populated. (|error_message| may also be
|
||||
// populated if 0 <= return value < range.size(), such as if one of a
|
||||
// FileSlices object's failed. However, it's safe to simply retry such
|
||||
// partial failures later.)
|
||||
virtual int64_t AddRange(ByteRange range, EvBuffer *buf,
|
||||
std::string *error_message) const = 0;
|
||||
};
|
||||
|
||||
class VirtualFile : public FileSlice {
|
||||
public:
|
||||
virtual ~VirtualFile() {}
|
||||
|
||||
// Return the given property of the file.
|
||||
virtual time_t last_modified() const = 0;
|
||||
virtual std::string etag() const = 0;
|
||||
virtual std::string mime_type() const = 0;
|
||||
};
|
||||
|
||||
class RealFileSlice : public FileSlice {
|
||||
public:
|
||||
// |dir| must outlive the RealFileSlice.
|
||||
void Init(File *dir, re2::StringPiece filename, ByteRange range);
|
||||
|
||||
int64_t size() const final { return range_.size(); }
|
||||
|
||||
int64_t AddRange(ByteRange range, EvBuffer *buf,
|
||||
std::string *error_message) const final;
|
||||
|
||||
private:
|
||||
File *dir_;
|
||||
std::string filename_;
|
||||
ByteRange range_;
|
||||
};
|
||||
|
||||
// A FileSlice of a pre-defined length which calls a function which fills the
|
||||
// slice on demand. The FillerFileSlice is responsible for subsetting.
|
||||
class FillerFileSlice : public FileSlice {
|
||||
public:
|
||||
using FillFunction =
|
||||
std::function<bool(std::string *slice, std::string *error_message)>;
|
||||
|
||||
void Init(size_t size, FillFunction fn) {
|
||||
fn_ = fn;
|
||||
size_ = size;
|
||||
}
|
||||
|
||||
int64_t size() const final { return size_; }
|
||||
|
||||
int64_t AddRange(ByteRange range, EvBuffer *buf,
|
||||
std::string *error_message) const final;
|
||||
|
||||
private:
|
||||
FillFunction fn_;
|
||||
size_t size_;
|
||||
};
|
||||
|
||||
// A FileSlice backed by in-memory data which outlives this object.
|
||||
class StringPieceSlice : public FileSlice {
|
||||
public:
|
||||
StringPieceSlice() = default;
|
||||
explicit StringPieceSlice(re2::StringPiece piece) : piece_(piece) {}
|
||||
void Init(re2::StringPiece piece) { piece_ = piece; }
|
||||
|
||||
int64_t size() const final { return piece_.size(); }
|
||||
int64_t AddRange(ByteRange range, EvBuffer *buf,
|
||||
std::string *error_message) const final;
|
||||
|
||||
private:
|
||||
re2::StringPiece piece_;
|
||||
};
|
||||
|
||||
// A slice composed of other slices.
|
||||
class FileSlices : public FileSlice {
|
||||
public:
|
||||
FileSlices() {}
|
||||
FileSlices(const FileSlices &) = delete;
|
||||
FileSlices &operator=(const FileSlices &) = delete;
|
||||
|
||||
// |slice| must outlive the FileSlices.
|
||||
// |slice->size()| should not change after this call.
|
||||
// |flags| should be a bitmask of Flags values below.
|
||||
void Append(const FileSlice *slice, int flags = 0) {
|
||||
int64_t new_size = size_ + slice->size();
|
||||
slices_.emplace_back(ByteRange(size_, new_size), slice, flags);
|
||||
size_ = new_size;
|
||||
}
|
||||
|
||||
size_t num_slices() const { return slices_.size(); }
|
||||
|
||||
int64_t size() const final { return size_; }
|
||||
int64_t AddRange(ByteRange range, EvBuffer *buf,
|
||||
std::string *error_message) const final;
|
||||
|
||||
enum Flags {
|
||||
// kLazy, as an argument to Append, instructs the FileSlices to append
|
||||
// this slice in AddRange only if it is the first slice in the requested
|
||||
// range. Otherwise it returns early, expecting HttpServe to call AddRange
|
||||
// again after the earlier ranges have been sent. This is useful if it is
|
||||
// expensive to have the given slice pending. In particular, it is useful
|
||||
// when serving many file slices on 32-bit machines to avoid exhausting
|
||||
// the address space with too many memory mappings.
|
||||
kLazy = 1
|
||||
};
|
||||
|
||||
private:
|
||||
struct SliceInfo {
|
||||
SliceInfo(ByteRange range, const FileSlice *slice, int flags)
|
||||
: range(range), slice(slice), flags(flags) {}
|
||||
ByteRange range;
|
||||
const FileSlice *slice = nullptr;
|
||||
int flags;
|
||||
};
|
||||
int64_t size_ = 0;
|
||||
|
||||
std::vector<SliceInfo> slices_;
|
||||
};
|
||||
|
||||
// Serve an HTTP request |req| from |file|, handling byte range and
|
||||
// conditional serving. (Similar to golang's http.ServeContent.)
|
||||
//
|
||||
// |file| will be retained as long as the request is being served.
|
||||
void HttpServe(const std::shared_ptr<VirtualFile> &file, evhttp_request *req);
|
||||
|
||||
namespace internal {
|
||||
|
||||
// Value to represent result of parsing HTTP 1.1 "Range:" header.
|
||||
enum class RangeHeaderType {
|
||||
// Ignore the header, serving all bytes in the file.
|
||||
kAbsentOrInvalid,
|
||||
|
||||
// The server SHOULD return a response with status 416 (Requested range not
|
||||
// satisfiable).
|
||||
kNotSatisfiable,
|
||||
|
||||
// The server SHOULD return a response with status 406 (Partial Content).
|
||||
kSatisfiable
|
||||
};
|
||||
|
||||
// Parse an HTTP 1.1 "Range:" header value, following RFC 2616 section 14.35.
|
||||
// This function is for use by HttpServe; it is exposed for testing only.
|
||||
//
|
||||
// |value| on entry should be the header value (after the ": "), or nullptr.
|
||||
// |size| on entry should be the number of bytes available to serve.
|
||||
// On kSatisfiable return, |ranges| will be filled with the satisfiable ranges.
|
||||
// Otherwise, its contents are undefined.
|
||||
RangeHeaderType ParseRangeHeader(const char *value, int64_t size,
|
||||
std::vector<ByteRange> *ranges);
|
||||
|
||||
} // namespace internal
|
||||
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
#endif // MOONFIRE_NVR_HTTP_H
|
195
src/main.rs
Normal file
195
src/main.rs
Normal file
@ -0,0 +1,195 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#![cfg_attr(test, feature(test))]
|
||||
#![feature(alloc, box_syntax, conservative_impl_trait, plugin, proc_macro)]
|
||||
#![plugin(clippy)]
|
||||
|
||||
extern crate alloc;
|
||||
extern crate byteorder;
|
||||
extern crate core;
|
||||
#[macro_use] extern crate chan;
|
||||
extern crate chan_signal;
|
||||
extern crate docopt;
|
||||
#[macro_use] extern crate ffmpeg;
|
||||
extern crate ffmpeg_sys;
|
||||
extern crate fnv;
|
||||
extern crate hyper;
|
||||
#[macro_use] extern crate lazy_static;
|
||||
extern crate libc;
|
||||
#[macro_use] extern crate log;
|
||||
extern crate lru_cache;
|
||||
extern crate rusqlite;
|
||||
extern crate memmap;
|
||||
#[macro_use] extern crate mime;
|
||||
extern crate openssl;
|
||||
extern crate regex;
|
||||
extern crate rustc_serialize;
|
||||
extern crate serde;
|
||||
#[macro_use] extern crate serde_derive;
|
||||
extern crate serde_json;
|
||||
extern crate slog;
|
||||
extern crate slog_envlogger;
|
||||
extern crate slog_stdlog;
|
||||
extern crate slog_term;
|
||||
extern crate smallvec;
|
||||
extern crate time;
|
||||
extern crate url;
|
||||
extern crate uuid;
|
||||
|
||||
use hyper::server::Server;
|
||||
use slog::DrainExt;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread;
|
||||
|
||||
mod db;
|
||||
mod dir;
|
||||
mod error;
|
||||
mod h264;
|
||||
mod mmapfile;
|
||||
mod mp4;
|
||||
mod pieces;
|
||||
mod recording;
|
||||
mod resource;
|
||||
mod stream;
|
||||
mod streamer;
|
||||
#[cfg(test)] mod testutil;
|
||||
mod web;
|
||||
|
||||
/// Commandline usage string. This is in the particular format expected by the `docopt` crate.
|
||||
/// Besides being printed on --help or argument parsing error, it's actually parsed to define the
|
||||
/// allowed commandline arguments and their defaults.
|
||||
const USAGE: &'static str = "
|
||||
Usage: moonfire-nvr [options]
|
||||
moonfire-nvr (--help | --version)
|
||||
|
||||
Options:
|
||||
-h, --help Show this message.
|
||||
--version Show the version of moonfire-nvr.
|
||||
--db-dir DIR Set the directory holding the SQLite3 index database.
|
||||
This is typically on a flash device.
|
||||
[default: /var/lib/moonfire-nvr/db]
|
||||
--sample-file-dir DIR Set the directory holding video data.
|
||||
This is typically on a hard drive.
|
||||
[default: /var/lib/moonfire-nvr/sample]
|
||||
--http-addr ADDR Set the bind address for the unencrypted HTTP server.
|
||||
[default: 0.0.0.0:8080]
|
||||
--read-only Forces read-only mode / disables recording.
|
||||
";
|
||||
|
||||
/// Commandline arguments corresponding to `USAGE`; automatically filled by the `docopt` crate.
|
||||
#[derive(RustcDecodable)]
|
||||
struct Args {
|
||||
flag_db_dir: String,
|
||||
flag_sample_file_dir: String,
|
||||
flag_http_addr: String,
|
||||
flag_read_only: bool,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Watch for termination signals.
|
||||
// This must be started before any threads are spawned (such as the async logger thread) so
|
||||
// that signals will be blocked in all threads.
|
||||
let signal = chan_signal::notify(&[chan_signal::Signal::INT, chan_signal::Signal::TERM]);
|
||||
|
||||
// Initialize logging.
|
||||
let drain = slog_term::StreamerBuilder::new().async().full().build();
|
||||
let drain = slog_envlogger::new(drain);
|
||||
slog_stdlog::set_logger(slog::Logger::root(drain.ignore_err(), None)).unwrap();
|
||||
|
||||
// Parse commandline arguments.
|
||||
let version = "Moonfire NVR 0.1.0".to_owned();
|
||||
let args: Args = docopt::Docopt::new(USAGE)
|
||||
.and_then(|d| d.version(Some(version)).decode())
|
||||
.unwrap_or_else(|e| e.exit());
|
||||
|
||||
// Open the database and populate cached state.
|
||||
let db_dir = dir::Fd::open(&args.flag_db_dir).unwrap();
|
||||
db_dir.lock(if args.flag_read_only { libc::LOCK_SH } else { libc::LOCK_EX } | libc::LOCK_NB)
|
||||
.unwrap();
|
||||
let conn = rusqlite::Connection::open_with_flags(
|
||||
Path::new(&args.flag_db_dir).join("db"),
|
||||
if args.flag_read_only {
|
||||
rusqlite::SQLITE_OPEN_READ_ONLY
|
||||
} else {
|
||||
rusqlite::SQLITE_OPEN_READ_WRITE
|
||||
} |
|
||||
// rusqlite::Connection is not Sync, so there's no reason to tell SQLite3 to use the
|
||||
// serialized threading mode.
|
||||
rusqlite::SQLITE_OPEN_NO_MUTEX).unwrap();
|
||||
let db = Arc::new(db::Database::new(conn).unwrap());
|
||||
let dir = dir::SampleFileDir::new(&args.flag_sample_file_dir, db.clone()).unwrap();
|
||||
info!("Database is loaded.");
|
||||
|
||||
// Start a streamer for each camera.
|
||||
let shutdown = Arc::new(AtomicBool::new(false));
|
||||
let mut streamers = Vec::new();
|
||||
let syncer = if !args.flag_read_only {
|
||||
let (syncer_channel, syncer_join) = dir::start_syncer(dir.clone()).unwrap();
|
||||
let l = db.lock();
|
||||
let cameras = l.cameras_by_id().len();
|
||||
for (i, (id, camera)) in l.cameras_by_id().iter().enumerate() {
|
||||
let rotate_offset_sec = streamer::ROTATE_INTERVAL_SEC * i as i64 / cameras as i64;
|
||||
let mut streamer = streamer::Streamer::new(
|
||||
db.clone(), dir.clone(), syncer_channel.clone(), shutdown.clone(), *id, camera,
|
||||
rotate_offset_sec);
|
||||
let name = format!("stream-{}", streamer.short_name());
|
||||
streamers.push(thread::Builder::new().name(name).spawn(move|| {
|
||||
streamer.run();
|
||||
}).expect("can't create thread"));
|
||||
}
|
||||
Some((syncer_channel, syncer_join))
|
||||
} else { None };
|
||||
|
||||
// Start the web interface.
|
||||
let server = Server::http(args.flag_http_addr.as_str()).unwrap();
|
||||
let h = web::Handler::new(db.clone(), dir.clone());
|
||||
let _guard = server.handle(h);
|
||||
info!("Ready to serve HTTP requests");
|
||||
|
||||
// Wait for a signal and shut down.
|
||||
chan_select! {
|
||||
signal.recv() -> signal => info!("Received signal {:?}; shutting down streamers.", signal),
|
||||
}
|
||||
shutdown.store(true, Ordering::SeqCst);
|
||||
for streamer in streamers.drain(..) {
|
||||
streamer.join().unwrap();
|
||||
}
|
||||
if let Some((syncer_channel, syncer_join)) = syncer {
|
||||
info!("Shutting down syncer.");
|
||||
drop(syncer_channel);
|
||||
syncer_join.join().unwrap();
|
||||
}
|
||||
info!("Exiting.");
|
||||
// TODO: drain the logger.
|
||||
std::process::exit(0);
|
||||
}
|
71
src/mmapfile.rs
Normal file
71
src/mmapfile.rs
Normal file
@ -0,0 +1,71 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Memory-mapped file serving.
|
||||
|
||||
extern crate memmap;
|
||||
|
||||
use error::Result;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::ops::Range;
|
||||
|
||||
/// Memory-mapped file slice.
|
||||
/// This struct is meant to be used in constructing an implementation of the `resource::Resource`
|
||||
/// or `pieces::ContextWriter` traits. The file in question should be immutable, as files shrinking
|
||||
/// during `mmap` will cause the process to fail with `SIGBUS`. Moonfire NVR sample files satisfy
|
||||
/// this requirement:
|
||||
///
|
||||
/// * They should only be modified by Moonfire NVR itself. Installation instructions encourage
|
||||
/// creating a dedicated user/group for Moonfire NVR and ensuring only this group has
|
||||
/// permissions to Moonfire NVR's directories.
|
||||
/// * Moonfire NVR never modifies sample files after inserting their matching recording entries
|
||||
/// into the database. They are kept as-is until they are deleted.
|
||||
pub struct MmapFileSlice {
|
||||
f: File,
|
||||
range: Range<u64>,
|
||||
}
|
||||
|
||||
impl MmapFileSlice {
|
||||
pub fn new(f: File, range: Range<u64>) -> MmapFileSlice {
|
||||
MmapFileSlice{f: f, range: range}
|
||||
}
|
||||
|
||||
pub fn write_to(&self, range: Range<u64>, out: &mut io::Write) -> Result<()> {
|
||||
// TODO: overflow check (in case u64 is larger than usize).
|
||||
let r = self.range.start + range.start .. self.range.start + range.end;
|
||||
assert!(r.end <= self.range.end,
|
||||
"requested={:?} within={:?}", range, self.range);
|
||||
let mmap = memmap::Mmap::open_with_offset(
|
||||
&self.f, memmap::Protection::Read, r.start as usize, (r.end - r.start) as usize)?;
|
||||
unsafe { out.write_all(mmap.as_slice())?; }
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,458 +0,0 @@
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// moonfire-db-test.cc: tests of the moonfire-db.h interface.
|
||||
|
||||
#include <time.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "moonfire-db.h"
|
||||
#include "sqlite.h"
|
||||
#include "string.h"
|
||||
#include "testutil.h"
|
||||
|
||||
DECLARE_bool(alsologtostderr);
|
||||
|
||||
using testing::_;
|
||||
using testing::HasSubstr;
|
||||
using testing::DoAll;
|
||||
using testing::Return;
|
||||
using testing::SetArgPointee;
|
||||
|
||||
namespace moonfire_nvr {
|
||||
namespace {
|
||||
|
||||
class MoonfireDbTest : public testing::Test {
|
||||
protected:
|
||||
MoonfireDbTest() {
|
||||
tmpdir_ = PrepareTempDirOrDie("moonfire-db-test");
|
||||
std::string error_message;
|
||||
CHECK(db_.Open(StrCat(tmpdir_, "/db").c_str(),
|
||||
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, &error_message))
|
||||
<< error_message;
|
||||
std::string create_sql = ReadFileOrDie("../src/schema.sql");
|
||||
DatabaseContext ctx(&db_);
|
||||
CHECK(RunStatements(&ctx, create_sql, &error_message)) << error_message;
|
||||
}
|
||||
|
||||
int64_t AddCamera(Uuid uuid, re2::StringPiece short_name) {
|
||||
DatabaseContext ctx(&db_);
|
||||
auto run = ctx.UseOnce(
|
||||
R"(
|
||||
insert into camera (uuid, short_name, host, username, password,
|
||||
main_rtsp_path, sub_rtsp_path, retain_bytes)
|
||||
values (:uuid, :short_name, :host, :username, :password,
|
||||
:main_rtsp_path, :sub_rtsp_path, :retain_bytes);
|
||||
)");
|
||||
run.BindBlob(":uuid", uuid.binary_view());
|
||||
run.BindText(":short_name", short_name);
|
||||
run.BindText(":host", "test-camera");
|
||||
run.BindText(":username", "foo");
|
||||
run.BindText(":password", "bar");
|
||||
run.BindText(":main_rtsp_path", "/main");
|
||||
run.BindText(":sub_rtsp_path", "/sub");
|
||||
run.BindInt64(":retain_bytes", 42);
|
||||
CHECK_EQ(SQLITE_DONE, run.Step()) << run.error_message();
|
||||
if (run.Step() != SQLITE_DONE) {
|
||||
ADD_FAILURE() << run.error_message();
|
||||
return -1;
|
||||
}
|
||||
return ctx.last_insert_rowid();
|
||||
}
|
||||
|
||||
void ExpectNoRecordings(Uuid camera_uuid) {
|
||||
int rows = 0;
|
||||
mdb_->ListCameras([&](const ListCamerasRow &row) {
|
||||
++rows;
|
||||
EXPECT_EQ(camera_uuid, row.uuid);
|
||||
EXPECT_EQ("test-camera", row.host);
|
||||
EXPECT_EQ("foo", row.username);
|
||||
EXPECT_EQ("bar", row.password);
|
||||
EXPECT_EQ("/main", row.main_rtsp_path);
|
||||
EXPECT_EQ("/sub", row.sub_rtsp_path);
|
||||
EXPECT_EQ(42, row.retain_bytes);
|
||||
EXPECT_EQ(std::numeric_limits<int64_t>::max(), row.min_start_time_90k);
|
||||
EXPECT_EQ(std::numeric_limits<int64_t>::min(), row.max_end_time_90k);
|
||||
EXPECT_EQ(0, row.total_duration_90k);
|
||||
EXPECT_EQ(0, row.total_sample_file_bytes);
|
||||
return IterationControl::kContinue;
|
||||
});
|
||||
EXPECT_EQ(1, rows);
|
||||
|
||||
GetCameraRow row;
|
||||
EXPECT_TRUE(mdb_->GetCamera(camera_uuid, &row));
|
||||
EXPECT_THAT(row.days, testing::ElementsAre());
|
||||
|
||||
std::string error_message;
|
||||
rows = 0;
|
||||
EXPECT_TRUE(mdb_->ListCameraRecordings(
|
||||
camera_uuid, 0, std::numeric_limits<int64_t>::max(),
|
||||
[&](const ListCameraRecordingsRow &row) {
|
||||
++rows;
|
||||
return IterationControl::kBreak;
|
||||
},
|
||||
&error_message))
|
||||
<< error_message;
|
||||
EXPECT_EQ(0, rows);
|
||||
|
||||
rows = 0;
|
||||
EXPECT_TRUE(mdb_->ListMp4Recordings(
|
||||
camera_uuid, 0, std::numeric_limits<int64_t>::max(),
|
||||
[&](Recording &recording, const VideoSampleEntry &entry) {
|
||||
++rows;
|
||||
return IterationControl::kBreak;
|
||||
},
|
||||
&error_message))
|
||||
<< error_message;
|
||||
EXPECT_EQ(0, rows);
|
||||
}
|
||||
|
||||
void ExpectSingleRecording(Uuid camera_uuid, const Recording &recording,
|
||||
const VideoSampleEntry &entry,
|
||||
ListOldestSampleFilesRow *save_oldest_row) {
|
||||
std::string error_message;
|
||||
int rows = 0;
|
||||
mdb_->ListCameras([&](const ListCamerasRow &row) {
|
||||
++rows;
|
||||
EXPECT_EQ(camera_uuid, row.uuid);
|
||||
EXPECT_EQ(recording.start_time_90k, row.min_start_time_90k);
|
||||
EXPECT_EQ(recording.end_time_90k, row.max_end_time_90k);
|
||||
EXPECT_EQ(recording.end_time_90k - recording.start_time_90k,
|
||||
row.total_duration_90k);
|
||||
EXPECT_EQ(recording.sample_file_bytes, row.total_sample_file_bytes);
|
||||
return IterationControl::kContinue;
|
||||
});
|
||||
EXPECT_EQ(1, rows);
|
||||
|
||||
std::map<std::string, int64_t> expected_days;
|
||||
internal::AdjustDaysMap(recording.start_time_90k, recording.end_time_90k, 1,
|
||||
&expected_days);
|
||||
GetCameraRow row;
|
||||
EXPECT_TRUE(mdb_->GetCamera(camera_uuid, &row));
|
||||
EXPECT_THAT(row.days, testing::Eq(expected_days));
|
||||
|
||||
GetCameraRow camera_row;
|
||||
EXPECT_TRUE(mdb_->GetCamera(camera_uuid, &camera_row));
|
||||
EXPECT_EQ(recording.start_time_90k, camera_row.min_start_time_90k);
|
||||
EXPECT_EQ(recording.end_time_90k, camera_row.max_end_time_90k);
|
||||
EXPECT_EQ(recording.end_time_90k - recording.start_time_90k,
|
||||
camera_row.total_duration_90k);
|
||||
EXPECT_EQ(recording.sample_file_bytes, camera_row.total_sample_file_bytes);
|
||||
|
||||
rows = 0;
|
||||
EXPECT_TRUE(mdb_->ListCameraRecordings(
|
||||
camera_uuid, 0, std::numeric_limits<int64_t>::max(),
|
||||
[&](const ListCameraRecordingsRow &row) {
|
||||
++rows;
|
||||
EXPECT_EQ(recording.start_time_90k, row.start_time_90k);
|
||||
EXPECT_EQ(recording.end_time_90k, row.end_time_90k);
|
||||
EXPECT_EQ(recording.video_samples, row.video_samples);
|
||||
EXPECT_EQ(recording.sample_file_bytes, row.sample_file_bytes);
|
||||
EXPECT_EQ(entry.sha1, row.video_sample_entry_sha1);
|
||||
EXPECT_EQ(entry.width, row.width);
|
||||
EXPECT_EQ(entry.height, row.height);
|
||||
return IterationControl::kContinue;
|
||||
},
|
||||
&error_message))
|
||||
<< error_message;
|
||||
EXPECT_EQ(1, rows);
|
||||
|
||||
rows = 0;
|
||||
EXPECT_TRUE(mdb_->ListOldestSampleFiles(
|
||||
camera_uuid,
|
||||
[&](const ListOldestSampleFilesRow &row) {
|
||||
++rows;
|
||||
EXPECT_EQ(recording.id, row.recording_id);
|
||||
EXPECT_EQ(recording.sample_file_uuid, row.sample_file_uuid);
|
||||
EXPECT_EQ(recording.end_time_90k - recording.start_time_90k,
|
||||
row.duration_90k);
|
||||
EXPECT_EQ(recording.sample_file_bytes, row.sample_file_bytes);
|
||||
*save_oldest_row = row;
|
||||
return IterationControl::kContinue;
|
||||
},
|
||||
&error_message))
|
||||
<< error_message;
|
||||
EXPECT_EQ(1, rows);
|
||||
|
||||
rows = 0;
|
||||
EXPECT_TRUE(mdb_->ListMp4Recordings(
|
||||
camera_uuid, 0, std::numeric_limits<int64_t>::max(),
|
||||
[&](Recording &some_recording, const VideoSampleEntry &some_entry) {
|
||||
++rows;
|
||||
|
||||
EXPECT_EQ(recording.id, some_recording.id);
|
||||
EXPECT_EQ(recording.camera_id, some_recording.camera_id);
|
||||
EXPECT_EQ(recording.sample_file_sha1,
|
||||
some_recording.sample_file_sha1);
|
||||
EXPECT_EQ(recording.sample_file_uuid,
|
||||
some_recording.sample_file_uuid);
|
||||
EXPECT_EQ(recording.video_sample_entry_id,
|
||||
some_recording.video_sample_entry_id);
|
||||
EXPECT_EQ(recording.start_time_90k, some_recording.start_time_90k);
|
||||
EXPECT_EQ(recording.end_time_90k, some_recording.end_time_90k);
|
||||
EXPECT_EQ(recording.sample_file_bytes,
|
||||
some_recording.sample_file_bytes);
|
||||
EXPECT_EQ(recording.video_samples, some_recording.video_samples);
|
||||
EXPECT_EQ(recording.video_sync_samples,
|
||||
some_recording.video_sync_samples);
|
||||
EXPECT_EQ(recording.video_index, some_recording.video_index);
|
||||
|
||||
EXPECT_EQ(entry.id, some_entry.id);
|
||||
EXPECT_EQ(entry.sha1, some_entry.sha1);
|
||||
EXPECT_EQ(entry.data, some_entry.data);
|
||||
EXPECT_EQ(entry.width, some_entry.width);
|
||||
EXPECT_EQ(entry.height, some_entry.height);
|
||||
|
||||
return IterationControl::kContinue;
|
||||
},
|
||||
&error_message))
|
||||
<< error_message;
|
||||
EXPECT_EQ(1, rows);
|
||||
}
|
||||
|
||||
std::string tmpdir_;
|
||||
Database db_;
|
||||
std::unique_ptr<MoonfireDatabase> mdb_;
|
||||
};
|
||||
|
||||
TEST(AdjustDaysMapTest, Basic) {
|
||||
std::map<std::string, int64_t> days;
|
||||
|
||||
// Create a day.
|
||||
const int64_t kTestTime = INT64_C(130647162600000); // 2015-12-31 23:59:00
|
||||
moonfire_nvr::internal::AdjustDaysMap(
|
||||
kTestTime, kTestTime + 60 * kTimeUnitsPerSecond, 1, &days);
|
||||
EXPECT_THAT(days, testing::ElementsAre(std::make_pair(
|
||||
"2015-12-31", 60 * kTimeUnitsPerSecond)));
|
||||
|
||||
// Add to a day.
|
||||
moonfire_nvr::internal::AdjustDaysMap(
|
||||
kTestTime, kTestTime + 60 * kTimeUnitsPerSecond, 1, &days);
|
||||
EXPECT_THAT(days, testing::ElementsAre(std::make_pair(
|
||||
"2015-12-31", 120 * kTimeUnitsPerSecond)));
|
||||
|
||||
// Subtract from a day.
|
||||
moonfire_nvr::internal::AdjustDaysMap(
|
||||
kTestTime, kTestTime + 60 * kTimeUnitsPerSecond, -1, &days);
|
||||
EXPECT_THAT(days, testing::ElementsAre(std::make_pair(
|
||||
"2015-12-31", 60 * kTimeUnitsPerSecond)));
|
||||
|
||||
// Remove a day.
|
||||
moonfire_nvr::internal::AdjustDaysMap(
|
||||
kTestTime, kTestTime + 60 * kTimeUnitsPerSecond, -1, &days);
|
||||
EXPECT_THAT(days, testing::ElementsAre());
|
||||
|
||||
// Create two days.
|
||||
moonfire_nvr::internal::AdjustDaysMap(
|
||||
kTestTime, kTestTime + 3 * 60 * kTimeUnitsPerSecond, 1, &days);
|
||||
EXPECT_THAT(days,
|
||||
testing::ElementsAre(
|
||||
std::make_pair("2015-12-31", 1 * 60 * kTimeUnitsPerSecond),
|
||||
std::make_pair("2016-01-01", 2 * 60 * kTimeUnitsPerSecond)));
|
||||
|
||||
// Add to two days.
|
||||
moonfire_nvr::internal::AdjustDaysMap(
|
||||
kTestTime, kTestTime + 3 * 60 * kTimeUnitsPerSecond, 1, &days);
|
||||
EXPECT_THAT(days,
|
||||
testing::ElementsAre(
|
||||
std::make_pair("2015-12-31", 2 * 60 * kTimeUnitsPerSecond),
|
||||
std::make_pair("2016-01-01", 4 * 60 * kTimeUnitsPerSecond)));
|
||||
|
||||
// Subtract from two days.
|
||||
moonfire_nvr::internal::AdjustDaysMap(
|
||||
kTestTime, kTestTime + 3 * 60 * kTimeUnitsPerSecond, -1, &days);
|
||||
EXPECT_THAT(days,
|
||||
testing::ElementsAre(
|
||||
std::make_pair("2015-12-31", 1 * 60 * kTimeUnitsPerSecond),
|
||||
std::make_pair("2016-01-01", 2 * 60 * kTimeUnitsPerSecond)));
|
||||
|
||||
// Remove two days.
|
||||
moonfire_nvr::internal::AdjustDaysMap(
|
||||
kTestTime, kTestTime + 3 * 60 * kTimeUnitsPerSecond, -1, &days);
|
||||
EXPECT_THAT(days, testing::ElementsAre());
|
||||
}
|
||||
|
||||
TEST(GetDayBoundsTest, Basic) {
|
||||
int64_t start_90k;
|
||||
int64_t end_90k;
|
||||
std::string error_msg;
|
||||
|
||||
// Normal day.
|
||||
EXPECT_TRUE(GetDayBounds("2015-12-31", &start_90k, &end_90k, &error_msg))
|
||||
<< error_msg;
|
||||
EXPECT_EQ(INT64_C(130639392000000), start_90k);
|
||||
EXPECT_EQ(INT64_C(130647168000000), end_90k);
|
||||
|
||||
// Spring forward (23-hour day).
|
||||
EXPECT_TRUE(GetDayBounds("2016-03-13", &start_90k, &end_90k, &error_msg));
|
||||
EXPECT_EQ(INT64_C(131207040000000), start_90k);
|
||||
EXPECT_EQ(INT64_C(131214492000000), end_90k);
|
||||
|
||||
// Fall back (25-hour day).
|
||||
EXPECT_TRUE(GetDayBounds("2016-11-06", &start_90k, &end_90k, &error_msg));
|
||||
EXPECT_EQ(INT64_C(133057404000000), start_90k);
|
||||
EXPECT_EQ(INT64_C(133065504000000), end_90k);
|
||||
|
||||
// Unparseable day.
|
||||
EXPECT_FALSE(GetDayBounds("xxxx-xx-xx", &start_90k, &end_90k, &error_msg));
|
||||
}
|
||||
|
||||
// Basic test of running some queries on an empty database.
|
||||
TEST_F(MoonfireDbTest, EmptyDatabase) {
|
||||
std::string error_message;
|
||||
mdb_.reset(new MoonfireDatabase);
|
||||
ASSERT_TRUE(mdb_->Init(&db_, &error_message)) << error_message;
|
||||
|
||||
mdb_->ListCameras([&](const ListCamerasRow &row) {
|
||||
ADD_FAILURE() << "row unexpected";
|
||||
return IterationControl::kBreak;
|
||||
});
|
||||
|
||||
GetCameraRow get_camera_row;
|
||||
EXPECT_FALSE(mdb_->GetCamera(Uuid(), &get_camera_row));
|
||||
|
||||
EXPECT_FALSE(
|
||||
mdb_->ListCameraRecordings(Uuid(), 0, std::numeric_limits<int64_t>::max(),
|
||||
[&](const ListCameraRecordingsRow &row) {
|
||||
ADD_FAILURE() << "row unexpected";
|
||||
return IterationControl::kBreak;
|
||||
},
|
||||
&error_message));
|
||||
|
||||
EXPECT_FALSE(mdb_->ListMp4Recordings(
|
||||
Uuid(), 0, std::numeric_limits<int64_t>::max(),
|
||||
[&](Recording &recording, const VideoSampleEntry &entry) {
|
||||
ADD_FAILURE() << "row unexpected";
|
||||
return IterationControl::kBreak;
|
||||
},
|
||||
&error_message));
|
||||
}
|
||||
|
||||
// Basic test of the full lifecycle of recording.
|
||||
// Does not exercise many error cases.
|
||||
TEST_F(MoonfireDbTest, FullLifecycle) {
|
||||
std::string error_message;
|
||||
const char kCameraShortName[] = "testcam";
|
||||
Uuid camera_uuid = GetRealUuidGenerator()->Generate();
|
||||
int64_t camera_id = AddCamera(camera_uuid, kCameraShortName);
|
||||
ASSERT_GT(camera_id, 0);
|
||||
mdb_.reset(new MoonfireDatabase);
|
||||
ASSERT_TRUE(mdb_->Init(&db_, &error_message)) << error_message;
|
||||
|
||||
ExpectNoRecordings(camera_uuid);
|
||||
|
||||
std::vector<Uuid> reserved;
|
||||
EXPECT_TRUE(mdb_->ListReservedSampleFiles(&reserved, &error_message))
|
||||
<< error_message;
|
||||
EXPECT_THAT(reserved, testing::IsEmpty());
|
||||
|
||||
std::vector<Uuid> uuids = mdb_->ReserveSampleFiles(2, &error_message);
|
||||
ASSERT_THAT(uuids, testing::SizeIs(2)) << error_message;
|
||||
|
||||
EXPECT_TRUE(mdb_->ListReservedSampleFiles(&reserved, &error_message))
|
||||
<< error_message;
|
||||
EXPECT_THAT(reserved, testing::UnorderedElementsAre(uuids[0], uuids[1]));
|
||||
|
||||
VideoSampleEntry entry;
|
||||
entry.sha1.resize(20);
|
||||
entry.width = 768;
|
||||
entry.height = 512;
|
||||
entry.data.resize(100);
|
||||
ASSERT_TRUE(mdb_->InsertVideoSampleEntry(&entry, &error_message))
|
||||
<< error_message;
|
||||
ASSERT_GT(entry.id, 0);
|
||||
|
||||
Recording recording;
|
||||
recording.camera_id = camera_id;
|
||||
recording.sample_file_uuid = GetRealUuidGenerator()->Generate();
|
||||
recording.video_sample_entry_id = entry.id;
|
||||
SampleIndexEncoder encoder;
|
||||
encoder.Init(&recording, UINT64_C(1430006400) * kTimeUnitsPerSecond);
|
||||
encoder.AddSample(kTimeUnitsPerSecond, 42, true);
|
||||
|
||||
// Inserting a recording should succeed and remove its uuid from the
|
||||
// reserved table.
|
||||
ASSERT_FALSE(mdb_->InsertRecording(&recording, &error_message));
|
||||
EXPECT_THAT(error_message, testing::HasSubstr("not reserved"));
|
||||
recording.sample_file_uuid = uuids.back();
|
||||
recording.sample_file_sha1.resize(20);
|
||||
ASSERT_TRUE(mdb_->InsertRecording(&recording, &error_message))
|
||||
<< error_message;
|
||||
ASSERT_GT(recording.id, 0);
|
||||
EXPECT_TRUE(mdb_->ListReservedSampleFiles(&reserved, &error_message))
|
||||
<< error_message;
|
||||
EXPECT_THAT(reserved, testing::ElementsAre(uuids[0]));
|
||||
|
||||
// Queries should return the correct result (with caches updated on insert).
|
||||
ListOldestSampleFilesRow oldest;
|
||||
ExpectSingleRecording(camera_uuid, recording, entry, &oldest);
|
||||
|
||||
// Queries on a fresh database should return the correct result (with caches
|
||||
// populated from existing database contents).
|
||||
mdb_.reset(new MoonfireDatabase);
|
||||
ASSERT_TRUE(mdb_->Init(&db_, &error_message)) << error_message;
|
||||
ExpectSingleRecording(camera_uuid, recording, entry, &oldest);
|
||||
|
||||
// Deleting a recording should succeed, update the min/max times, and mark
|
||||
// the uuid as reserved.
|
||||
std::vector<ListOldestSampleFilesRow> to_delete;
|
||||
to_delete.push_back(oldest);
|
||||
ASSERT_TRUE(mdb_->DeleteRecordings(to_delete, &error_message))
|
||||
<< error_message;
|
||||
EXPECT_TRUE(mdb_->ListReservedSampleFiles(&reserved, &error_message))
|
||||
<< error_message;
|
||||
EXPECT_THAT(reserved, testing::UnorderedElementsAre(uuids[0], uuids[1]));
|
||||
ExpectNoRecordings(camera_uuid);
|
||||
|
||||
EXPECT_TRUE(mdb_->MarkSampleFilesDeleted(uuids, &error_message))
|
||||
<< error_message;
|
||||
EXPECT_TRUE(mdb_->ListReservedSampleFiles(&reserved, &error_message))
|
||||
<< error_message;
|
||||
EXPECT_THAT(reserved, testing::IsEmpty());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
FLAGS_alsologtostderr = true;
|
||||
google::ParseCommandLineFlags(&argc, &argv, true);
|
||||
testing::InitGoogleTest(&argc, argv);
|
||||
google::InitGoogleLogging(argv[0]);
|
||||
|
||||
// The calendar day math assumes this timezone.
|
||||
CHECK_EQ(0, setenv("TZ", "America/Los_Angeles", 1)) << strerror(errno);
|
||||
tzset();
|
||||
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
@ -1,954 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// moonfire-db.cc: implementation of moonfire-db.h interface.
|
||||
// see top-level comments there on performance & efficiency.
|
||||
|
||||
#include "moonfire-db.h"
|
||||
|
||||
#include <time.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <glog/logging.h>
|
||||
|
||||
#include "http.h"
|
||||
#include "mp4.h"
|
||||
#include "recording.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
namespace {
|
||||
|
||||
const char kDayFmt[] = "%Y-%m-%d";
|
||||
constexpr size_t kDayFmtBufSize = sizeof("YYYY-mm-DD");
|
||||
|
||||
// Helper for AdjustDaysMap.
|
||||
void AdjustDay(const std::string &day, int64_t delta,
|
||||
std::map<std::string, int64_t> *days) {
|
||||
auto it = days->find(day);
|
||||
if (it != days->end()) {
|
||||
it->second += delta;
|
||||
DCHECK_GE(it->second, 0) << day << ", " << delta;
|
||||
if (it->second == 0) {
|
||||
days->erase(it);
|
||||
}
|
||||
} else {
|
||||
days->insert(it, std::make_pair(day, delta));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace internal {
|
||||
|
||||
void AdjustDaysMap(int64_t start_time_90k, int64_t end_time_90k, int sign,
|
||||
std::map<std::string, int64_t> *days) {
|
||||
// There will always be at most two days adjusted, because
|
||||
// kMaxRecordingDuration is less than a day (even during spring forward).
|
||||
DCHECK_LE(start_time_90k, end_time_90k);
|
||||
DCHECK_LE(end_time_90k - start_time_90k, kMaxRecordingDuration);
|
||||
static_assert(kMaxRecordingDuration <= 23 * 60 * kTimeUnitsPerSecond,
|
||||
"max duration should be less than a (spring-forward) day");
|
||||
if (start_time_90k == end_time_90k) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill |buf| with the first day.
|
||||
struct tm mytm;
|
||||
memset(&mytm, 0, sizeof(mytm));
|
||||
time_t start_ts = start_time_90k / kTimeUnitsPerSecond;
|
||||
localtime_r(&start_ts, &mytm);
|
||||
char buf[kDayFmtBufSize];
|
||||
strftime(buf, sizeof(buf), kDayFmt, &mytm);
|
||||
|
||||
// Determine the start of the next day.
|
||||
// Note that mktime(3) normalizes tm_mday, so this should work on the last
|
||||
// day of the month/year.
|
||||
mytm.tm_isdst = -1;
|
||||
mytm.tm_hour = 0;
|
||||
mytm.tm_min = 0;
|
||||
mytm.tm_sec = 0;
|
||||
++mytm.tm_mday;
|
||||
auto boundary_90k = kTimeUnitsPerSecond * static_cast<int64_t>(mktime(&mytm));
|
||||
|
||||
// Adjust the first day.
|
||||
auto first_day_delta =
|
||||
sign * (std::min(end_time_90k, boundary_90k) - start_time_90k);
|
||||
DCHECK_NE(first_day_delta, 0) << "start=" << start_time_90k
|
||||
<< ", end=" << end_time_90k;
|
||||
AdjustDay(buf, first_day_delta, days);
|
||||
|
||||
if (end_time_90k <= boundary_90k) {
|
||||
return; // no second day.
|
||||
}
|
||||
|
||||
// Fill |buf| with the second day.
|
||||
strftime(buf, sizeof(buf), kDayFmt, &mytm);
|
||||
|
||||
// Adjust the second day.
|
||||
auto second_day_delta = sign * (end_time_90k - boundary_90k);
|
||||
DCHECK_NE(second_day_delta, 0) << "start=" << start_time_90k
|
||||
<< ", end=" << end_time_90k;
|
||||
AdjustDay(buf, second_day_delta, days);
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
bool GetDayBounds(const std::string &day, int64_t *start_time_90k,
|
||||
int64_t *end_time_90k, std::string *error_message) {
|
||||
struct tm mytm;
|
||||
memset(&mytm, 0, sizeof(mytm));
|
||||
if (strptime(day.c_str(), kDayFmt, &mytm) != day.c_str() + day.size()) {
|
||||
*error_message = StrCat("Unparseable day: ", day);
|
||||
return false;
|
||||
}
|
||||
|
||||
mytm.tm_isdst = -1;
|
||||
mytm.tm_hour = 0;
|
||||
mytm.tm_min = 0;
|
||||
mytm.tm_sec = 0;
|
||||
*start_time_90k = kTimeUnitsPerSecond * static_cast<int64_t>(mktime(&mytm));
|
||||
|
||||
mytm.tm_isdst = -1;
|
||||
mytm.tm_hour = 0;
|
||||
mytm.tm_min = 0;
|
||||
mytm.tm_sec = 0;
|
||||
++mytm.tm_mday;
|
||||
*end_time_90k = kTimeUnitsPerSecond * static_cast<int64_t>(mktime(&mytm));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MoonfireDatabase::Init(Database *db, std::string *error_message) {
|
||||
CHECK(db_ == nullptr);
|
||||
db_ = db;
|
||||
|
||||
{
|
||||
DatabaseContext ctx(db_);
|
||||
|
||||
auto list_cameras_run = ctx.UseOnce(
|
||||
R"(
|
||||
select
|
||||
camera.id,
|
||||
camera.uuid,
|
||||
camera.short_name,
|
||||
camera.description,
|
||||
camera.host,
|
||||
camera.username,
|
||||
camera.password,
|
||||
camera.main_rtsp_path,
|
||||
camera.sub_rtsp_path,
|
||||
camera.retain_bytes
|
||||
from
|
||||
camera;
|
||||
)");
|
||||
while (list_cameras_run.Step() == SQLITE_ROW) {
|
||||
CameraData data;
|
||||
data.id = list_cameras_run.ColumnInt64(0);
|
||||
Uuid uuid;
|
||||
if (!uuid.ParseBinary(list_cameras_run.ColumnBlob(1))) {
|
||||
*error_message =
|
||||
StrCat("bad uuid ", ToHex(list_cameras_run.ColumnBlob(1)),
|
||||
" for camera id ", data.id);
|
||||
return false;
|
||||
}
|
||||
data.short_name = list_cameras_run.ColumnText(2).as_string();
|
||||
data.description = list_cameras_run.ColumnText(3).as_string();
|
||||
data.host = list_cameras_run.ColumnText(4).as_string();
|
||||
data.username = list_cameras_run.ColumnText(5).as_string();
|
||||
data.password = list_cameras_run.ColumnText(6).as_string();
|
||||
data.main_rtsp_path = list_cameras_run.ColumnText(7).as_string();
|
||||
data.sub_rtsp_path = list_cameras_run.ColumnText(8).as_string();
|
||||
data.retain_bytes = list_cameras_run.ColumnInt64(9);
|
||||
|
||||
auto ret = cameras_by_uuid_.insert(std::make_pair(uuid, data));
|
||||
if (!ret.second) {
|
||||
*error_message = StrCat("Duplicate camera uuid ", uuid.UnparseText());
|
||||
return false;
|
||||
}
|
||||
CameraData *data_p = &ret.first->second;
|
||||
if (!cameras_by_id_.insert(std::make_pair(data.id, data_p)).second) {
|
||||
*error_message = StrCat("Duplicate camera id ", data.id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (list_cameras_run.status() != SQLITE_DONE) {
|
||||
*error_message = StrCat("Camera list query failed: ",
|
||||
list_cameras_run.error_message());
|
||||
return false;
|
||||
}
|
||||
|
||||
// This query scans the entirety of the recording table's index.
|
||||
// It is quite slow, so the results are cached.
|
||||
auto list_recordings_run = ctx.UseOnce(
|
||||
R"(
|
||||
select
|
||||
recording.start_time_90k,
|
||||
recording.duration_90k,
|
||||
recording.sample_file_bytes,
|
||||
recording.camera_id
|
||||
from
|
||||
recording
|
||||
)");
|
||||
while (list_recordings_run.Step() == SQLITE_ROW) {
|
||||
int64_t start_time_90k = list_recordings_run.ColumnInt64(0);
|
||||
int64_t duration_90k = list_recordings_run.ColumnInt64(1);
|
||||
int64_t end_time_90k = start_time_90k + duration_90k;
|
||||
int64_t sample_file_bytes = list_recordings_run.ColumnInt64(2);
|
||||
int64_t camera_id = list_recordings_run.ColumnInt64(3);
|
||||
auto it = cameras_by_id_.find(camera_id);
|
||||
if (it == cameras_by_id_.end()) {
|
||||
*error_message =
|
||||
StrCat("Recording refers to unknown camera ", camera_id);
|
||||
return false;
|
||||
}
|
||||
CameraData *data = it->second;
|
||||
data->min_start_time_90k =
|
||||
std::min(data->min_start_time_90k, start_time_90k);
|
||||
data->max_end_time_90k = std::max(data->max_end_time_90k, end_time_90k);
|
||||
data->total_sample_file_bytes += sample_file_bytes;
|
||||
data->total_duration_90k += duration_90k;
|
||||
internal::AdjustDaysMap(start_time_90k, end_time_90k, 1, &data->days);
|
||||
}
|
||||
if (list_cameras_run.status() != SQLITE_DONE) {
|
||||
*error_message = StrCat("Recording list query failed: ",
|
||||
list_recordings_run.error_message());
|
||||
return false;
|
||||
}
|
||||
|
||||
// It's simplest to just keep the video sample entries in RAM.
|
||||
auto video_sample_entries_run = ctx.UseOnce(
|
||||
R"(
|
||||
select
|
||||
id,
|
||||
sha1,
|
||||
width,
|
||||
height,
|
||||
data
|
||||
from
|
||||
video_sample_entry
|
||||
)");
|
||||
while (video_sample_entries_run.Step() == SQLITE_ROW) {
|
||||
VideoSampleEntry entry;
|
||||
entry.id = video_sample_entries_run.ColumnInt64(0);
|
||||
entry.sha1 = video_sample_entries_run.ColumnBlob(1).as_string();
|
||||
int64_t width_tmp = video_sample_entries_run.ColumnInt64(2);
|
||||
int64_t height_tmp = video_sample_entries_run.ColumnInt64(3);
|
||||
auto max = std::numeric_limits<uint16_t>::max();
|
||||
if (width_tmp <= 0 || width_tmp > max || height_tmp <= 0 ||
|
||||
height_tmp > max) {
|
||||
*error_message =
|
||||
StrCat("video_sample_entry id ", entry.id, " width ", width_tmp,
|
||||
" / height ", height_tmp, " out of range.");
|
||||
return false;
|
||||
}
|
||||
entry.width = width_tmp;
|
||||
entry.height = height_tmp;
|
||||
entry.data = video_sample_entries_run.ColumnBlob(4).as_string();
|
||||
CHECK(
|
||||
video_sample_entries_.insert(std::make_pair(entry.id, entry)).second)
|
||||
<< "duplicate: " << entry.id;
|
||||
}
|
||||
}
|
||||
|
||||
std::string list_camera_recordings_sql = StrCat(
|
||||
R"(
|
||||
select
|
||||
recording.start_time_90k,
|
||||
recording.duration_90k,
|
||||
recording.video_samples,
|
||||
recording.sample_file_bytes,
|
||||
recording.video_sample_entry_id
|
||||
from
|
||||
recording
|
||||
where
|
||||
camera_id = :camera_id and
|
||||
recording.start_time_90k > :start_time_90k - )",
|
||||
kMaxRecordingDuration, " and\n",
|
||||
R"(
|
||||
recording.start_time_90k < :end_time_90k and
|
||||
recording.start_time_90k + recording.duration_90k > :start_time_90k
|
||||
order by
|
||||
recording.start_time_90k desc;)");
|
||||
list_camera_recordings_stmt_ =
|
||||
db_->Prepare(list_camera_recordings_sql, nullptr, error_message);
|
||||
if (!list_camera_recordings_stmt_.valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string build_mp4_sql = StrCat(
|
||||
R"(
|
||||
select
|
||||
recording.id,
|
||||
recording.start_time_90k,
|
||||
recording.duration_90k,
|
||||
recording.sample_file_bytes,
|
||||
recording.sample_file_uuid,
|
||||
recording.sample_file_sha1,
|
||||
recording.video_index,
|
||||
recording.video_samples,
|
||||
recording.video_sync_samples,
|
||||
recording.video_sample_entry_id
|
||||
from
|
||||
recording
|
||||
where
|
||||
camera_id = :camera_id and
|
||||
recording.start_time_90k > :start_time_90k - )",
|
||||
kMaxRecordingDuration, " and\n",
|
||||
R"(
|
||||
recording.start_time_90k < :end_time_90k and
|
||||
recording.start_time_90k + recording.duration_90k > :start_time_90k
|
||||
order by
|
||||
recording.start_time_90k;)");
|
||||
build_mp4_stmt_ = db_->Prepare(build_mp4_sql, nullptr, error_message);
|
||||
if (!build_mp4_stmt_.valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
insert_reservation_stmt_ = db_->Prepare(
|
||||
"insert into reserved_sample_files (uuid, state)\n"
|
||||
" values (:uuid, :state);",
|
||||
nullptr, error_message);
|
||||
if (!insert_reservation_stmt_.valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
delete_reservation_stmt_ =
|
||||
db_->Prepare("delete from reserved_sample_files where uuid = :uuid;",
|
||||
nullptr, error_message);
|
||||
if (!delete_reservation_stmt_.valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
insert_video_sample_entry_stmt_ = db_->Prepare(
|
||||
R"(
|
||||
insert into video_sample_entry (sha1, width, height, data)
|
||||
values (:sha1, :width, :height, :data);
|
||||
)",
|
||||
nullptr, error_message);
|
||||
if (!insert_video_sample_entry_stmt_.valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
insert_recording_stmt_ = db_->Prepare(
|
||||
R"(
|
||||
insert into recording (camera_id, sample_file_bytes, start_time_90k,
|
||||
duration_90k, local_time_delta_90k, video_samples,
|
||||
video_sync_samples, video_sample_entry_id,
|
||||
sample_file_uuid, sample_file_sha1, video_index)
|
||||
values (:camera_id, :sample_file_bytes, :start_time_90k,
|
||||
:duration_90k, :local_time_delta_90k,
|
||||
:video_samples, :video_sync_samples,
|
||||
:video_sample_entry_id, :sample_file_uuid,
|
||||
:sample_file_sha1, :video_index);
|
||||
)",
|
||||
nullptr, error_message);
|
||||
if (!insert_recording_stmt_.valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
list_oldest_sample_files_stmt_ = db_->Prepare(
|
||||
R"(
|
||||
select
|
||||
id,
|
||||
sample_file_uuid,
|
||||
start_time_90k,
|
||||
duration_90k,
|
||||
sample_file_bytes
|
||||
from
|
||||
recording
|
||||
where
|
||||
camera_id = :camera_id
|
||||
order by
|
||||
start_time_90k
|
||||
)",
|
||||
nullptr, error_message);
|
||||
if (!list_oldest_sample_files_stmt_.valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
delete_recording_stmt_ =
|
||||
db_->Prepare("delete from recording where id = :recording_id;", nullptr,
|
||||
error_message);
|
||||
if (!delete_recording_stmt_.valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
camera_min_start_stmt_ = db_->Prepare(
|
||||
R"(
|
||||
select
|
||||
start_time_90k
|
||||
from
|
||||
recording
|
||||
where
|
||||
camera_id = :camera_id
|
||||
order by start_time_90k limit 1;
|
||||
)",
|
||||
nullptr, error_message);
|
||||
if (!camera_min_start_stmt_.valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
camera_max_start_stmt_ = db_->Prepare(
|
||||
R"(
|
||||
select
|
||||
start_time_90k,
|
||||
duration_90k
|
||||
from
|
||||
recording
|
||||
where
|
||||
camera_id = :camera_id
|
||||
order by start_time_90k desc;
|
||||
)",
|
||||
nullptr, error_message);
|
||||
if (!camera_max_start_stmt_.valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MoonfireDatabase::ListCameras(
|
||||
std::function<IterationControl(const ListCamerasRow &)> cb) {
|
||||
DatabaseContext ctx(db_);
|
||||
ListCamerasRow row;
|
||||
for (const auto &entry : cameras_by_uuid_) {
|
||||
row.id = entry.second.id;
|
||||
row.uuid = entry.first;
|
||||
row.short_name = entry.second.short_name;
|
||||
row.description = entry.second.description;
|
||||
row.host = entry.second.host;
|
||||
row.username = entry.second.username;
|
||||
row.password = entry.second.password;
|
||||
row.main_rtsp_path = entry.second.main_rtsp_path;
|
||||
row.sub_rtsp_path = entry.second.sub_rtsp_path;
|
||||
row.retain_bytes = entry.second.retain_bytes;
|
||||
row.min_start_time_90k = entry.second.min_start_time_90k;
|
||||
row.max_end_time_90k = entry.second.max_end_time_90k;
|
||||
row.total_duration_90k = entry.second.total_duration_90k;
|
||||
row.total_sample_file_bytes = entry.second.total_sample_file_bytes;
|
||||
if (cb(row) == IterationControl::kBreak) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool MoonfireDatabase::GetCamera(Uuid camera_uuid, GetCameraRow *row) {
|
||||
DatabaseContext ctx(db_);
|
||||
const auto it = cameras_by_uuid_.find(camera_uuid);
|
||||
if (it == cameras_by_uuid_.end()) {
|
||||
return false;
|
||||
}
|
||||
const CameraData &data = it->second;
|
||||
row->short_name = data.short_name;
|
||||
row->description = data.description;
|
||||
row->retain_bytes = data.retain_bytes;
|
||||
row->min_start_time_90k = data.min_start_time_90k;
|
||||
row->max_end_time_90k = data.max_end_time_90k;
|
||||
row->total_duration_90k = data.total_duration_90k;
|
||||
row->total_sample_file_bytes = data.total_sample_file_bytes;
|
||||
row->days = data.days;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MoonfireDatabase::ListCameraRecordings(
|
||||
Uuid camera_uuid, int64_t start_time_90k, int64_t end_time_90k,
|
||||
std::function<IterationControl(const ListCameraRecordingsRow &)> cb,
|
||||
std::string *error_message) {
|
||||
DatabaseContext ctx(db_);
|
||||
const auto camera_it = cameras_by_uuid_.find(camera_uuid);
|
||||
if (camera_it == cameras_by_uuid_.end()) {
|
||||
*error_message = StrCat("no such camera ", camera_uuid.UnparseText());
|
||||
return false;
|
||||
}
|
||||
auto run = ctx.Borrow(&list_camera_recordings_stmt_);
|
||||
run.BindInt64(":camera_id", camera_it->second.id);
|
||||
run.BindInt64(":start_time_90k", start_time_90k);
|
||||
run.BindInt64(":end_time_90k", end_time_90k);
|
||||
ListCameraRecordingsRow row;
|
||||
while (run.Step() == SQLITE_ROW) {
|
||||
row.start_time_90k = run.ColumnInt64(0);
|
||||
row.end_time_90k = row.start_time_90k + run.ColumnInt64(1);
|
||||
row.video_samples = run.ColumnInt64(2);
|
||||
row.sample_file_bytes = run.ColumnInt64(3);
|
||||
int64_t video_sample_entry_id = run.ColumnInt64(4);
|
||||
const auto it = video_sample_entries_.find(video_sample_entry_id);
|
||||
if (it == video_sample_entries_.end()) {
|
||||
*error_message =
|
||||
StrCat("recording references invalid video sample entry ",
|
||||
video_sample_entry_id);
|
||||
return false;
|
||||
}
|
||||
const VideoSampleEntry &entry = it->second;
|
||||
row.video_sample_entry_sha1 = entry.sha1;
|
||||
row.width = entry.width;
|
||||
row.height = entry.height;
|
||||
if (cb(row) == IterationControl::kBreak) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (run.status() != SQLITE_DONE) {
|
||||
*error_message = StrCat("sqlite query failed: ", run.error_message());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MoonfireDatabase::ListMp4Recordings(
|
||||
Uuid camera_uuid, int64_t start_time_90k, int64_t end_time_90k,
|
||||
std::function<IterationControl(Recording &, const VideoSampleEntry &)>
|
||||
row_cb,
|
||||
std::string *error_message) {
|
||||
VLOG(1) << "...(1/4): Waiting for database lock";
|
||||
DatabaseContext ctx(db_);
|
||||
const auto it = cameras_by_uuid_.find(camera_uuid);
|
||||
if (it == cameras_by_uuid_.end()) {
|
||||
*error_message = StrCat("no such camera ", camera_uuid.UnparseText());
|
||||
return false;
|
||||
}
|
||||
const CameraData &data = it->second;
|
||||
VLOG(1) << "...(2/4): Querying database";
|
||||
auto run = ctx.Borrow(&build_mp4_stmt_);
|
||||
run.BindInt64(":camera_id", data.id);
|
||||
run.BindInt64(":end_time_90k", end_time_90k);
|
||||
run.BindInt64(":start_time_90k", start_time_90k);
|
||||
Recording recording;
|
||||
VideoSampleEntry sample_entry;
|
||||
while (run.Step() == SQLITE_ROW) {
|
||||
recording.id = run.ColumnInt64(0);
|
||||
recording.camera_id = data.id;
|
||||
recording.start_time_90k = run.ColumnInt64(1);
|
||||
recording.end_time_90k = recording.start_time_90k + run.ColumnInt64(2);
|
||||
recording.sample_file_bytes = run.ColumnInt64(3);
|
||||
if (!recording.sample_file_uuid.ParseBinary(run.ColumnBlob(4))) {
|
||||
*error_message =
|
||||
StrCat("recording ", recording.id, " has unparseable uuid ",
|
||||
ToHex(run.ColumnBlob(4)));
|
||||
return false;
|
||||
}
|
||||
recording.sample_file_sha1 = run.ColumnBlob(5).as_string();
|
||||
recording.video_index = run.ColumnBlob(6).as_string();
|
||||
recording.video_samples = run.ColumnInt64(7);
|
||||
recording.video_sync_samples = run.ColumnInt64(8);
|
||||
recording.video_sample_entry_id = run.ColumnInt64(9);
|
||||
|
||||
auto it = video_sample_entries_.find(recording.video_sample_entry_id);
|
||||
if (it == video_sample_entries_.end()) {
|
||||
*error_message = StrCat("recording ", recording.id,
|
||||
" references unknown video sample entry ",
|
||||
recording.video_sample_entry_id);
|
||||
return false;
|
||||
}
|
||||
const VideoSampleEntry &entry = it->second;
|
||||
|
||||
if (row_cb(recording, entry) == IterationControl::kBreak) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (run.status() != SQLITE_DONE && run.status() != SQLITE_ROW) {
|
||||
*error_message = StrCat("sqlite query failed: ", run.error_message());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MoonfireDatabase::ListReservedSampleFiles(std::vector<Uuid> *reserved,
|
||||
std::string *error_message) {
|
||||
reserved->clear();
|
||||
DatabaseContext ctx(db_);
|
||||
auto run = ctx.UseOnce("select uuid from reserved_sample_files;");
|
||||
while (run.Step() == SQLITE_ROW) {
|
||||
Uuid uuid;
|
||||
if (!uuid.ParseBinary(run.ColumnBlob(0))) {
|
||||
*error_message = StrCat("unparseable uuid ", ToHex(run.ColumnBlob(0)));
|
||||
return false;
|
||||
}
|
||||
reserved->push_back(uuid);
|
||||
}
|
||||
if (run.status() != SQLITE_DONE) {
|
||||
*error_message = run.error_message();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<Uuid> MoonfireDatabase::ReserveSampleFiles(
|
||||
int n, std::string *error_message) {
|
||||
if (n == 0) {
|
||||
return std::vector<Uuid>();
|
||||
}
|
||||
std::vector<Uuid> uuids;
|
||||
uuids.reserve(n);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
uuids.push_back(uuidgen_->Generate());
|
||||
}
|
||||
DatabaseContext ctx(db_);
|
||||
if (!ctx.BeginTransaction(error_message)) {
|
||||
return std::vector<Uuid>();
|
||||
}
|
||||
for (const auto &uuid : uuids) {
|
||||
auto run = ctx.Borrow(&insert_reservation_stmt_);
|
||||
run.BindBlob(":uuid", uuid.binary_view());
|
||||
run.BindInt64(":state", static_cast<int64_t>(ReservationState::kWriting));
|
||||
if (run.Step() != SQLITE_DONE) {
|
||||
ctx.RollbackTransaction();
|
||||
*error_message = run.error_message();
|
||||
return std::vector<Uuid>();
|
||||
}
|
||||
}
|
||||
if (!ctx.CommitTransaction(error_message)) {
|
||||
return std::vector<Uuid>();
|
||||
}
|
||||
return uuids;
|
||||
}
|
||||
|
||||
bool MoonfireDatabase::InsertVideoSampleEntry(VideoSampleEntry *entry,
|
||||
std::string *error_message) {
|
||||
if (entry->id != -1) {
|
||||
*error_message = StrCat("video_sample_entry already has id ", entry->id);
|
||||
return false;
|
||||
}
|
||||
DatabaseContext ctx(db_);
|
||||
for (const auto &some_entry : video_sample_entries_) {
|
||||
if (some_entry.second.sha1 == entry->sha1) {
|
||||
if (entry->width != some_entry.second.width ||
|
||||
entry->height != some_entry.second.height) {
|
||||
*error_message =
|
||||
StrCat("inconsistent entry for sha1 ", ToHex(entry->sha1),
|
||||
": existing entry has ", some_entry.second.width, "x",
|
||||
some_entry.second.height, ", new entry has ", entry->width,
|
||||
"x", entry->height);
|
||||
return false;
|
||||
}
|
||||
entry->id = some_entry.first;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
auto insert_run = ctx.Borrow(&insert_video_sample_entry_stmt_);
|
||||
insert_run.BindBlob(":sha1", entry->sha1);
|
||||
insert_run.BindInt64(":width", entry->width);
|
||||
insert_run.BindInt64(":height", entry->height);
|
||||
insert_run.BindBlob(":data", entry->data);
|
||||
if (insert_run.Step() != SQLITE_DONE) {
|
||||
*error_message =
|
||||
StrCat("insert video sample entry: ", insert_run.error_message(),
|
||||
": sha1=", ToHex(entry->sha1), ", dimensions=", entry->width,
|
||||
"x", entry->height, ", data=", ToHex(entry->data));
|
||||
return false;
|
||||
}
|
||||
entry->id = ctx.last_insert_rowid();
|
||||
CHECK(video_sample_entries_.insert(std::make_pair(entry->id, *entry)).second)
|
||||
<< "duplicate: " << entry->id;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MoonfireDatabase::InsertRecording(Recording *recording,
|
||||
std::string *error_message) {
|
||||
if (recording->id != -1) {
|
||||
*error_message = StrCat("recording already has id ", recording->id);
|
||||
return false;
|
||||
}
|
||||
if (recording->end_time_90k < recording->start_time_90k) {
|
||||
*error_message =
|
||||
StrCat("end time ", recording->end_time_90k, " must be >= start time ",
|
||||
recording->start_time_90k);
|
||||
return false;
|
||||
}
|
||||
DatabaseContext ctx(db_);
|
||||
auto it = cameras_by_id_.find(recording->camera_id);
|
||||
if (it == cameras_by_id_.end()) {
|
||||
*error_message = StrCat("no camera with id ", recording->camera_id);
|
||||
return false;
|
||||
}
|
||||
CameraData *camera_data = it->second;
|
||||
if (!ctx.BeginTransaction(error_message)) {
|
||||
return false;
|
||||
}
|
||||
auto delete_run = ctx.Borrow(&delete_reservation_stmt_);
|
||||
delete_run.BindBlob(":uuid", recording->sample_file_uuid.binary_view());
|
||||
if (delete_run.Step() != SQLITE_DONE) {
|
||||
*error_message = delete_run.error_message();
|
||||
ctx.RollbackTransaction();
|
||||
return false;
|
||||
}
|
||||
if (ctx.changes() != 1) {
|
||||
*error_message = StrCat("uuid ", recording->sample_file_uuid.UnparseText(),
|
||||
" is not reserved");
|
||||
ctx.RollbackTransaction();
|
||||
return false;
|
||||
}
|
||||
auto insert_run = ctx.Borrow(&insert_recording_stmt_);
|
||||
insert_run.BindInt64(":camera_id", recording->camera_id);
|
||||
insert_run.BindInt64(":sample_file_bytes", recording->sample_file_bytes);
|
||||
insert_run.BindInt64(":start_time_90k", recording->start_time_90k);
|
||||
insert_run.BindInt64(":duration_90k",
|
||||
recording->end_time_90k - recording->start_time_90k);
|
||||
insert_run.BindInt64(":local_time_delta_90k",
|
||||
recording->local_time_90k - recording->start_time_90k);
|
||||
insert_run.BindInt64(":video_samples", recording->video_samples);
|
||||
insert_run.BindInt64(":video_sync_samples", recording->video_sync_samples);
|
||||
insert_run.BindInt64(":video_sample_entry_id",
|
||||
recording->video_sample_entry_id);
|
||||
insert_run.BindBlob(":sample_file_uuid",
|
||||
recording->sample_file_uuid.binary_view());
|
||||
insert_run.BindBlob(":sample_file_sha1", recording->sample_file_sha1);
|
||||
insert_run.BindBlob(":video_index", recording->video_index);
|
||||
if (insert_run.Step() != SQLITE_DONE) {
|
||||
*error_message =
|
||||
StrCat("insert failed: ", insert_run.error_message(), ", camera_id=",
|
||||
recording->camera_id, ", sample_file_bytes=",
|
||||
recording->sample_file_bytes, ", start_time_90k=",
|
||||
recording->start_time_90k, ", duration_90k=",
|
||||
recording->end_time_90k - recording->start_time_90k,
|
||||
", local_time_delta_90k=",
|
||||
recording->local_time_90k - recording->start_time_90k,
|
||||
", video_samples=", recording->video_samples,
|
||||
", video_sync_samples=", recording->video_sync_samples,
|
||||
", video_sample_entry_id=", recording->video_sample_entry_id,
|
||||
", sample_file_uuid=", recording->sample_file_uuid.UnparseText(),
|
||||
", sample_file_sha1=", ToHex(recording->sample_file_sha1),
|
||||
", video_index length ", recording->video_index.size());
|
||||
ctx.RollbackTransaction();
|
||||
return false;
|
||||
}
|
||||
if (!ctx.CommitTransaction(error_message)) {
|
||||
LOG(ERROR) << "commit failed";
|
||||
return false;
|
||||
}
|
||||
recording->id = ctx.last_insert_rowid();
|
||||
if (camera_data->min_start_time_90k == -1 ||
|
||||
camera_data->min_start_time_90k > recording->start_time_90k) {
|
||||
camera_data->min_start_time_90k = recording->start_time_90k;
|
||||
}
|
||||
if (camera_data->max_end_time_90k == -1 ||
|
||||
camera_data->max_end_time_90k < recording->end_time_90k) {
|
||||
camera_data->max_end_time_90k = recording->end_time_90k;
|
||||
}
|
||||
camera_data->total_duration_90k +=
|
||||
recording->end_time_90k - recording->start_time_90k;
|
||||
camera_data->total_sample_file_bytes += recording->sample_file_bytes;
|
||||
internal::AdjustDaysMap(recording->start_time_90k, recording->end_time_90k, 1,
|
||||
&camera_data->days);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MoonfireDatabase::ListOldestSampleFiles(
|
||||
Uuid camera_uuid,
|
||||
std::function<IterationControl(const ListOldestSampleFilesRow &)> row_cb,
|
||||
std::string *error_message) {
|
||||
DatabaseContext ctx(db_);
|
||||
auto it = cameras_by_uuid_.find(camera_uuid);
|
||||
if (it == cameras_by_uuid_.end()) {
|
||||
*error_message = StrCat("no such camera ", camera_uuid.UnparseText());
|
||||
return false;
|
||||
}
|
||||
const CameraData &camera_data = it->second;
|
||||
auto run = ctx.Borrow(&list_oldest_sample_files_stmt_);
|
||||
run.BindInt64(":camera_id", camera_data.id);
|
||||
ListOldestSampleFilesRow row;
|
||||
while (run.Step() == SQLITE_ROW) {
|
||||
row.camera_id = camera_data.id;
|
||||
row.recording_id = run.ColumnInt64(0);
|
||||
if (!row.sample_file_uuid.ParseBinary(run.ColumnBlob(1))) {
|
||||
*error_message =
|
||||
StrCat("recording ", row.recording_id, " has unparseable uuid ",
|
||||
ToHex(run.ColumnBlob(1)));
|
||||
return false;
|
||||
}
|
||||
row.start_time_90k = run.ColumnInt64(2);
|
||||
row.duration_90k = run.ColumnInt64(3);
|
||||
row.sample_file_bytes = run.ColumnInt64(4);
|
||||
if (row_cb(row) == IterationControl::kBreak) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (run.status() != SQLITE_DONE) {
|
||||
*error_message = run.error_message();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MoonfireDatabase::DeleteRecordings(
|
||||
const std::vector<ListOldestSampleFilesRow> &recordings,
|
||||
std::string *error_message) {
|
||||
if (recordings.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
DatabaseContext ctx(db_);
|
||||
if (!ctx.BeginTransaction(error_message)) {
|
||||
return false;
|
||||
}
|
||||
struct State {
|
||||
int64_t deleted_duration_90k = 0;
|
||||
int64_t deleted_sample_file_bytes = 0;
|
||||
int64_t min_start_time_90k = -1;
|
||||
int64_t max_end_time_90k = -1;
|
||||
std::map<std::string, int64_t> days;
|
||||
CameraData *camera_data = nullptr;
|
||||
};
|
||||
std::map<int64_t, State> state_by_camera_id;
|
||||
for (const auto &recording : recordings) {
|
||||
State &state = state_by_camera_id[recording.camera_id];
|
||||
state.deleted_duration_90k += recording.duration_90k;
|
||||
state.deleted_sample_file_bytes += recording.sample_file_bytes;
|
||||
internal::AdjustDaysMap(recording.start_time_90k,
|
||||
recording.start_time_90k + recording.duration_90k,
|
||||
1, &state.days);
|
||||
|
||||
auto delete_run = ctx.Borrow(&delete_recording_stmt_);
|
||||
delete_run.BindInt64(":recording_id", recording.recording_id);
|
||||
if (delete_run.Step() != SQLITE_DONE) {
|
||||
ctx.RollbackTransaction();
|
||||
*error_message = StrCat("delete: ", delete_run.error_message());
|
||||
return false;
|
||||
}
|
||||
if (ctx.changes() != 1) {
|
||||
ctx.RollbackTransaction();
|
||||
*error_message = StrCat("no such recording ", recording.recording_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto insert_run = ctx.Borrow(&insert_reservation_stmt_);
|
||||
insert_run.BindBlob(":uuid", recording.sample_file_uuid.binary_view());
|
||||
insert_run.BindInt64(":state",
|
||||
static_cast<int64_t>(ReservationState::kDeleting));
|
||||
if (insert_run.Step() != SQLITE_DONE) {
|
||||
ctx.RollbackTransaction();
|
||||
*error_message = StrCat("insert: ", insert_run.error_message());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Recompute start and end times for each camera.
|
||||
for (auto &state_entry : state_by_camera_id) {
|
||||
int64_t camera_id = state_entry.first;
|
||||
State &state = state_entry.second;
|
||||
auto it = cameras_by_id_.find(camera_id);
|
||||
if (it == cameras_by_id_.end()) {
|
||||
*error_message =
|
||||
StrCat("internal error; can't find camera id ", camera_id);
|
||||
return false;
|
||||
}
|
||||
state.camera_data = it->second;
|
||||
|
||||
// The minimum is straightforward, taking advantage of the start_time_90k
|
||||
// index for speed.
|
||||
auto min_run = ctx.Borrow(&camera_min_start_stmt_);
|
||||
min_run.BindInt64(":camera_id", camera_id);
|
||||
if (min_run.Step() == SQLITE_ROW) {
|
||||
state.min_start_time_90k = min_run.ColumnInt64(0);
|
||||
} else if (min_run.Step() == SQLITE_DONE) {
|
||||
// There are no recordings left.
|
||||
state.min_start_time_90k = std::numeric_limits<int64_t>::max();
|
||||
state.max_end_time_90k = std::numeric_limits<int64_t>::min();
|
||||
continue; // skip additional query below to calculate max.
|
||||
} else {
|
||||
ctx.RollbackTransaction();
|
||||
*error_message = StrCat("min: ", min_run.error_message());
|
||||
return false;
|
||||
}
|
||||
|
||||
// The maximum is less straightforward in the case of overlap - all
|
||||
// recordings starting in the last kMaxRecordingDuration must be examined
|
||||
// to take advantage of the start_time_90k index.
|
||||
auto max_run = ctx.Borrow(&camera_max_start_stmt_);
|
||||
max_run.BindInt64(":camera_id", camera_id);
|
||||
if (max_run.Step() != SQLITE_ROW) {
|
||||
// If there was a min row, there should be a max row too, so this is an
|
||||
// error even in the SQLITE_DONE case.
|
||||
ctx.RollbackTransaction();
|
||||
*error_message = StrCat("max[0]: ", max_run.error_message());
|
||||
return false;
|
||||
}
|
||||
int64_t max_start_90k = max_run.ColumnInt64(0);
|
||||
do {
|
||||
auto end_time_90k = max_run.ColumnInt64(0) + max_run.ColumnInt64(1);
|
||||
state.max_end_time_90k = std::max(state.max_end_time_90k, end_time_90k);
|
||||
} while (max_run.Step() == SQLITE_ROW &&
|
||||
max_run.ColumnInt64(0) > max_start_90k - kMaxRecordingDuration);
|
||||
if (max_run.status() != SQLITE_DONE && max_run.status() != SQLITE_ROW) {
|
||||
*error_message = StrCat("max[1]: ", max_run.error_message());
|
||||
ctx.RollbackTransaction();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ctx.CommitTransaction(error_message)) {
|
||||
*error_message = StrCat("commit: ", *error_message);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto &state_entry : state_by_camera_id) {
|
||||
State &state = state_entry.second;
|
||||
state.camera_data->total_duration_90k -= state.deleted_duration_90k;
|
||||
state.camera_data->total_sample_file_bytes -=
|
||||
state.deleted_sample_file_bytes;
|
||||
state.camera_data->min_start_time_90k = state.min_start_time_90k;
|
||||
state.camera_data->max_end_time_90k = state.max_end_time_90k;
|
||||
for (const auto &day : state.days) {
|
||||
AdjustDay(day.first, -day.second, &state.camera_data->days);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MoonfireDatabase::MarkSampleFilesDeleted(const std::vector<Uuid> &uuids,
|
||||
std::string *error_message) {
|
||||
if (uuids.empty()) {
|
||||
return true;
|
||||
}
|
||||
DatabaseContext ctx(db_);
|
||||
if (!ctx.BeginTransaction(error_message)) {
|
||||
return false;
|
||||
}
|
||||
for (const auto &uuid : uuids) {
|
||||
auto run = ctx.Borrow(&delete_reservation_stmt_);
|
||||
run.BindBlob(":uuid", uuid.binary_view());
|
||||
if (run.Step() != SQLITE_DONE) {
|
||||
*error_message = run.error_message();
|
||||
ctx.RollbackTransaction();
|
||||
return false;
|
||||
}
|
||||
if (ctx.changes() != 1) {
|
||||
*error_message = StrCat("no reservation for uuid ", uuid.UnparseText());
|
||||
ctx.RollbackTransaction();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!ctx.CommitTransaction(error_message)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace moonfire_nvr
|
@ -1,279 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// moonfire-db.h: database access logic for the Moonfire NVR SQLite schema.
|
||||
// Currently focused on stuff needed by WebInterface to build a HTML or JSON
|
||||
// interface.
|
||||
//
|
||||
// This caches data in RAM, making the assumption that only one process is
|
||||
// accessing the database at a time. (TODO: enforce with flock or some such.)
|
||||
// Performance and efficiency notes:
|
||||
//
|
||||
// * several query operations here feature row callbacks. The callback is
|
||||
// invoked with the database lock. Thus, the caller mustn't perform database
|
||||
// operations or other long-running operations.
|
||||
//
|
||||
// * startup may be slow, as it scans the entire index for the recording
|
||||
// table. This seems acceptable.
|
||||
//
|
||||
// * the operations used for web file serving should return results with
|
||||
// acceptable latency.
|
||||
//
|
||||
// * however, the database lock may be held for longer than is acceptable for
|
||||
// the critical path of recording frames. It may be necessary to preallocate
|
||||
// sample file uuids and such to avoid this.
|
||||
//
|
||||
// * the caller may need to perform several different types of write
|
||||
// operations in a row. It might be worth creating an interface for batching
|
||||
// these inside a transaction, to reduce latency and SSD write cycles. The
|
||||
// pre-commit and post-commit logic of each operation would have to be
|
||||
// pulled apart, with the latter being called by this wrapper class on
|
||||
// commit of the overall transaction.
|
||||
|
||||
#ifndef MOONFIRE_NVR_MOONFIRE_DB_H
|
||||
#define MOONFIRE_NVR_MOONFIRE_DB_H
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "common.h"
|
||||
#include "http.h"
|
||||
#include "mp4.h"
|
||||
#include "sqlite.h"
|
||||
#include "uuid.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
// For use with MoonfireDatabase::ListCameras.
|
||||
struct ListCamerasRow {
|
||||
int64_t id = -1;
|
||||
Uuid uuid;
|
||||
std::string short_name;
|
||||
std::string description;
|
||||
std::string host;
|
||||
std::string username;
|
||||
std::string password;
|
||||
std::string main_rtsp_path;
|
||||
std::string sub_rtsp_path;
|
||||
int64_t retain_bytes = -1;
|
||||
|
||||
// Aggregates summarizing completed recordings.
|
||||
int64_t min_start_time_90k = -1;
|
||||
int64_t max_end_time_90k = -1;
|
||||
int64_t total_duration_90k = -1;
|
||||
int64_t total_sample_file_bytes = -1;
|
||||
};
|
||||
|
||||
// For use with MoonfireDatabase::GetCamera.
|
||||
// This includes everything in ListCamerasRow. In the future, it will include
|
||||
// more data. Likely, that will mean a list of calendar days (in the system
|
||||
// time zone) in which there is any data.
|
||||
struct GetCameraRow {
|
||||
std::string short_name;
|
||||
std::string description;
|
||||
int64_t retain_bytes = -1;
|
||||
int64_t min_start_time_90k = -1;
|
||||
int64_t max_end_time_90k = -1;
|
||||
int64_t total_duration_90k = -1;
|
||||
int64_t total_sample_file_bytes = -1;
|
||||
std::map<std::string, int64_t> days; // YYYY-mm-dd -> duration_90k.
|
||||
};
|
||||
|
||||
// For use with MoonfireDatabase::ListCameraRecordings.
|
||||
struct ListCameraRecordingsRow {
|
||||
// From the recording table.
|
||||
int64_t start_time_90k = -1;
|
||||
int64_t end_time_90k = -1;
|
||||
int64_t video_samples = -1;
|
||||
int64_t sample_file_bytes = -1;
|
||||
|
||||
// Joined from the video_sample_entry table.
|
||||
// |video_sample_entry_sha1| is valid as long as the MoonfireDatabase.
|
||||
re2::StringPiece video_sample_entry_sha1;
|
||||
uint16_t width = 0;
|
||||
uint16_t height = 0;
|
||||
};
|
||||
|
||||
// For use with MoonfireDatabase::ListOldestSampleFiles.
|
||||
struct ListOldestSampleFilesRow {
|
||||
int64_t camera_id = -1;
|
||||
int64_t recording_id = -1;
|
||||
Uuid sample_file_uuid;
|
||||
int64_t start_time_90k = -1;
|
||||
int64_t duration_90k = -1;
|
||||
int64_t sample_file_bytes = -1;
|
||||
};
|
||||
|
||||
// Thread-safe after Init.
|
||||
// (Uses a DatabaseContext for locking.)
|
||||
class MoonfireDatabase {
|
||||
public:
|
||||
MoonfireDatabase() {}
|
||||
MoonfireDatabase(const MoonfireDatabase &) = delete;
|
||||
void operator=(const MoonfireDatabase &) = delete;
|
||||
|
||||
// |db| must outlive the MoonfireDatabase.
|
||||
bool Init(Database *db, std::string *error_message);
|
||||
|
||||
// List all cameras in the system, ordered by short name.
|
||||
void ListCameras(std::function<IterationControl(const ListCamerasRow &)> cb);
|
||||
|
||||
// Get a single camera.
|
||||
// Return true iff the camera exists.
|
||||
bool GetCamera(Uuid camera_uuid, GetCameraRow *row);
|
||||
|
||||
// List all recordings associated with a camera, descending by end time.
|
||||
bool ListCameraRecordings(
|
||||
Uuid camera_uuid, int64_t start_time_90k, int64_t end_time_90k,
|
||||
std::function<IterationControl(const ListCameraRecordingsRow &)>,
|
||||
std::string *error_message);
|
||||
|
||||
bool ListMp4Recordings(
|
||||
Uuid camera_uuid, int64_t start_time_90k, int64_t end_time_90k,
|
||||
std::function<IterationControl(Recording &, const VideoSampleEntry &)>
|
||||
row_cb,
|
||||
std::string *error_message);
|
||||
|
||||
bool ListReservedSampleFiles(std::vector<Uuid> *reserved,
|
||||
std::string *error_message);
|
||||
|
||||
// Reserve |n| new sample file uuids.
|
||||
// Returns an empty vector on error.
|
||||
std::vector<Uuid> ReserveSampleFiles(int n, std::string *error_message);
|
||||
|
||||
// Insert a video sample entry if not already inserted.
|
||||
// On success, |entry->id| is filled in with the id of a freshly-created or
|
||||
// existing row.
|
||||
bool InsertVideoSampleEntry(VideoSampleEntry *entry,
|
||||
std::string *error_message);
|
||||
|
||||
// Insert a new recording.
|
||||
// The uuid must have been already reserved with ReserveSampleFileUuid above.
|
||||
// On success, |recording->id| is filled in.
|
||||
bool InsertRecording(Recording *recording, std::string *error_message);
|
||||
|
||||
// List sample files, starting from the oldest.
|
||||
// The caller is expected to supply a |row_cb| that returns kBreak when
|
||||
// enough have been listed.
|
||||
bool ListOldestSampleFiles(
|
||||
Uuid camera_uuid,
|
||||
std::function<IterationControl(const ListOldestSampleFilesRow &)> row_cb,
|
||||
std::string *error_message);
|
||||
|
||||
// Delete recording rows, moving their sample file uuids to the deleting
|
||||
// state.
|
||||
bool DeleteRecordings(const std::vector<ListOldestSampleFilesRow> &rows,
|
||||
std::string *error_message);
|
||||
|
||||
// Mark a set of sample files as deleted.
|
||||
// This shouldn't be called until the files have been unlinke()ed and the
|
||||
// parent directory fsync()ed.
|
||||
// Returns error if any sample files are not in the deleting state.
|
||||
bool MarkSampleFilesDeleted(const std::vector<Uuid> &uuids,
|
||||
std::string *error_message);
|
||||
|
||||
// Replace the default real UUID generator with the supplied one.
|
||||
// Exposed only for testing; not thread-safe.
|
||||
void SetUuidGeneratorForTesting(UuidGenerator *uuidgen) {
|
||||
uuidgen_ = uuidgen;
|
||||
}
|
||||
|
||||
private:
|
||||
struct CameraData {
|
||||
// Cached values of the matching fields from the camera row.
|
||||
int64_t id = -1;
|
||||
std::string short_name;
|
||||
std::string description;
|
||||
std::string host;
|
||||
std::string username;
|
||||
std::string password;
|
||||
std::string main_rtsp_path;
|
||||
std::string sub_rtsp_path;
|
||||
int64_t retain_bytes = -1;
|
||||
|
||||
// Aggregates of all recordings associated with the camera.
|
||||
int64_t min_start_time_90k = std::numeric_limits<int64_t>::max();
|
||||
int64_t max_end_time_90k = std::numeric_limits<int64_t>::min();
|
||||
int64_t total_sample_file_bytes = 0;
|
||||
int64_t total_duration_90k = 0;
|
||||
|
||||
// A map of calendar days (in the local timezone, "YYYY-mm-DD") to the
|
||||
// total duration (in 90k units) of recorded data in the day. A day is
|
||||
// present in the map ff the value is non-zero.
|
||||
std::map<std::string, int64_t> days;
|
||||
};
|
||||
|
||||
enum class ReservationState { kWriting = 0, kDeleting = 1 };
|
||||
|
||||
// Efficiently (re-)compute the bounds of recorded time for a given camera.
|
||||
bool ComputeCameraRecordingBounds(DatabaseContext *ctx, int64_t camera_id,
|
||||
int64_t *min_start_time_90k,
|
||||
int64_t *max_end_time_90k,
|
||||
std::string *error_message);
|
||||
|
||||
Database *db_ = nullptr;
|
||||
UuidGenerator *uuidgen_ = GetRealUuidGenerator();
|
||||
Statement list_camera_recordings_stmt_;
|
||||
Statement build_mp4_stmt_;
|
||||
Statement insert_reservation_stmt_;
|
||||
Statement delete_reservation_stmt_;
|
||||
Statement insert_video_sample_entry_stmt_;
|
||||
Statement insert_recording_stmt_;
|
||||
Statement list_oldest_sample_files_stmt_;
|
||||
Statement delete_recording_stmt_;
|
||||
Statement camera_min_start_stmt_;
|
||||
Statement camera_max_start_stmt_;
|
||||
|
||||
std::map<Uuid, CameraData> cameras_by_uuid_;
|
||||
std::map<int64_t, CameraData *> cameras_by_id_;
|
||||
std::map<int64_t, VideoSampleEntry> video_sample_entries_;
|
||||
};
|
||||
|
||||
// Given a key in the day-to-duration map, produce the start and end times of
|
||||
// the day. (Typically the end time is 24 hours later than the start; but it's
|
||||
// 23 or 25 hours for the days of spring forward and fall back, respectively.)
|
||||
bool GetDayBounds(const std::string &day, int64_t *start_time_90k,
|
||||
int64_t *end_time_90k, std::string *error_message);
|
||||
|
||||
namespace internal {
|
||||
|
||||
// Adjust a day-to-duration map (see MoonfireDatabase::CameraData::days_)
|
||||
// to reflect a recording.
|
||||
void AdjustDaysMap(int64_t start_time_90k, int64_t end_time_90k, int sign,
|
||||
std::map<std::string, int64_t> *days);
|
||||
|
||||
} // namespace internal
|
||||
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
#endif // MOONFIRE_NVR_MOONFIRE_DB_H
|
@ -1,237 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// moonfire-nvr-main.cc: main program. This should be kept as short as
|
||||
// practical, so that individual parts of the program can be tested with the
|
||||
// googletest framework.
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/event.h>
|
||||
#include <event2/event_struct.h>
|
||||
#include <event2/http.h>
|
||||
#include <gflags/gflags.h>
|
||||
#include <glog/logging.h>
|
||||
|
||||
#include "ffmpeg.h"
|
||||
#include "profiler.h"
|
||||
#include "moonfire-db.h"
|
||||
#include "moonfire-nvr.h"
|
||||
#include "sqlite.h"
|
||||
#include "string.h"
|
||||
#include "web.h"
|
||||
|
||||
using moonfire_nvr::StrCat;
|
||||
|
||||
DEFINE_int32(http_port, 0, "");
|
||||
DEFINE_string(db_dir, "", "");
|
||||
DEFINE_string(sample_file_dir, "", "");
|
||||
DEFINE_bool(read_only, false, "");
|
||||
|
||||
namespace {
|
||||
|
||||
const struct timeval kLogFlushInterval = {1, 0};
|
||||
|
||||
struct event_base* base;
|
||||
|
||||
void EventLogCallback(int severity, const char* msg) {
|
||||
int vlog_level = 0;
|
||||
google::LogSeverity glog_level;
|
||||
if (severity <= EVENT_LOG_DEBUG) {
|
||||
vlog_level = 1;
|
||||
glog_level = google::GLOG_INFO;
|
||||
} else if (severity <= EVENT_LOG_MSG) {
|
||||
glog_level = google::GLOG_INFO;
|
||||
} else if (severity <= EVENT_LOG_WARN) {
|
||||
glog_level = google::GLOG_WARNING;
|
||||
} else {
|
||||
glog_level = google::GLOG_ERROR;
|
||||
}
|
||||
|
||||
if (vlog_level > 0 && !VLOG_IS_ON(vlog_level)) {
|
||||
return;
|
||||
}
|
||||
google::LogMessage("libevent", 0, glog_level).stream() << msg;
|
||||
}
|
||||
|
||||
// Called on SIGTERM or SIGINT.
|
||||
void SignalCallback(evutil_socket_t, short, void*) {
|
||||
event_base_loopexit(base, nullptr);
|
||||
}
|
||||
|
||||
void FlushLogsCallback(evutil_socket_t, short, void* ev) {
|
||||
google::FlushLogFiles(google::GLOG_INFO);
|
||||
CHECK_EQ(0,
|
||||
event_add(reinterpret_cast<struct event*>(ev), &kLogFlushInterval));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Note that main never returns; it calls exit on either success or failure.
|
||||
// This avoids the need to design an orderly shutdown for all dependencies,
|
||||
// instead letting the OS clean up memory allocations en masse. State may be
|
||||
// allocated in whatever way is most convenient: on the stack, in a unique_ptr
|
||||
// (that may never go out of scope), or as a bare pointer that is never
|
||||
// deleted.
|
||||
int main(int argc, char** argv) {
|
||||
google::ParseCommandLineFlags(&argc, &argv, true);
|
||||
google::InitGoogleLogging(argv[0]);
|
||||
google::InstallFailureSignalHandler();
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
if (FLAGS_sample_file_dir.empty()) {
|
||||
LOG(ERROR) << "--sample_file_dir must be specified; exiting.";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (FLAGS_db_dir.empty()) {
|
||||
LOG(ERROR) << "--db_dir must be specified; exiting.";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (FLAGS_http_port == 0) {
|
||||
LOG(ERROR) << "--http_port must be specified; exiting.";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
moonfire_nvr::Environment env;
|
||||
env.clock = moonfire_nvr::GetRealClock();
|
||||
env.video_source = moonfire_nvr::GetRealVideoSource();
|
||||
|
||||
std::unique_ptr<moonfire_nvr::File> sample_file_dir;
|
||||
std::string sample_file_dirname = FLAGS_sample_file_dir;
|
||||
int ret = moonfire_nvr::GetRealFilesystem()->Open(
|
||||
sample_file_dirname.c_str(), O_DIRECTORY | O_RDONLY, &sample_file_dir);
|
||||
if (ret != 0) {
|
||||
LOG(ERROR) << "Unable to open --sample_file_dir=" << sample_file_dirname
|
||||
<< ": " << strerror(ret) << "; exiting.";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Separately, ensure the sample file directory is writable.
|
||||
// (Opening the directory above with O_DIRECTORY|O_RDWR doesn't work even
|
||||
// when the directory is writable; it fails with EISDIR.)
|
||||
ret = sample_file_dir->Access(".", W_OK, 0);
|
||||
if (ret != 0) {
|
||||
LOG(ERROR) << "--sample_file_dir=" << sample_file_dirname
|
||||
<< " is not writable: " << strerror(ret) << "; exiting.";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
env.sample_file_dir = sample_file_dir.release();
|
||||
|
||||
std::unique_ptr<moonfire_nvr::File> db_dir;
|
||||
std::string db_dirname = FLAGS_db_dir;
|
||||
ret = moonfire_nvr::GetRealFilesystem()->Open(
|
||||
db_dirname.c_str(), O_DIRECTORY | O_RDONLY, &db_dir);
|
||||
if (ret != 0) {
|
||||
LOG(ERROR) << "Unable to open --db_dir=" << db_dirname << ": "
|
||||
<< strerror(ret) << "; exiting.";
|
||||
exit(1);
|
||||
}
|
||||
bool read_only = FLAGS_read_only;
|
||||
ret = db_dir->Lock((read_only ? LOCK_SH : LOCK_EX) | LOCK_NB);
|
||||
if (ret != 0) {
|
||||
LOG(ERROR) << "Unable to lock --db_dir=" << db_dirname << ": "
|
||||
<< strerror(ret) << "; exiting.";
|
||||
exit(1);
|
||||
}
|
||||
moonfire_nvr::Database db;
|
||||
std::string error_msg;
|
||||
std::string db_path = StrCat(FLAGS_db_dir, "/db");
|
||||
if (!db.Open(db_path.c_str(),
|
||||
read_only ? SQLITE_OPEN_READONLY : SQLITE_OPEN_READWRITE,
|
||||
&error_msg)) {
|
||||
LOG(ERROR) << error_msg << "; exiting.";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
moonfire_nvr::MoonfireDatabase mdb;
|
||||
CHECK(mdb.Init(&db, &error_msg)) << error_msg;
|
||||
env.mdb = &mdb;
|
||||
|
||||
moonfire_nvr::WebInterface web(&env);
|
||||
|
||||
event_set_log_callback(&EventLogCallback);
|
||||
LOG(INFO) << "libevent: compiled with version " << LIBEVENT_VERSION
|
||||
<< ", running with version " << event_get_version();
|
||||
base = CHECK_NOTNULL(event_base_new());
|
||||
|
||||
std::unique_ptr<moonfire_nvr::Nvr> nvr;
|
||||
if (!read_only) {
|
||||
nvr.reset(new moonfire_nvr::Nvr(&env));
|
||||
if (!nvr->Init(&error_msg)) {
|
||||
LOG(ERROR) << "Unable to initialize: " << error_msg << "; exiting.";
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
evhttp* http = CHECK_NOTNULL(evhttp_new(base));
|
||||
moonfire_nvr::RegisterProfiler(base, http);
|
||||
web.Register(http);
|
||||
if (evhttp_bind_socket(http, "0.0.0.0", FLAGS_http_port) != 0) {
|
||||
LOG(ERROR) << "Unable to bind to --http_port=" << FLAGS_http_port
|
||||
<< "; exiting.";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Register for termination signals.
|
||||
struct event ev_sigterm;
|
||||
struct event ev_sigint;
|
||||
CHECK_EQ(0, event_assign(&ev_sigterm, base, SIGTERM, EV_SIGNAL | EV_PERSIST,
|
||||
&SignalCallback, nullptr));
|
||||
CHECK_EQ(0, event_assign(&ev_sigint, base, SIGINT, EV_SIGNAL | EV_PERSIST,
|
||||
&SignalCallback, nullptr));
|
||||
CHECK_EQ(0, event_add(&ev_sigterm, nullptr));
|
||||
CHECK_EQ(0, event_add(&ev_sigint, nullptr));
|
||||
|
||||
// Flush the logfiles regularly for debuggability.
|
||||
struct event ev_flushlogs;
|
||||
CHECK_EQ(0, event_assign(&ev_flushlogs, base, 0, 0, &FlushLogsCallback,
|
||||
&ev_flushlogs));
|
||||
CHECK_EQ(0, event_add(&ev_flushlogs, &kLogFlushInterval));
|
||||
|
||||
// Wait for events.
|
||||
LOG(INFO) << "Main thread entering event loop.";
|
||||
CHECK_EQ(0, event_base_loop(base, 0));
|
||||
|
||||
LOG(INFO) << "Shutting down.";
|
||||
google::FlushLogFiles(google::GLOG_INFO);
|
||||
nvr.reset();
|
||||
LOG(INFO) << "Done.";
|
||||
google::ShutdownGoogleLogging();
|
||||
exit(0);
|
||||
}
|
@ -1,423 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// moonfire-nvr-test.cc: tests of the moonfire-nvr.cc interface.
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "moonfire-nvr.h"
|
||||
#include "string.h"
|
||||
#include "testutil.h"
|
||||
|
||||
DECLARE_bool(alsologtostderr);
|
||||
|
||||
using testing::_;
|
||||
using testing::AnyNumber;
|
||||
using testing::HasSubstr;
|
||||
using testing::Invoke;
|
||||
using testing::Return;
|
||||
|
||||
namespace moonfire_nvr {
|
||||
namespace {
|
||||
|
||||
class MockVideoSource : public VideoSource {
|
||||
public:
|
||||
// Proxy, as gmock doesn't support non-copyable return values.
|
||||
std::unique_ptr<InputVideoPacketStream> OpenRtsp(
|
||||
const std::string &url, std::string *error_message) final {
|
||||
return std::unique_ptr<InputVideoPacketStream>(
|
||||
OpenRtspRaw(url, error_message));
|
||||
}
|
||||
std::unique_ptr<InputVideoPacketStream> OpenFile(
|
||||
const std::string &file, std::string *error_message) final {
|
||||
return std::unique_ptr<InputVideoPacketStream>(
|
||||
OpenFileRaw(file, error_message));
|
||||
}
|
||||
|
||||
MOCK_METHOD2(OpenRtspRaw,
|
||||
InputVideoPacketStream *(const std::string &, std::string *));
|
||||
MOCK_METHOD2(OpenFileRaw,
|
||||
InputVideoPacketStream *(const std::string &, std::string *));
|
||||
};
|
||||
|
||||
class StreamTest : public testing::Test {
|
||||
public:
|
||||
StreamTest() {
|
||||
std::string error_message;
|
||||
test_dir_ = PrepareTempDirOrDie("moonfire-nvr-stream-copier");
|
||||
env_.clock = &clock_;
|
||||
env_.video_source = &video_source_;
|
||||
int ret = moonfire_nvr::GetRealFilesystem()->Open(
|
||||
test_dir_.c_str(), O_DIRECTORY | O_RDONLY, &sample_file_dir_);
|
||||
CHECK_EQ(0, ret) << "open: " << strerror(ret);
|
||||
env_.sample_file_dir = sample_file_dir_.get();
|
||||
|
||||
CHECK(db_.Open(StrCat(test_dir_, "/db").c_str(),
|
||||
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, &error_message))
|
||||
<< error_message;
|
||||
std::string create_sql = ReadFileOrDie("../src/schema.sql");
|
||||
{
|
||||
DatabaseContext ctx(&db_);
|
||||
CHECK(RunStatements(&ctx, create_sql, &error_message)) << error_message;
|
||||
auto run = ctx.UseOnce(
|
||||
R"(
|
||||
insert into camera (uuid, short_name, host, username, password,
|
||||
main_rtsp_path, sub_rtsp_path, retain_bytes)
|
||||
values (:uuid, :short_name, :host, :username, :password,
|
||||
:main_rtsp_path, :sub_rtsp_path, :retain_bytes);
|
||||
)");
|
||||
run.BindBlob(":uuid", GetRealUuidGenerator()->Generate().binary_view());
|
||||
run.BindText(":short_name", "test");
|
||||
run.BindText(":host", "test-camera");
|
||||
run.BindText(":username", "foo");
|
||||
run.BindText(":password", "bar");
|
||||
run.BindText(":main_rtsp_path", "/main");
|
||||
run.BindText(":sub_rtsp_path", "/sub");
|
||||
run.BindInt64(":retain_bytes", 1000000);
|
||||
CHECK_EQ(SQLITE_DONE, run.Step()) << run.error_message();
|
||||
}
|
||||
mdb_.SetUuidGeneratorForTesting(&uuidgen_);
|
||||
CHECK(mdb_.Init(&db_, &error_message)) << error_message;
|
||||
env_.mdb = &mdb_;
|
||||
|
||||
ListCamerasRow row;
|
||||
int n_rows = 0;
|
||||
mdb_.ListCameras([&row, &n_rows](const ListCamerasRow &some_row) {
|
||||
++n_rows;
|
||||
row = some_row;
|
||||
return IterationControl::kContinue;
|
||||
});
|
||||
CHECK_EQ(1, n_rows);
|
||||
|
||||
clock_.Sleep({1430006400, 0}); // 2015-04-26 00:00:00 UTC
|
||||
|
||||
stream_.reset(new Stream(&signal_, &env_, row, 0, 5));
|
||||
}
|
||||
|
||||
// A function to use in OpenRtspRaw invocations which shuts down the stream
|
||||
// and indicates that the input video source can't be opened.
|
||||
InputVideoPacketStream *Shutdown(const std::string &url,
|
||||
std::string *error_message) {
|
||||
*error_message = "(shutting down)";
|
||||
signal_.Shutdown();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
struct Frame {
|
||||
Frame(bool is_key, int64_t pts, int64_t duration)
|
||||
: is_key(is_key), pts(pts), duration(duration) {}
|
||||
bool is_key;
|
||||
int64_t pts;
|
||||
int64_t duration;
|
||||
|
||||
bool operator==(const Frame &o) const {
|
||||
return is_key == o.is_key && pts == o.pts && duration == o.duration;
|
||||
}
|
||||
|
||||
friend std::ostream &operator<<(std::ostream &os, const Frame &f) {
|
||||
return os << "Frame(" << f.is_key << ", " << f.pts << ", " << f.duration
|
||||
<< ")";
|
||||
}
|
||||
};
|
||||
|
||||
#if 0
|
||||
std::vector<Frame> GetFrames(const std::string &path) {
|
||||
std::vector<Frame> frames;
|
||||
std::string error_message;
|
||||
std::string full_path = StrCat(test_dir_, "/test/", path);
|
||||
auto f = GetRealVideoSource()->OpenFile(full_path, &error_message);
|
||||
if (f == nullptr) {
|
||||
ADD_FAILURE() << full_path << ": " << error_message;
|
||||
return frames;
|
||||
}
|
||||
VideoPacket pkt;
|
||||
while (f->GetNext(&pkt, &error_message)) {
|
||||
frames.push_back(Frame(pkt.is_key(), pkt.pts(), pkt.pkt()->duration));
|
||||
}
|
||||
EXPECT_EQ("", error_message);
|
||||
return frames;
|
||||
}
|
||||
#else
|
||||
std::vector<Frame> GetFrames(const re2::StringPiece uuid_text) {
|
||||
std::vector<Frame> frames;
|
||||
Uuid uuid;
|
||||
if (!uuid.ParseText(uuid_text)) {
|
||||
ADD_FAILURE() << "unparseable: " << uuid_text;
|
||||
return frames;
|
||||
}
|
||||
DatabaseContext ctx(&db_);
|
||||
auto run = ctx.UseOnce(
|
||||
"select video_index from recording where sample_file_uuid = :uuid;");
|
||||
run.BindBlob(":uuid", uuid.binary_view());
|
||||
if (run.Step() != SQLITE_ROW) {
|
||||
ADD_FAILURE() << run.error_message();
|
||||
return frames;
|
||||
}
|
||||
for (SampleIndexIterator it(run.ColumnBlob(0)); !it.done(); it.Next()) {
|
||||
frames.push_back(Frame(it.is_key(), it.start_90k(), it.duration_90k()));
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
#endif
|
||||
|
||||
MockUuidGenerator uuidgen_;
|
||||
ShutdownSignal signal_;
|
||||
SimulatedClock clock_;
|
||||
testing::StrictMock<MockVideoSource> video_source_;
|
||||
Database db_;
|
||||
MoonfireDatabase mdb_;
|
||||
std::unique_ptr<moonfire_nvr::File> sample_file_dir_;
|
||||
Environment env_;
|
||||
std::string test_dir_;
|
||||
std::unique_ptr<Stream> stream_;
|
||||
};
|
||||
|
||||
class ProxyingInputVideoPacketStream : public InputVideoPacketStream {
|
||||
public:
|
||||
explicit ProxyingInputVideoPacketStream(
|
||||
std::unique_ptr<InputVideoPacketStream> base, SimulatedClock *clock)
|
||||
: base_(std::move(base)), clock_(clock) {}
|
||||
|
||||
bool GetNext(VideoPacket *pkt, std::string *error_message) final {
|
||||
if (pkts_left_-- == 0) {
|
||||
*error_message = "(pkt limit reached)";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Advance time to when this packet starts.
|
||||
clock_->Sleep(SecToTimespec(last_duration_sec_));
|
||||
if (!base_->GetNext(pkt, error_message)) {
|
||||
return false;
|
||||
}
|
||||
last_duration_sec_ =
|
||||
pkt->pkt()->duration * av_q2d(base_->stream()->time_base);
|
||||
|
||||
// Adjust timestamps.
|
||||
if (ts_offset_pkts_left_ > 0) {
|
||||
pkt->pkt()->pts += ts_offset_;
|
||||
pkt->pkt()->dts += ts_offset_;
|
||||
--ts_offset_pkts_left_;
|
||||
}
|
||||
|
||||
// Use a fixed duration, as the duration from a real RTSP stream is only
|
||||
// an estimate. Our test video is 1 fps, 90 kHz time base.
|
||||
pkt->pkt()->duration = 90000;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const AVStream *stream() const final { return base_->stream(); }
|
||||
|
||||
void set_ts_offset(int64_t offset, int pkts) {
|
||||
ts_offset_ = offset;
|
||||
ts_offset_pkts_left_ = pkts;
|
||||
}
|
||||
|
||||
void set_pkts(int num) { pkts_left_ = num; }
|
||||
|
||||
private:
|
||||
std::unique_ptr<InputVideoPacketStream> base_;
|
||||
SimulatedClock *clock_ = nullptr;
|
||||
double last_duration_sec_ = 0.;
|
||||
int64_t ts_offset_ = 0;
|
||||
int ts_offset_pkts_left_ = 0;
|
||||
int pkts_left_ = std::numeric_limits<int>::max();
|
||||
};
|
||||
|
||||
TEST_F(StreamTest, Basic) {
|
||||
std::string error_message;
|
||||
|
||||
// This is a ~1 fps test video with a timebase of 90 kHz.
|
||||
auto in_stream = GetRealVideoSource()->OpenFile("../src/testdata/clip.mp4",
|
||||
&error_message);
|
||||
ASSERT_TRUE(in_stream != nullptr) << error_message;
|
||||
auto *proxy_stream =
|
||||
new ProxyingInputVideoPacketStream(std::move(in_stream), &clock_);
|
||||
|
||||
// The starting pts of the input should be irrelevant.
|
||||
proxy_stream->set_ts_offset(180000, std::numeric_limits<int>::max());
|
||||
|
||||
Uuid uuid1;
|
||||
ASSERT_TRUE(uuid1.ParseText("00000000-0000-0000-0000-000000000001"));
|
||||
Uuid uuid2;
|
||||
ASSERT_TRUE(uuid2.ParseText("00000000-0000-0000-0000-000000000002"));
|
||||
EXPECT_CALL(uuidgen_, Generate())
|
||||
.WillOnce(Return(uuid1))
|
||||
.WillOnce(Return(uuid2));
|
||||
|
||||
EXPECT_CALL(video_source_, OpenRtspRaw("rtsp://foo:bar@test-camera/main", _))
|
||||
.WillOnce(Return(proxy_stream))
|
||||
.WillOnce(Invoke(this, &StreamTest::Shutdown));
|
||||
stream_->Run();
|
||||
// Compare frame-by-frame.
|
||||
// Note below that while the rotation is scheduled to happen near 5-second
|
||||
// boundaries (such as 2016-04-26 00:00:05), it gets deferred until the next
|
||||
// key frame, which in this case is 00:00:07.
|
||||
EXPECT_THAT(GetFrames("00000000-0000-0000-0000-000000000001"),
|
||||
testing::ElementsAre(
|
||||
Frame(true, 0, 90379), Frame(false, 90379, 89884),
|
||||
Frame(false, 180263, 89749), Frame(false, 270012, 89981),
|
||||
Frame(true, 359993, 90055),
|
||||
Frame(false, 450048,
|
||||
89967), // pts_time 5.000533, past rotation time.
|
||||
Frame(false, 540015, 90021),
|
||||
Frame(false, 630036, 89958)));
|
||||
EXPECT_THAT(
|
||||
GetFrames("00000000-0000-0000-0000-000000000002"),
|
||||
testing::ElementsAre(Frame(true, 0, 90011), Frame(false, 90011, 0)));
|
||||
}
|
||||
|
||||
TEST_F(StreamTest, NonIncreasingTimestamp) {
|
||||
std::string error_message;
|
||||
auto in_stream = GetRealVideoSource()->OpenFile("../src/testdata/clip.mp4",
|
||||
&error_message);
|
||||
ASSERT_TRUE(in_stream != nullptr) << error_message;
|
||||
auto *proxy_stream =
|
||||
new ProxyingInputVideoPacketStream(std::move(in_stream), &clock_);
|
||||
proxy_stream->set_ts_offset(12345678, 1);
|
||||
EXPECT_CALL(video_source_, OpenRtspRaw("rtsp://foo:bar@test-camera/main", _))
|
||||
.WillOnce(Return(proxy_stream))
|
||||
.WillOnce(Invoke(this, &StreamTest::Shutdown));
|
||||
|
||||
Uuid uuid1;
|
||||
ASSERT_TRUE(uuid1.ParseText("00000000-0000-0000-0000-000000000001"));
|
||||
EXPECT_CALL(uuidgen_, Generate()).WillOnce(Return(uuid1));
|
||||
|
||||
{
|
||||
ScopedMockLog log;
|
||||
EXPECT_CALL(log, Log(_, _, _)).Times(AnyNumber());
|
||||
EXPECT_CALL(log,
|
||||
Log(_, _, HasSubstr("Rejecting non-increasing pts=90379")));
|
||||
log.Start();
|
||||
stream_->Run();
|
||||
}
|
||||
|
||||
// The output file should still be added to the file manager, with the one
|
||||
// packet that made it. The final packet on input error will have 0
|
||||
// duration.
|
||||
EXPECT_THAT(GetFrames("00000000-0000-0000-0000-000000000001"),
|
||||
testing::ElementsAre(Frame(true, 0, 0)));
|
||||
}
|
||||
|
||||
TEST_F(StreamTest, RetryOnInputError) {
|
||||
std::string error_message;
|
||||
auto in_stream_1 = GetRealVideoSource()->OpenFile("../src/testdata/clip.mp4",
|
||||
&error_message);
|
||||
ASSERT_TRUE(in_stream_1 != nullptr) << error_message;
|
||||
auto *proxy_stream_1 =
|
||||
new ProxyingInputVideoPacketStream(std::move(in_stream_1), &clock_);
|
||||
proxy_stream_1->set_pkts(1);
|
||||
|
||||
auto in_stream_2 = GetRealVideoSource()->OpenFile("../src/testdata/clip.mp4",
|
||||
&error_message);
|
||||
ASSERT_TRUE(in_stream_2 != nullptr) << error_message;
|
||||
auto *proxy_stream_2 =
|
||||
new ProxyingInputVideoPacketStream(std::move(in_stream_2), &clock_);
|
||||
proxy_stream_2->set_pkts(1);
|
||||
|
||||
EXPECT_CALL(video_source_, OpenRtspRaw("rtsp://foo:bar@test-camera/main", _))
|
||||
.WillOnce(Return(proxy_stream_1))
|
||||
.WillOnce(Return(proxy_stream_2))
|
||||
.WillOnce(Invoke(this, &StreamTest::Shutdown));
|
||||
|
||||
Uuid uuid1;
|
||||
ASSERT_TRUE(uuid1.ParseText("00000000-0000-0000-0000-000000000001"));
|
||||
Uuid uuid2;
|
||||
ASSERT_TRUE(uuid2.ParseText("00000000-0000-0000-0000-000000000002"));
|
||||
EXPECT_CALL(uuidgen_, Generate())
|
||||
.WillOnce(Return(uuid1))
|
||||
.WillOnce(Return(uuid2));
|
||||
stream_->Run();
|
||||
|
||||
// Each attempt should have resulted in a file with one packet.
|
||||
EXPECT_THAT(GetFrames("00000000-0000-0000-0000-000000000001"),
|
||||
testing::ElementsAre(Frame(true, 0, 0)));
|
||||
EXPECT_THAT(GetFrames("00000000-0000-0000-0000-000000000002"),
|
||||
testing::ElementsAre(Frame(true, 0, 0)));
|
||||
}
|
||||
|
||||
TEST_F(StreamTest, DiscardInitialNonKeyFrames) {
|
||||
std::string error_message;
|
||||
auto in_stream = GetRealVideoSource()->OpenFile("../src/testdata/clip.mp4",
|
||||
&error_message);
|
||||
ASSERT_TRUE(in_stream != nullptr) << error_message;
|
||||
|
||||
// Discard the initial key frame packet.
|
||||
VideoPacket dummy;
|
||||
ASSERT_TRUE(in_stream->GetNext(&dummy, &error_message)) << error_message;
|
||||
|
||||
auto *proxy_stream =
|
||||
new ProxyingInputVideoPacketStream(std::move(in_stream), &clock_);
|
||||
EXPECT_CALL(video_source_, OpenRtspRaw("rtsp://foo:bar@test-camera/main", _))
|
||||
.WillOnce(Return(proxy_stream))
|
||||
.WillOnce(Invoke(this, &StreamTest::Shutdown));
|
||||
|
||||
Uuid uuid1;
|
||||
ASSERT_TRUE(uuid1.ParseText("00000000-0000-0000-0000-000000000001"));
|
||||
Uuid uuid2;
|
||||
ASSERT_TRUE(uuid2.ParseText("00000000-0000-0000-0000-000000000002"));
|
||||
EXPECT_CALL(uuidgen_, Generate())
|
||||
.WillOnce(Return(uuid1))
|
||||
.WillOnce(Return(uuid2));
|
||||
stream_->Run();
|
||||
|
||||
// Skipped: initial key frame packet (duration 90379)
|
||||
// Ignored: duration 89884, 89749, 89981 (total pts time: 2.99571... sec)
|
||||
// Thus, the first output file should start at 00:00:02.
|
||||
EXPECT_THAT(
|
||||
GetFrames("00000000-0000-0000-0000-000000000001"),
|
||||
testing::ElementsAre(
|
||||
Frame(true, 0, 90055),
|
||||
Frame(false, 90055, 89967), // pts_time 5.000533, past rotation time.
|
||||
Frame(false, 180022, 90021), Frame(false, 270043, 89958)));
|
||||
EXPECT_THAT(
|
||||
GetFrames("00000000-0000-0000-0000-000000000002"),
|
||||
testing::ElementsAre(Frame(true, 0, 90011), Frame(false, 90011, 0)));
|
||||
}
|
||||
|
||||
// TODO: test output stream error (on open, writing packet, closing).
|
||||
// TODO: test rotation!
|
||||
|
||||
} // namespace
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
FLAGS_alsologtostderr = true;
|
||||
google::ParseCommandLineFlags(&argc, &argv, true);
|
||||
testing::InitGoogleTest(&argc, argv);
|
||||
google::InitGoogleLogging(argv[0]);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
@ -1,462 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// moonfire-nvr.cc: implementation of moonfire-nvr.h.
|
||||
//
|
||||
// Caveats:
|
||||
//
|
||||
// Currently the recording thread blocks while a just-finished recording
|
||||
// is synced to disk and written to the database, which can be 250+ ms.
|
||||
// Likewise when recordings are being deleted. It would be better to hand
|
||||
// off to a separate syncer thread, only blocking the recording when there
|
||||
// would otherwise be insufficient disk space.
|
||||
//
|
||||
// This also commits to the SQLite database potentially several times per
|
||||
// minute per camera:
|
||||
//
|
||||
// 1. (rarely) to get a new video sample entry id
|
||||
// 2. to reserve a new uuid
|
||||
// 3. to move uuids planned for deletion from "recording" to
|
||||
// "reserved_sample_Files"
|
||||
// 4. to mark those uuids as deleted
|
||||
// 5. to insert the new recording
|
||||
//
|
||||
// These could be combined into a single batch per minute per camera or even
|
||||
// per minute by doing some operations sooner (such as reserving the next
|
||||
// minute's uuid when inserting the previous minute's recording) and some
|
||||
// later (such as marking uuids as deleted).
|
||||
|
||||
#define _BSD_SOURCE // for timegm(3).
|
||||
|
||||
#include "moonfire-nvr.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <event2/http.h>
|
||||
#include <gflags/gflags.h>
|
||||
#include <glog/logging.h>
|
||||
#include <re2/re2.h>
|
||||
|
||||
#include "filesystem.h"
|
||||
#include "h264.h"
|
||||
#include "http.h"
|
||||
#include "recording.h"
|
||||
#include "string.h"
|
||||
#include "time.h"
|
||||
|
||||
using std::string;
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
namespace {
|
||||
|
||||
const int kRotateIntervalSec = 60;
|
||||
|
||||
} // namespace
|
||||
|
||||
// Call from dedicated thread. Runs until shutdown requested.
|
||||
void Stream::Run() {
|
||||
std::string error_message;
|
||||
|
||||
// Do an initial rotation so that if retain_bytes has been reduced, the
|
||||
// bulk deletion happens now, rather than while an input stream is open.
|
||||
if (!RotateFiles(&error_message)) {
|
||||
LOG(WARNING) << row_.short_name
|
||||
<< ": initial rotation failed: " << error_message;
|
||||
}
|
||||
|
||||
while (!signal_->ShouldShutdown()) {
|
||||
if (in_ == nullptr && !OpenInput(&error_message)) {
|
||||
LOG(WARNING) << row_.short_name
|
||||
<< ": Failed to open input; sleeping before retrying: "
|
||||
<< error_message;
|
||||
env_->clock->Sleep({1, 0});
|
||||
continue;
|
||||
}
|
||||
|
||||
LOG(INFO) << row_.short_name << ": Calling ProcessPackets.";
|
||||
ProcessPacketsResult res = ProcessPackets(&error_message);
|
||||
if (res == kInputError) {
|
||||
CloseOutput(-1);
|
||||
in_.reset();
|
||||
start_localtime_90k_ = -1;
|
||||
LOG(WARNING) << row_.short_name
|
||||
<< ": Input error; sleeping before retrying: "
|
||||
<< error_message;
|
||||
env_->clock->Sleep({1, 0});
|
||||
continue;
|
||||
} else if (res == kOutputError) {
|
||||
CloseOutput(-1);
|
||||
LOG(WARNING) << row_.short_name
|
||||
<< ": Output error; sleeping before retrying: "
|
||||
<< error_message;
|
||||
env_->clock->Sleep({1, 0});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
CloseOutput(-1);
|
||||
}
|
||||
|
||||
Stream::ProcessPacketsResult Stream::ProcessPackets(
|
||||
std::string *error_message) {
|
||||
moonfire_nvr::VideoPacket pkt;
|
||||
CHECK(in_ != nullptr);
|
||||
CHECK(!writer_.is_open());
|
||||
while (!signal_->ShouldShutdown()) {
|
||||
if (!in_->GetNext(&pkt, error_message)) {
|
||||
if (error_message->empty()) {
|
||||
*error_message = "unexpected end of stream";
|
||||
}
|
||||
return kInputError;
|
||||
}
|
||||
|
||||
// With gcc 4.9 (Raspbian Jessie),
|
||||
// #define AV_NOPTS_VALUE INT64_C(0x8000000000000000)
|
||||
// produces an unsigned value. Argh. Work around.
|
||||
static const int64_t kAvNoptsValue = AV_NOPTS_VALUE;
|
||||
if (pkt.pkt()->pts == kAvNoptsValue || pkt.pkt()->dts == kAvNoptsValue) {
|
||||
*error_message = "Rejecting packet with missing pts/dts";
|
||||
return kInputError;
|
||||
}
|
||||
|
||||
if (pkt.pkt()->pts != pkt.pkt()->dts) {
|
||||
*error_message =
|
||||
StrCat("Rejecting packet with pts=", pkt.pkt()->pts, " != dts=",
|
||||
pkt.pkt()->dts, "; expecting only I or P frames.");
|
||||
return kInputError;
|
||||
}
|
||||
|
||||
if (pkt.pkt()->pts < min_next_pts_) {
|
||||
*error_message = StrCat("Rejecting non-increasing pts=", pkt.pkt()->pts,
|
||||
"; expected at least ", min_next_pts_);
|
||||
return kInputError;
|
||||
}
|
||||
min_next_pts_ = pkt.pkt()->pts + 1;
|
||||
|
||||
frame_realtime_ = env_->clock->Now();
|
||||
|
||||
if (writer_.is_open() && frame_realtime_.tv_sec >= rotate_time_ &&
|
||||
pkt.is_key()) {
|
||||
LOG(INFO) << row_.short_name << ": Reached rotation time; closing "
|
||||
<< recording_.sample_file_uuid.UnparseText() << ".";
|
||||
CloseOutput(pkt.pkt()->pts - start_pts_);
|
||||
} else if (writer_.is_open()) {
|
||||
VLOG(3) << row_.short_name << ": Rotation time=" << rotate_time_
|
||||
<< " vs current time=" << frame_realtime_.tv_sec;
|
||||
}
|
||||
|
||||
// Discard the initial, non-key frames from the input.
|
||||
if (!seen_key_frame_ && !pkt.is_key()) {
|
||||
continue;
|
||||
} else if (!seen_key_frame_) {
|
||||
seen_key_frame_ = true;
|
||||
}
|
||||
|
||||
if (!writer_.is_open()) {
|
||||
start_pts_ = pkt.pts();
|
||||
if (!OpenOutput(error_message)) {
|
||||
return kOutputError;
|
||||
}
|
||||
rotate_time_ = frame_realtime_.tv_sec -
|
||||
(frame_realtime_.tv_sec % rotate_interval_sec_) +
|
||||
rotate_offset_sec_;
|
||||
if (rotate_time_ <= frame_realtime_.tv_sec) {
|
||||
rotate_time_ += rotate_interval_sec_;
|
||||
}
|
||||
}
|
||||
|
||||
auto start_time_90k = pkt.pkt()->pts - start_pts_;
|
||||
if (prev_pkt_start_time_90k_ != -1) {
|
||||
index_.AddSample(start_time_90k - prev_pkt_start_time_90k_,
|
||||
prev_pkt_bytes_, prev_pkt_key_);
|
||||
}
|
||||
re2::StringPiece data = pkt.data();
|
||||
if (need_transform_) {
|
||||
if (!TransformSampleData(data, &transform_tmp_, error_message)) {
|
||||
return kInputError;
|
||||
}
|
||||
data = transform_tmp_;
|
||||
}
|
||||
if (!writer_.Write(data, error_message)) {
|
||||
return kOutputError;
|
||||
}
|
||||
prev_pkt_start_time_90k_ = start_time_90k;
|
||||
prev_pkt_bytes_ = data.size();
|
||||
prev_pkt_key_ = pkt.is_key();
|
||||
}
|
||||
return kStopped;
|
||||
}
|
||||
|
||||
bool Stream::OpenInput(std::string *error_message) {
|
||||
CHECK(in_ == nullptr);
|
||||
string url = StrCat("rtsp://", row_.username, ":", row_.password, "@",
|
||||
row_.host, row_.main_rtsp_path);
|
||||
string redacted_url = StrCat("rtsp://", row_.username, ":redacted@",
|
||||
row_.host, row_.main_rtsp_path);
|
||||
LOG(INFO) << row_.short_name << ": Opening input: " << redacted_url;
|
||||
in_ = env_->video_source->OpenRtsp(url, error_message);
|
||||
min_next_pts_ = std::numeric_limits<int64_t>::min();
|
||||
seen_key_frame_ = false;
|
||||
if (in_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The time base should match the 90kHz frequency specified in RFC 3551
|
||||
// section 5.
|
||||
if (in_->stream()->time_base.num != 1 ||
|
||||
in_->stream()->time_base.den != kTimeUnitsPerSecond) {
|
||||
*error_message =
|
||||
StrCat("unexpected time base ", in_->stream()->time_base.num, "/",
|
||||
in_->stream()->time_base.den);
|
||||
return false;
|
||||
}
|
||||
|
||||
// width and height must fix into 16-bit ints for MP4 encoding.
|
||||
int max_dimension = std::numeric_limits<uint16_t>::max();
|
||||
if (in_->stream()->codec->width > max_dimension ||
|
||||
in_->stream()->codec->height > max_dimension) {
|
||||
*error_message =
|
||||
StrCat("input dimensions ", in_->stream()->codec->width, "x",
|
||||
in_->stream()->codec->height, " are too large.");
|
||||
return false;
|
||||
}
|
||||
entry_.id = -1;
|
||||
entry_.width = in_->stream()->codec->width;
|
||||
entry_.height = in_->stream()->codec->height;
|
||||
re2::StringPiece extradata = in_->extradata();
|
||||
if (!ParseExtraData(extradata, entry_.width, entry_.height, &entry_.data,
|
||||
&need_transform_, error_message)) {
|
||||
in_.reset();
|
||||
return false;
|
||||
}
|
||||
auto sha1 = Digest::SHA1();
|
||||
sha1->Update(entry_.data);
|
||||
entry_.sha1 = sha1->Finalize();
|
||||
if (!env_->mdb->InsertVideoSampleEntry(&entry_, error_message)) {
|
||||
in_.reset();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Stream::CloseOutput(int64_t pts) {
|
||||
if (!writer_.is_open()) {
|
||||
return;
|
||||
}
|
||||
std::string error_message;
|
||||
if (prev_pkt_start_time_90k_ != -1) {
|
||||
int64_t duration_90k = pts - prev_pkt_start_time_90k_;
|
||||
index_.AddSample(duration_90k > 0 ? duration_90k : 0, prev_pkt_bytes_,
|
||||
prev_pkt_key_);
|
||||
}
|
||||
if (!writer_.Close(&recording_.sample_file_sha1, &error_message)) {
|
||||
LOG(ERROR) << row_.short_name << ": Closing output "
|
||||
<< recording_.sample_file_uuid.UnparseText()
|
||||
<< " failed with error: " << error_message;
|
||||
uuids_to_unlink_.push_back(recording_.sample_file_uuid);
|
||||
TryUnlink();
|
||||
return;
|
||||
}
|
||||
int ret = env_->sample_file_dir->Sync();
|
||||
if (ret != 0) {
|
||||
LOG(ERROR) << row_.short_name
|
||||
<< ": Unable to sync sample file dir after writing "
|
||||
<< recording_.sample_file_uuid.UnparseText() << ": "
|
||||
<< strerror(ret);
|
||||
uuids_to_unlink_.push_back(recording_.sample_file_uuid);
|
||||
TryUnlink();
|
||||
return;
|
||||
}
|
||||
if (!env_->mdb->InsertRecording(&recording_, &error_message)) {
|
||||
LOG(ERROR) << row_.short_name << ": Unable to insert recording "
|
||||
<< recording_.sample_file_uuid.UnparseText() << ": "
|
||||
<< error_message;
|
||||
uuids_to_unlink_.push_back(recording_.sample_file_uuid);
|
||||
TryUnlink();
|
||||
return;
|
||||
}
|
||||
row_.total_sample_file_bytes += recording_.sample_file_bytes;
|
||||
VLOG(1) << row_.short_name << ": ...wrote "
|
||||
<< recording_.sample_file_uuid.UnparseText() << "; usage now "
|
||||
<< HumanizeWithBinaryPrefix(row_.total_sample_file_bytes, "B");
|
||||
}
|
||||
|
||||
void Stream::TryUnlink() {
|
||||
std::vector<Uuid> still_not_unlinked;
|
||||
for (const auto &uuid : uuids_to_unlink_) {
|
||||
std::string text = uuid.UnparseText();
|
||||
int ret = env_->sample_file_dir->Unlink(text.c_str());
|
||||
if (ret == ENOENT) {
|
||||
LOG(WARNING) << row_.short_name << ": Sample file " << text
|
||||
<< " already deleted!";
|
||||
} else if (ret != 0) {
|
||||
LOG(WARNING) << row_.short_name << ": Unable to unlink " << text << ": "
|
||||
<< strerror(ret);
|
||||
still_not_unlinked.push_back(uuid);
|
||||
continue;
|
||||
}
|
||||
uuids_to_mark_deleted_.push_back(uuid);
|
||||
}
|
||||
uuids_to_unlink_ = std::move(still_not_unlinked);
|
||||
}
|
||||
|
||||
bool Stream::OpenOutput(std::string *error_message) {
|
||||
int64_t frame_localtime_90k = To90k(frame_realtime_);
|
||||
if (start_localtime_90k_ == -1) {
|
||||
start_localtime_90k_ = frame_localtime_90k - start_pts_;
|
||||
}
|
||||
if (!RotateFiles(error_message)) {
|
||||
return false;
|
||||
}
|
||||
std::vector<Uuid> reserved = env_->mdb->ReserveSampleFiles(1, error_message);
|
||||
if (reserved.size() != 1) {
|
||||
return false;
|
||||
}
|
||||
CHECK(!writer_.is_open());
|
||||
string filename = reserved[0].UnparseText();
|
||||
recording_.id = -1;
|
||||
recording_.camera_id = row_.id;
|
||||
recording_.sample_file_uuid = reserved[0];
|
||||
recording_.video_sample_entry_id = entry_.id;
|
||||
recording_.local_time_90k = frame_localtime_90k;
|
||||
index_.Init(&recording_, start_localtime_90k_ + start_pts_);
|
||||
if (!writer_.Open(filename.c_str(), error_message)) {
|
||||
return false;
|
||||
}
|
||||
prev_pkt_start_time_90k_ = -1;
|
||||
prev_pkt_bytes_ = -1;
|
||||
prev_pkt_key_ = false;
|
||||
LOG(INFO) << row_.short_name << ": Opened output " << filename
|
||||
<< ", using start_pts=" << start_pts_
|
||||
<< ", input timebase=" << in_->stream()->time_base.num << "/"
|
||||
<< in_->stream()->time_base.den;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Stream::RotateFiles(std::string *error_message) {
|
||||
int64_t bytes_needed = row_.total_sample_file_bytes - row_.retain_bytes;
|
||||
int64_t bytes_to_delete = 0;
|
||||
if (bytes_needed <= 0) {
|
||||
VLOG(1) << row_.short_name << ": have remaining quota of "
|
||||
<< HumanizeWithBinaryPrefix(-bytes_needed, "B");
|
||||
return true;
|
||||
}
|
||||
LOG(INFO) << row_.short_name << ": need to delete "
|
||||
<< HumanizeWithBinaryPrefix(bytes_needed, "B");
|
||||
std::vector<ListOldestSampleFilesRow> to_delete;
|
||||
auto row_cb = [&](const ListOldestSampleFilesRow &row) {
|
||||
bytes_needed -= row.sample_file_bytes;
|
||||
bytes_to_delete += row.sample_file_bytes;
|
||||
to_delete.push_back(row);
|
||||
return bytes_needed < 0 ? IterationControl::kBreak
|
||||
: IterationControl::kContinue;
|
||||
};
|
||||
if (!env_->mdb->ListOldestSampleFiles(row_.uuid, row_cb, error_message)) {
|
||||
return false;
|
||||
}
|
||||
if (bytes_needed > 0) {
|
||||
*error_message =
|
||||
StrCat("couldn't find enough files to delete; ",
|
||||
HumanizeWithBinaryPrefix(bytes_needed, "B"), " left.");
|
||||
return false;
|
||||
}
|
||||
if (!env_->mdb->DeleteRecordings(to_delete, error_message)) {
|
||||
return false;
|
||||
}
|
||||
for (const auto &to_delete_row : to_delete) {
|
||||
uuids_to_unlink_.push_back(to_delete_row.sample_file_uuid);
|
||||
}
|
||||
row_.total_sample_file_bytes -= bytes_to_delete;
|
||||
TryUnlink();
|
||||
if (!uuids_to_unlink_.empty()) {
|
||||
*error_message =
|
||||
StrCat("failed to unlink ", uuids_to_unlink_.size(), " files.");
|
||||
return false;
|
||||
}
|
||||
int ret = env_->sample_file_dir->Sync();
|
||||
if (ret != 0) {
|
||||
*error_message = StrCat("fsync sample directory: ", strerror(ret));
|
||||
return false;
|
||||
}
|
||||
if (!env_->mdb->MarkSampleFilesDeleted(uuids_to_mark_deleted_,
|
||||
error_message)) {
|
||||
*error_message = StrCat("unable to mark ", uuids_to_mark_deleted_.size(),
|
||||
" sample files as deleted");
|
||||
return false;
|
||||
}
|
||||
uuids_to_mark_deleted_.clear();
|
||||
VLOG(1) << row_.short_name << ": ...deleted successfully; usage now "
|
||||
<< HumanizeWithBinaryPrefix(row_.total_sample_file_bytes, "B");
|
||||
return true;
|
||||
}
|
||||
|
||||
Nvr::~Nvr() {
|
||||
signal_.Shutdown();
|
||||
for (auto &thread : stream_threads_) {
|
||||
thread.join();
|
||||
}
|
||||
// TODO: cleanup reservations?
|
||||
}
|
||||
|
||||
bool Nvr::Init(std::string *error_msg) {
|
||||
std::vector<Uuid> all_reserved;
|
||||
if (!env_->mdb->ListReservedSampleFiles(&all_reserved, error_msg)) {
|
||||
return false;
|
||||
}
|
||||
for (const auto &reserved : all_reserved) {
|
||||
int ret = env_->sample_file_dir->Unlink(reserved.UnparseText().c_str());
|
||||
if (ret != 0 && ret != ENOENT) {
|
||||
LOG(WARNING) << "Unable to remove reserved sample file: "
|
||||
<< reserved.UnparseText();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<ListCamerasRow> cameras;
|
||||
env_->mdb->ListCameras([&](const ListCamerasRow &row) {
|
||||
cameras.push_back(row);
|
||||
return IterationControl::kContinue;
|
||||
});
|
||||
for (size_t i = 0; i < cameras.size(); ++i) {
|
||||
int rotate_offset_sec = kRotateIntervalSec * i / cameras.size();
|
||||
auto *stream = new Stream(&signal_, env_, cameras[i], rotate_offset_sec,
|
||||
kRotateIntervalSec);
|
||||
streams_.emplace_back(stream);
|
||||
stream_threads_.emplace_back([stream]() { stream->Run(); });
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace moonfire_nvr
|
@ -1,187 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// moonfire-nvr.h: main digital video recorder components.
|
||||
|
||||
#ifndef MOONFIRE_NVR_NVR_H
|
||||
#define MOONFIRE_NVR_NVR_H
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <event2/http.h>
|
||||
|
||||
#include "filesystem.h"
|
||||
#include "moonfire-db.h"
|
||||
#include "ffmpeg.h"
|
||||
#include "time.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
// A signal that all streams associated with an Nvr should shut down.
|
||||
class ShutdownSignal {
|
||||
public:
|
||||
ShutdownSignal() {}
|
||||
ShutdownSignal(const ShutdownSignal &) = delete;
|
||||
ShutdownSignal &operator=(const ShutdownSignal &) = delete;
|
||||
|
||||
void Shutdown() { shutdown_.store(true, std::memory_order_relaxed); }
|
||||
|
||||
bool ShouldShutdown() const {
|
||||
return shutdown_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic_bool shutdown_{false};
|
||||
};
|
||||
|
||||
// The Nvr's environment. This is supplied for testability.
|
||||
struct Environment {
|
||||
WallClock *clock = nullptr;
|
||||
VideoSource *video_source = nullptr;
|
||||
File *sample_file_dir = nullptr;
|
||||
MoonfireDatabase *mdb = nullptr;
|
||||
};
|
||||
|
||||
// A single video stream, currently always a camera's "main" (as opposed to
|
||||
// "sub") stream. Methods are thread-compatible rather than thread-safe; the
|
||||
// Nvr should call Run in a dedicated thread.
|
||||
class Stream {
|
||||
public:
|
||||
Stream(const ShutdownSignal *signal, Environment *const env,
|
||||
const moonfire_nvr::ListCamerasRow &row, int rotate_offset_sec,
|
||||
int rotate_interval_sec)
|
||||
: signal_(signal),
|
||||
env_(env),
|
||||
row_(row),
|
||||
rotate_offset_sec_(rotate_offset_sec),
|
||||
rotate_interval_sec_(rotate_interval_sec),
|
||||
writer_(env->sample_file_dir) {}
|
||||
Stream(const Stream &) = delete;
|
||||
Stream &operator=(const Stream &) = delete;
|
||||
|
||||
// Call from dedicated thread. Runs until shutdown requested.
|
||||
void Run();
|
||||
|
||||
private:
|
||||
enum ProcessPacketsResult { kInputError, kOutputError, kStopped };
|
||||
|
||||
ProcessPacketsResult ProcessPackets(std::string *error_message);
|
||||
bool OpenInput(std::string *error_message);
|
||||
|
||||
// |pts| should be the relative pts within this output segment if closing
|
||||
// due to normal rotation, or -1 if closing abruptly.
|
||||
void CloseOutput(int64_t pts);
|
||||
|
||||
bool OpenOutput(std::string *error_message);
|
||||
bool RotateFiles(std::string *error_message);
|
||||
void TryUnlink();
|
||||
|
||||
const ShutdownSignal *signal_;
|
||||
const Environment *env_;
|
||||
ListCamerasRow row_;
|
||||
const int rotate_offset_sec_;
|
||||
const int rotate_interval_sec_;
|
||||
|
||||
//
|
||||
// State below is used only by the thread in Run().
|
||||
//
|
||||
|
||||
std::unique_ptr<moonfire_nvr::InputVideoPacketStream> in_;
|
||||
int64_t min_next_pts_ = std::numeric_limits<int64_t>::min();
|
||||
bool seen_key_frame_ = false;
|
||||
|
||||
// need_transform_ indicates if TransformSampleData will need to be called
|
||||
// on each video sample.
|
||||
bool need_transform_ = false;
|
||||
|
||||
VideoSampleEntry entry_;
|
||||
std::string transform_tmp_;
|
||||
std::vector<Uuid> uuids_to_unlink_;
|
||||
std::vector<Uuid> uuids_to_mark_deleted_;
|
||||
|
||||
// Current output segment.
|
||||
Recording recording_;
|
||||
moonfire_nvr::SampleFileWriter writer_;
|
||||
SampleIndexEncoder index_;
|
||||
time_t rotate_time_ = 0; // rotate when frame_realtime_ >= rotate_time_.
|
||||
|
||||
// start_pts_ is the pts of the first frame included in the current output.
|
||||
int64_t start_pts_ = -1;
|
||||
|
||||
// start_localtime_90k_ is the local system's time since epoch (in 90k units)
|
||||
// to match start_pts_.
|
||||
int64_t start_localtime_90k_ = -1;
|
||||
|
||||
// These fields describe a packet which has been written to the
|
||||
// sample file but (because the duration is not yet known) has not been
|
||||
// added to the index.
|
||||
int32_t prev_pkt_start_time_90k_ = -1;
|
||||
int32_t prev_pkt_bytes_ = -1;
|
||||
bool prev_pkt_key_ = false;
|
||||
struct timespec frame_realtime_ = {0, 0};
|
||||
};
|
||||
|
||||
// The main network video recorder, which manages a collection of streams.
|
||||
class Nvr {
|
||||
public:
|
||||
explicit Nvr(Environment *env) : env_(env) {}
|
||||
Nvr(const Nvr &) = delete;
|
||||
Nvr &operator=(const Nvr &) = delete;
|
||||
|
||||
// Shut down, blocking for outstanding streams.
|
||||
// Caller only has to guarantee that HttpCallback is not being called / will
|
||||
// not be called again, likely by having already shut down the event loop.
|
||||
~Nvr();
|
||||
|
||||
// Initialize the NVR. Call before any other operation.
|
||||
// Verifies configuration and starts background threads to capture/rotate
|
||||
// streams.
|
||||
bool Init(std::string *error_msg);
|
||||
|
||||
private:
|
||||
void HttpCallbackForTopLevel(evhttp_request *req);
|
||||
|
||||
Environment *const env_;
|
||||
std::vector<std::unique_ptr<Stream>> streams_;
|
||||
std::vector<std::thread> stream_threads_;
|
||||
ShutdownSignal signal_;
|
||||
};
|
||||
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
#endif // MOONFIRE_NVR_NVR_H
|
404
src/mp4-test.cc
404
src/mp4-test.cc
@ -1,404 +0,0 @@
|
||||
// This file is part of Moonfire DVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2015 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// mp4-test.cc: tests of the mp4.h interface.
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include <gflags/gflags.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "ffmpeg.h"
|
||||
#include "h264.h"
|
||||
#include "http.h"
|
||||
#include "mp4.h"
|
||||
#include "string.h"
|
||||
#include "testutil.h"
|
||||
|
||||
DECLARE_bool(alsologtostderr);
|
||||
|
||||
using moonfire_nvr::internal::Mp4SampleTablePieces;
|
||||
|
||||
namespace moonfire_nvr {
|
||||
namespace {
|
||||
|
||||
std::string ToHex(const FileSlice *slice, bool pad) {
|
||||
EvBuffer buf;
|
||||
std::string error_message;
|
||||
size_t size = slice->size();
|
||||
CHECK(slice->AddRange(ByteRange(0, size), &buf, &error_message))
|
||||
<< error_message;
|
||||
CHECK_EQ(size, evbuffer_get_length(buf.get()));
|
||||
return ::moonfire_nvr::ToHex(
|
||||
re2::StringPiece(
|
||||
reinterpret_cast<const char *>(evbuffer_pullup(buf.get(), size)),
|
||||
size),
|
||||
pad);
|
||||
}
|
||||
|
||||
std::string Digest(const FileSlice *slice) {
|
||||
EvBuffer buf;
|
||||
std::string error_message;
|
||||
ByteRange left(0, slice->size());
|
||||
while (left.size() > 0) {
|
||||
auto ret = slice->AddRange(left, &buf, &error_message);
|
||||
CHECK_GT(ret, 0) << error_message;
|
||||
left.begin += ret;
|
||||
}
|
||||
evbuffer_iovec vec;
|
||||
auto digest = Digest::SHA1();
|
||||
while (evbuffer_peek(buf.get(), -1, nullptr, &vec, 1) > 0) {
|
||||
digest->Update(re2::StringPiece(
|
||||
reinterpret_cast<const char *>(vec.iov_base), vec.iov_len));
|
||||
evbuffer_drain(buf.get(), vec.iov_len);
|
||||
}
|
||||
return ::moonfire_nvr::ToHex(digest->Finalize());
|
||||
}
|
||||
|
||||
TEST(Mp4SampleTablePiecesTest, AllSyncFrames) {
|
||||
Recording recording;
|
||||
SampleIndexEncoder encoder;
|
||||
encoder.Init(&recording, 42);
|
||||
for (int i = 1; i <= 5; ++i) {
|
||||
int64_t sample_duration_90k = 2 * i;
|
||||
int64_t sample_bytes = 3 * i;
|
||||
encoder.AddSample(sample_duration_90k, sample_bytes, true);
|
||||
}
|
||||
|
||||
Mp4SampleTablePieces pieces;
|
||||
std::string error_message;
|
||||
// Time range [2, 2 + 4 + 6 + 8) means the 2nd, 3rd, 4th samples should be
|
||||
// included.
|
||||
ASSERT_TRUE(pieces.Init(&recording, 2, 10, 2, 2 + 4 + 6 + 8, &error_message))
|
||||
<< error_message;
|
||||
|
||||
EXPECT_EQ(3, pieces.stts_entry_count());
|
||||
const char kExpectedStts[] =
|
||||
"00 00 00 01 00 00 00 04 " // run length / timestamps.
|
||||
"00 00 00 01 00 00 00 06 "
|
||||
"00 00 00 01 00 00 00 08";
|
||||
EXPECT_EQ(kExpectedStts, ToHex(pieces.stts_entries(), true));
|
||||
|
||||
// Initial index "10" as given above.
|
||||
EXPECT_EQ(3, pieces.stss_entry_count());
|
||||
const char kExpectedStss[] = "00 00 00 0a 00 00 00 0b 00 00 00 0c";
|
||||
EXPECT_EQ(kExpectedStss, ToHex(pieces.stss_entries(), true));
|
||||
|
||||
EXPECT_EQ(3, pieces.stsz_entry_count());
|
||||
const char kExpectedStsz[] = "00 00 00 06 00 00 00 09 00 00 00 0c";
|
||||
EXPECT_EQ(kExpectedStsz, ToHex(pieces.stsz_entries(), true));
|
||||
}
|
||||
|
||||
TEST(Mp4SampleTablePiecesTest, HalfSyncFrames) {
|
||||
Recording recording;
|
||||
SampleIndexEncoder encoder;
|
||||
encoder.Init(&recording, 42);
|
||||
for (int i = 1; i <= 5; ++i) {
|
||||
int64_t sample_duration_90k = 2 * i;
|
||||
int64_t sample_bytes = 3 * i;
|
||||
encoder.AddSample(sample_duration_90k, sample_bytes, (i % 2) == 1);
|
||||
}
|
||||
|
||||
Mp4SampleTablePieces pieces;
|
||||
std::string error_message;
|
||||
// Time range [2 + 4 + 6, 2 + 4 + 6 + 8) means the 4th samples should be
|
||||
// included. The 3rd gets pulled in also because it is a sync frame and the
|
||||
// 4th is not.
|
||||
ASSERT_TRUE(
|
||||
pieces.Init(&recording, 2, 10, 2 + 4 + 6, 2 + 4 + 6 + 8, &error_message))
|
||||
<< error_message;
|
||||
|
||||
EXPECT_EQ(2, pieces.stts_entry_count());
|
||||
const char kExpectedStts[] =
|
||||
"00 00 00 01 00 00 00 06 "
|
||||
"00 00 00 01 00 00 00 08";
|
||||
EXPECT_EQ(kExpectedStts, ToHex(pieces.stts_entries(), true));
|
||||
|
||||
EXPECT_EQ(1, pieces.stss_entry_count());
|
||||
const char kExpectedStss[] = "00 00 00 0a";
|
||||
EXPECT_EQ(kExpectedStss, ToHex(pieces.stss_entries(), true));
|
||||
|
||||
EXPECT_EQ(2, pieces.stsz_entry_count());
|
||||
const char kExpectedStsz[] = "00 00 00 09 00 00 00 0c";
|
||||
EXPECT_EQ(kExpectedStsz, ToHex(pieces.stsz_entries(), true));
|
||||
}
|
||||
|
||||
TEST(Mp4SampleTablePiecesTest, FastPath) {
|
||||
Recording recording;
|
||||
SampleIndexEncoder encoder;
|
||||
encoder.Init(&recording, 42);
|
||||
for (int i = 1; i <= 5; ++i) {
|
||||
int64_t sample_duration_90k = 2 * i;
|
||||
int64_t sample_bytes = 3 * i;
|
||||
encoder.AddSample(sample_duration_90k, sample_bytes, (i % 2) == 1);
|
||||
}
|
||||
auto total_duration_90k = recording.end_time_90k - recording.start_time_90k;
|
||||
|
||||
Mp4SampleTablePieces pieces;
|
||||
std::string error_message;
|
||||
// Time range [0, end - start) means to pull in everything.
|
||||
// This uses a fast path which can determine the size without examining the
|
||||
// index.
|
||||
ASSERT_TRUE(
|
||||
pieces.Init(&recording, 2, 10, 0, total_duration_90k, &error_message))
|
||||
<< error_message;
|
||||
|
||||
EXPECT_EQ(5, pieces.stts_entry_count());
|
||||
const char kExpectedStts[] =
|
||||
"00 00 00 01 00 00 00 02 "
|
||||
"00 00 00 01 00 00 00 04 "
|
||||
"00 00 00 01 00 00 00 06 "
|
||||
"00 00 00 01 00 00 00 08 "
|
||||
"00 00 00 01 00 00 00 0a";
|
||||
EXPECT_EQ(kExpectedStts, ToHex(pieces.stts_entries(), true));
|
||||
|
||||
EXPECT_EQ(3, pieces.stss_entry_count());
|
||||
const char kExpectedStss[] = "00 00 00 0a 00 00 00 0c 00 00 00 0e";
|
||||
EXPECT_EQ(kExpectedStss, ToHex(pieces.stss_entries(), true));
|
||||
|
||||
EXPECT_EQ(5, pieces.stsz_entry_count());
|
||||
const char kExpectedStsz[] =
|
||||
"00 00 00 03 00 00 00 06 00 00 00 09 00 00 00 0c 00 00 00 0f";
|
||||
EXPECT_EQ(kExpectedStsz, ToHex(pieces.stsz_entries(), true));
|
||||
}
|
||||
|
||||
class IntegrationTest : public testing::Test {
|
||||
protected:
|
||||
IntegrationTest() {
|
||||
tmpdir_path_ = PrepareTempDirOrDie("mp4-integration-test");
|
||||
int ret = GetRealFilesystem()->Open(tmpdir_path_.c_str(),
|
||||
O_RDONLY | O_DIRECTORY, &tmpdir_);
|
||||
CHECK_EQ(0, ret) << strerror(ret);
|
||||
}
|
||||
|
||||
Recording CopyMp4ToSingleRecording() {
|
||||
std::string error_message;
|
||||
Recording recording;
|
||||
SampleIndexEncoder index;
|
||||
|
||||
// Set start time to 2015-04-26 00:00:00 UTC.
|
||||
index.Init(&recording, UINT64_C(1430006400) * kTimeUnitsPerSecond);
|
||||
SampleFileWriter writer(tmpdir_.get());
|
||||
std::string filename = recording.sample_file_uuid.UnparseText();
|
||||
if (!writer.Open(filename.c_str(), &error_message)) {
|
||||
ADD_FAILURE() << "open " << filename << ": " << error_message;
|
||||
return recording;
|
||||
}
|
||||
auto in = GetRealVideoSource()->OpenFile("../src/testdata/clip.mp4",
|
||||
&error_message);
|
||||
if (in == nullptr) {
|
||||
ADD_FAILURE() << "open clip.mp4" << error_message;
|
||||
return recording;
|
||||
}
|
||||
|
||||
video_sample_entry_.width = in->stream()->codec->width;
|
||||
video_sample_entry_.height = in->stream()->codec->height;
|
||||
bool need_transform;
|
||||
if (!ParseExtraData(in->extradata(), in->stream()->codec->width,
|
||||
in->stream()->codec->height, &video_sample_entry_.data,
|
||||
&need_transform, &error_message)) {
|
||||
ADD_FAILURE() << "GetH264SampleEntry: " << error_message;
|
||||
return recording;
|
||||
}
|
||||
EXPECT_FALSE(need_transform);
|
||||
|
||||
while (true) {
|
||||
VideoPacket pkt;
|
||||
if (!in->GetNext(&pkt, &error_message)) {
|
||||
if (!error_message.empty()) {
|
||||
ADD_FAILURE() << "GetNext: " << error_message;
|
||||
return recording;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!writer.Write(GetData(pkt), &error_message)) {
|
||||
ADD_FAILURE() << "Write: " << error_message;
|
||||
return recording;
|
||||
}
|
||||
index.AddSample(pkt.pkt()->duration, pkt.pkt()->size, pkt.is_key());
|
||||
}
|
||||
|
||||
if (!writer.Close(&recording.sample_file_sha1, &error_message)) {
|
||||
ADD_FAILURE() << "Close: " << error_message;
|
||||
}
|
||||
return recording;
|
||||
}
|
||||
|
||||
std::shared_ptr<VirtualFile> CreateMp4FromSingleRecording(
|
||||
const Recording &recording, int32_t rel_start_90k, int32_t rel_end_90k,
|
||||
bool include_ts) {
|
||||
Mp4FileBuilder builder(tmpdir_.get());
|
||||
builder.SetSampleEntry(video_sample_entry_);
|
||||
builder.Append(Recording(recording), rel_start_90k, rel_end_90k);
|
||||
builder.include_timestamp_subtitle_track(include_ts);
|
||||
std::string error_message;
|
||||
auto mp4 = builder.Build(&error_message);
|
||||
EXPECT_TRUE(mp4 != nullptr) << error_message;
|
||||
return mp4;
|
||||
}
|
||||
|
||||
void WriteMp4(VirtualFile *f) {
|
||||
EvBuffer buf;
|
||||
std::string error_message;
|
||||
ByteRange left(0, f->size());
|
||||
while (left.size() > 0) {
|
||||
auto ret = f->AddRange(left, &buf, &error_message);
|
||||
ASSERT_GT(ret, 0) << error_message;
|
||||
left.begin += ret;
|
||||
}
|
||||
WriteFileOrDie(StrCat(tmpdir_path_, "/clip.new.mp4"), &buf);
|
||||
}
|
||||
|
||||
void CompareMp4s(int64_t pts_offset) {
|
||||
std::string error_message;
|
||||
auto original = GetRealVideoSource()->OpenFile("../src/testdata/clip.mp4",
|
||||
&error_message);
|
||||
ASSERT_TRUE(original != nullptr) << error_message;
|
||||
auto copied = GetRealVideoSource()->OpenFile(
|
||||
StrCat(tmpdir_path_, "/clip.new.mp4"), &error_message);
|
||||
ASSERT_TRUE(copied != nullptr) << error_message;
|
||||
|
||||
EXPECT_EQ(original->extradata(), copied->extradata());
|
||||
EXPECT_EQ(original->stream()->codec->width, copied->stream()->codec->width);
|
||||
EXPECT_EQ(original->stream()->codec->height,
|
||||
copied->stream()->codec->height);
|
||||
|
||||
int pkt = 0;
|
||||
while (true) {
|
||||
VideoPacket original_pkt;
|
||||
VideoPacket copied_pkt;
|
||||
|
||||
bool original_has_next = original->GetNext(&original_pkt, &error_message);
|
||||
ASSERT_TRUE(original_has_next || error_message.empty())
|
||||
<< "pkt " << pkt << ": " << error_message;
|
||||
bool copied_has_next = copied->GetNext(&copied_pkt, &error_message);
|
||||
ASSERT_TRUE(copied_has_next || error_message.empty())
|
||||
<< "pkt " << pkt << ": " << error_message;
|
||||
if (!original_has_next && !copied_has_next) {
|
||||
break;
|
||||
}
|
||||
ASSERT_TRUE(original_has_next) << "pkt " << pkt;
|
||||
ASSERT_TRUE(copied_has_next) << "pkt " << pkt;
|
||||
EXPECT_EQ(original_pkt.pkt()->pts + pts_offset, copied_pkt.pkt()->pts)
|
||||
<< "pkt " << pkt;
|
||||
|
||||
// One would normally expect the duration to be exactly the same, but
|
||||
// when using an edit list, ffmpeg appears to extend the last packet's
|
||||
// duration by the amount skipped at the beginning. I think this is a
|
||||
// bug on their side.
|
||||
EXPECT_LE(original_pkt.pkt()->duration, copied_pkt.pkt()->duration)
|
||||
<< "pkt " << pkt;
|
||||
EXPECT_EQ(GetData(original_pkt), GetData(copied_pkt)) << "pkt " << pkt;
|
||||
++pkt;
|
||||
}
|
||||
}
|
||||
|
||||
re2::StringPiece GetData(const VideoPacket &pkt) {
|
||||
return re2::StringPiece(reinterpret_cast<const char *>(pkt.pkt()->data),
|
||||
pkt.pkt()->size);
|
||||
}
|
||||
|
||||
std::string tmpdir_path_;
|
||||
std::unique_ptr<File> tmpdir_;
|
||||
std::string etag_;
|
||||
VideoSampleEntry video_sample_entry_;
|
||||
};
|
||||
|
||||
TEST_F(IntegrationTest, RoundTrip) {
|
||||
Recording recording = CopyMp4ToSingleRecording();
|
||||
if (HasFailure()) {
|
||||
return;
|
||||
}
|
||||
auto f = CreateMp4FromSingleRecording(
|
||||
recording, 0, std::numeric_limits<int32_t>::max(), false);
|
||||
WriteMp4(f.get());
|
||||
CompareMp4s(0);
|
||||
|
||||
// This test is brittle, which is the point. Any time the digest comparison
|
||||
// here fails, it can be updated, but the etag must change as well!
|
||||
// Otherwise clients may combine ranges from the new format with ranges
|
||||
// from the old format!
|
||||
EXPECT_EQ("1e5331e8371bd97ac3158b3a86494abc87cdc70e", Digest(f.get()));
|
||||
EXPECT_EQ("\"268db2cd6e4814676d38832f1f9340c7555e4e71\"", f->etag());
|
||||
|
||||
// 10 seconds later than the segment's start time.
|
||||
EXPECT_EQ(1430006410, f->last_modified());
|
||||
}
|
||||
|
||||
TEST_F(IntegrationTest, RoundTripWithSubtitle) {
|
||||
Recording recording = CopyMp4ToSingleRecording();
|
||||
if (HasFailure()) {
|
||||
return;
|
||||
}
|
||||
auto f = CreateMp4FromSingleRecording(
|
||||
recording, 0, std::numeric_limits<int32_t>::max(), true);
|
||||
WriteMp4(f.get());
|
||||
CompareMp4s(0);
|
||||
|
||||
// This test is brittle, which is the point. Any time the digest comparison
|
||||
// here fails, it can be updated, but the etag must change as well!
|
||||
// Otherwise clients may combine ranges from the new format with ranges
|
||||
// from the old format!
|
||||
EXPECT_EQ("0081a442ba73092027fc580eeac2ebf25cb1ef50", Digest(f.get()));
|
||||
EXPECT_EQ("\"8a29042355e1e28c10fbba328d1ddc9d54e450cd\"", f->etag());
|
||||
}
|
||||
|
||||
TEST_F(IntegrationTest, RoundTripWithEditList) {
|
||||
Recording recording = CopyMp4ToSingleRecording();
|
||||
if (HasFailure()) {
|
||||
return;
|
||||
}
|
||||
auto f = CreateMp4FromSingleRecording(
|
||||
recording, 1, std::numeric_limits<int32_t>::max(), false);
|
||||
WriteMp4(f.get());
|
||||
CompareMp4s(-1);
|
||||
|
||||
// This test is brittle, which is the point. Any time the digest comparison
|
||||
// here fails, it can be updated, but the etag must change as well!
|
||||
// Otherwise clients may combine ranges from the new format with ranges
|
||||
// from the old format!
|
||||
EXPECT_EQ("685e026af44204bc9cc52115c5e17058e9fb7c70", Digest(f.get()));
|
||||
EXPECT_EQ("\"1373289ddc7c05580deeeb1f1624e2d6cac7ddd3\"", f->etag());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
FLAGS_alsologtostderr = true;
|
||||
google::ParseCommandLineFlags(&argc, &argv, true);
|
||||
testing::InitGoogleTest(&argc, argv);
|
||||
google::InitGoogleLogging(argv[0]);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
1147
src/mp4.cc
1147
src/mp4.cc
File diff suppressed because it is too large
Load Diff
188
src/mp4.h
188
src/mp4.h
@ -1,188 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// mp4.h: interface for building VirtualFiles representing ISO/IEC 14496-12
|
||||
// (ISO base media format / MPEG-4 / .mp4) video. These can be constructed
|
||||
// from one or more recordings and are suitable for HTTP range serving or
|
||||
// download.
|
||||
|
||||
#ifndef MOONFIRE_NVR_MP4_H
|
||||
#define MOONFIRE_NVR_MP4_H
|
||||
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "recording.h"
|
||||
#include "http.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
namespace internal {
|
||||
|
||||
// Represents pieces of .mp4 sample tables for one recording. Many recordings,
|
||||
// and thus many of these objects, may be spliced together into a single
|
||||
// virtual .mp4 file. For internal use by Mp4FileBuilder. Exposed for testing.
|
||||
class Mp4SampleTablePieces {
|
||||
public:
|
||||
Mp4SampleTablePieces() {}
|
||||
Mp4SampleTablePieces(const Mp4SampleTablePieces &) = delete;
|
||||
void operator=(const Mp4SampleTablePieces &) = delete;
|
||||
|
||||
// |recording| must outlive the Mp4SampleTablePieces.
|
||||
//
|
||||
// |sample_entry_index| should be the (1-based) index into the "stsd" box
|
||||
// of an entry matching this recording's video_sample_entry_sha1. It may
|
||||
// be shared with other recordings.
|
||||
//
|
||||
// |sample_offset| should be the (1-based) index of the first sample in
|
||||
// this file. It should be 1 + the sum of all previous Mp4SampleTablePieces'
|
||||
// samples() values.
|
||||
//
|
||||
// |start_90k| and |end_90k| should be relative to the start of the recording.
|
||||
// They indicate the *desired* time range. The *actual* time range will
|
||||
// start at the last sync sample <= |start_90k|. (The caller is responsible
|
||||
// for creating an edit list to skip the undesired portion.) It will end at
|
||||
// the desired range, or the end of the recording, whichever is sooner.
|
||||
bool Init(const Recording *recording, int sample_entry_index,
|
||||
int32_t sample_offset, int32_t start_90k, int32_t end_90k,
|
||||
std::string *error_message);
|
||||
|
||||
int32_t stts_entry_count() const { return frames_; }
|
||||
const FileSlice *stts_entries() const { return &stts_entries_; }
|
||||
|
||||
int32_t stss_entry_count() const { return key_frames_; }
|
||||
const FileSlice *stss_entries() const { return &stss_entries_; }
|
||||
|
||||
int32_t stsz_entry_count() const { return frames_; }
|
||||
const FileSlice *stsz_entries() const { return &stsz_entries_; }
|
||||
|
||||
int32_t samples() const { return frames_; }
|
||||
|
||||
// Return the byte range in the sample file of the frames represented here.
|
||||
ByteRange sample_pos() const { return sample_pos_; }
|
||||
|
||||
// As described above, these may differ from the desired range.
|
||||
uint64_t duration_90k() const { return actual_end_90k_ - begin_.start_90k(); }
|
||||
int32_t start_90k() const { return begin_.start_90k(); }
|
||||
int32_t end_90k() const { return actual_end_90k_; }
|
||||
|
||||
private:
|
||||
bool FillSttsEntries(std::string *s, std::string *error_message) const;
|
||||
bool FillStssEntries(std::string *s, std::string *error_message) const;
|
||||
bool FillStscEntries(std::string *s, std::string *error_message) const;
|
||||
bool FillStszEntries(std::string *s, std::string *error_message) const;
|
||||
|
||||
// After Init(), |begin_| will be on the first sample after the start of the
|
||||
// range (or it will be done()).
|
||||
SampleIndexIterator begin_;
|
||||
|
||||
ByteRange sample_pos_;
|
||||
|
||||
FillerFileSlice stts_entries_;
|
||||
FillerFileSlice stss_entries_;
|
||||
FillerFileSlice stsz_entries_;
|
||||
|
||||
int sample_entry_index_ = -1;
|
||||
int32_t sample_offset_ = -1;
|
||||
int32_t desired_end_90k_ = -1;
|
||||
int32_t actual_end_90k_ = -1;
|
||||
int32_t frames_ = 0;
|
||||
int32_t key_frames_ = 0;
|
||||
};
|
||||
|
||||
struct Mp4FileSegment {
|
||||
Recording recording;
|
||||
Mp4SampleTablePieces pieces;
|
||||
RealFileSlice sample_file_slice;
|
||||
|
||||
// Requested start time, relative to recording.start_90k.
|
||||
// If there is no key frame at exactly this position, |pieces| will actually
|
||||
// start sooner, and an edit list should be used to skip the undesired
|
||||
// prefix.
|
||||
int32_t rel_start_90k = 0;
|
||||
|
||||
// Requested end time, relative to recording.end_90k.
|
||||
// This will be clamped to the actual duration of the recording.
|
||||
int32_t rel_end_90k = std::numeric_limits<int32_t>::max();
|
||||
};
|
||||
|
||||
} // namespace internal
|
||||
|
||||
// Builder for a virtual .mp4 file.
|
||||
class Mp4FileBuilder {
|
||||
public:
|
||||
// |sample_file_dir| must outlive the Mp4FileBuilder and the returned
|
||||
// VirtualFile.
|
||||
explicit Mp4FileBuilder(File *sample_file_dir)
|
||||
: sample_file_dir_(sample_file_dir) {}
|
||||
Mp4FileBuilder(const Mp4FileBuilder &) = delete;
|
||||
void operator=(const Mp4FileBuilder &) = delete;
|
||||
|
||||
// Append part or all of a recording.
|
||||
// Note that |recording.video_sample_entry_sha1| must be added via
|
||||
// AddSampleEntry.
|
||||
Mp4FileBuilder &Append(Recording &&recording, int32_t rel_start_300ths,
|
||||
int32_t rel_end_300ths);
|
||||
|
||||
// TODO: support multiple sample entries?
|
||||
Mp4FileBuilder &SetSampleEntry(const VideoSampleEntry &entry);
|
||||
|
||||
// Set if a subtitle track should be added with timestamps.
|
||||
Mp4FileBuilder &include_timestamp_subtitle_track(bool v) {
|
||||
include_timestamp_subtitle_track_ = v;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// TODO: MPEG-DASH / ISO BMFF Byte Stream Format support.
|
||||
|
||||
// Build the .mp4 file, returning it to the caller.
|
||||
// The Mp4FileBuilder is left in an undefined state; it should not
|
||||
// be used afterward. On error, nullptr is returned, with |error_message|
|
||||
// populated.
|
||||
//
|
||||
// Errors include:
|
||||
// * TODO: width/height mismatch? or is this okay?
|
||||
// * No segments.
|
||||
// * Non-final segment has zero duration of last sample.
|
||||
// * Data error in one of the recording sample indexes.
|
||||
// * Invalid start/end.
|
||||
std::shared_ptr<VirtualFile> Build(std::string *error_message);
|
||||
|
||||
private:
|
||||
File *sample_file_dir_;
|
||||
std::vector<std::unique_ptr<internal::Mp4FileSegment>> segments_;
|
||||
VideoSampleEntry video_sample_entry_;
|
||||
bool include_timestamp_subtitle_track_ = false;
|
||||
};
|
||||
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
#endif // MOONFIRE_NVR_MP4_H
|
1534
src/mp4.rs
Normal file
1534
src/mp4.rs
Normal file
File diff suppressed because it is too large
Load Diff
295
src/pieces.rs
Normal file
295
src/pieces.rs
Normal file
@ -0,0 +1,295 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use error::{Error, Result};
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::Range;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SliceInfo<W> {
|
||||
end: u64,
|
||||
writer: W,
|
||||
}
|
||||
|
||||
pub trait ContextWriter<Ctx> {
|
||||
fn write_to(&self, ctx: &Ctx, r: Range<u64>, l: u64, out: &mut io::Write) -> Result<()>;
|
||||
}
|
||||
|
||||
/// Calls `f` with an `io::Write` which delegates to `inner` only for the section defined by `r`.
|
||||
/// This is useful for easily implementing the `ContextWriter` interface for pieces that generate
|
||||
/// data on-the-fly rather than simply copying a buffer.
|
||||
pub fn clip_to_range<F>(r: Range<u64>, l: u64, inner: &mut io::Write, mut f: F) -> Result<()>
|
||||
where F: FnMut(&mut Vec<u8>) -> Result<()> {
|
||||
// Just create a buffer for the whole slice and copy out the relevant portion.
|
||||
// One might expect it to be faster to avoid this memory allocation and extra copying, but
|
||||
// benchmarks show when making many 4-byte writes it's better to be able to inline many
|
||||
// Vec::write_all calls then make one call through traits to hyper's write logic.
|
||||
let mut buf = Vec::with_capacity(l as usize);
|
||||
f(&mut buf)?;
|
||||
inner.write_all(&buf[r.start as usize .. r.end as usize])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct Slices<W, C> {
|
||||
len: u64,
|
||||
slices: Vec<SliceInfo<W>>,
|
||||
phantom: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<W, C> fmt::Debug for Slices<W, C> where W: fmt::Debug {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{} slices with overall length {}:", self.slices.len(), self.len)?;
|
||||
let mut start = 0;
|
||||
for (i, s) in self.slices.iter().enumerate() {
|
||||
write!(f, "\n{:7}: [{:12}, {:12}): {:?}", i, start, s.end, s.writer)?;
|
||||
start = s.end;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<W, C> Slices<W, C> where W: ContextWriter<C> {
|
||||
pub fn new() -> Slices<W, C> {
|
||||
Slices{len: 0, slices: Vec::new(), phantom: PhantomData}
|
||||
}
|
||||
|
||||
pub fn reserve(&mut self, additional: usize) {
|
||||
self.slices.reserve(additional)
|
||||
}
|
||||
|
||||
pub fn append(&mut self, len: u64, writer: W) {
|
||||
self.len += len;
|
||||
self.slices.push(SliceInfo{end: self.len, writer: writer});
|
||||
}
|
||||
|
||||
/// Returns the total byte length of all slices.
|
||||
pub fn len(&self) -> u64 { self.len }
|
||||
|
||||
/// Returns the number of slices.
|
||||
pub fn num(&self) -> usize { self.slices.len() }
|
||||
|
||||
pub fn write_to(&self, ctx: &C, range: Range<u64>, out: &mut io::Write)
|
||||
-> Result<()> {
|
||||
if range.start > range.end || range.end > self.len {
|
||||
return Err(Error{
|
||||
description: format!("Bad range {:?} for slice of length {}", range, self.len),
|
||||
cause: None});
|
||||
}
|
||||
|
||||
// Binary search for the first slice of the range to write, determining its index and
|
||||
// (from the preceding slice) the start of its range.
|
||||
let (mut i, mut slice_start) = match self.slices.binary_search_by_key(&range.start,
|
||||
|s| s.end) {
|
||||
Ok(i) if i == self.slices.len() - 1 => return Ok(()), // at end.
|
||||
Ok(i) => (i+1, self.slices[i].end), // desired start == slice i's end; first is i+1!
|
||||
Err(i) if i == 0 => (i, 0), // desired start < slice 0's end; first is 0.
|
||||
Err(i) => (i, self.slices[i-1].end), // desired start < slice i's end; first is i.
|
||||
};
|
||||
|
||||
// Iterate through and write each slice until the end.
|
||||
let mut start_pos = range.start - slice_start;
|
||||
loop {
|
||||
let s = &self.slices[i];
|
||||
let l = s.end - slice_start;
|
||||
if range.end <= s.end { // last slice.
|
||||
return s.writer.write_to(ctx, start_pos .. range.end - slice_start, l, out);
|
||||
}
|
||||
s.writer.write_to(ctx, start_pos .. s.end - slice_start, l, out)?;
|
||||
|
||||
// setup next iteration.
|
||||
start_pos = 0;
|
||||
slice_start = s.end;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use error::{Error, Result};
|
||||
use std::cell::RefCell;
|
||||
use std::error::Error as E;
|
||||
use std::io::Write;
|
||||
use std::ops::Range;
|
||||
use std::vec::Vec;
|
||||
use super::{ContextWriter, Slices, clip_to_range};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct FakeWrite {
|
||||
writer: &'static str,
|
||||
range: Range<u64>,
|
||||
}
|
||||
|
||||
pub struct FakeWriter {
|
||||
name: &'static str,
|
||||
}
|
||||
|
||||
impl ContextWriter<RefCell<Vec<FakeWrite>>> for FakeWriter {
|
||||
fn write_to(&self, ctx: &RefCell<Vec<FakeWrite>>, r: Range<u64>, _l: u64, _out: &mut Write)
|
||||
-> Result<()> {
|
||||
ctx.borrow_mut().push(FakeWrite{writer: self.name, range: r});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_slices() -> Slices<FakeWriter, RefCell<Vec<FakeWrite>>> {
|
||||
let mut s = Slices::new();
|
||||
s.append(5, FakeWriter{name: "a"});
|
||||
s.append(13, FakeWriter{name: "b"});
|
||||
s.append(7, FakeWriter{name: "c"});
|
||||
s.append(17, FakeWriter{name: "d"});
|
||||
s.append(19, FakeWriter{name: "e"});
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn size() {
|
||||
assert_eq!(5 + 13 + 7 + 17 + 19, new_slices().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn exact_slice() {
|
||||
// Test writing exactly slice b.
|
||||
let s = new_slices();
|
||||
let w = RefCell::new(Vec::new());
|
||||
let mut dummy = Vec::new();
|
||||
s.write_to(&w, 5 .. 18, &mut dummy).unwrap();
|
||||
assert_eq!(&[FakeWrite{writer: "b", range: 0 .. 13}], &w.borrow()[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn offset_first() {
|
||||
// Test writing part of slice a.
|
||||
let s = new_slices();
|
||||
let w = RefCell::new(Vec::new());
|
||||
let mut dummy = Vec::new();
|
||||
s.write_to(&w, 1 .. 3, &mut dummy).unwrap();
|
||||
assert_eq!(&[FakeWrite{writer: "a", range: 1 .. 3}], &w.borrow()[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn offset_mid() {
|
||||
// Test writing part of slice b, all of slice c, and part of slice d.
|
||||
let s = new_slices();
|
||||
let w = RefCell::new(Vec::new());
|
||||
let mut dummy = Vec::new();
|
||||
s.write_to(&w, 17 .. 26, &mut dummy).unwrap();
|
||||
assert_eq!(&[
|
||||
FakeWrite{writer: "b", range: 12 .. 13},
|
||||
FakeWrite{writer: "c", range: 0 .. 7},
|
||||
FakeWrite{writer: "d", range: 0 .. 1},
|
||||
], &w.borrow()[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn everything() {
|
||||
// Test writing the whole Slices.
|
||||
let s = new_slices();
|
||||
let w = RefCell::new(Vec::new());
|
||||
let mut dummy = Vec::new();
|
||||
s.write_to(&w, 0 .. 61, &mut dummy).unwrap();
|
||||
assert_eq!(&[
|
||||
FakeWrite{writer: "a", range: 0 .. 5},
|
||||
FakeWrite{writer: "b", range: 0 .. 13},
|
||||
FakeWrite{writer: "c", range: 0 .. 7},
|
||||
FakeWrite{writer: "d", range: 0 .. 17},
|
||||
FakeWrite{writer: "e", range: 0 .. 19},
|
||||
], &w.borrow()[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn at_end() {
|
||||
let s = new_slices();
|
||||
let w = RefCell::new(Vec::new());
|
||||
let mut dummy = Vec::new();
|
||||
s.write_to(&w, 61 .. 61, &mut dummy).unwrap();
|
||||
let empty: &[FakeWrite] = &[];
|
||||
assert_eq!(empty, &w.borrow()[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_clip_to_range() {
|
||||
let mut out = Vec::new();
|
||||
|
||||
// Simple case: one write with everything.
|
||||
clip_to_range(0 .. 5, 5, &mut out, |w| {
|
||||
w.write_all(b"01234").unwrap();
|
||||
Ok(())
|
||||
}).unwrap();
|
||||
assert_eq!(b"01234", &out[..]);
|
||||
|
||||
// Same in a few writes.
|
||||
out.clear();
|
||||
clip_to_range(0 .. 5, 5, &mut out, |w| {
|
||||
w.write_all(b"0").unwrap();
|
||||
w.write_all(b"123").unwrap();
|
||||
w.write_all(b"4").unwrap();
|
||||
Ok(())
|
||||
}).unwrap();
|
||||
assert_eq!(b"01234", &out[..]);
|
||||
|
||||
// Limiting to a prefix.
|
||||
out.clear();
|
||||
clip_to_range(0 .. 2, 5, &mut out, |w| {
|
||||
w.write_all(b"0").unwrap(); // all of this write
|
||||
w.write_all(b"123").unwrap(); // some of this write
|
||||
w.write_all(b"4").unwrap(); // none of this write
|
||||
Ok(())
|
||||
}).unwrap();
|
||||
assert_eq!(b"01", &out[..]);
|
||||
|
||||
// Limiting to part in the middle.
|
||||
out.clear();
|
||||
clip_to_range(2 .. 4, 5, &mut out, |w| {
|
||||
w.write_all(b"0").unwrap(); // none of this write
|
||||
w.write_all(b"1234").unwrap(); // middle of this write
|
||||
w.write_all(b"5678").unwrap(); // none of this write
|
||||
Ok(())
|
||||
}).unwrap();
|
||||
assert_eq!(b"23", &out[..]);
|
||||
|
||||
// If the callback returns an error, it should be propagated (fast path or not).
|
||||
out.clear();
|
||||
assert_eq!(
|
||||
clip_to_range(0 .. 4, 4, &mut out, |_| Err(Error::new("some error".to_owned())))
|
||||
.unwrap_err().description(),
|
||||
"some error");
|
||||
out.clear();
|
||||
assert_eq!(
|
||||
clip_to_range(0 .. 1, 4, &mut out, |_| Err(Error::new("some error".to_owned())))
|
||||
.unwrap_err().description(),
|
||||
"some error");
|
||||
|
||||
// TODO: if inner.write does a partial write, the next try should start at the correct
|
||||
// position.
|
||||
}
|
||||
}
|
145
src/profiler.cc
145
src/profiler.cc
@ -1,145 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// profiler.cc: See profiler.h.
|
||||
|
||||
#include "profiler.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/event.h>
|
||||
#include <event2/http.h>
|
||||
#include <gperftools/profiler.h>
|
||||
#include <glog/logging.h>
|
||||
|
||||
#include "http.h"
|
||||
#include "string.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
namespace {
|
||||
|
||||
const int kDefaultProfileSeconds = 30;
|
||||
|
||||
// Only a single CPU profile may be active at once. Track if it is active now.
|
||||
bool profiling;
|
||||
|
||||
struct ProfileRequestContext {
|
||||
#define TEMPLATE "/tmp/moonfire-nvr.profile.XXXXXX"
|
||||
char filename[sizeof(TEMPLATE)] = TEMPLATE;
|
||||
#undef TEMPLATE
|
||||
evhttp_request *req = nullptr;
|
||||
event *timer = nullptr;
|
||||
int fd = -1;
|
||||
};
|
||||
|
||||
// End a CPU profile. Serve the result from the temporary file and delete it.
|
||||
void EndProfileCallback(evutil_socket_t, short, void *arg) {
|
||||
CHECK(profiling);
|
||||
ProfilerStop();
|
||||
profiling = false;
|
||||
std::unique_ptr<ProfileRequestContext> ctx(
|
||||
reinterpret_cast<ProfileRequestContext *>(arg));
|
||||
if (unlink(ctx->filename) < 0) {
|
||||
int err = errno;
|
||||
LOG(WARNING) << "Unable to unlink temporary profile file: " << ctx->filename
|
||||
<< ": " << strerror(err);
|
||||
}
|
||||
event_free(ctx->timer);
|
||||
struct stat statbuf;
|
||||
if (fstat(ctx->fd, &statbuf) < 0) {
|
||||
close(ctx->fd);
|
||||
return HttpSendError(ctx->req, HTTP_INTERNAL, "fstat: ", errno);
|
||||
}
|
||||
EvBuffer buf;
|
||||
std::string error_message;
|
||||
if (!buf.AddFile(ctx->fd, 0, statbuf.st_size, &error_message)) {
|
||||
evhttp_send_error(ctx->req, HTTP_INTERNAL,
|
||||
EscapeHtml(error_message).c_str());
|
||||
close(ctx->fd);
|
||||
return;
|
||||
}
|
||||
evhttp_send_reply(ctx->req, HTTP_OK, "OK", buf.get());
|
||||
}
|
||||
|
||||
// Start a CPU profile. Creates a temporary file for the profiler library
|
||||
// to use and schedules a call to EndProfileCallback.
|
||||
void StartProfileCallback(struct evhttp_request *req, void *arg) {
|
||||
auto *base = reinterpret_cast<event_base *>(arg);
|
||||
if (evhttp_request_get_command(req) != EVHTTP_REQ_GET) {
|
||||
return evhttp_send_error(req, HTTP_BADMETHOD, "only GET allowed");
|
||||
}
|
||||
if (profiling) {
|
||||
return evhttp_send_error(req, HTTP_SERVUNAVAIL,
|
||||
"Profiling already in progress");
|
||||
}
|
||||
struct timeval timeout = {0, 0};
|
||||
QueryParameters params(evhttp_request_get_uri(req));
|
||||
const char *seconds_value = params.Get("seconds");
|
||||
timeout.tv_sec =
|
||||
seconds_value == nullptr ? kDefaultProfileSeconds : atoi(seconds_value);
|
||||
if (timeout.tv_sec <= 0) {
|
||||
return evhttp_send_error(req, HTTP_BADREQUEST, "invalid seconds");
|
||||
}
|
||||
|
||||
auto *ctx = new ProfileRequestContext;
|
||||
ctx->fd = mkstemp(ctx->filename);
|
||||
if (ctx->fd < 0) {
|
||||
delete ctx;
|
||||
return HttpSendError(req, HTTP_INTERNAL, "mkstemp: ", errno);
|
||||
}
|
||||
|
||||
if (ProfilerStart(ctx->filename) == 0) {
|
||||
delete ctx;
|
||||
return evhttp_send_error(req, HTTP_INTERNAL, "ProfilerStart failed");
|
||||
}
|
||||
profiling = true;
|
||||
ctx->req = req;
|
||||
ctx->timer = evtimer_new(base, &EndProfileCallback, ctx);
|
||||
evtimer_add(ctx->timer, &timeout);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RegisterProfiler(event_base *base, evhttp *http) {
|
||||
evhttp_set_cb(http, "/pprof/profile", &StartProfileCallback, base);
|
||||
}
|
||||
|
||||
} // namespace moonfire_nvr
|
@ -1,51 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// profiler.h: support for on-demand profiling. The interface is described here:
|
||||
// <https://github.com/gperftools/gperftools/blob/master/doc/pprof_remote_servers.html>
|
||||
// Currently this only supports CPU profiling; heap profiling may be added
|
||||
// later.
|
||||
|
||||
#ifndef MOONFIRE_NVR_PROFILER_H
|
||||
#define MOONFIRE_NVR_PROFILER_H
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/http.h>
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
// Register a HTTP URI for a CPU profile with google-perftools.
|
||||
// Not thread-safe, so |http| must be running on |base|, and |base| must be
|
||||
// using a single thread.
|
||||
void RegisterProfiler(event_base *base, evhttp *http);
|
||||
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
#endif // MOONFIRE_NVR_PROFILER_H
|
@ -1,66 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// recording-bench.cc: benchmarks of the recording.h interface.
|
||||
|
||||
#include <benchmark/benchmark.h>
|
||||
#include <gflags/gflags.h>
|
||||
|
||||
#include "recording.h"
|
||||
#include "testutil.h"
|
||||
|
||||
DECLARE_bool(alsologtostderr);
|
||||
|
||||
static void BM_Iterator(benchmark::State &state) {
|
||||
using moonfire_nvr::ReadFileOrDie;
|
||||
using moonfire_nvr::SampleIndexIterator;
|
||||
// state.PauseTiming();
|
||||
std::string index = ReadFileOrDie("../src/testdata/video_sample_index.bin");
|
||||
// state.ResumeTiming();
|
||||
while (state.KeepRunning()) {
|
||||
SampleIndexIterator it(index);
|
||||
while (!it.done()) it.Next();
|
||||
CHECK(!it.has_error()) << it.error();
|
||||
}
|
||||
state.SetBytesProcessed(int64_t(state.iterations()) * int64_t(index.size()));
|
||||
}
|
||||
BENCHMARK(BM_Iterator);
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
FLAGS_alsologtostderr = true;
|
||||
|
||||
// Sadly, these two flag-parsing libraries don't appear to get along.
|
||||
// google::ParseCommandLineFlags(&argc, &argv, true);
|
||||
benchmark::Initialize(&argc, argv);
|
||||
|
||||
google::InitGoogleLogging(argv[0]);
|
||||
benchmark::RunSpecifiedBenchmarks();
|
||||
return 0;
|
||||
}
|
@ -1,264 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// recording-test.cc: tests of the recording.h interface.
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "recording.h"
|
||||
#include "string.h"
|
||||
#include "testutil.h"
|
||||
|
||||
DECLARE_bool(alsologtostderr);
|
||||
|
||||
using testing::_;
|
||||
using testing::HasSubstr;
|
||||
using testing::DoAll;
|
||||
using testing::Return;
|
||||
using testing::SetArgPointee;
|
||||
|
||||
namespace moonfire_nvr {
|
||||
namespace {
|
||||
|
||||
// Example from design/schema.md.
|
||||
TEST(SampleIndexTest, EncodeExample) {
|
||||
Recording recording;
|
||||
SampleIndexEncoder encoder;
|
||||
encoder.Init(&recording, 1000);
|
||||
encoder.AddSample(10, 1000, true);
|
||||
encoder.AddSample(9, 10, false);
|
||||
encoder.AddSample(11, 15, false);
|
||||
encoder.AddSample(10, 12, false);
|
||||
encoder.AddSample(10, 1050, true);
|
||||
EXPECT_EQ("29 d0 0f 02 14 08 0a 02 05 01 64",
|
||||
ToHex(recording.video_index, true));
|
||||
EXPECT_EQ(1000, recording.start_time_90k);
|
||||
EXPECT_EQ(1000 + 10 + 9 + 11 + 10 + 10, recording.end_time_90k);
|
||||
EXPECT_EQ(1000 + 10 + 15 + 12 + 1050, recording.sample_file_bytes);
|
||||
EXPECT_EQ(5, recording.video_samples);
|
||||
EXPECT_EQ(2, recording.video_sync_samples);
|
||||
}
|
||||
|
||||
TEST(SampleIndexTest, RoundTrip) {
|
||||
Recording recording;
|
||||
SampleIndexEncoder encoder;
|
||||
encoder.Init(&recording, 1000);
|
||||
encoder.AddSample(10, 30000, true);
|
||||
encoder.AddSample(9, 1000, false);
|
||||
encoder.AddSample(11, 1100, false);
|
||||
encoder.AddSample(18, 31000, true);
|
||||
|
||||
SampleIndexIterator it = SampleIndexIterator(recording.video_index);
|
||||
std::string error_message;
|
||||
ASSERT_FALSE(it.done()) << it.error();
|
||||
EXPECT_EQ(10, it.duration_90k());
|
||||
EXPECT_EQ(30000, it.bytes());
|
||||
EXPECT_TRUE(it.is_key());
|
||||
|
||||
it.Next();
|
||||
ASSERT_FALSE(it.done()) << it.error();
|
||||
EXPECT_EQ(9, it.duration_90k());
|
||||
EXPECT_EQ(1000, it.bytes());
|
||||
EXPECT_FALSE(it.is_key());
|
||||
|
||||
it.Next();
|
||||
ASSERT_FALSE(it.done()) << it.error();
|
||||
EXPECT_EQ(11, it.duration_90k());
|
||||
EXPECT_EQ(1100, it.bytes());
|
||||
EXPECT_FALSE(it.is_key());
|
||||
|
||||
it.Next();
|
||||
ASSERT_FALSE(it.done()) << it.error();
|
||||
EXPECT_EQ(18, it.duration_90k());
|
||||
EXPECT_EQ(31000, it.bytes());
|
||||
EXPECT_TRUE(it.is_key());
|
||||
|
||||
it.Next();
|
||||
ASSERT_TRUE(it.done());
|
||||
ASSERT_FALSE(it.has_error()) << it.error();
|
||||
}
|
||||
|
||||
TEST(SampleIndexTest, IteratorErrors) {
|
||||
std::string bad_first_varint("\x80");
|
||||
SampleIndexIterator it(bad_first_varint);
|
||||
EXPECT_TRUE(it.has_error());
|
||||
EXPECT_EQ("buffer underrun", it.error());
|
||||
|
||||
std::string bad_second_varint("\x00\x80", 2);
|
||||
it = SampleIndexIterator(bad_second_varint);
|
||||
EXPECT_TRUE(it.has_error());
|
||||
EXPECT_EQ("buffer underrun", it.error());
|
||||
|
||||
std::string zero_durations("\x00\x02\x00\x00", 4);
|
||||
it = SampleIndexIterator(zero_durations);
|
||||
EXPECT_TRUE(it.has_error());
|
||||
EXPECT_THAT(it.error(), HasSubstr("zero duration"));
|
||||
|
||||
std::string negative_duration("\x02\x02", 2);
|
||||
it = SampleIndexIterator(negative_duration);
|
||||
EXPECT_TRUE(it.has_error());
|
||||
EXPECT_THAT(it.error(), HasSubstr("negative duration"));
|
||||
|
||||
std::string non_positive_bytes("\x04\x00", 2);
|
||||
it = SampleIndexIterator(non_positive_bytes);
|
||||
EXPECT_TRUE(it.has_error());
|
||||
EXPECT_THAT(it.error(), HasSubstr("non-positive bytes"));
|
||||
}
|
||||
|
||||
TEST(SampleFileWriterTest, Simple) {
|
||||
testing::StrictMock<MockFile> parent;
|
||||
auto *f = new testing::StrictMock<MockFile>;
|
||||
|
||||
re2::StringPiece write_1("write 1");
|
||||
re2::StringPiece write_2("write 2");
|
||||
|
||||
EXPECT_CALL(parent, OpenRaw("foo", O_WRONLY | O_EXCL | O_CREAT, 0600, _))
|
||||
.WillOnce(DoAll(SetArgPointee<3>(f), Return(0)));
|
||||
EXPECT_CALL(*f, Write(write_1, _))
|
||||
.WillOnce(DoAll(SetArgPointee<1>(7), Return(0)));
|
||||
EXPECT_CALL(*f, Write(write_2, _))
|
||||
.WillOnce(DoAll(SetArgPointee<1>(7), Return(0)));
|
||||
EXPECT_CALL(*f, Sync()).WillOnce(Return(0));
|
||||
EXPECT_CALL(*f, Close()).WillOnce(Return(0));
|
||||
|
||||
SampleFileWriter writer(&parent);
|
||||
std::string error_message;
|
||||
std::string sha1;
|
||||
ASSERT_TRUE(writer.Open("foo", &error_message)) << error_message;
|
||||
EXPECT_TRUE(writer.Write(write_1, &error_message)) << error_message;
|
||||
EXPECT_TRUE(writer.Write(write_2, &error_message)) << error_message;
|
||||
EXPECT_TRUE(writer.Close(&sha1, &error_message)) << error_message;
|
||||
EXPECT_EQ("6bc37325b36fb5fd205e57284429e75764338618", ToHex(sha1));
|
||||
}
|
||||
|
||||
TEST(SampleFileWriterTest, PartialWriteIsRetried) {
|
||||
testing::StrictMock<MockFile> parent;
|
||||
auto *f = new testing::StrictMock<MockFile>;
|
||||
|
||||
re2::StringPiece write_1("write 1");
|
||||
re2::StringPiece write_2("write 2");
|
||||
re2::StringPiece write_2b(write_2);
|
||||
write_2b.remove_prefix(3);
|
||||
|
||||
EXPECT_CALL(parent, OpenRaw("foo", O_WRONLY | O_EXCL | O_CREAT, 0600, _))
|
||||
.WillOnce(DoAll(SetArgPointee<3>(f), Return(0)));
|
||||
EXPECT_CALL(*f, Write(write_1, _))
|
||||
.WillOnce(DoAll(SetArgPointee<1>(7), Return(0)));
|
||||
EXPECT_CALL(*f, Write(write_2, _))
|
||||
.WillOnce(DoAll(SetArgPointee<1>(3), Return(0)));
|
||||
EXPECT_CALL(*f, Write(write_2b, _))
|
||||
.WillOnce(DoAll(SetArgPointee<1>(4), Return(0)));
|
||||
EXPECT_CALL(*f, Sync()).WillOnce(Return(0));
|
||||
EXPECT_CALL(*f, Close()).WillOnce(Return(0));
|
||||
|
||||
SampleFileWriter writer(&parent);
|
||||
std::string error_message;
|
||||
std::string sha1;
|
||||
ASSERT_TRUE(writer.Open("foo", &error_message)) << error_message;
|
||||
EXPECT_TRUE(writer.Write(write_1, &error_message)) << error_message;
|
||||
EXPECT_TRUE(writer.Write(write_2, &error_message)) << error_message;
|
||||
EXPECT_TRUE(writer.Close(&sha1, &error_message)) << error_message;
|
||||
EXPECT_EQ("6bc37325b36fb5fd205e57284429e75764338618", ToHex(sha1));
|
||||
}
|
||||
|
||||
TEST(SampleFileWriterTest, PartialWriteIsTruncated) {
|
||||
testing::StrictMock<MockFile> parent;
|
||||
auto *f = new testing::StrictMock<MockFile>;
|
||||
|
||||
re2::StringPiece write_1("write 1");
|
||||
re2::StringPiece write_2("write 2");
|
||||
re2::StringPiece write_2b(write_2);
|
||||
write_2b.remove_prefix(3);
|
||||
|
||||
EXPECT_CALL(parent, OpenRaw("foo", O_WRONLY | O_EXCL | O_CREAT, 0600, _))
|
||||
.WillOnce(DoAll(SetArgPointee<3>(f), Return(0)));
|
||||
EXPECT_CALL(*f, Write(write_1, _))
|
||||
.WillOnce(DoAll(SetArgPointee<1>(7), Return(0)));
|
||||
EXPECT_CALL(*f, Write(write_2, _))
|
||||
.WillOnce(DoAll(SetArgPointee<1>(3), Return(0)));
|
||||
EXPECT_CALL(*f, Write(write_2b, _)).WillOnce(Return(ENOSPC));
|
||||
EXPECT_CALL(*f, Truncate(7)).WillOnce(Return(0));
|
||||
EXPECT_CALL(*f, Sync()).WillOnce(Return(0));
|
||||
EXPECT_CALL(*f, Close()).WillOnce(Return(0));
|
||||
|
||||
SampleFileWriter writer(&parent);
|
||||
std::string error_message;
|
||||
std::string sha1;
|
||||
ASSERT_TRUE(writer.Open("foo", &error_message)) << error_message;
|
||||
EXPECT_TRUE(writer.Write(write_1, &error_message)) << error_message;
|
||||
EXPECT_FALSE(writer.Write(write_2, &error_message)) << error_message;
|
||||
EXPECT_TRUE(writer.Close(&sha1, &error_message)) << error_message;
|
||||
EXPECT_EQ("b1ccee339b935587c09997a9ec8bb2374e02b5d0", ToHex(sha1));
|
||||
}
|
||||
|
||||
TEST(SampleFileWriterTest, PartialWriteTruncateFailureCausesCloseToFail) {
|
||||
testing::StrictMock<MockFile> parent;
|
||||
auto *f = new testing::StrictMock<MockFile>;
|
||||
|
||||
re2::StringPiece write_1("write 1");
|
||||
re2::StringPiece write_2("write 2");
|
||||
re2::StringPiece write_2b(write_2);
|
||||
write_2b.remove_prefix(3);
|
||||
|
||||
EXPECT_CALL(parent, OpenRaw("foo", O_WRONLY | O_EXCL | O_CREAT, 0600, _))
|
||||
.WillOnce(DoAll(SetArgPointee<3>(f), Return(0)));
|
||||
EXPECT_CALL(*f, Write(write_1, _))
|
||||
.WillOnce(DoAll(SetArgPointee<1>(7), Return(0)));
|
||||
EXPECT_CALL(*f, Write(write_2, _))
|
||||
.WillOnce(DoAll(SetArgPointee<1>(3), Return(0)));
|
||||
EXPECT_CALL(*f, Write(write_2b, _)).WillOnce(Return(EIO));
|
||||
EXPECT_CALL(*f, Truncate(7)).WillOnce(Return(EIO));
|
||||
EXPECT_CALL(*f, Close()).WillOnce(Return(0));
|
||||
|
||||
SampleFileWriter writer(&parent);
|
||||
std::string error_message;
|
||||
std::string sha1;
|
||||
ASSERT_TRUE(writer.Open("foo", &error_message)) << error_message;
|
||||
EXPECT_TRUE(writer.Write(write_1, &error_message)) << error_message;
|
||||
EXPECT_FALSE(writer.Write(write_2, &error_message)) << error_message;
|
||||
EXPECT_FALSE(writer.Close(&sha1, &error_message)) << error_message;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
FLAGS_alsologtostderr = true;
|
||||
google::ParseCommandLineFlags(&argc, &argv, true);
|
||||
testing::InitGoogleTest(&argc, argv);
|
||||
google::InitGoogleLogging(argv[0]);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
229
src/recording.cc
229
src/recording.cc
@ -1,229 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2015 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// recording.cc: see recording.h.
|
||||
|
||||
#include "recording.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "common.h"
|
||||
#include "coding.h"
|
||||
#include "string.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
void SampleIndexEncoder::Init(Recording *recording, int64_t start_time_90k) {
|
||||
recording_ = recording;
|
||||
recording_->start_time_90k = start_time_90k;
|
||||
recording_->end_time_90k = start_time_90k;
|
||||
recording_->sample_file_bytes = 0;
|
||||
recording_->video_samples = 0;
|
||||
recording_->video_sync_samples = 0;
|
||||
recording_->video_index.clear();
|
||||
prev_duration_90k_ = 0;
|
||||
prev_bytes_key_ = 0;
|
||||
prev_bytes_nonkey_ = 0;
|
||||
}
|
||||
|
||||
void SampleIndexEncoder::AddSample(int32_t duration_90k, int32_t bytes,
|
||||
bool is_key) {
|
||||
CHECK_GE(duration_90k, 0);
|
||||
CHECK_GT(bytes, 0);
|
||||
int32_t duration_delta = duration_90k - prev_duration_90k_;
|
||||
prev_duration_90k_ = duration_90k;
|
||||
int32_t bytes_delta;
|
||||
recording_->end_time_90k += duration_90k;
|
||||
recording_->sample_file_bytes += bytes;
|
||||
++recording_->video_samples;
|
||||
if (is_key) {
|
||||
bytes_delta = bytes - prev_bytes_key_;
|
||||
prev_bytes_key_ = bytes;
|
||||
++recording_->video_sync_samples;
|
||||
} else {
|
||||
bytes_delta = bytes - prev_bytes_nonkey_;
|
||||
prev_bytes_nonkey_ = bytes;
|
||||
}
|
||||
uint32_t zigzagged_bytes_delta = Zigzag32(bytes_delta);
|
||||
AppendVar32((Zigzag32(duration_delta) << 1) | is_key,
|
||||
&recording_->video_index);
|
||||
AppendVar32(zigzagged_bytes_delta, &recording_->video_index);
|
||||
}
|
||||
|
||||
void SampleIndexIterator::Next() {
|
||||
uint32_t raw1;
|
||||
uint32_t raw2;
|
||||
pos_ += bytes_;
|
||||
if (UNLIKELY(data_.empty()) ||
|
||||
UNLIKELY(!DecodeVar32(&data_, &raw1, &error_)) ||
|
||||
UNLIKELY(!DecodeVar32(&data_, &raw2, &error_))) {
|
||||
done_ = true;
|
||||
return;
|
||||
}
|
||||
start_90k_ += duration_90k_;
|
||||
int32_t duration_90k_delta = Unzigzag32(raw1 >> 1);
|
||||
duration_90k_ += duration_90k_delta;
|
||||
if (UNLIKELY(duration_90k_ < 0)) {
|
||||
error_ = StrCat("negative duration ", duration_90k_,
|
||||
" after applying delta ", duration_90k_delta);
|
||||
done_ = true;
|
||||
return;
|
||||
}
|
||||
if (UNLIKELY(duration_90k_ == 0 && !data_.empty())) {
|
||||
error_ = StrCat("zero duration only allowed at end; have ", data_.size(),
|
||||
"bytes left.");
|
||||
done_ = true;
|
||||
return;
|
||||
}
|
||||
is_key_ = raw1 & 0x01;
|
||||
int32_t bytes_delta = Unzigzag32(raw2);
|
||||
if (UNLIKELY(is_key_)) {
|
||||
bytes_ = bytes_key_ += bytes_delta;
|
||||
} else {
|
||||
bytes_ = bytes_nonkey_ += bytes_delta;
|
||||
}
|
||||
if (UNLIKELY(bytes_ <= 0)) {
|
||||
error_ = StrCat("non-positive bytes ", bytes_, " after applying delta ",
|
||||
bytes_delta, " to ", (is_key_ ? "key" : "non-key"),
|
||||
" frame at ts ", start_90k_);
|
||||
done_ = true;
|
||||
return;
|
||||
}
|
||||
done_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
void SampleIndexIterator::Clear() {
|
||||
data_.clear();
|
||||
error_.clear();
|
||||
pos_ = 0;
|
||||
start_90k_ = 0;
|
||||
duration_90k_ = 0;
|
||||
bytes_key_ = 0;
|
||||
bytes_nonkey_ = 0;
|
||||
bytes_ = 0;
|
||||
is_key_ = false;
|
||||
done_ = true;
|
||||
}
|
||||
|
||||
SampleFileWriter::SampleFileWriter(File *parent_dir)
|
||||
: parent_dir_(parent_dir), sha1_(Digest::SHA1()) {}
|
||||
|
||||
bool SampleFileWriter::Open(const char *filename, std::string *error_message) {
|
||||
if (is_open()) {
|
||||
*error_message = "already open!";
|
||||
return false;
|
||||
}
|
||||
int ret =
|
||||
parent_dir_->Open(filename, O_WRONLY | O_CREAT | O_EXCL, 0600, &file_);
|
||||
if (ret != 0) {
|
||||
*error_message = StrCat("open ", filename, " (within dir ",
|
||||
parent_dir_->name(), "): ", strerror(ret));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SampleFileWriter::Write(re2::StringPiece pkt, std::string *error_message) {
|
||||
if (!is_open()) {
|
||||
*error_message = "not open!";
|
||||
return false;
|
||||
}
|
||||
auto old_pos = pos_;
|
||||
re2::StringPiece remaining(pkt);
|
||||
while (!remaining.empty()) {
|
||||
size_t written;
|
||||
int write_ret = file_->Write(remaining, &written);
|
||||
if (write_ret != 0) {
|
||||
if (pos_ > old_pos) {
|
||||
int truncate_ret = file_->Truncate(old_pos);
|
||||
if (truncate_ret != 0) {
|
||||
*error_message =
|
||||
StrCat("write failed with: ", strerror(write_ret),
|
||||
" and ftruncate failed with: ", strerror(truncate_ret));
|
||||
corrupt_ = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
*error_message = StrCat("write: ", strerror(write_ret));
|
||||
return false;
|
||||
}
|
||||
remaining.remove_prefix(written);
|
||||
pos_ += written;
|
||||
}
|
||||
sha1_->Update(pkt);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SampleFileWriter::Close(std::string *sha1, std::string *error_message) {
|
||||
if (!is_open()) {
|
||||
*error_message = "not open!";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (corrupt_) {
|
||||
*error_message = "File already corrupted.";
|
||||
} else {
|
||||
int ret = file_->Sync();
|
||||
if (ret != 0) {
|
||||
*error_message = StrCat("fsync failed with: ", strerror(ret));
|
||||
corrupt_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
int ret = file_->Close();
|
||||
if (ret != 0 && !corrupt_) {
|
||||
corrupt_ = true;
|
||||
*error_message = StrCat("close failed with: ", strerror(ret));
|
||||
}
|
||||
|
||||
bool ok = !corrupt_;
|
||||
file_.reset();
|
||||
*sha1 = sha1_->Finalize();
|
||||
sha1_ = Digest::SHA1();
|
||||
pos_ = 0;
|
||||
corrupt_ = false;
|
||||
return ok;
|
||||
}
|
||||
|
||||
std::string PrettyTimestamp(int64_t ts_90k) {
|
||||
struct tm mytm;
|
||||
memset(&mytm, 0, sizeof(mytm));
|
||||
time_t ts = ts_90k / kTimeUnitsPerSecond;
|
||||
localtime_r(&ts, &mytm);
|
||||
const size_t kTimeBufLen = 50;
|
||||
char tmbuf[kTimeBufLen];
|
||||
strftime(tmbuf, kTimeBufLen, "%a, %d %b %Y %H:%M:%S %Z", &mytm);
|
||||
return tmbuf;
|
||||
}
|
||||
|
||||
} // namespace moonfire_nvr
|
214
src/recording.h
214
src/recording.h
@ -1,214 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// recording.h: Write and read recordings. See design/schema.md for a
|
||||
// description of the storage schema.
|
||||
|
||||
#ifndef MOONFIRE_NVR_RECORDING_H
|
||||
#define MOONFIRE_NVR_RECORDING_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <glog/logging.h>
|
||||
#include <re2/stringpiece.h>
|
||||
|
||||
#include "crypto.h"
|
||||
#include "filesystem.h"
|
||||
#include "uuid.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
constexpr int64_t kTimeUnitsPerSecond = 90000;
|
||||
|
||||
// Recordings are never longer than this (5 minutes).
|
||||
// Having such a limit dramatically speeds up some SQL queries.
|
||||
// This limit should be more than the normal rotation time,
|
||||
// as recording doesn't happen until the next key frame.
|
||||
// 5 minutes is generously more than 1 minute, but still sufficient to
|
||||
// allow the optimization to be useful. This value must match the CHECK
|
||||
// constraint on duration_90k in schema.sql.
|
||||
constexpr int64_t kMaxRecordingDuration = 5 * 60 * kTimeUnitsPerSecond;
|
||||
|
||||
// Various fields from the "recording" table which are useful when viewing
|
||||
// recordings.
|
||||
struct Recording {
|
||||
int64_t id = -1;
|
||||
int64_t camera_id = -1;
|
||||
std::string sample_file_sha1;
|
||||
std::string sample_file_path;
|
||||
Uuid sample_file_uuid;
|
||||
int64_t video_sample_entry_id = -1;
|
||||
int64_t local_time_90k = -1;
|
||||
|
||||
// Fields populated by SampleIndexEncoder.
|
||||
int64_t start_time_90k = -1;
|
||||
int64_t end_time_90k = -1;
|
||||
int64_t sample_file_bytes = -1;
|
||||
int64_t video_samples = -1;
|
||||
int64_t video_sync_samples = -1;
|
||||
std::string video_index;
|
||||
};
|
||||
|
||||
// Reusable object to encode sample index data to a Recording object.
|
||||
class SampleIndexEncoder {
|
||||
public:
|
||||
SampleIndexEncoder() {}
|
||||
SampleIndexEncoder(const SampleIndexEncoder &) = delete;
|
||||
void operator=(const SampleIndexEncoder &) = delete;
|
||||
|
||||
void Init(Recording *recording, int64_t start_time_90k);
|
||||
void AddSample(int32_t duration_90k, int32_t bytes, bool is_key);
|
||||
|
||||
private:
|
||||
Recording *recording_;
|
||||
int32_t prev_duration_90k_ = 0;
|
||||
int32_t prev_bytes_key_ = 0;
|
||||
int32_t prev_bytes_nonkey_ = 0;
|
||||
};
|
||||
|
||||
// Iterates through an encoded index, decoding on the fly. Copyable.
|
||||
// Example usage:
|
||||
//
|
||||
// SampleIndexIterator it;
|
||||
// for (it = index; !it.done(); it.Next()) {
|
||||
// LOG(INFO) << "sample size: " << it.bytes();
|
||||
// }
|
||||
// if (it.has_error()) {
|
||||
// LOG(ERROR) << "error: " << it.error();
|
||||
// }
|
||||
class SampleIndexIterator {
|
||||
public:
|
||||
SampleIndexIterator() { Clear(); }
|
||||
|
||||
// |index| must outlive the iterator.
|
||||
explicit SampleIndexIterator(re2::StringPiece index) {
|
||||
Clear();
|
||||
data_ = index;
|
||||
Next();
|
||||
}
|
||||
|
||||
// Iteration control.
|
||||
void Next();
|
||||
bool done() const { return done_; }
|
||||
bool has_error() const { return !error_.empty(); }
|
||||
const std::string &error() const { return error_; }
|
||||
|
||||
// Return properties of the current sample.
|
||||
// Note pos() and start_90k() are valid when done(); the others are not.
|
||||
int64_t pos() const { return pos_; }
|
||||
int32_t start_90k() const { return start_90k_; }
|
||||
int32_t duration_90k() const {
|
||||
DCHECK(!done_);
|
||||
return duration_90k_;
|
||||
}
|
||||
int32_t end_90k() const { return start_90k_ + duration_90k(); }
|
||||
int32_t bytes() const {
|
||||
DCHECK(!done_);
|
||||
return bytes_;
|
||||
}
|
||||
bool is_key() const {
|
||||
DCHECK(!done_);
|
||||
return is_key_;
|
||||
}
|
||||
|
||||
private:
|
||||
void Clear();
|
||||
|
||||
re2::StringPiece data_;
|
||||
std::string error_;
|
||||
int64_t pos_;
|
||||
int32_t start_90k_;
|
||||
int32_t duration_90k_;
|
||||
int32_t bytes_; // bytes taken by the current sample, or 0 after Clear().
|
||||
int32_t bytes_key_;
|
||||
int32_t bytes_nonkey_;
|
||||
bool is_key_;
|
||||
bool done_;
|
||||
};
|
||||
|
||||
// Writes a sample file. Can be used repeatedly. Thread-compatible.
|
||||
class SampleFileWriter {
|
||||
public:
|
||||
// |parent_dir| must outlive the writer.
|
||||
SampleFileWriter(File *parent_dir);
|
||||
SampleFileWriter(const SampleFileWriter &) = delete;
|
||||
void operator=(const SampleFileWriter &) = delete;
|
||||
|
||||
// PRE: !is_open().
|
||||
bool Open(const char *filename, std::string *error_message);
|
||||
|
||||
// Writes a single packet, returning success.
|
||||
// On failure, the stream should be closed. If Close() returns true, the
|
||||
// file contains the results of all packets up to (but not including) this
|
||||
// one.
|
||||
//
|
||||
// PRE: is_open().
|
||||
bool Write(re2::StringPiece pkt, std::string *error_message);
|
||||
|
||||
// fsync() and close() the stream.
|
||||
// Note the caller is still responsible for fsync()ing the parent stream,
|
||||
// so that operations can be batched.
|
||||
// On success, |sha1| will be filled with the raw SHA-1 hash of the file.
|
||||
// On failure, the file should be considered corrupt and discarded.
|
||||
//
|
||||
// PRE: is_open().
|
||||
bool Close(std::string *sha1, std::string *error_message);
|
||||
|
||||
bool is_open() const { return file_ != nullptr; }
|
||||
|
||||
private:
|
||||
File *parent_dir_;
|
||||
std::unique_ptr<File> file_;
|
||||
std::unique_ptr<Digest> sha1_;
|
||||
int64_t pos_ = 0;
|
||||
bool corrupt_ = false;
|
||||
};
|
||||
|
||||
struct VideoSampleEntry {
|
||||
int64_t id = -1;
|
||||
std::string sha1;
|
||||
std::string data;
|
||||
uint16_t width = 0;
|
||||
uint16_t height = 0;
|
||||
};
|
||||
|
||||
std::string PrettyTimestamp(int64_t ts_90k);
|
||||
|
||||
inline int64_t To90k(const struct timespec &ts) {
|
||||
return (ts.tv_sec * kTimeUnitsPerSecond) +
|
||||
(ts.tv_nsec * kTimeUnitsPerSecond / 1000000000);
|
||||
}
|
||||
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
#endif // MOONFIRE_NVR_RECORDING_H
|
726
src/recording.rs
Normal file
726
src/recording.rs
Normal file
@ -0,0 +1,726 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#![allow(inline_always)]
|
||||
|
||||
extern crate uuid;
|
||||
|
||||
use db;
|
||||
use std::ops;
|
||||
use error::Error;
|
||||
use openssl::crypto::hash;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::ops::Range;
|
||||
use std::string::String;
|
||||
use time;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub const TIME_UNITS_PER_SEC: i64 = 90000;
|
||||
pub const DESIRED_RECORDING_DURATION: i64 = 60 * TIME_UNITS_PER_SEC;
|
||||
pub const MAX_RECORDING_DURATION: i64 = 5 * 60 * TIME_UNITS_PER_SEC;
|
||||
|
||||
/// A time specified as 90,000ths of a second since 1970-01-01 00:00:00 UTC.
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct Time(pub i64);
|
||||
|
||||
impl Time {
|
||||
pub fn new(tm: time::Timespec) -> Self {
|
||||
Time(tm.sec * TIME_UNITS_PER_SEC + tm.nsec as i64 * TIME_UNITS_PER_SEC / 1_000_000_000)
|
||||
}
|
||||
|
||||
pub fn unix_seconds(&self) -> i64 { self.0 / TIME_UNITS_PER_SEC }
|
||||
}
|
||||
|
||||
impl ops::Sub for Time {
|
||||
type Output = Duration;
|
||||
fn sub(self, rhs: Time) -> Duration { Duration(self.0 - rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::AddAssign<Duration> for Time {
|
||||
fn add_assign(&mut self, rhs: Duration) { self.0 += rhs.0 }
|
||||
}
|
||||
|
||||
impl ops::Add<Duration> for Time {
|
||||
type Output = Time;
|
||||
fn add(self, rhs: Duration) -> Time { Time(self.0 + rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Sub<Duration> for Time {
|
||||
type Output = Time;
|
||||
fn sub(self, rhs: Duration) -> Time { Time(self.0 - rhs.0) }
|
||||
}
|
||||
|
||||
impl fmt::Display for Time {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let tm = time::at(time::Timespec{sec: self.0 / TIME_UNITS_PER_SEC, nsec: 0});
|
||||
write!(f, "{}:{:05}", tm.strftime("%FT%T%Z").or_else(|_| Err(fmt::Error))?,
|
||||
self.0 % TIME_UNITS_PER_SEC)
|
||||
}
|
||||
}
|
||||
|
||||
/// A duration specified in 1/90,000ths of a second.
|
||||
/// Durations are typically non-negative, but a `db::CameraDayValue::duration` may be negative.
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct Duration(pub i64);
|
||||
|
||||
impl fmt::Display for Duration {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mut seconds = self.0 / TIME_UNITS_PER_SEC;
|
||||
const MINUTE_IN_SECONDS: i64 = 60;
|
||||
const HOUR_IN_SECONDS: i64 = 60 * MINUTE_IN_SECONDS;
|
||||
const DAY_IN_SECONDS: i64 = 24 * HOUR_IN_SECONDS;
|
||||
let days = seconds / DAY_IN_SECONDS;
|
||||
seconds %= DAY_IN_SECONDS;
|
||||
let hours = seconds / HOUR_IN_SECONDS;
|
||||
seconds %= HOUR_IN_SECONDS;
|
||||
let minutes = seconds / MINUTE_IN_SECONDS;
|
||||
seconds %= MINUTE_IN_SECONDS;
|
||||
let mut have_written = if days > 0 {
|
||||
write!(f, "{} day{}", days, if days == 1 { "" } else { "s" })?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if hours > 0 {
|
||||
write!(f, "{}{} hour{}", if have_written { " " } else { "" },
|
||||
hours, if hours == 1 { "" } else { "s" })?;
|
||||
have_written = true;
|
||||
}
|
||||
if minutes > 0 {
|
||||
write!(f, "{}{} minute{}", if have_written { " " } else { "" },
|
||||
minutes, if minutes == 1 { "" } else { "s" })?;
|
||||
have_written = true;
|
||||
}
|
||||
if seconds > 0 || !have_written {
|
||||
write!(f, "{}{} second{}", if have_written { " " } else { "" },
|
||||
seconds, if seconds == 1 { "" } else { "s" })?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add for Duration {
|
||||
type Output = Duration;
|
||||
fn add(self, rhs: Duration) -> Duration { Duration(self.0 + rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::AddAssign for Duration {
|
||||
fn add_assign(&mut self, rhs: Duration) { self.0 += rhs.0 }
|
||||
}
|
||||
|
||||
impl ops::SubAssign for Duration {
|
||||
fn sub_assign(&mut self, rhs: Duration) { self.0 -= rhs.0 }
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct SampleIndexIterator {
|
||||
i: usize,
|
||||
pub pos: i32,
|
||||
pub start_90k: i32,
|
||||
pub duration_90k: i32,
|
||||
pub bytes: i32,
|
||||
bytes_key: i32,
|
||||
bytes_nonkey: i32,
|
||||
pub is_key: bool
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SampleIndexEncoder {
|
||||
// Internal state.
|
||||
prev_duration_90k: i32,
|
||||
prev_bytes_key: i32,
|
||||
prev_bytes_nonkey: i32,
|
||||
|
||||
// Eventual output.
|
||||
// TODO: move to another struct?
|
||||
pub sample_file_bytes: i32,
|
||||
pub total_duration_90k: i32,
|
||||
pub video_samples: i32,
|
||||
pub video_sync_samples: i32,
|
||||
pub video_index: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct Writer {
|
||||
f: fs::File,
|
||||
index: SampleIndexEncoder,
|
||||
uuid: Uuid,
|
||||
corrupt: bool,
|
||||
hasher: hash::Hasher,
|
||||
start_time: Time,
|
||||
local_time: Time,
|
||||
camera_id: i32,
|
||||
video_sample_entry_id: i32,
|
||||
}
|
||||
|
||||
/// Zigzag-encodes a signed integer, as in [protocol buffer
|
||||
/// encoding](https://developers.google.com/protocol-buffers/docs/encoding#types). Uses the low bit
|
||||
/// to indicate signedness (1 = negative, 0 = non-negative).
|
||||
#[inline(always)]
|
||||
fn zigzag32(i: i32) -> u32 { ((i << 1) as u32) ^ ((i >> 31) as u32) }
|
||||
|
||||
/// Zigzag-decodes to a signed integer.
|
||||
/// See `zigzag`.
|
||||
#[inline(always)]
|
||||
fn unzigzag32(i: u32) -> i32 { ((i >> 1) as i32) ^ -((i & 1) as i32) }
|
||||
|
||||
#[inline(always)]
|
||||
fn decode_varint32(data: &[u8], i: usize) -> Result<(u32, usize), ()> {
|
||||
// Unroll a few likely possibilities before going into the robust out-of-line loop.
|
||||
// This aids branch prediction.
|
||||
if data.len() > i && (data[i] & 0x80) == 0 {
|
||||
return Ok((data[i] as u32, i+1))
|
||||
} else if data.len() > i + 1 && (data[i+1] & 0x80) == 0 {
|
||||
return Ok((( (data[i] & 0x7f) as u32) |
|
||||
(( data[i+1] as u32) << 7),
|
||||
i+2))
|
||||
} else if data.len() > i + 2 && (data[i+2] & 0x80) == 0 {
|
||||
return Ok((( (data[i] & 0x7f) as u32) |
|
||||
(((data[i+1] & 0x7f) as u32) << 7) |
|
||||
(( data[i+2] as u32) << 14),
|
||||
i+3))
|
||||
}
|
||||
decode_varint32_slow(data, i)
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn decode_varint32_slow(data: &[u8], mut i: usize) -> Result<(u32, usize), ()> {
|
||||
let l = data.len();
|
||||
let mut out = 0;
|
||||
let mut shift = 0;
|
||||
loop {
|
||||
if i == l {
|
||||
return Err(())
|
||||
}
|
||||
let b = data[i];
|
||||
if shift == 28 && (b & 0xf0) != 0 {
|
||||
return Err(())
|
||||
}
|
||||
out |= ((b & 0x7f) as u32) << shift;
|
||||
shift += 7;
|
||||
i += 1;
|
||||
if (b & 0x80) == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok((out, i))
|
||||
}
|
||||
|
||||
fn append_varint32(i: u32, data: &mut Vec<u8>) {
|
||||
if i < 1u32 << 7 {
|
||||
data.push(i as u8);
|
||||
} else if i < 1u32 << 14 {
|
||||
data.extend_from_slice(&[(( i & 0x7F) | 0x80) as u8,
|
||||
(i >> 7) as u8]);
|
||||
} else if i < 1u32 << 21 {
|
||||
data.extend_from_slice(&[(( i & 0x7F) | 0x80) as u8,
|
||||
(((i >> 7) & 0x7F) | 0x80) as u8,
|
||||
(i >> 14) as u8]);
|
||||
} else if i < 1u32 << 28 {
|
||||
data.extend_from_slice(&[(( i & 0x7F) | 0x80) as u8,
|
||||
(((i >> 7) & 0x7F) | 0x80) as u8,
|
||||
(((i >> 14) & 0x7F) | 0x80) as u8,
|
||||
(i >> 21) as u8]);
|
||||
} else {
|
||||
data.extend_from_slice(&[(( i & 0x7F) | 0x80) as u8,
|
||||
(((i >> 7) & 0x7F) | 0x80) as u8,
|
||||
(((i >> 14) & 0x7F) | 0x80) as u8,
|
||||
(((i >> 21) & 0x7F) | 0x80) as u8,
|
||||
(i >> 28) as u8]);
|
||||
}
|
||||
}
|
||||
|
||||
impl SampleIndexIterator {
|
||||
pub fn new() -> SampleIndexIterator {
|
||||
SampleIndexIterator{i: 0,
|
||||
pos: 0,
|
||||
start_90k: 0,
|
||||
duration_90k: 0,
|
||||
bytes: 0,
|
||||
bytes_key: 0,
|
||||
bytes_nonkey: 0,
|
||||
is_key: false}
|
||||
}
|
||||
|
||||
pub fn next(&mut self, data: &[u8]) -> Result<bool, Error> {
|
||||
self.pos += self.bytes;
|
||||
self.start_90k += self.duration_90k;
|
||||
if self.i == data.len() {
|
||||
return Ok(false)
|
||||
}
|
||||
let (raw1, i1) = match decode_varint32(data, self.i) {
|
||||
Ok(tuple) => tuple,
|
||||
Err(()) => return Err(Error::new(format!("bad varint 1 at offset {}", self.i))),
|
||||
};
|
||||
let (raw2, i2) = match decode_varint32(data, i1) {
|
||||
Ok(tuple) => tuple,
|
||||
Err(()) => return Err(Error::new(format!("bad varint 2 at offset {}", i1))),
|
||||
};
|
||||
self.i = i2;
|
||||
let duration_90k_delta = unzigzag32(raw1 >> 1);
|
||||
self.duration_90k += duration_90k_delta;
|
||||
if self.duration_90k < 0 {
|
||||
return Err(Error{
|
||||
description: format!("negative duration {} after applying delta {}",
|
||||
self.duration_90k, duration_90k_delta),
|
||||
cause: None});
|
||||
}
|
||||
if self.duration_90k == 0 && data.len() > self.i {
|
||||
return Err(Error{
|
||||
description: format!("zero duration only allowed at end; have {} bytes left",
|
||||
data.len() - self.i),
|
||||
cause: None});
|
||||
}
|
||||
self.is_key = (raw1 & 1) == 1;
|
||||
let bytes_delta = unzigzag32(raw2);
|
||||
self.bytes = if self.is_key {
|
||||
self.bytes_key += bytes_delta;
|
||||
self.bytes_key
|
||||
} else {
|
||||
self.bytes_nonkey += bytes_delta;
|
||||
self.bytes_nonkey
|
||||
};
|
||||
if self.bytes <= 0 {
|
||||
return Err(Error{
|
||||
description: format!("non-positive bytes {} after applying delta {} to key={} frame at ts {}",
|
||||
self.bytes, bytes_delta, self.is_key,
|
||||
self.start_90k),
|
||||
cause: None});
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl SampleIndexEncoder {
|
||||
pub fn new() -> Self {
|
||||
SampleIndexEncoder{
|
||||
prev_duration_90k: 0,
|
||||
prev_bytes_key: 0,
|
||||
prev_bytes_nonkey: 0,
|
||||
total_duration_90k: 0,
|
||||
sample_file_bytes: 0,
|
||||
video_samples: 0,
|
||||
video_sync_samples: 0,
|
||||
video_index: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_sample(&mut self, duration_90k: i32, bytes: i32, is_key: bool) {
|
||||
let duration_delta = duration_90k - self.prev_duration_90k;
|
||||
self.prev_duration_90k = duration_90k;
|
||||
self.total_duration_90k += duration_90k;
|
||||
self.sample_file_bytes += bytes;
|
||||
self.video_samples += 1;
|
||||
let bytes_delta = bytes - if is_key {
|
||||
let prev = self.prev_bytes_key;
|
||||
self.video_sync_samples += 1;
|
||||
self.prev_bytes_key = bytes;
|
||||
prev
|
||||
} else {
|
||||
let prev = self.prev_bytes_nonkey;
|
||||
self.prev_bytes_nonkey = bytes;
|
||||
prev
|
||||
};
|
||||
append_varint32((zigzag32(duration_delta) << 1) | (is_key as u32), &mut self.video_index);
|
||||
append_varint32(zigzag32(bytes_delta), &mut self.video_index);
|
||||
}
|
||||
}
|
||||
|
||||
impl Writer {
|
||||
pub fn open(f: fs::File, uuid: Uuid, start_time: Time, local_time: Time,
|
||||
camera_id: i32, video_sample_entry_id: i32) -> Result<Self, Error> {
|
||||
Ok(Writer{
|
||||
f: f,
|
||||
index: SampleIndexEncoder::new(),
|
||||
uuid: uuid,
|
||||
corrupt: false,
|
||||
hasher: hash::Hasher::new(hash::Type::SHA1)?,
|
||||
start_time: start_time,
|
||||
local_time: local_time,
|
||||
camera_id: camera_id,
|
||||
video_sample_entry_id: video_sample_entry_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write(&mut self, pkt: &[u8], duration_90k: i32, is_key: bool) -> Result<(), Error> {
|
||||
let mut remaining = pkt;
|
||||
while !remaining.is_empty() {
|
||||
let written = match self.f.write(remaining) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
if remaining.len() < pkt.len() {
|
||||
// Partially written packet. Truncate if possible.
|
||||
if let Err(e2) = self.f.set_len(self.index.sample_file_bytes as u64) {
|
||||
error!("After write to {} failed with {}, truncate failed with {}; \
|
||||
sample file is corrupt.", self.uuid.hyphenated(), e, e2);
|
||||
self.corrupt = true;
|
||||
}
|
||||
}
|
||||
return Err(Error::from(e));
|
||||
},
|
||||
};
|
||||
remaining = &remaining[written..];
|
||||
}
|
||||
self.index.add_sample(duration_90k, pkt.len() as i32, is_key);
|
||||
self.hasher.update(pkt)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn end(&self) -> Time {
|
||||
self.start_time + Duration(self.index.total_duration_90k as i64)
|
||||
}
|
||||
|
||||
// TODO: clean up this interface.
|
||||
pub fn close(mut self) -> Result<(db::RecordingToInsert, fs::File), Error> {
|
||||
if self.corrupt {
|
||||
return Err(Error::new(format!("recording {} is corrupt", self.uuid)));
|
||||
}
|
||||
let mut sha1_bytes = [0u8; 20];
|
||||
sha1_bytes.copy_from_slice(&self.hasher.finish()?[..]);
|
||||
Ok((db::RecordingToInsert{
|
||||
camera_id: self.camera_id,
|
||||
sample_file_bytes: self.index.sample_file_bytes,
|
||||
time: self.start_time .. self.end(),
|
||||
local_time: self.local_time,
|
||||
video_samples: self.index.video_samples,
|
||||
video_sync_samples: self.index.video_sync_samples,
|
||||
video_sample_entry_id: self.video_sample_entry_id,
|
||||
sample_file_uuid: self.uuid,
|
||||
video_index: self.index.video_index,
|
||||
sample_file_sha1: sha1_bytes,
|
||||
}, self.f))
|
||||
}
|
||||
}
|
||||
|
||||
/// A segment represents a view of some or all of a single recording, starting from a key frame.
|
||||
/// Used by the `Mp4FileBuilder` class to splice together recordings into a single virtual .mp4.
|
||||
pub struct Segment {
|
||||
pub id: i64,
|
||||
pub start: Time,
|
||||
begin: SampleIndexIterator,
|
||||
pub file_end: i32,
|
||||
pub desired_range_90k: Range<i32>,
|
||||
actual_end_90k: i32,
|
||||
pub frames: i32,
|
||||
pub key_frames: i32,
|
||||
pub video_sample_entry_id: i32,
|
||||
}
|
||||
|
||||
impl Segment {
|
||||
/// Creates a segment in a semi-initialized state. This is very light initialization because
|
||||
/// it is called with the database lock held. `init` must be called before usage, and the
|
||||
/// Segment should not be used if `init` fails.
|
||||
///
|
||||
/// `desired_range_90k` represents the desired range of the segment relative to the start of
|
||||
/// the recording. The actual range will start at the first key frame at or before the
|
||||
/// desired start time. (The caller is responsible for creating an edit list to skip the
|
||||
/// undesired portion.) It will end at the first frame after the desired range (unless the
|
||||
/// desired range extends beyond the recording).
|
||||
pub fn new(recording: &db::ListCameraRecordingsRow,
|
||||
desired_range_90k: Range<i32>) -> Segment {
|
||||
Segment{
|
||||
id: recording.id,
|
||||
start: recording.start,
|
||||
begin: SampleIndexIterator::new(),
|
||||
file_end: recording.sample_file_bytes,
|
||||
desired_range_90k: desired_range_90k,
|
||||
actual_end_90k: recording.duration_90k,
|
||||
frames: recording.video_samples,
|
||||
key_frames: recording.video_sync_samples,
|
||||
video_sample_entry_id: recording.video_sample_entry.id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Completes initialization of the segment. Must be called without the database lock held;
|
||||
/// this will use the database to retrieve the video index for partial recordings.
|
||||
pub fn init(&mut self, db: &db::Database) -> Result<(), Error> {
|
||||
if self.desired_range_90k.start > self.desired_range_90k.end ||
|
||||
self.desired_range_90k.end > self.actual_end_90k {
|
||||
return Err(Error::new(format!(
|
||||
"desired range [{}, {}) invalid for recording of length {}",
|
||||
self.desired_range_90k.start, self.desired_range_90k.end, self.actual_end_90k)));
|
||||
}
|
||||
|
||||
if self.desired_range_90k.start == 0 &&
|
||||
self.desired_range_90k.end == self.actual_end_90k {
|
||||
// Fast path. Existing entry is fine.
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
// Slow path. Need to iterate through the index.
|
||||
let extra = db.lock().get_recording(self.id)?;
|
||||
let data = &(&extra).video_index;
|
||||
let mut it = SampleIndexIterator::new();
|
||||
if !it.next(data)? {
|
||||
return Err(Error{description: String::from("no index"),
|
||||
cause: None});
|
||||
}
|
||||
if !it.is_key {
|
||||
return Err(Error{description: String::from("not key frame"),
|
||||
cause: None});
|
||||
}
|
||||
loop {
|
||||
if it.start_90k <= self.desired_range_90k.start && it.is_key {
|
||||
// new start candidate.
|
||||
self.begin = it;
|
||||
self.frames = 0;
|
||||
self.key_frames = 0;
|
||||
}
|
||||
if it.start_90k >= self.desired_range_90k.end {
|
||||
break;
|
||||
}
|
||||
self.frames += 1;
|
||||
self.key_frames += it.is_key as i32;
|
||||
if !it.next(data)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.file_end = it.pos;
|
||||
self.actual_end_90k = it.start_90k;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the byte range within the sample file of data associated with this segment.
|
||||
pub fn sample_file_range(&self) -> Range<u64> {
|
||||
Range{start: self.begin.pos as u64, end: self.file_end as u64}
|
||||
}
|
||||
|
||||
/// Returns the actual time range as described in `new`.
|
||||
pub fn actual_time_90k(&self) -> Range<i32> {
|
||||
Range{start: self.begin.start_90k, end: self.actual_end_90k}
|
||||
}
|
||||
|
||||
/// Iterates through each frame in the segment.
|
||||
/// Must be called without the database lock held; retrieves video index from the cache.
|
||||
pub fn foreach<F>(&self, db: &db::Database, mut f: F) -> Result<(), Error>
|
||||
where F: FnMut(&SampleIndexIterator) -> Result<(), Error>
|
||||
{
|
||||
let extra = db.lock().get_recording(self.id)?;
|
||||
let data = &(&extra).video_index;
|
||||
let mut it = self.begin;
|
||||
if it.i == 0 {
|
||||
assert!(it.next(data)?);
|
||||
assert!(it.is_key);
|
||||
}
|
||||
loop {
|
||||
f(&it)?;
|
||||
if !it.next(data)? {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate test;
|
||||
|
||||
use super::{append_varint32, decode_varint32, unzigzag32, zigzag32};
|
||||
use super::*;
|
||||
use self::test::Bencher;
|
||||
|
||||
#[test]
|
||||
fn test_zigzag() {
|
||||
struct Test {
|
||||
decoded: i32,
|
||||
encoded: u32,
|
||||
}
|
||||
let tests = [
|
||||
Test{decoded: 0, encoded: 0},
|
||||
Test{decoded: -1, encoded: 1},
|
||||
Test{decoded: 1, encoded: 2},
|
||||
Test{decoded: -2, encoded: 3},
|
||||
Test{decoded: 2147483647, encoded: 4294967294},
|
||||
Test{decoded: -2147483648, encoded: 4294967295},
|
||||
];
|
||||
for test in &tests {
|
||||
assert_eq!(test.encoded, zigzag32(test.decoded));
|
||||
assert_eq!(test.decoded, unzigzag32(test.encoded));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_correct_varints() {
|
||||
struct Test {
|
||||
decoded: u32,
|
||||
encoded: &'static [u8],
|
||||
}
|
||||
let tests = [
|
||||
Test{decoded: 1, encoded: b"\x01"},
|
||||
Test{decoded: 257, encoded: b"\x81\x02"},
|
||||
Test{decoded: 49409, encoded: b"\x81\x82\x03"},
|
||||
Test{decoded: 8438017, encoded: b"\x81\x82\x83\x04"},
|
||||
Test{decoded: 1350615297, encoded: b"\x81\x82\x83\x84\x05"},
|
||||
];
|
||||
for test in &tests {
|
||||
// Test encoding to an empty buffer.
|
||||
let mut out = Vec::new();
|
||||
append_varint32(test.decoded, &mut out);
|
||||
assert_eq!(&out[..], test.encoded);
|
||||
|
||||
// ...and to a non-empty buffer.
|
||||
let mut buf = Vec::new();
|
||||
out.clear();
|
||||
out.push(b'x');
|
||||
buf.push(b'x');
|
||||
buf.extend_from_slice(test.encoded);
|
||||
append_varint32(test.decoded, &mut out);
|
||||
assert_eq!(out, buf);
|
||||
|
||||
// Test decoding from the beginning of the string.
|
||||
assert_eq!((test.decoded, test.encoded.len()),
|
||||
decode_varint32(test.encoded, 0).unwrap());
|
||||
|
||||
// ...and from the middle of a buffer.
|
||||
buf.push(b'x');
|
||||
assert_eq!((test.decoded, test.encoded.len() + 1),
|
||||
decode_varint32(&buf, 1).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_duration() {
|
||||
let tests = &[
|
||||
// (output, seconds)
|
||||
("0 seconds", 0),
|
||||
("1 second", 1),
|
||||
("1 minute", 60),
|
||||
("1 minute 1 second", 61),
|
||||
("2 minutes", 120),
|
||||
("1 hour", 3600),
|
||||
("1 hour 1 minute", 3660),
|
||||
("2 hours", 7200),
|
||||
("1 day", 86400),
|
||||
("1 day 1 hour", 86400 + 3600),
|
||||
("2 days", 2 * 86400),
|
||||
];
|
||||
for test in tests {
|
||||
assert_eq!(test.0, format!("{}", Duration(test.1 * TIME_UNITS_PER_SEC)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bad_varints() {
|
||||
let tests: &[&[u8]] = &[
|
||||
// buffer underruns
|
||||
b"",
|
||||
b"\x80",
|
||||
b"\x80\x80",
|
||||
b"\x80\x80\x80",
|
||||
b"\x80\x80\x80\x80",
|
||||
|
||||
// int32 overflows
|
||||
b"\x80\x80\x80\x80\x80",
|
||||
b"\x80\x80\x80\x80\x80\x00",
|
||||
];
|
||||
for (i, encoded) in tests.iter().enumerate() {
|
||||
assert!(decode_varint32(encoded, 0).is_err(), "while on test {}", i);
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests the example from design/schema.md.
|
||||
#[test]
|
||||
fn test_encode_example() {
|
||||
let mut e = SampleIndexEncoder::new();
|
||||
e.add_sample(10, 1000, true);
|
||||
e.add_sample(9, 10, false);
|
||||
e.add_sample(11, 15, false);
|
||||
e.add_sample(10, 12, false);
|
||||
e.add_sample(10, 1050, true);
|
||||
assert_eq!(e.video_index, b"\x29\xd0\x0f\x02\x14\x08\x0a\x02\x05\x01\x64");
|
||||
assert_eq!(10 + 9 + 11 + 10 + 10, e.total_duration_90k);
|
||||
assert_eq!(5, e.video_samples);
|
||||
assert_eq!(2, e.video_sync_samples);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip() {
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct Sample {
|
||||
duration_90k: i32,
|
||||
bytes: i32,
|
||||
is_key: bool,
|
||||
}
|
||||
let samples = [
|
||||
Sample{duration_90k: 10, bytes: 30000, is_key: true},
|
||||
Sample{duration_90k: 9, bytes: 1000, is_key: false},
|
||||
Sample{duration_90k: 11, bytes: 1100, is_key: false},
|
||||
Sample{duration_90k: 18, bytes: 31000, is_key: true},
|
||||
Sample{duration_90k: 0, bytes: 1000, is_key: false},
|
||||
];
|
||||
let mut e = SampleIndexEncoder::new();
|
||||
for sample in &samples {
|
||||
e.add_sample(sample.duration_90k, sample.bytes, sample.is_key);
|
||||
}
|
||||
let mut it = SampleIndexIterator::new();
|
||||
for sample in &samples {
|
||||
assert!(it.next(&e.video_index).unwrap());
|
||||
assert_eq!(sample,
|
||||
&Sample{duration_90k: it.duration_90k, bytes: it.bytes, is_key: it.is_key});
|
||||
}
|
||||
assert!(!it.next(&e.video_index).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iterator_errors() {
|
||||
struct Test {
|
||||
encoded: &'static [u8],
|
||||
err: &'static str,
|
||||
}
|
||||
let tests = [
|
||||
Test{encoded: b"\x80", err: "bad varint 1 at offset 0"},
|
||||
Test{encoded: b"\x00\x80", err: "bad varint 2 at offset 1"},
|
||||
Test{encoded: b"\x00\x02\x00\x00",
|
||||
err: "zero duration only allowed at end; have 2 bytes left"},
|
||||
Test{encoded: b"\x02\x02",
|
||||
err: "negative duration -1 after applying delta -1"},
|
||||
Test{encoded: b"\x04\x00",
|
||||
err: "non-positive bytes 0 after applying delta 0 to key=false frame at ts 0"},
|
||||
];
|
||||
for test in &tests {
|
||||
let mut it = SampleIndexIterator::new();
|
||||
assert_eq!(it.next(test.encoded).unwrap_err().description, test.err);
|
||||
}
|
||||
}
|
||||
|
||||
/// Benchmarks the decoder, which is performance-critical for .mp4 serving.
|
||||
#[bench]
|
||||
fn bench_decoder(b: &mut Bencher) {
|
||||
let data = include_bytes!("testdata/video_sample_index.bin");
|
||||
b.bytes = data.len() as u64;
|
||||
b.iter(|| {
|
||||
let mut it = SampleIndexIterator::new();
|
||||
while it.next(data).unwrap() {}
|
||||
assert_eq!(30104460, it.pos);
|
||||
assert_eq!(5399985, it.start_90k);
|
||||
});
|
||||
}
|
||||
}
|
682
src/resource.rs
Normal file
682
src/resource.rs
Normal file
@ -0,0 +1,682 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
extern crate core;
|
||||
extern crate hyper;
|
||||
extern crate time;
|
||||
|
||||
use error::Result;
|
||||
use hyper::server::{Request, Response};
|
||||
use hyper::header;
|
||||
use hyper::method::Method;
|
||||
use hyper::net::Fresh;
|
||||
use mime;
|
||||
use smallvec::SmallVec;
|
||||
use std::cmp;
|
||||
use std::io;
|
||||
use std::ops::Range;
|
||||
|
||||
/// An HTTP resource for GET and HEAD serving.
|
||||
pub trait Resource {
|
||||
/// Returns the length of the slice in bytes.
|
||||
fn len(&self) -> u64;
|
||||
|
||||
/// Writes bytes within this slice indicated by `range` to `out.`
|
||||
/// TODO: different result type?
|
||||
fn write_to(&self, range: Range<u64>, out: &mut io::Write) -> Result<()>;
|
||||
|
||||
fn content_type(&self) -> mime::Mime;
|
||||
fn etag(&self) -> Option<&header::EntityTag>;
|
||||
fn last_modified(&self) -> &header::HttpDate;
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
enum ResolvedRanges {
|
||||
AbsentOrInvalid,
|
||||
NotSatisfiable,
|
||||
Satisfiable(SmallVec<[Range<u64>; 1]>)
|
||||
}
|
||||
|
||||
fn parse_range_header(range: Option<&header::Range>, resource_len: u64) -> ResolvedRanges {
|
||||
if let Some(&header::Range::Bytes(ref byte_ranges)) = range {
|
||||
let mut ranges: SmallVec<[Range<u64>; 1]> = SmallVec::new();
|
||||
for range in byte_ranges {
|
||||
match *range {
|
||||
header::ByteRangeSpec::FromTo(range_from, range_to) => {
|
||||
let end = cmp::min(range_to + 1, resource_len);
|
||||
if range_from >= end {
|
||||
debug!("Range {:?} not satisfiable with length {:?}", range, resource_len);
|
||||
continue;
|
||||
}
|
||||
ranges.push(Range{start: range_from, end: end});
|
||||
},
|
||||
header::ByteRangeSpec::AllFrom(range_from) => {
|
||||
if range_from >= resource_len {
|
||||
debug!("Range {:?} not satisfiable with length {:?}", range, resource_len);
|
||||
continue;
|
||||
}
|
||||
ranges.push(Range{start: range_from, end: resource_len});
|
||||
},
|
||||
header::ByteRangeSpec::Last(last) => {
|
||||
if last >= resource_len {
|
||||
debug!("Range {:?} not satisfiable with length {:?}", range, resource_len);
|
||||
continue;
|
||||
}
|
||||
ranges.push(Range{start: resource_len - last,
|
||||
end: resource_len});
|
||||
},
|
||||
}
|
||||
}
|
||||
if !ranges.is_empty() {
|
||||
debug!("Ranges {:?} all satisfiable with length {:?}", range, resource_len);
|
||||
return ResolvedRanges::Satisfiable(ranges);
|
||||
}
|
||||
return ResolvedRanges::NotSatisfiable;
|
||||
}
|
||||
ResolvedRanges::AbsentOrInvalid
|
||||
}
|
||||
|
||||
/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`.
|
||||
fn none_match(etag: Option<&header::EntityTag>, req: &Request) -> bool {
|
||||
match req.headers.get::<header::IfNoneMatch>() {
|
||||
Some(&header::IfNoneMatch::Any) => false,
|
||||
Some(&header::IfNoneMatch::Items(ref items)) => {
|
||||
if let Some(some_etag) = etag {
|
||||
for item in items {
|
||||
if item.weak_eq(some_etag) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
},
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if `req` has no `If-Match` header or one which matches `etag`.
|
||||
fn any_match(etag: Option<&header::EntityTag>, req: &Request) -> bool {
|
||||
match req.headers.get::<header::IfMatch>() {
|
||||
Some(&header::IfMatch::Any) => true,
|
||||
Some(&header::IfMatch::Items(ref items)) => {
|
||||
if let Some(some_etag) = etag {
|
||||
for item in items {
|
||||
if item.strong_eq(some_etag) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
},
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Serves GET and HEAD requests for a given byte-ranged resource.
|
||||
/// Handles conditional & subrange requests.
|
||||
/// The caller is expected to have already determined the correct resource and appended
|
||||
/// Expires, Cache-Control, and Vary headers.
|
||||
///
|
||||
/// TODO: is it appropriate to include those headers on all response codes used in this function?
|
||||
///
|
||||
/// TODO: check HTTP rules about weak vs strong comparisons with range requests. I don't think I'm
|
||||
/// doing this correctly.
|
||||
pub fn serve(rsrc: &Resource, req: &Request, mut res: Response<Fresh>) -> Result<()> {
|
||||
if req.method != Method::Get && req.method != Method::Head {
|
||||
*res.status_mut() = hyper::status::StatusCode::MethodNotAllowed;
|
||||
res.headers_mut().set(header::ContentType(mime!(Text/Plain)));
|
||||
res.headers_mut().set(header::Allow(vec![Method::Get, Method::Head]));
|
||||
res.send(b"This resource only supports GET and HEAD.")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let last_modified = rsrc.last_modified();
|
||||
let etag = rsrc.etag();
|
||||
res.headers_mut().set(header::AcceptRanges(vec![header::RangeUnit::Bytes]));
|
||||
res.headers_mut().set(header::LastModified(*last_modified));
|
||||
if let Some(some_etag) = etag {
|
||||
res.headers_mut().set(header::ETag(some_etag.clone()));
|
||||
}
|
||||
|
||||
if let Some(&header::IfUnmodifiedSince(ref since)) = req.headers.get() {
|
||||
if last_modified.0.to_timespec() > since.0.to_timespec() {
|
||||
*res.status_mut() = hyper::status::StatusCode::PreconditionFailed;
|
||||
res.send(b"Precondition failed")?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if any_match(etag, req) {
|
||||
*res.status_mut() = hyper::status::StatusCode::PreconditionFailed;
|
||||
res.send(b"Precondition failed")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !none_match(etag, req) {
|
||||
*res.status_mut() = hyper::status::StatusCode::NotModified;
|
||||
res.send(b"")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(&header::IfModifiedSince(ref since)) = req.headers.get() {
|
||||
if last_modified <= since {
|
||||
*res.status_mut() = hyper::status::StatusCode::NotModified;
|
||||
res.send(b"")?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let mut range_hdr = req.headers.get::<header::Range>();
|
||||
|
||||
// See RFC 2616 section 10.2.7: a Partial Content response should include certain
|
||||
// entity-headers or not based on the If-Range response.
|
||||
let include_entity_headers_on_range = match req.headers.get::<header::IfRange>() {
|
||||
Some(&header::IfRange::EntityTag(ref if_etag)) => {
|
||||
if let Some(some_etag) = etag {
|
||||
if if_etag.strong_eq(some_etag) {
|
||||
false
|
||||
} else {
|
||||
range_hdr = None;
|
||||
true
|
||||
}
|
||||
} else {
|
||||
range_hdr = None;
|
||||
true
|
||||
}
|
||||
},
|
||||
Some(&header::IfRange::Date(ref if_date)) => {
|
||||
// The to_timespec conversion appears necessary because in the If-Range off the wire,
|
||||
// fields such as tm_yday are absent, causing strict equality to spuriously fail.
|
||||
if if_date.0.to_timespec() != last_modified.0.to_timespec() {
|
||||
range_hdr = None;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
None => true,
|
||||
};
|
||||
let len = rsrc.len();
|
||||
let (range, include_entity_headers) = match parse_range_header(range_hdr, len) {
|
||||
ResolvedRanges::AbsentOrInvalid => (0 .. len, true),
|
||||
ResolvedRanges::Satisfiable(rs) => {
|
||||
if rs.len() == 1 {
|
||||
res.headers_mut().set(header::ContentRange(
|
||||
header::ContentRangeSpec::Bytes{
|
||||
range: Some((rs[0].start, rs[0].end-1)),
|
||||
instance_length: Some(len)}));
|
||||
*res.status_mut() = hyper::status::StatusCode::PartialContent;
|
||||
(rs[0].clone(), include_entity_headers_on_range)
|
||||
} else {
|
||||
// Ignore multi-part range headers for now. They require additional complexity, and
|
||||
// I don't see clients sending them in the wild.
|
||||
(0 .. len, true)
|
||||
}
|
||||
},
|
||||
ResolvedRanges::NotSatisfiable => {
|
||||
res.headers_mut().set(header::ContentRange(
|
||||
header::ContentRangeSpec::Bytes{
|
||||
range: None,
|
||||
instance_length: Some(len)}));
|
||||
*res.status_mut() = hyper::status::StatusCode::RangeNotSatisfiable;
|
||||
res.send(b"")?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
if include_entity_headers {
|
||||
res.headers_mut().set(header::ContentType(rsrc.content_type()));
|
||||
}
|
||||
res.headers_mut().set(header::ContentLength(range.end - range.start));
|
||||
let mut stream = res.start()?;
|
||||
if req.method == Method::Get {
|
||||
rsrc.write_to(range, &mut stream)?;
|
||||
}
|
||||
stream.end()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use error::Result;
|
||||
use hyper;
|
||||
use hyper::header::{self, ByteRangeSpec, ContentRangeSpec, EntityTag};
|
||||
use hyper::header::Range::Bytes;
|
||||
use mime;
|
||||
use smallvec::SmallVec;
|
||||
use std::io::{Read, Write};
|
||||
use std::ops::Range;
|
||||
use std::sync::Mutex;
|
||||
use super::{ResolvedRanges, parse_range_header};
|
||||
use super::*;
|
||||
use testutil;
|
||||
use time;
|
||||
|
||||
/// Tests the specific examples enumerated in RFC 2616 section 14.35.1.
|
||||
#[test]
|
||||
fn test_resolve_ranges_rfc() {
|
||||
let mut v = SmallVec::new();
|
||||
|
||||
v.push(0 .. 500);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(0, 499)])),
|
||||
10000));
|
||||
|
||||
v.clear();
|
||||
v.push(500 .. 1000);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(500, 999)])),
|
||||
10000));
|
||||
|
||||
v.clear();
|
||||
v.push(9500 .. 10000);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::Last(500)])),
|
||||
10000));
|
||||
|
||||
v.clear();
|
||||
v.push(9500 .. 10000);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::AllFrom(9500)])),
|
||||
10000));
|
||||
|
||||
v.clear();
|
||||
v.push(0 .. 1);
|
||||
v.push(9999 .. 10000);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(0, 0),
|
||||
ByteRangeSpec::Last(1)])),
|
||||
10000));
|
||||
|
||||
// Non-canonical ranges. Possibly the point of these is that the adjacent and overlapping
|
||||
// ranges are supposed to be coalesced into one? I'm not going to do that for now.
|
||||
|
||||
v.clear();
|
||||
v.push(500 .. 601);
|
||||
v.push(601 .. 1000);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(500, 600),
|
||||
ByteRangeSpec::FromTo(601, 999)])),
|
||||
10000));
|
||||
|
||||
v.clear();
|
||||
v.push(500 .. 701);
|
||||
v.push(601 .. 1000);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(500, 700),
|
||||
ByteRangeSpec::FromTo(601, 999)])),
|
||||
10000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_ranges_satisfiability() {
|
||||
assert_eq!(ResolvedRanges::NotSatisfiable,
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::AllFrom(10000)])),
|
||||
10000));
|
||||
|
||||
let mut v = SmallVec::new();
|
||||
v.push(0 .. 500);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(0, 499),
|
||||
ByteRangeSpec::AllFrom(10000)])),
|
||||
10000));
|
||||
|
||||
assert_eq!(ResolvedRanges::NotSatisfiable,
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::Last(1)])), 0));
|
||||
assert_eq!(ResolvedRanges::NotSatisfiable,
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(0, 0)])), 0));
|
||||
assert_eq!(ResolvedRanges::NotSatisfiable,
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::AllFrom(0)])), 0));
|
||||
|
||||
v.clear();
|
||||
v.push(0 .. 1);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(0, 0)])), 1));
|
||||
|
||||
v.clear();
|
||||
v.push(0 .. 500);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(0, 10000)])),
|
||||
500));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_ranges_absent_or_invalid() {
|
||||
assert_eq!(ResolvedRanges::AbsentOrInvalid, parse_range_header(None, 10000));
|
||||
}
|
||||
|
||||
struct FakeResource {
|
||||
etag: Option<EntityTag>,
|
||||
mime: mime::Mime,
|
||||
last_modified: header::HttpDate,
|
||||
body: &'static [u8],
|
||||
}
|
||||
|
||||
impl Resource for FakeResource {
|
||||
fn len(&self) -> u64 { self.body.len() as u64 }
|
||||
fn write_to(&self, range: Range<u64>, out: &mut Write) -> Result<()> {
|
||||
Ok(out.write_all(&self.body[range.start as usize .. range.end as usize])?)
|
||||
}
|
||||
fn content_type(&self) -> mime::Mime { self.mime.clone() }
|
||||
fn etag(&self) -> Option<&EntityTag> { self.etag.as_ref() }
|
||||
fn last_modified(&self) -> &header::HttpDate { &self.last_modified }
|
||||
}
|
||||
|
||||
fn new_server() -> String {
|
||||
let mut listener = hyper::net::HttpListener::new("127.0.0.1:0").unwrap();
|
||||
use hyper::net::NetworkListener;
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let server = hyper::Server::new(listener);
|
||||
use std::thread::spawn;
|
||||
spawn(move || {
|
||||
use hyper::server::{Request, Response, Fresh};
|
||||
let _ = server.handle(move |req: Request, res: Response<Fresh>| {
|
||||
let l = RESOURCE.lock().unwrap();
|
||||
let resource = l.as_ref().unwrap();
|
||||
serve(resource, &req, res).unwrap();
|
||||
});
|
||||
});
|
||||
format!("http://{}:{}/", addr.ip(), addr.port())
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref RESOURCE: Mutex<Option<FakeResource>> = { Mutex::new(None) };
|
||||
static ref SERVER: String = { new_server() };
|
||||
static ref SOME_DATE: header::HttpDate = {
|
||||
header::HttpDate(time::at_utc(time::Timespec::new(1430006400i64, 0)))
|
||||
};
|
||||
static ref LATER_DATE: header::HttpDate = {
|
||||
header::HttpDate(time::at_utc(time::Timespec::new(1430092800i64, 0)))
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serve_without_etag() {
|
||||
testutil::init_logging();
|
||||
*RESOURCE.lock().unwrap() = Some(FakeResource{
|
||||
etag: None,
|
||||
mime: mime!(Application/OctetStream),
|
||||
last_modified: *SOME_DATE,
|
||||
body: b"01234",
|
||||
});
|
||||
let client = hyper::Client::new();
|
||||
let mut buf = Vec::new();
|
||||
|
||||
// Full body.
|
||||
let mut resp = client.get(&*SERVER).send().unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
||||
resp.headers.get::<header::ContentType>());
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
|
||||
// If-None-Match any.
|
||||
let mut resp = client.get(&*SERVER)
|
||||
.header(header::IfNoneMatch::Any)
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::NotModified, resp.status);
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"", &buf[..]);
|
||||
|
||||
// If-None-Match by etag doesn't match (as this request has no etag).
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(header::IfNoneMatch::Items(vec![EntityTag::strong("foo".to_owned())]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
||||
resp.headers.get::<header::ContentType>());
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
|
||||
// Unmodified since supplied date.
|
||||
let mut resp = client.get(&*SERVER)
|
||||
.header(header::IfModifiedSince(*SOME_DATE))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::NotModified, resp.status);
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"", &buf[..]);
|
||||
|
||||
// Range serving - basic case.
|
||||
let mut resp = client.get(&*SERVER)
|
||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::PartialContent, resp.status);
|
||||
assert_eq!(Some(&header::ContentRange(ContentRangeSpec::Bytes{
|
||||
range: Some((1, 3)),
|
||||
instance_length: Some(5),
|
||||
})), resp.headers.get());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"123", &buf[..]);
|
||||
|
||||
// Range serving - multiple ranges. Currently falls back to whole range.
|
||||
let mut resp = client.get(&*SERVER)
|
||||
.header(Bytes(vec![ByteRangeSpec::FromTo(0, 1),
|
||||
ByteRangeSpec::FromTo(3, 4)]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
|
||||
// Range serving - not satisfiable.
|
||||
let mut resp = client.get(&*SERVER)
|
||||
.header(Bytes(vec![ByteRangeSpec::AllFrom(500)]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::RangeNotSatisfiable, resp.status);
|
||||
assert_eq!(Some(&header::ContentRange(ContentRangeSpec::Bytes{
|
||||
range: None,
|
||||
instance_length: Some(5),
|
||||
})), resp.headers.get());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"", &buf[..]);
|
||||
|
||||
// Range serving - matching If-Range by date honors the range.
|
||||
let mut resp = client.get(&*SERVER)
|
||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
||||
.header(header::IfRange::Date(*SOME_DATE))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::PartialContent, resp.status);
|
||||
assert_eq!(Some(&header::ContentRange(ContentRangeSpec::Bytes{
|
||||
range: Some((1, 3)),
|
||||
instance_length: Some(5),
|
||||
})), resp.headers.get());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"123", &buf[..]);
|
||||
|
||||
// Range serving - non-matching If-Range by date ignores the range.
|
||||
let mut resp = client.get(&*SERVER)
|
||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
||||
.header(header::IfRange::Date(*LATER_DATE))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
||||
resp.headers.get::<header::ContentType>());
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
|
||||
// Range serving - this resource has no etag, so any If-Range by etag ignores the range.
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
||||
.header(header::IfRange::EntityTag(EntityTag::strong("foo".to_owned())))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
||||
resp.headers.get::<header::ContentType>());
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serve_with_strong_etag() {
|
||||
testutil::init_logging();
|
||||
*RESOURCE.lock().unwrap() = Some(FakeResource{
|
||||
etag: Some(EntityTag::strong("foo".to_owned())),
|
||||
mime: mime!(Application/OctetStream),
|
||||
last_modified: *SOME_DATE,
|
||||
body: b"01234",
|
||||
});
|
||||
let client = hyper::Client::new();
|
||||
let mut buf = Vec::new();
|
||||
|
||||
// If-None-Match by etag which matches.
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(header::IfNoneMatch::Items(vec![EntityTag::strong("foo".to_owned())]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::NotModified, resp.status);
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"", &buf[..]);
|
||||
|
||||
// If-None-Match by etag which doesn't match.
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(header::IfNoneMatch::Items(vec![EntityTag::strong("bar".to_owned())]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
|
||||
// Range serving - If-Range matching by etag.
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
||||
.header(header::IfRange::EntityTag(EntityTag::strong("foo".to_owned())))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::PartialContent, resp.status);
|
||||
assert_eq!(None, resp.headers.get::<header::ContentType>());
|
||||
assert_eq!(Some(&header::ContentRange(ContentRangeSpec::Bytes{
|
||||
range: Some((1, 3)),
|
||||
instance_length: Some(5),
|
||||
})), resp.headers.get());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"123", &buf[..]);
|
||||
|
||||
// Range serving - If-Range not matching by etag.
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
||||
.header(header::IfRange::EntityTag(EntityTag::strong("bar".to_owned())))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
||||
resp.headers.get::<header::ContentType>());
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serve_with_weak_etag() {
|
||||
testutil::init_logging();
|
||||
*RESOURCE.lock().unwrap() = Some(FakeResource{
|
||||
etag: Some(EntityTag::weak("foo".to_owned())),
|
||||
mime: mime!(Application/OctetStream),
|
||||
last_modified: *SOME_DATE,
|
||||
body: b"01234",
|
||||
});
|
||||
let client = hyper::Client::new();
|
||||
let mut buf = Vec::new();
|
||||
|
||||
// If-None-Match by identical weak etag is sufficient.
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(header::IfNoneMatch::Items(vec![EntityTag::weak("foo".to_owned())]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::NotModified, resp.status);
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"", &buf[..]);
|
||||
|
||||
// If-None-Match by etag which doesn't match.
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(header::IfNoneMatch::Items(vec![EntityTag::weak("bar".to_owned())]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
|
||||
// Range serving - If-Range matching by weak etag isn't sufficient.
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
||||
.header(header::IfRange::EntityTag(EntityTag::weak("foo".to_owned())))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
||||
resp.headers.get::<header::ContentType>());
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
}
|
||||
}
|
@ -110,6 +110,7 @@ create index recording_cover on recording (
|
||||
-- to consult the underlying row.
|
||||
duration_90k,
|
||||
video_samples,
|
||||
video_sync_samples,
|
||||
video_sample_entry_id,
|
||||
sample_file_bytes
|
||||
);
|
||||
|
@ -1,111 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// sqlite-test.cc: tests of the sqlite.h interface.
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
#include <glog/logging.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "sqlite.h"
|
||||
#include "string.h"
|
||||
#include "testutil.h"
|
||||
|
||||
DECLARE_bool(alsologtostderr);
|
||||
|
||||
namespace moonfire_nvr {
|
||||
namespace {
|
||||
|
||||
class SqliteTest : public testing::Test {
|
||||
protected:
|
||||
SqliteTest() {
|
||||
tmpdir_ = PrepareTempDirOrDie("sqlite-test");
|
||||
|
||||
std::string error_message;
|
||||
CHECK(db_.Open(StrCat(tmpdir_, "/db").c_str(),
|
||||
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, &error_message))
|
||||
<< error_message;
|
||||
|
||||
std::string create_sql = ReadFileOrDie("../src/schema.sql");
|
||||
DatabaseContext ctx(&db_);
|
||||
CHECK(RunStatements(&ctx, create_sql, &error_message)) << error_message;
|
||||
}
|
||||
|
||||
std::string tmpdir_;
|
||||
Database db_;
|
||||
};
|
||||
|
||||
TEST_F(SqliteTest, JustCreate) {}
|
||||
|
||||
TEST_F(SqliteTest, BindAndColumn) {
|
||||
std::string error_message;
|
||||
auto insert_stmt = db_.Prepare(
|
||||
"insert into camera (uuid, short_name, retain_bytes) "
|
||||
" values (:uuid, :short_name, :retain_bytes)",
|
||||
nullptr, &error_message);
|
||||
ASSERT_TRUE(insert_stmt.valid()) << error_message;
|
||||
const char kBlob[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f};
|
||||
re2::StringPiece blob_piece = re2::StringPiece(kBlob, sizeof(kBlob));
|
||||
const char kText[] = "foo";
|
||||
const int64_t kInt64 = INT64_C(0xbeeffeedface);
|
||||
|
||||
DatabaseContext ctx(&db_);
|
||||
{
|
||||
auto run = ctx.Borrow(&insert_stmt);
|
||||
run.BindBlob(1, blob_piece);
|
||||
run.BindText(2, kText);
|
||||
run.BindInt64(3, kInt64);
|
||||
ASSERT_EQ(SQLITE_DONE, run.Step()) << run.error_message();
|
||||
}
|
||||
|
||||
{
|
||||
auto run = ctx.UseOnce("select uuid, short_name, retain_bytes from camera");
|
||||
ASSERT_EQ(SQLITE_ROW, run.Step()) << run.error_message();
|
||||
EXPECT_EQ(ToHex(blob_piece, true), ToHex(run.ColumnBlob(0), true));
|
||||
EXPECT_EQ(kText, run.ColumnText(1).as_string());
|
||||
EXPECT_EQ(kInt64, run.ColumnInt64(2));
|
||||
ASSERT_EQ(SQLITE_DONE, run.Step()) << run.error_message();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
FLAGS_alsologtostderr = true;
|
||||
google::ParseCommandLineFlags(&argc, &argv, true);
|
||||
testing::InitGoogleTest(&argc, argv);
|
||||
google::InitGoogleLogging(argv[0]);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
408
src/sqlite.cc
408
src/sqlite.cc
@ -1,408 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// sqlite.cc: implementation of the sqlite.h interface.
|
||||
|
||||
#include "sqlite.h"
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include <glog/logging.h>
|
||||
|
||||
#include "string.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
namespace {
|
||||
|
||||
void LogCallback(void *, int err_code, const char *msg) {
|
||||
LOG(ERROR) << "(" << err_code << ") " << msg;
|
||||
}
|
||||
|
||||
void GlobalSetup() {
|
||||
VLOG(1) << "Installing sqlite3 log callback";
|
||||
sqlite3_config(SQLITE_CONFIG_LOG, &LogCallback, nullptr);
|
||||
}
|
||||
|
||||
std::once_flag global_setup;
|
||||
|
||||
} // namespace
|
||||
|
||||
Statement::Statement(Statement &&other) { *this = std::move(other); }
|
||||
|
||||
void Statement::operator=(Statement &&other) {
|
||||
Clear();
|
||||
memcpy(this, &other, sizeof(Statement));
|
||||
other.me_ = nullptr;
|
||||
other.borrowed_ = false;
|
||||
}
|
||||
|
||||
Statement::~Statement() { Clear(); }
|
||||
|
||||
void Statement::Clear() {
|
||||
CHECK(!borrowed_) << "can't delete statement while still borrowed!";
|
||||
sqlite3_finalize(me_);
|
||||
}
|
||||
|
||||
DatabaseContext::DatabaseContext(Database *db) : db_(db), lock_(db->ctx_mu_) {}
|
||||
|
||||
DatabaseContext::~DatabaseContext() {
|
||||
if (transaction_open_) {
|
||||
LOG(WARNING) << this << ": transaction left open! closing in destructor.";
|
||||
RollbackTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
bool DatabaseContext::BeginTransaction(std::string *error_message) {
|
||||
if (transaction_open_) {
|
||||
*error_message = "transaction already open!";
|
||||
return false;
|
||||
}
|
||||
sqlite3_step(db_->begin_transaction_.me_);
|
||||
int ret = sqlite3_reset(db_->begin_transaction_.me_);
|
||||
if (ret != SQLITE_OK) {
|
||||
*error_message =
|
||||
StrCat("begin transaction: ", sqlite3_errstr(ret), " (", ret, ")");
|
||||
return false;
|
||||
}
|
||||
transaction_open_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DatabaseContext::CommitTransaction(std::string *error_message) {
|
||||
if (!transaction_open_) {
|
||||
*error_message = "transaction not open!";
|
||||
return false;
|
||||
}
|
||||
sqlite3_step(db_->commit_transaction_.me_);
|
||||
int ret = sqlite3_reset(db_->commit_transaction_.me_);
|
||||
if (ret != SQLITE_OK) {
|
||||
*error_message =
|
||||
StrCat("commit transaction: ", sqlite3_errstr(ret), " (", ret, ")");
|
||||
return false;
|
||||
}
|
||||
transaction_open_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DatabaseContext::RollbackTransaction() {
|
||||
if (!transaction_open_) {
|
||||
LOG(WARNING) << this << ": rollback failed: transaction not open!";
|
||||
return;
|
||||
}
|
||||
sqlite3_step(db_->rollback_transaction_.me_);
|
||||
int ret = sqlite3_reset(db_->rollback_transaction_.me_);
|
||||
if (ret != SQLITE_OK) {
|
||||
LOG(WARNING) << this << ": rollback failed: " << sqlite3_errstr(ret) << " ("
|
||||
<< ret << ")";
|
||||
return;
|
||||
}
|
||||
transaction_open_ = false;
|
||||
}
|
||||
|
||||
RunningStatement DatabaseContext::Borrow(Statement *statement) {
|
||||
return RunningStatement(statement, std::string(), false);
|
||||
}
|
||||
|
||||
RunningStatement DatabaseContext::UseOnce(re2::StringPiece sql) {
|
||||
std::string error_message;
|
||||
auto *statement = new Statement(db_->Prepare(sql, nullptr, &error_message));
|
||||
return RunningStatement(statement, error_message, true);
|
||||
}
|
||||
|
||||
RunningStatement::RunningStatement(Statement *statement,
|
||||
const std::string &deferred_error,
|
||||
bool owns_statement)
|
||||
: error_message_(deferred_error), owns_statement_(owns_statement) {
|
||||
if (statement != nullptr && statement->valid()) {
|
||||
CHECK(!statement->borrowed_) << "Statement already borrowed!";
|
||||
statement->borrowed_ = true;
|
||||
statement_ = statement;
|
||||
} else if (error_message_.empty()) {
|
||||
error_message_ = "invalid statement";
|
||||
}
|
||||
|
||||
if (!error_message_.empty()) {
|
||||
status_ = SQLITE_MISUSE;
|
||||
}
|
||||
}
|
||||
|
||||
RunningStatement::RunningStatement(RunningStatement &&o) {
|
||||
statement_ = o.statement_;
|
||||
status_ = o.status_;
|
||||
owns_statement_ = o.owns_statement_;
|
||||
o.statement_ = nullptr;
|
||||
}
|
||||
|
||||
RunningStatement::~RunningStatement() {
|
||||
if (statement_ != nullptr) {
|
||||
CHECK(statement_->borrowed_) << "Statement no longer borrowed!";
|
||||
sqlite3_clear_bindings(statement_->me_);
|
||||
sqlite3_reset(statement_->me_);
|
||||
statement_->borrowed_ = false;
|
||||
if (owns_statement_) {
|
||||
delete statement_;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RunningStatement::BindBlob(int param, re2::StringPiece value) {
|
||||
if (status_ != SQLITE_OK) {
|
||||
return;
|
||||
}
|
||||
status_ = sqlite3_bind_blob64(statement_->me_, param, value.data(),
|
||||
value.size(), SQLITE_TRANSIENT);
|
||||
if (status_ != SQLITE_OK) {
|
||||
error_message_ = StrCat("Unable to bind parameter ", param, ": ",
|
||||
sqlite3_errstr(status_), " (", status_, ")");
|
||||
}
|
||||
}
|
||||
|
||||
void RunningStatement::BindBlob(const char *name, re2::StringPiece value) {
|
||||
if (status_ != SQLITE_OK) {
|
||||
return;
|
||||
}
|
||||
int param = sqlite3_bind_parameter_index(statement_->me_, name);
|
||||
if (param == 0) {
|
||||
status_ = SQLITE_MISUSE;
|
||||
error_message_ = StrCat("Unable to bind parameter ", name, ": not found.");
|
||||
return;
|
||||
}
|
||||
status_ = sqlite3_bind_blob64(statement_->me_, param, value.data(),
|
||||
value.size(), SQLITE_TRANSIENT);
|
||||
if (status_ != SQLITE_OK) {
|
||||
error_message_ = StrCat("Unable to bind parameter ", name, ": ",
|
||||
sqlite3_errstr(status_), " (", status_, ")");
|
||||
}
|
||||
}
|
||||
|
||||
void RunningStatement::BindInt64(int param, int64_t value) {
|
||||
if (status_ != SQLITE_OK) {
|
||||
return;
|
||||
}
|
||||
status_ = sqlite3_bind_int64(statement_->me_, param, value);
|
||||
if (status_ != SQLITE_OK) {
|
||||
error_message_ = StrCat("Unable to bind parameter ", param, ": ",
|
||||
sqlite3_errstr(status_), " (", status_, ")");
|
||||
}
|
||||
}
|
||||
|
||||
void RunningStatement::BindInt64(const char *name, int64_t value) {
|
||||
if (status_ != SQLITE_OK) {
|
||||
return;
|
||||
}
|
||||
int param = sqlite3_bind_parameter_index(statement_->me_, name);
|
||||
if (param == 0) {
|
||||
status_ = SQLITE_MISUSE;
|
||||
error_message_ = StrCat("Unable to bind parameter ", name, ": not found.");
|
||||
return;
|
||||
}
|
||||
status_ = sqlite3_bind_int64(statement_->me_, param, value);
|
||||
if (status_ != SQLITE_OK) {
|
||||
error_message_ = StrCat("Unable to bind parameter ", name, ": ",
|
||||
sqlite3_errstr(status_), " (", status_, ")");
|
||||
}
|
||||
}
|
||||
|
||||
void RunningStatement::BindText(int param, re2::StringPiece value) {
|
||||
if (status_ != SQLITE_OK) {
|
||||
return;
|
||||
}
|
||||
status_ = sqlite3_bind_text64(statement_->me_, param, value.data(),
|
||||
value.size(), SQLITE_TRANSIENT, SQLITE_UTF8);
|
||||
if (status_ != SQLITE_OK) {
|
||||
error_message_ = StrCat("Unable to bind parameter ", param, ": ",
|
||||
sqlite3_errstr(status_), " (", status_, ")");
|
||||
}
|
||||
}
|
||||
|
||||
void RunningStatement::BindText(const char *name, re2::StringPiece value) {
|
||||
if (status_ != SQLITE_OK) {
|
||||
return;
|
||||
}
|
||||
int param = sqlite3_bind_parameter_index(statement_->me_, name);
|
||||
if (param == 0) {
|
||||
error_message_ = StrCat("Unable to bind parameter ", name, ": not found.");
|
||||
return;
|
||||
}
|
||||
status_ = sqlite3_bind_text64(statement_->me_, param, value.data(),
|
||||
value.size(), SQLITE_TRANSIENT, SQLITE_UTF8);
|
||||
if (status_ != SQLITE_OK) {
|
||||
error_message_ = StrCat("Unable to bind parameter ", name, ": ",
|
||||
sqlite3_errstr(status_), " (", status_, ")");
|
||||
}
|
||||
}
|
||||
|
||||
int RunningStatement::Step() {
|
||||
if (status_ != SQLITE_OK && status_ != SQLITE_ROW) {
|
||||
return status_;
|
||||
}
|
||||
status_ = sqlite3_step(statement_->me_);
|
||||
error_message_ =
|
||||
StrCat("step: ", sqlite3_errstr(status_), " (", status_, ")");
|
||||
return status_;
|
||||
}
|
||||
|
||||
int RunningStatement::ColumnType(int col) {
|
||||
return sqlite3_column_type(statement_->me_, col);
|
||||
}
|
||||
|
||||
re2::StringPiece RunningStatement::ColumnBlob(int col) {
|
||||
// Order matters: call _blob first, then _bytes.
|
||||
const void *data = sqlite3_column_blob(statement_->me_, col);
|
||||
size_t len = sqlite3_column_bytes(statement_->me_, col);
|
||||
return re2::StringPiece(reinterpret_cast<const char *>(data), len);
|
||||
}
|
||||
|
||||
int64_t RunningStatement::ColumnInt64(int col) {
|
||||
return sqlite3_column_int64(statement_->me_, col);
|
||||
}
|
||||
|
||||
re2::StringPiece RunningStatement::ColumnText(int col) {
|
||||
// Order matters: call _text first, then _bytes.
|
||||
const unsigned char *data = sqlite3_column_text(statement_->me_, col);
|
||||
size_t len = sqlite3_column_bytes(statement_->me_, col);
|
||||
return re2::StringPiece(reinterpret_cast<const char *>(data), len);
|
||||
}
|
||||
|
||||
Database::~Database() {
|
||||
begin_transaction_ = Statement();
|
||||
commit_transaction_ = Statement();
|
||||
rollback_transaction_ = Statement();
|
||||
int err = sqlite3_close(me_);
|
||||
CHECK_EQ(SQLITE_OK, err) << "sqlite3_close: " << sqlite3_errstr(err);
|
||||
}
|
||||
|
||||
bool Database::Open(const char *filename, int flags,
|
||||
std::string *error_message) {
|
||||
std::call_once(global_setup, &GlobalSetup);
|
||||
int ret = sqlite3_open_v2(filename, &me_, flags, nullptr);
|
||||
if (ret != SQLITE_OK) {
|
||||
*error_message =
|
||||
StrCat("open ", filename, ": ", sqlite3_errstr(ret), " (", ret, ")");
|
||||
return false;
|
||||
}
|
||||
|
||||
ret = sqlite3_extended_result_codes(me_, 1);
|
||||
if (ret != SQLITE_OK) {
|
||||
sqlite3_close(me_);
|
||||
me_ = nullptr;
|
||||
*error_message = StrCat("while enabling extended result codes: ",
|
||||
sqlite3_errstr(ret), " (", ret, ")");
|
||||
return false;
|
||||
}
|
||||
|
||||
Statement pragma_foreignkeys;
|
||||
struct StatementToInitialize {
|
||||
Statement *p;
|
||||
re2::StringPiece sql;
|
||||
};
|
||||
StatementToInitialize stmts[] = {
|
||||
{&begin_transaction_, "begin transaction;"},
|
||||
{&commit_transaction_, "commit transaction;"},
|
||||
{&rollback_transaction_, "rollback transaction;"},
|
||||
{&pragma_foreignkeys, "pragma foreign_keys = true;"}};
|
||||
|
||||
for (const auto &stmt : stmts) {
|
||||
*stmt.p = Prepare(stmt.sql, nullptr, error_message);
|
||||
if (!stmt.p->valid()) {
|
||||
sqlite3_close(me_);
|
||||
me_ = nullptr;
|
||||
*error_message = StrCat("while preparing SQL for \"", stmt.sql, "\": ",
|
||||
*error_message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ret = sqlite3_step(pragma_foreignkeys.me_);
|
||||
sqlite3_reset(pragma_foreignkeys.me_);
|
||||
if (ret != SQLITE_DONE) {
|
||||
sqlite3_close(me_);
|
||||
me_ = nullptr;
|
||||
*error_message = StrCat("while enabling foreign keys: ",
|
||||
sqlite3_errstr(ret), " (", ret, ")");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Statement Database::Prepare(re2::StringPiece sql, size_t *used,
|
||||
std::string *error_message) {
|
||||
Statement statement;
|
||||
const char *tail;
|
||||
int err =
|
||||
sqlite3_prepare_v2(me_, sql.data(), sql.size(), &statement.me_, &tail);
|
||||
if (err != SQLITE_OK) {
|
||||
*error_message = StrCat("prepare: ", sqlite3_errstr(err), " (", err, ")");
|
||||
return statement;
|
||||
}
|
||||
if (used != nullptr) {
|
||||
*used = tail - sql.data();
|
||||
}
|
||||
if (statement.me_ == nullptr) {
|
||||
error_message->clear();
|
||||
}
|
||||
return statement;
|
||||
}
|
||||
|
||||
bool RunStatements(DatabaseContext *ctx, re2::StringPiece stmts,
|
||||
std::string *error_message) {
|
||||
while (true) {
|
||||
size_t used = 0;
|
||||
auto stmt = ctx->db()->Prepare(stmts, &used, error_message);
|
||||
if (!stmt.valid()) {
|
||||
// Statement didn't parse. If |error_message| is empty, there are just no
|
||||
// more statements. Otherwise this is due to an error. Either way, return.
|
||||
return error_message->empty();
|
||||
}
|
||||
VLOG(1) << "Running statement:\n" << stmts.substr(0, used).as_string();
|
||||
int64_t rows = 0;
|
||||
auto run = ctx->Borrow(&stmt);
|
||||
while (run.Step() == SQLITE_ROW) {
|
||||
++rows;
|
||||
}
|
||||
if (rows > 0) {
|
||||
VLOG(1) << "Statement returned " << rows << " row(s).";
|
||||
}
|
||||
if (run.status() != SQLITE_DONE) {
|
||||
VLOG(1) << "Statement failed with " << run.status() << ": "
|
||||
<< run.error_message();
|
||||
*error_message =
|
||||
StrCat("Unexpected error ", run.error_message(),
|
||||
" from statement: \"", stmts.substr(0, used), "\"");
|
||||
return false;
|
||||
}
|
||||
VLOG(1) << "Statement succeeded.";
|
||||
stmts.remove_prefix(used);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace moonfire_nvr
|
276
src/sqlite.h
276
src/sqlite.h
@ -1,276 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// sqlite.h: a quick C++ wrapper interface around the SQLite3 C API.
|
||||
// This provides RAII and takes advantage of some types like re2::StringPiece.
|
||||
// It makes no attempt to hide how the underlying API works, so read
|
||||
// alongside: <https://www.sqlite.org/capi3ref.html>
|
||||
|
||||
#ifndef MOONFIRE_NVR_SQLITE_H
|
||||
#define MOONFIRE_NVR_SQLITE_H
|
||||
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include <glog/logging.h>
|
||||
#include <re2/stringpiece.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include "common.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
// Prepared statement. Movable, not copyable.
|
||||
// The caller can obtain a Statement via Database::Prepare
|
||||
// and use one via DatabaseContext::Borrow.
|
||||
class Statement {
|
||||
public:
|
||||
Statement() {}
|
||||
Statement(Statement &&);
|
||||
void operator=(Statement &&);
|
||||
~Statement();
|
||||
|
||||
bool valid() const { return me_ != nullptr; }
|
||||
|
||||
private:
|
||||
friend class Database;
|
||||
friend class DatabaseContext;
|
||||
friend class RunningStatement;
|
||||
|
||||
Statement(const Statement &) = delete;
|
||||
void operator=(const Statement &) = delete;
|
||||
|
||||
void Clear();
|
||||
|
||||
sqlite3_stmt *me_ = nullptr; // owned.
|
||||
bool borrowed_ = false;
|
||||
};
|
||||
|
||||
class Database {
|
||||
public:
|
||||
Database() {}
|
||||
Database(const Database &) = delete;
|
||||
Database &operator=(const Database &) = delete;
|
||||
|
||||
// PRE: all DatabaseContext and Statement objects have been deleted.
|
||||
~Database();
|
||||
|
||||
// Open the database and do initial setup.
|
||||
//
|
||||
// Foreign keys will always be enabled via "pragma foreign_keys = true;".
|
||||
//
|
||||
// extended result codes will always be enabled via
|
||||
// sqlite3_extended_result_codes.
|
||||
bool Open(const char *filename, int flags, std::string *error_message);
|
||||
|
||||
// Prepare a statement. Thread-safe.
|
||||
//
|
||||
// |used|, if non-null, will be updated with the number of bytes used from
|
||||
// |sql| on success. (Only the first statement is parsed.)
|
||||
//
|
||||
// Returns a statement, which may or may not be valid().
|
||||
// |error_message| will be empty if there is simply no statement to parse.
|
||||
Statement Prepare(re2::StringPiece sql, size_t *used,
|
||||
std::string *error_message);
|
||||
|
||||
private:
|
||||
friend class DatabaseContext;
|
||||
friend class RunningStatement;
|
||||
sqlite3 *me_ = nullptr;
|
||||
Statement begin_transaction_;
|
||||
Statement commit_transaction_;
|
||||
Statement rollback_transaction_;
|
||||
|
||||
std::mutex ctx_mu_; // used by DatabaseContext.
|
||||
};
|
||||
|
||||
// A running statement; get via DatabaseContext::Borrow or
|
||||
// DatabaseContext::UseOnce. Example uses:
|
||||
//
|
||||
// {
|
||||
// DatabaseContext ctx(&db);
|
||||
// auto run = ctx.UseOnce("insert into table (column) values (:column)");
|
||||
// run.BindText(":column", "value");
|
||||
// if (run.Step() != SQLITE_DONE) {
|
||||
// LOG(ERROR) << "Operation failed: " << run.error_message();
|
||||
// return;
|
||||
// }
|
||||
// int64_t rowid = ctx.last_insert_rowid();
|
||||
// }
|
||||
//
|
||||
// Statement select_stmt = db.Prepare(
|
||||
// "select rowid from table;", nullptr, &error_message);
|
||||
// ...
|
||||
// {
|
||||
// auto run = ctx.Borrow(&select_stmt);
|
||||
// while (run.Step() == SQLITE_ROW) {
|
||||
// int64_t rowid = op.ColumnInt64(0);
|
||||
// ...
|
||||
// }
|
||||
// if (run.status() != SQLITE_DONE) {
|
||||
// LOG(ERROR) << "Operation failed: " << run.error_message();
|
||||
// }
|
||||
// }
|
||||
class RunningStatement {
|
||||
public:
|
||||
RunningStatement(RunningStatement &&o);
|
||||
|
||||
// Reset/unbind/return the statement for the next use (in the case of
|
||||
// Borrow) or delete it (in the case of UseOnce).
|
||||
~RunningStatement();
|
||||
|
||||
// Bind a value to a parameter. Call before the first Step.
|
||||
// |param| is indexed from 1 (unlike columns!).
|
||||
//
|
||||
// StringPiece |value|s will be copied; they do not need to outlive the
|
||||
// Bind{Blob,Text} call. Text values are assumed to be UTF-8.
|
||||
//
|
||||
// Errors are deferred until Step() for simplicity of caller code.
|
||||
void BindBlob(int param, re2::StringPiece value);
|
||||
void BindBlob(const char *param, re2::StringPiece value);
|
||||
void BindText(int param, re2::StringPiece value);
|
||||
void BindText(const char *param, re2::StringPiece value);
|
||||
void BindInt64(int param, int64_t value);
|
||||
void BindInt64(const char *param, int64_t value);
|
||||
|
||||
// Advance the statement, returning SQLITE_ROW, SQLITE_DONE, or an error.
|
||||
// Note that this may return a "deferred error" if UseOnce failed to parse
|
||||
// the SQL or if a bind failed.
|
||||
int Step();
|
||||
|
||||
// Convenience function; re-return the last status from Step().
|
||||
int status() { return status_; }
|
||||
|
||||
// Return a stringified version of the last status.
|
||||
// This may have more information than sqlite3_errstr(status()),
|
||||
// in the case of "deferred errors".
|
||||
std::string error_message() { return error_message_; }
|
||||
|
||||
// Column accessors, to be called after Step() returns SQLITE_ROW.
|
||||
// Columns are indexed from 0 (unlike bind parameters!).
|
||||
// StringPiece values are valid only until a type conversion, the following
|
||||
// NextRow() call, or destruction of the RunningStatement, whichever
|
||||
// happens first.
|
||||
//
|
||||
// Note there is no useful way to report error here. In particular, the
|
||||
// underlying SQLite functions return a default value on error, which can't
|
||||
// be distinguished from a legitimate value. The error code is set on the
|
||||
// database, but it's not guaranteed to *not* be set if there's no error.
|
||||
|
||||
// Return the type of a given column; if SQLITE_NULL, the value is null.
|
||||
// As noted in sqlite3_column_type() documentation, this is only meaningful
|
||||
// if other Column* calls have not forced a type conversion.
|
||||
int ColumnType(int col);
|
||||
|
||||
re2::StringPiece ColumnBlob(int col);
|
||||
int64_t ColumnInt64(int col);
|
||||
re2::StringPiece ColumnText(int col);
|
||||
|
||||
private:
|
||||
friend class DatabaseContext;
|
||||
RunningStatement(Statement *stmt, const std::string &deferred_error,
|
||||
bool own_statement);
|
||||
RunningStatement(const RunningStatement &) = delete;
|
||||
void operator=(const RunningStatement &) = delete;
|
||||
|
||||
Statement *statement_ = nullptr; // maybe owned; see owns_statement_.
|
||||
std::string error_message_;
|
||||
int status_ = SQLITE_OK;
|
||||
bool owns_statement_ = false;
|
||||
};
|
||||
|
||||
// A scoped database lock and transaction manager.
|
||||
//
|
||||
// Moonfire NVR does all SQLite operations under a lock, to avoid SQLITE_BUSY
|
||||
// and so that calls such as sqlite3_last_insert_rowid return useful values.
|
||||
// This class implicitly acquires the lock on entry / releases it on exit.
|
||||
// In the future, it may have instrumentation to track slow operations.
|
||||
class DatabaseContext {
|
||||
public:
|
||||
// Acquire a lock on |db|, which must already be opened.
|
||||
explicit DatabaseContext(Database *db);
|
||||
DatabaseContext(const DatabaseContext &) = delete;
|
||||
void operator=(const DatabaseContext &) = delete;
|
||||
|
||||
// Release the lock and, if an explicit transaction is active, roll it
|
||||
// back with a logged warning.
|
||||
~DatabaseContext();
|
||||
|
||||
// Begin a transaction, or return false and fill |error_message|.
|
||||
// If successful, the caller should explicitly call CommitTransaction or
|
||||
// RollbackTransaction before the DatabaseContext goes out of scope.
|
||||
bool BeginTransaction(std::string *error_message);
|
||||
|
||||
// Commit the transaction, or return false and fill |error_message|.
|
||||
bool CommitTransaction(std::string *error_message);
|
||||
|
||||
// Roll back the transaction, logging error on failure.
|
||||
// The error code is not returned; there's nothing useful the caller can do.
|
||||
void RollbackTransaction();
|
||||
|
||||
// Borrow a prepared statement to run.
|
||||
// |statement| should outlive the RunningStatement. It can't be borrowed
|
||||
// twice simultaneously, but two similar statements can be run side-by-side
|
||||
// (in the same context).
|
||||
RunningStatement Borrow(Statement *statement);
|
||||
|
||||
// Use the given |sql| once.
|
||||
// Note that parse errors are "deferred" until RunningStatement::Step().
|
||||
RunningStatement UseOnce(re2::StringPiece sql);
|
||||
|
||||
// Return the number of changes for the last DML statement (insert, update, or
|
||||
// delete), as with sqlite3_changes.
|
||||
int64_t changes() { return sqlite3_changes(db_->me_); }
|
||||
|
||||
// Return the last rowid inserted into a table that does not specify "WITHOUT
|
||||
// ROWID", as with sqlite3_last_insert_rowid.
|
||||
int64_t last_insert_rowid() { return sqlite3_last_insert_rowid(db_->me_); }
|
||||
|
||||
Database *db() { return db_; }
|
||||
|
||||
private:
|
||||
Database *db_;
|
||||
std::lock_guard<std::mutex> lock_;
|
||||
bool transaction_open_ = false;
|
||||
};
|
||||
|
||||
// Convenience routines below.
|
||||
|
||||
// Run through all the statements in |stmts|.
|
||||
// Return error if any do not parse or return something other than SQLITE_DONE
|
||||
// when stepped. (SQLITE_ROW returns are skipped over, though. This is useful
|
||||
// for "pragma journal_mode = wal;" which returns a row.)
|
||||
bool RunStatements(DatabaseContext *ctx, re2::StringPiece stmts,
|
||||
std::string *error_message);
|
||||
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
#endif // MOONFIRE_NVR_SQLITE_H
|
162
src/stream.rs
Normal file
162
src/stream.rs
Normal file
@ -0,0 +1,162 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use error::Error;
|
||||
use ffmpeg::{self, format, media};
|
||||
use ffmpeg_sys::{self, AVLockOp};
|
||||
use h264;
|
||||
use libc::{self, c_int, c_void};
|
||||
use std::mem;
|
||||
use std::ptr;
|
||||
use std::result::Result;
|
||||
use std::slice;
|
||||
use std::sync;
|
||||
|
||||
static START: sync::Once = sync::ONCE_INIT;
|
||||
|
||||
pub enum StreamSource<'a> {
|
||||
#[cfg(test)]
|
||||
File(&'a str), // filename, for testing.
|
||||
|
||||
Rtsp(&'a str), // url, for production use.
|
||||
}
|
||||
|
||||
// TODO: I think this should be provided by ffmpeg-sys. Otherwise, ffmpeg-sys is thread-hostile,
|
||||
// which I believe is not allowed at all in Rust. (Also, this method's signature should include
|
||||
// unsafe.)
|
||||
extern "C" fn lock_callback(untyped_ptr: *mut *mut c_void, op: AVLockOp) -> c_int {
|
||||
unsafe {
|
||||
let ptr = mem::transmute::<*mut *mut c_void, *mut *mut libc::pthread_mutex_t>(untyped_ptr);
|
||||
match op {
|
||||
AVLockOp::AV_LOCK_CREATE => {
|
||||
let m = Box::<libc::pthread_mutex_t>::new(mem::uninitialized());
|
||||
*ptr = Box::into_raw(m);
|
||||
libc::pthread_mutex_init(*ptr, ptr::null());
|
||||
},
|
||||
AVLockOp::AV_LOCK_DESTROY => {
|
||||
libc::pthread_mutex_destroy(*ptr);
|
||||
Box::from_raw(*ptr); // delete.
|
||||
*ptr = ptr::null_mut();
|
||||
},
|
||||
AVLockOp::AV_LOCK_OBTAIN => {
|
||||
libc::pthread_mutex_lock(*ptr);
|
||||
},
|
||||
AVLockOp::AV_LOCK_RELEASE => {
|
||||
libc::pthread_mutex_unlock(*ptr);
|
||||
},
|
||||
};
|
||||
};
|
||||
0
|
||||
}
|
||||
|
||||
impl<'a> StreamSource<'a> {
|
||||
pub fn open(&self) -> Result<Stream, Error> {
|
||||
START.call_once(|| {
|
||||
unsafe { ffmpeg_sys::av_lockmgr_register(lock_callback); };
|
||||
ffmpeg::init().unwrap();
|
||||
ffmpeg::format::network::init();
|
||||
|
||||
});
|
||||
|
||||
let (input, discard_first) = match *self {
|
||||
#[cfg(test)]
|
||||
StreamSource::File(filename) =>
|
||||
(format::input_with(&format!("file:{}", filename), ffmpeg::Dictionary::new())?,
|
||||
false),
|
||||
StreamSource::Rtsp(url) => {
|
||||
let open_options = dict![
|
||||
"rtsp_transport" => "tcp",
|
||||
// https://trac.ffmpeg.org/ticket/5018 workaround attempt.
|
||||
"probesize" => "262144",
|
||||
"user-agent" => "moonfire-nvr",
|
||||
// 10-second socket timeout, in microseconds.
|
||||
"stimeout" => "10000000"
|
||||
];
|
||||
(format::input_with(&url, open_options)?, true)
|
||||
},
|
||||
};
|
||||
|
||||
// Find the video stream.
|
||||
let mut video_i = None;
|
||||
for (i, stream) in input.streams().enumerate() {
|
||||
if stream.codec().medium() == media::Type::Video {
|
||||
debug!("Video stream index is {}", i);
|
||||
video_i = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let video_i = match video_i {
|
||||
Some(i) => i,
|
||||
None => { return Err(Error::new("no video stream".to_owned())) },
|
||||
};
|
||||
|
||||
let mut stream = Stream{
|
||||
input: input,
|
||||
video_i: video_i,
|
||||
};
|
||||
|
||||
if discard_first {
|
||||
info!("Discarding the first packet to work around https://trac.ffmpeg.org/ticket/5018");
|
||||
stream.get_next()?;
|
||||
}
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Stream {
|
||||
input: format::context::Input,
|
||||
video_i: usize,
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
pub fn get_extra_data(&self) -> Result<h264::ExtraData, Error> {
|
||||
let video = self.input.stream(self.video_i).expect("can't get video stream known to exist");
|
||||
let codec = video.codec();
|
||||
let (extradata, width, height) = unsafe {
|
||||
let ptr = codec.as_ptr();
|
||||
(slice::from_raw_parts((*ptr).extradata, (*ptr).extradata_size as usize),
|
||||
(*ptr).width as u16,
|
||||
(*ptr).height as u16)
|
||||
};
|
||||
// TODO: verify video stream is h264.
|
||||
h264::ExtraData::parse(extradata, width, height)
|
||||
}
|
||||
|
||||
pub fn get_next(&mut self) -> Result<ffmpeg::Packet, ffmpeg::Error> {
|
||||
let mut pkt = ffmpeg::Packet::empty();
|
||||
loop {
|
||||
pkt.read(&mut self.input)?;
|
||||
if pkt.stream() == self.video_i {
|
||||
return Ok(pkt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
158
src/streamer.rs
Normal file
158
src/streamer.rs
Normal file
@ -0,0 +1,158 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use db::{Camera, Database};
|
||||
use dir;
|
||||
use error::Error;
|
||||
use h264;
|
||||
use recording;
|
||||
use std::result::Result;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use stream::StreamSource;
|
||||
use time;
|
||||
|
||||
pub static ROTATE_INTERVAL_SEC: i64 = 60;
|
||||
|
||||
pub struct Streamer {
|
||||
shutdown: Arc<AtomicBool>,
|
||||
|
||||
// State below is only used by the thread in Run.
|
||||
rotate_offset_sec: i64,
|
||||
db: Arc<Database>,
|
||||
dir: Arc<dir::SampleFileDir>,
|
||||
syncer_channel: dir::SyncerChannel,
|
||||
camera_id: i32,
|
||||
short_name: String,
|
||||
url: String,
|
||||
redacted_url: String,
|
||||
}
|
||||
|
||||
impl Streamer {
|
||||
pub fn new(db: Arc<Database>, dir: Arc<dir::SampleFileDir>, syncer_channel: dir::SyncerChannel,
|
||||
shutdown: Arc<AtomicBool>, camera_id: i32, c: &Camera, rotate_offset_sec: i64)
|
||||
-> Self {
|
||||
Streamer{
|
||||
shutdown: shutdown,
|
||||
rotate_offset_sec: rotate_offset_sec,
|
||||
db: db,
|
||||
dir: dir,
|
||||
syncer_channel: syncer_channel,
|
||||
camera_id: camera_id,
|
||||
short_name: c.short_name.to_owned(),
|
||||
url: format!("rtsp://{}:{}@{}{}", c.username, c.password, c.host, c.main_rtsp_path),
|
||||
redacted_url: format!("rtsp://{}:redacted@{}{}", c.username, c.host, c.main_rtsp_path),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_name(&self) -> &str { &self.short_name }
|
||||
|
||||
pub fn run(&mut self) {
|
||||
while !self.shutdown.load(Ordering::SeqCst) {
|
||||
if let Err(e) = self.run_once() {
|
||||
let sleep_time = Duration::from_secs(1);
|
||||
warn!("{}: sleeping for {:?} after error: {}", self.short_name, sleep_time, e);
|
||||
thread::sleep(sleep_time);
|
||||
}
|
||||
}
|
||||
info!("{}: shutting down", self.short_name);
|
||||
}
|
||||
|
||||
fn run_once(&mut self) -> Result<(), Error> {
|
||||
info!("{}: Opening input: {}", self.short_name, self.redacted_url);
|
||||
|
||||
// TODO: mockability?
|
||||
let mut stream = StreamSource::Rtsp(&self.url).open()?;
|
||||
// TODO: verify time base.
|
||||
// TODO: verify width/height.
|
||||
let extra_data = stream.get_extra_data()?;
|
||||
let video_sample_entry_id =
|
||||
self.db.lock().insert_video_sample_entry(extra_data.width, extra_data.height,
|
||||
&extra_data.sample_entry)?;
|
||||
debug!("{}: video_sample_entry_id={}", self.short_name, video_sample_entry_id);
|
||||
let mut seen_key_frame = false;
|
||||
let mut rotate = None;
|
||||
let mut writer: Option<recording::Writer> = None;
|
||||
let mut transformed = Vec::new();
|
||||
let mut next_start = None;
|
||||
while !self.shutdown.load(Ordering::SeqCst) {
|
||||
let pkt = stream.get_next()?;
|
||||
if !seen_key_frame && !pkt.is_key() {
|
||||
continue;
|
||||
} else if !seen_key_frame {
|
||||
debug!("{}: have first key frame", self.short_name);
|
||||
seen_key_frame = true;
|
||||
}
|
||||
let frame_realtime = time::get_time();
|
||||
if let Some(r) = rotate {
|
||||
if frame_realtime.sec > r && pkt.is_key() {
|
||||
let w = writer.take().expect("rotate set implies writer is set");
|
||||
next_start = Some(w.end());
|
||||
// TODO: restore this log message.
|
||||
// info!("{}: wrote {}: [{}, {})", self.short_name, r.sample_file_uuid,
|
||||
// r.time.start, r.time.end);
|
||||
self.syncer_channel.async_save_writer(w)?;
|
||||
}
|
||||
};
|
||||
let mut w = match writer {
|
||||
Some(w) => w,
|
||||
None => {
|
||||
let r = frame_realtime.sec -
|
||||
(frame_realtime.sec % ROTATE_INTERVAL_SEC) +
|
||||
self.rotate_offset_sec;
|
||||
rotate = Some(
|
||||
if r <= frame_realtime.sec { r + ROTATE_INTERVAL_SEC } else { r });
|
||||
let local_realtime = recording::Time::new(frame_realtime);
|
||||
|
||||
self.dir.create_writer(next_start.unwrap_or(local_realtime), local_realtime,
|
||||
self.camera_id, video_sample_entry_id)?
|
||||
},
|
||||
};
|
||||
let orig_data = match pkt.data() {
|
||||
Some(d) => d,
|
||||
None => return Err(Error::new("packet has no data".to_owned())),
|
||||
};
|
||||
let transformed_data = if extra_data.need_transform {
|
||||
h264::transform_sample_data(orig_data, &mut transformed)?;
|
||||
transformed.as_slice()
|
||||
} else {
|
||||
orig_data
|
||||
};
|
||||
w.write(transformed_data, pkt.duration() as i32, pkt.is_key())?;
|
||||
writer = Some(w);
|
||||
}
|
||||
if let Some(w) = writer {
|
||||
self.syncer_channel.async_save_writer(w)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// string-test.cc: tests of the string.h interface.
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
#include <glog/logging.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "string.h"
|
||||
|
||||
DECLARE_bool(alsologtostderr);
|
||||
|
||||
namespace moonfire_nvr {
|
||||
namespace {
|
||||
|
||||
TEST(StrCatTest, Simple) {
|
||||
EXPECT_EQ("foo", StrCat("foo"));
|
||||
EXPECT_EQ("foobar", StrCat("foo", "bar"));
|
||||
EXPECT_EQ("foo", StrCat(std::string("foo")));
|
||||
|
||||
EXPECT_EQ("42", StrCat(uint64_t(42)));
|
||||
EXPECT_EQ("0", StrCat(uint64_t(0)));
|
||||
EXPECT_EQ("18446744073709551615",
|
||||
StrCat(std::numeric_limits<uint64_t>::max()));
|
||||
|
||||
EXPECT_EQ("42", StrCat(int64_t(42)));
|
||||
EXPECT_EQ("0", StrCat(int64_t(0)));
|
||||
EXPECT_EQ("-9223372036854775808",
|
||||
StrCat(std::numeric_limits<int64_t>::min()));
|
||||
EXPECT_EQ("9223372036854775807", StrCat(std::numeric_limits<int64_t>::max()));
|
||||
}
|
||||
|
||||
TEST(JoinTest, Simple) {
|
||||
EXPECT_EQ("", Join(std::initializer_list<std::string>(), ","));
|
||||
EXPECT_EQ("a", Join(std::initializer_list<std::string>({"a"}), ","));
|
||||
EXPECT_EQ("a,b", Join(std::initializer_list<const char *>({"a", "b"}), ","));
|
||||
EXPECT_EQ(
|
||||
"a,b,c",
|
||||
Join(std::initializer_list<re2::StringPiece>({"a", "b", "c"}), ","));
|
||||
}
|
||||
|
||||
TEST(EscapeTest, Simple) {
|
||||
EXPECT_EQ("", moonfire_nvr::EscapeHtml(""));
|
||||
EXPECT_EQ("no special chars", moonfire_nvr::EscapeHtml("no special chars"));
|
||||
EXPECT_EQ("<tag> & text", moonfire_nvr::EscapeHtml("<tag> & text"));
|
||||
}
|
||||
|
||||
TEST(ToHexTest, Simple) {
|
||||
EXPECT_EQ("", ToHex("", false));
|
||||
EXPECT_EQ("", ToHex("", true));
|
||||
EXPECT_EQ("1234deadbeef", ToHex("\x12\x34\xde\xad\xbe\xef", false));
|
||||
EXPECT_EQ("12 34 de ad be ef", ToHex("\x12\x34\xde\xad\xbe\xef", true));
|
||||
}
|
||||
|
||||
TEST(HumanizeTest, Simple) {
|
||||
EXPECT_EQ("1.0 B", HumanizeWithBinaryPrefix(1.f, "B"));
|
||||
EXPECT_EQ("1.0 KiB", HumanizeWithBinaryPrefix(UINT64_C(1) << 10, "B"));
|
||||
EXPECT_EQ("1.0 EiB", HumanizeWithBinaryPrefix(UINT64_C(1) << 60, "B"));
|
||||
EXPECT_EQ("1.5 EiB", HumanizeWithBinaryPrefix(
|
||||
(UINT64_C(1) << 60) + (UINT64_C(1) << 59), "B"));
|
||||
EXPECT_EQ("16.0 EiB", HumanizeWithBinaryPrefix(
|
||||
std::numeric_limits<uint64_t>::max(), "B"));
|
||||
|
||||
EXPECT_EQ("1.0 Mbps", HumanizeWithDecimalPrefix(1e6f, "bps"));
|
||||
EXPECT_EQ("1000.0 Ebps", HumanizeWithDecimalPrefix(1e21, "bps"));
|
||||
}
|
||||
|
||||
TEST(AtoiTest, Simple) {
|
||||
int64_t out;
|
||||
EXPECT_TRUE(Atoi64("1234", 10, &out));
|
||||
EXPECT_EQ(1234, out);
|
||||
EXPECT_FALSE(Atoi64(nullptr, 10, &out));
|
||||
EXPECT_FALSE(Atoi64("", 10, &out));
|
||||
EXPECT_FALSE(Atoi64("asdf", 10, &out));
|
||||
EXPECT_FALSE(Atoi64("1234asdf", 10, &out));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
FLAGS_alsologtostderr = true;
|
||||
google::ParseCommandLineFlags(&argc, &argv, true);
|
||||
testing::InitGoogleTest(&argc, argv);
|
||||
google::InitGoogleLogging(argv[0]);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
189
src/string.cc
189
src/string.cc
@ -1,189 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// string.cc: See string.h.
|
||||
|
||||
#include "string.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <glog/logging.h>
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
namespace {
|
||||
|
||||
char HexDigit(unsigned int i) {
|
||||
static char kHexadigits[] = "0123456789abcdef";
|
||||
return (i < 16) ? kHexadigits[i] : 'x';
|
||||
}
|
||||
|
||||
std::string Humanize(std::initializer_list<const re2::StringPiece> prefixes,
|
||||
float f, float n, re2::StringPiece suffix) {
|
||||
size_t i;
|
||||
for (i = 0; i < prefixes.size() - 1 && n >= f; ++i) n /= f;
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%.1f", n);
|
||||
return StrCat(buf, *(prefixes.begin() + i), suffix);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace internal {
|
||||
|
||||
StrCatPiece::StrCatPiece(uint64_t p) {
|
||||
if (p == 0) {
|
||||
piece_ = "0";
|
||||
} else {
|
||||
size_t i = sizeof(buf_);
|
||||
while (p != 0) {
|
||||
buf_[--i] = '0' + (p % 10);
|
||||
p /= 10;
|
||||
}
|
||||
piece_.set(buf_ + i, sizeof(buf_) - i);
|
||||
}
|
||||
}
|
||||
|
||||
StrCatPiece::StrCatPiece(int64_t p) {
|
||||
if (p == 0) {
|
||||
piece_ = "0";
|
||||
} else {
|
||||
bool negative = p < 0;
|
||||
size_t i = sizeof(buf_);
|
||||
while (p != 0) {
|
||||
buf_[--i] = '0' + std::abs(p % 10);
|
||||
p /= 10;
|
||||
}
|
||||
if (negative) {
|
||||
buf_[--i] = '-';
|
||||
}
|
||||
piece_.set(buf_ + i, sizeof(buf_) - i);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
std::string EscapeHtml(const std::string &input) {
|
||||
std::string output;
|
||||
output.reserve(input.size());
|
||||
for (char c : input) {
|
||||
switch (c) {
|
||||
case '&':
|
||||
output.append("&");
|
||||
break;
|
||||
case '<':
|
||||
output.append("<");
|
||||
break;
|
||||
case '>':
|
||||
output.append(">");
|
||||
break;
|
||||
default:
|
||||
output.push_back(c);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
std::string ToHex(re2::StringPiece in, bool pad) {
|
||||
std::string out;
|
||||
out.reserve(in.size() * (2 + pad) + pad);
|
||||
for (int i = 0; i < in.size(); ++i) {
|
||||
if (pad && i > 0) out.push_back(' ');
|
||||
uint8_t byte = in[i];
|
||||
out.push_back(HexDigit(byte >> 4));
|
||||
out.push_back(HexDigit(byte & 0x0F));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string HumanizeWithDecimalPrefix(float n, re2::StringPiece suffix) {
|
||||
static const std::initializer_list<const re2::StringPiece> kPrefixes = {
|
||||
" ", " k", " M", " G", " T", " P", " E"};
|
||||
return Humanize(kPrefixes, 1000., n, suffix);
|
||||
}
|
||||
|
||||
std::string HumanizeWithBinaryPrefix(float n, re2::StringPiece suffix) {
|
||||
static const std::initializer_list<const re2::StringPiece> kPrefixes = {
|
||||
" ", " Ki", " Mi", " Gi", " Ti", " Pi", " Ei"};
|
||||
return Humanize(kPrefixes, 1024., n, suffix);
|
||||
}
|
||||
|
||||
std::string HumanizeDuration(int64_t seconds) {
|
||||
static const int64_t kMinuteInSeconds = 60;
|
||||
static const int64_t kHourInSeconds = 60 * kMinuteInSeconds;
|
||||
static const int64_t kDayInSeconds = 24 * kHourInSeconds;
|
||||
int64_t days = seconds / kDayInSeconds;
|
||||
seconds %= kDayInSeconds;
|
||||
int64_t hours = seconds / kHourInSeconds;
|
||||
seconds %= kHourInSeconds;
|
||||
int64_t minutes = seconds / kMinuteInSeconds;
|
||||
seconds %= kMinuteInSeconds;
|
||||
std::string out;
|
||||
if (days > 0) {
|
||||
out.append(StrCat(days, days == 1 ? " day" : " days"));
|
||||
}
|
||||
if (hours > 0) {
|
||||
if (!out.empty()) {
|
||||
out.append(" ");
|
||||
}
|
||||
out.append(StrCat(hours, hours == 1 ? " hour" : " hours"));
|
||||
}
|
||||
if (minutes > 0) {
|
||||
if (!out.empty()) {
|
||||
out.append(" ");
|
||||
}
|
||||
out.append(StrCat(minutes, minutes == 1 ? " minute" : " minutes"));
|
||||
}
|
||||
if (seconds > 0 || out.empty()) {
|
||||
if (!out.empty()) {
|
||||
out.append(" ");
|
||||
}
|
||||
out.append(StrCat(seconds, seconds == 1 ? " second" : " seconds"));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool strto64(const char *str, int base, const char **endptr, int64_t *value) {
|
||||
static_assert(sizeof(int64_t) == sizeof(long long int),
|
||||
"unknown memory model");
|
||||
if (str == nullptr) {
|
||||
return false;
|
||||
}
|
||||
errno = 0;
|
||||
*value = ::strtoll(str, const_cast<char **>(endptr), base);
|
||||
return *endptr != str && errno == 0;
|
||||
}
|
||||
|
||||
bool Atoi64(const char *str, int base, int64_t *value) {
|
||||
const char *endptr;
|
||||
return strto64(str, base, &endptr, value) && *endptr == '\0';
|
||||
}
|
||||
|
||||
} // namespace moonfire_nvr
|
138
src/string.h
138
src/string.h
@ -1,138 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// string.h: convenience methods for dealing with strings.
|
||||
|
||||
#ifndef MOONFIRE_NVR_STRING_H
|
||||
#define MOONFIRE_NVR_STRING_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <re2/stringpiece.h>
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
namespace internal {
|
||||
|
||||
// Only used within StrCat() and Join().
|
||||
// Note implicit constructor, which is necessary to avoid a copy,
|
||||
// though it could be wrapped in another type.
|
||||
// http://stackoverflow.com/questions/34112755/can-i-avoid-a-c11-move-when-initializing-an-array/34113744
|
||||
class StrCatPiece {
|
||||
public:
|
||||
StrCatPiece(uint64_t p);
|
||||
StrCatPiece(int64_t p);
|
||||
StrCatPiece(uint32_t p) : StrCatPiece(static_cast<uint64_t>(p)) {}
|
||||
StrCatPiece(int32_t p) : StrCatPiece(static_cast<int64_t>(p)) {}
|
||||
|
||||
#ifndef __LP64__ // if sizeof(long) == sizeof(int32_t)
|
||||
// Need to resolve ambiguity.
|
||||
StrCatPiece(long p) : StrCatPiece(static_cast<int32_t>(p)) {}
|
||||
StrCatPiece(unsigned long p) : StrCatPiece(static_cast<uint32_t>(p)) {}
|
||||
#endif
|
||||
|
||||
StrCatPiece(re2::StringPiece p) : piece_(p) {}
|
||||
|
||||
StrCatPiece(const StrCatPiece &) = delete;
|
||||
StrCatPiece &operator=(const StrCatPiece &) = delete;
|
||||
|
||||
const char *data() const { return piece_.data(); }
|
||||
size_t size() const { return piece_.size(); }
|
||||
|
||||
private:
|
||||
// Not allowed: ambiguous meaning.
|
||||
StrCatPiece(char);
|
||||
|
||||
// |piece_| points either to within buf_ (numeric constructors) or to unowned
|
||||
// string data (StringPiece constructor).
|
||||
re2::StringPiece piece_;
|
||||
char buf_[20]; // length of maximum uint64 (no terminator needed).
|
||||
};
|
||||
|
||||
} // namespace internal
|
||||
|
||||
// Concatenate any number of strings, StringPieces, and numeric values into a
|
||||
// single string.
|
||||
template <typename... Types>
|
||||
std::string StrCat(Types... args) {
|
||||
internal::StrCatPiece pieces[] = {{args}...};
|
||||
size_t size = 0;
|
||||
for (const auto &p : pieces) {
|
||||
size += p.size();
|
||||
}
|
||||
std::string out;
|
||||
out.reserve(size);
|
||||
for (const auto &p : pieces) {
|
||||
out.append(p.data(), p.size());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Join any number of string fragments (of like type) together into a single
|
||||
// string, with a separator.
|
||||
template <typename Container>
|
||||
std::string Join(const Container &pieces, re2::StringPiece separator) {
|
||||
std::string out;
|
||||
bool first = true;
|
||||
for (const auto &p : pieces) {
|
||||
if (!first) {
|
||||
out.append(separator.data(), separator.size());
|
||||
}
|
||||
first = false;
|
||||
internal::StrCatPiece piece(p);
|
||||
out.append(piece.data(), piece.size());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// HTML-escape the given UTF-8-encoded string.
|
||||
std::string EscapeHtml(const std::string &input);
|
||||
|
||||
// Return a hex-encoded version of |in|, optionally padding between bytes.
|
||||
// For example, ToHex("\xde\xad\xbe\xef", true) returns "de ad be ef".
|
||||
std::string ToHex(re2::StringPiece in, bool pad = false);
|
||||
|
||||
// Return a human-friendly approximation of the given non-negative value, using
|
||||
// SI (base-10) or IEC (base-2) prefixes.
|
||||
std::string HumanizeWithDecimalPrefix(float n, re2::StringPiece suffix);
|
||||
std::string HumanizeWithBinaryPrefix(float n, re2::StringPiece suffix);
|
||||
|
||||
std::string HumanizeDuration(int64_t sec);
|
||||
|
||||
// Wrapper around ::strtoll that returns true iff valid and corrects
|
||||
// constness. Returns false if |str| is null.
|
||||
bool strto64(const char *str, int base, const char **endptr, int64_t *value);
|
||||
|
||||
// Simpler form that expects the entire string to be a single integer.
|
||||
bool Atoi64(const char *str, int base, int64_t *value);
|
||||
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
#endif // MOONFIRE_NVR_STRING_H
|
153
src/testutil.cc
153
src/testutil.cc
@ -1,153 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// testutil.cc: implementation of testutil.h interface.
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include <glog/logging.h>
|
||||
|
||||
#include "filesystem.h"
|
||||
#include "string.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
namespace {
|
||||
|
||||
bool DeleteChildrenRecursively(const char *dirname, std::string *error_msg) {
|
||||
bool ok = true;
|
||||
auto fn = [&dirname, &ok, error_msg](const struct dirent *ent) {
|
||||
std::string name(ent->d_name);
|
||||
std::string path = StrCat(dirname, "/", name);
|
||||
if (name == "." || name == "..") {
|
||||
return IterationControl::kContinue;
|
||||
}
|
||||
bool is_dir = (ent->d_type == DT_DIR);
|
||||
if (ent->d_type == DT_UNKNOWN) {
|
||||
struct stat buf;
|
||||
int ret = GetRealFilesystem()->Stat(path.c_str(), &buf);
|
||||
CHECK_EQ(ret, 0) << path << ": " << strerror(ret);
|
||||
is_dir = S_ISDIR(buf.st_mode);
|
||||
}
|
||||
if (is_dir) {
|
||||
ok = ok && DeleteChildrenRecursively(path.c_str(), error_msg);
|
||||
if (!ok) {
|
||||
return IterationControl::kBreak;
|
||||
}
|
||||
int ret = GetRealFilesystem()->Rmdir(path.c_str());
|
||||
if (ret != 0) {
|
||||
*error_msg = StrCat("rmdir failed on ", path, ": ", strerror(ret));
|
||||
ok = false;
|
||||
return IterationControl::kBreak;
|
||||
}
|
||||
} else {
|
||||
int ret = GetRealFilesystem()->Unlink(path.c_str());
|
||||
if (ret != 0) {
|
||||
*error_msg = StrCat("unlink failed on ", path, ": ", strerror(ret));
|
||||
ok = false;
|
||||
return IterationControl::kBreak;
|
||||
}
|
||||
}
|
||||
return IterationControl::kContinue;
|
||||
};
|
||||
if (!GetRealFilesystem()->DirForEach(dirname, fn, error_msg)) {
|
||||
return false;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string PrepareTempDirOrDie(const std::string &test_name) {
|
||||
std::string dirname = StrCat("/tmp/test.", test_name);
|
||||
int ret = GetRealFilesystem()->Mkdir(dirname.c_str(), 0700);
|
||||
if (ret != 0) {
|
||||
CHECK_EQ(ret, EEXIST) << "mkdir failed: " << strerror(ret);
|
||||
std::string error_msg;
|
||||
CHECK(DeleteChildrenRecursively(dirname.c_str(), &error_msg)) << error_msg;
|
||||
}
|
||||
return dirname;
|
||||
}
|
||||
|
||||
void WriteFileOrDie(const std::string &path, re2::StringPiece contents) {
|
||||
std::unique_ptr<File> f;
|
||||
int ret = GetRealFilesystem()->Open(path.c_str(),
|
||||
O_WRONLY | O_CREAT | O_TRUNC, 0600, &f);
|
||||
CHECK_EQ(ret, 0) << "open " << path << ": " << strerror(ret);
|
||||
while (!contents.empty()) {
|
||||
size_t written;
|
||||
ret = f->Write(contents, &written);
|
||||
CHECK_EQ(ret, 0) << "write " << path << ": " << strerror(ret);
|
||||
contents.remove_prefix(written);
|
||||
}
|
||||
ret = f->Close();
|
||||
CHECK_EQ(ret, 0) << "close " << path << ": " << strerror(ret);
|
||||
}
|
||||
|
||||
void WriteFileOrDie(const std::string &path, EvBuffer *buf) {
|
||||
int fd = open(path.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0600);
|
||||
PCHECK(fd >= 0) << "open: " << path;
|
||||
while (evbuffer_get_length(buf->get()) > 0) {
|
||||
size_t buf_len = evbuffer_get_length(buf->get());
|
||||
int written = evbuffer_write(buf->get(), fd);
|
||||
PCHECK(written >= 0) << "buf_len: " << buf_len << ", written: " << written;
|
||||
}
|
||||
PCHECK(close(fd) == 0) << "close";
|
||||
}
|
||||
|
||||
std::string ReadFileOrDie(const std::string &path) {
|
||||
std::unique_ptr<File> f;
|
||||
int ret = GetRealFilesystem()->Open(path.c_str(), O_RDONLY, &f);
|
||||
CHECK_EQ(ret, 0) << "open " << path << ": " << strerror(ret);
|
||||
struct stat statbuf;
|
||||
ret = f->Stat(&statbuf);
|
||||
CHECK_EQ(ret, 0) << "fstat " << path << ": " << strerror(ret);
|
||||
std::string out(statbuf.st_size, '0');
|
||||
off_t bytes_read_total = 0;
|
||||
size_t bytes_read;
|
||||
while (bytes_read_total < statbuf.st_size) {
|
||||
ret = f->Read(&out[bytes_read_total], statbuf.st_size - bytes_read_total,
|
||||
&bytes_read);
|
||||
CHECK_EQ(ret, 0) << "read " << path << ": " << strerror(ret);
|
||||
CHECK_GT(bytes_read, 0);
|
||||
bytes_read_total += bytes_read;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace moonfire_nvr
|
155
src/testutil.h
155
src/testutil.h
@ -1,155 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// testutil.h: utilities for testing.
|
||||
|
||||
#ifndef MOONFIRE_NVR_TESTUTIL_H
|
||||
#define MOONFIRE_NVR_TESTUTIL_H
|
||||
|
||||
#include <glog/logging.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <re2/stringpiece.h>
|
||||
|
||||
#include "filesystem.h"
|
||||
#include "http.h"
|
||||
#include "uuid.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
// Create or empty the given test directory, or die.
|
||||
// Returns the full path.
|
||||
std::string PrepareTempDirOrDie(const std::string &test_name);
|
||||
|
||||
// Write the given file contents to the given path, or die.
|
||||
void WriteFileOrDie(const std::string &path, re2::StringPiece contents);
|
||||
void WriteFileOrDie(const std::string &path, EvBuffer *buf);
|
||||
|
||||
// Read the contents of the given path, or die.
|
||||
std::string ReadFileOrDie(const std::string &path);
|
||||
|
||||
// A scoped log sink for testing that the right log messages are sent.
|
||||
// Modelled after glog's "mock-log.h", which is not exported.
|
||||
// Use as follows:
|
||||
//
|
||||
// {
|
||||
// ScopedMockLog log;
|
||||
// EXPECT_CALL(log, Log(ERROR, _, HasSubstr("blah blah")));
|
||||
// log.Start();
|
||||
// ThingThatLogs();
|
||||
// }
|
||||
class ScopedMockLog : public google::LogSink {
|
||||
public:
|
||||
~ScopedMockLog() final { google::RemoveLogSink(this); }
|
||||
|
||||
// Start logging to this sink.
|
||||
// This is not done at construction time so that it's possible to set
|
||||
// expectations first, which is important if some background thread is
|
||||
// already logging.
|
||||
void Start() { google::AddLogSink(this); }
|
||||
|
||||
// Set expectations here.
|
||||
MOCK_METHOD3(Log, void(google::LogSeverity severity,
|
||||
const std::string &full_filename,
|
||||
const std::string &message));
|
||||
|
||||
private:
|
||||
struct LogEntry {
|
||||
google::LogSeverity severity = -1;
|
||||
std::string full_filename;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
// This method is called with locks held and thus shouldn't call Log.
|
||||
// It just stashes away the log entry for later.
|
||||
void send(google::LogSeverity severity, const char *full_filename,
|
||||
const char *base_filename, int line, const tm *tm_time,
|
||||
const char *message, size_t message_len) final {
|
||||
pending_.severity = severity;
|
||||
pending_.full_filename = full_filename;
|
||||
pending_.message.assign(message, message_len);
|
||||
}
|
||||
|
||||
// This method is always called after send() without locks.
|
||||
// It does the actual work of calling Log. It moves data away from
|
||||
// pending_ in case Log() logs itself (causing a nested call to send() and
|
||||
// WaitTillSent()).
|
||||
void WaitTillSent() final {
|
||||
LogEntry entry = std::move(pending_);
|
||||
Log(entry.severity, entry.full_filename, entry.message);
|
||||
}
|
||||
|
||||
LogEntry pending_;
|
||||
};
|
||||
|
||||
class MockUuidGenerator : public UuidGenerator {
|
||||
public:
|
||||
MOCK_METHOD0(Generate, Uuid());
|
||||
};
|
||||
|
||||
class MockFile : public File {
|
||||
public:
|
||||
MOCK_CONST_METHOD0(name, const std::string &());
|
||||
MOCK_METHOD3(Access, int(const char *, int, int));
|
||||
MOCK_METHOD0(Close, int());
|
||||
|
||||
// The std::unique_ptr<File> variants of Open are wrapped here because gmock's
|
||||
// SetArgPointee doesn't work well with std::unique_ptr.
|
||||
|
||||
int Open(const char *path, int flags, std::unique_ptr<File> *f) final {
|
||||
File *f_tmp = nullptr;
|
||||
int ret = OpenRaw(path, flags, &f_tmp);
|
||||
f->reset(f_tmp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int Open(const char *path, int flags, mode_t mode,
|
||||
std::unique_ptr<File> *f) final {
|
||||
File *f_tmp = nullptr;
|
||||
int ret = OpenRaw(path, flags, mode, &f_tmp);
|
||||
f->reset(f_tmp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
MOCK_METHOD1(Lock, int(int));
|
||||
MOCK_METHOD3(Open, int(const char *, int, int *));
|
||||
MOCK_METHOD4(Open, int(const char *, int, mode_t, int *));
|
||||
MOCK_METHOD3(OpenRaw, int(const char *, int, File **));
|
||||
MOCK_METHOD4(OpenRaw, int(const char *, int, mode_t, File **));
|
||||
MOCK_METHOD3(Read, int(void *, size_t, size_t *));
|
||||
MOCK_METHOD1(Stat, int(struct stat *));
|
||||
MOCK_METHOD0(Sync, int());
|
||||
MOCK_METHOD1(Truncate, int(off_t));
|
||||
MOCK_METHOD1(Unlink, int(const char *));
|
||||
MOCK_METHOD2(Write, int(re2::StringPiece, size_t *));
|
||||
};
|
||||
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
#endif // MOONFIRE_NVR_TESTUTIL_H
|
@ -1,4 +1,4 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@ -27,23 +27,19 @@
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// common.h: basic enums/defines/whatever.
|
||||
|
||||
#ifndef MOONFIRE_NVR_COMMON_H
|
||||
#define MOONFIRE_NVR_COMMON_H
|
||||
use std::sync;
|
||||
use slog::{self, DrainExt};
|
||||
use slog_envlogger;
|
||||
use slog_stdlog;
|
||||
use slog_term;
|
||||
|
||||
#define LIKELY(x) __builtin_expect((x), 1)
|
||||
#define UNLIKELY(x) __builtin_expect((x), 0)
|
||||
static INIT_LOGGING: sync::Once = sync::ONCE_INIT;
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
// Return value for *ForEach callbacks.
|
||||
enum class IterationControl {
|
||||
kContinue, // indicates the caller should proceed with the loop.
|
||||
kBreak // indicates the caller should terminate the loop with success.
|
||||
};
|
||||
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
#endif // MOONFIRE_NVR_COMMON_H
|
||||
pub fn init_logging() {
|
||||
INIT_LOGGING.call_once(|| {
|
||||
let drain = slog_term::StreamerBuilder::new().async().full().build();
|
||||
let drain = slog_envlogger::new(drain);
|
||||
slog_stdlog::set_logger(slog::Logger::root(drain.ignore_err(), None)).unwrap();
|
||||
});
|
||||
}
|
90
src/time.cc
90
src/time.cc
@ -1,90 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// time.cc: implementation of time.h interface.
|
||||
|
||||
#include "time.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <glog/logging.h>
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
namespace {
|
||||
|
||||
class RealClock : public WallClock {
|
||||
public:
|
||||
struct timespec Now() const final {
|
||||
struct timespec now;
|
||||
CHECK_EQ(0, clock_gettime(CLOCK_REALTIME, &now)) << strerror(errno);
|
||||
return now;
|
||||
}
|
||||
|
||||
void Sleep(struct timespec req) final {
|
||||
struct timespec rem;
|
||||
while (true) {
|
||||
int ret = nanosleep(&req, &rem);
|
||||
if (ret != 0 && errno != EINTR) {
|
||||
PLOG(FATAL) << "nanosleep";
|
||||
}
|
||||
if (ret == 0) {
|
||||
return;
|
||||
}
|
||||
req = rem;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// Returns the real wall clock, which will never be deleted.
|
||||
WallClock *GetRealClock() {
|
||||
static RealClock *real_clock = new RealClock; // never deleted.
|
||||
return real_clock;
|
||||
}
|
||||
|
||||
struct timespec SimulatedClock::Now() const {
|
||||
std::lock_guard<std::mutex> l(mu_);
|
||||
return now_;
|
||||
}
|
||||
|
||||
void SimulatedClock::Sleep(struct timespec req) {
|
||||
std::lock_guard<std::mutex> l(mu_);
|
||||
now_.tv_sec += req.tv_sec;
|
||||
now_.tv_nsec += req.tv_nsec;
|
||||
if (now_.tv_nsec > kNanos) {
|
||||
now_.tv_nsec -= kNanos;
|
||||
now_.tv_sec++;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace moonfire_nvr
|
78
src/time.h
78
src/time.h
@ -1,78 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// time.h: functions dealing with (wall) time.
|
||||
|
||||
#ifndef MOONFIRE_NVR_TIME_H
|
||||
#define MOONFIRE_NVR_TIME_H
|
||||
|
||||
#include <math.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <mutex>
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
constexpr long kNanos = 1000000000;
|
||||
|
||||
class WallClock {
|
||||
public:
|
||||
virtual ~WallClock() {}
|
||||
virtual struct timespec Now() const = 0;
|
||||
virtual void Sleep(struct timespec) = 0;
|
||||
};
|
||||
|
||||
class SimulatedClock : public WallClock {
|
||||
public:
|
||||
SimulatedClock() : now_({0, 0}) {}
|
||||
struct timespec Now() const final;
|
||||
void Sleep(struct timespec req) final;
|
||||
|
||||
private:
|
||||
mutable std::mutex mu_;
|
||||
struct timespec now_;
|
||||
};
|
||||
|
||||
inline struct timespec SecToTimespec(double sec) {
|
||||
double intpart;
|
||||
double fractpart = modf(sec, &intpart);
|
||||
return {static_cast<time_t>(intpart), static_cast<long>(fractpart * kNanos)};
|
||||
}
|
||||
|
||||
inline double TimespecToSec(struct timespec t) {
|
||||
return t.tv_sec + static_cast<double>(t.tv_nsec) / kNanos;
|
||||
}
|
||||
|
||||
// Returns the real wall clock, which will never be deleted.
|
||||
WallClock *GetRealClock();
|
||||
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
#endif // MOONFIRE_NVR_TIME_H
|
94
src/uuid.cc
94
src/uuid.cc
@ -1,94 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// uuid.cc: implementation of uuid.h interface.
|
||||
|
||||
#include "uuid.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
namespace {
|
||||
|
||||
const size_t kTextFormatLength =
|
||||
sizeof("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") - 1;
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Uuid::ParseText(re2::StringPiece input) {
|
||||
if (input.size() != kTextFormatLength) {
|
||||
return false;
|
||||
}
|
||||
char tmp[kTextFormatLength + 1];
|
||||
memcpy(tmp, input.data(), kTextFormatLength);
|
||||
tmp[kTextFormatLength] = 0;
|
||||
return uuid_parse(tmp, me_) == 0;
|
||||
}
|
||||
|
||||
bool Uuid::ParseBinary(re2::StringPiece input) {
|
||||
if (input.size() != sizeof(uuid_t)) {
|
||||
return false;
|
||||
}
|
||||
memcpy(me_, input.data(), sizeof(uuid_t));
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string Uuid::UnparseText() const {
|
||||
char tmp[kTextFormatLength + 1];
|
||||
uuid_unparse_lower(me_, tmp);
|
||||
return tmp;
|
||||
}
|
||||
|
||||
re2::StringPiece Uuid::binary_view() const {
|
||||
return re2::StringPiece(reinterpret_cast<const char *>(me_), sizeof(me_));
|
||||
}
|
||||
|
||||
bool Uuid::operator==(const Uuid &other) const {
|
||||
return uuid_compare(me_, other.me_) == 0;
|
||||
}
|
||||
|
||||
bool Uuid::operator<(const Uuid &other) const {
|
||||
return uuid_compare(me_, other.me_) < 0;
|
||||
}
|
||||
|
||||
class RealUuidGenerator : public UuidGenerator {
|
||||
public:
|
||||
Uuid Generate() final {
|
||||
Uuid out;
|
||||
uuid_generate(out.me_);
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
UuidGenerator *GetRealUuidGenerator() {
|
||||
static RealUuidGenerator *gen = new RealUuidGenerator; // never freed.
|
||||
return gen;
|
||||
}
|
||||
|
||||
} // namespace moonfire_nvr
|
81
src/uuid.h
81
src/uuid.h
@ -1,81 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// uuid.h: small wrapper around the C UUID library for generating/parsing
|
||||
// RFC 4122 UUIDs.
|
||||
|
||||
#ifndef MOONFIRE_NVR_UUID_H
|
||||
#define MOONFIRE_NVR_UUID_H
|
||||
|
||||
#include <re2/stringpiece.h>
|
||||
#include <uuid/uuid.h>
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
class Uuid {
|
||||
public:
|
||||
// Create a null uuid.
|
||||
Uuid() { uuid_clear(me_); }
|
||||
|
||||
// Parse the text UUID. Returns success.
|
||||
bool ParseText(re2::StringPiece input);
|
||||
|
||||
// Parse a binary UUID. In practice any 16-byte string is considered valid.
|
||||
bool ParseBinary(re2::StringPiece input);
|
||||
|
||||
// Return a 36-byte lowercase text representation, such as
|
||||
// 1b4e28ba-2fa1-11d2-883f-0016d3cca427.
|
||||
std::string UnparseText() const;
|
||||
|
||||
// Return a reference to the 16-byte binary form.
|
||||
// Invalidated by any change to the Uuid object.
|
||||
re2::StringPiece binary_view() const;
|
||||
|
||||
bool operator==(const Uuid &) const;
|
||||
bool operator<(const Uuid &) const;
|
||||
|
||||
bool is_null() const { return uuid_is_null(me_); }
|
||||
|
||||
private:
|
||||
friend class RealUuidGenerator;
|
||||
uuid_t me_;
|
||||
};
|
||||
|
||||
class UuidGenerator {
|
||||
public:
|
||||
virtual ~UuidGenerator() {}
|
||||
virtual Uuid Generate() = 0;
|
||||
};
|
||||
|
||||
UuidGenerator *GetRealUuidGenerator();
|
||||
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
#endif // MOONFIRE_NVR_CODING_H
|
505
src/web.cc
505
src/web.cc
@ -1,505 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// web.cc: implementation of web.h interface.
|
||||
|
||||
#include "web.h"
|
||||
|
||||
#include <glog/logging.h>
|
||||
#include <json/value.h>
|
||||
#include <json/writer.h>
|
||||
#include <re2/re2.h>
|
||||
|
||||
#include "recording.h"
|
||||
#include "string.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
namespace {
|
||||
|
||||
static const char kJsonMimeType[] = "application/json";
|
||||
|
||||
bool ParseOptionalStartAndEnd(const QueryParameters ¶ms,
|
||||
int64_t *start_time_90k, int64_t *end_time_90k) {
|
||||
*start_time_90k = std::numeric_limits<int64_t>::min();
|
||||
*end_time_90k = std::numeric_limits<int64_t>::max();
|
||||
if (!params.ok() ||
|
||||
(params.Get("start_time_90k") != nullptr &&
|
||||
!Atoi64(params.Get("start_time_90k"), 10, start_time_90k)) ||
|
||||
(params.Get("end_time_90k") != nullptr &&
|
||||
!Atoi64(params.Get("end_time_90k"), 10, end_time_90k))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ReplyWithJson(evhttp_request *req, const Json::Value &value) {
|
||||
EvBuffer buf;
|
||||
buf.Add(Json::FastWriter().write(value));
|
||||
evhttp_add_header(evhttp_request_get_output_headers(req), "Content-Type",
|
||||
kJsonMimeType);
|
||||
evhttp_send_reply(req, HTTP_OK, "OK", buf.get());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void WebInterface::Register(evhttp *http) {
|
||||
evhttp_set_gencb(http, &WebInterface::DispatchHttpRequest, this);
|
||||
}
|
||||
|
||||
void WebInterface::DispatchHttpRequest(evhttp_request *req, void *arg) {
|
||||
static const RE2 kCameraUri("/cameras/([^/]+)/");
|
||||
static const RE2 kCameraRecordingsUri("/cameras/([^/]+)/recordings");
|
||||
static const RE2 kCameraViewUri("/cameras/([^/]+)/view.mp4");
|
||||
|
||||
re2::StringPiece accept =
|
||||
evhttp_find_header(evhttp_request_get_input_headers(req), "Accept");
|
||||
bool json = accept == kJsonMimeType;
|
||||
|
||||
auto *this_ = reinterpret_cast<WebInterface *>(arg);
|
||||
const evhttp_uri *uri = evhttp_request_get_evhttp_uri(req);
|
||||
re2::StringPiece path = evhttp_uri_get_path(uri);
|
||||
re2::StringPiece camera_uuid_str;
|
||||
Uuid camera_uuid;
|
||||
if (path == "/" || path == "/cameras/") {
|
||||
if (json) {
|
||||
this_->HandleJsonCameraList(req);
|
||||
} else {
|
||||
this_->HandleHtmlCameraList(req);
|
||||
}
|
||||
} else if (RE2::FullMatch(path, kCameraUri, &camera_uuid_str) &&
|
||||
camera_uuid.ParseText(camera_uuid_str)) {
|
||||
if (json) {
|
||||
this_->HandleJsonCameraDetail(req, camera_uuid);
|
||||
} else {
|
||||
this_->HandleHtmlCameraDetail(req, camera_uuid);
|
||||
}
|
||||
} else if (RE2::FullMatch(path, kCameraRecordingsUri, &camera_uuid_str) &&
|
||||
camera_uuid.ParseText(camera_uuid_str)) {
|
||||
// The HTML version includes this in the top-level camera view.
|
||||
// So only support JSON at this URI.
|
||||
this_->HandleJsonCameraRecordings(req, camera_uuid);
|
||||
} else if (RE2::FullMatch(path, kCameraViewUri, &camera_uuid_str) &&
|
||||
camera_uuid.ParseText(camera_uuid_str)) {
|
||||
this_->HandleMp4View(req, camera_uuid);
|
||||
} else {
|
||||
evhttp_send_error(req, HTTP_NOTFOUND, "path not understood");
|
||||
}
|
||||
}
|
||||
|
||||
void WebInterface::HandleHtmlCameraList(evhttp_request *req) {
|
||||
EvBuffer buf;
|
||||
buf.Add(
|
||||
"<!DOCTYPE html>\n"
|
||||
"<html>\n"
|
||||
"<head>\n"
|
||||
"<title>Camera list</title>\n"
|
||||
"<meta http-equiv=\"Content-Language\" content=\"en\">\n"
|
||||
"<style type=\"text/css\">\n"
|
||||
".header { background-color: #ddd; }\n"
|
||||
"td { padding-right: 3em; }\n"
|
||||
"</style>\n"
|
||||
"</head>\n"
|
||||
"<body>\n"
|
||||
"<table>\n");
|
||||
auto row_cb = [&](const ListCamerasRow &row) {
|
||||
auto seconds = row.total_duration_90k / kTimeUnitsPerSecond;
|
||||
std::string min_start_time_90k =
|
||||
row.min_start_time_90k == -1 ? std::string("n/a")
|
||||
: PrettyTimestamp(row.min_start_time_90k);
|
||||
std::string max_end_time_90k = row.max_end_time_90k == -1
|
||||
? std::string("n/a")
|
||||
: PrettyTimestamp(row.max_end_time_90k);
|
||||
buf.AddPrintf(
|
||||
"<tr class=header><td colspan=2><a href=\"/cameras/%s/\">%s</a>"
|
||||
"</td></tr>\n"
|
||||
"<tr><td>description</td><td>%s</td></tr>\n"
|
||||
"<tr><td>space</td><td>%s / %s (%.1f%%)</td></tr>\n"
|
||||
"<tr><td>uuid</td><td>%s</td></tr>\n"
|
||||
"<tr><td>oldest recording</td><td>%s</td></tr>\n"
|
||||
"<tr><td>newest recording</td><td>%s</td></tr>\n"
|
||||
"<tr><td>total duration</td><td>%s</td></tr>\n",
|
||||
row.uuid.UnparseText().c_str(), EscapeHtml(row.short_name).c_str(),
|
||||
EscapeHtml(row.description).c_str(),
|
||||
EscapeHtml(HumanizeWithBinaryPrefix(row.total_sample_file_bytes, "B"))
|
||||
.c_str(),
|
||||
EscapeHtml(HumanizeWithBinaryPrefix(row.retain_bytes, "B")).c_str(),
|
||||
100.f * row.total_sample_file_bytes / row.retain_bytes,
|
||||
EscapeHtml(row.uuid.UnparseText()).c_str(),
|
||||
EscapeHtml(min_start_time_90k).c_str(),
|
||||
EscapeHtml(max_end_time_90k).c_str(),
|
||||
EscapeHtml(HumanizeDuration(seconds)).c_str());
|
||||
return IterationControl::kContinue;
|
||||
};
|
||||
env_->mdb->ListCameras(row_cb);
|
||||
buf.Add(
|
||||
"</table>\n"
|
||||
"</body>\n"
|
||||
"<html>\n");
|
||||
evhttp_send_reply(req, HTTP_OK, "OK", buf.get());
|
||||
}
|
||||
|
||||
void WebInterface::HandleJsonCameraList(evhttp_request *req) {
|
||||
Json::Value cameras(Json::arrayValue);
|
||||
auto row_cb = [&](const ListCamerasRow &row) {
|
||||
Json::Value camera(Json::objectValue);
|
||||
camera["uuid"] = row.uuid.UnparseText();
|
||||
camera["short_name"] = row.short_name;
|
||||
camera["description"] = row.description;
|
||||
camera["retain_bytes"] = static_cast<Json::Int64>(row.retain_bytes);
|
||||
camera["total_duration_90k"] =
|
||||
static_cast<Json::Int64>(row.total_duration_90k);
|
||||
camera["total_sample_file_bytes"] =
|
||||
static_cast<Json::Int64>(row.total_sample_file_bytes);
|
||||
if (row.min_start_time_90k != -1) {
|
||||
camera["min_start_time_90k"] =
|
||||
static_cast<Json::Int64>(row.min_start_time_90k);
|
||||
}
|
||||
if (row.max_end_time_90k != -1) {
|
||||
camera["max_end_time_90k"] =
|
||||
static_cast<Json::Int64>(row.max_end_time_90k);
|
||||
}
|
||||
cameras.append(camera);
|
||||
return IterationControl::kContinue;
|
||||
};
|
||||
env_->mdb->ListCameras(row_cb);
|
||||
ReplyWithJson(req, cameras);
|
||||
}
|
||||
|
||||
bool WebInterface::ListAggregatedCameraRecordings(
|
||||
Uuid camera_uuid, int64_t start_time_90k, int64_t end_time_90k,
|
||||
int64_t forced_split_duration_90k,
|
||||
const std::function<void(const ListCameraRecordingsRow &)> &fn,
|
||||
std::string *error_message) {
|
||||
ListCameraRecordingsRow aggregated;
|
||||
auto handle_sql_row = [&](const ListCameraRecordingsRow &row) {
|
||||
auto new_duration_90k = aggregated.end_time_90k - row.start_time_90k;
|
||||
if (row.video_sample_entry_sha1 == aggregated.video_sample_entry_sha1 &&
|
||||
row.end_time_90k == aggregated.start_time_90k &&
|
||||
new_duration_90k < forced_split_duration_90k) {
|
||||
// Append to current .mp4.
|
||||
aggregated.start_time_90k = row.start_time_90k;
|
||||
aggregated.video_samples += row.video_samples;
|
||||
aggregated.sample_file_bytes += row.sample_file_bytes;
|
||||
} else {
|
||||
// Start a new .mp4.
|
||||
if (aggregated.start_time_90k != -1) { fn(aggregated); }
|
||||
aggregated = row;
|
||||
}
|
||||
return IterationControl::kContinue;
|
||||
};
|
||||
if (!env_->mdb->ListCameraRecordings(camera_uuid, start_time_90k,
|
||||
end_time_90k, handle_sql_row,
|
||||
error_message)) {
|
||||
return false;
|
||||
}
|
||||
if (aggregated.start_time_90k != -1) { fn(aggregated); }
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebInterface::HandleHtmlCameraDetail(evhttp_request *req,
|
||||
Uuid camera_uuid) {
|
||||
GetCameraRow camera_row;
|
||||
if (!env_->mdb->GetCamera(camera_uuid, &camera_row)) {
|
||||
return evhttp_send_error(req, HTTP_NOTFOUND, "no such camera");
|
||||
}
|
||||
|
||||
int64_t start_time_90k;
|
||||
int64_t end_time_90k;
|
||||
QueryParameters params(evhttp_request_get_uri(req));
|
||||
if (!ParseOptionalStartAndEnd(params, &start_time_90k, &end_time_90k)) {
|
||||
return evhttp_send_error(req, HTTP_BADREQUEST, "bad query parameters");
|
||||
}
|
||||
|
||||
EvBuffer buf;
|
||||
buf.AddPrintf(
|
||||
"<!DOCTYPE html>\n"
|
||||
"<html>\n"
|
||||
"<head>\n"
|
||||
"<title>%s recordings</title>\n"
|
||||
"<meta http-equiv=\"Content-Language\" content=\"en\">\n"
|
||||
"<style type=\"text/css\">\n"
|
||||
"tr:not(:first-child):hover { background-color: #ddd; }\n"
|
||||
"th, td { padding: 0.5ex 1.5em; text-align: right; }\n"
|
||||
"</style>\n"
|
||||
"</head>\n"
|
||||
"<body>\n"
|
||||
"<h1>%s</h1>\n"
|
||||
"<p>%s</p>\n"
|
||||
"<table>\n"
|
||||
"<tr><th>start</th><th>end</th><th>resolution</th>"
|
||||
"<th>fps</th><th>size</th><th>bitrate</th>"
|
||||
"</tr>\n",
|
||||
EscapeHtml(camera_row.short_name).c_str(),
|
||||
EscapeHtml(camera_row.short_name).c_str(),
|
||||
EscapeHtml(camera_row.description).c_str());
|
||||
|
||||
// Rather than listing each 60-second recording, generate a HTML row for
|
||||
// aggregated .mp4 files of up to kForceSplitDuration90k each, provided
|
||||
// there is no gap or change in video parameters between recordings.
|
||||
static const int64_t kForceSplitDuration90k = 60 * 60 * kTimeUnitsPerSecond;
|
||||
auto finish_html_row = [&](const ListCameraRecordingsRow &aggregated) {
|
||||
auto seconds = static_cast<float>(aggregated.end_time_90k -
|
||||
aggregated.start_time_90k) /
|
||||
kTimeUnitsPerSecond;
|
||||
buf.AddPrintf(
|
||||
"<tr><td><a href=\"view.mp4?start_time_90k=%" PRId64
|
||||
"&end_time_90k=%" PRId64
|
||||
"\">%s</a></td><td>%s</td><td>%dx%d</td>"
|
||||
"<td>%.0f</td><td>%s</td><td>%s</td></tr>\n",
|
||||
aggregated.start_time_90k, aggregated.end_time_90k,
|
||||
PrettyTimestamp(aggregated.start_time_90k).c_str(),
|
||||
PrettyTimestamp(aggregated.end_time_90k).c_str(),
|
||||
static_cast<int>(aggregated.width), static_cast<int>(aggregated.height),
|
||||
static_cast<float>(aggregated.video_samples) / seconds,
|
||||
HumanizeWithBinaryPrefix(aggregated.sample_file_bytes, "B").c_str(),
|
||||
HumanizeWithDecimalPrefix(
|
||||
static_cast<float>(aggregated.sample_file_bytes) * 8 / seconds,
|
||||
"bps")
|
||||
.c_str());
|
||||
};
|
||||
std::string error_message;
|
||||
if (!ListAggregatedCameraRecordings(camera_uuid, start_time_90k,
|
||||
end_time_90k, kForceSplitDuration90k,
|
||||
finish_html_row, &error_message)) {
|
||||
return evhttp_send_error(
|
||||
req, HTTP_INTERNAL,
|
||||
StrCat("sqlite query failed: ", EscapeHtml(error_message)).c_str());
|
||||
}
|
||||
buf.Add(
|
||||
"</table>\n"
|
||||
"</html>\n");
|
||||
evhttp_send_reply(req, HTTP_OK, "OK", buf.get());
|
||||
}
|
||||
|
||||
void WebInterface::HandleJsonCameraDetail(evhttp_request *req,
|
||||
Uuid camera_uuid) {
|
||||
GetCameraRow camera_row;
|
||||
if (!env_->mdb->GetCamera(camera_uuid, &camera_row)) {
|
||||
return evhttp_send_error(req, HTTP_NOTFOUND, "no such camera");
|
||||
}
|
||||
|
||||
Json::Value camera(Json::objectValue);
|
||||
camera["short_name"] = camera_row.short_name;
|
||||
camera["description"] = camera_row.description;
|
||||
camera["retain_bytes"] = static_cast<Json::Int64>(camera_row.retain_bytes);
|
||||
camera["total_duration_90k"] =
|
||||
static_cast<Json::Int64>(camera_row.total_duration_90k);
|
||||
camera["total_sample_file_bytes"] =
|
||||
static_cast<Json::Int64>(camera_row.total_sample_file_bytes);
|
||||
if (camera_row.min_start_time_90k != std::numeric_limits<int64_t>::max()) {
|
||||
camera["min_start_time_90k"] =
|
||||
static_cast<Json::Int64>(camera_row.min_start_time_90k);
|
||||
}
|
||||
if (camera_row.max_end_time_90k != std::numeric_limits<int64_t>::min()) {
|
||||
camera["max_end_time_90k"] =
|
||||
static_cast<Json::Int64>(camera_row.max_end_time_90k);
|
||||
}
|
||||
|
||||
Json::Value days(Json::objectValue);
|
||||
std::string error_message;
|
||||
for (const auto &day : camera_row.days) {
|
||||
int64_t start_time_90k;
|
||||
int64_t end_time_90k;
|
||||
if (!GetDayBounds(day.first, &start_time_90k, &end_time_90k,
|
||||
&error_message)) {
|
||||
return evhttp_send_error(
|
||||
req, HTTP_INTERNAL,
|
||||
StrCat("internal error: ", EscapeHtml(error_message)).c_str());
|
||||
}
|
||||
|
||||
Json::Value day_val(Json::objectValue);
|
||||
day_val["start_time_90k"] = static_cast<Json::Int64>(start_time_90k);
|
||||
day_val["end_time_90k"] = static_cast<Json::Int64>(end_time_90k);
|
||||
day_val["total_duration_90k"] = static_cast<Json::Int64>(day.second);
|
||||
days[day.first] = day_val;
|
||||
}
|
||||
camera["days"] = days;
|
||||
ReplyWithJson(req, camera);
|
||||
}
|
||||
|
||||
void WebInterface::HandleJsonCameraRecordings(evhttp_request *req,
|
||||
Uuid camera_uuid) {
|
||||
int64_t start_time_90k;
|
||||
int64_t end_time_90k;
|
||||
QueryParameters params(evhttp_request_get_uri(req));
|
||||
if (!ParseOptionalStartAndEnd(params, &start_time_90k, &end_time_90k)) {
|
||||
return evhttp_send_error(req, HTTP_BADREQUEST, "bad query parameters");
|
||||
}
|
||||
|
||||
GetCameraRow camera_row;
|
||||
if (!env_->mdb->GetCamera(camera_uuid, &camera_row)) {
|
||||
return evhttp_send_error(req, HTTP_NOTFOUND, "no such camera");
|
||||
}
|
||||
|
||||
// TODO(slamb): paging support.
|
||||
|
||||
Json::Value recordings(Json::arrayValue);
|
||||
auto handle_row = [&](const ListCameraRecordingsRow &row) {
|
||||
Json::Value recording(Json::objectValue);
|
||||
recording["end_time_90k"] = static_cast<Json::Int64>(row.end_time_90k);
|
||||
recording["start_time_90k"] = static_cast<Json::Int64>(row.start_time_90k);
|
||||
recording["video_samples"] = static_cast<Json::Int64>(row.video_samples);
|
||||
recording["sample_file_bytes"] =
|
||||
static_cast<Json::Int64>(row.sample_file_bytes);
|
||||
recording["video_sample_entry_sha1"] = ToHex(row.video_sample_entry_sha1);
|
||||
recording["video_sample_entry_width"] = row.width;
|
||||
recording["video_sample_entry_height"] = row.height;
|
||||
recordings.append(recording);
|
||||
return IterationControl::kContinue;
|
||||
};
|
||||
std::string error_message;
|
||||
const auto kForceSplitDuration90k = std::numeric_limits<int64_t>::max();
|
||||
if (!ListAggregatedCameraRecordings(camera_uuid, start_time_90k,
|
||||
end_time_90k, kForceSplitDuration90k,
|
||||
handle_row, &error_message)) {
|
||||
return evhttp_send_error(
|
||||
req, HTTP_INTERNAL,
|
||||
StrCat("sqlite query failed: ", EscapeHtml(error_message)).c_str());
|
||||
}
|
||||
|
||||
Json::Value response(Json::objectValue);
|
||||
response["recordings"] = recordings;
|
||||
ReplyWithJson(req, response);
|
||||
}
|
||||
|
||||
void WebInterface::HandleMp4View(evhttp_request *req, Uuid camera_uuid) {
|
||||
int64_t start_time_90k;
|
||||
int64_t end_time_90k;
|
||||
QueryParameters params(evhttp_request_get_uri(req));
|
||||
if (!params.ok() ||
|
||||
!Atoi64(params.Get("start_time_90k"), 10, &start_time_90k) ||
|
||||
!Atoi64(params.Get("end_time_90k"), 10, &end_time_90k) ||
|
||||
start_time_90k < 0 || start_time_90k >= end_time_90k) {
|
||||
return evhttp_send_error(req, HTTP_BADREQUEST, "bad query parameters");
|
||||
}
|
||||
bool include_ts = re2::StringPiece(params.Get("ts")) == "true";
|
||||
|
||||
std::string error_message;
|
||||
auto file = BuildMp4(camera_uuid, start_time_90k, end_time_90k, include_ts,
|
||||
&error_message);
|
||||
if (file == nullptr) {
|
||||
// TODO: more nuanced HTTP status codes.
|
||||
LOG(WARNING) << "BuildMp4 failed: " << error_message;
|
||||
return evhttp_send_error(req, HTTP_INTERNAL,
|
||||
EscapeHtml(error_message).c_str());
|
||||
}
|
||||
|
||||
return HttpServe(file, req);
|
||||
}
|
||||
|
||||
std::shared_ptr<VirtualFile> WebInterface::BuildMp4(
|
||||
Uuid camera_uuid, int64_t start_time_90k, int64_t end_time_90k,
|
||||
bool include_ts, std::string *error_message) {
|
||||
LOG(INFO) << "Building mp4 for camera: " << camera_uuid.UnparseText()
|
||||
<< ", start_time_90k: " << start_time_90k
|
||||
<< ", end_time_90k: " << end_time_90k;
|
||||
|
||||
Mp4FileBuilder builder(env_->sample_file_dir);
|
||||
int64_t next_row_start_time_90k = start_time_90k;
|
||||
int64_t rows = 0;
|
||||
bool ok = true;
|
||||
auto row_cb = [&](Recording &recording,
|
||||
const VideoSampleEntry &sample_entry) {
|
||||
if (rows == 0 && recording.start_time_90k > next_row_start_time_90k) {
|
||||
*error_message = StrCat(
|
||||
"recording starts late: ", PrettyTimestamp(recording.start_time_90k),
|
||||
" (", recording.start_time_90k, ") rather than requested: ",
|
||||
PrettyTimestamp(start_time_90k), " (", start_time_90k, ")");
|
||||
ok = false;
|
||||
return IterationControl::kBreak;
|
||||
} else if (rows > 0 &&
|
||||
recording.start_time_90k != next_row_start_time_90k) {
|
||||
*error_message = StrCat("gap/overlap in recording: ",
|
||||
PrettyTimestamp(next_row_start_time_90k), " (",
|
||||
next_row_start_time_90k, ") to: ",
|
||||
PrettyTimestamp(recording.start_time_90k), " (",
|
||||
recording.start_time_90k, ") before row ", rows);
|
||||
ok = false;
|
||||
return IterationControl::kBreak;
|
||||
}
|
||||
|
||||
next_row_start_time_90k = recording.end_time_90k;
|
||||
|
||||
if (rows > 0 && recording.video_sample_entry_id != sample_entry.id) {
|
||||
*error_message =
|
||||
StrCat("inconsistent video sample entries: this recording has id ",
|
||||
recording.video_sample_entry_id, " previous had ",
|
||||
sample_entry.id, " (sha1 ", ToHex(sample_entry.sha1), ")");
|
||||
ok = false;
|
||||
return IterationControl::kBreak;
|
||||
} else if (rows == 0) {
|
||||
builder.SetSampleEntry(sample_entry);
|
||||
}
|
||||
|
||||
int32_t rel_start_90k = 0;
|
||||
int32_t rel_end_90k = std::numeric_limits<int32_t>::max();
|
||||
if (recording.start_time_90k < start_time_90k) {
|
||||
rel_start_90k = start_time_90k - recording.start_time_90k;
|
||||
}
|
||||
if (recording.end_time_90k > end_time_90k) {
|
||||
rel_end_90k = end_time_90k - recording.start_time_90k;
|
||||
}
|
||||
builder.Append(std::move(recording), rel_start_90k, rel_end_90k);
|
||||
++rows;
|
||||
return IterationControl::kContinue;
|
||||
};
|
||||
if (!env_->mdb->ListMp4Recordings(camera_uuid, start_time_90k, end_time_90k,
|
||||
row_cb, error_message) ||
|
||||
!ok) {
|
||||
return false;
|
||||
}
|
||||
if (rows == 0) {
|
||||
*error_message = StrCat("no recordings in range");
|
||||
return false;
|
||||
}
|
||||
if (next_row_start_time_90k < end_time_90k) {
|
||||
*error_message = StrCat("recording ends early: ",
|
||||
PrettyTimestamp(next_row_start_time_90k), " (",
|
||||
next_row_start_time_90k, "), not requested: ",
|
||||
PrettyTimestamp(end_time_90k), " (", end_time_90k,
|
||||
") after ", rows, " rows");
|
||||
return false;
|
||||
}
|
||||
|
||||
builder.include_timestamp_subtitle_track(include_ts);
|
||||
|
||||
VLOG(1) << "...(3/4) building VirtualFile from " << rows << " recordings.";
|
||||
auto file = builder.Build(error_message);
|
||||
if (file == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
VLOG(1) << "...(4/4) success, " << file->size() << " bytes, etag "
|
||||
<< file->etag();
|
||||
return file;
|
||||
}
|
||||
|
||||
} // namespace moonfire_nvr
|
92
src/web.h
92
src/web.h
@ -1,92 +0,0 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// web.h: web (HTTP/HTML/JSON) interface to the SQLite-based recording schema.
|
||||
// See design/api.md for a description of the JSON API.
|
||||
//
|
||||
// In the future, the interface will be reworked for tighter integration with
|
||||
// the recording system to support more features:
|
||||
//
|
||||
// * including the recording currently being written in the web interface
|
||||
// * subscribing to changes
|
||||
// * reconfiguring the recording system, such as
|
||||
// adding/removing/starting/stopping/editing cameras
|
||||
// * showing thumbnails of the latest key frame from each camera
|
||||
// * ...
|
||||
|
||||
#ifndef MOONFIRE_NVR_WEB_H
|
||||
#define MOONFIRE_NVR_WEB_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <event2/http.h>
|
||||
|
||||
#include "moonfire-db.h"
|
||||
#include "moonfire-nvr.h"
|
||||
#include "http.h"
|
||||
|
||||
namespace moonfire_nvr {
|
||||
|
||||
class WebInterface {
|
||||
public:
|
||||
explicit WebInterface(Environment *env) : env_(env) {}
|
||||
WebInterface(const WebInterface &) = delete;
|
||||
void operator=(const WebInterface &) = delete;
|
||||
|
||||
void Register(evhttp *http);
|
||||
|
||||
private:
|
||||
static void DispatchHttpRequest(evhttp_request *req, void *arg);
|
||||
|
||||
void HandleHtmlCameraList(evhttp_request *req);
|
||||
void HandleJsonCameraList(evhttp_request *req);
|
||||
void HandleHtmlCameraDetail(evhttp_request *req, Uuid camera_uuid);
|
||||
void HandleJsonCameraDetail(evhttp_request *req, Uuid camera_uuid);
|
||||
void HandleJsonCameraRecordings(evhttp_request *req, Uuid camera_uuid);
|
||||
void HandleMp4View(evhttp_request *req, Uuid camera_uuid);
|
||||
|
||||
bool ListAggregatedCameraRecordings(
|
||||
Uuid camera_uuid, int64_t start_time_90k, int64_t end_time_90k,
|
||||
int64_t forced_split_duration_90k,
|
||||
const std::function<void (const ListCameraRecordingsRow &)> &fn,
|
||||
std::string *error_message);
|
||||
|
||||
// TODO: more nuanced error code for HTTP.
|
||||
std::shared_ptr<VirtualFile> BuildMp4(Uuid camera_uuid,
|
||||
int64_t start_time_90k,
|
||||
int64_t end_time_90k, bool include_ts,
|
||||
std::string *error_message);
|
||||
|
||||
Environment *const env_;
|
||||
};
|
||||
|
||||
} // namespace moonfire_nvr
|
||||
|
||||
#endif // MOONFIRE_NVR_WEB_H
|
449
src/web.rs
Normal file
449
src/web.rs
Normal file
@ -0,0 +1,449 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
extern crate hyper;
|
||||
|
||||
use core::borrow::Borrow;
|
||||
use core::str::FromStr;
|
||||
use db;
|
||||
use dir::SampleFileDir;
|
||||
use error::{Error, Result};
|
||||
use hyper::{header,server,status};
|
||||
use hyper::uri::RequestUri;
|
||||
use mime;
|
||||
use mp4;
|
||||
use recording;
|
||||
use resource;
|
||||
use serde_json;
|
||||
use serde::ser::Serializer;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::io::Write;
|
||||
use std::result;
|
||||
use std::sync::{Arc,MutexGuard};
|
||||
use time;
|
||||
use url::form_urlencoded;
|
||||
use uuid::Uuid;
|
||||
|
||||
const BINARY_PREFIXES: &'static [&'static str] = &[" ", " Ki", " Mi", " Gi", " Ti", " Pi", " Ei"];
|
||||
const DECIMAL_PREFIXES: &'static [&'static str] =&[" ", " k", " M", " G", " T", " P", " E"];
|
||||
|
||||
lazy_static! {
|
||||
static ref JSON: mime::Mime = mime!(Application/Json);
|
||||
static ref HTML: mime::Mime = mime!(Text/Html);
|
||||
}
|
||||
|
||||
enum Path {
|
||||
CamerasList, // "/" or "/cameras/"
|
||||
Camera(Uuid), // "/cameras/<uuid>/"
|
||||
CameraRecordings(Uuid), // "/cameras/<uuid>/recordings"
|
||||
CameraViewMp4(Uuid), // "/cameras/<uuid>/view.mp4"
|
||||
NotFound,
|
||||
}
|
||||
|
||||
fn get_path_and_query(uri: &RequestUri) -> (&str, &str) {
|
||||
match *uri {
|
||||
RequestUri::AbsolutePath(ref both) => match both.find('?') {
|
||||
Some(split) => (&both[..split], &both[split+1..]),
|
||||
None => (both, ""),
|
||||
},
|
||||
RequestUri::AbsoluteUri(ref u) => (u.path(), u.query().unwrap_or("")),
|
||||
_ => ("", ""),
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_path(path: &str) -> Path {
|
||||
if path == "/" {
|
||||
return Path::CamerasList;
|
||||
}
|
||||
if !path.starts_with("/cameras/") {
|
||||
return Path::NotFound;
|
||||
}
|
||||
let path = &path["/cameras/".len()..];
|
||||
if path == "" {
|
||||
return Path::CamerasList;
|
||||
}
|
||||
let slash = match path.find('/') {
|
||||
None => { return Path::NotFound; },
|
||||
Some(s) => s,
|
||||
};
|
||||
let (uuid, path) = path.split_at(slash);
|
||||
|
||||
// TODO(slamb): require uuid to be in canonical format.
|
||||
let uuid = match Uuid::parse_str(uuid) {
|
||||
Ok(u) => u,
|
||||
Err(_) => { return Path::NotFound },
|
||||
};
|
||||
match path {
|
||||
"/" => Path::Camera(uuid),
|
||||
"/recordings" => Path::CameraRecordings(uuid),
|
||||
"/view.mp4" => Path::CameraViewMp4(uuid),
|
||||
_ => Path::NotFound,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_json(req: &server::Request) -> bool {
|
||||
if let Some(accept) = req.headers.get::<header::Accept>() {
|
||||
return accept.len() == 1 && accept[0].item == *JSON &&
|
||||
accept[0].quality == header::Quality(1000);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub struct HtmlEscaped<'a>(&'a str);
|
||||
|
||||
impl<'a> fmt::Display for HtmlEscaped<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mut last_end = 0;
|
||||
for (start, part) in self.0.match_indices(|c| c == '<' || c == '&') {
|
||||
try!(f.write_str(&self.0[last_end..start]));
|
||||
try!(f.write_str(if part == "<" { "<" } else { "&" }));
|
||||
last_end = start + 1;
|
||||
}
|
||||
f.write_str(&self.0[last_end..])
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Humanized(i64);
|
||||
|
||||
impl Humanized {
|
||||
fn do_fmt(&self, base: f32, prefixes: &[&str], f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mut n = self.0 as f32;
|
||||
let mut i = 0;
|
||||
loop {
|
||||
if n < base || i >= prefixes.len() - 1 {
|
||||
break;
|
||||
}
|
||||
n /= base;
|
||||
i += 1;
|
||||
}
|
||||
write!(f, "{:.1}{}", n, prefixes[i])
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Humanized {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.do_fmt(1000., DECIMAL_PREFIXES, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Binary for Humanized {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.do_fmt(1024., BINARY_PREFIXES, f)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HumanizedTimestamp(Option<recording::Time>);
|
||||
|
||||
impl fmt::Display for HumanizedTimestamp {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self.0 {
|
||||
None => f.write_str("n/a"),
|
||||
Some(t) => {
|
||||
let tm = time::at(time::Timespec{sec: t.unix_seconds(), nsec: 0});
|
||||
write!(f, "{}",
|
||||
try!(tm.strftime("%a, %d %b %Y %H:%M:%S %Z").or_else(|_| Err(fmt::Error))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Handler {
|
||||
db: Arc<db::Database>,
|
||||
dir: Arc<SampleFileDir>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ListCameras<'a> {
|
||||
// Use a custom serializer which presents the map's values as a sequence.
|
||||
#[serde(serialize_with = "ListCameras::serialize_cameras")]
|
||||
cameras: &'a BTreeMap<i32, db::Camera>,
|
||||
}
|
||||
|
||||
impl<'a> ListCameras<'a> {
|
||||
fn serialize_cameras<S>(cameras: &BTreeMap<i32, db::Camera>,
|
||||
serializer: &mut S) -> result::Result<(), S::Error>
|
||||
where S: Serializer {
|
||||
let mut state = try!(serializer.serialize_seq(Some(cameras.len())));
|
||||
for c in cameras.values() {
|
||||
try!(serializer.serialize_seq_elt(&mut state, c));
|
||||
}
|
||||
serializer.serialize_seq_end(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler {
|
||||
pub fn new(db: Arc<db::Database>, dir: Arc<SampleFileDir>) -> Self {
|
||||
Handler{db: db, dir: dir}
|
||||
}
|
||||
|
||||
fn not_found(&self, mut res: server::Response) -> Result<()> {
|
||||
*res.status_mut() = status::StatusCode::NotFound;
|
||||
try!(res.send(b"not found"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list_cameras(&self, req: &server::Request, mut res: server::Response) -> Result<()> {
|
||||
let json = is_json(req);
|
||||
let buf = {
|
||||
let db = self.db.lock();
|
||||
if json {
|
||||
try!(serde_json::to_vec(&ListCameras{cameras: db.cameras_by_id()}))
|
||||
} else {
|
||||
try!(self.list_cameras_html(db))
|
||||
}
|
||||
};
|
||||
res.headers_mut().set(header::ContentType(if json { JSON.clone() } else { HTML.clone() }));
|
||||
try!(res.send(&buf));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list_cameras_html(&self, db: MutexGuard<db::LockedDatabase>) -> Result<Vec<u8>> {
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(b"\
|
||||
<!DOCTYPE html>\n\
|
||||
<html>\n\
|
||||
<head>\n\
|
||||
<title>Camera list</title>\n\
|
||||
<meta http-equiv=\"Content-Language\" content=\"en\">\n\
|
||||
<style type=\"text/css\">\n\
|
||||
.header { background-color: #ddd; }\n\
|
||||
td { padding-right: 3em; }\n\
|
||||
</style>\n\
|
||||
</head>\n\
|
||||
<body>\n\
|
||||
<table>\n");
|
||||
for row in db.cameras_by_id().values() {
|
||||
try!(write!(&mut buf, "\
|
||||
<tr class=header><td colspan=2><a href=\"/cameras/{}/\">{}</a></td></tr>\n\
|
||||
<tr><td>description</td><td>{}</td></tr>\n\
|
||||
<tr><td>space</td><td>{:b}B / {:b}B ({:.1}%)</td></tr>\n\
|
||||
<tr><td>uuid</td><td>{}</td></tr>\n\
|
||||
<tr><td>oldest recording</td><td>{}</td></tr>\n\
|
||||
<tr><td>newest recording</td><td>{}</td></tr>\n\
|
||||
<tr><td>total duration</td><td>{}</td></tr>\n",
|
||||
row.uuid, HtmlEscaped(&row.short_name), HtmlEscaped(&row.description),
|
||||
Humanized(row.sample_file_bytes), Humanized(row.retain_bytes),
|
||||
100. * row.sample_file_bytes as f32 / row.retain_bytes as f32,
|
||||
row.uuid, HumanizedTimestamp(row.range.as_ref().map(|r| r.start)),
|
||||
HumanizedTimestamp(row.range.as_ref().map(|r| r.end)),
|
||||
row.duration));
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn camera(&self, uuid: Uuid, req: &server::Request, mut res: server::Response) -> Result<()> {
|
||||
let json = is_json(req);
|
||||
let buf = {
|
||||
let db = self.db.lock();
|
||||
if json {
|
||||
let camera = try!(db.get_camera(uuid)
|
||||
.ok_or_else(|| Error::new("no such camera".to_owned())));
|
||||
try!(serde_json::to_vec(&camera))
|
||||
} else {
|
||||
try!(self.camera_html(db, uuid))
|
||||
}
|
||||
};
|
||||
res.headers_mut().set(header::ContentType(if json { JSON.clone() } else { HTML.clone() }));
|
||||
try!(res.send(&buf));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn camera_html(&self, db: MutexGuard<db::LockedDatabase>, uuid: Uuid) -> Result<Vec<u8>> {
|
||||
let camera = try!(db.get_camera(uuid)
|
||||
.ok_or_else(|| Error::new("no such camera".to_owned())));
|
||||
let mut buf = Vec::new();
|
||||
try!(write!(&mut buf, "\
|
||||
<!DOCTYPE html>\n\
|
||||
<html>\n\
|
||||
<head>\n\
|
||||
<title>{0} recordings</title>\n\
|
||||
<meta http-equiv=\"Content-Language\" content=\"en\">\n\
|
||||
<style type=\"text/css\">\n\
|
||||
tr:not(:first-child):hover {{ background-color: #ddd; }}\n\
|
||||
th, td {{ padding: 0.5ex 1.5em; text-align: right; }}\n\
|
||||
</style>\n\
|
||||
</head>\n\
|
||||
<body>\n\
|
||||
<h1>{0}</h1>\n\
|
||||
<p>{1}</p>\n\
|
||||
<table>\n\
|
||||
<tr><th>start</th><th>end</th><th>resolution</th>\
|
||||
<th>fps</th><th>size</th><th>bitrate</th>\
|
||||
</tr>\n",
|
||||
HtmlEscaped(&camera.short_name), HtmlEscaped(&camera.description)));
|
||||
let r = recording::Time(i64::min_value()) .. recording::Time(i64::max_value());
|
||||
|
||||
// Rather than listing each 60-second recording, generate a HTML row for aggregated .mp4
|
||||
// files of up to FORCE_SPLIT_DURATION each, provided there is no gap or change in video
|
||||
// parameters between recordings.
|
||||
static FORCE_SPLIT_DURATION: recording::Duration =
|
||||
recording::Duration(60 * 60 * recording::TIME_UNITS_PER_SEC);
|
||||
try!(db.list_aggregated_recordings(camera.id, &r, FORCE_SPLIT_DURATION, |row| {
|
||||
let seconds = (row.range.end.0 - row.range.start.0) / recording::TIME_UNITS_PER_SEC;
|
||||
try!(write!(&mut buf, "\
|
||||
<tr><td><a href=\"view.mp4?start_time_90k={}&end_time_90k={}\">{}</a></td>\
|
||||
<td>{}</td><td>{}x{}</td><td>{:.0}</td><td>{:b}B</td><td>{}bps</td></tr>\n",
|
||||
row.range.start.0, row.range.end.0,
|
||||
HumanizedTimestamp(Some(row.range.start)),
|
||||
HumanizedTimestamp(Some(row.range.end)), row.video_sample_entry.width,
|
||||
row.video_sample_entry.height,
|
||||
if seconds == 0 { 0. } else { row.video_samples as f32 / seconds as f32 },
|
||||
Humanized(row.sample_file_bytes),
|
||||
Humanized(if seconds == 0 { 0 } else { row.sample_file_bytes * 8 / seconds })));
|
||||
Ok(())
|
||||
}));
|
||||
buf.extend_from_slice(b"</table>\n</html>\n");
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn camera_recordings(&self, _uuid: Uuid, _req: &server::Request,
|
||||
mut res: server::Response) -> Result<()> {
|
||||
*res.status_mut() = status::StatusCode::NotImplemented;
|
||||
try!(res.send(b"not implemented"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn camera_view_mp4(&self, uuid: Uuid, query: &str, req: &server::Request,
|
||||
res: server::Response) -> Result<()> {
|
||||
let camera_id = {
|
||||
let db = self.db.lock();
|
||||
let camera = try!(db.get_camera(uuid)
|
||||
.ok_or_else(|| Error::new("no such camera".to_owned())));
|
||||
camera.id
|
||||
};
|
||||
let mut start = None;
|
||||
let mut end = None;
|
||||
let mut include_ts = false;
|
||||
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
|
||||
let (key, value) = (key.borrow(), value.borrow());
|
||||
match key {
|
||||
"start_time_90k" => start = Some(recording::Time(try!(i64::from_str(value)))),
|
||||
"end_time_90k" => end = Some(recording::Time(try!(i64::from_str(value)))),
|
||||
"ts" => { include_ts = value == "true"; },
|
||||
_ => {},
|
||||
}
|
||||
};
|
||||
let start = try!(start.ok_or_else(|| Error::new("start_time_90k missing".to_owned())));
|
||||
let end = try!(end.ok_or_else(|| Error::new("end_time_90k missing".to_owned())));
|
||||
let desired_range = start .. end;
|
||||
let mut builder = mp4::Mp4FileBuilder::new();
|
||||
|
||||
// There should be roughly ceil((end - start) / desired_recording_duration) recordings
|
||||
// in the desired timespan if there are no gaps or overlap. Add a couple more to be safe:
|
||||
// one for misalignment of the requested timespan with the rotate offset, another because
|
||||
// rotation only happens at key frames.
|
||||
let ceil_durations = ((end - start).0 + recording::DESIRED_RECORDING_DURATION - 1) /
|
||||
recording::DESIRED_RECORDING_DURATION;
|
||||
let est_records = (ceil_durations + 2) as usize;
|
||||
let mut next_start = start;
|
||||
builder.reserve(est_records);
|
||||
{
|
||||
let db = self.db.lock();
|
||||
try!(db.list_recordings(camera_id, &desired_range, |r| {
|
||||
if builder.len() == 0 && r.start > next_start {
|
||||
return Err(Error::new(format!("recording started late ({} vs requested {})",
|
||||
r.start, start)));
|
||||
} else if builder.len() != 0 && r.start != next_start {
|
||||
return Err(Error::new(format!("gap/overlap in recording: {} to {} after row {}",
|
||||
next_start, r.start, builder.len())));
|
||||
}
|
||||
next_start = r.start + recording::Duration(r.duration_90k as i64);
|
||||
// TODO: check for inconsistent video sample entries.
|
||||
|
||||
let rel_start = if r.start < start {
|
||||
(start - r.start).0 as i32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let rel_end = if r.start + recording::Duration(r.duration_90k as i64) > end {
|
||||
(end - r.start).0 as i32
|
||||
} else {
|
||||
r.duration_90k
|
||||
};
|
||||
builder.append(r, rel_start .. rel_end);
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
if next_start < end {
|
||||
return Err(Error::new(format!(
|
||||
"recording ends early: {}, not requested: {} after {} rows.",
|
||||
next_start, end, builder.len())))
|
||||
}
|
||||
if builder.len() > est_records {
|
||||
warn!("Estimated {} records for time [{}, {}); actually were {}",
|
||||
est_records, start, end, builder.len());
|
||||
} else {
|
||||
debug!("Estimated {} records for time [{}, {}); actually were {}",
|
||||
est_records, start, end, builder.len());
|
||||
}
|
||||
builder.include_timestamp_subtitle_track(include_ts);
|
||||
let mp4 = try!(builder.build(self.db.clone(), self.dir.clone()));
|
||||
try!(resource::serve(&mp4, req, res));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl server::Handler for Handler {
|
||||
fn handle(&self, req: server::Request, res: server::Response) {
|
||||
let (path, query) = get_path_and_query(&req.uri);
|
||||
let res = match decode_path(path) {
|
||||
Path::CamerasList => self.list_cameras(&req, res),
|
||||
Path::Camera(uuid) => self.camera(uuid, &req, res),
|
||||
Path::CameraRecordings(uuid) => self.camera_recordings(uuid, &req, res),
|
||||
Path::CameraViewMp4(uuid) => self.camera_view_mp4(uuid, query, &req, res),
|
||||
Path::NotFound => self.not_found(res),
|
||||
};
|
||||
if let Err(ref e) = res {
|
||||
warn!("Error handling request: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{HtmlEscaped, Humanized};
|
||||
|
||||
#[test]
|
||||
fn test_humanize() {
|
||||
assert_eq!("1.0 B", format!("{:b}B", Humanized(1)));
|
||||
assert_eq!("1.0 EiB", format!("{:b}B", Humanized(1i64 << 60)));
|
||||
assert_eq!("1.5 EiB", format!("{:b}B", Humanized((1i64 << 60) + (1i64 << 59))));
|
||||
assert_eq!("8.0 EiB", format!("{:b}B", Humanized(i64::max_value())));
|
||||
assert_eq!("1.0 Mbps", format!("{}bps", Humanized(1_000_000)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_html_escaped() {
|
||||
assert_eq!("", format!("{}", HtmlEscaped("")));
|
||||
assert_eq!("no special chars", format!("{}", HtmlEscaped("no special chars")));
|
||||
assert_eq!("a <tag> & text", format!("{}", HtmlEscaped("a <tag> & text")));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user