Skip to content

Commit

Permalink
remove rtcp dependency, prep v0.1.0
Browse files Browse the repository at this point in the history
rtcp recently had an accidental semver break, so this fixes #8.
I also don't like that newer versions depend on several unnecessary
crates including tokio. retina also depends on tokio, but in the future
I want it to be IO library-agnostic.

I'm moving to v0.1.0 so I *can* release versions that claim semver
compatibility. There are still plenty of API changes to make but also
likely upcoming camera compatibility changes that won't break anything.
  • Loading branch information
scottlamb committed Aug 13, 2021
1 parent b1db9a9 commit 4176500
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 46 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
## unreleased
## v0.1.0 (2021-08-13)

* use `SET_PARAMETERS` rather than `GET_PARAMETERS` for keepalives.
The latter doesn't work with GW Security GW4089IP cameras.
* removed `rtcp` dependency. Fixes
[#8](https://github.com/scottlamb/retina/issues/8). Avoids picking up
various transitive dependencies needed by later versions of the `rtcp`
crate, including `tokio`. (`retina`'s own `tokio` dependency will likely
become optional in a future version.)

## v0.0.5 (2021-07-08)

Expand Down
13 changes: 1 addition & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "retina"
version = "0.0.5"
version = "0.1.0"
authors = ["Scott Lamb <[email protected]>"]
license = "MIT/Apache-2.0"
edition = "2018"
Expand All @@ -22,7 +22,6 @@ 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.6.0"
rtsp-types = "0.0.2"
sdp = "0.1.4"
Expand Down
3 changes: 3 additions & 0 deletions examples/client/mp4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,9 @@ pub async fn run(opts: Opts) -> Result<(), Error> {
match pkt.ok_or_else(|| anyhow!("EOF"))?? {
CodecItem::VideoFrame(f) => mp4.video(f).await?,
CodecItem::AudioFrame(f) => mp4.audio(f).await?,
CodecItem::SenderReport(sr) => {
println!("{}: SR ts={}", sr.timestamp, sr.ntp_timestamp);
},
_ => continue,
};
},
Expand Down
55 changes: 24 additions & 31 deletions src/client/rtp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,51 +184,44 @@ impl StrictSequenceChecker {
msg_ctx: &crate::RtspMessageContext,
timeline: &mut super::Timeline,
stream_id: usize,
mut data: Bytes,
data: Bytes,
) -> Result<Option<PacketItem>, String> {
use rtcp::packet::Packet;
let mut sr = None;
let mut i = 0;
let mut data = &data[..];
while !data.is_empty() {
let h = match rtcp::header::Header::unmarshal(&data) {
Err(e) => return Err(format!("corrupt RTCP header: {}", e)),
Ok(h) => h,
};
let pkt_len = (usize::from(h.length) + 1) * 4;
if pkt_len > data.len() {
return Err(format!(
"RTCP packet length {} exceeds remaining data message length {}",
pkt_len,
data.len(),
));
}
let pkt = data.split_to(pkt_len);
match h.packet_type {
rtcp::header::PacketType::SenderReport => {
let (pkt, rest) = crate::rtcp::Packet::parse(data)?;
data = rest;
match pkt {
crate::rtcp::Packet::SenderReport(pkt) => {
if i > 0 {
return Err("RTCP SR must be first in packet".into());
}
let pkt = rtcp::sender_report::SenderReport::unmarshal(&pkt)
.map_err(|e| format!("corrupt RTCP SR: {}", e))?;
let timestamp = timeline.place(pkt.rtp_time).map_err(|mut description| {
description.push_str(" in RTCP SR");
description
})?;

// TODO: verify ssrc.
let timestamp =
timeline
.place(pkt.rtp_timestamp())
.map_err(|mut description| {
description.push_str(" in RTCP SR");
description
})?;

let ssrc = pkt.ssrc();
if matches!(self.ssrc, Some(s) if s != ssrc) {
return Err(format!(
"Expected ssrc={:08x?}, got RTCP SR ssrc={:08x}",
self.ssrc, ssrc
));
}
self.ssrc = Some(ssrc);

sr = Some(SenderReport {
stream_id,
ctx: *msg_ctx,
timestamp,
ntp_timestamp: crate::NtpTimestamp(pkt.ntp_time),
ntp_timestamp: pkt.ntp_timestamp(),
});
}
/*rtcp::header::PacketType::SourceDescription => {
let pkt = rtcp::source_description::SourceDescription::unmarshal(&pkt)?;
debug!("rtcp source description: {:#?}", &pkt);
},*/
_ => debug!("rtcp: {:?}", h.packet_type),
crate::rtcp::Packet::Unknown(pkt) => debug!("rtcp: {:?}", pkt.payload_type()),
}
i += 1;
}
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::fmt::{Debug, Display};
use std::num::NonZeroU32;

mod error;
mod rtcp;

pub use error::Error;

Expand Down
212 changes: 212 additions & 0 deletions src/rtcp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// Copyright (C) 2021 Scott Lamb <[email protected]>
// SPDX-License-Identifier: MIT OR Apache-2.0

/// Handles RTCP data as described in
/// [RFC 3550 section 6](https://datatracker.ietf.org/doc/html/rfc3550#section-6).
use std::convert::TryInto;

pub enum Packet<'a> {
SenderReport(SenderReport<'a>),
Unknown(GenericPacket<'a>),
}

impl<'a> Packet<'a> {
pub fn parse(buf: &'a [u8]) -> Result<(Self, &'a [u8]), String> {
let (pkt, rest) = GenericPacket::parse(buf)?;
let pkt = match pkt.payload_type() {
200 => Packet::SenderReport(SenderReport::validate(pkt)?),
_ => Packet::Unknown(pkt),
};
Ok((pkt, rest))
}
}

/// A RTCP sender report, as defined in
/// [RFC 3550 section 6.4.1](https://datatracker.ietf.org/doc/html/rfc3550#section-6.4.1).
///
/// ```text
/// 0 1 2 3
/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// header |V=2|P| RC | PT=SR=200 | length |
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// | SSRC of sender |
/// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
/// sender | NTP timestamp, most significant word |
/// info +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// | NTP timestamp, least significant word |
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// | RTP timestamp |
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// | sender's packet count |
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// | sender's octet count |
/// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
/// report | SSRC_1 (SSRC of first source) |
/// block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// 1 | fraction lost | cumulative number of packets lost |
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// | extended highest sequence number received |
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// | interarrival jitter |
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// | last SR (LSR) |
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// | delay since last SR (DLSR) |
/// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
/// report | SSRC_2 (SSRC of second source) |
/// block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// 2 : ... :
/// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
/// | profile-specific extensions |
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// ```
pub struct SenderReport<'a>(GenericPacket<'a>);

impl<'a> SenderReport<'a> {
fn validate(pkt: GenericPacket<'a>) -> Result<Self, String> {
let count = usize::from(pkt.count());
const HEADER_LEN: usize = 8;
const SENDER_INFO_LEN: usize = 20;
const REPORT_BLOCK_LEN: usize = 24;
let expected_len = HEADER_LEN + SENDER_INFO_LEN + (count * REPORT_BLOCK_LEN);
if pkt.payload_end < expected_len {
return Err(format!(
"RTCP SR has invalid count={} with unpadded_byte_len={}",
count, pkt.payload_end
));
}
Ok(SenderReport(pkt))
}

pub fn ssrc(&self) -> u32 {
u32::from_be_bytes(self.0.buf[4..8].try_into().unwrap())
}

pub fn ntp_timestamp(&self) -> crate::NtpTimestamp {
crate::NtpTimestamp(u64::from_be_bytes(self.0.buf[8..16].try_into().unwrap()))
}

pub fn rtp_timestamp(&self) -> u32 {
u32::from_be_bytes(self.0.buf[16..20].try_into().unwrap())
}
}

/// A generic packet, not parsed as any particular payload type.
///
/// This only inteprets the leading four bytes:
///
/// ```text
/// 0 1 2 3
/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// |V=2|P| | PT | length |
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// ```
pub struct GenericPacket<'a> {
buf: &'a [u8],
payload_end: usize,
}

const COMMON_HEADER_LEN: usize = 4;

impl<'a> GenericPacket<'a> {
/// Parses a buffer into this packet and rest, doing only basic validation
/// of the version, padding, and length.
pub fn parse(buf: &'a [u8]) -> Result<(Self, &'a [u8]), String> {
if buf.len() < COMMON_HEADER_LEN {
return Err(format!(
"RTCP packets must be at least {} bytes; have only {}",
COMMON_HEADER_LEN,
buf.len()
));
}
let ver = buf[0] >> 6;
if ver != 2 {
return Err(format!("RTCP packets must be version 2; got {}", ver));
}

// raw_len is "The length of this RTCP packet in 32-bit words minus one,
// including the header and any padding."
let raw_len = (u16::from(buf[2]) << 8) | u16::from(buf[3]);
let len = (usize::from(raw_len) + 1) * 4;
if buf.len() < len {
return Err(format!(
"RTCP packet header has length {} bytes; have only {}",
len,
buf.len()
));
}
let (this, rest) = buf.split_at(len);
let padding_bit = this[0] & 0b00100_0000;
if padding_bit != 0 {
if raw_len == 0 {
return Err("RTCP packet has invalid combination of padding and len=0".to_owned());
}
let padding_bytes = usize::from(this[len - 1]);
if padding_bytes == 0 || padding_bytes > len - COMMON_HEADER_LEN {
return Err(format!(
"RTCP packet of len {} states invalid {} padding bytes",
len, padding_bytes
));
}
Ok((
GenericPacket {
buf: this,
payload_end: len - padding_bytes,
},
rest,
))
} else {
Ok((
GenericPacket {
buf: this,
payload_end: len,
},
rest,
))
}
}

/// Returns the uninterpreted payload type of this RTCP packet.
pub fn payload_type(&self) -> u8 {
self.buf[1]
}

/// Returns the low 5 bits of the first octet, which is typically a count
/// or subtype.
pub fn count(&self) -> u8 {
self.buf[0] & 0b0001_1111
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn dahua() {
// Sender report and source description from a Dahua camera.
let buf = b"\x80\xc8\x00\x06\x66\x42\x6a\xe1\
\xe4\x36\x2f\x99\xcc\xcc\xcc\xcc\
\x85\x2e\xf8\x07\x00\x2a\x43\x33\
\x2f\x4c\x34\x1d\
\x81\xca\x00\x04\x66\x42\x6a\xe1\
\x01\x06\x28\x6e\x6f\x6e\x65\x29\
\x00\x00\x00\x00";
let (sr, buf) = Packet::parse(buf).unwrap();
match sr {
Packet::SenderReport(p) => {
assert_eq!(p.ntp_timestamp(), crate::NtpTimestamp(0xe4362f99cccccccc));
assert_eq!(p.rtp_timestamp(), 0x852ef807);
}
_ => panic!(),
}
let (sdes, buf) = Packet::parse(buf).unwrap();
match sdes {
Packet::Unknown(p) => assert_eq!(p.payload_type(), 202),
_ => panic!(),
}
assert_eq!(buf.len(), 0);
}
}

0 comments on commit 4176500

Please sign in to comment.