initial commit
This commit is contained in:
commit
5cc69e751c
66
.github/workflows/check-license.py
vendored
Executable file
66
.github/workflows/check-license.py
vendored
Executable file
@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
"""Checks that expected header lines are present.
|
||||
|
||||
Call in either of two modes:
|
||||
|
||||
has-license.py FILE [...]
|
||||
check if all files with certain extensions have expected lines.
|
||||
This is useful in a CI action.
|
||||
|
||||
has-license.py
|
||||
check if stdin has expected lines.
|
||||
This is useful in a pre-commit hook, as in
|
||||
git-format-staged --no-write --formatter '.../has-license.py' '*.rs'
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
|
||||
# Filenames matching this regexp are expected to have the header lines.
|
||||
FILENAME_MATCHER = re.compile(r'.*\.rs$')
|
||||
|
||||
MAX_LINE_COUNT = 10
|
||||
|
||||
EXPECTED_LINES = [
|
||||
re.compile(r'Copyright \(C\) 20\d{2} Scott Lamb <slamb@slamb\.org>'),
|
||||
re.compile(r'SPDX-License-Identifier: MIT OR Apache-2\.0'),
|
||||
]
|
||||
|
||||
def has_license(f):
|
||||
"""Returns if all of EXPECTED_LINES are present within the first
|
||||
MAX_LINE_COUNT lines of f."""
|
||||
needed = set(EXPECTED_LINES)
|
||||
i = 0
|
||||
for line in f:
|
||||
if i == 10:
|
||||
break
|
||||
i += 1
|
||||
for e in needed:
|
||||
if e.search(line):
|
||||
needed.remove(e)
|
||||
break
|
||||
if not needed:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def file_has_license(filename):
|
||||
with open(filename, 'r') as f:
|
||||
return has_license(f)
|
||||
|
||||
|
||||
def main(args):
|
||||
if not args:
|
||||
sys.exit(0 if has_license(sys.stdin) else 1)
|
||||
|
||||
missing = [f for f in args
|
||||
if FILENAME_MATCHER.match(f) and not file_has_license(f)]
|
||||
if missing:
|
||||
print('The following files are missing expected copyright/license headers:', file=sys.stderr)
|
||||
print('\n'.join(missing), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:])
|
49
.github/workflows/ci.yml
vendored
Normal file
49
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
name: CI
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
rust:
|
||||
name: Test
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- stable
|
||||
- 1.52
|
||||
include:
|
||||
- rust: stable
|
||||
extra_components: rustfmt
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.rust }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: ${{ matrix.extra_components }}
|
||||
- name: Test
|
||||
run: cargo test --all-features
|
||||
- name: Check formatting
|
||||
if: matrix.rust == 'stable'
|
||||
run: cargo fmt -- --check
|
||||
license:
|
||||
name: Check copyright/license headers
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- run: find . -type f -print0 | xargs -0 .github/workflows/check-license.py
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
1205
Cargo.lock
generated
Normal file
1205
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
Cargo.toml
Normal file
38
Cargo.toml
Normal file
@ -0,0 +1,38 @@
|
||||
[package]
|
||||
name = "retina"
|
||||
version = "0.0.1"
|
||||
authors = ["Scott Lamb <slamb@slamb.org>"]
|
||||
license = "MIT/Apache-2.0"
|
||||
edition = "2018"
|
||||
keywords = ["rtsp", "multimedia", "video", "streaming", "ip-camera"]
|
||||
categories = ["network-programming", "multimedia"]
|
||||
description = "high-level RTSP multimedia streaming library"
|
||||
|
||||
[dependencies]
|
||||
async-stream = "0.3.1"
|
||||
base64 = "0.13.0"
|
||||
bitreader = "0.3.3"
|
||||
bytes = "1.0.1"
|
||||
digest_auth = "0.3.0"
|
||||
failure = "0.1.8"
|
||||
futures = "0.3.14"
|
||||
hex = "0.4.3"
|
||||
h264-reader = { git = "https://github.com/dholroyd/h264-reader" }
|
||||
log = "0.4.8"
|
||||
once_cell = "1.7.2"
|
||||
pin-project = "1.0.7"
|
||||
pretty-hex = "0.2.1"
|
||||
rtcp = "0.2.1"
|
||||
rtp-rs = "0.5.0"
|
||||
rtsp-types = { git = "https://github.com/sdroege/rtsp-types" }
|
||||
sdp = "0.1.3"
|
||||
smallvec = { version = "1.6.1", features = ["union"] }
|
||||
structopt = "0.3.21"
|
||||
time = "0.1.43"
|
||||
tokio = { version = "1.5.0", features = ["full", "parking_lot"] }
|
||||
tokio-util = { version = "0.6.6", features = ["codec"] }
|
||||
url = "2.2.1"
|
||||
|
||||
[dev-dependencies]
|
||||
mylog = { git = "https://github.com/scottlamb/mylog" }
|
||||
parking_lot = "0.11.0"
|
202
LICENSE-APACHE.txt
Normal file
202
LICENSE-APACHE.txt
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2021 Scott Lamb <slamb@slamb.org>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
20
LICENSE-MIT.txt
Normal file
20
LICENSE-MIT.txt
Normal file
@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2021 Scott Lamb <slamb@slamb.org>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
59
README.md
Normal file
59
README.md
Normal file
@ -0,0 +1,59 @@
|
||||
# retina
|
||||
|
||||
[![CI](https://github.com/scottlamb/retina/workflows/CI/badge.svg)](https://github.com/scottlamb/retina/actions?query=workflow%3ACI)
|
||||
|
||||
High-level RTSP multimedia RTSP streaming library, in Rust. Good support for
|
||||
IP surveillance cameras, as needed by
|
||||
[Moonfire NVR](https://github.com/scottlamb/moonfire-nvr).
|
||||
|
||||
Progress:
|
||||
|
||||
* [x] client support
|
||||
* * [x] digest authentication
|
||||
* * [x] RTP over TCP via RTSP interleaved channels.
|
||||
* * [ ] RTP over UDP. (Shouldn't be hard but I haven't needed it.)
|
||||
* * [ ] SRTP
|
||||
* * [ ] ONVIF backchannel support (for sending audio).
|
||||
* [ ] server support
|
||||
* async
|
||||
* * [x] tokio
|
||||
* * [ ] async-std. (Most of the crate's code is independent of the async
|
||||
library, so I don't expect this would be too hard to add.)
|
||||
* codec depacketization
|
||||
* * [x] video: H.264
|
||||
* * * [ ] SVC
|
||||
* * * [ ] periodic infra refresh
|
||||
* * * [ ] partitioned slices
|
||||
* * audio
|
||||
* * * [x] AAC
|
||||
* * * * [ ] interleaving
|
||||
* * * [x] RFC 3551 codecs: G.711,G.723, L8/L16
|
||||
* * [x] application: ONVIF metadata
|
||||
* [ ] uniform, documented API. (Currently haphazard in terms of naming, what
|
||||
fields are exposed directly vs use an accessors, etc.)
|
||||
* [ ] rich errors. (Currently uses untyped errors with the deprecated failure
|
||||
crate; some error messages are quite detailed, others aren't.)
|
||||
* [ ] released versions of all deps. (crates.io publishing requirement.)
|
||||
* [ ] good functional testing coverage. (Currently lightly / unevenly tested.
|
||||
The depacketizers have no test coverage, and there's at least one place
|
||||
left that can panic on bad input.)
|
||||
* [ ] fuzz testing
|
||||
|
||||
Help welcome!
|
||||
|
||||
## Why "retina"?
|
||||
|
||||
It's a working name. Other ideas welcome. I started by looking at dictionary
|
||||
words with the letters R, T, S, and P in order and picking out ones related to
|
||||
video:
|
||||
|
||||
| `$ egrep '^r.*t.*s.*p' /usr/share/dict/words'` | |
|
||||
| ---------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| <b>r</b>e<b>t</b>ino<b>s</b>co<b>p</b>e | close but too long, thus `retina` |
|
||||
| <b>r</b>e<b>t</b>ro<b>sp</b>ect | good name for an NVR, but I already picked Moonfire |
|
||||
| <b>r</b>o<b>t</b>a<b>s</b>co<b>p</b>e | misspelling of "rotascope" (animation tool) or archaic name for "gyroscope"? |
|
||||
|
||||
## License
|
||||
|
||||
Your choice of MIT or Apache; see [LICENSE-MIT.txt](LICENSE-MIT.txt) or
|
||||
[LICENSE-APACHE](LICENSE-APACHE.txt), respectively.
|
97
examples/client/main.rs
Normal file
97
examples/client/main.rs
Normal file
@ -0,0 +1,97 @@
|
||||
// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
//! RTSP client examples.
|
||||
|
||||
mod metadata;
|
||||
mod mp4;
|
||||
|
||||
use failure::Error;
|
||||
use log::{error, info};
|
||||
use std::{fmt::Write, str::FromStr};
|
||||
use structopt::StructOpt;
|
||||
|
||||
#[derive(StructOpt)]
|
||||
struct Source {
|
||||
#[structopt(long, parse(try_from_str))]
|
||||
url: url::Url,
|
||||
|
||||
#[structopt(long, requires = "password")]
|
||||
username: Option<String>,
|
||||
|
||||
#[structopt(long, requires = "username")]
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
enum Cmd {
|
||||
Mp4(mp4::Opts),
|
||||
Metadata(metadata::Opts),
|
||||
}
|
||||
|
||||
/// Returns a pretty-and-informative version of `e`.
|
||||
pub fn prettify_failure(e: &failure::Error) -> String {
|
||||
let mut msg = e.to_string();
|
||||
for cause in e.iter_causes() {
|
||||
write!(&mut msg, "\ncaused by: {}", cause).unwrap();
|
||||
}
|
||||
if e.backtrace().is_empty() {
|
||||
write!(
|
||||
&mut msg,
|
||||
"\n\n(set environment variable RUST_BACKTRACE=1 to see backtraces)"
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
write!(&mut msg, "\n\nBacktrace:\n{}", e.backtrace()).unwrap();
|
||||
}
|
||||
msg
|
||||
}
|
||||
|
||||
fn init_logging() -> mylog::Handle {
|
||||
let h = mylog::Builder::new()
|
||||
.set_format(
|
||||
::std::env::var("MOONFIRE_FORMAT")
|
||||
.map_err(|_| ())
|
||||
.and_then(|s| mylog::Format::from_str(&s))
|
||||
.unwrap_or(mylog::Format::Google),
|
||||
)
|
||||
.set_spec(::std::env::var("MOONFIRE_LOG").as_deref().unwrap_or("info"))
|
||||
.build();
|
||||
h.clone().install().unwrap();
|
||||
h
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let mut h = init_logging();
|
||||
if let Err(e) = {
|
||||
let _a = h.async_scope();
|
||||
main_inner().await
|
||||
} {
|
||||
error!("Fatal: {}", prettify_failure(&e));
|
||||
std::process::exit(1);
|
||||
}
|
||||
info!("Done");
|
||||
}
|
||||
|
||||
/// Interpets the `username` and `password` of a [Source].
|
||||
fn creds(
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
) -> Option<retina::client::Credentials> {
|
||||
match (username, password) {
|
||||
(Some(username), Some(password)) => {
|
||||
Some(retina::client::Credentials { username, password })
|
||||
}
|
||||
(None, None) => None,
|
||||
_ => unreachable!(), // structopt/clap enforce username and password's mutual "requires".
|
||||
}
|
||||
}
|
||||
|
||||
async fn main_inner() -> Result<(), Error> {
|
||||
let cmd = Cmd::from_args();
|
||||
match cmd {
|
||||
Cmd::Mp4(opts) => mp4::run(opts).await,
|
||||
Cmd::Metadata(opts) => metadata::run(opts).await,
|
||||
}
|
||||
}
|
49
examples/client/metadata.rs
Normal file
49
examples/client/metadata.rs
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use failure::{format_err, Error};
|
||||
use futures::StreamExt;
|
||||
use log::info;
|
||||
use retina::codec::CodecItem;
|
||||
|
||||
#[derive(structopt::StructOpt)]
|
||||
pub struct Opts {
|
||||
#[structopt(flatten)]
|
||||
src: super::Source,
|
||||
}
|
||||
|
||||
pub async fn run(opts: Opts) -> Result<(), Error> {
|
||||
let stop = tokio::signal::ctrl_c();
|
||||
|
||||
let creds = super::creds(opts.src.username, opts.src.password);
|
||||
let mut session = retina::client::Session::describe(opts.src.url, creds).await?;
|
||||
let onvif_stream_i = session
|
||||
.streams()
|
||||
.iter()
|
||||
.position(|s| matches!(s.parameters(), Some(retina::codec::Parameters::Message(..))))
|
||||
.ok_or_else(|| format_err!("couldn't find onvif stream"))?;
|
||||
session.setup(onvif_stream_i).await?;
|
||||
let session = session
|
||||
.play(retina::client::PlayPolicy::default().ignore_zero_seq(true))
|
||||
.await?
|
||||
.demuxed()?;
|
||||
|
||||
tokio::pin!(session);
|
||||
tokio::pin!(stop);
|
||||
loop {
|
||||
tokio::select! {
|
||||
item = session.next() => {
|
||||
match item.ok_or_else(|| format_err!("EOF"))?? {
|
||||
CodecItem::MessageFrame(m) => {
|
||||
info!("{}: {}\n", &m.timestamp, std::str::from_utf8(&m.data[..]).unwrap());
|
||||
},
|
||||
_ => continue,
|
||||
};
|
||||
},
|
||||
_ = &mut stop => {
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
601
examples/client/mp4.rs
Normal file
601
examples/client/mp4.rs
Normal file
@ -0,0 +1,601 @@
|
||||
// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
//! Proof-of-concept `.mp4` writer.
|
||||
//!
|
||||
//! This writes media data (`mdat`) to a stream, buffering parameters for a
|
||||
//! `moov` atom at the end. This avoids the need to buffer the media data
|
||||
//! (`mdat`) first or reserved a fixed size for the `moov`, but it will slow
|
||||
//! playback, particularly when serving `.mp4` files remotely.
|
||||
//!
|
||||
//! For a more high-quality implementation, see [Moonfire NVR](https://github.com/scottlamb/moonfire-nvr).
|
||||
//! It's better tested, places the `moov` atom at the start, can do HTTP range
|
||||
//! serving for arbitrary time ranges, and supports standard and fragmented
|
||||
//! `.mp4` files.
|
||||
//!
|
||||
//! See the BMFF spec, ISO/IEC 14496-12:2015:
|
||||
//! https://github.com/scottlamb/moonfire-nvr/wiki/Standards-and-specifications
|
||||
//! https://standards.iso.org/ittf/PubliclyAvailableStandards/c068960_ISO_IEC_14496-12_2015.zip
|
||||
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use failure::{bail, format_err, Error};
|
||||
use futures::StreamExt;
|
||||
use log::info;
|
||||
use retina::codec::{AudioParameters, CodecItem, VideoParameters};
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::io::SeekFrom;
|
||||
use std::num::NonZeroU32;
|
||||
use std::path::PathBuf;
|
||||
use tokio::io::{AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt};
|
||||
|
||||
#[derive(structopt::StructOpt)]
|
||||
pub struct Opts {
|
||||
#[structopt(flatten)]
|
||||
src: super::Source,
|
||||
|
||||
#[structopt(default_value, long)]
|
||||
initial_timestamp: retina::client::InitialTimestampPolicy,
|
||||
|
||||
#[structopt(long)]
|
||||
no_video: bool,
|
||||
|
||||
#[structopt(long)]
|
||||
no_audio: bool,
|
||||
|
||||
#[structopt(parse(try_from_str))]
|
||||
out: PathBuf,
|
||||
}
|
||||
|
||||
/// Writes a box length for everything appended in the supplied scope.
|
||||
macro_rules! write_box {
|
||||
($buf:expr, $fourcc:expr, $b:block) => {{
|
||||
let _: &mut BytesMut = $buf; // type-check.
|
||||
let pos_start = $buf.len();
|
||||
let fourcc: &[u8; 4] = $fourcc;
|
||||
$buf.extend_from_slice(&[0, 0, 0, 0, fourcc[0], fourcc[1], fourcc[2], fourcc[3]]);
|
||||
let r = {
|
||||
$b;
|
||||
};
|
||||
let pos_end = $buf.len();
|
||||
let len = pos_end.checked_sub(pos_start).unwrap();
|
||||
$buf[pos_start..pos_start + 4].copy_from_slice(&u32::try_from(len)?.to_be_bytes()[..]);
|
||||
r
|
||||
}};
|
||||
}
|
||||
|
||||
async fn write_all_buf<W: AsyncWrite + Unpin, B: Buf>(
|
||||
writer: &mut W,
|
||||
buf: &mut B,
|
||||
) -> Result<(), Error> {
|
||||
// TODO: this doesn't use vectored I/O. Annoying.
|
||||
while buf.has_remaining() {
|
||||
writer.write_buf(buf).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes `.mp4` data to a sink.
|
||||
/// See module-level documentation for details.
|
||||
pub struct Mp4Writer<W: AsyncWrite + AsyncSeek + Send + Unpin> {
|
||||
mdat_start: u32,
|
||||
mdat_pos: u32,
|
||||
video_params: Option<VideoParameters>,
|
||||
audio_params: Option<AudioParameters>,
|
||||
|
||||
/// The (1-indexed) video sample (frame) number of each sync sample (random access point).
|
||||
video_sync_sample_nums: Vec<u32>,
|
||||
|
||||
video_trak: TrakTracker,
|
||||
audio_trak: TrakTracker,
|
||||
inner: W,
|
||||
}
|
||||
|
||||
/// Tracks the parts of a `trak` atom which are common between video and audio samples.
|
||||
#[derive(Default)]
|
||||
struct TrakTracker {
|
||||
samples: u32,
|
||||
next_pos: Option<u32>,
|
||||
chunks: Vec<(u32, u32)>, // (1-based sample_number, byte_pos)
|
||||
sizes: Vec<u32>,
|
||||
|
||||
/// The durations of samples in a run-length encoding form: (number of samples, duration).
|
||||
/// This lags one sample behind calls to `add_sample` because each sample's duration
|
||||
/// is calculated using the PTS of the following sample.
|
||||
durations: Vec<(u32, u32)>,
|
||||
last_pts: Option<i64>,
|
||||
tot_duration: u64,
|
||||
}
|
||||
|
||||
impl TrakTracker {
|
||||
fn add_sample(
|
||||
&mut self,
|
||||
pos: u32,
|
||||
size: u32,
|
||||
timestamp: retina::Timestamp,
|
||||
loss: u16,
|
||||
) -> Result<(), Error> {
|
||||
if self.samples > 0 && loss > 0 {
|
||||
bail!("Lost {} RTP packets mid-stream", loss);
|
||||
}
|
||||
self.samples += 1;
|
||||
if self.next_pos != Some(pos) {
|
||||
self.chunks.push((self.samples, pos));
|
||||
}
|
||||
self.sizes.push(size);
|
||||
self.next_pos = Some(pos + size);
|
||||
if let Some(last_pts) = self.last_pts.replace(timestamp.timestamp()) {
|
||||
let duration = timestamp.timestamp().checked_sub(last_pts).unwrap();
|
||||
self.tot_duration += u64::try_from(duration).unwrap();
|
||||
let duration = u32::try_from(duration)?;
|
||||
match self.durations.last_mut() {
|
||||
Some((s, d)) if *d == duration => *s += 1,
|
||||
_ => self.durations.push((1, duration)),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish(&mut self) {
|
||||
if self.last_pts.is_some() {
|
||||
self.durations.push((1, 0));
|
||||
}
|
||||
}
|
||||
|
||||
/// Estimates the sum of the variable-sized portions of the data.
|
||||
fn size_estimate(&self) -> usize {
|
||||
(self.durations.len() * 8) + // stts
|
||||
(self.chunks.len() * 12) + // stsc
|
||||
(self.sizes.len() * 4) + // stsz
|
||||
(self.chunks.len() * 4) // stco
|
||||
}
|
||||
|
||||
fn write_common_stbl_parts(&self, buf: &mut BytesMut) -> Result<(), Error> {
|
||||
// TODO: add an edit list so the video and audio tracks are in sync.
|
||||
write_box!(buf, b"stts", {
|
||||
buf.put_u32(0);
|
||||
buf.put_u32(u32::try_from(self.durations.len())?);
|
||||
for (samples, duration) in &self.durations {
|
||||
buf.put_u32(*samples);
|
||||
buf.put_u32(*duration);
|
||||
}
|
||||
});
|
||||
write_box!(buf, b"stsc", {
|
||||
buf.put_u32(0); // version
|
||||
buf.put_u32(u32::try_from(self.chunks.len())?);
|
||||
let mut prev_sample_number = 1;
|
||||
let mut chunk_number = 1;
|
||||
for &(sample_number, _pos) in &self.chunks[1..] {
|
||||
buf.put_u32(chunk_number);
|
||||
buf.put_u32(sample_number - prev_sample_number);
|
||||
buf.put_u32(1); // sample_description_index
|
||||
prev_sample_number = sample_number;
|
||||
chunk_number += 1;
|
||||
}
|
||||
if !self.chunks.is_empty() {
|
||||
buf.put_u32(chunk_number);
|
||||
buf.put_u32(self.samples + 1 - prev_sample_number);
|
||||
buf.put_u32(1); // sample_description_index
|
||||
}
|
||||
});
|
||||
write_box!(buf, b"stsz", {
|
||||
buf.put_u32(0); // version
|
||||
buf.put_u32(0); // sample_size
|
||||
buf.put_u32(u32::try_from(self.sizes.len())?);
|
||||
for s in &self.sizes {
|
||||
buf.put_u32(*s);
|
||||
}
|
||||
});
|
||||
write_box!(buf, b"stco", {
|
||||
buf.put_u32(0); // version
|
||||
buf.put_u32(u32::try_from(self.chunks.len())?); // entry_count
|
||||
for &(_sample_number, pos) in &self.chunks {
|
||||
buf.put_u32(pos);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: AsyncWrite + AsyncSeek + Send + Unpin> Mp4Writer<W> {
|
||||
pub async fn new(
|
||||
video_params: Option<VideoParameters>,
|
||||
audio_params: Option<AudioParameters>,
|
||||
mut inner: W,
|
||||
) -> Result<Self, Error> {
|
||||
let mut buf = BytesMut::new();
|
||||
write_box!(&mut buf, b"ftyp", {
|
||||
buf.extend_from_slice(&[
|
||||
b'i', b's', b'o', b'm', // major_brand
|
||||
0, 0, 0, 0, // minor_version
|
||||
b'i', b's', b'o', b'm', // compatible_brands[0]
|
||||
]);
|
||||
});
|
||||
buf.extend_from_slice(&b"\0\0\0\0mdat"[..]);
|
||||
let mdat_start = u32::try_from(buf.len())?;
|
||||
write_all_buf(&mut inner, &mut buf).await?;
|
||||
Ok(Mp4Writer {
|
||||
inner,
|
||||
video_params,
|
||||
audio_params,
|
||||
video_trak: TrakTracker::default(),
|
||||
audio_trak: TrakTracker::default(),
|
||||
video_sync_sample_nums: Vec::new(),
|
||||
mdat_start,
|
||||
mdat_pos: mdat_start,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn finish(mut self) -> Result<(), Error> {
|
||||
self.video_trak.finish();
|
||||
self.audio_trak.finish();
|
||||
let mut buf = BytesMut::with_capacity(
|
||||
1024 + self.video_trak.size_estimate()
|
||||
+ self.audio_trak.size_estimate()
|
||||
+ 4 * self.video_sync_sample_nums.len(),
|
||||
);
|
||||
write_box!(&mut buf, b"moov", {
|
||||
write_box!(&mut buf, b"mvhd", {
|
||||
buf.put_u32(1 << 24); // version
|
||||
buf.put_u64(0); // creation_time
|
||||
buf.put_u64(0); // modification_time
|
||||
buf.put_u32(90000); // timescale
|
||||
buf.put_u64(self.video_trak.tot_duration);
|
||||
buf.put_u32(0x00010000); // rate
|
||||
buf.put_u16(0x0100); // volume
|
||||
buf.put_u16(0); // reserved
|
||||
buf.put_u64(0); // reserved
|
||||
for v in &[0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000] {
|
||||
buf.put_u32(*v); // matrix
|
||||
}
|
||||
for _ in 0..6 {
|
||||
buf.put_u32(0); // pre_defined
|
||||
}
|
||||
buf.put_u32(2); // next_track_id
|
||||
});
|
||||
if let Some(p) = self.video_params.as_ref() {
|
||||
self.write_video_trak(&mut buf, p)?;
|
||||
}
|
||||
if let Some(p) = self.audio_params.as_ref() {
|
||||
self.write_audio_trak(&mut buf, p)?;
|
||||
}
|
||||
});
|
||||
write_all_buf(&mut self.inner, &mut buf.freeze()).await?;
|
||||
self.inner
|
||||
.seek(SeekFrom::Start(u64::from(self.mdat_start - 8)))
|
||||
.await?;
|
||||
self.inner
|
||||
.write_all(&u32::try_from(self.mdat_pos + 8 - self.mdat_start)?.to_be_bytes()[..])
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_video_trak(
|
||||
&self,
|
||||
buf: &mut BytesMut,
|
||||
parameters: &VideoParameters,
|
||||
) -> Result<(), Error> {
|
||||
write_box!(buf, b"trak", {
|
||||
write_box!(buf, b"tkhd", {
|
||||
buf.put_u32((1 << 24) | 7); // version, flags
|
||||
buf.put_u64(0); // creation_time
|
||||
buf.put_u64(0); // modification_time
|
||||
buf.put_u32(1); // track_id
|
||||
buf.put_u32(0); // reserved
|
||||
buf.put_u64(self.video_trak.tot_duration);
|
||||
buf.put_u64(0); // reserved
|
||||
buf.put_u16(0); // layer
|
||||
buf.put_u16(0); // alternate_group
|
||||
buf.put_u16(0); // volume
|
||||
buf.put_u16(0); // reserved
|
||||
for v in &[0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000] {
|
||||
buf.put_u32(*v); // matrix
|
||||
}
|
||||
let dims = self
|
||||
.video_params
|
||||
.as_ref()
|
||||
.map(VideoParameters::pixel_dimensions)
|
||||
.unwrap_or((0, 0));
|
||||
let width = u32::from(u16::try_from(dims.0)?) << 16;
|
||||
let height = u32::from(u16::try_from(dims.1)?) << 16;
|
||||
buf.put_u32(width);
|
||||
buf.put_u32(height);
|
||||
});
|
||||
write_box!(buf, b"mdia", {
|
||||
write_box!(buf, b"mdhd", {
|
||||
buf.put_u32(1 << 24); // version
|
||||
buf.put_u64(0); // creation_time
|
||||
buf.put_u64(0); // modification_time
|
||||
buf.put_u32(90000); // timebase
|
||||
buf.put_u64(self.video_trak.tot_duration);
|
||||
buf.put_u32(0x55c40000); // language=und + pre-defined
|
||||
});
|
||||
write_box!(buf, b"hdlr", {
|
||||
buf.extend_from_slice(&[
|
||||
0x00, 0x00, 0x00, 0x00, // version + flags
|
||||
0x00, 0x00, 0x00, 0x00, // pre_defined
|
||||
b'v', b'i', b'd', b'e', // handler = vide
|
||||
0x00, 0x00, 0x00, 0x00, // reserved[0]
|
||||
0x00, 0x00, 0x00, 0x00, // reserved[1]
|
||||
0x00, 0x00, 0x00, 0x00, // reserved[2]
|
||||
0x00, // name, zero-terminated (empty)
|
||||
]);
|
||||
});
|
||||
write_box!(buf, b"minf", {
|
||||
write_box!(buf, b"vmhd", {
|
||||
buf.put_u32(1);
|
||||
buf.put_u64(0);
|
||||
});
|
||||
write_box!(buf, b"dinf", {
|
||||
write_box!(buf, b"dref", {
|
||||
buf.put_u32(0);
|
||||
buf.put_u32(1); // entry_count
|
||||
write_box!(buf, b"url ", {
|
||||
buf.put_u32(1); // version, flags=self-contained
|
||||
});
|
||||
});
|
||||
});
|
||||
write_box!(buf, b"stbl", {
|
||||
write_box!(buf, b"stsd", {
|
||||
buf.put_u32(0); // version
|
||||
buf.put_u32(1); // entry_count
|
||||
self.write_video_sample_entry(buf, parameters)?;
|
||||
});
|
||||
self.video_trak.write_common_stbl_parts(buf)?;
|
||||
write_box!(buf, b"stss", {
|
||||
buf.put_u32(0); // version
|
||||
buf.put_u32(u32::try_from(self.video_sync_sample_nums.len())?);
|
||||
for n in &self.video_sync_sample_nums {
|
||||
buf.put_u32(*n);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_audio_trak(
|
||||
&self,
|
||||
buf: &mut BytesMut,
|
||||
parameters: &AudioParameters,
|
||||
) -> Result<(), Error> {
|
||||
write_box!(buf, b"trak", {
|
||||
write_box!(buf, b"tkhd", {
|
||||
buf.put_u32((1 << 24) | 7); // version, flags
|
||||
buf.put_u64(0); // creation_time
|
||||
buf.put_u64(0); // modification_time
|
||||
buf.put_u32(2); // track_id
|
||||
buf.put_u32(0); // reserved
|
||||
buf.put_u64(self.audio_trak.tot_duration);
|
||||
buf.put_u64(0); // reserved
|
||||
buf.put_u16(0); // layer
|
||||
buf.put_u16(0); // alternate_group
|
||||
buf.put_u16(0); // volume
|
||||
buf.put_u16(0); // reserved
|
||||
for v in &[0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000] {
|
||||
buf.put_u32(*v); // matrix
|
||||
}
|
||||
buf.put_u32(0); // width
|
||||
buf.put_u32(0); // height
|
||||
});
|
||||
write_box!(buf, b"mdia", {
|
||||
write_box!(buf, b"mdhd", {
|
||||
buf.put_u32(1 << 24); // version
|
||||
buf.put_u64(0); // creation_time
|
||||
buf.put_u64(0); // modification_time
|
||||
buf.put_u32(parameters.clock_rate());
|
||||
buf.put_u64(self.audio_trak.tot_duration);
|
||||
buf.put_u32(0x55c40000); // language=und + pre-defined
|
||||
});
|
||||
write_box!(buf, b"hdlr", {
|
||||
buf.extend_from_slice(&[
|
||||
0x00, 0x00, 0x00, 0x00, // version + flags
|
||||
0x00, 0x00, 0x00, 0x00, // pre_defined
|
||||
b's', b'o', b'u', b'n', // handler = soun
|
||||
0x00, 0x00, 0x00, 0x00, // reserved[0]
|
||||
0x00, 0x00, 0x00, 0x00, // reserved[1]
|
||||
0x00, 0x00, 0x00, 0x00, // reserved[2]
|
||||
0x00, // name, zero-terminated (empty)
|
||||
]);
|
||||
});
|
||||
write_box!(buf, b"minf", {
|
||||
write_box!(buf, b"smhd", {
|
||||
buf.extend_from_slice(&[
|
||||
0x00, 0x00, 0x00, 0x00, // version + flags
|
||||
0x00, 0x00, // balance
|
||||
0x00, 0x00, // reserved
|
||||
]);
|
||||
});
|
||||
write_box!(buf, b"dinf", {
|
||||
write_box!(buf, b"dref", {
|
||||
buf.put_u32(0);
|
||||
buf.put_u32(1); // entry_count
|
||||
write_box!(buf, b"url ", {
|
||||
buf.put_u32(1); // version, flags=self-contained
|
||||
});
|
||||
});
|
||||
});
|
||||
write_box!(buf, b"stbl", {
|
||||
write_box!(buf, b"stsd", {
|
||||
buf.put_u32(0); // version
|
||||
buf.put_u32(1); // entry_count
|
||||
buf.extend_from_slice(¶meters.sample_entry().unwrap()[..]);
|
||||
});
|
||||
self.audio_trak.write_common_stbl_parts(buf)?;
|
||||
|
||||
// AAC requires two samples (really, each is a set of 960 or 1024 samples)
|
||||
// to decode accurately. See
|
||||
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFAppenG/QTFFAppenG.html .
|
||||
write_box!(buf, b"sgpd", {
|
||||
// BMFF section 8.9.3: SampleGroupDescriptionBox
|
||||
buf.put_u32(0); // version
|
||||
buf.extend_from_slice(b"roll"); // grouping type
|
||||
buf.put_u32(1); // entry_count
|
||||
// BMFF section 10.1: AudioRollRecoveryEntry
|
||||
buf.put_i16(-1); // roll_distance
|
||||
});
|
||||
write_box!(buf, b"sbgp", {
|
||||
// BMFF section 8.9.2: SampleToGroupBox
|
||||
buf.put_u32(0); // version
|
||||
buf.extend_from_slice(b"roll"); // grouping type
|
||||
buf.put_u32(1); // entry_count
|
||||
buf.put_u32(self.audio_trak.samples);
|
||||
buf.put_u32(1); // group_description_index
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_video_sample_entry(
|
||||
&self,
|
||||
buf: &mut BytesMut,
|
||||
parameters: &VideoParameters,
|
||||
) -> Result<(), Error> {
|
||||
// TODO: this should move to client::VideoParameters::sample_entry() or some such.
|
||||
write_box!(buf, b"avc1", {
|
||||
buf.put_u32(0);
|
||||
buf.put_u32(1); // data_reference_index = 1
|
||||
buf.extend_from_slice(&[0; 16]);
|
||||
buf.put_u16(u16::try_from(parameters.pixel_dimensions().0)?);
|
||||
buf.put_u16(u16::try_from(parameters.pixel_dimensions().1)?);
|
||||
buf.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
|
||||
]);
|
||||
write_box!(buf, b"avcC", {
|
||||
buf.extend_from_slice(parameters.extra_data());
|
||||
});
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn video(&mut self, mut frame: retina::codec::VideoFrame) -> Result<(), failure::Error> {
|
||||
println!(
|
||||
"{}: {}-byte video frame",
|
||||
&frame.timestamp,
|
||||
frame.remaining()
|
||||
);
|
||||
if let Some(ref p) = frame.new_parameters {
|
||||
bail!("parameters change unimplemented. new parameters: {:#?}", p);
|
||||
}
|
||||
let size = u32::try_from(frame.remaining())?;
|
||||
self.video_trak
|
||||
.add_sample(self.mdat_pos, size, frame.timestamp, frame.loss)?;
|
||||
self.mdat_pos = self
|
||||
.mdat_pos
|
||||
.checked_add(size)
|
||||
.ok_or_else(|| format_err!("mdat_pos overflow"))?;
|
||||
if frame.is_random_access_point {
|
||||
self.video_sync_sample_nums
|
||||
.push(u32::try_from(self.video_trak.samples)?);
|
||||
}
|
||||
write_all_buf(&mut self.inner, &mut frame).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn audio(&mut self, mut frame: retina::codec::AudioFrame) -> Result<(), failure::Error> {
|
||||
println!(
|
||||
"{}: {}-byte audio frame",
|
||||
&frame.timestamp,
|
||||
frame.data.remaining()
|
||||
);
|
||||
let size = u32::try_from(frame.data.remaining())?;
|
||||
self.audio_trak
|
||||
.add_sample(self.mdat_pos, size, frame.timestamp, frame.loss)?;
|
||||
self.mdat_pos = self
|
||||
.mdat_pos
|
||||
.checked_add(size)
|
||||
.ok_or_else(|| format_err!("mdat_pos overflow"))?;
|
||||
write_all_buf(&mut self.inner, &mut frame.data).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(opts: Opts) -> Result<(), Error> {
|
||||
let creds = super::creds(opts.src.username, opts.src.password);
|
||||
let stop = tokio::signal::ctrl_c();
|
||||
let mut session = retina::client::Session::describe(opts.src.url, creds).await?;
|
||||
let video_stream = if !opts.no_video {
|
||||
session
|
||||
.streams()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(i, s)| match s.parameters() {
|
||||
Some(retina::codec::Parameters::Video(v)) => Some((i, v.clone())),
|
||||
_ => None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some((i, _)) = video_stream {
|
||||
session.setup(i).await?;
|
||||
}
|
||||
let audio_stream = if !opts.no_audio {
|
||||
session
|
||||
.streams()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(i, s)| match s.parameters() {
|
||||
Some(retina::codec::Parameters::Audio(a)) => Some((i, a.clone())),
|
||||
_ => None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some((i, _)) = audio_stream {
|
||||
session.setup(i).await?;
|
||||
}
|
||||
let session = session
|
||||
.play(
|
||||
retina::client::PlayPolicy::default()
|
||||
.initial_timestamp(opts.initial_timestamp)
|
||||
.enforce_timestamps_with_max_jump_secs(NonZeroU32::new(10).unwrap()),
|
||||
)
|
||||
.await?
|
||||
.demuxed()?;
|
||||
|
||||
// Read RTP data.
|
||||
let out = tokio::fs::File::create(opts.out).await?;
|
||||
let mut mp4 = Mp4Writer::new(
|
||||
video_stream.map(|(_, p)| p),
|
||||
audio_stream.map(|(_, p)| p),
|
||||
out,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tokio::pin!(session);
|
||||
tokio::pin!(stop);
|
||||
loop {
|
||||
tokio::select! {
|
||||
pkt = session.next() => {
|
||||
match pkt.ok_or_else(|| format_err!("EOF"))?? {
|
||||
CodecItem::VideoFrame(f) => mp4.video(f).await?,
|
||||
CodecItem::AudioFrame(f) => mp4.audio(f).await?,
|
||||
_ => continue,
|
||||
};
|
||||
},
|
||||
_ = &mut stop => {
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
info!("Stopping");
|
||||
mp4.finish().await?;
|
||||
Ok(())
|
||||
}
|
152
src/client/channel_mapping.rs
Normal file
152
src/client/channel_mapping.rs
Normal file
@ -0,0 +1,152 @@
|
||||
// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
//! Track RTSP interleaved channel->stream assignments.
|
||||
|
||||
use std::num::NonZeroU8;
|
||||
|
||||
use failure::{bail, Error};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ChannelType {
|
||||
Rtp,
|
||||
Rtcp,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ChannelMapping {
|
||||
pub stream_i: usize,
|
||||
pub channel_type: ChannelType,
|
||||
}
|
||||
|
||||
/// Mapping of the 256 possible RTSP interleaved channels to stream indices and
|
||||
/// RTP/RTCP. Assumptions:
|
||||
/// * We only need to support 255 possible streams in a presentation. If
|
||||
/// there are more than 128, we couldn't actually stream them all at once
|
||||
/// anyway with one RTP and one RTCP channel per stream.
|
||||
/// * We'll always assign even channels numbers as RTP and their odd
|
||||
/// successors as RTCP for the same stream. This seems reasonable given
|
||||
/// that there is no clear way to assign a single channel in the RTSP spec.
|
||||
/// [RFC 2326 section 10.12](https://tools.ietf.org/html/rfc2326#section-10.12)
|
||||
/// says that `interleaved=n` also assigns channel `n+1`, and it's ambiguous
|
||||
/// what `interleaved=n-m` does when `m > n+1` (section 10.12 suggests it
|
||||
/// assigns only `n` and `m`; section 12.39 the suggests full range `[n,
|
||||
/// m]`) or when `n==m`. We'll get into trouble if an RTSP server insists on
|
||||
/// specifying an odd `n`, but that just seems obstinate.
|
||||
/// These assumptions let us keep the full mapping with little space and an
|
||||
/// efficient lookup operation.
|
||||
#[derive(Default)]
|
||||
pub struct ChannelMappings(smallvec::SmallVec<[Option<NonZeroU8>; 16]>);
|
||||
|
||||
impl ChannelMappings {
|
||||
/// Returns the next unassigned even channel id, or errors.
|
||||
pub fn next_unassigned(&self) -> Result<u8, Error> {
|
||||
if let Some(i) = self.0.iter().position(Option::is_none) {
|
||||
return Ok((i as u8) << 1);
|
||||
}
|
||||
if self.0.len() < 128 {
|
||||
return Ok((self.0.len() as u8) << 1);
|
||||
}
|
||||
bail!("all RTSP channels have been assigned");
|
||||
}
|
||||
|
||||
/// Assigns an even channel id (to RTP) and its odd successor (to RTCP) or errors.
|
||||
pub fn assign(&mut self, channel_id: u8, stream_i: usize) -> Result<(), Error> {
|
||||
if (channel_id & 1) != 0 {
|
||||
bail!("Can't assign odd channel id {}", channel_id);
|
||||
}
|
||||
if stream_i >= 255 {
|
||||
bail!(
|
||||
"Can't assign channel to stream id {} because it's >= 255",
|
||||
stream_i
|
||||
);
|
||||
}
|
||||
let i = usize::from(channel_id >> 1);
|
||||
if i >= self.0.len() {
|
||||
self.0.resize(i + 1, None);
|
||||
}
|
||||
let c = &mut self.0[i];
|
||||
if let Some(c) = c {
|
||||
bail!(
|
||||
"Channel id {} is already assigned to stream {}; won't reassign to stream {}",
|
||||
channel_id,
|
||||
c.get() - 1,
|
||||
channel_id
|
||||
);
|
||||
}
|
||||
*c = Some(NonZeroU8::new((stream_i + 1) as u8).expect("[0, 255) + 1 is non-zero"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Looks up a channel id's mapping.
|
||||
pub fn lookup(&self, channel_id: u8) -> Option<ChannelMapping> {
|
||||
let i = usize::from(channel_id >> 1);
|
||||
if i >= self.0.len() {
|
||||
return None;
|
||||
}
|
||||
self.0[i].map(|c| ChannelMapping {
|
||||
stream_i: usize::from(c.get() - 1),
|
||||
channel_type: match (channel_id & 1) != 0 {
|
||||
false => ChannelType::Rtp,
|
||||
true => ChannelType::Rtcp,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ChannelMappings {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_map()
|
||||
.entries(self.0.iter().enumerate().filter_map(|(i, v)| {
|
||||
v.map(|v| (format!("{}-{}", i << 1, (i << 1) + 1), v.get() - 1))
|
||||
}))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ChannelMapping, ChannelType};
|
||||
|
||||
#[test]
|
||||
fn channel_mappings() {
|
||||
let mut mappings = super::ChannelMappings::default();
|
||||
assert_eq!(mappings.next_unassigned().unwrap(), 0);
|
||||
assert_eq!(mappings.lookup(0), None);
|
||||
mappings.assign(0, 42).unwrap();
|
||||
mappings.assign(0, 43).unwrap_err();
|
||||
mappings.assign(1, 43).unwrap_err();
|
||||
assert_eq!(
|
||||
mappings.lookup(0),
|
||||
Some(ChannelMapping {
|
||||
stream_i: 42,
|
||||
channel_type: ChannelType::Rtp,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
mappings.lookup(1),
|
||||
Some(ChannelMapping {
|
||||
stream_i: 42,
|
||||
channel_type: ChannelType::Rtcp,
|
||||
})
|
||||
);
|
||||
assert_eq!(mappings.next_unassigned().unwrap(), 2);
|
||||
mappings.assign(9, 26).unwrap_err();
|
||||
mappings.assign(8, 26).unwrap();
|
||||
assert_eq!(
|
||||
mappings.lookup(8),
|
||||
Some(ChannelMapping {
|
||||
stream_i: 26,
|
||||
channel_type: ChannelType::Rtp,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
mappings.lookup(9),
|
||||
Some(ChannelMapping {
|
||||
stream_i: 26,
|
||||
channel_type: ChannelType::Rtcp,
|
||||
})
|
||||
);
|
||||
assert_eq!(mappings.next_unassigned().unwrap(), 2);
|
||||
}
|
||||
}
|
810
src/client/mod.rs
Normal file
810
src/client/mod.rs
Normal file
@ -0,0 +1,810 @@
|
||||
// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use std::num::NonZeroU32;
|
||||
use std::{borrow::Cow, fmt::Debug, num::NonZeroU16, pin::Pin};
|
||||
|
||||
use self::channel_mapping::*;
|
||||
use self::timeline::Timeline;
|
||||
use async_stream::try_stream;
|
||||
use bytes::Bytes;
|
||||
use failure::{bail, format_err, Error};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use log::{debug, trace, warn};
|
||||
use pin_project::pin_project;
|
||||
use sdp::session_description::SessionDescription;
|
||||
use tokio::pin;
|
||||
use tokio_util::codec::Framed;
|
||||
use url::Url;
|
||||
|
||||
use crate::{codec::CodecItem, Context};
|
||||
|
||||
mod channel_mapping;
|
||||
mod parse;
|
||||
pub mod rtp;
|
||||
mod timeline;
|
||||
|
||||
/// Duration between keepalive RTSP requests during [Playing] state.
|
||||
pub const KEEPALIVE_DURATION: std::time::Duration = std::time::Duration::from_secs(30);
|
||||
|
||||
/// Policy for handling the `rtptime` parameter normally seem in the `RTP-Info` header.
|
||||
/// This parameter is used to map each stream's RTP timestamp to NPT ("normal play time"),
|
||||
/// allowing multiple streams to be played in sync.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum InitialTimestampPolicy {
|
||||
/// Default policy: currently `Require` when playing multiple streams,
|
||||
/// `Ignore` otherwise.
|
||||
Default,
|
||||
|
||||
/// Require the `rtptime` parameter be present and use it to set NPT. Use
|
||||
/// when accurate multi-stream NPT is important.
|
||||
Require,
|
||||
|
||||
/// Ignore the `rtptime` parameter and assume the first received packet for
|
||||
/// each stream is at NPT 0. Use with cameras that are known to set
|
||||
/// `rtptime` incorrectly.
|
||||
Ignore,
|
||||
|
||||
/// Use the `rtptime` parameter when playing multiple streams if it's
|
||||
/// specified for all of them; otherwise assume the first received packet
|
||||
/// for each stream is at NPT 0.
|
||||
Permissive,
|
||||
}
|
||||
|
||||
impl Default for InitialTimestampPolicy {
|
||||
fn default() -> Self {
|
||||
InitialTimestampPolicy::Default
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for InitialTimestampPolicy {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
InitialTimestampPolicy::Default => f.pad("default"),
|
||||
InitialTimestampPolicy::Require => f.pad("require"),
|
||||
InitialTimestampPolicy::Ignore => f.pad("ignore"),
|
||||
InitialTimestampPolicy::Permissive => f.pad("permissive"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for InitialTimestampPolicy {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s {
|
||||
"default" => InitialTimestampPolicy::Default,
|
||||
"require" => InitialTimestampPolicy::Require,
|
||||
"ignore" => InitialTimestampPolicy::Ignore,
|
||||
"permissive" => InitialTimestampPolicy::Permissive,
|
||||
_ => bail!("Initial timestamp mode {:?} not understood", s),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Policy decisions to make on `PLAY`.
|
||||
///
|
||||
/// These are mostly adjustments for non-compliant server implementations.
|
||||
#[derive(Default)]
|
||||
pub struct PlayPolicy {
|
||||
initial_timestamp: InitialTimestampPolicy,
|
||||
ignore_zero_seq: bool,
|
||||
enforce_timestamps_with_max_jump_secs: Option<NonZeroU32>,
|
||||
}
|
||||
|
||||
impl PlayPolicy {
|
||||
pub fn initial_timestamp(self, initial_timestamp: InitialTimestampPolicy) -> Self {
|
||||
Self {
|
||||
initial_timestamp,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// If the `RTP-Time` specifies `seq=0`, ignore it. Some cameras set this value then start
|
||||
/// the stream with something dramatically different. (Eg the Hikvision DS-2CD2032-I on its
|
||||
/// metadata stream; the other streams are fine.)
|
||||
pub fn ignore_zero_seq(self, ignore_zero_seq: bool) -> Self {
|
||||
Self {
|
||||
ignore_zero_seq,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Enforces that timestamps are non-decreasing and jump forward by no more
|
||||
/// than the given number of seconds.
|
||||
///
|
||||
/// By default, no enforcement is done, and computed [crate::Timestamp]
|
||||
/// values will go backward if subsequent 32-bit RTP timestamps differ by
|
||||
/// more than `i32::MAX`.
|
||||
pub fn enforce_timestamps_with_max_jump_secs(self, secs: NonZeroU32) -> Self {
|
||||
Self {
|
||||
enforce_timestamps_with_max_jump_secs: Some(secs),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Presentation {
|
||||
pub streams: Vec<Stream>,
|
||||
base_url: Url,
|
||||
pub control: Url,
|
||||
pub accept_dynamic_rate: bool,
|
||||
sdp: SessionDescription,
|
||||
}
|
||||
|
||||
/// Information about a stream offered within a presentation.
|
||||
/// Currently if multiple formats are offered, this only describes the first.
|
||||
#[derive(Debug)]
|
||||
pub struct Stream {
|
||||
/// Media type, as specified in the [IANA SDP parameters media
|
||||
/// registry](https://www.iana.org/assignments/sdp-parameters/sdp-parameters.xhtml#sdp-parameters-1).
|
||||
pub media: String,
|
||||
|
||||
/// An encoding name, as specified in the [IANA media type
|
||||
/// registry](https://www.iana.org/assignments/media-types/media-types.xhtml), with
|
||||
/// ASCII characters in lowercase.
|
||||
///
|
||||
/// Commonly used but not specified in that registry: the ONVIF types
|
||||
/// claimed in the
|
||||
/// [ONVIF Streaming Spec](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf):
|
||||
/// * `vnd.onvif.metadata`
|
||||
/// * `vnd.onvif.metadata.gzip`,
|
||||
/// * `vnd.onvif.metadata.exi.onvif`
|
||||
/// * `vnd.onvif.metadata.exi.ext`
|
||||
pub encoding_name: String,
|
||||
|
||||
/// RTP payload type.
|
||||
/// See the [registry](https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml#rtp-parameters-1).
|
||||
/// It's common to use one of the dynamically assigned values, 96–127.
|
||||
pub rtp_payload_type: u8,
|
||||
|
||||
/// RTP clock rate, in Hz.
|
||||
pub clock_rate: u32,
|
||||
|
||||
/// Number of audio channels, if applicable (`media` is `audio`) and known.
|
||||
pub channels: Option<NonZeroU16>,
|
||||
|
||||
depacketizer: Result<crate::codec::Depacketizer, Error>,
|
||||
|
||||
/// The specified control URL.
|
||||
/// This is needed with multiple streams to send `SETUP` requests and
|
||||
/// interpret the `PLAY` response's `RTP-Info` header.
|
||||
/// [RFC 2326 section C.3](https://datatracker.ietf.org/doc/html/rfc2326#appendix-C.3)
|
||||
/// says the server is allowed to omit it when there is only a single stream.
|
||||
pub control: Option<Url>,
|
||||
|
||||
/// Some buggy cameras expect the base URL to be interpreted as if it had an
|
||||
/// implicit trailing slash. (This is approximately what ffmpeg 4.3.1 does
|
||||
/// when the base URL has a query string.) If `RTP-Info` matching fails, try
|
||||
/// again with this URL.
|
||||
alt_control: Option<Url>,
|
||||
|
||||
state: StreamState,
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
/// Returns the parameters for this stream.
|
||||
///
|
||||
/// Returns `None` on unknown codecs, bad parameters, or if parameters aren't specified
|
||||
/// via SDP. Some codecs allow parameters to be specified in-band instead.
|
||||
pub fn parameters(&self) -> Option<&crate::codec::Parameters> {
|
||||
self.depacketizer.as_ref().ok().and_then(|d| d.parameters())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum StreamState {
|
||||
/// Uninitialized; no `SETUP` has yet been sent.
|
||||
Uninit,
|
||||
|
||||
/// `SETUP` reply has been received.
|
||||
Init(StreamStateInit),
|
||||
|
||||
/// `PLAY` reply has been received.
|
||||
Playing {
|
||||
timeline: Timeline,
|
||||
rtp_handler: rtp::StrictSequenceChecker,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
struct StreamStateInit {
|
||||
/// The RTP synchronization source (SSRC), as defined in
|
||||
/// [RFC 3550](https://tools.ietf.org/html/rfc3550). This is normally
|
||||
/// supplied in the `SETUP` response's `Transport` header. Reolink cameras
|
||||
/// instead supply it in the `PLAY` response's `RTP-Info` header.
|
||||
ssrc: Option<u32>,
|
||||
|
||||
/// The initial RTP sequence number, as specified in the `PLAY` response's
|
||||
/// `RTP-Info` header. This field is only used during the `play()` call
|
||||
/// itself; by the time it returns, the stream will be in state `Playing`.
|
||||
initial_seq: Option<u16>,
|
||||
|
||||
/// The initial RTP timestamp, as specified in the `PLAY` response's
|
||||
/// `RTP-Info` header. This field is only used during the `play()` call
|
||||
/// itself; by the time it returns, the stream will be in state `Playing`.
|
||||
initial_rtptime: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Credentials {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// Marker trait for the state of a [Session].
|
||||
/// This doesn't closely match [RFC 2326
|
||||
/// A.1](https://tools.ietf.org/html/rfc2326#appendix-A.1). In practice, we've
|
||||
/// found that cheap IP cameras are more restrictive than RTSP suggests. Eg, a
|
||||
/// `DESCRIBE` changes the connection's state such that another one will fail,
|
||||
/// before assigning a session id. Thus [Session] represents something more like
|
||||
/// an RTSP connection than an RTSP session.
|
||||
pub trait State {}
|
||||
|
||||
/// Initial state after a `DESCRIBE`.
|
||||
/// One or more `SETUP`s may have also been issued, in which case a `session_id`
|
||||
/// will be assigned.
|
||||
pub struct Described {
|
||||
presentation: Presentation,
|
||||
session_id: Option<String>,
|
||||
channels: ChannelMappings,
|
||||
}
|
||||
impl State for Described {}
|
||||
|
||||
/// State after a `PLAY`.
|
||||
#[pin_project(project = PlayingProj)]
|
||||
pub struct Playing {
|
||||
presentation: Presentation,
|
||||
session_id: String,
|
||||
channels: ChannelMappings,
|
||||
pending_keepalive_cseq: Option<u32>,
|
||||
|
||||
#[pin]
|
||||
keepalive_timer: tokio::time::Sleep,
|
||||
}
|
||||
impl State for Playing {}
|
||||
|
||||
/// The raw connection, without tracking session state.
|
||||
struct RtspConnection {
|
||||
creds: Option<Credentials>,
|
||||
requested_auth: Option<digest_auth::WwwAuthenticateHeader>,
|
||||
stream: Framed<tokio::net::TcpStream, crate::Codec>,
|
||||
user_agent: String,
|
||||
|
||||
/// The next `CSeq` header value to use when sending an RTSP request.
|
||||
next_cseq: u32,
|
||||
}
|
||||
|
||||
/// An RTSP session, or a connection that may be used in a proscriptive way.
|
||||
/// See discussion at [State].
|
||||
#[pin_project]
|
||||
pub struct Session<S: State> {
|
||||
conn: RtspConnection,
|
||||
|
||||
#[pin]
|
||||
state: S,
|
||||
}
|
||||
|
||||
impl RtspConnection {
|
||||
async fn connect(url: &Url, creds: Option<Credentials>) -> Result<Self, Error> {
|
||||
if url.scheme() != "rtsp" {
|
||||
bail!("Only rtsp urls supported (no rtsps yet)");
|
||||
}
|
||||
if url.username() != "" || url.password().is_some() {
|
||||
// Url apparently doesn't even have a way to clear the credentials,
|
||||
// so this has to be an error.
|
||||
bail!("URL must not contain credentials");
|
||||
}
|
||||
let host = url
|
||||
.host_str()
|
||||
.ok_or_else(|| format_err!("Must specify host in rtsp url {}", &url))?;
|
||||
let port = url.port().unwrap_or(554);
|
||||
let stream = tokio::net::TcpStream::connect((host, port)).await?;
|
||||
let conn_established_wall = time::get_time();
|
||||
let conn_established = std::time::Instant::now();
|
||||
let conn_local_addr = stream.local_addr()?;
|
||||
let conn_peer_addr = stream.peer_addr()?;
|
||||
let stream = Framed::new(
|
||||
stream,
|
||||
crate::Codec {
|
||||
ctx: crate::Context {
|
||||
conn_established_wall,
|
||||
conn_established,
|
||||
conn_local_addr,
|
||||
conn_peer_addr,
|
||||
msg_pos: 0,
|
||||
msg_received_wall: conn_established_wall,
|
||||
msg_received: conn_established,
|
||||
},
|
||||
},
|
||||
);
|
||||
Ok(Self {
|
||||
creds,
|
||||
requested_auth: None,
|
||||
stream,
|
||||
user_agent: "moonfire-rtsp test".to_string(),
|
||||
next_cseq: 1,
|
||||
})
|
||||
}
|
||||
|
||||
/// Sends a request and expects the next message from the peer to be its response.
|
||||
/// Takes care of authorization and `CSeq`. Returns `Error` if not successful.
|
||||
async fn send(
|
||||
&mut self,
|
||||
req: &mut rtsp_types::Request<Bytes>,
|
||||
) -> Result<rtsp_types::Response<Bytes>, Error> {
|
||||
loop {
|
||||
let cseq = self.send_nowait(req).await?;
|
||||
let msg = self
|
||||
.stream
|
||||
.next()
|
||||
.await
|
||||
.ok_or_else(|| format_err!("unexpected EOF while waiting for reply"))??;
|
||||
let resp = match msg.msg {
|
||||
rtsp_types::Message::Response(r) => r,
|
||||
o => bail!("Unexpected RTSP message {:?}", &o),
|
||||
};
|
||||
if parse::get_cseq(&resp) != Some(cseq) {
|
||||
bail!(
|
||||
"didn't get expected CSeq {:?} on {:?} at {:#?}",
|
||||
&cseq,
|
||||
&resp,
|
||||
&msg.ctx
|
||||
);
|
||||
}
|
||||
if resp.status() == rtsp_types::StatusCode::Unauthorized {
|
||||
if self.requested_auth.is_some() {
|
||||
bail!(
|
||||
"Received Unauthorized after trying digest auth at {:#?}",
|
||||
&msg.ctx
|
||||
);
|
||||
}
|
||||
let www_authenticate = match resp.header(&rtsp_types::headers::WWW_AUTHENTICATE) {
|
||||
None => bail!(
|
||||
"Unauthorized without WWW-Authenticate header at {:#?}",
|
||||
&msg.ctx
|
||||
),
|
||||
Some(h) => h,
|
||||
};
|
||||
let www_authenticate = www_authenticate.as_str();
|
||||
if !www_authenticate.starts_with("Digest ") {
|
||||
bail!(
|
||||
"Non-digest authentication requested at {:#?}: {}",
|
||||
&msg.ctx,
|
||||
www_authenticate
|
||||
);
|
||||
}
|
||||
let www_authenticate = digest_auth::WwwAuthenticateHeader::parse(www_authenticate)?;
|
||||
self.requested_auth = Some(www_authenticate);
|
||||
continue;
|
||||
} else if !resp.status().is_success() {
|
||||
bail!(
|
||||
"RTSP {:?} request returned {} at {:#?}",
|
||||
req.method(),
|
||||
resp.status(),
|
||||
&msg.ctx
|
||||
);
|
||||
}
|
||||
return Ok(resp);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a request without waiting for a response, returning the `CSeq`.
|
||||
async fn send_nowait(&mut self, req: &mut rtsp_types::Request<Bytes>) -> Result<u32, Error> {
|
||||
let cseq = self.next_cseq;
|
||||
self.next_cseq += 1;
|
||||
match (self.requested_auth.as_mut(), self.creds.as_ref()) {
|
||||
(None, _) => {}
|
||||
(Some(auth), Some(creds)) => {
|
||||
let uri = req.request_uri().map(|u| u.as_str()).unwrap_or("*");
|
||||
let method = digest_auth::HttpMethod(Cow::Borrowed(req.method().into()));
|
||||
let ctx = digest_auth::AuthContext::new_with_method(
|
||||
&creds.username,
|
||||
&creds.password,
|
||||
uri,
|
||||
Option::<&'static [u8]>::None,
|
||||
method,
|
||||
);
|
||||
let authorization = auth.respond(&ctx)?.to_string();
|
||||
req.insert_header(rtsp_types::headers::AUTHORIZATION, authorization);
|
||||
}
|
||||
(Some(_), None) => bail!("Authentication required; no credentials supplied"),
|
||||
}
|
||||
req.insert_header(rtsp_types::headers::CSEQ, cseq.to_string());
|
||||
req.insert_header(rtsp_types::headers::USER_AGENT, self.user_agent.clone());
|
||||
self.stream
|
||||
.send(rtsp_types::Message::Request(req.clone()))
|
||||
.await?;
|
||||
Ok(cseq)
|
||||
}
|
||||
}
|
||||
|
||||
impl Session<Described> {
|
||||
pub async fn describe(url: Url, creds: Option<Credentials>) -> Result<Self, Error> {
|
||||
let mut conn = RtspConnection::connect(&url, creds).await?;
|
||||
let mut req =
|
||||
rtsp_types::Request::builder(rtsp_types::Method::Describe, rtsp_types::Version::V1_0)
|
||||
.header(rtsp_types::headers::ACCEPT, "application/sdp")
|
||||
.request_uri(url.clone())
|
||||
.build(Bytes::new());
|
||||
let response = conn.send(&mut req).await?;
|
||||
let presentation = parse::parse_describe(url, response)?;
|
||||
Ok(Session {
|
||||
conn,
|
||||
state: Described {
|
||||
presentation,
|
||||
session_id: None,
|
||||
channels: ChannelMappings::default(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn streams(&self) -> &[Stream] {
|
||||
&self.state.presentation.streams
|
||||
}
|
||||
|
||||
/// Sends a `SETUP` request for a stream.
|
||||
/// Note these can't reasonably be pipelined because subsequent requests
|
||||
/// are expected to adopt the previous response's `Session`. Likewise,
|
||||
/// the server may override the preferred interleaved channel id and it
|
||||
/// seems like a bad idea to try to assign more interleaved channels without
|
||||
/// inspect that first.
|
||||
///
|
||||
/// Panics if `stream_i >= self.streams().len()`.
|
||||
pub async fn setup(&mut self, stream_i: usize) -> Result<(), Error> {
|
||||
let stream = &mut self.state.presentation.streams[stream_i];
|
||||
if !matches!(stream.state, StreamState::Uninit) {
|
||||
bail!("stream already set up");
|
||||
}
|
||||
let proposed_channel_id = self.state.channels.next_unassigned()?;
|
||||
let url = stream
|
||||
.control
|
||||
.as_ref()
|
||||
.unwrap_or(&self.state.presentation.control)
|
||||
.clone();
|
||||
let mut req =
|
||||
rtsp_types::Request::builder(rtsp_types::Method::Setup, rtsp_types::Version::V1_0)
|
||||
.request_uri(url)
|
||||
.header(
|
||||
rtsp_types::headers::TRANSPORT,
|
||||
format!(
|
||||
"RTP/AVP/TCP;unicast;interleaved={}-{}",
|
||||
proposed_channel_id,
|
||||
proposed_channel_id + 1
|
||||
),
|
||||
)
|
||||
.header(crate::X_DYNAMIC_RATE.clone(), "1".to_owned());
|
||||
if let Some(ref s) = self.state.session_id {
|
||||
req = req.header(rtsp_types::headers::SESSION, s.clone());
|
||||
}
|
||||
let response = self.conn.send(&mut req.build(Bytes::new())).await?;
|
||||
debug!("SETUP response: {:#?}", &response);
|
||||
let response = parse::parse_setup(&response)?;
|
||||
match self.state.session_id.as_ref() {
|
||||
Some(old) if old != response.session_id => {
|
||||
bail!(
|
||||
"SETUP response changed session id from {:?} to {:?}",
|
||||
old,
|
||||
response.session_id
|
||||
);
|
||||
}
|
||||
Some(_) => {}
|
||||
None => self.state.session_id = Some(response.session_id.to_owned()),
|
||||
};
|
||||
self.state.channels.assign(response.channel_id, stream_i)?;
|
||||
stream.state = StreamState::Init(StreamStateInit {
|
||||
ssrc: response.ssrc,
|
||||
initial_seq: None,
|
||||
initial_rtptime: None,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a `PLAY` request for the entire presentation.
|
||||
/// The presentation must support aggregate control, as defined in [RFC 2326
|
||||
/// section 1.3](https://tools.ietf.org/html/rfc2326#section-1.3).
|
||||
pub async fn play(mut self, policy: PlayPolicy) -> Result<Session<Playing>, Error> {
|
||||
let session_id = self
|
||||
.state
|
||||
.session_id
|
||||
.take()
|
||||
.ok_or_else(|| format_err!("must SETUP before PLAY"))?;
|
||||
trace!("PLAY with channel mappings: {:#?}", &self.state.channels);
|
||||
let response = self
|
||||
.conn
|
||||
.send(
|
||||
&mut rtsp_types::Request::builder(
|
||||
rtsp_types::Method::Play,
|
||||
rtsp_types::Version::V1_0,
|
||||
)
|
||||
.request_uri(self.state.presentation.control.clone())
|
||||
.header(rtsp_types::headers::SESSION, session_id.clone())
|
||||
.header(rtsp_types::headers::RANGE, "npt=0.000-".to_owned())
|
||||
.build(Bytes::new()),
|
||||
)
|
||||
.await?;
|
||||
parse::parse_play(response, &mut self.state.presentation)?;
|
||||
|
||||
// Count how many streams have been setup (not how many are in the presentation).
|
||||
let setup_streams = self
|
||||
.state
|
||||
.presentation
|
||||
.streams
|
||||
.iter()
|
||||
.filter(|s| matches!(s.state, StreamState::Init(_)))
|
||||
.count();
|
||||
|
||||
let all_have_time = self
|
||||
.state
|
||||
.presentation
|
||||
.streams
|
||||
.iter()
|
||||
.all(|s| match s.state {
|
||||
StreamState::Init(StreamStateInit {
|
||||
initial_rtptime, ..
|
||||
}) => initial_rtptime.is_some(),
|
||||
_ => true,
|
||||
});
|
||||
|
||||
// Move all streams that have been set up from Init to Playing state. Check that required
|
||||
// parameters are present while doing so.
|
||||
for (i, s) in self.state.presentation.streams.iter_mut().enumerate() {
|
||||
match s.state {
|
||||
StreamState::Init(StreamStateInit {
|
||||
initial_rtptime,
|
||||
initial_seq,
|
||||
ssrc,
|
||||
..
|
||||
}) => {
|
||||
let initial_rtptime =
|
||||
match policy.initial_timestamp {
|
||||
InitialTimestampPolicy::Require | InitialTimestampPolicy::Default
|
||||
if setup_streams > 1 =>
|
||||
{
|
||||
if initial_rtptime.is_none() {
|
||||
bail!(
|
||||
"Expected rtptime on PLAY with mode {:?}, missing on stream \
|
||||
{} ({:?}). Consider setting initial timestamp mode \
|
||||
use-if-all-present.",
|
||||
policy.initial_timestamp, i, &s.control);
|
||||
}
|
||||
initial_rtptime
|
||||
}
|
||||
InitialTimestampPolicy::Permissive
|
||||
if setup_streams > 1 && all_have_time =>
|
||||
{
|
||||
initial_rtptime
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let initial_seq = match initial_seq {
|
||||
Some(0) if policy.ignore_zero_seq => {
|
||||
log::info!("Ignoring seq=0 on stream {}", i);
|
||||
None
|
||||
}
|
||||
o => o,
|
||||
};
|
||||
s.state = StreamState::Playing {
|
||||
timeline: Timeline::new(
|
||||
initial_rtptime,
|
||||
s.clock_rate,
|
||||
policy.enforce_timestamps_with_max_jump_secs,
|
||||
)?,
|
||||
rtp_handler: rtp::StrictSequenceChecker::new(ssrc, initial_seq),
|
||||
};
|
||||
}
|
||||
StreamState::Uninit => {}
|
||||
StreamState::Playing { .. } => unreachable!(),
|
||||
};
|
||||
}
|
||||
Ok(Session {
|
||||
conn: self.conn,
|
||||
state: Playing {
|
||||
presentation: self.state.presentation,
|
||||
session_id,
|
||||
channels: self.state.channels,
|
||||
keepalive_timer: tokio::time::sleep(KEEPALIVE_DURATION),
|
||||
pending_keepalive_cseq: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub enum PacketItem {
|
||||
RtpPacket(rtp::Packet),
|
||||
SenderReport(rtp::SenderReport),
|
||||
}
|
||||
|
||||
impl Session<Playing> {
|
||||
/// Returns a stream of packets.
|
||||
pub fn pkts(self) -> impl futures::Stream<Item = Result<PacketItem, Error>> {
|
||||
try_stream! {
|
||||
let self_ = self;
|
||||
tokio::pin!(self_);
|
||||
while let Some(pkt) = self_.as_mut().next().await {
|
||||
let pkt = pkt?;
|
||||
yield pkt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn demuxed(
|
||||
mut self,
|
||||
) -> Result<impl futures::Stream<Item = Result<CodecItem, Error>>, Error> {
|
||||
for s in &mut self.state.presentation.streams {
|
||||
if matches!(s.state, StreamState::Playing { .. }) {
|
||||
if let Err(ref mut e) = s.depacketizer {
|
||||
return Err(std::mem::replace(e, format_err!("(placeholder)")));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(try_stream! {
|
||||
let self_ = self;
|
||||
tokio::pin!(self_);
|
||||
while let Some(pkt) = self_.as_mut().next().await {
|
||||
let pkt = pkt?;
|
||||
match pkt {
|
||||
PacketItem::RtpPacket(p) => {
|
||||
let self_ = self_.as_mut().project();
|
||||
let state = self_.state.project();
|
||||
let depacketizer = match &mut state.presentation.streams[p.stream_id].depacketizer {
|
||||
Ok(d) => d,
|
||||
Err(_) => unreachable!("depacketizer was Ok"),
|
||||
};
|
||||
depacketizer.push(p)?;
|
||||
while let Some(demuxed) = depacketizer.pull()? {
|
||||
yield demuxed;
|
||||
}
|
||||
},
|
||||
PacketItem::SenderReport(p) => yield CodecItem::SenderReport(p),
|
||||
};
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the next packet, an error, or `None` on end of stream.
|
||||
/// Also manages keepalives; this will send them as necessary to keep the
|
||||
/// stream open, and fail when sending a following keepalive if the
|
||||
/// previous one was never acknowledged.
|
||||
///
|
||||
/// TODO: this should also pass along RTCP packets. There can be multiple
|
||||
/// RTCP packets per data message, so that will require keeping more state.
|
||||
async fn next(self: Pin<&mut Self>) -> Option<Result<PacketItem, Error>> {
|
||||
let this = self.project();
|
||||
let mut state = this.state.project();
|
||||
loop {
|
||||
tokio::select! {
|
||||
// Prefer receiving data to sending keepalives. If we can't keep
|
||||
// up with the server's data stream, it probably should drop us.
|
||||
biased;
|
||||
|
||||
msg = this.conn.stream.next() => {
|
||||
let msg = match msg {
|
||||
Some(Ok(m)) => m,
|
||||
Some(Err(e)) => return Some(Err(e)),
|
||||
None => return None,
|
||||
};
|
||||
match msg.msg {
|
||||
rtsp_types::Message::Data(data) => {
|
||||
match Session::handle_data(&mut state, msg.ctx, data) {
|
||||
Err(e) => return Some(Err(e)),
|
||||
Ok(Some(pkt)) => return Some(Ok(pkt)),
|
||||
Ok(None) => continue,
|
||||
};
|
||||
},
|
||||
rtsp_types::Message::Response(response) => {
|
||||
if let Err(e) = Session::handle_response(&mut state, response) {
|
||||
return Some(Err(e));
|
||||
}
|
||||
},
|
||||
rtsp_types::Message::Request(request) => {
|
||||
warn!("Received RTSP request in Playing state. Responding unimplemented.\n{:#?}",
|
||||
request);
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
() = &mut state.keepalive_timer => {
|
||||
// TODO: deadlock possibility. Once we decide to send a
|
||||
// keepalive, we don't try receiving anything until the
|
||||
// keepalive is fully sent. The server might similarly be
|
||||
// stubbornly trying to send before receiving. If all the
|
||||
// socket buffers are full, deadlock can result.
|
||||
//
|
||||
// This is really unlikely right now when all we send are
|
||||
// keepalives, which are probably much smaller than our send
|
||||
// buffer. But if we start supporting ONVIF backchannel, it
|
||||
// will become more of a concern.
|
||||
if let Err(e) = Session::handle_keepalive_timer(this.conn, &mut state).await {
|
||||
return Some(Err(e));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_keepalive_timer(
|
||||
conn: &mut RtspConnection,
|
||||
state: &mut PlayingProj<'_>,
|
||||
) -> Result<(), Error> {
|
||||
// Check on the previous keepalive request.
|
||||
if let Some(cseq) = state.pending_keepalive_cseq {
|
||||
bail!(
|
||||
"Server failed to respond to keepalive {} within {:?}",
|
||||
cseq,
|
||||
KEEPALIVE_DURATION
|
||||
);
|
||||
}
|
||||
|
||||
// Send a new one and reset the timer.
|
||||
*state.pending_keepalive_cseq = Some(
|
||||
conn.send_nowait(
|
||||
&mut rtsp_types::Request::builder(
|
||||
rtsp_types::Method::GetParameter,
|
||||
rtsp_types::Version::V1_0,
|
||||
)
|
||||
.request_uri(state.presentation.base_url.clone())
|
||||
.header(rtsp_types::headers::SESSION, state.session_id.clone())
|
||||
.build(Bytes::new()),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
state
|
||||
.keepalive_timer
|
||||
.as_mut()
|
||||
.reset(tokio::time::Instant::now() + KEEPALIVE_DURATION);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
state: &mut PlayingProj<'_>,
|
||||
response: rtsp_types::Response<Bytes>,
|
||||
) -> Result<(), Error> {
|
||||
if matches!(*state.pending_keepalive_cseq,
|
||||
Some(cseq) if parse::get_cseq(&response) == Some(cseq))
|
||||
{
|
||||
// We don't care if the keepalive response succeeds or fails. Just mark complete.
|
||||
*state.pending_keepalive_cseq = None;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// The only response we expect in this state is to our keepalive request.
|
||||
bail!("Unexpected RTSP response {:#?}", response);
|
||||
}
|
||||
|
||||
fn handle_data(
|
||||
state: &mut PlayingProj<'_>,
|
||||
ctx: Context,
|
||||
data: rtsp_types::Data<Bytes>,
|
||||
) -> Result<Option<PacketItem>, Error> {
|
||||
let c = data.channel_id();
|
||||
let m = match state.channels.lookup(c) {
|
||||
Some(m) => m,
|
||||
None => bail!("Data message on unexpected channel {} at {:#?}", c, &ctx),
|
||||
};
|
||||
let stream = &mut state.presentation.streams[m.stream_i];
|
||||
let (mut timeline, rtp_handler) = match &mut stream.state {
|
||||
StreamState::Playing {
|
||||
timeline,
|
||||
rtp_handler,
|
||||
} => (timeline, rtp_handler),
|
||||
_ => unreachable!("Session<Playing>'s {}->{:?} not in Playing state", c, m),
|
||||
};
|
||||
match m.channel_type {
|
||||
ChannelType::Rtp => Ok(Some(rtp_handler.rtp(
|
||||
ctx,
|
||||
&mut timeline,
|
||||
m.stream_i,
|
||||
data.into_body(),
|
||||
)?)),
|
||||
ChannelType::Rtcp => {
|
||||
Ok(rtp_handler.rtcp(ctx, &mut timeline, m.stream_i, data.into_body())?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn streams(&self) -> &[Stream] {
|
||||
&self.state.presentation.streams
|
||||
}
|
||||
}
|
1131
src/client/parse.rs
Normal file
1131
src/client/parse.rs
Normal file
File diff suppressed because it is too large
Load Diff
216
src/client/rtp.rs
Normal file
216
src/client/rtp.rs
Normal file
@ -0,0 +1,216 @@
|
||||
// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
//! RTP and RTCP handling; see [RFC 3550](https://datatracker.ietf.org/doc/html/rfc3550).
|
||||
|
||||
use bytes::{Buf, Bytes};
|
||||
use failure::{bail, format_err, Error};
|
||||
use log::{debug, trace};
|
||||
use pretty_hex::PrettyHex;
|
||||
|
||||
use crate::client::PacketItem;
|
||||
|
||||
/// An RTP packet.
|
||||
#[derive(Debug)]
|
||||
pub struct Packet {
|
||||
pub rtsp_ctx: crate::Context,
|
||||
pub stream_id: usize,
|
||||
pub timestamp: crate::Timestamp,
|
||||
pub sequence_number: u16,
|
||||
|
||||
/// Number of skipped sequence numbers since the last packet.
|
||||
///
|
||||
/// In the case of the first packet on the stream, this may also report loss
|
||||
/// packets since the `RTP-Info` header's `seq` value. However, currently
|
||||
/// that header is not required to be present and may be ignored (see
|
||||
/// [`crate::client::PlayPolicy::ignore_zero_seq()`].)
|
||||
pub loss: u16,
|
||||
|
||||
pub mark: bool,
|
||||
|
||||
/// Guaranteed to be less than u16::MAX bytes.
|
||||
pub payload: Bytes,
|
||||
}
|
||||
|
||||
/// An RTCP sender report.
|
||||
#[derive(Debug)]
|
||||
pub struct SenderReport {
|
||||
pub stream_id: usize,
|
||||
pub rtsp_ctx: crate::Context,
|
||||
pub timestamp: crate::Timestamp,
|
||||
pub ntp_timestamp: crate::NtpTimestamp,
|
||||
}
|
||||
|
||||
/// RTP/RTCP demarshaller which ensures packets have the correct SSRC and
|
||||
/// monotonically increasing SEQ.
|
||||
///
|
||||
/// This reports packet loss (via [Packet::loss]) but doesn't prohibit it, except for losses
|
||||
/// of more than `i16::MAX` which would be indistinguishable from non-monotonic sequence numbers.
|
||||
/// Servers sometimes drop packets internally even when sending data via TCP.
|
||||
///
|
||||
/// At least [one camera](https://github.com/scottlamb/moonfire-nvr/wiki/Cameras:-Reolink#reolink-rlc-410-hardware-version-ipc_3816m)
|
||||
/// sometimes sends data from old RTSP sessions over new ones. This seems like a
|
||||
/// serious bug, and currently `StrictSequenceChecker` will error in this case,
|
||||
/// although it'd be possible to discard the incorrect SSRC instead.
|
||||
///
|
||||
/// [RFC 3550 section 8.2](https://tools.ietf.org/html/rfc3550#section-8.2) says that SSRC
|
||||
/// can change mid-session with a RTCP BYE message. This currently isn't handled. I'm
|
||||
/// not sure it will ever come up with IP cameras.
|
||||
#[derive(Debug)]
|
||||
pub(super) struct StrictSequenceChecker {
|
||||
ssrc: Option<u32>,
|
||||
next_seq: Option<u16>,
|
||||
}
|
||||
|
||||
impl StrictSequenceChecker {
|
||||
pub(super) fn new(ssrc: Option<u32>, next_seq: Option<u16>) -> Self {
|
||||
Self { ssrc, next_seq }
|
||||
}
|
||||
|
||||
pub(super) fn rtp(
|
||||
&mut self,
|
||||
rtsp_ctx: crate::Context,
|
||||
timeline: &mut super::Timeline,
|
||||
stream_id: usize,
|
||||
mut data: Bytes,
|
||||
) -> Result<PacketItem, Error> {
|
||||
// Terrible hack to try to make sense of the GW Security GW4089IP's audio stream.
|
||||
// It appears to have one RTSP interleaved message wrapped in another. RTP and RTCP
|
||||
// packets can never start with '$', so this shouldn't interfere with well-behaved
|
||||
// servers.
|
||||
if data.len() > 4
|
||||
&& data[0] == b'$'
|
||||
&& usize::from(u16::from_be_bytes([data[2], data[3]])) <= data.len() - 4
|
||||
{
|
||||
log::debug!("stripping extra interleaved data header");
|
||||
data.advance(4);
|
||||
// also remove suffix? dunno.
|
||||
}
|
||||
|
||||
let reader = rtp_rs::RtpReader::new(&data[..]).map_err(|e| {
|
||||
format_err!(
|
||||
"corrupt RTP header while expecting seq={:04x?} at {:#?}: {:?}\n{:#?}",
|
||||
self.next_seq,
|
||||
&rtsp_ctx,
|
||||
e,
|
||||
data.hex_dump()
|
||||
)
|
||||
})?;
|
||||
let sequence_number = u16::from_be_bytes([data[2], data[3]]); // I don't like rtsp_rs::Seq.
|
||||
let timestamp = match timeline.advance_to(reader.timestamp()) {
|
||||
Ok(ts) => ts,
|
||||
Err(e) => {
|
||||
return Err(e
|
||||
.context(format!(
|
||||
"timestamp error in stream {} seq={:04x} {:#?}",
|
||||
stream_id, sequence_number, &rtsp_ctx
|
||||
))
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let ssrc = reader.ssrc();
|
||||
let loss = sequence_number.wrapping_sub(self.next_seq.unwrap_or(sequence_number));
|
||||
if matches!(self.ssrc, Some(s) if s != ssrc) || loss > 0x80_00 {
|
||||
bail!(
|
||||
"Expected ssrc={:08x?} seq={:04x?} got ssrc={:08x} seq={:04x} ts={} at {:#?}",
|
||||
self.ssrc,
|
||||
self.next_seq,
|
||||
ssrc,
|
||||
sequence_number,
|
||||
timestamp,
|
||||
&rtsp_ctx
|
||||
);
|
||||
}
|
||||
self.ssrc = Some(ssrc);
|
||||
let mark = reader.mark();
|
||||
let payload_range = crate::as_range(&data, reader.payload())
|
||||
.ok_or_else(|| format_err!("empty payload at {:#?}", &rtsp_ctx))?;
|
||||
trace!(
|
||||
"{:?} pkt {:04x}{} ts={} len={}",
|
||||
&rtsp_ctx,
|
||||
sequence_number,
|
||||
if mark { " " } else { "(M)" },
|
||||
×tamp,
|
||||
payload_range.len()
|
||||
);
|
||||
data.truncate(payload_range.end);
|
||||
data.advance(payload_range.start);
|
||||
self.next_seq = Some(sequence_number.wrapping_add(1));
|
||||
Ok(PacketItem::RtpPacket(Packet {
|
||||
stream_id,
|
||||
rtsp_ctx,
|
||||
timestamp,
|
||||
sequence_number,
|
||||
loss,
|
||||
mark,
|
||||
payload: data,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) fn rtcp(
|
||||
&mut self,
|
||||
rtsp_ctx: crate::Context,
|
||||
timeline: &mut super::Timeline,
|
||||
stream_id: usize,
|
||||
mut data: Bytes,
|
||||
) -> Result<Option<PacketItem>, Error> {
|
||||
use rtcp::packet::Packet;
|
||||
let mut sr = None;
|
||||
let mut i = 0;
|
||||
while !data.is_empty() {
|
||||
let h = match rtcp::header::Header::unmarshal(&data) {
|
||||
Err(e) => bail!("corrupt RTCP header at {:#?}: {}", &rtsp_ctx, e),
|
||||
Ok(h) => h,
|
||||
};
|
||||
let pkt_len = (usize::from(h.length) + 1) * 4;
|
||||
if pkt_len > data.len() {
|
||||
bail!(
|
||||
"rtcp pkt len {} vs remaining body len {} at {:#?}",
|
||||
pkt_len,
|
||||
data.len(),
|
||||
&rtsp_ctx
|
||||
);
|
||||
}
|
||||
let pkt = data.split_to(pkt_len);
|
||||
match h.packet_type {
|
||||
rtcp::header::PacketType::SenderReport => {
|
||||
if i > 0 {
|
||||
bail!("RTCP SR must be first in packet");
|
||||
}
|
||||
let pkt = match rtcp::sender_report::SenderReport::unmarshal(&pkt) {
|
||||
Err(e) => bail!("corrupt RTCP SR at {:#?}: {}", &rtsp_ctx, e),
|
||||
Ok(p) => p,
|
||||
};
|
||||
|
||||
let timestamp = match timeline.place(pkt.rtp_time) {
|
||||
Ok(ts) => ts,
|
||||
Err(e) => {
|
||||
return Err(e
|
||||
.context(format!(
|
||||
"bad RTP timestamp in RTCP SR {:#?} at {:#?}",
|
||||
&pkt, &rtsp_ctx
|
||||
))
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: verify ssrc.
|
||||
|
||||
sr = Some(SenderReport {
|
||||
stream_id,
|
||||
rtsp_ctx,
|
||||
timestamp,
|
||||
ntp_timestamp: crate::NtpTimestamp(pkt.ntp_time),
|
||||
});
|
||||
}
|
||||
/*rtcp::header::PacketType::SourceDescription => {
|
||||
let pkt = rtcp::source_description::SourceDescription::unmarshal(&pkt)?;
|
||||
debug!("rtcp source description: {:#?}", &pkt);
|
||||
},*/
|
||||
_ => debug!("rtcp: {:?}", h.packet_type),
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
Ok(sr.map(PacketItem::SenderReport))
|
||||
}
|
||||
}
|
30
src/client/testdata/bunny_describe.txt
vendored
Normal file
30
src/client/testdata/bunny_describe.txt
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
RTSP/1.0 200 OK
|
||||
CSeq: 1
|
||||
Server: Wowza Streaming Engine 4.8.10 build20210217143515
|
||||
Cache-Control: no-cache
|
||||
Expires: Sat, 8 May 2021 04:35:51 UTC
|
||||
Content-Length: 589
|
||||
Content-Base: rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov/
|
||||
Date: Sat, 8 May 2021 04:35:51 UTC
|
||||
Content-Type: application/sdp
|
||||
Session: 1642021126;timeout=60
|
||||
|
||||
v=0
|
||||
o=- 1642021126 1642021126 IN IP4 34.227.104.115
|
||||
s=BigBuckBunny_115k.mov
|
||||
c=IN IP4 34.227.104.115
|
||||
t=0 0
|
||||
a=sdplang:en
|
||||
a=range:npt=0- 596.48
|
||||
a=control:*
|
||||
m=audio 0 RTP/AVP 96
|
||||
a=rtpmap:96 mpeg4-generic/12000/2
|
||||
a=fmtp:96 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1490
|
||||
a=control:trackID=1
|
||||
m=video 0 RTP/AVP 97
|
||||
a=rtpmap:97 H264/90000
|
||||
a=fmtp:97 packetization-mode=1;profile-level-id=42C01E;sprop-parameter-sets=Z0LAHtkDxWhAAAADAEAAAAwDxYuS,aMuMsg==
|
||||
a=cliprect:0,0,160,240
|
||||
a=framesize:97 240-160
|
||||
a=framerate:24.0
|
||||
a=control:trackID=2
|
8
src/client/testdata/bunny_play.txt
vendored
Normal file
8
src/client/testdata/bunny_play.txt
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
RTSP/1.0 200 OK
|
||||
RTP-Info: url=trackID=1;seq=1;rtptime=0,url=rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov/trackID=2;seq=1;rtptime=0
|
||||
CSeq: 3
|
||||
Server: Wowza Streaming Engine 4.8.10 build20210217143515
|
||||
Cache-Control: no-cache
|
||||
Range: npt=0.0-596.48
|
||||
Session: 551045787;timeout=60
|
||||
|
9
src/client/testdata/bunny_setup.txt
vendored
Normal file
9
src/client/testdata/bunny_setup.txt
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
RTSP/1.0 200 OK
|
||||
CSeq: 2
|
||||
Server: Wowza Streaming Engine 4.8.10 build20210217143515
|
||||
Cache-Control: no-cache
|
||||
Expires: Sat, 8 May 2021 04:35:51 UTC
|
||||
Transport: RTP/AVP/TCP;unicast;interleaved=0-1
|
||||
Date: Sat, 8 May 2021 04:35:51 UTC
|
||||
Session: 1642021126;timeout=60
|
||||
|
32
src/client/testdata/dahua_describe_h264_aac_onvif.txt
vendored
Normal file
32
src/client/testdata/dahua_describe_h264_aac_onvif.txt
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
RTSP/1.0 200 OK
|
||||
CSeq: 3
|
||||
x-Accept-Dynamic-Rate: 1
|
||||
Content-Base: rtsp://192.168.5.111:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif/
|
||||
Cache-Control: must-revalidate
|
||||
Content-Length: 705
|
||||
Content-Type: application/sdp
|
||||
|
||||
v=0
|
||||
o=- 2252542845 2252542845 IN IP4 0.0.0.0
|
||||
s=Media Server
|
||||
c=IN IP4 0.0.0.0
|
||||
t=0 0
|
||||
a=control:*
|
||||
a=packetization-supported:DH
|
||||
a=rtppayload-supported:DH
|
||||
a=range:npt=now-
|
||||
m=video 0 RTP/AVP 96
|
||||
a=control:trackID=0
|
||||
a=framerate:15.000000
|
||||
a=rtpmap:96 H264/90000
|
||||
a=fmtp:96 packetization-mode=1;profile-level-id=64001E;sprop-parameter-sets=Z2QAHqwsaoLA9puCgIKgAAADACAAAAMD0IAA,aO4xshsA
|
||||
a=recvonly
|
||||
m=audio 0 RTP/AVP 97
|
||||
a=control:trackID=1
|
||||
a=rtpmap:97 MPEG4-GENERIC/48000
|
||||
a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1188
|
||||
a=recvonly
|
||||
m=application 0 RTP/AVP 107
|
||||
a=control:trackID=4
|
||||
a=rtpmap:107 vnd.onvif.metadata/90000
|
||||
a=recvonly
|
27
src/client/testdata/dahua_describe_h265_pcma.txt
vendored
Normal file
27
src/client/testdata/dahua_describe_h265_pcma.txt
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
RTSP/1.0 200 OK
|
||||
CSeq: 3
|
||||
x-Accept-Dynamic-Rate: 1
|
||||
Content-Base: rtsp://cam-driveway/cam/realmonitor?channel=1&subtype=2/
|
||||
Cache-Control: must-revalidate
|
||||
Content-Length: 531
|
||||
Content-Type: application/sdp
|
||||
|
||||
v=0
|
||||
o=- 2253040596 2253040596 IN IP4 0.0.0.0
|
||||
s=Media Server
|
||||
c=IN IP4 0.0.0.0
|
||||
t=0 0
|
||||
a=control:*
|
||||
a=packetization-supported:DH
|
||||
a=rtppayload-supported:DH
|
||||
a=range:npt=now-
|
||||
m=video 0 RTP/AVP 98
|
||||
a=control:trackID=0
|
||||
a=framerate:12.000000
|
||||
a=rtpmap:98 H265/90000
|
||||
a=fmtp:98 profile-id=1;sprop-sps=QgEBAWAAAAMAsAAAAwAAAwBaoAWCAeFja5JFL83BQYFBAAADAAEAAAMADKE=;sprop-pps=RAHA8saNA7NA;sprop-vps=QAEMAf//AWAAAAMAsAAAAwAAAwBarAwAAAMABAAAAwAyqA==
|
||||
a=recvonly
|
||||
m=audio 0 RTP/AVP 8
|
||||
a=control:trackID=1
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=recvonly
|
6
src/client/testdata/dahua_play.txt
vendored
Normal file
6
src/client/testdata/dahua_play.txt
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
RTSP/1.0 200 OK
|
||||
CSeq: 7
|
||||
Session: 634214684047
|
||||
Range: npt=0.000000-
|
||||
RTP-Info: url=trackID=0;seq=47121;rtptime=3475222385,url=trackID=1;seq=45186;rtptime=2234446919,url=trackID=4;seq=36583;rtptime=816418535
|
||||
|
6
src/client/testdata/dahua_setup.txt
vendored
Normal file
6
src/client/testdata/dahua_setup.txt
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
RTSP/1.0 200 OK
|
||||
CSeq: 4
|
||||
Session: 634214675641;timeout=60
|
||||
Transport: RTP/AVP/TCP;unicast;interleaved=0-1;ssrc=30A98EE7
|
||||
x-Dynamic-Rate: 1
|
||||
|
29
src/client/testdata/foscam_describe.txt
vendored
Normal file
29
src/client/testdata/foscam_describe.txt
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
RTSP/1.0 200 OK
|
||||
CSeq: 2
|
||||
Date: Wed, May 12 2021 18:56:25 GMT
|
||||
Content-Base: rtsp://192.168.5.107:65534/videoMain/
|
||||
Content-Type: application/sdp
|
||||
Content-Length: 518
|
||||
|
||||
v=0
|
||||
o=- 1620845785796009 1 IN IP4 192.168.233.233
|
||||
s=IP Camera Video
|
||||
i=videoMain
|
||||
t=0 0
|
||||
a=tool:LIVE555 Streaming Media v2014.02.10
|
||||
a=type:broadcast
|
||||
a=control:*
|
||||
a=range:npt=0-
|
||||
a=x-qt-text-nam:IP Camera Video
|
||||
a=x-qt-text-inf:videoMain
|
||||
m=video 0 RTP/AVP 96
|
||||
c=IN IP4 0.0.0.0
|
||||
b=AS:96
|
||||
a=rtpmap:96 H264/90000
|
||||
a=range:npt=0-
|
||||
a=fmtp:96 packetization-mode=1;profile-level-id=4D001F;sprop-parameter-sets=Z00AH5WoFAFuQA==,aO48gA==
|
||||
a=control:track1
|
||||
m=audio 0 RTP/AVP 0
|
||||
c=IN IP4 0.0.0.0
|
||||
b=AS:64
|
||||
a=control:track2
|
25
src/client/testdata/gw_main_describe.txt
vendored
Normal file
25
src/client/testdata/gw_main_describe.txt
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
RTSP/1.0 200 OK
|
||||
Server: Rtsp Server/2.0
|
||||
CSeq: 1
|
||||
Content-Base: rtsp://192.168.1.110:5050/H264?channel=1&subtype=0&unicast=true&proto=Onvif
|
||||
Content-Type: application/sdp
|
||||
Content-Length: 451
|
||||
|
||||
v=0
|
||||
o=- 1109162014219182 1109162014219282 IN IP4 x.y.z.w
|
||||
s=Session streamed by "H264Server"
|
||||
e=NONE
|
||||
c=IN IP4 0.0.0.0
|
||||
t=0 0
|
||||
a=range:npt=now-
|
||||
a=control:*
|
||||
m=video 0 RTP/AVP 96
|
||||
a=rtpmap:96 H264/90000
|
||||
a=control:video
|
||||
a=fmtp:96 packetization-mode=1;profile-level-id=5046314;sprop-parameter-sets=Z00AKpWoHgCJ+WbgICAgQAAAAAE=,aO48gAAAAAE=
|
||||
m=audio 0 RTP/AVP 8
|
||||
a=control:audio
|
||||
a=rtpmap:8 PCMU/8000/1
|
||||
a=ptime:20
|
||||
a=fmtp:8 vad=no
|
||||
a=appversion:2.0
|
6
src/client/testdata/gw_main_play.txt
vendored
Normal file
6
src/client/testdata/gw_main_play.txt
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
RTSP/1.0 200 OK
|
||||
CSeq: 4
|
||||
Range: npt=0.000-
|
||||
Session: 9a90de54; timeout=120
|
||||
RTP-Info: url=rtsp://192.168.1.110:5050/H264/video;seq=271;rtptime=1621990950
|
||||
|
5
src/client/testdata/gw_main_setup_audio.txt
vendored
Normal file
5
src/client/testdata/gw_main_setup_audio.txt
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
RTSP/1.0 200 OK
|
||||
CSeq: 3
|
||||
Transport: RTP/AVP/TCP;unicast;destination=192.168.1.210;source=192.168.1.110;interleaved=2-3
|
||||
Session: 9a90de54; timeout=60
|
||||
|
5
src/client/testdata/gw_main_setup_video.txt
vendored
Normal file
5
src/client/testdata/gw_main_setup_video.txt
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
RTSP/1.0 200 OK
|
||||
CSeq: 2
|
||||
Transport: RTP/AVP/TCP;unicast;destination=192.168.1.210;source=192.168.1.110;interleaved=0-1
|
||||
Session: 9a90de54; timeout=60
|
||||
|
19
src/client/testdata/gw_sub_describe.txt
vendored
Normal file
19
src/client/testdata/gw_sub_describe.txt
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
RTSP/1.0 200 OK
|
||||
Server: Rtsp Server/2.0
|
||||
CSeq: 1
|
||||
Content-Base: rtsp://192.168.1.110:5049/H264?channel=1&subtype=1&unicast=true&proto=Onvif
|
||||
Content-Type: application/sdp
|
||||
Content-Length: 338
|
||||
|
||||
v=0
|
||||
o=- 1109162014219182 1109162014219282 IN IP4 x.y.z.w
|
||||
s=Session streamed by "H264Server"
|
||||
e=NONE
|
||||
c=IN IP4 0.0.0.0
|
||||
t=0 0
|
||||
a=range:npt=now-
|
||||
a=control:*
|
||||
m=video 0 RTP/AVP 96
|
||||
a=rtpmap:96 H264/90000
|
||||
a=control:video
|
||||
a=fmtp:96 packetization-mode=1;profile-level-id=5046302;sprop-parameter-sets=Z00AHpWoLQ9puAgICBAAAAAB,aO48gAAAAAE=
|
6
src/client/testdata/gw_sub_play.txt
vendored
Normal file
6
src/client/testdata/gw_sub_play.txt
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
RTSP/1.0 200 OK
|
||||
CSeq: 3
|
||||
Range: npt=0.000-
|
||||
Session: 9b0d0e54; timeout=120
|
||||
RTP-Info: url=rtsp://192.168.1.110:5049/H264/video;seq=273;rtptime=1621810809
|
||||
|
5
src/client/testdata/gw_sub_setup.txt
vendored
Normal file
5
src/client/testdata/gw_sub_setup.txt
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
RTSP/1.0 200 OK
|
||||
CSeq: 2
|
||||
Transport: RTP/AVP/TCP;unicast;destination=192.168.1.210;source=192.168.1.110;interleaved=0-1
|
||||
Session: 9b0d0e54; timeout=60
|
||||
|
29
src/client/testdata/hikvision_describe.txt
vendored
Normal file
29
src/client/testdata/hikvision_describe.txt
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
RTSP/1.0 200 OK
|
||||
CSeq: 2
|
||||
Content-Type: application/sdp
|
||||
Content-Base: rtsp://192.168.5.106:554/Streaming/Channels/101/
|
||||
Content-Length: 902
|
||||
|
||||
v=0
|
||||
o=- 1620251477190769 1620251477190769 IN IP4 192.168.5.106
|
||||
s=Media Presentation
|
||||
e=NONE
|
||||
b=AS:5050
|
||||
t=0 0
|
||||
a=control:rtsp://192.168.5.106:554/Streaming/Channels/101/?transportmode=unicast&profile=Profile_1
|
||||
m=video 0 RTP/AVP 96
|
||||
c=IN IP4 0.0.0.0
|
||||
b=AS:5000
|
||||
a=recvonly
|
||||
a=x-dimensions:1920,1080
|
||||
a=control:rtsp://192.168.5.106:554/Streaming/Channels/101/trackID=1?transportmode=unicast&profile=Profile_1
|
||||
a=rtpmap:96 H264/90000
|
||||
a=fmtp:96 profile-level-id=420029; packetization-mode=1; sprop-parameter-sets=Z00AKZpkA8ARPyzUBAQFAAADA+gAAOpgBA==,aO48gA==
|
||||
m=application 0 RTP/AVP 107
|
||||
c=IN IP4 0.0.0.0
|
||||
b=AS:50
|
||||
a=recvonly
|
||||
a=control:rtsp://192.168.5.106:554/Streaming/Channels/101/trackID=3?transportmode=unicast&profile=Profile_1
|
||||
a=rtpmap:107 vnd.onvif.metadata/90000
|
||||
a=Media_header:MEDIAINFO=494D4B48010100000400010000000000000000000000000000000000000000000000000000000000;
|
||||
a=appversion:1.0
|
6
src/client/testdata/hikvision_play.txt
vendored
Normal file
6
src/client/testdata/hikvision_play.txt
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
RTSP/1.0 200 OK
|
||||
CSeq: 4
|
||||
Session: 708345999
|
||||
RTP-Info: url=rtsp://192.168.5.106:554/Streaming/Channels/101/trackID=1?transportmode=unicast&profile=Profile_1;seq=24104;rtptime=1270711678
|
||||
Date: Wed, May 05 2021 21:51:17 GMT
|
||||
|
6
src/client/testdata/hikvision_setup.txt
vendored
Normal file
6
src/client/testdata/hikvision_setup.txt
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
RTSP/1.0 200 OK
|
||||
CSeq: 3
|
||||
Session: 708345999;timeout=60
|
||||
Transport: RTP/AVP/TCP;unicast;interleaved=0-1;ssrc=4cacc3d1;mode="play"
|
||||
Date: Wed, May 05 2021 21:51:17 GMT
|
||||
|
31
src/client/testdata/reolink_describe.txt
vendored
Normal file
31
src/client/testdata/reolink_describe.txt
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
RTSP/1.0 200 OK
|
||||
CSeq: 3
|
||||
Date: Fri, Apr 30 2021 20:12:32 GMT
|
||||
Content-Base: rtsp://192.168.5.206/h264Preview_01_main/
|
||||
Content-Type: application/sdp
|
||||
Content-Length: 734
|
||||
|
||||
v=0
|
||||
o=- 1619813458434609 1 IN IP4 192.168.5.206
|
||||
s=Session streamed by "preview"
|
||||
i=h264Preview_01_main
|
||||
t=0 0
|
||||
a=tool:LIVE555 Streaming Media v2013.04.08
|
||||
a=type:broadcast
|
||||
a=control:*
|
||||
a=range:npt=0-
|
||||
a=x-qt-text-nam:Session streamed by "preview"
|
||||
a=x-qt-text-inf:h264Preview_01_main
|
||||
m=video 0 RTP/AVP 96
|
||||
c=IN IP4 0.0.0.0
|
||||
b=AS:500
|
||||
a=rtpmap:96 H264/90000
|
||||
a=range:npt=0-
|
||||
a=fmtp:96 packetization-mode=1;profile-level-id=640033;sprop-parameter-sets=Z2QAM6zoAoALWQ==,aO48MA==
|
||||
a=control:trackID=1
|
||||
m=audio 0 RTP/AVP 97
|
||||
c=IN IP4 0.0.0.0
|
||||
b=AS:256
|
||||
a=rtpmap:97 MPEG4-GENERIC/16000
|
||||
a=fmtp:97 streamtype=5;profile-level-id=15;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408; profile=1;
|
||||
a=control:trackID=2
|
8
src/client/testdata/reolink_play.txt
vendored
Normal file
8
src/client/testdata/reolink_play.txt
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
RTSP/1.0 200 OK
|
||||
Server: Rtsp Server/2.0
|
||||
CSeq: 6
|
||||
Date: Fri, Apr 30 2021 20:12:32 GMT
|
||||
Range: npt=0.000-
|
||||
Session: F8F8E425
|
||||
RTP-Info: url=trackID=1;seq=16852;rtptime=1070938629;ssrc=dcc4a0d8,url=trackID=2;seq=39409;rtptime=3075976528;ssrc=9fc9fff8
|
||||
|
6
src/client/testdata/reolink_setup.txt
vendored
Normal file
6
src/client/testdata/reolink_setup.txt
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
RTSP/1.0 200 OK
|
||||
CSeq: 4
|
||||
Date: Fri, Apr 30 2021 20:12:32 GMT
|
||||
Transport: RTP/AVP/TCP;unicast;destination=192.168.1.210;source=192.168.5.206;interleaved=0-1
|
||||
Session: F8F8E425
|
||||
|
187
src/client/timeline.rs
Normal file
187
src/client/timeline.rs
Normal file
@ -0,0 +1,187 @@
|
||||
// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use failure::{bail, format_err, Error};
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
num::{NonZeroI32, NonZeroU32},
|
||||
};
|
||||
|
||||
use crate::Timestamp;
|
||||
|
||||
const MAX_FORWARD_TIME_JUMP_SECS: u32 = 10;
|
||||
|
||||
/// Creates [Timestamp]s (which don't wrap and can be converted to NPT aka normal play time)
|
||||
/// from 32-bit (wrapping) RTP timestamps.
|
||||
#[derive(Debug)]
|
||||
pub(super) struct Timeline {
|
||||
timestamp: i64,
|
||||
clock_rate: NonZeroU32,
|
||||
start: Option<u32>,
|
||||
|
||||
/// The maximum forward jump to allow, in clock rate units.
|
||||
/// If this is absent, don't do any enforcement of sane time units.
|
||||
max_forward_jump: Option<NonZeroI32>,
|
||||
|
||||
/// The same in seconds, for logging.
|
||||
max_forward_jump_secs: u32,
|
||||
}
|
||||
|
||||
impl Timeline {
|
||||
/// Creates a new timeline, erroring on crazy clock rates.
|
||||
pub(super) fn new(
|
||||
start: Option<u32>,
|
||||
clock_rate: u32,
|
||||
enforce_with_max_forward_jump_secs: Option<NonZeroU32>,
|
||||
) -> Result<Self, Error> {
|
||||
let clock_rate = NonZeroU32::new(clock_rate)
|
||||
.ok_or_else(|| format_err!("clock_rate=0 rejected to prevent division by zero"))?;
|
||||
let max_forward_jump =
|
||||
enforce_with_max_forward_jump_secs
|
||||
.map(|j| i32::try_from(u64::from(j.get()) * u64::from(clock_rate.get())))
|
||||
.transpose()
|
||||
.map_err(|_| {
|
||||
format_err!(
|
||||
"clock_rate={} rejected because max forward jump of {} sec exceeds i32::MAX",
|
||||
clock_rate, MAX_FORWARD_TIME_JUMP_SECS)
|
||||
})?
|
||||
.map(|j| NonZeroI32::new(j).expect("non-zero times non-zero must be non-zero"));
|
||||
Ok(Timeline {
|
||||
timestamp: i64::from(start.unwrap_or(0)),
|
||||
start,
|
||||
clock_rate,
|
||||
max_forward_jump,
|
||||
max_forward_jump_secs: enforce_with_max_forward_jump_secs
|
||||
.map(NonZeroU32::get)
|
||||
.unwrap_or(0),
|
||||
})
|
||||
}
|
||||
|
||||
/// Advances to the given (wrapping) RTP timestamp.
|
||||
///
|
||||
/// If enforcement was enabled, this produces a monotonically increasing
|
||||
/// [Timestamp], erroring on excessive or backward time jumps.
|
||||
pub(super) fn advance_to(&mut self, rtp_timestamp: u32) -> Result<Timestamp, Error> {
|
||||
let (timestamp, delta) = self.ts_and_delta(rtp_timestamp)?;
|
||||
if matches!(self.max_forward_jump, Some(j) if !(0..j.get()).contains(&delta)) {
|
||||
bail!(
|
||||
"Timestamp jumped {} ({:.03} sec) from {} to {}; \
|
||||
policy is to allow 0..{} sec only",
|
||||
delta,
|
||||
(delta as f64) / f64::from(self.clock_rate.get()),
|
||||
self.timestamp,
|
||||
timestamp,
|
||||
self.max_forward_jump_secs
|
||||
);
|
||||
}
|
||||
self.timestamp = timestamp.timestamp;
|
||||
Ok(timestamp)
|
||||
}
|
||||
|
||||
/// Places `rtp_timestamp` on the timeline without advancing the timeline
|
||||
/// or applying time jump policy. Will set the NPT epoch if unset.
|
||||
///
|
||||
/// This is useful for RTP timestamps in RTCP packets. They commonly refer
|
||||
/// to time slightly before the most timestamp of the matching RTP stream.
|
||||
pub(super) fn place(&mut self, rtp_timestamp: u32) -> Result<Timestamp, Error> {
|
||||
Ok(self.ts_and_delta(rtp_timestamp)?.0)
|
||||
}
|
||||
|
||||
fn ts_and_delta(&mut self, rtp_timestamp: u32) -> Result<(Timestamp, i32), Error> {
|
||||
let start = match self.start {
|
||||
None => {
|
||||
self.start = Some(rtp_timestamp);
|
||||
self.timestamp = i64::from(rtp_timestamp);
|
||||
rtp_timestamp
|
||||
}
|
||||
Some(start) => start,
|
||||
};
|
||||
let delta = (rtp_timestamp as i32).wrapping_sub(self.timestamp as i32);
|
||||
let timestamp = self
|
||||
.timestamp
|
||||
.checked_add(i64::from(delta))
|
||||
.ok_or_else(|| {
|
||||
// This probably won't happen even with a hostile server. It'd
|
||||
// take ~2^31 packets (~ 4 billion) to advance the time this far
|
||||
// forward or backward even with no limits on time jump per
|
||||
// packet.
|
||||
format_err!(
|
||||
"timestamp {} + delta {} won't fit in i64!",
|
||||
self.timestamp,
|
||||
delta
|
||||
)
|
||||
})?;
|
||||
|
||||
// Also error in similarly-unlikely NPT underflow.
|
||||
if timestamp.checked_sub(i64::from(start)).is_none() {
|
||||
bail!(
|
||||
"timestamp {} + delta {} - start {} underflows i64!",
|
||||
self.timestamp,
|
||||
delta,
|
||||
start
|
||||
);
|
||||
}
|
||||
Ok((
|
||||
Timestamp {
|
||||
timestamp,
|
||||
clock_rate: self.clock_rate,
|
||||
start,
|
||||
},
|
||||
delta,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
use super::Timeline;
|
||||
|
||||
#[test]
|
||||
fn timeline() {
|
||||
// Don't allow crazy clock rates that will get us into trouble.
|
||||
Timeline::new(Some(0), 0, None).unwrap_err();
|
||||
Timeline::new(Some(0), u32::MAX, NonZeroU32::new(10)).unwrap_err();
|
||||
|
||||
// Don't allow excessive forward jumps when enforcement is enabled.
|
||||
let mut t = Timeline::new(Some(100), 90_000, NonZeroU32::new(10)).unwrap();
|
||||
t.advance_to(100 + (10 * 90_000) + 1).unwrap_err();
|
||||
|
||||
// Or any backward jump when enforcement is enabled.
|
||||
let mut t = Timeline::new(Some(100), 90_000, NonZeroU32::new(10)).unwrap();
|
||||
t.advance_to(99).unwrap_err();
|
||||
|
||||
// ...but do allow backward RTP timestamps in RTCP.
|
||||
let mut t = Timeline::new(Some(100), 90_000, NonZeroU32::new(10)).unwrap();
|
||||
assert_eq!(t.place(99).unwrap().elapsed(), -1);
|
||||
assert_eq!(t.advance_to(101).unwrap().elapsed(), 1);
|
||||
|
||||
// ...and be more permissive when enforcement is disabled.
|
||||
let mut t = Timeline::new(Some(100), 90_000, None).unwrap();
|
||||
t.advance_to(100 + (10 * 90_000) + 1).unwrap();
|
||||
let mut t = Timeline::new(Some(100), 90_000, None).unwrap();
|
||||
t.advance_to(99).unwrap();
|
||||
|
||||
// Normal usage.
|
||||
let mut t = Timeline::new(Some(42), 90_000, NonZeroU32::new(10)).unwrap();
|
||||
assert_eq!(t.advance_to(83).unwrap().elapsed(), 83 - 42);
|
||||
assert_eq!(t.advance_to(453).unwrap().elapsed(), 453 - 42);
|
||||
|
||||
// Wraparound is normal too.
|
||||
let mut t = Timeline::new(Some(u32::MAX), 90_000, NonZeroU32::new(10)).unwrap();
|
||||
assert_eq!(t.advance_to(5).unwrap().elapsed(), 5 + 1);
|
||||
|
||||
// No initial rtptime.
|
||||
let mut t = Timeline::new(None, 90_000, NonZeroU32::new(10)).unwrap();
|
||||
assert_eq!(t.advance_to(218250000).unwrap().elapsed(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cast() {
|
||||
let a = 0x1FFFF_FFFFi64;
|
||||
let b = 0x10000_0000i64;
|
||||
assert_eq!(a as i32, -1);
|
||||
assert_eq!(b as i32, 0);
|
||||
}
|
||||
}
|
680
src/codec/aac.rs
Normal file
680
src/codec/aac.rs
Normal file
@ -0,0 +1,680 @@
|
||||
// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
//! AAC (Advanced Audio Codec) depacketization.
|
||||
//! There are many intertwined standards; see the following references:
|
||||
//! * [RFC 3640](https://datatracker.ietf.org/doc/html/rfc3640): RTP Payload
|
||||
//! for Transport of MPEG-4 Elementary Streams.
|
||||
//! * ISO/IEC 13818-7: Advanced Audio Coding.
|
||||
//! * ISO/IEC 14496: Information technology — Coding of audio-visual objects
|
||||
//! * ISO/IEC 14496-1: Systems.
|
||||
//! * ISO/IEC 14496-3: Audio, subpart 1: Main.
|
||||
//! * ISO/IEC 14496-3: Audio, subpart 4: General Audio coding (GA) — AAC, TwinVQ, BSAC.
|
||||
//! * [ISO/IEC 14496-12](https://standards.iso.org/ittf/PubliclyAvailableStandards/c068960_ISO_IEC_14496-12_2015.zip):
|
||||
//! ISO base media file format.
|
||||
//! * ISO/IEC 14496-14: MP4 File Format.
|
||||
|
||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||
use failure::{bail, format_err, Error};
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
fmt::Debug,
|
||||
num::{NonZeroU16, NonZeroU32},
|
||||
};
|
||||
|
||||
use crate::client::rtp::Packet;
|
||||
|
||||
use super::CodecItem;
|
||||
|
||||
/// An AudioSpecificConfig as in ISO/IEC 14496-3 section 1.6.2.1.
|
||||
/// Currently just a few fields of interest.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct AudioSpecificConfig {
|
||||
/// See ISO/IEC 14496-3 Table 1.3.
|
||||
audio_object_type: u8,
|
||||
frame_length: NonZeroU16,
|
||||
sampling_frequency: u32,
|
||||
channels: &'static ChannelConfig,
|
||||
}
|
||||
|
||||
/// A channel configuration as in ISO/IEC 14496-3 Table 1.19.
|
||||
#[derive(Debug)]
|
||||
struct ChannelConfig {
|
||||
channels: u16,
|
||||
|
||||
/// The "number of considered channels" as defined in ISO/IEC 13818-7 Term
|
||||
/// 3.58. Roughly, non-subwoofer channels.
|
||||
ncc: u16,
|
||||
|
||||
/// A human-friendly name for the channel configuration.
|
||||
name: &'static str,
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
const CHANNEL_CONFIGS: [Option<ChannelConfig>; 8] = [
|
||||
/* 0 */ None, // "defined in AOT related SpecificConfig"
|
||||
/* 1 */ Some(ChannelConfig { channels: 1, ncc: 1, name: "mono" }),
|
||||
/* 2 */ Some(ChannelConfig { channels: 2, ncc: 2, name: "stereo" }),
|
||||
/* 3 */ Some(ChannelConfig { channels: 3, ncc: 3, name: "3.0" }),
|
||||
/* 4 */ Some(ChannelConfig { channels: 4, ncc: 4, name: "4.0" }),
|
||||
/* 5 */ Some(ChannelConfig { channels: 5, ncc: 5, name: "5.0" }),
|
||||
/* 6 */ Some(ChannelConfig { channels: 6, ncc: 5, name: "5.1" }),
|
||||
/* 7 */ Some(ChannelConfig { channels: 8, ncc: 7, name: "7.1" }),
|
||||
];
|
||||
|
||||
impl AudioSpecificConfig {
|
||||
/// Parses from raw bytes.
|
||||
fn parse(config: &[u8]) -> Result<Self, Error> {
|
||||
let mut r = bitreader::BitReader::new(config);
|
||||
let audio_object_type = match r.read_u8(5)? {
|
||||
31 => 32 + r.read_u8(6)?,
|
||||
o => o,
|
||||
};
|
||||
|
||||
// ISO/IEC 14496-3 section 1.6.3.4.
|
||||
let sampling_frequency = match r.read_u8(4)? {
|
||||
0x0 => 96_000,
|
||||
0x1 => 88_200,
|
||||
0x2 => 64_000,
|
||||
0x3 => 48_000,
|
||||
0x5 => 32_000,
|
||||
0x6 => 24_000,
|
||||
0x7 => 22_050,
|
||||
0x8 => 16_000,
|
||||
0x9 => 12_000,
|
||||
0xa => 11_025,
|
||||
0xb => 8_000,
|
||||
0xc => 7_350,
|
||||
v @ 0xd | v @ 0xe => bail!("reserved sampling_frequency_index value 0x{:x}", v),
|
||||
0xf => r.read_u32(24)?,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let channels = {
|
||||
let c = r.read_u8(4)?;
|
||||
CHANNEL_CONFIGS
|
||||
.get(usize::from(c))
|
||||
.ok_or_else(|| format_err!("reserved channelConfiguration 0x{:x}", c))?
|
||||
.as_ref()
|
||||
.ok_or_else(|| format_err!("program_config_element parsing unimplemented"))?
|
||||
};
|
||||
if audio_object_type == 5 || audio_object_type == 29 {
|
||||
// extensionSamplingFrequencyIndex + extensionSamplingFrequency.
|
||||
if r.read_u8(4)? == 0xf {
|
||||
r.skip(24)?;
|
||||
}
|
||||
// audioObjectType (a different one) + extensionChannelConfiguration.
|
||||
if r.read_u8(5)? == 22 {
|
||||
r.skip(4)?;
|
||||
}
|
||||
}
|
||||
|
||||
// The supported types here are the ones that use GASpecificConfig.
|
||||
match audio_object_type {
|
||||
1 | 2 | 3 | 4 | 6 | 7 | 17 | 19 | 20 | 21 | 22 | 23 => {}
|
||||
o => bail!("unsupported audio_object_type {}", o),
|
||||
}
|
||||
|
||||
// GASpecificConfig, ISO/IEC 14496-3 section 4.4.1.
|
||||
let frame_length = match (audio_object_type, r.read_bool()?) {
|
||||
(3 /* AAC SR */, false) => NonZeroU16::new(256).expect("non-zero"),
|
||||
(3 /* AAC SR */, true) => bail!("frame_length_flag must be false for AAC SSR"),
|
||||
(23 /* ER AAC LD */, false) => NonZeroU16::new(512).expect("non-zero"),
|
||||
(23 /* ER AAC LD */, true) => NonZeroU16::new(480).expect("non-zero"),
|
||||
(_, false) => NonZeroU16::new(1024).expect("non-zero"),
|
||||
(_, true) => NonZeroU16::new(960).expect("non-zero"),
|
||||
};
|
||||
|
||||
Ok(AudioSpecificConfig {
|
||||
audio_object_type,
|
||||
frame_length,
|
||||
sampling_frequency,
|
||||
channels,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Overwrites a buffer with a varint length, returning the length of the length.
|
||||
/// See ISO/IEC 14496-1 section 8.3.3.
|
||||
fn set_length(len: usize, data: &mut [u8]) -> Result<usize, Error> {
|
||||
if len < 1 << 7 {
|
||||
data[0] = len as u8;
|
||||
Ok(1)
|
||||
} else if len < 1 << 14 {
|
||||
data[0] = ((len & 0x7F) | 0x80) as u8;
|
||||
data[1] = (len >> 7) as u8;
|
||||
Ok(2)
|
||||
} else if len < 1 << 21 {
|
||||
data[0] = ((len & 0x7F) | 0x80) as u8;
|
||||
data[1] = (((len >> 7) & 0x7F) | 0x80) as u8;
|
||||
data[2] = (len >> 14) as u8;
|
||||
Ok(3)
|
||||
} else if len < 1 << 28 {
|
||||
data[0] = ((len & 0x7F) | 0x80) as u8;
|
||||
data[1] = (((len >> 7) & 0x7F) | 0x80) as u8;
|
||||
data[2] = (((len >> 14) & 0x7F) | 0x80) as u8;
|
||||
data[3] = (len >> 21) as u8;
|
||||
Ok(4)
|
||||
} else {
|
||||
// BaseDescriptor sets a maximum length of 2**28 - 1.
|
||||
bail!("length {} too long", len);
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a box length and type (four-character code) for everything appended
|
||||
/// in the supplied scope.
|
||||
macro_rules! write_box {
|
||||
($buf:expr, $fourcc:expr, $b:block) => {{
|
||||
let _: &mut BytesMut = $buf; // type-check.
|
||||
let pos_start = $buf.len();
|
||||
let fourcc: &[u8; 4] = $fourcc;
|
||||
$buf.extend_from_slice(&[0, 0, 0, 0, fourcc[0], fourcc[1], fourcc[2], fourcc[3]]);
|
||||
let r = {
|
||||
$b;
|
||||
};
|
||||
let pos_end = $buf.len();
|
||||
let len = pos_end.checked_sub(pos_start).unwrap();
|
||||
$buf[pos_start..pos_start + 4].copy_from_slice(&u32::try_from(len)?.to_be_bytes()[..]);
|
||||
r
|
||||
}};
|
||||
}
|
||||
|
||||
/// Writes a descriptor tag and length for everything appended in the supplied
|
||||
/// scope. See ISO/IEC 14496-1 Table 1 for the `tag`.
|
||||
macro_rules! write_descriptor {
|
||||
($buf:expr, $tag:expr, $b:block) => {{
|
||||
let _: &mut BytesMut = $buf; // type-check.
|
||||
let _: u8 = $tag;
|
||||
let pos_start = $buf.len();
|
||||
|
||||
// Overallocate room for the varint length and append the body.
|
||||
$buf.extend_from_slice(&[$tag, 0, 0, 0, 0]);
|
||||
let r = {
|
||||
$b;
|
||||
};
|
||||
let pos_end = $buf.len();
|
||||
|
||||
// Then fix it afterward: write the correct varint length and move
|
||||
// the body backward. This approach seems better than requiring the
|
||||
// caller to first prepare the body in a separate allocation (and
|
||||
// awkward code ordering), or (as ffmpeg does) writing a "varint"
|
||||
// which is padded with leading 0x80 bytes.
|
||||
let len = pos_end.checked_sub(pos_start + 5).unwrap();
|
||||
let len_len = set_length(len, &mut $buf[pos_start + 1..pos_start + 4])?;
|
||||
$buf.copy_within(pos_start + 5..pos_end, pos_start + 1 + len_len);
|
||||
$buf.truncate(pos_end + len_len - 4);
|
||||
r
|
||||
}};
|
||||
}
|
||||
|
||||
/// Returns an MP4AudioSampleEntry (`mp4a`) box as in ISO/IEC 14496-14 section 5.6.1.
|
||||
/// `config` should be a raw AudioSpecificConfig (matching `parsed`).
|
||||
pub(super) fn get_mp4a_box(parameters: &super::AudioParameters) -> Result<Bytes, Error> {
|
||||
let parsed = match parameters.config {
|
||||
super::AudioCodecConfig::Aac(ref c) => c,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let config = ¶meters.extra_data[..];
|
||||
let mut buf = BytesMut::new();
|
||||
|
||||
// Write an MP4AudioSampleEntry (`mp4a`), as in ISO/IEC 14496-14 section 5.6.1.
|
||||
// It's based on AudioSampleEntry, ISO/IEC 14496-12 section 12.2.3.2,
|
||||
// in turn based on SampleEntry, ISO/IEC 14496-12 section 8.5.2.2.
|
||||
write_box!(&mut buf, b"mp4a", {
|
||||
buf.extend_from_slice(&[
|
||||
0, 0, 0, 0, // SampleEntry.reserved
|
||||
0, 0, 0, 1, // SampleEntry.reserved, SampleEntry.data_reference_index (1)
|
||||
0, 0, 0, 0, // AudioSampleEntry.reserved
|
||||
0, 0, 0, 0, // AudioSampleEntry.reserved
|
||||
]);
|
||||
buf.put_u16(parsed.channels.channels);
|
||||
buf.extend_from_slice(&[
|
||||
0x00, 0x10, // AudioSampleEntry.samplesize
|
||||
0x00, 0x00, 0x00, 0x00, // AudioSampleEntry.pre_defined, AudioSampleEntry.reserved
|
||||
]);
|
||||
|
||||
// ISO/IEC 14496-12 section 12.2.3 says to put the samplerate (aka
|
||||
// clock_rate aka sampling_frequency) as a 16.16 fixed-point number or
|
||||
// use a SamplingRateBox. The latter also requires changing the
|
||||
// version/structure of the AudioSampleEntryBox and the version of the
|
||||
// stsd box. Just support the former for now.
|
||||
let sampling_frequency = u16::try_from(parsed.sampling_frequency).map_err(|_| {
|
||||
format_err!(
|
||||
"aac sampling_frequency={} unsupported",
|
||||
parsed.sampling_frequency
|
||||
)
|
||||
})?;
|
||||
buf.put_u32(u32::from(sampling_frequency) << 16);
|
||||
|
||||
// Write the embedded ESDBox (`esds`), as in ISO/IEC 14496-14 section 5.6.1.
|
||||
write_box!(&mut buf, b"esds", {
|
||||
buf.put_u32(0); // version
|
||||
|
||||
write_descriptor!(&mut buf, 0x03 /* ES_DescrTag */, {
|
||||
// The ESDBox contains an ES_Descriptor, defined in ISO/IEC 14496-1 section 8.3.3.
|
||||
// ISO/IEC 14496-14 section 3.1.2 has advice on how to set its
|
||||
// fields within the scope of a .mp4 file.
|
||||
buf.extend_from_slice(&[
|
||||
0, 0, // ES_ID=0
|
||||
0x00, // streamDependenceFlag, URL_Flag, OCRStreamFlag, streamPriority.
|
||||
]);
|
||||
|
||||
// DecoderConfigDescriptor, defined in ISO/IEC 14496-1 section 7.2.6.6.
|
||||
write_descriptor!(&mut buf, 0x04 /* DecoderConfigDescrTag */, {
|
||||
buf.extend_from_slice(&[
|
||||
0x40, // objectTypeIndication = Audio ISO/IEC 14496-3
|
||||
0x15, // streamType = audio, upstream = false, reserved = 1
|
||||
]);
|
||||
|
||||
// bufferSizeDb is "the size of the decoding buffer for this
|
||||
// elementary stream in byte". ISO/IEC 13818-7 section
|
||||
// 8.2.2.1 defines the total decoder input buffer size as
|
||||
// 6144 bits per NCC.
|
||||
let buffer_size_bytes = (6144 / 8) * u32::from(parsed.channels.ncc);
|
||||
debug_assert!(buffer_size_bytes <= 0xFF_FFFF);
|
||||
|
||||
// buffer_size_bytes as a 24-bit number
|
||||
buf.put_u8((buffer_size_bytes >> 16) as u8);
|
||||
buf.put_u16(buffer_size_bytes as u16);
|
||||
|
||||
let max_bitrate = (6144 / 1024)
|
||||
* u32::from(parsed.channels.ncc)
|
||||
* u32::from(sampling_frequency);
|
||||
buf.put_u32(max_bitrate);
|
||||
|
||||
// avg_bitrate. ISO/IEC 14496-1 section 7.2.6.6 says "for streams with
|
||||
// variable bitrate this value shall be set to zero."
|
||||
buf.put_u32(0);
|
||||
|
||||
// AudioSpecificConfiguration, ISO/IEC 14496-3 subpart 1 section 1.6.2.
|
||||
write_descriptor!(&mut buf, 0x05 /* DecSpecificInfoTag */, {
|
||||
buf.extend_from_slice(config);
|
||||
});
|
||||
});
|
||||
|
||||
// SLConfigDescriptor, ISO/IEC 14496-1 section 7.3.2.3.1.
|
||||
write_descriptor!(&mut buf, 0x06 /* SLConfigDescrTag */, {
|
||||
buf.put_u8(2); // predefined = reserved for use in MP4 files
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Ok(buf.freeze())
|
||||
}
|
||||
|
||||
/// Parses metadata from the `format-specific-params` of a SDP `fmtp` media attribute.
|
||||
/// The metadata is defined in [RFC 3640 section
|
||||
/// 4.1](https://datatracker.ietf.org/doc/html/rfc3640#section-4.1).
|
||||
fn parse_format_specific_params(
|
||||
clock_rate: u32,
|
||||
format_specific_params: &str,
|
||||
) -> Result<super::AudioParameters, Error> {
|
||||
let mut mode = None;
|
||||
let mut config = None;
|
||||
let mut size_length = None;
|
||||
let mut index_length = None;
|
||||
let mut index_delta_length = None;
|
||||
for p in format_specific_params.split(';') {
|
||||
let p = p.trim();
|
||||
if p.is_empty() {
|
||||
// Reolink cameras leave a trailing ';'.
|
||||
continue;
|
||||
}
|
||||
let (key, value) = p
|
||||
.split_once('=')
|
||||
.ok_or_else(|| format_err!("bad format-specific-param {}", p))?;
|
||||
match &key.to_ascii_lowercase()[..] {
|
||||
"config" => {
|
||||
config = Some(
|
||||
hex::decode(value)
|
||||
.map_err(|_| format_err!("config has invalid hex encoding"))?,
|
||||
);
|
||||
}
|
||||
"mode" => mode = Some(value),
|
||||
"sizelength" => {
|
||||
size_length = Some(
|
||||
u16::from_str_radix(value, 10).map_err(|_| format_err!("bad sizeLength"))?,
|
||||
);
|
||||
}
|
||||
"indexlength" => {
|
||||
index_length = Some(
|
||||
u16::from_str_radix(value, 10).map_err(|_| format_err!("bad indexLength"))?,
|
||||
);
|
||||
}
|
||||
"indexdeltalength" => {
|
||||
index_delta_length = Some(
|
||||
u16::from_str_radix(value, 10)
|
||||
.map_err(|_| format_err!("bad indexDeltaLength"))?,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// https://datatracker.ietf.org/doc/html/rfc3640#section-3.3.6 AAC-hbr
|
||||
if mode != Some("AAC-hbr") {
|
||||
bail!("Expected mode AAC-hbr, got {:#?}", mode);
|
||||
}
|
||||
let config = config.ok_or_else(|| format_err!("config must be specified"))?;
|
||||
if size_length != Some(13) || index_length != Some(3) || index_delta_length != Some(3) {
|
||||
bail!(
|
||||
"Unexpected sizeLength={:?} indexLength={:?} indexDeltaLength={:?}",
|
||||
size_length,
|
||||
index_length,
|
||||
index_delta_length
|
||||
);
|
||||
}
|
||||
|
||||
let parsed = AudioSpecificConfig::parse(&config[..])?;
|
||||
|
||||
// TODO: is this a requirement? I might have read somewhere one can be a multiple of the other.
|
||||
if clock_rate != parsed.sampling_frequency {
|
||||
bail!(
|
||||
"Expected RTP clock rate {} and AAC sampling frequency {} to match",
|
||||
clock_rate,
|
||||
parsed.sampling_frequency
|
||||
);
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6381#section-3.3
|
||||
let rfc6381_codec = Some(format!("mp4a.40.{}", parsed.audio_object_type));
|
||||
let frame_length = Some(parsed.frame_length);
|
||||
Ok(super::AudioParameters {
|
||||
config: super::AudioCodecConfig::Aac(parsed),
|
||||
clock_rate,
|
||||
rfc6381_codec,
|
||||
frame_length: frame_length.map(NonZeroU32::from),
|
||||
extra_data: Bytes::from(config),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Depacketizer {
|
||||
parameters: super::Parameters,
|
||||
|
||||
/// This is in parameters but duplicated here to avoid destructuring.
|
||||
frame_length: NonZeroU16,
|
||||
state: DepacketizerState,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Aggregate {
|
||||
ctx: crate::Context,
|
||||
|
||||
/// RTP packets lost before the next frame in this aggregate. Includes old
|
||||
/// loss that caused a previous fragment to be too short.
|
||||
/// This should be 0 when `frame_i > 0`.
|
||||
loss: u16,
|
||||
|
||||
/// True iff there was loss immediately before the packet that started this
|
||||
/// aggregate. The distinction between old and recent loss is relevant
|
||||
/// because only the latter should be capable of causing following fragments
|
||||
/// to be too short.
|
||||
loss_since_mark: bool,
|
||||
|
||||
stream_id: usize,
|
||||
|
||||
/// The RTP-level timestamp; frame `i` is at timestamp `timestamp + frame_length*i`.
|
||||
timestamp: crate::Timestamp,
|
||||
|
||||
/// The buffer, positioned at frame 0's header.
|
||||
buf: Bytes,
|
||||
|
||||
/// The index in range `[0, frame_count)` of the next frame to output.
|
||||
frame_i: u16,
|
||||
|
||||
/// The non-zero total frames within this aggregate.
|
||||
frame_count: u16,
|
||||
|
||||
/// The starting byte offset of `frame_i`'s data within `buf`.
|
||||
data_off: usize,
|
||||
|
||||
/// If a mark was set on this packet. When this is false, this should
|
||||
/// actually be the start of a fragmented frame, but that conversion is
|
||||
/// currently deferred until `pull`.
|
||||
mark: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Fragment {
|
||||
rtp_timestamp: u16,
|
||||
|
||||
/// Number of RTP packets lost before between the previous output AudioFrame
|
||||
/// and now.
|
||||
loss: u16,
|
||||
|
||||
/// True iff packets have been lost since the last mark. If so, this
|
||||
/// fragment may be incomplete.
|
||||
loss_since_mark: bool,
|
||||
|
||||
size: u16,
|
||||
buf: BytesMut,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum DepacketizerState {
|
||||
Idle { prev_loss: u16 },
|
||||
Aggregated(Aggregate),
|
||||
Fragmented(Fragment),
|
||||
Ready(super::AudioFrame),
|
||||
}
|
||||
|
||||
impl Depacketizer {
|
||||
pub(super) fn new(
|
||||
clock_rate: u32,
|
||||
channels: Option<NonZeroU16>,
|
||||
format_specific_params: Option<&str>,
|
||||
) -> Result<Self, Error> {
|
||||
let format_specific_params = format_specific_params
|
||||
.ok_or_else(|| format_err!("AAC requires format specific params"))?;
|
||||
let parameters = parse_format_specific_params(clock_rate, format_specific_params)?;
|
||||
let parsed = match parameters.config {
|
||||
super::AudioCodecConfig::Aac(ref c) => c,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if matches!(channels, Some(c) if c.get() != parsed.channels.channels) {
|
||||
bail!(
|
||||
"Expected RTP channels {:?} and AAC channels {:?} to match",
|
||||
channels,
|
||||
parsed.channels
|
||||
);
|
||||
}
|
||||
let frame_length = parsed.frame_length;
|
||||
Ok(Self {
|
||||
parameters: super::Parameters::Audio(parameters),
|
||||
frame_length,
|
||||
state: DepacketizerState::Idle { prev_loss: 0 },
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn parameters(&self) -> Option<&super::Parameters> {
|
||||
Some(&self.parameters)
|
||||
}
|
||||
|
||||
pub(super) fn push(&mut self, mut pkt: Packet) -> Result<(), Error> {
|
||||
if pkt.loss > 0 && matches!(self.state, DepacketizerState::Fragmented(_)) {
|
||||
log::debug!(
|
||||
"Discarding fragmented AAC frame due to loss of {} RTP packets.",
|
||||
pkt.loss
|
||||
);
|
||||
self.state = DepacketizerState::Idle { prev_loss: 0 };
|
||||
}
|
||||
|
||||
// Read the AU headers.
|
||||
if pkt.payload.len() < 2 {
|
||||
bail!("packet too short for au-header-length");
|
||||
}
|
||||
let au_headers_length_bits = pkt.payload.get_u16();
|
||||
|
||||
// AAC-hbr requires 16-bit AU headers: 13-bit size, 3-bit index.
|
||||
if (au_headers_length_bits & 0x7) != 0 {
|
||||
bail!("bad au-headers-length {}", au_headers_length_bits);
|
||||
}
|
||||
let au_headers_count = au_headers_length_bits >> 4;
|
||||
let data_off = usize::from(au_headers_count) << 1;
|
||||
if pkt.payload.len() < (usize::from(au_headers_count) << 1) {
|
||||
bail!("packet too short for au-headers");
|
||||
}
|
||||
match &mut self.state {
|
||||
DepacketizerState::Fragmented(ref mut frag) => {
|
||||
if au_headers_count != 1 {
|
||||
bail!(
|
||||
"Got {}-AU packet while fragment in progress",
|
||||
au_headers_count
|
||||
);
|
||||
}
|
||||
if (pkt.timestamp.timestamp as u16) != frag.rtp_timestamp {
|
||||
bail!(
|
||||
"Timestamp changed from 0x{:04x} to 0x{:04x} mid-fragment",
|
||||
frag.rtp_timestamp,
|
||||
pkt.timestamp.timestamp as u16
|
||||
);
|
||||
}
|
||||
let au_header = u16::from_be_bytes([pkt.payload[0], pkt.payload[1]]);
|
||||
let size = usize::from(au_header >> 3);
|
||||
if size != usize::from(frag.size) {
|
||||
bail!("size changed {}->{} mid-fragment", frag.size, size);
|
||||
}
|
||||
let data = &pkt.payload[data_off..];
|
||||
match (frag.buf.len() + data.len()).cmp(&size) {
|
||||
std::cmp::Ordering::Less => {
|
||||
if pkt.mark {
|
||||
if frag.loss > 0 {
|
||||
self.state = DepacketizerState::Idle {
|
||||
prev_loss: frag.loss,
|
||||
};
|
||||
return Ok(());
|
||||
}
|
||||
bail!(
|
||||
"frag marked complete when {}+{}<{}",
|
||||
frag.buf.len(),
|
||||
data.len(),
|
||||
size
|
||||
);
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Equal => {
|
||||
if !pkt.mark {
|
||||
bail!("frag not marked complete when full data present");
|
||||
}
|
||||
frag.buf.extend_from_slice(data);
|
||||
println!("au {}: len-{}, fragmented", &pkt.timestamp, size);
|
||||
self.state = DepacketizerState::Ready(super::AudioFrame {
|
||||
ctx: pkt.rtsp_ctx,
|
||||
loss: frag.loss,
|
||||
frame_length: NonZeroU32::from(self.frame_length),
|
||||
stream_id: pkt.stream_id,
|
||||
timestamp: pkt.timestamp,
|
||||
data: std::mem::take(&mut frag.buf).freeze(),
|
||||
});
|
||||
}
|
||||
std::cmp::Ordering::Greater => bail!("too much data in fragment"),
|
||||
}
|
||||
}
|
||||
DepacketizerState::Aggregated(_) => panic!("push when already in state aggregated"),
|
||||
DepacketizerState::Idle { prev_loss } => {
|
||||
if au_headers_count == 0 {
|
||||
bail!("aggregate with no headers");
|
||||
}
|
||||
self.state = DepacketizerState::Aggregated(Aggregate {
|
||||
ctx: pkt.rtsp_ctx,
|
||||
loss: *prev_loss + pkt.loss,
|
||||
loss_since_mark: pkt.loss > 0,
|
||||
stream_id: pkt.stream_id,
|
||||
timestamp: pkt.timestamp,
|
||||
buf: pkt.payload,
|
||||
frame_i: 0,
|
||||
frame_count: au_headers_count,
|
||||
data_off,
|
||||
mark: pkt.mark,
|
||||
});
|
||||
}
|
||||
DepacketizerState::Ready(..) => panic!("push when in state ready"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn pull(&mut self) -> Result<Option<super::CodecItem>, Error> {
|
||||
match std::mem::replace(&mut self.state, DepacketizerState::Idle { prev_loss: 0 }) {
|
||||
s @ DepacketizerState::Idle { .. } | s @ DepacketizerState::Fragmented(..) => {
|
||||
self.state = s;
|
||||
Ok(None)
|
||||
}
|
||||
DepacketizerState::Ready(f) => {
|
||||
self.state = DepacketizerState::Idle { prev_loss: 0 };
|
||||
Ok(Some(CodecItem::AudioFrame(f)))
|
||||
}
|
||||
DepacketizerState::Aggregated(mut agg) => {
|
||||
let i = usize::from(agg.frame_i);
|
||||
let au_header = u16::from_be_bytes([agg.buf[i << 1], agg.buf[(i << 1) + 1]]);
|
||||
let size = usize::from(au_header >> 3);
|
||||
let index = au_header & 0b111;
|
||||
if index != 0 {
|
||||
// First AU's index must be zero; subsequent AU's deltas > 1
|
||||
// indicate interleaving, which we don't support.
|
||||
// TODO: https://datatracker.ietf.org/doc/html/rfc3640#section-3.3.6
|
||||
// says "receivers MUST support de-interleaving".
|
||||
bail!("interleaving not yet supported");
|
||||
}
|
||||
if size > agg.buf.len() - agg.data_off {
|
||||
// start of fragment
|
||||
if agg.frame_count != 1 {
|
||||
bail!("fragmented AUs must not share packets");
|
||||
}
|
||||
if agg.mark {
|
||||
bail!("mark can't be set on beginning of fragment");
|
||||
}
|
||||
let mut buf = BytesMut::with_capacity(size);
|
||||
buf.extend_from_slice(&agg.buf[agg.data_off..]);
|
||||
self.state = DepacketizerState::Fragmented(Fragment {
|
||||
rtp_timestamp: agg.timestamp.timestamp as u16,
|
||||
loss: agg.loss,
|
||||
loss_since_mark: agg.loss_since_mark,
|
||||
size: size as u16,
|
||||
buf,
|
||||
});
|
||||
return Ok(None);
|
||||
}
|
||||
if !agg.mark {
|
||||
bail!("mark must be set on non-fragmented au");
|
||||
}
|
||||
let frame = super::AudioFrame {
|
||||
ctx: agg.ctx,
|
||||
loss: agg.loss,
|
||||
stream_id: agg.stream_id,
|
||||
frame_length: NonZeroU32::from(self.frame_length),
|
||||
|
||||
// u16 * u16 can't overflow u32, but i64 + u32 can overflow i64.
|
||||
timestamp: agg
|
||||
.timestamp
|
||||
.try_add(u32::from(agg.frame_i) * u32::from(self.frame_length.get()))?,
|
||||
data: agg.buf.slice(agg.data_off..agg.data_off + size),
|
||||
};
|
||||
agg.loss = 0;
|
||||
agg.data_off += size;
|
||||
agg.frame_i += 1;
|
||||
if agg.frame_i < agg.frame_count {
|
||||
self.state = DepacketizerState::Aggregated(agg);
|
||||
}
|
||||
Ok(Some(CodecItem::AudioFrame(frame)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn parse_audio_specific_config() {
|
||||
let dahua = super::AudioSpecificConfig::parse(&[0x11, 0x88]).unwrap();
|
||||
assert_eq!(dahua.sampling_frequency, 48_000);
|
||||
assert_eq!(dahua.channels.name, "mono");
|
||||
|
||||
let bunny = super::AudioSpecificConfig::parse(&[0x14, 0x90]).unwrap();
|
||||
assert_eq!(bunny.sampling_frequency, 12_000);
|
||||
assert_eq!(bunny.channels.name, "stereo");
|
||||
|
||||
let rfc3640 = super::AudioSpecificConfig::parse(&[0x11, 0xB0]).unwrap();
|
||||
assert_eq!(rfc3640.sampling_frequency, 48_000);
|
||||
assert_eq!(rfc3640.channels.name, "5.1");
|
||||
}
|
||||
}
|
70
src/codec/g723.rs
Normal file
70
src/codec/g723.rs
Normal file
@ -0,0 +1,70 @@
|
||||
// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
//! G.723.1 audio as specified in [RFC 3551 section 4.5.3](https://datatracker.ietf.org/doc/html/rfc3551#section-4.5.3).
|
||||
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
use bytes::Bytes;
|
||||
use failure::{bail, Error};
|
||||
use pretty_hex::PrettyHex;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Depacketizer {
|
||||
parameters: super::Parameters,
|
||||
pending: Option<super::AudioFrame>,
|
||||
}
|
||||
|
||||
impl Depacketizer {
|
||||
/// Creates a new Depacketizer.
|
||||
pub(super) fn new(clock_rate: u32) -> Result<Self, Error> {
|
||||
if clock_rate != 8_000 {
|
||||
bail!("Expected clock rate of 8000 for G.723, got {}", clock_rate);
|
||||
}
|
||||
Ok(Self {
|
||||
parameters: super::Parameters::Audio(super::AudioParameters {
|
||||
rfc6381_codec: None,
|
||||
frame_length: NonZeroU32::new(240),
|
||||
clock_rate,
|
||||
extra_data: Bytes::new(),
|
||||
config: super::AudioCodecConfig::Other,
|
||||
}),
|
||||
pending: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn parameters(&self) -> Option<&super::Parameters> {
|
||||
Some(&self.parameters)
|
||||
}
|
||||
|
||||
fn validate(pkt: &crate::client::rtp::Packet) -> bool {
|
||||
let expected_hdr_bits = match pkt.payload.len() {
|
||||
24 => 0b00,
|
||||
20 => 0b01,
|
||||
4 => 0b10,
|
||||
_ => return false,
|
||||
};
|
||||
let actual_hdr_bits = pkt.payload[0] & 0b11;
|
||||
actual_hdr_bits == expected_hdr_bits
|
||||
}
|
||||
|
||||
pub(super) fn push(&mut self, pkt: crate::client::rtp::Packet) -> Result<(), Error> {
|
||||
assert!(self.pending.is_none());
|
||||
if !Self::validate(&pkt) {
|
||||
bail!("Invalid G.723 packet: {:#?}", pkt.payload.hex_dump());
|
||||
}
|
||||
self.pending = Some(super::AudioFrame {
|
||||
ctx: pkt.rtsp_ctx,
|
||||
loss: pkt.loss,
|
||||
stream_id: pkt.stream_id,
|
||||
timestamp: pkt.timestamp,
|
||||
frame_length: NonZeroU32::new(240).unwrap(),
|
||||
data: pkt.payload,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn pull(&mut self) -> Result<Option<super::CodecItem>, Error> {
|
||||
Ok(self.pending.take().map(super::CodecItem::AudioFrame))
|
||||
}
|
||||
}
|
621
src/codec/h264.rs
Normal file
621
src/codec/h264.rs
Normal file
@ -0,0 +1,621 @@
|
||||
// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
//! [H.264](https://www.itu.int/rec/T-REC-H.264-201906-I/en)-encoded video.
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||
use failure::{bail, format_err, Error};
|
||||
use h264_reader::nal::UnitType;
|
||||
use log::debug;
|
||||
|
||||
use crate::client::rtp::Packet;
|
||||
|
||||
/// A [super::Depacketizer] implementation which finds access unit boundaries
|
||||
/// and produces unfragmented NAL units as specified in [RFC
|
||||
/// 6184](https://tools.ietf.org/html/rfc6184).
|
||||
///
|
||||
/// This doesn't inspect the contents of the NAL units, so it doesn't depend on or
|
||||
/// verify compliance with H.264 section 7.4.1.2.3 "Order of NAL units and coded
|
||||
/// pictures and association to access units".
|
||||
///
|
||||
/// Currently expects that the stream starts at an access unit boundary and has no lost packets.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Depacketizer {
|
||||
input_state: DepacketizerInputState,
|
||||
pending: Option<AccessUnit>,
|
||||
parameters: InternalParameters,
|
||||
|
||||
/// The largest fragment used. This is used for the buffer capacity on subsequent fragments, minimizing reallocation.
|
||||
frag_high_water: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AccessUnit {
|
||||
start_ctx: crate::Context,
|
||||
end_ctx: crate::Context,
|
||||
timestamp: crate::Timestamp,
|
||||
stream_id: usize,
|
||||
new_sps: Option<Bytes>,
|
||||
new_pps: Option<Bytes>,
|
||||
|
||||
/// RTP packets lost as this access unit was starting.
|
||||
loss: u16,
|
||||
|
||||
/// Currently we expect only a single slice NAL.
|
||||
picture: Option<Bytes>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PreMark {
|
||||
/// If a FU-A fragment is in progress, the buffer used to accumulate the NAL.
|
||||
frag_buf: Option<BytesMut>,
|
||||
|
||||
access_unit: AccessUnit,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::clippy::large_enum_variant)]
|
||||
enum DepacketizerInputState {
|
||||
/// Not yet processing an access unit.
|
||||
New,
|
||||
|
||||
Loss {
|
||||
timestamp: crate::Timestamp,
|
||||
pkts: u16,
|
||||
},
|
||||
|
||||
/// Currently processing an access unit.
|
||||
/// This will be flushed after a marked packet or when receiving a later timestamp.
|
||||
PreMark(PreMark),
|
||||
|
||||
/// Finished processing the given packet. It's an error to receive the same timestamp again.
|
||||
PostMark {
|
||||
timestamp: crate::Timestamp,
|
||||
loss: u16,
|
||||
},
|
||||
}
|
||||
|
||||
impl Depacketizer {
|
||||
pub(super) fn new(
|
||||
clock_rate: u32,
|
||||
format_specific_params: Option<&str>,
|
||||
) -> Result<Self, Error> {
|
||||
if clock_rate != 90_000 {
|
||||
bail!("H.264 clock rate must always be 90000");
|
||||
}
|
||||
|
||||
// TODO: the spec doesn't require out-of-band parameters, so we shouldn't either.
|
||||
let format_specific_params = format_specific_params
|
||||
.ok_or_else(|| format_err!("H.264 depacketizer expects out-of-band parameters"))?;
|
||||
Ok(Depacketizer {
|
||||
input_state: DepacketizerInputState::New,
|
||||
pending: None,
|
||||
frag_high_water: 0,
|
||||
parameters: InternalParameters::parse_format_specific_params(format_specific_params)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn parameters(&self) -> Option<&super::Parameters> {
|
||||
Some(&self.parameters.generic_parameters)
|
||||
}
|
||||
|
||||
pub(super) fn push(&mut self, pkt: Packet) -> Result<(), Error> {
|
||||
// Push shouldn't be called until pull is exhausted.
|
||||
if let Some(p) = self.pending.as_ref() {
|
||||
panic!("push with data already pending: {:?}", p);
|
||||
}
|
||||
|
||||
// The rtp crate also has [H.264 depacketization
|
||||
// logic](https://docs.rs/rtp/0.2.2/rtp/codecs/h264/struct.H264Packet.html),
|
||||
// but it doesn't seem to match my use case. I want to iterate the NALs,
|
||||
// not re-encode them in Annex B format.
|
||||
let seq = pkt.sequence_number;
|
||||
let mut premark = match std::mem::replace(
|
||||
&mut self.input_state,
|
||||
DepacketizerInputState::New,
|
||||
) {
|
||||
DepacketizerInputState::New => PreMark {
|
||||
access_unit: AccessUnit::start(&pkt),
|
||||
frag_buf: None,
|
||||
},
|
||||
DepacketizerInputState::PreMark(mut premark) => {
|
||||
if pkt.loss > 0 {
|
||||
if premark.access_unit.timestamp.timestamp == pkt.timestamp.timestamp {
|
||||
// Loss within this access unit. Ignore until mark or new timestamp.
|
||||
self.input_state = if pkt.mark {
|
||||
DepacketizerInputState::PostMark {
|
||||
timestamp: pkt.timestamp,
|
||||
loss: pkt.loss,
|
||||
}
|
||||
} else {
|
||||
DepacketizerInputState::Loss {
|
||||
timestamp: pkt.timestamp,
|
||||
pkts: pkt.loss,
|
||||
}
|
||||
};
|
||||
return Ok(());
|
||||
}
|
||||
// A suffix of a previous access unit was lost; discard it.
|
||||
// A prefix of the new one may have been lost; try parsing.
|
||||
PreMark {
|
||||
access_unit: AccessUnit::start(&pkt),
|
||||
frag_buf: None,
|
||||
}
|
||||
} else {
|
||||
if premark.access_unit.timestamp.timestamp != pkt.timestamp.timestamp {
|
||||
if premark.frag_buf.is_some() {
|
||||
bail!("Timestamp changed from {} to {} in the middle of a fragmented NAL at seq={:04x} {:#?}", premark.access_unit.timestamp, pkt.timestamp, seq, &pkt.rtsp_ctx);
|
||||
}
|
||||
premark.access_unit.end_ctx = pkt.rtsp_ctx;
|
||||
self.pending = Some(std::mem::replace(
|
||||
&mut premark.access_unit,
|
||||
AccessUnit::start(&pkt),
|
||||
));
|
||||
}
|
||||
premark
|
||||
}
|
||||
}
|
||||
DepacketizerInputState::PostMark {
|
||||
timestamp: state_ts,
|
||||
loss,
|
||||
} => {
|
||||
if state_ts.timestamp == pkt.timestamp.timestamp {
|
||||
bail!("Received packet with timestamp {} after marked packet with same timestamp at seq={:04x} {:#?}", pkt.timestamp, seq, &pkt.rtsp_ctx);
|
||||
}
|
||||
let mut access_unit = AccessUnit::start(&pkt);
|
||||
access_unit.loss += loss;
|
||||
PreMark {
|
||||
access_unit,
|
||||
frag_buf: None,
|
||||
}
|
||||
}
|
||||
DepacketizerInputState::Loss {
|
||||
timestamp,
|
||||
mut pkts,
|
||||
} => {
|
||||
if pkt.timestamp.timestamp == timestamp.timestamp {
|
||||
pkts += pkt.loss;
|
||||
self.input_state = DepacketizerInputState::Loss { timestamp, pkts };
|
||||
return Ok(());
|
||||
}
|
||||
let mut access_unit = AccessUnit::start(&pkt);
|
||||
access_unit.loss += pkts;
|
||||
PreMark {
|
||||
access_unit,
|
||||
frag_buf: None,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut data = pkt.payload;
|
||||
if data.is_empty() {
|
||||
bail!("Empty NAL at RTP seq {:04x}, {:#?}", seq, &pkt.rtsp_ctx);
|
||||
}
|
||||
// https://tools.ietf.org/html/rfc6184#section-5.2
|
||||
let nal_header = data[0];
|
||||
if (nal_header >> 7) != 0 {
|
||||
bail!(
|
||||
"NAL header has F bit set at seq {:04x} {:#?}",
|
||||
seq,
|
||||
&pkt.rtsp_ctx
|
||||
);
|
||||
}
|
||||
match nal_header & 0b11111 {
|
||||
1..=23 => {
|
||||
if premark.frag_buf.is_some() {
|
||||
bail!(
|
||||
"Non-fragmented NAL while fragment in progress seq {:04x} {:#?}",
|
||||
seq,
|
||||
&pkt.rtsp_ctx
|
||||
);
|
||||
}
|
||||
premark.access_unit.nal(&mut self.parameters, data)?;
|
||||
}
|
||||
24 => {
|
||||
// STAP-A. https://tools.ietf.org/html/rfc6184#section-5.7.1
|
||||
data.advance(1); // skip the header byte.
|
||||
loop {
|
||||
if data.remaining() < 2 {
|
||||
bail!(
|
||||
"STAP-A has {} remaining bytes while expecting 2-byte length",
|
||||
data.remaining()
|
||||
);
|
||||
}
|
||||
let len = usize::from(data.get_u16());
|
||||
match data.remaining().cmp(&len) {
|
||||
std::cmp::Ordering::Less => bail!(
|
||||
"STAP-A too short: {} bytes remaining, expecting {}-byte NAL",
|
||||
data.remaining(),
|
||||
len
|
||||
),
|
||||
std::cmp::Ordering::Equal => {
|
||||
premark.access_unit.nal(&mut self.parameters, data)?;
|
||||
break;
|
||||
}
|
||||
std::cmp::Ordering::Greater => premark
|
||||
.access_unit
|
||||
.nal(&mut self.parameters, data.split_to(len))?,
|
||||
}
|
||||
}
|
||||
}
|
||||
25..=27 | 29 => unimplemented!(
|
||||
"unimplemented NAL (header 0x{:02x}) at seq {:04x} {:#?}",
|
||||
nal_header,
|
||||
seq,
|
||||
&pkt.rtsp_ctx
|
||||
),
|
||||
28 => {
|
||||
// FU-A. https://tools.ietf.org/html/rfc6184#section-5.8
|
||||
if data.len() < 3 {
|
||||
bail!("FU-A is too short at seq {:04x} {:#?}", seq, &pkt.rtsp_ctx);
|
||||
}
|
||||
let fu_header = data[1];
|
||||
let start = (fu_header & 0b10000000) != 0;
|
||||
let end = (fu_header & 0b01000000) != 0;
|
||||
let reserved = (fu_header & 0b00100000) != 0;
|
||||
let nal_header = (nal_header & 0b011100000) | (fu_header & 0b00011111);
|
||||
if (start && end) || reserved {
|
||||
bail!(
|
||||
"Invalid FU-A header {:08b} at seq {:04x} {:#?}",
|
||||
fu_header,
|
||||
seq,
|
||||
&pkt.rtsp_ctx
|
||||
);
|
||||
}
|
||||
match (start, premark.frag_buf.take()) {
|
||||
(true, Some(_)) => bail!(
|
||||
"FU-A with start bit while frag in progress at seq {:04x} {:#?}",
|
||||
seq,
|
||||
&pkt.rtsp_ctx
|
||||
),
|
||||
(true, None) => {
|
||||
let mut frag_buf = BytesMut::with_capacity(std::cmp::max(
|
||||
self.frag_high_water,
|
||||
data.len() - 1,
|
||||
));
|
||||
frag_buf.put_u8(nal_header);
|
||||
data.advance(2);
|
||||
frag_buf.put(data);
|
||||
premark.frag_buf = Some(frag_buf);
|
||||
}
|
||||
(false, Some(mut frag_buf)) => {
|
||||
if frag_buf[0] != nal_header {
|
||||
bail!("FU-A has inconsistent NAL type: {:08b} then {:08b} at seq {:04x} {:#?}", frag_buf[0], nal_header, seq, &pkt.rtsp_ctx);
|
||||
}
|
||||
data.advance(2);
|
||||
frag_buf.put(data);
|
||||
if end {
|
||||
self.frag_high_water = frag_buf.len();
|
||||
premark
|
||||
.access_unit
|
||||
.nal(&mut self.parameters, frag_buf.freeze())?;
|
||||
} else if pkt.mark {
|
||||
bail!(
|
||||
"FU-A with MARK and no END at seq {:04x} {:#?}",
|
||||
seq,
|
||||
pkt.rtsp_ctx
|
||||
);
|
||||
} else {
|
||||
premark.frag_buf = Some(frag_buf);
|
||||
}
|
||||
}
|
||||
(false, None) => {
|
||||
if pkt.loss > 0 {
|
||||
self.input_state = DepacketizerInputState::Loss {
|
||||
timestamp: pkt.timestamp,
|
||||
pkts: pkt.loss,
|
||||
};
|
||||
return Ok(());
|
||||
}
|
||||
bail!(
|
||||
"FU-A with start bit unset while no frag in progress at {:04x} {:#?}",
|
||||
seq,
|
||||
&pkt.rtsp_ctx
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => bail!(
|
||||
"bad nal header {:0x} at seq {:04x} {:#?}",
|
||||
nal_header,
|
||||
seq,
|
||||
&pkt.rtsp_ctx
|
||||
),
|
||||
}
|
||||
self.input_state = if pkt.mark {
|
||||
premark.access_unit.end_ctx = pkt.rtsp_ctx;
|
||||
self.pending = Some(premark.access_unit);
|
||||
DepacketizerInputState::PostMark {
|
||||
timestamp: pkt.timestamp,
|
||||
loss: 0,
|
||||
}
|
||||
} else {
|
||||
DepacketizerInputState::PreMark(premark)
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn pull(&mut self) -> Result<Option<super::CodecItem>, Error> {
|
||||
let pending = match self.pending.take() {
|
||||
None => return Ok(None),
|
||||
Some(p) => p,
|
||||
};
|
||||
let new_parameters = if pending.new_sps.is_some() || pending.new_pps.is_some() {
|
||||
let sps_nal = pending
|
||||
.new_sps
|
||||
.as_deref()
|
||||
.unwrap_or(&self.parameters.sps_nal);
|
||||
let pps_nal = pending
|
||||
.new_pps
|
||||
.as_deref()
|
||||
.unwrap_or(&self.parameters.pps_nal);
|
||||
self.parameters = InternalParameters::parse_sps_and_pps(sps_nal, pps_nal)?;
|
||||
match self.parameters.generic_parameters {
|
||||
super::Parameters::Video(ref p) => Some(p.clone()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let picture = pending
|
||||
.picture
|
||||
.ok_or_else(|| format_err!("access unit has no picture"))?;
|
||||
let nal_header =
|
||||
h264_reader::nal::NalHeader::new(picture[0]).expect("nal header was previously valid");
|
||||
Ok(Some(super::CodecItem::VideoFrame(super::VideoFrame {
|
||||
start_ctx: pending.start_ctx,
|
||||
end_ctx: pending.end_ctx,
|
||||
loss: pending.loss,
|
||||
new_parameters,
|
||||
timestamp: pending.timestamp,
|
||||
stream_id: pending.stream_id,
|
||||
is_random_access_point: nal_header.nal_unit_type()
|
||||
== UnitType::SliceLayerWithoutPartitioningIdr,
|
||||
is_disposable: nal_header.nal_ref_idc() == 0,
|
||||
pos: 0,
|
||||
data_prefix: u32::try_from(picture.len()).unwrap().to_be_bytes(),
|
||||
data: picture,
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
impl AccessUnit {
|
||||
fn start(pkt: &crate::client::rtp::Packet) -> Self {
|
||||
AccessUnit {
|
||||
start_ctx: pkt.rtsp_ctx,
|
||||
end_ctx: pkt.rtsp_ctx,
|
||||
timestamp: pkt.timestamp,
|
||||
stream_id: pkt.stream_id,
|
||||
loss: pkt.loss,
|
||||
new_sps: None,
|
||||
new_pps: None,
|
||||
picture: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn nal(&mut self, parameters: &mut InternalParameters, nal: Bytes) -> Result<(), Error> {
|
||||
let nal_header = h264_reader::nal::NalHeader::new(nal[0])
|
||||
.map_err(|e| format_err!("bad NAL header 0x{:x}: {:#?}", nal[0], e))?;
|
||||
let unit_type = nal_header.nal_unit_type();
|
||||
match unit_type {
|
||||
UnitType::SeqParameterSet => {
|
||||
if self.new_sps.is_some() {
|
||||
bail!("multiple SPSs in access unit");
|
||||
}
|
||||
if nal != parameters.sps_nal {
|
||||
self.new_sps = Some(nal);
|
||||
}
|
||||
}
|
||||
UnitType::PicParameterSet => {
|
||||
if self.new_pps.is_some() {
|
||||
bail!("multiple PPSs in access unit");
|
||||
}
|
||||
if nal != parameters.pps_nal {
|
||||
self.new_pps = Some(nal);
|
||||
}
|
||||
}
|
||||
UnitType::SliceLayerWithoutPartitioningIdr
|
||||
| UnitType::SliceLayerWithoutPartitioningNonIdr => {
|
||||
if self.picture.is_some() {
|
||||
bail!("currently expect only one picture NAL per access unit");
|
||||
}
|
||||
self.picture = Some(nal);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes a NAL unit (minus header byte) into its RBSP.
|
||||
/// Stolen from h264-reader's src/avcc.rs. This shouldn't last long, see:
|
||||
/// <https://github.com/dholroyd/h264-reader/issues/4>.
|
||||
fn decode(encoded: &[u8]) -> Vec<u8> {
|
||||
struct NalRead(Vec<u8>);
|
||||
use h264_reader::nal::NalHandler;
|
||||
use h264_reader::Context;
|
||||
impl NalHandler for NalRead {
|
||||
type Ctx = ();
|
||||
fn start(&mut self, _ctx: &mut Context<Self::Ctx>, _header: h264_reader::nal::NalHeader) {}
|
||||
|
||||
fn push(&mut self, _ctx: &mut Context<Self::Ctx>, buf: &[u8]) {
|
||||
self.0.extend_from_slice(buf)
|
||||
}
|
||||
|
||||
fn end(&mut self, _ctx: &mut Context<Self::Ctx>) {}
|
||||
}
|
||||
let mut decode = h264_reader::rbsp::RbspDecoder::new(NalRead(vec![]));
|
||||
let mut ctx = Context::new(());
|
||||
decode.push(&mut ctx, encoded);
|
||||
let read = decode.into_handler();
|
||||
read.0
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct InternalParameters {
|
||||
generic_parameters: super::Parameters,
|
||||
|
||||
/// The (single) SPS NAL.
|
||||
sps_nal: Bytes,
|
||||
|
||||
/// The (single) PPS NAL.
|
||||
pps_nal: Bytes,
|
||||
}
|
||||
|
||||
impl InternalParameters {
|
||||
/// Parses metadata from the `format-specific-params` of a SDP `fmtp` media attribute.
|
||||
fn parse_format_specific_params(format_specific_params: &str) -> Result<Self, Error> {
|
||||
let mut sprop_parameter_sets = None;
|
||||
for p in format_specific_params.split(';') {
|
||||
let (key, value) = p.trim().split_once('=').unwrap();
|
||||
if key == "sprop-parameter-sets" {
|
||||
sprop_parameter_sets = Some(value);
|
||||
}
|
||||
}
|
||||
let sprop_parameter_sets = sprop_parameter_sets.ok_or_else(|| {
|
||||
format_err!("no sprop-parameter-sets in H.264 format-specific-params")
|
||||
})?;
|
||||
|
||||
let mut sps_nal = None;
|
||||
let mut pps_nal = None;
|
||||
for nal in sprop_parameter_sets.split(',') {
|
||||
let nal =
|
||||
base64::decode(nal).map_err(|_| format_err!("NAL has invalid base64 encoding"))?;
|
||||
if nal.is_empty() {
|
||||
bail!("empty NAL");
|
||||
}
|
||||
let header = h264_reader::nal::NalHeader::new(nal[0])
|
||||
.map_err(|_| format_err!("bad NAL header {:0x}", nal[0]))?;
|
||||
match header.nal_unit_type() {
|
||||
UnitType::SeqParameterSet => {
|
||||
if sps_nal.is_some() {
|
||||
bail!("multiple SPSs");
|
||||
}
|
||||
sps_nal = Some(nal);
|
||||
}
|
||||
UnitType::PicParameterSet => {
|
||||
if pps_nal.is_some() {
|
||||
bail!("multiple PPSs");
|
||||
}
|
||||
pps_nal = Some(nal);
|
||||
}
|
||||
_ => bail!("only SPS and PPS expected in parameter sets"),
|
||||
}
|
||||
}
|
||||
let sps_nal = sps_nal.ok_or_else(|| format_err!("no sps"))?;
|
||||
let pps_nal = pps_nal.ok_or_else(|| format_err!("no pps"))?;
|
||||
|
||||
// GW security GW4089IP leaves Annex B start codes at the end of both
|
||||
// SPS and PPS in the sprop-parameter-sets. Leaving them in means
|
||||
// there's an immediate parameter change (from in-band parameters) once
|
||||
// the first frame is received. Strip them out.
|
||||
let sps_nal = sps_nal
|
||||
.strip_suffix(b"\x00\x00\x00\x01")
|
||||
.unwrap_or(&sps_nal);
|
||||
let pps_nal = pps_nal
|
||||
.strip_suffix(b"\x00\x00\x00\x01")
|
||||
.unwrap_or(&pps_nal);
|
||||
Self::parse_sps_and_pps(sps_nal, pps_nal)
|
||||
}
|
||||
|
||||
fn parse_sps_and_pps(sps_nal: &[u8], pps_nal: &[u8]) -> Result<InternalParameters, Error> {
|
||||
let sps_rbsp = decode(&sps_nal[1..]);
|
||||
if sps_rbsp.len() < 4 {
|
||||
bail!("bad sps");
|
||||
}
|
||||
let rfc6381_codec = format!(
|
||||
"avc1.{:02X}{:02X}{:02X}",
|
||||
sps_rbsp[0], sps_rbsp[1], sps_rbsp[2]
|
||||
);
|
||||
let sps = h264_reader::nal::sps::SeqParameterSet::from_bytes(&sps_rbsp)
|
||||
.map_err(|e| format_err!("Bad SPS: {:?}", e))?;
|
||||
debug!("sps: {:#?}", &sps);
|
||||
|
||||
let pixel_dimensions = sps
|
||||
.pixel_dimensions()
|
||||
.map_err(|e| format_err!("SPS has invalid pixel dimensions: {:?}", e))?;
|
||||
|
||||
// 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).
|
||||
let mut avc_decoder_config = BytesMut::with_capacity(11 + sps_nal.len() + pps_nal.len());
|
||||
avc_decoder_config.put_u8(1); // configurationVersion
|
||||
avc_decoder_config.extend(&sps_rbsp[0..=2]); // profile_idc . AVCProfileIndication
|
||||
// ...misc bits... . profile_compatibility
|
||||
// level_idc . AVCLevelIndication
|
||||
|
||||
// Hardcode lengthSizeMinusOne to 3, matching TransformSampleData's 4-byte
|
||||
// lengths.
|
||||
avc_decoder_config.put_u8(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).
|
||||
avc_decoder_config.put_u8(0xe1);
|
||||
avc_decoder_config.extend(&u16::try_from(sps_nal.len())?.to_be_bytes()[..]);
|
||||
let sps_nal_start = avc_decoder_config.len();
|
||||
avc_decoder_config.extend_from_slice(sps_nal);
|
||||
let sps_nal_end = avc_decoder_config.len();
|
||||
avc_decoder_config.put_u8(1); // # of PPSs.
|
||||
avc_decoder_config.extend(&u16::try_from(pps_nal.len())?.to_be_bytes()[..]);
|
||||
let pps_nal_start = avc_decoder_config.len();
|
||||
avc_decoder_config.extend_from_slice(pps_nal);
|
||||
let pps_nal_end = avc_decoder_config.len();
|
||||
assert_eq!(avc_decoder_config.len(), 11 + sps_nal.len() + pps_nal.len());
|
||||
|
||||
let (pixel_aspect_ratio, frame_rate);
|
||||
match sps.vui_parameters {
|
||||
Some(ref vui) => {
|
||||
pixel_aspect_ratio = vui
|
||||
.aspect_ratio_info
|
||||
.as_ref()
|
||||
.and_then(|a| a.clone().get())
|
||||
.map(|(h, v)| (u32::from(h), (u32::from(v))));
|
||||
|
||||
// TODO: study H.264, (E-34). This quick'n'dirty calculation isn't always right.
|
||||
frame_rate = vui
|
||||
.timing_info
|
||||
.as_ref()
|
||||
.map(|t| (2 * t.num_units_in_tick, t.time_scale));
|
||||
}
|
||||
None => {
|
||||
pixel_aspect_ratio = None;
|
||||
frame_rate = None;
|
||||
}
|
||||
}
|
||||
let avc_decoder_config = avc_decoder_config.freeze();
|
||||
let sps_nal = avc_decoder_config.slice(sps_nal_start..sps_nal_end);
|
||||
let pps_nal = avc_decoder_config.slice(pps_nal_start..pps_nal_end);
|
||||
Ok(InternalParameters {
|
||||
generic_parameters: super::Parameters::Video(super::VideoParameters {
|
||||
rfc6381_codec,
|
||||
pixel_dimensions,
|
||||
pixel_aspect_ratio,
|
||||
frame_rate,
|
||||
extra_data: avc_decoder_config,
|
||||
}),
|
||||
sps_nal,
|
||||
pps_nal,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn gw_security() {
|
||||
let params = super::InternalParameters::parse_format_specific_params(
|
||||
"packetization-mode=1;\
|
||||
profile-level-id=5046302;\
|
||||
sprop-parameter-sets=Z00AHpWoLQ9puAgICBAAAAAB,aO48gAAAAAE=",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
¶ms.sps_nal[..],
|
||||
b"\x67\x4d\x00\x1e\x95\xa8\x2d\x0f\x69\xb8\x08\x08\x08\x10"
|
||||
);
|
||||
assert_eq!(¶ms.pps_nal[..], b"\x68\xee\x3c\x80");
|
||||
}
|
||||
}
|
450
src/codec/mod.rs
Normal file
450
src/codec/mod.rs
Normal file
@ -0,0 +1,450 @@
|
||||
// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
//! Codec-specific logic (for audio, video, and application media types).
|
||||
//!
|
||||
//! Currently this primarily consists of RTP depacketization logic for each
|
||||
//! codec, as needed for a client during `PLAY` and a server during `RECORD`.
|
||||
//! Packetization (needed for the reverse) may be added in the future.
|
||||
|
||||
use std::num::{NonZeroU16, NonZeroU32};
|
||||
|
||||
use crate::client::rtp;
|
||||
use bytes::{Buf, Bytes};
|
||||
use failure::{bail, Error};
|
||||
use pretty_hex::PrettyHex;
|
||||
|
||||
pub(crate) mod aac;
|
||||
pub(crate) mod g723;
|
||||
pub(crate) mod h264;
|
||||
pub(crate) mod onvif;
|
||||
pub(crate) mod simple_audio;
|
||||
|
||||
pub enum CodecItem {
|
||||
VideoFrame(VideoFrame),
|
||||
AudioFrame(AudioFrame),
|
||||
MessageFrame(MessageFrame),
|
||||
SenderReport(crate::client::rtp::SenderReport),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Parameters {
|
||||
Video(VideoParameters),
|
||||
Audio(AudioParameters),
|
||||
Message(MessageParameters),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct VideoParameters {
|
||||
pixel_dimensions: (u32, u32),
|
||||
rfc6381_codec: String,
|
||||
pixel_aspect_ratio: Option<(u32, u32)>,
|
||||
frame_rate: Option<(u32, u32)>,
|
||||
extra_data: Bytes,
|
||||
}
|
||||
|
||||
impl VideoParameters {
|
||||
/// Returns a codec description in
|
||||
/// [RFC-6381](https://tools.ietf.org/html/rfc6381) form, eg `avc1.4D401E`.
|
||||
// TODO: use https://github.com/dholroyd/rfc6381-codec crate once published?
|
||||
pub fn rfc6381_codec(&self) -> &str {
|
||||
&self.rfc6381_codec
|
||||
}
|
||||
|
||||
/// Returns the overall dimensions of the video frame in pixels, as `(width, height)`.
|
||||
pub fn pixel_dimensions(&self) -> (u32, u32) {
|
||||
self.pixel_dimensions
|
||||
}
|
||||
|
||||
/// Returns the displayed size of a pixel, if known, as a dimensionless ratio `(h_spacing, v_spacing)`.
|
||||
/// This is as specified in [ISO/IEC 14496-12:2015](https://standards.iso.org/ittf/PubliclyAvailableStandards/c068960_ISO_IEC_14496-12_2015.zip])
|
||||
/// section 12.1.4.
|
||||
///
|
||||
/// It's common for IP cameras to use [anamorphic](https://en.wikipedia.org/wiki/Anamorphic_format) sub streams.
|
||||
/// Eg a 16x9 camera may export the same video source as a 1920x1080 "main"
|
||||
/// stream and a 704x480 "sub" stream, without cropping. The former has a
|
||||
/// pixel aspect ratio of `(1, 1)` while the latter has a pixel aspect ratio
|
||||
/// of `(40, 33)`.
|
||||
pub fn pixel_aspect_ratio(&self) -> Option<(u32, u32)> {
|
||||
self.pixel_aspect_ratio
|
||||
}
|
||||
|
||||
/// Returns the maximum frame rate in seconds as `(numerator, denominator)`,
|
||||
/// if known.
|
||||
///
|
||||
/// May not be minimized, and may not be in terms of the clock rate. Eg 15
|
||||
/// frames per second might be returned as `(1, 15)` or `(6000, 90000)`. The
|
||||
/// standard NTSC framerate (roughly 29.97 fps) might be returned as
|
||||
/// `(1001, 30000)`.
|
||||
///
|
||||
/// TODO: maybe return in clock rate units instead?
|
||||
/// TODO: expose fixed vs max distinction (see H.264 fixed_frame_rate_flag).
|
||||
pub fn frame_rate(&self) -> Option<(u32, u32)> {
|
||||
self.frame_rate
|
||||
}
|
||||
|
||||
/// The codec-specific "extra data" to feed to eg ffmpeg to decode the video frames.
|
||||
/// * H.264: an AvcDecoderConfig.
|
||||
pub fn extra_data(&self) -> &Bytes {
|
||||
&self.extra_data
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for VideoParameters {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("VideoParameters")
|
||||
.field("rfc6381_codec", &self.rfc6381_codec)
|
||||
.field("pixel_dimensions", &self.pixel_dimensions)
|
||||
.field("pixel_aspect_ratio", &self.pixel_aspect_ratio)
|
||||
.field("frame_rate", &self.frame_rate)
|
||||
.field("extra_data", &self.extra_data.hex_dump())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AudioParameters {
|
||||
rfc6381_codec: Option<String>,
|
||||
frame_length: Option<NonZeroU32>,
|
||||
clock_rate: u32,
|
||||
extra_data: Bytes,
|
||||
config: AudioCodecConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum AudioCodecConfig {
|
||||
Aac(aac::AudioSpecificConfig),
|
||||
Other,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AudioParameters {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AudioParameters")
|
||||
.field("rfc6381_codec", &self.rfc6381_codec)
|
||||
.field("frame_length", &self.frame_length)
|
||||
.field("extra_data", &self.extra_data.hex_dump())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioParameters {
|
||||
pub fn rfc6381_codec(&self) -> Option<&str> {
|
||||
self.rfc6381_codec.as_deref()
|
||||
}
|
||||
|
||||
/// The length of each frame (in clock_rate units), if fixed.
|
||||
pub fn frame_length(&self) -> Option<NonZeroU32> {
|
||||
self.frame_length
|
||||
}
|
||||
|
||||
pub fn clock_rate(&self) -> u32 {
|
||||
self.clock_rate
|
||||
}
|
||||
|
||||
/// The codec-specific "extra data" to feed to eg ffmpeg to decode the audio.
|
||||
/// * AAC: a serialized `AudioSpecificConfig`.
|
||||
pub fn extra_data(&self) -> &Bytes {
|
||||
&self.extra_data
|
||||
}
|
||||
|
||||
/// Builds an `.mp4` `SimpleAudioEntry` box (as defined in ISO/IEC 14496-12) if possible.
|
||||
/// Not all codecs can be placed into a `.mp4` file, and even for supported codecs there
|
||||
/// may be unsupported edge cases.
|
||||
pub fn sample_entry(&self) -> Result<Bytes, Error> {
|
||||
aac::get_mp4a_box(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// An audio frame, which consists of one or more samples.
|
||||
pub struct AudioFrame {
|
||||
pub ctx: crate::Context,
|
||||
pub stream_id: usize,
|
||||
pub timestamp: crate::Timestamp,
|
||||
pub frame_length: NonZeroU32,
|
||||
|
||||
/// Number of lost RTP packets before this audio frame. See [crate::client::rtp::Packet::loss].
|
||||
/// Note that if loss occurs during a fragmented frame, more than this number of packets' worth
|
||||
/// of data may be skipped.
|
||||
pub loss: u16,
|
||||
|
||||
// TODO: expose bytes or Buf (for zero-copy)?
|
||||
pub data: Bytes,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AudioFrame {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AudioFrame")
|
||||
.field("stream_id", &self.stream_id)
|
||||
.field("ctx", &self.ctx)
|
||||
.field("loss", &self.loss)
|
||||
.field("timestamp", &self.timestamp)
|
||||
.field("frame_length", &self.frame_length)
|
||||
.field("data", &self.data.hex_dump())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Buf for AudioFrame {
|
||||
fn remaining(&self) -> usize {
|
||||
self.data.remaining()
|
||||
}
|
||||
|
||||
fn chunk(&self) -> &[u8] {
|
||||
self.data.chunk()
|
||||
}
|
||||
|
||||
fn advance(&mut self, cnt: usize) {
|
||||
self.data.advance(cnt)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MessageParameters(onvif::CompressionType);
|
||||
|
||||
pub struct MessageFrame {
|
||||
pub ctx: crate::Context,
|
||||
pub timestamp: crate::Timestamp,
|
||||
pub stream_id: usize,
|
||||
|
||||
/// Number of lost RTP packets before this message frame. See [crate::client::rtp::Packet::loss].
|
||||
/// If this is non-zero, a prefix of the message may be missing.
|
||||
pub loss: u16,
|
||||
|
||||
// TODO: expose bytes or Buf (for zero-copy)?
|
||||
pub data: Bytes,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for MessageFrame {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AudioFrame")
|
||||
.field("ctx", &self.ctx)
|
||||
.field("stream_id", &self.stream_id)
|
||||
.field("loss", &self.loss)
|
||||
.field("timestamp", &self.timestamp)
|
||||
.field("data", &self.data.hex_dump())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A single encoded video frame (aka picture, video sample, or video access unit).
|
||||
///
|
||||
/// Use the [bytes::Buf] implementation to retrieve data. Durations aren't
|
||||
/// specified here; they can be calculated from the timestamp of a following
|
||||
/// picture, or approximated via the frame rate.
|
||||
pub struct VideoFrame {
|
||||
pub new_parameters: Option<VideoParameters>,
|
||||
|
||||
/// Number of lost RTP packets before this video frame. See [crate::client::rtp::Packet::loss].
|
||||
/// Note that if loss occurs during a fragmented frame, more than this number of packets' worth
|
||||
/// of data may be skipped.
|
||||
pub loss: u16,
|
||||
|
||||
// A pair of contexts: for the start and for the end.
|
||||
// Having both can be useful to measure the total time elapsed while receiving the frame.
|
||||
start_ctx: crate::Context,
|
||||
end_ctx: crate::Context,
|
||||
|
||||
/// This picture's timestamp in the time base associated with the stream.
|
||||
pub timestamp: crate::Timestamp,
|
||||
|
||||
pub stream_id: usize,
|
||||
|
||||
/// If this is a "random access point (RAP)" aka "instantaneous decoding refresh (IDR)" picture.
|
||||
/// The former is defined in ISO/IEC 14496-12; the latter in H.264. Both mean that this picture
|
||||
/// can be decoded without any other AND no pictures following this one depend on any pictures
|
||||
/// before this one.
|
||||
pub is_random_access_point: bool,
|
||||
|
||||
/// If no other pictures require this one to be decoded correctly.
|
||||
/// In H.264 terms, this is a frame with `nal_ref_idc == 0`.
|
||||
pub is_disposable: bool,
|
||||
|
||||
/// Position within `concat(data_prefix, data)`.
|
||||
pos: u32,
|
||||
|
||||
data_prefix: [u8; 4],
|
||||
|
||||
/// Frame content in the requested format. Currently in a single [bytes::Bytes]
|
||||
/// allocation, but this may change when supporting H.264 partitioned slices
|
||||
/// or if we revise the fragmentation implementation.
|
||||
data: bytes::Bytes,
|
||||
}
|
||||
|
||||
impl VideoFrame {
|
||||
pub fn start_ctx(&self) -> crate::Context {
|
||||
self.start_ctx
|
||||
}
|
||||
|
||||
pub fn end_ctx(&self) -> crate::Context {
|
||||
self.end_ctx
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for VideoFrame {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
//use pretty_hex::PrettyHex;
|
||||
f.debug_struct("VideoFrame")
|
||||
.field("timestamp", &self.timestamp)
|
||||
.field("start_ctx", &self.start_ctx)
|
||||
.field("end_ctx", &self.end_ctx)
|
||||
.field("loss", &self.loss)
|
||||
.field("new_parameters", &self.new_parameters)
|
||||
.field("is_random_access_point", &self.is_random_access_point)
|
||||
.field("is_disposable", &self.is_disposable)
|
||||
.field("pos", &self.pos)
|
||||
.field("data_len", &(self.data.len() + 4))
|
||||
//.field("data", &self.data.hex_dump())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl bytes::Buf for VideoFrame {
|
||||
fn remaining(&self) -> usize {
|
||||
self.data.len() + 4 - (self.pos as usize)
|
||||
}
|
||||
|
||||
fn chunk(&self) -> &[u8] {
|
||||
let pos = self.pos as usize;
|
||||
if let Some(pos_within_data) = pos.checked_sub(4) {
|
||||
&self.data[pos_within_data..]
|
||||
} else {
|
||||
&self.data_prefix[pos..]
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self, cnt: usize) {
|
||||
assert!((self.pos as usize) + cnt <= 4 + self.data.len());
|
||||
self.pos += cnt as u32;
|
||||
}
|
||||
|
||||
fn chunks_vectored<'a>(&'a self, dst: &mut [std::io::IoSlice<'a>]) -> usize {
|
||||
match dst.len() {
|
||||
0 => 0,
|
||||
1 => {
|
||||
dst[0] = std::io::IoSlice::new(self.chunk());
|
||||
1
|
||||
}
|
||||
_ if self.pos < 4 => {
|
||||
dst[0] = std::io::IoSlice::new(&self.data_prefix[self.pos as usize..]);
|
||||
dst[1] = std::io::IoSlice::new(&self.data);
|
||||
2
|
||||
}
|
||||
_ => {
|
||||
dst[0] = std::io::IoSlice::new(&self.data[(self.pos - 4) as usize..]);
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::clippy::large_enum_variant)]
|
||||
pub(crate) enum Depacketizer {
|
||||
Aac(aac::Depacketizer),
|
||||
SimpleAudio(simple_audio::Depacketizer),
|
||||
G723(g723::Depacketizer),
|
||||
H264(h264::Depacketizer),
|
||||
Onvif(onvif::Depacketizer),
|
||||
}
|
||||
|
||||
impl Depacketizer {
|
||||
pub(crate) fn new(
|
||||
media: &str,
|
||||
encoding_name: &str,
|
||||
clock_rate: u32,
|
||||
channels: Option<NonZeroU16>,
|
||||
format_specific_params: Option<&str>,
|
||||
) -> Result<Self, Error> {
|
||||
use onvif::CompressionType;
|
||||
|
||||
// RTP Payload Format Media Types
|
||||
// https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml#rtp-parameters-2
|
||||
match (media, encoding_name) {
|
||||
("video", "h264") => Ok(Depacketizer::H264(h264::Depacketizer::new(
|
||||
clock_rate,
|
||||
format_specific_params,
|
||||
)?)),
|
||||
("audio", "mpeg4-generic") => Ok(Depacketizer::Aac(aac::Depacketizer::new(
|
||||
clock_rate,
|
||||
channels,
|
||||
format_specific_params,
|
||||
)?)),
|
||||
("audio", "g726-16") => Ok(Depacketizer::SimpleAudio(simple_audio::Depacketizer::new(
|
||||
clock_rate, 2,
|
||||
))),
|
||||
("audio", "g726-24") => Ok(Depacketizer::SimpleAudio(simple_audio::Depacketizer::new(
|
||||
clock_rate, 3,
|
||||
))),
|
||||
("audio", "dvi4") | ("audio", "g726-32") => Ok(Depacketizer::SimpleAudio(
|
||||
simple_audio::Depacketizer::new(clock_rate, 4),
|
||||
)),
|
||||
("audio", "g726-40") => Ok(Depacketizer::SimpleAudio(simple_audio::Depacketizer::new(
|
||||
clock_rate, 5,
|
||||
))),
|
||||
("audio", "pcma") | ("audio", "pcmu") | ("audio", "u8") | ("audio", "g722") => Ok(
|
||||
Depacketizer::SimpleAudio(simple_audio::Depacketizer::new(clock_rate, 8)),
|
||||
),
|
||||
("audio", "l16") => Ok(Depacketizer::SimpleAudio(simple_audio::Depacketizer::new(
|
||||
clock_rate, 16,
|
||||
))),
|
||||
// Dahua cameras when configured with G723 send packets with a
|
||||
// non-standard encoding-name "G723.1" and length 40, which doesn't
|
||||
// make sense. Don't try to depacketize these.
|
||||
("audio", "g723") => Ok(Depacketizer::G723(g723::Depacketizer::new(clock_rate)?)),
|
||||
("application", "vnd.onvif.metadata") => Ok(Depacketizer::Onvif(
|
||||
onvif::Depacketizer::new(CompressionType::Uncompressed),
|
||||
)),
|
||||
("application", "vnd.onvif.metadata.gzip") => Ok(Depacketizer::Onvif(
|
||||
onvif::Depacketizer::new(CompressionType::GzipCompressed),
|
||||
)),
|
||||
("application", "vnd.onvif.metadata.exi.onvif") => Ok(Depacketizer::Onvif(
|
||||
onvif::Depacketizer::new(CompressionType::ExiDefault),
|
||||
)),
|
||||
("application", "vnd.onvif.metadata.exi.ext") => Ok(Depacketizer::Onvif(
|
||||
onvif::Depacketizer::new(CompressionType::ExiInBand),
|
||||
)),
|
||||
(_, _) => {
|
||||
log::info!(
|
||||
"no depacketizer for media/encoding_name {}/{}",
|
||||
media,
|
||||
encoding_name
|
||||
);
|
||||
bail!(
|
||||
"no depacketizer for media/encoding_name {}/{}",
|
||||
media,
|
||||
encoding_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parameters(&self) -> Option<&Parameters> {
|
||||
match self {
|
||||
Depacketizer::Aac(d) => d.parameters(),
|
||||
Depacketizer::G723(d) => d.parameters(),
|
||||
Depacketizer::H264(d) => d.parameters(),
|
||||
Depacketizer::Onvif(d) => d.parameters(),
|
||||
Depacketizer::SimpleAudio(d) => d.parameters(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn push(&mut self, input: rtp::Packet) -> Result<(), Error> {
|
||||
match self {
|
||||
Depacketizer::Aac(d) => d.push(input),
|
||||
Depacketizer::G723(d) => d.push(input),
|
||||
Depacketizer::H264(d) => d.push(input),
|
||||
Depacketizer::Onvif(d) => d.push(input),
|
||||
Depacketizer::SimpleAudio(d) => d.push(input),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pull(&mut self) -> Result<Option<CodecItem>, Error> {
|
||||
match self {
|
||||
Depacketizer::Aac(d) => d.pull(),
|
||||
Depacketizer::G723(d) => d.pull(),
|
||||
Depacketizer::H264(d) => d.pull(),
|
||||
Depacketizer::Onvif(d) => d.pull(),
|
||||
Depacketizer::SimpleAudio(d) => d.pull(),
|
||||
}
|
||||
}
|
||||
}
|
129
src/codec/onvif.rs
Normal file
129
src/codec/onvif.rs
Normal file
@ -0,0 +1,129 @@
|
||||
// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
//! ONVIF metadata streams.
|
||||
//!
|
||||
//! See the
|
||||
//! [ONVIF Streaming Specification](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf)
|
||||
//! version 19.12 section 5.2.1.1. The RTP layer muxing is simple: RTP packets with the MARK
|
||||
//! bit set end messages.
|
||||
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use failure::{bail, Error};
|
||||
|
||||
use super::CodecItem;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CompressionType {
|
||||
Uncompressed,
|
||||
GzipCompressed,
|
||||
ExiDefault,
|
||||
ExiInBand,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Depacketizer {
|
||||
parameters: super::Parameters,
|
||||
state: State,
|
||||
high_water_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum State {
|
||||
Idle,
|
||||
InProgress(InProgress),
|
||||
Ready(super::MessageFrame),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InProgress {
|
||||
ctx: crate::Context,
|
||||
timestamp: crate::Timestamp,
|
||||
data: BytesMut,
|
||||
loss: u16,
|
||||
}
|
||||
|
||||
impl Depacketizer {
|
||||
pub(super) fn new(compression_type: CompressionType) -> Self {
|
||||
Depacketizer {
|
||||
parameters: super::Parameters::Message(super::MessageParameters(compression_type)),
|
||||
state: State::Idle,
|
||||
high_water_size: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn parameters(&self) -> Option<&super::Parameters> {
|
||||
Some(&self.parameters)
|
||||
}
|
||||
|
||||
pub(super) fn push(&mut self, pkt: crate::client::rtp::Packet) -> Result<(), failure::Error> {
|
||||
if pkt.loss > 0 {
|
||||
if let State::InProgress(in_progress) = &self.state {
|
||||
log::debug!(
|
||||
"Discarding {}-byte message prefix due to loss of {} RTP packets",
|
||||
in_progress.data.len(),
|
||||
pkt.loss
|
||||
);
|
||||
self.state = State::Idle;
|
||||
}
|
||||
}
|
||||
let mut in_progress = match std::mem::replace(&mut self.state, State::Idle) {
|
||||
State::InProgress(in_progress) => {
|
||||
if in_progress.timestamp.timestamp != pkt.timestamp.timestamp {
|
||||
bail!(
|
||||
"Timestamp changed from {} to {} (@ seq {:04x}) with message in progress",
|
||||
&in_progress.timestamp,
|
||||
&pkt.timestamp,
|
||||
pkt.sequence_number
|
||||
);
|
||||
}
|
||||
in_progress
|
||||
}
|
||||
State::Ready(..) => panic!("push while in state ready"),
|
||||
State::Idle => {
|
||||
if pkt.mark {
|
||||
// fast-path: avoid copy.
|
||||
self.state = State::Ready(super::MessageFrame {
|
||||
stream_id: pkt.stream_id,
|
||||
loss: pkt.loss,
|
||||
ctx: pkt.rtsp_ctx,
|
||||
timestamp: pkt.timestamp,
|
||||
data: pkt.payload,
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
InProgress {
|
||||
loss: pkt.loss,
|
||||
ctx: pkt.rtsp_ctx,
|
||||
timestamp: pkt.timestamp,
|
||||
data: BytesMut::with_capacity(self.high_water_size),
|
||||
}
|
||||
}
|
||||
};
|
||||
in_progress.data.put(pkt.payload);
|
||||
if pkt.mark {
|
||||
self.high_water_size =
|
||||
std::cmp::max(self.high_water_size, in_progress.data.remaining());
|
||||
self.state = State::Ready(super::MessageFrame {
|
||||
stream_id: pkt.stream_id,
|
||||
ctx: in_progress.ctx,
|
||||
timestamp: in_progress.timestamp,
|
||||
data: in_progress.data.freeze(),
|
||||
loss: in_progress.loss,
|
||||
});
|
||||
} else {
|
||||
self.state = State::InProgress(in_progress);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn pull(&mut self) -> Result<Option<CodecItem>, Error> {
|
||||
Ok(match std::mem::replace(&mut self.state, State::Idle) {
|
||||
State::Ready(message) => Some(CodecItem::MessageFrame(message)),
|
||||
s => {
|
||||
self.state = s;
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
76
src/codec/simple_audio.rs
Normal file
76
src/codec/simple_audio.rs
Normal file
@ -0,0 +1,76 @@
|
||||
// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
//! Fixed-size audio sample codecs as defined in
|
||||
//! [RFC 3551 section 4.5](https://datatracker.ietf.org/doc/html/rfc3551#section-4.5).
|
||||
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
use bytes::Bytes;
|
||||
use failure::format_err;
|
||||
use failure::Error;
|
||||
|
||||
use super::CodecItem;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Depacketizer {
|
||||
parameters: super::Parameters,
|
||||
pending: Option<super::AudioFrame>,
|
||||
bits_per_sample: u32,
|
||||
}
|
||||
|
||||
impl Depacketizer {
|
||||
/// Creates a new Depacketizer.
|
||||
pub(super) fn new(clock_rate: u32, bits_per_sample: u32) -> Self {
|
||||
Self {
|
||||
parameters: super::Parameters::Audio(super::AudioParameters {
|
||||
rfc6381_codec: None,
|
||||
frame_length: None, // variable
|
||||
clock_rate,
|
||||
extra_data: Bytes::new(),
|
||||
config: super::AudioCodecConfig::Other,
|
||||
}),
|
||||
bits_per_sample,
|
||||
pending: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn parameters(&self) -> Option<&super::Parameters> {
|
||||
Some(&self.parameters)
|
||||
}
|
||||
|
||||
fn frame_length(&self, payload_len: usize) -> Option<NonZeroU32> {
|
||||
// This calculation could be strength-reduced but it's just once per frame anyway.
|
||||
// Let's do it in a straightforward way.
|
||||
assert!(payload_len < usize::from(u16::MAX));
|
||||
let bits = (payload_len) as u32 * 8;
|
||||
match (bits % self.bits_per_sample) != 0 {
|
||||
true => None,
|
||||
false => NonZeroU32::new(bits / self.bits_per_sample),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn push(&mut self, pkt: crate::client::rtp::Packet) -> Result<(), Error> {
|
||||
assert!(self.pending.is_none());
|
||||
let frame_length = self.frame_length(pkt.payload.len()).ok_or_else(|| {
|
||||
format_err!(
|
||||
"invalid length {} for payload of {}-bit audio samples",
|
||||
pkt.payload.len(),
|
||||
self.bits_per_sample
|
||||
)
|
||||
})?;
|
||||
self.pending = Some(super::AudioFrame {
|
||||
loss: pkt.loss,
|
||||
ctx: pkt.rtsp_ctx,
|
||||
stream_id: pkt.stream_id,
|
||||
timestamp: pkt.timestamp,
|
||||
frame_length,
|
||||
data: pkt.payload,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn pull(&mut self) -> Result<Option<super::CodecItem>, Error> {
|
||||
Ok(self.pending.take().map(CodecItem::AudioFrame))
|
||||
}
|
||||
}
|
310
src/lib.rs
Normal file
310
src/lib.rs
Normal file
@ -0,0 +1,310 @@
|
||||
// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||
use failure::{bail, format_err, Error};
|
||||
use once_cell::sync::Lazy;
|
||||
use rtsp_types::Message;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
pub mod client;
|
||||
pub mod codec;
|
||||
|
||||
pub static X_ACCEPT_DYNAMIC_RATE: Lazy<rtsp_types::HeaderName> = Lazy::new(|| {
|
||||
rtsp_types::HeaderName::from_static_str("x-Accept-Dynamic-Rate").expect("is ascii")
|
||||
});
|
||||
pub static X_DYNAMIC_RATE: Lazy<rtsp_types::HeaderName> =
|
||||
Lazy::new(|| rtsp_types::HeaderName::from_static_str("x-Dynamic-Rate").expect("is ascii"));
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ReceivedMessage {
|
||||
pub ctx: Context,
|
||||
pub msg: Message<Bytes>,
|
||||
}
|
||||
|
||||
/// A monotonically increasing timestamp within an RTP stream.
|
||||
/// The [Display] and [Debug] implementations display:
|
||||
/// * the bottom 32 bits, as seen in RTP packet headers. This advances at a
|
||||
/// codec-specified clock rate.
|
||||
/// * the full timestamp, with top bits accumulated as RTP packet timestamps wrap around.
|
||||
/// * a conversion to RTSP "normal play time" (NPT): zero-based and normalized to seconds.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Timestamp {
|
||||
/// A timestamp which must be compared to `start`. The top bits are inferred
|
||||
/// from wraparounds of 32-bit RTP timestamps. The `i64` itself is not
|
||||
/// allowed to overflow/underflow; similarly `timestamp - start` is not
|
||||
/// allowed to underflow.
|
||||
timestamp: i64,
|
||||
|
||||
/// The codec-specified clock rate, in Hz. Must be non-zero.
|
||||
clock_rate: NonZeroU32,
|
||||
|
||||
/// The stream's starting time, as specified in the RTSP `RTP-Info` header.
|
||||
start: u32,
|
||||
}
|
||||
|
||||
impl Timestamp {
|
||||
/// Returns time since some arbitrary point before the stream started.
|
||||
#[inline]
|
||||
pub fn timestamp(&self) -> i64 {
|
||||
self.timestamp
|
||||
}
|
||||
|
||||
/// Returns timestamp of the start of the stream.
|
||||
#[inline]
|
||||
pub fn start(&self) -> u32 {
|
||||
self.start
|
||||
}
|
||||
|
||||
/// Returns codec-specified clock rate, in Hz.
|
||||
#[inline]
|
||||
pub fn clock_rate(&self) -> NonZeroU32 {
|
||||
self.clock_rate
|
||||
}
|
||||
|
||||
/// Returns elapsed time since the stream start in clock rate units.
|
||||
#[inline]
|
||||
pub fn elapsed(&self) -> i64 {
|
||||
self.timestamp - i64::from(self.start)
|
||||
}
|
||||
|
||||
/// Returns elapsed time since the stream start in seconds, aka "normal play
|
||||
/// time" (NPT).
|
||||
#[inline]
|
||||
pub fn elapsed_secs(&self) -> f64 {
|
||||
(self.elapsed() as f64) / (self.clock_rate.get() as f64)
|
||||
}
|
||||
|
||||
pub fn try_add(&self, delta: u32) -> Result<Self, Error> {
|
||||
// Check for `timestamp` overflow only. We don't need to check for
|
||||
// `timestamp - start` underflow because delta is non-negative.
|
||||
Ok(Timestamp {
|
||||
timestamp: self
|
||||
.timestamp
|
||||
.checked_add(i64::from(delta))
|
||||
.ok_or_else(|| format_err!("overflow on {:?} + {}", &self, delta))?,
|
||||
clock_rate: self.clock_rate,
|
||||
start: self.start,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Timestamp {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} (mod-2^32: {}), npt {:.03}",
|
||||
self.timestamp,
|
||||
self.timestamp as u32,
|
||||
self.elapsed_secs()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Timestamp {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
pub const UNIX_EPOCH: NtpTimestamp = NtpTimestamp((2_208_988_800) << 32);
|
||||
|
||||
/// A wallclock time represented using the format of the Network Time Protocol.
|
||||
/// This isn't necessarily gathered from a real NTP server. Reported NTP
|
||||
/// timestamps are allowed to jump backwards and/or be complete nonsense.
|
||||
#[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Ord)]
|
||||
pub struct NtpTimestamp(pub u64);
|
||||
|
||||
impl std::fmt::Display for NtpTimestamp {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let since_epoch = self.0.wrapping_sub(UNIX_EPOCH.0);
|
||||
let sec_since_epoch = (since_epoch >> 32) as u32;
|
||||
let tm = time::at(time::Timespec {
|
||||
sec: i64::from(sec_since_epoch),
|
||||
nsec: 0,
|
||||
});
|
||||
let ms = ((since_epoch & 0xFFFF_FFFF) * 1_000) >> 32;
|
||||
let zone_minutes = tm.tm_utcoff.abs() / 60;
|
||||
write!(
|
||||
f,
|
||||
"{}.{:03}{}{:02}:{:02}",
|
||||
tm.strftime("%FT%T").map_err(|_| std::fmt::Error)?,
|
||||
ms,
|
||||
if tm.tm_utcoff > 0 { '+' } else { '-' },
|
||||
zone_minutes / 60,
|
||||
zone_minutes % 60
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for NtpTimestamp {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// Write both the raw and display forms.
|
||||
write!(f, "{} /* {} */", self.0, self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Context of a received message within an RTSP stream.
|
||||
/// This is meant to help find the correct TCP stream and packet in a matching
|
||||
/// packet capture.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Context {
|
||||
conn_local_addr: std::net::SocketAddr,
|
||||
conn_peer_addr: std::net::SocketAddr,
|
||||
conn_established_wall: time::Timespec,
|
||||
conn_established: std::time::Instant,
|
||||
|
||||
/// The byte position within the input stream. The bottom 32 bits can be
|
||||
/// compared to the TCP sequence number.
|
||||
msg_pos: u64,
|
||||
|
||||
/// Time when the application parsed the message. Caveat: this may not
|
||||
/// closely match the time on a packet capture if the application is
|
||||
/// overloaded (or `CLOCK_REALTIME` jumps).
|
||||
msg_received_wall: time::Timespec,
|
||||
msg_received: std::time::Instant,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn conn_established(&self) -> std::time::Instant {
|
||||
self.conn_established
|
||||
}
|
||||
|
||||
pub fn msg_received(&self) -> std::time::Instant {
|
||||
self.msg_received
|
||||
}
|
||||
|
||||
pub fn msg_pos(&self) -> u64 {
|
||||
self.msg_pos
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Context {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// TODO: this current hardcodes the assumption we are the client.
|
||||
// Change if/when adding server code.
|
||||
write!(
|
||||
f,
|
||||
"[{}(me)->{}@{} pos={}@{}]",
|
||||
&self.conn_local_addr,
|
||||
&self.conn_peer_addr,
|
||||
time::at(self.conn_established_wall)
|
||||
.strftime("%FT%T")
|
||||
.map_err(|_| std::fmt::Error)?,
|
||||
self.msg_pos,
|
||||
time::at(self.msg_received_wall)
|
||||
.strftime("%FT%T")
|
||||
.map_err(|_| std::fmt::Error)?
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct Codec {
|
||||
ctx: Context,
|
||||
}
|
||||
|
||||
/// Returns the range within `buf` that represents `subset`.
|
||||
/// If `subset` is empty, returns None; otherwise panics if `subset` is not within `buf`.
|
||||
pub(crate) fn as_range(buf: &[u8], subset: &[u8]) -> Option<std::ops::Range<usize>> {
|
||||
if subset.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let subset_p = subset.as_ptr() as usize;
|
||||
let buf_p = buf.as_ptr() as usize;
|
||||
let off = match subset_p.checked_sub(buf_p) {
|
||||
Some(off) => off,
|
||||
None => panic!(
|
||||
"{}-byte subset not within {}-byte buf",
|
||||
subset.len(),
|
||||
buf.len()
|
||||
),
|
||||
};
|
||||
let end = off + subset.len();
|
||||
assert!(end <= buf.len());
|
||||
Some(off..end)
|
||||
}
|
||||
|
||||
impl tokio_util::codec::Decoder for Codec {
|
||||
type Item = ReceivedMessage;
|
||||
type Error = failure::Error;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
let (msg, len): (Message<&[u8]>, _) = match rtsp_types::Message::parse(src) {
|
||||
Ok((m, l)) => (m, l),
|
||||
Err(rtsp_types::ParseError::Error) => bail!("RTSP parse error: {:#?}", &self.ctx),
|
||||
Err(rtsp_types::ParseError::Incomplete) => return Ok(None),
|
||||
};
|
||||
|
||||
// Map msg's body to a Bytes representation and advance `src`. Awkward:
|
||||
// 1. lifetime concerns require mapping twice: first so the message
|
||||
// doesn't depend on the BytesMut, which needs to be split/advanced;
|
||||
// then to get the proper Bytes body in place post-split.
|
||||
// 2. rtsp_types messages must be AsRef<[u8]>, so we can't use the
|
||||
// range as an intermediate body.
|
||||
// 3. within a match because the rtsp_types::Message enum itself
|
||||
// doesn't have body/replace_body/map_body methods.
|
||||
let msg = match msg {
|
||||
Message::Request(msg) => {
|
||||
let body_range = as_range(src, msg.body());
|
||||
let msg = msg.replace_body(rtsp_types::Empty);
|
||||
if let Some(r) = body_range {
|
||||
let mut raw_msg = src.split_to(len);
|
||||
raw_msg.advance(r.start);
|
||||
raw_msg.truncate(r.len());
|
||||
Message::Request(msg.replace_body(raw_msg.freeze()))
|
||||
} else {
|
||||
src.advance(len);
|
||||
Message::Request(msg.replace_body(Bytes::new()))
|
||||
}
|
||||
}
|
||||
Message::Response(msg) => {
|
||||
let body_range = as_range(src, msg.body());
|
||||
let msg = msg.replace_body(rtsp_types::Empty);
|
||||
if let Some(r) = body_range {
|
||||
let mut raw_msg = src.split_to(len);
|
||||
raw_msg.advance(r.start);
|
||||
raw_msg.truncate(r.len());
|
||||
Message::Response(msg.replace_body(raw_msg.freeze()))
|
||||
} else {
|
||||
src.advance(len);
|
||||
Message::Response(msg.replace_body(Bytes::new()))
|
||||
}
|
||||
}
|
||||
Message::Data(msg) => {
|
||||
let body_range = as_range(src, msg.as_slice());
|
||||
let msg = msg.replace_body(rtsp_types::Empty);
|
||||
if let Some(r) = body_range {
|
||||
let mut raw_msg = src.split_to(len);
|
||||
raw_msg.advance(r.start);
|
||||
raw_msg.truncate(r.len());
|
||||
Message::Data(msg.replace_body(raw_msg.freeze()))
|
||||
} else {
|
||||
src.advance(len);
|
||||
Message::Data(msg.replace_body(Bytes::new()))
|
||||
}
|
||||
}
|
||||
};
|
||||
self.ctx.msg_received_wall = time::get_time();
|
||||
self.ctx.msg_received = std::time::Instant::now();
|
||||
let msg = ReceivedMessage { ctx: self.ctx, msg };
|
||||
self.ctx.msg_pos += u64::try_from(len).expect("usize fits in u64");
|
||||
Ok(Some(msg))
|
||||
}
|
||||
}
|
||||
|
||||
impl tokio_util::codec::Encoder<rtsp_types::Message<bytes::Bytes>> for Codec {
|
||||
type Error = failure::Error;
|
||||
|
||||
fn encode(
|
||||
&mut self,
|
||||
item: rtsp_types::Message<bytes::Bytes>,
|
||||
dst: &mut BytesMut,
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut w = std::mem::replace(dst, BytesMut::new()).writer();
|
||||
item.write(&mut w).expect("bytes Writer is infallible");
|
||||
*dst = w.into_inner();
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user