use my own ffmpeg crate

This significantly improves safety of the ffmpeg interface. The complex
ABIs aren't accessed directly from Rust. Instead, I have a small C
wrapper which uses the ffmpeg C API and the C headers at compile-time to
determine the proper ABI in the same way any C program using ffmpeg
would, so that the ABI doesn't have to be duplicated in Rust code.
I've tested with ffmpeg 2.x and ffmpeg 3.x; it seems to work properly
with both where before ffmpeg 3.x caused segfaults.

It still depends on ABI compatibility between the compiled and running
versions. C programs need this, too, and normal shared library
versioning practices provide this guarantee. But log both versions on
startup for diagnosing problems with this.

Fixes #7
This commit is contained in:
Scott Lamb 2017-09-20 21:06:06 -07:00
parent 8ff1d0dcb8
commit 857a66f29c
14 changed files with 768 additions and 170 deletions

49
Cargo.lock generated
View File

@ -5,8 +5,6 @@ dependencies = [
"byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"cursive 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "cursive 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"docopt 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "docopt 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"ffmpeg 0.2.0-alpha.2 (git+https://github.com/scottlamb/rust-ffmpeg?branch=2.x)",
"ffmpeg-sys 2.8.9 (registry+https://github.com/rust-lang/crates.io-index)",
"fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
"http-entity 0.0.1 (git+https://github.com/scottlamb/http-entity?branch=hyper-0.11.x)", "http-entity 0.0.1 (git+https://github.com/scottlamb/http-entity?branch=hyper-0.11.x)",
@ -17,6 +15,7 @@ dependencies = [
"lru-cache 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "lru-cache 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"memmap 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "memmap 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
"mime 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "mime 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"moonfire-ffmpeg 0.0.1",
"mylog 0.1.0 (git+https://github.com/scottlamb/mylog)", "mylog 0.1.0 (git+https://github.com/scottlamb/mylog)",
"openssl 0.9.13 (registry+https://github.com/rust-lang/crates.io-index)", "openssl 0.9.13 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
@ -66,11 +65,6 @@ dependencies = [
"byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "bitflags"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "0.9.1" version = "0.9.1"
@ -161,25 +155,6 @@ name = "dtoa"
version = "0.4.1" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "ffmpeg"
version = "0.2.0-alpha.2"
source = "git+https://github.com/scottlamb/rust-ffmpeg?branch=2.x#a67d25cb85369597e0e4820172330e8a3168589f"
dependencies = [
"bitflags 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"ffmpeg-sys 2.8.9 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ffmpeg-sys"
version = "2.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.5" version = "1.0.5"
@ -472,6 +447,16 @@ dependencies = [
"ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "moonfire-ffmpeg"
version = "0.0.1"
dependencies = [
"gcc 0.3.50 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "mylog" name = "mylog"
version = "0.1.0" version = "0.1.0"
@ -582,14 +567,6 @@ name = "num-traits"
version = "0.1.39" version = "0.1.39"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "num_cpus"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.5.1" version = "1.5.1"
@ -1140,7 +1117,6 @@ dependencies = [
"checksum aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "500909c4f87a9e52355b26626d890833e9e1d53ac566db76c36faa984b889699" "checksum aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "500909c4f87a9e52355b26626d890833e9e1d53ac566db76c36faa984b889699"
"checksum antidote 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "34fde25430d87a9388dadbe6e34d7f72a462c8b43ac8d309b42b0a8505d7e2a5" "checksum antidote 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "34fde25430d87a9388dadbe6e34d7f72a462c8b43ac8d309b42b0a8505d7e2a5"
"checksum base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "30e93c03064e7590d0466209155251b90c22e37fab1daf2771582598b5827557" "checksum base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "30e93c03064e7590d0466209155251b90c22e37fab1daf2771582598b5827557"
"checksum bitflags 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "32866f4d103c4e438b1db1158aa1b1a80ee078e5d77a59a2f906fd62a577389c"
"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" "checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5"
"checksum byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c40977b0ee6b9885c9013cd41d9feffdd22deb3bb4dc3a71d901cc7a77de18c8" "checksum byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c40977b0ee6b9885c9013cd41d9feffdd22deb3bb4dc3a71d901cc7a77de18c8"
"checksum bytes 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8b24f16593f445422331a5eed46b72f7f171f910fead4f2ea8f17e727e9c5c14" "checksum bytes 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8b24f16593f445422331a5eed46b72f7f171f910fead4f2ea8f17e727e9c5c14"
@ -1152,8 +1128,6 @@ dependencies = [
"checksum cursive 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b717f6e01158b6d8bc6cad90badc2adfa8ce8a45594125efb90b854abc98ed93" "checksum cursive 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b717f6e01158b6d8bc6cad90badc2adfa8ce8a45594125efb90b854abc98ed93"
"checksum docopt 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "63e408eee8a772c5c61f62353992e3ebf51ef5c832dd04d986b3dc7d48c5b440" "checksum docopt 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "63e408eee8a772c5c61f62353992e3ebf51ef5c832dd04d986b3dc7d48c5b440"
"checksum dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80c8b71fd71146990a9742fc06dcbbde19161a267e0ad4e572c35162f4578c90" "checksum dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80c8b71fd71146990a9742fc06dcbbde19161a267e0ad4e572c35162f4578c90"
"checksum ffmpeg 0.2.0-alpha.2 (git+https://github.com/scottlamb/rust-ffmpeg?branch=2.x)" = "<none>"
"checksum ffmpeg-sys 2.8.9 (registry+https://github.com/rust-lang/crates.io-index)" = "683f0fa41ebe43e7162184d172190e24318af66f7065006a36c2cc877f0e0edd"
"checksum fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6cc484842f1e2884faf56f529f960cc12ad8c71ce96cc7abba0a067c98fee344" "checksum fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6cc484842f1e2884faf56f529f960cc12ad8c71ce96cc7abba0a067c98fee344"
"checksum foreign-types 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3e4056b9bd47f8ac5ba12be771f77a0dae796d1bbaaf5fd0b9c2d38b69b8a29d" "checksum foreign-types 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3e4056b9bd47f8ac5ba12be771f77a0dae796d1bbaaf5fd0b9c2d38b69b8a29d"
"checksum fs2 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9ab76cfd2aaa59b7bf6688ad9ba15bbae64bff97f04ea02144cfd3443e5c2866" "checksum fs2 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9ab76cfd2aaa59b7bf6688ad9ba15bbae64bff97f04ea02144cfd3443e5c2866"
@ -1200,7 +1174,6 @@ dependencies = [
"checksum num-iter 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)" = "f7d1891bd7b936f12349b7d1403761c8a0b85a18b148e9da4429d5d102c1a41e" "checksum num-iter 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)" = "f7d1891bd7b936f12349b7d1403761c8a0b85a18b148e9da4429d5d102c1a41e"
"checksum num-rational 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)" = "33c881e104a26e1accc09449374c095ff2312c8e0c27fab7bbefe16eac7c776d" "checksum num-rational 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)" = "33c881e104a26e1accc09449374c095ff2312c8e0c27fab7bbefe16eac7c776d"
"checksum num-traits 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "1708c0628602a98b52fad936cf3edb9a107af06e52e49fdf0707e884456a6af6" "checksum num-traits 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "1708c0628602a98b52fad936cf3edb9a107af06e52e49fdf0707e884456a6af6"
"checksum num_cpus 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "cee7e88156f3f9e19bdd598f8d6c9db7bf4078f99f8381f43a55b09648d1a6e3"
"checksum num_cpus 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6e416ba127a4bb3ff398cb19546a8d0414f73352efe2857f4060d36f5fe5983a" "checksum num_cpus 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6e416ba127a4bb3ff398cb19546a8d0414f73352efe2857f4060d36f5fe5983a"
"checksum odds 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)" = "c3df9b730298cea3a1c3faa90b7e2f9df3a9c400d0936d6015e6165734eefcba" "checksum odds 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)" = "c3df9b730298cea3a1c3faa90b7e2f9df3a9c400d0936d6015e6165734eefcba"
"checksum openssl 0.9.13 (registry+https://github.com/rust-lang/crates.io-index)" = "b34cd77cf91301fff3123fbd46b065c3b728b17a392835de34c397315dce5586" "checksum openssl 0.9.13 (registry+https://github.com/rust-lang/crates.io-index)" = "b34cd77cf91301fff3123fbd46b065c3b728b17a392835de34c397315dce5586"

View File

@ -26,6 +26,7 @@ log = { version = "0.3", features = ["release_max_level_info"] }
lru-cache = "0.1" lru-cache = "0.1"
memmap = "0.5" memmap = "0.5"
mime = "0.3" mime = "0.3"
moonfire-ffmpeg = { path = "ffmpeg" }
mylog = { git = "https://github.com/scottlamb/mylog" } mylog = { git = "https://github.com/scottlamb/mylog" }
openssl = "0.9" openssl = "0.9"
parking_lot = { version = "0.4", features = [] } parking_lot = { version = "0.4", features = [] }
@ -51,17 +52,6 @@ version = "0.5"
#default-features = false #default-features = false
#features = ["termion"] #features = ["termion"]
[dependencies.ffmpeg]
git = "https://github.com/scottlamb/rust-ffmpeg"
branch = "2.x"
default-features = false
features = ["codec", "format"]
[dependencies.ffmpeg-sys]
version = "2.8"
default-features = false
features = ["avcodec"]
[profile.release] [profile.release]
debug = true debug = true

View File

@ -63,8 +63,6 @@ edge version from the command line via git:
There are no binary packages of Moonfire NVR available yet, so it must be built There are no binary packages of Moonfire NVR available yet, so it must be built
from source. from source.
Moonfire NVR is written in the [Rust Programming Moonfire NVR is written in the [Rust Programming
Language](https://www.rust-lang.org/en-US/). In the long term, I expect this 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 will result in a more secure, full-featured, easy-to-install software. In the
@ -74,18 +72,14 @@ project.
You will need the following C libraries installed: You will need the following C libraries installed:
* [ffmpeg](http://ffmpeg.org/) version 2.x, including `libavutil`, * [ffmpeg](http://ffmpeg.org/) version 2.x or 3.x, including `libavutil`,
`libavcodec` (to inspect H.264 frames), and `libavformat` (to connect to RTSP `libavcodec` (to inspect H.264 frames), and `libavformat` (to connect to RTSP
servers and write `.mp4` files). servers and write `.mp4` files).
Note ffmpeg 3.x isn't supported yet by the Rust `ffmpeg` crate; see Note ffmpeg library versions older than 55.1.101, along with all versions of
[rust-ffmpeg/issues/64](https://github.com/meh/rust-ffmpeg/issues/64). the competing project [libav](http://libav.org), don't not support socket
timeouts for RTSP. For reliable reconnections on error, it's strongly
Additionally, ffmpeg library versions older than 55.1.101, along with recommended to use ffmpeg library versions >= 55.1.101.
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/). * [SQLite3](https://www.sqlite.org/).
@ -102,7 +96,9 @@ all non-Rust dependencies:
libavutil-dev \ libavutil-dev \
libncurses5-dev \ libncurses5-dev \
libncursesw5-dev \ libncursesw5-dev \
libsqlite3-dev libsqlite3-dev \
libssl-dev \
pkgconf
Next, you need Rust 1.15+ and Cargo. The easiest way to install them is by following Next, you need Rust 1.15+ and Cargo. The easiest way to install them is by following
the instructions at [rustup.rs](https://www.rustup.rs/). the instructions at [rustup.rs](https://www.rustup.rs/).
@ -181,7 +177,7 @@ database. If the daemon is running, you will need to stop it temporarily:
You can configure the system through a text-based user interface: You can configure the system through a text-based user interface:
$ sudo -u moonfire-nvr moonfire-nvr config $ sudo -u moonfire-nvr moonfire-nvr config 2>debug-log
In the user interface, add your cameras under the "Edit cameras" dialog. In the user interface, add your cameras under the "Edit cameras" dialog.
There's a "Test" button to verify your settings directly from the dialog. There's a "Test" button to verify your settings directly from the dialog.
@ -251,19 +247,13 @@ and `systemctl` may be of particular interest.
# Troubleshooting # Troubleshooting
While Moonfire NVR is running, logs will be written to stdout. The While Moonfire NVR is running, logs will be written to stderr. The
`MOONFIRE_LOG` environmental variable controls the log level; `MOONFIRE_LOG` environmental variable controls the log level;
`MOONFIRE_LOG=info` is the default. `MOONFIRE_FORMAT` controls the `MOONFIRE_LOG=info` is the default. `MOONFIRE_FORMAT` controls the
logging style; options are `google` (default, like the Google glog package) logging style; options are `google` (default, like the Google glog package)
or `google-systemd` (formatted for the systemd journal). If running through or `google-systemd` (formatted for the systemd journal). If running through
systemd, try `sudo journalctl --unit moonfire-nvr` to view the logs. 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 # <a name="help"></a> Getting help and getting involved
Please email the Please email the

35
ffmpeg/Cargo.lock generated Normal file
View File

@ -0,0 +1,35 @@
[root]
name = "moonfire-ffmpeg"
version = "0.0.1"
dependencies = [
"gcc 0.3.51 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.24 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "gcc"
version = "0.3.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "libc"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "log"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "pkg-config"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
[metadata]
"checksum gcc 0.3.51 (registry+https://github.com/rust-lang/crates.io-index)" = "120d07f202dcc3f72859422563522b66fe6463a4c513df062874daad05f85f0a"
"checksum libc 0.2.24 (registry+https://github.com/rust-lang/crates.io-index)" = "38f5c2b18a287cf78b4097db62e20f43cace381dc76ae5c0a3073067f78b7ddc"
"checksum log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "880f77541efa6e5cc74e76910c9884d9859683118839d6a1dc3b11e63512565b"
"checksum pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3a8b4c6b8165cd1a1cd4b9b120978131389f64bdaf456435caa41e630edba903"

17
ffmpeg/Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "moonfire-ffmpeg"
version = "0.0.1"
authors = ["Scott Lamb <slamb@slamb.org>"]
readme = "../README.md"
#links = "ffmpeg"
[lib]
path = "lib.rs"
[dependencies]
libc = "0.2"
log = { version = "0.3", features = ["release_max_level_info"] }
[build-dependencies]
gcc = "0.3"
pkg-config = "0.3"

54
ffmpeg/build.rs Normal file
View File

@ -0,0 +1,54 @@
// This file is part of Moonfire NVR, a security camera digital video recorder.
// Copyright (C) 2017 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 gcc;
extern crate pkg_config;
fn main() {
let libraries = [
pkg_config::Config::new().atleast_version("54.1").probe("libavutil").unwrap(),
pkg_config::Config::new().atleast_version("56.0").probe("libavcodec").unwrap(),
pkg_config::Config::new().atleast_version("56.0").probe("libavformat").unwrap(),
];
let mut wrapper = gcc::Config::new();
// Pass compilation flags on to gcc. It'd be nice if pkg-config made this easier; see
// <https://github.com/alexcrichton/pkg-config-rs/issues/43>.
for lib in &libraries {
for p in &lib.include_paths {
wrapper.include(p);
}
for l in &lib.libs {
println!("lib: {}", l);
}
}
wrapper.file("wrapper.c").compile("libwrapper.a");
}

433
ffmpeg/lib.rs Normal file
View File

@ -0,0 +1,433 @@
// This file is part of Moonfire NVR, a security camera digital video recorder.
// Copyright (C) 2017 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 libc;
#[macro_use] extern crate log;
use std::cell::{Ref, RefCell};
use std::ffi::CStr;
use std::fmt;
use std::marker::PhantomData;
use std::ptr;
use std::sync;
static START: sync::Once = sync::ONCE_INIT;
//#[link(name = "avcodec")]
extern "C" {
fn avcodec_version() -> libc::c_int;
fn av_init_packet(p: *mut AVPacket);
fn av_packet_unref(p: *mut AVPacket);
fn moonfire_ffmpeg_cctx_codec_id(ctx: *const AVCodecContext) -> libc::c_int;
fn moonfire_ffmpeg_cctx_codec_type(ctx: *const AVCodecContext) -> libc::c_int;
fn moonfire_ffmpeg_cctx_extradata(ctx: *const AVCodecContext) -> DataLen;
fn moonfire_ffmpeg_cctx_height(ctx: *const AVCodecContext) -> libc::c_int;
fn moonfire_ffmpeg_cctx_width(ctx: *const AVCodecContext) -> libc::c_int;
}
//#[link(name = "avformat")]
extern "C" {
fn avformat_version() -> libc::c_int;
fn avformat_open_input(ctx: *mut *mut AVFormatContext, url: *const libc::c_char,
fmt: *const AVInputFormat, options: *mut *mut AVDictionary)
-> libc::c_int;
fn avformat_close_input(ctx: *mut *mut AVFormatContext);
fn avformat_find_stream_info(ctx: *mut AVFormatContext, options: *mut *mut AVDictionary)
-> libc::c_int;
fn av_read_frame(ctx: *mut AVFormatContext, p: *mut AVPacket) -> libc::c_int;
fn av_register_all();
fn avformat_network_init() -> libc::c_int;
fn moonfire_ffmpeg_fctx_streams(ctx: *const AVFormatContext) -> StreamsLen;
fn moonfire_ffmpeg_stream_codec(stream: *const AVStream) -> *const AVCodecContext;
fn moonfire_ffmpeg_stream_time_base(stream: *const AVStream) -> AVRational;
}
//#[link(name = "avutil")]
extern "C" {
fn avutil_version() -> libc::c_int;
fn av_strerror(e: libc::c_int, b: *mut u8, s: libc::size_t) -> libc::c_int;
fn av_dict_count(d: *const AVDictionary) -> libc::c_int;
fn av_dict_get(d: *const AVDictionary, key: *const libc::c_char, prev: *mut AVDictionaryEntry,
flags: libc::c_int) -> *mut AVDictionaryEntry;
fn av_dict_set(d: *mut *mut AVDictionary, key: *const libc::c_char, value: *const libc::c_char,
flags: libc::c_int) -> libc::c_int;
fn av_dict_free(d: *mut *mut AVDictionary);
}
//#[link(name = "wrapper")]
extern "C" {
static moonfire_ffmpeg_compiled_libavcodec_version: libc::c_int;
static moonfire_ffmpeg_compiled_libavformat_version: libc::c_int;
static moonfire_ffmpeg_compiled_libavutil_version: libc::c_int;
static moonfire_ffmpeg_av_dict_ignore_suffix: libc::c_int;
static moonfire_ffmpeg_av_nopts_value: libc::int64_t;
static moonfire_ffmpeg_av_codec_id_h264: libc::c_int;
static moonfire_ffmpeg_avmedia_type_video: libc::c_int;
static moonfire_ffmpeg_averror_eof: libc::c_int;
fn moonfire_ffmpeg_init();
fn moonfire_ffmpeg_packet_alloc() -> *mut AVPacket;
fn moonfire_ffmpeg_packet_free(p: *mut AVPacket);
fn moonfire_ffmpeg_packet_is_key(p: *const AVPacket) -> bool;
fn moonfire_ffmpeg_packet_pts(p: *const AVPacket) -> libc::int64_t;
fn moonfire_ffmpeg_packet_dts(p: *const AVPacket) -> libc::int64_t;
fn moonfire_ffmpeg_packet_duration(p: *const AVPacket) -> libc::c_int;
fn moonfire_ffmpeg_packet_set_pts(p: *mut AVPacket, pts: libc::int64_t);
fn moonfire_ffmpeg_packet_set_dts(p: *mut AVPacket, dts: libc::int64_t);
fn moonfire_ffmpeg_packet_set_duration(p: *mut AVPacket, dur: libc::c_int);
fn moonfire_ffmpeg_packet_data(p: *const AVPacket) -> DataLen;
fn moonfire_ffmpeg_packet_stream_index(p: *const AVPacket) -> libc::c_uint;
}
pub struct Ffmpeg {}
// No accessors here; seems reasonable to assume ABI stability of this simple struct.
#[repr(C)]
struct AVDictionaryEntry {
key: *mut libc::c_char,
value: *mut libc::c_char,
}
// Likewise, seems reasonable to assume this struct has a stable ABI.
#[repr(C)]
pub struct AVRational {
pub num: libc::c_int,
pub den: libc::c_int,
}
// No ABI stability assumption here; use heap allocation/deallocation and accessors only.
enum AVCodecContext {}
enum AVDictionary {}
enum AVFormatContext {}
enum AVInputFormat {}
enum AVPacket {}
enum AVStream {}
pub struct InputFormatContext {
ctx: *mut AVFormatContext,
pkt: RefCell<*mut AVPacket>,
}
impl InputFormatContext {
pub fn open(source: &CStr, dict: &mut Dictionary) -> Result<InputFormatContext, Error> {
let mut ctx = ptr::null_mut();
Error::wrap(unsafe {
avformat_open_input(&mut ctx, source.as_ptr(), ptr::null(), &mut dict.0)
})?;
let pkt = unsafe { moonfire_ffmpeg_packet_alloc() };
if pkt.is_null() {
panic!("malloc failed");
}
unsafe { av_init_packet(pkt) };
Ok(InputFormatContext{
ctx,
pkt: RefCell::new(pkt),
})
}
pub fn find_stream_info(&mut self) -> Result<(), Error> {
Error::wrap(unsafe { avformat_find_stream_info(self.ctx, ptr::null_mut()) })
}
// XXX: non-mut because of lexical lifetime woes in the caller. This is also why we need a
// RefCell.
pub fn read_frame<'i>(&'i self) -> Result<Packet<'i>, Error> {
let pkt = self.pkt.borrow();
Error::wrap(unsafe { av_read_frame(self.ctx, *pkt) })?;
Ok(Packet { _ctx: PhantomData, pkt: pkt })
}
pub fn streams<'i>(&'i self) -> Streams<'i> {
Streams {
_owner: PhantomData,
streams: unsafe { moonfire_ffmpeg_fctx_streams(self.ctx) },
}
}
}
unsafe impl Send for InputFormatContext {}
impl Drop for InputFormatContext {
fn drop(&mut self) {
unsafe {
moonfire_ffmpeg_packet_free(*self.pkt.borrow());
avformat_close_input(&mut self.ctx);
}
}
}
// matches moonfire_ffmpeg_data_len
#[repr(C)]
struct DataLen {
data: *const u8,
len: libc::size_t,
}
// matches moonfire_ffmpeg_streams_len
#[repr(C)]
struct StreamsLen {
streams: *const *const AVStream,
len: libc::size_t,
}
pub struct Packet<'i> {
_ctx: PhantomData<&'i InputFormatContext>,
pkt: Ref<'i, *mut AVPacket>,
}
impl<'i> Packet<'i> {
pub fn is_key(&self) -> bool { unsafe { moonfire_ffmpeg_packet_is_key(*self.pkt) } }
pub fn pts(&self) -> Option<i64> {
match unsafe { moonfire_ffmpeg_packet_pts(*self.pkt) } {
v if v == unsafe { moonfire_ffmpeg_av_nopts_value } => None,
v => Some(v),
}
}
pub fn set_pts(&mut self, pts: Option<i64>) {
let real_pts = match pts {
None => unsafe { moonfire_ffmpeg_av_nopts_value },
Some(v) => v,
};
unsafe { moonfire_ffmpeg_packet_set_pts(*self.pkt, real_pts); }
}
pub fn dts(&self) -> i64 { unsafe { moonfire_ffmpeg_packet_dts(*self.pkt) } }
pub fn set_dts(&mut self, dts: i64) {
unsafe { moonfire_ffmpeg_packet_set_dts(*self.pkt, dts); }
}
pub fn duration(&self) -> i32 { unsafe { moonfire_ffmpeg_packet_duration(*self.pkt) } }
pub fn set_duration(&mut self, dur: i32) {
unsafe { moonfire_ffmpeg_packet_set_duration(*self.pkt, dur) }
}
pub fn stream_index(&self) -> usize {
unsafe { moonfire_ffmpeg_packet_stream_index(*self.pkt) as usize }
}
pub fn data(&self) -> Option<&[u8]> {
unsafe {
let d = moonfire_ffmpeg_packet_data(*self.pkt);
if d.data.is_null() {
None
} else {
Some(::std::slice::from_raw_parts(d.data, d.len))
}
}
}
//pub fn deref(self) -> &'i InputFormatContext { self.ctx }
}
impl<'i> Drop for Packet<'i> {
fn drop(&mut self) {
unsafe {
av_packet_unref(*self.pkt);
}
}
}
pub struct Streams<'owner> {
_owner: PhantomData<&'owner ()>,
streams: StreamsLen,
}
impl<'owner> Streams<'owner> {
pub fn get(&self, i: usize) -> Stream<'owner> {
assert!(i < self.streams.len);
Stream {
_owner: PhantomData,
stream: unsafe { *self.streams.streams.offset(i as isize) }
}
}
pub fn len(&self) -> usize { self.streams.len }
}
pub struct Stream<'o> {
_owner: PhantomData<&'o ()>,
stream: *const AVStream,
}
impl<'o> Stream<'o> {
pub fn codec<'s>(&'s self) -> CodecContext<'s> {
CodecContext {
_owner: PhantomData,
ctx: unsafe { moonfire_ffmpeg_stream_codec(self.stream) },
}
}
pub fn time_base(&self) -> AVRational {
unsafe { moonfire_ffmpeg_stream_time_base(self.stream) }
}
}
pub struct CodecContext<'s> {
_owner: PhantomData<&'s ()>,
ctx: *const AVCodecContext,
}
impl<'s> CodecContext<'s> {
pub fn extradata(&self) -> &[u8] {
unsafe {
let d = moonfire_ffmpeg_cctx_extradata(self.ctx);
::std::slice::from_raw_parts(d.data, d.len)
}
}
pub fn width(&self) -> libc::c_int { unsafe { moonfire_ffmpeg_cctx_width(self.ctx) } }
pub fn height(&self) -> libc::c_int { unsafe { moonfire_ffmpeg_cctx_height(self.ctx) } }
pub fn codec_id(&self) -> CodecId {
CodecId(unsafe { moonfire_ffmpeg_cctx_codec_id(self.ctx) })
}
pub fn codec_type(&self) -> MediaType {
MediaType(unsafe { moonfire_ffmpeg_cctx_codec_type(self.ctx) })
}
}
#[derive(Copy, Clone, Debug)]
pub struct CodecId(libc::c_int);
impl CodecId {
pub fn is_h264(self) -> bool { self.0 == unsafe { moonfire_ffmpeg_av_codec_id_h264 } }
}
#[derive(Copy, Clone, Debug)]
pub struct MediaType(libc::c_int);
impl MediaType {
pub fn is_video(self) -> bool { self.0 == unsafe { moonfire_ffmpeg_avmedia_type_video } }
}
#[derive(Copy, Clone, Debug)]
pub struct Error(libc::c_int);
impl Error {
pub fn eof() -> Self { Error(unsafe { moonfire_ffmpeg_averror_eof }) }
fn wrap(raw: libc::c_int) -> Result<(), Error> {
match raw {
0 => Ok(()),
r => Err(Error(r)),
}
}
pub fn is_eof(self) -> bool { return self.0 == unsafe { moonfire_ffmpeg_averror_eof } }
}
impl std::error::Error for Error {
fn description(&self) -> &str {
// TODO: pull out some common cases.
"ffmpeg error"
}
fn cause(&self) -> Option<&std::error::Error> { None }
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
const ARRAYLEN: usize = 64;
let mut buf = [0u8; ARRAYLEN];
unsafe { av_strerror(self.0, buf.as_mut_ptr(), ARRAYLEN) };
f.write_str(std::str::from_utf8(&buf).map_err(|_| fmt::Error)?)
}
}
struct Version(libc::c_int);
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}.{}.{}", (self.0 >> 16) & 0xFF, (self.0 >> 8) & 0xFF, self.0 & 0xFF)
}
}
pub struct Dictionary(*mut AVDictionary);
impl Dictionary {
pub fn new() -> Dictionary { Dictionary(ptr::null_mut()) }
pub fn size(&self) -> usize { (unsafe { av_dict_count(self.0) }) as usize }
pub fn empty(&self) -> bool { self.size() == 0 }
pub fn set(&mut self, key: &CStr, value: &CStr) -> Result<(), Error> {
Error::wrap(unsafe { av_dict_set(&mut self.0, key.as_ptr(), value.as_ptr(), 0) })
}
}
impl fmt::Display for Dictionary {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut ent = ptr::null_mut();
let mut first = true;
loop {
unsafe {
let c = 0;
ent = av_dict_get(self.0, &c, ent, moonfire_ffmpeg_av_dict_ignore_suffix);
if ent.is_null() {
break;
}
if first {
first = false;
} else {
write!(f, ", ")?;
}
write!(f, "{}={}", CStr::from_ptr((*ent).key).to_string_lossy(),
CStr::from_ptr((*ent).value).to_string_lossy())?;
}
}
Ok(())
}
}
impl Drop for Dictionary {
fn drop(&mut self) { unsafe { av_dict_free(&mut self.0) } }
}
impl Ffmpeg {
pub fn new() -> Ffmpeg {
START.call_once(|| unsafe {
moonfire_ffmpeg_init();
av_register_all();
if avformat_network_init() < 0 {
panic!("avformat_network_init failed");
}
info!("Initialized ffmpeg. Versions:\n\
avcodec compiled={} running={}\n\
avformat compiled={} running={}\n\
avutil compiled={} running={}",
Version(moonfire_ffmpeg_compiled_libavcodec_version),
Version(avcodec_version()),
Version(moonfire_ffmpeg_compiled_libavformat_version),
Version(avformat_version()),
Version(moonfire_ffmpeg_compiled_libavutil_version),
Version(avutil_version()));
});
Ffmpeg{}
}
}

128
ffmpeg/wrapper.c Normal file
View File

@ -0,0 +1,128 @@
// This file is part of Moonfire NVR, a security camera digital video recorder.
// Copyright (C) 2017 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/>.
/* vim: set sw=4 et: */
#include <libavcodec/avcodec.h>
#include <libavcodec/version.h>
#include <libavformat/avformat.h>
#include <libavformat/version.h>
#include <libavutil/avutil.h>
#include <libavutil/dict.h>
#include <libavutil/version.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdlib.h>
const int moonfire_ffmpeg_compiled_libavcodec_version = LIBAVCODEC_VERSION_INT;
const int moonfire_ffmpeg_compiled_libavformat_version = LIBAVFORMAT_VERSION_INT;
const int moonfire_ffmpeg_compiled_libavutil_version = LIBAVUTIL_VERSION_INT;
const int moonfire_ffmpeg_av_dict_ignore_suffix = AV_DICT_IGNORE_SUFFIX;
const int64_t moonfire_ffmpeg_av_nopts_value = AV_NOPTS_VALUE;
const int moonfire_ffmpeg_avmedia_type_video = AVMEDIA_TYPE_VIDEO;
const int moonfire_ffmpeg_av_codec_id_h264 = AV_CODEC_ID_H264;
const int moonfire_ffmpeg_averror_eof = AVERROR_EOF;
static int lock_callback(void **mutex, enum AVLockOp op) {
switch (op) {
case AV_LOCK_CREATE:
*mutex = malloc(sizeof(pthread_mutex_t));
if (*mutex == NULL)
return -1;
if (pthread_mutex_init(*mutex, NULL) != 0)
return -1;
break;
case AV_LOCK_DESTROY:
if (pthread_mutex_destroy(*mutex) != 0)
return -1;
free(*mutex);
*mutex = NULL;
break;
case AV_LOCK_OBTAIN:
if (pthread_mutex_lock(*mutex) != 0)
return -1;
break;
case AV_LOCK_RELEASE:
if (pthread_mutex_unlock(*mutex) != 0)
return -1;
}
}
void moonfire_ffmpeg_init(void) {
if (av_lockmgr_register(&lock_callback) < 0) {
abort();
}
}
struct moonfire_ffmpeg_streams {
AVStream** streams;
size_t len;
};
struct moonfire_ffmpeg_data {
const char *data;
size_t len;
};
struct moonfire_ffmpeg_streams moonfire_ffmpeg_fctx_streams(AVFormatContext *ctx) {
struct moonfire_ffmpeg_streams s = {ctx->streams, ctx->nb_streams};
return s;
}
AVPacket *moonfire_ffmpeg_packet_alloc(void) { return malloc(sizeof(AVPacket)); }
void moonfire_ffmpeg_packet_free(AVPacket *pkt) { free(pkt); }
bool moonfire_ffmpeg_packet_is_key(AVPacket *pkt) { return (pkt->flags & AV_PKT_FLAG_KEY) != 0; }
int64_t moonfire_ffmpeg_packet_pts(AVPacket *pkt) { return pkt->pts; }
void moonfire_ffmpeg_packet_set_dts(AVPacket *pkt, int64_t dts) { pkt->dts = dts; }
void moonfire_ffmpeg_packet_set_pts(AVPacket *pkt, int64_t pts) { pkt->pts = pts; }
void moonfire_ffmpeg_packet_set_duration(AVPacket *pkt, int dur) { pkt->duration = dur; }
int64_t moonfire_ffmpeg_packet_dts(AVPacket *pkt) { return pkt->dts; }
int moonfire_ffmpeg_packet_duration(AVPacket *pkt) { return pkt->duration; }
int moonfire_ffmpeg_packet_stream_index(AVPacket *pkt) { return pkt->stream_index; }
struct moonfire_ffmpeg_data moonfire_ffmpeg_packet_data(AVPacket *pkt) {
struct moonfire_ffmpeg_data d = {pkt->data, pkt->size};
return d;
}
AVCodecContext *moonfire_ffmpeg_stream_codec(AVStream *stream) { return stream->codec; }
AVRational moonfire_ffmpeg_stream_time_base(AVStream *stream) { return stream->time_base; }
int moonfire_ffmpeg_cctx_codec_id(AVCodecContext *cctx) { return cctx->codec_id; }
int moonfire_ffmpeg_cctx_codec_type(AVCodecContext *cctx) { return cctx->codec_type; }
struct moonfire_ffmpeg_data moonfire_ffmpeg_cctx_extradata(AVCodecContext *cctx) {
struct moonfire_ffmpeg_data d = {cctx->extradata, cctx->extradata_size};
return d;
}
int moonfire_ffmpeg_cctx_height(AVCodecContext *cctx) { return cctx->height; }
int moonfire_ffmpeg_cctx_width(AVCodecContext *cctx) { return cctx->width; }

View File

@ -182,8 +182,6 @@ impl SampleFileDir {
write!(&mut buf[..36], "{}", uuid.hyphenated()).expect("can't format uuid to pathname buf"); 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). // 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) } unsafe { mem::transmute::<[u8; 37], [libc::c_char; 37]>(buf) }
} }

View File

@ -34,8 +34,8 @@ extern crate uuid;
use core::ops::Deref; use core::ops::Deref;
use core::num; use core::num;
use ffmpeg;
use openssl::error::ErrorStack; use openssl::error::ErrorStack;
use moonfire_ffmpeg;
use serde_json; use serde_json;
use std::boxed::Box; use std::boxed::Box;
use std::convert::From; use std::convert::From;
@ -127,9 +127,9 @@ impl From<serde_json::Error> for Error {
} }
} }
impl From<ffmpeg::Error> for Error { impl From<moonfire_ffmpeg::Error> for Error {
fn from(err: ffmpeg::Error) -> Self { fn from(err: moonfire_ffmpeg::Error) -> Self {
Error{description: format!("{} ({})", err.description(), err), cause: Some(Box::new(err))} Error{description: format!("ffmpeg: {}", err), cause: Some(Box::new(err))}
} }
} }

View File

@ -33,8 +33,6 @@
extern crate byteorder; extern crate byteorder;
extern crate core; extern crate core;
extern crate docopt; extern crate docopt;
#[macro_use] extern crate ffmpeg;
extern crate ffmpeg_sys;
extern crate futures; extern crate futures;
extern crate fnv; extern crate fnv;
extern crate http_entity; extern crate http_entity;
@ -47,6 +45,7 @@ extern crate reffers;
extern crate rusqlite; extern crate rusqlite;
extern crate memmap; extern crate memmap;
extern crate mime; extern crate mime;
extern crate moonfire_ffmpeg;
extern crate mylog; extern crate mylog;
extern crate openssl; extern crate openssl;
extern crate parking_lot; extern crate parking_lot;

View File

@ -83,7 +83,7 @@ use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use db; use db;
use dir; use dir;
use error::Error; use error::Error;
use futures::{Future, Stream}; use futures::Stream;
use futures::stream; use futures::stream;
use http_entity; use http_entity;
use hyper::header; use hyper::header;
@ -1242,8 +1242,8 @@ mod tests {
use byteorder::{BigEndian, ByteOrder}; use byteorder::{BigEndian, ByteOrder};
use db; use db;
use dir; use dir;
use futures::Future;
use futures::Stream as FuturesStream; use futures::Stream as FuturesStream;
use ffmpeg;
use hyper::header; use hyper::header;
use openssl::hash; use openssl::hash;
use recording::{self, TIME_UNITS_PER_SEC}; use recording::{self, TIME_UNITS_PER_SEC};
@ -1475,14 +1475,14 @@ mod tests {
loop { loop {
let pkt = match input.get_next() { let pkt = match input.get_next() {
Ok(p) => p, Ok(p) => p,
Err(ffmpeg::Error::Eof) => { break; }, Err(e) if e.is_eof() => { break; },
Err(e) => { panic!("unexpected input error: {}", e); }, Err(e) => { panic!("unexpected input error: {}", e); },
}; };
let pts = pkt.pts().unwrap(); let pts = pkt.pts().unwrap();
frame_time += recording::Duration(pkt.duration()); frame_time += recording::Duration(pkt.duration() as i64);
output.write(pkt.data().expect("packet without data"), frame_time, pts, output.write(pkt.data().expect("packet without data"), frame_time, pts,
pkt.is_key()).unwrap(); pkt.is_key()).unwrap();
end_pts = Some(pts + pkt.duration()); end_pts = Some(pts + pkt.duration() as i64);
} }
output.close(end_pts).unwrap(); output.close(end_pts).unwrap();
db.syncer_channel.flush(); db.syncer_channel.flush();
@ -1528,12 +1528,12 @@ mod tests {
loop { loop {
let orig_pkt = match orig.get_next() { let orig_pkt = match orig.get_next() {
Ok(p) => Some(p), Ok(p) => Some(p),
Err(ffmpeg::Error::Eof) => None, Err(e) if e.is_eof() => None,
Err(e) => { panic!("unexpected input error: {}", e); }, Err(e) => { panic!("unexpected input error: {}", e); },
}; };
let new_pkt = match new.get_next() { let new_pkt = match new.get_next() {
Ok(p) => Some(p), Ok(p) => Some(p),
Err(ffmpeg::Error::Eof) => { break; }, Err(e) if e.is_eof() => { break; },
Err(e) => { panic!("unexpected input error: {}", e); }, Err(e) => { panic!("unexpected input error: {}", e); },
}; };
let (orig_pkt, new_pkt) = match (orig_pkt, new_pkt) { let (orig_pkt, new_pkt) = match (orig_pkt, new_pkt) {
@ -1545,7 +1545,7 @@ mod tests {
assert_eq!(orig_pkt.dts(), new_pkt.dts() + pts_offset); assert_eq!(orig_pkt.dts(), new_pkt.dts() + pts_offset);
assert_eq!(orig_pkt.data(), new_pkt.data()); assert_eq!(orig_pkt.data(), new_pkt.data());
assert_eq!(orig_pkt.is_key(), new_pkt.is_key()); assert_eq!(orig_pkt.is_key(), new_pkt.is_key());
final_durations = Some((orig_pkt.duration(), new_pkt.duration())); final_durations = Some((orig_pkt.duration() as i64, new_pkt.duration() as i64));
} }
if let Some((orig_dur, new_dur)) = final_durations { if let Some((orig_dur, new_dur)) = final_durations {

View File

@ -29,14 +29,10 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
use error::Error; use error::Error;
use ffmpeg::{self, format, media};
use ffmpeg_sys::{self, AVLockOp};
use h264; use h264;
use libc::{self, c_int, c_void}; use moonfire_ffmpeg;
use std::mem; use std::ffi::{CStr, CString};
use std::ptr;
use std::result::Result; use std::result::Result;
use std::slice;
use std::sync; use std::sync;
static START: sync::Once = sync::ONCE_INIT; static START: sync::Once = sync::ONCE_INIT;
@ -52,41 +48,13 @@ pub enum Source<'a> {
Rtsp(&'a str), // url, for production use. 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
}
pub trait Opener<S : Stream> : Sync { pub trait Opener<S : Stream> : Sync {
fn open(&self, src: Source) -> Result<S, Error>; fn open(&self, src: Source) -> Result<S, Error>;
} }
pub trait Stream { pub trait Stream {
fn get_extra_data(&self) -> Result<h264::ExtraData, Error>; fn get_extra_data(&self) -> Result<h264::ExtraData, Error>;
fn get_next(&mut self) -> Result<ffmpeg::Packet, ffmpeg::Error>; fn get_next<'p>(&'p mut self) -> Result<moonfire_ffmpeg::Packet<'p>, moonfire_ffmpeg::Error>;
} }
pub struct Ffmpeg {} pub struct Ffmpeg {}
@ -94,41 +62,58 @@ pub struct Ffmpeg {}
impl Ffmpeg { impl Ffmpeg {
fn new() -> Ffmpeg { fn new() -> Ffmpeg {
START.call_once(|| { START.call_once(|| {
unsafe { ffmpeg_sys::av_lockmgr_register(lock_callback); }; moonfire_ffmpeg::Ffmpeg::new();
ffmpeg::init().unwrap(); //ffmpeg::init().unwrap();
ffmpeg::format::network::init(); //ffmpeg::format::network::init();
}); });
Ffmpeg{} Ffmpeg{}
} }
} }
macro_rules! c_str {
($s:expr) => { {
unsafe { CStr::from_ptr(concat!($s, "\0").as_ptr() as *const i8) }
} }
}
impl Opener<FfmpegStream> for Ffmpeg { impl Opener<FfmpegStream> for Ffmpeg {
fn open(&self, src: Source) -> Result<FfmpegStream, Error> { fn open(&self, src: Source) -> Result<FfmpegStream, Error> {
let (input, discard_first) = match src { use moonfire_ffmpeg::InputFormatContext;
let (mut input, discard_first) = match src {
#[cfg(test)] #[cfg(test)]
Source::File(filename) => Source::File(filename) =>
(format::input_with(&format!("file:{}", filename), ffmpeg::Dictionary::new())?, (InputFormatContext::open(&CString::new(format!("file:{}", filename)).unwrap(),
&mut moonfire_ffmpeg::Dictionary::new())?,
false), false),
Source::Rtsp(url) => { Source::Rtsp(url) => {
let open_options = dict![ let mut open_options = moonfire_ffmpeg::Dictionary::new();
"rtsp_transport" => "tcp", open_options.set(c_str!("rtsp_transport"), c_str!("tcp")).unwrap();
// https://trac.ffmpeg.org/ticket/5018 workaround attempt. // https://trac.ffmpeg.org/ticket/5018 workaround attempt.
"probesize" => "262144", open_options.set(c_str!("probesize"), c_str!("262144")).unwrap();
"user-agent" => "moonfire-nvr", open_options.set(c_str!("user-agent"), c_str!("moonfire-nvr")).unwrap();
// 10-second socket timeout, in microseconds. // 10-second socket timeout, in microseconds.
"stimeout" => "10000000" open_options.set(c_str!("stimeout"), c_str!("10000000")).unwrap();
]; let i = InputFormatContext::open(&CString::new(url).unwrap(), &mut open_options)?;
(format::input_with(&url, open_options)?, true) if !open_options.empty() {
warn!("While opening URL {}, some options were not understood: {}",
url, open_options);
}
(i, true)
}, },
}; };
input.find_stream_info()?;
// Find the video stream. // Find the video stream.
let mut video_i = None; let mut video_i = None;
for (i, stream) in input.streams().enumerate() { {
if stream.codec().medium() == media::Type::Video { let s = input.streams();
debug!("Video stream index is {}", i); for i in 0 .. s.len() {
video_i = Some(i); if s.get(i).codec().codec_type().is_video() {
break; debug!("Video stream index is {}", i);
video_i = Some(i);
break;
}
} }
} }
let video_i = match video_i { let video_i = match video_i {
@ -137,8 +122,8 @@ impl Opener<FfmpegStream> for Ffmpeg {
}; };
let mut stream = FfmpegStream{ let mut stream = FfmpegStream{
input: input, input,
video_i: video_i, video_i,
}; };
if discard_first { if discard_first {
@ -151,30 +136,31 @@ impl Opener<FfmpegStream> for Ffmpeg {
} }
pub struct FfmpegStream { pub struct FfmpegStream {
input: format::context::Input, input: moonfire_ffmpeg::InputFormatContext,
video_i: usize, video_i: usize,
} }
impl Stream for FfmpegStream { impl Stream for FfmpegStream {
fn get_extra_data(&self) -> Result<h264::ExtraData, Error> { 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 video = self.input.streams().get(self.video_i);
let tb = video.time_base();
if tb.num != 1 || tb.den != 90000 {
return Err(Error::new(format!("video stream has timebase {}/{}; expected 1/90000",
tb.num, tb.den)));
}
let codec = video.codec(); let codec = video.codec();
let (extradata, width, height) = unsafe { let codec_id = codec.codec_id();
let ptr = codec.as_ptr(); if !codec_id.is_h264() {
(slice::from_raw_parts((*ptr).extradata, (*ptr).extradata_size as usize), return Err(Error::new(format!("stream's video codec {:?} is not h264", codec_id)));
(*ptr).width as u16, }
(*ptr).height as u16) h264::ExtraData::parse(codec.extradata(), codec.width() as u16, codec.height() as u16)
};
// TODO: verify video stream is h264.
h264::ExtraData::parse(extradata, width, height)
} }
fn get_next(&mut self) -> Result<ffmpeg::Packet, ffmpeg::Error> { fn get_next<'i>(&'i mut self) -> Result<moonfire_ffmpeg::Packet<'i>, moonfire_ffmpeg::Error> {
let mut pkt = ffmpeg::Packet::empty();
loop { loop {
pkt.read(&mut self.input)?; let p = self.input.read_frame()?;
if pkt.stream() == self.video_i { if p.stream_index() == self.video_i {
return Ok(pkt); return Ok(p);
} }
} }
} }

View File

@ -113,7 +113,6 @@ impl<'a, C, S> Streamer<'a, C, S> where C: 'a + Clocks, S: 'a + stream::Stream {
let mut stream = self.opener.open(stream::Source::Rtsp(&self.url))?; let mut stream = self.opener.open(stream::Source::Rtsp(&self.url))?;
let realtime_offset = self.clocks.realtime() - self.clocks.monotonic(); let realtime_offset = self.clocks.realtime() - self.clocks.monotonic();
// TODO: verify time base.
// TODO: verify width/height. // TODO: verify width/height.
let extra_data = stream.get_extra_data()?; let extra_data = stream.get_extra_data()?;
let video_sample_entry_id = let video_sample_entry_id =
@ -190,9 +189,8 @@ mod tests {
use clock::{self, Clocks}; use clock::{self, Clocks};
use db; use db;
use error::Error; use error::Error;
use ffmpeg;
use ffmpeg::packet::Mut;
use h264; use h264;
use moonfire_ffmpeg;
use recording; use recording;
use std::cmp; use std::cmp;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@ -228,9 +226,9 @@ mod tests {
} }
impl<'a> Stream for ProxyingStream<'a> { impl<'a> Stream for ProxyingStream<'a> {
fn get_next(&mut self) -> Result<ffmpeg::Packet, ffmpeg::Error> { fn get_next(&mut self) -> Result<moonfire_ffmpeg::Packet, moonfire_ffmpeg::Error> {
if self.pkts_left == 0 { if self.pkts_left == 0 {
return Err(ffmpeg::Error::Eof); return Err(moonfire_ffmpeg::Error::eof());
} }
self.pkts_left -= 1; self.pkts_left -= 1;
@ -240,7 +238,7 @@ mod tests {
// Avoid accumulating conversion error by tracking the total amount to sleep and how // Avoid accumulating conversion error by tracking the total amount to sleep and how
// much we've already slept, rather than considering each frame in isolation. // much we've already slept, rather than considering each frame in isolation.
{ {
let goal = pkt.pts().unwrap() + pkt.duration(); let goal = pkt.pts().unwrap() + pkt.duration() as i64;
let goal = time::Duration::nanoseconds( let goal = time::Duration::nanoseconds(
goal * 1_000_000_000 / recording::TIME_UNITS_PER_SEC); goal * 1_000_000_000 / recording::TIME_UNITS_PER_SEC);
let duration = goal - self.slept; let duration = goal - self.slept;
@ -254,15 +252,12 @@ mod tests {
self.ts_offset_pkts_left -= 1; self.ts_offset_pkts_left -= 1;
let old_pts = pkt.pts().unwrap(); let old_pts = pkt.pts().unwrap();
let old_dts = pkt.dts(); let old_dts = pkt.dts();
unsafe { pkt.set_pts(Some(old_pts + self.ts_offset));
let pkt = pkt.as_mut_ptr(); pkt.set_dts(old_dts + self.ts_offset);
(*pkt).pts = old_pts + self.ts_offset;
(*pkt).dts = old_dts + self.ts_offset;
// In a real rtsp stream, the duration of a packet is not known until the // In a real rtsp stream, the duration of a packet is not known until the
// next packet. ffmpeg's duration is an unreliable estimate. // next packet. ffmpeg's duration is an unreliable estimate.
(*pkt).duration = recording::TIME_UNITS_PER_SEC as i32; pkt.set_duration(recording::TIME_UNITS_PER_SEC as i32);
}
} }
Ok(pkt) Ok(pkt)