initial commit

This commit is contained in:
Scott Lamb 2021-06-03 16:26:45 -07:00
commit 5cc69e751c
44 changed files with 7523 additions and 0 deletions

66
.github/workflows/check-license.py vendored Executable file
View 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
View 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
View File

@ -0,0 +1 @@
/target

1205
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

38
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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,
}
}

View 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
View 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(&parameters.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(())
}

View 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
View 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, 96127.
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

File diff suppressed because it is too large Load Diff

216
src/client/rtp.rs Normal file
View 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)" },
&timestamp,
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
View 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
View 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
View 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

View 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

View 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
View 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
View 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
View 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

View 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
View 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

View 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

View 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
View 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
View 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
View 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

View 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

View 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

View 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

View 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
View 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
View 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
View 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
View 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 = &parameters.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
View 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
View 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!(
&params.sps_nal[..],
b"\x67\x4d\x00\x1e\x95\xa8\x2d\x0f\x69\xb8\x08\x08\x08\x10"
);
assert_eq!(&params.pps_nal[..], b"\x68\xee\x3c\x80");
}
}

450
src/codec/mod.rs Normal file
View 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
View 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
View 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
View 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(())
}
}