#![forbid(unsafe_code)]
#![allow(clippy::new_without_default)]
#![allow(clippy::bool_to_int_with_if)]
#![allow(clippy::assertions_on_constants)]
#![allow(clippy::manual_range_contains)]
#![allow(clippy::get_first)]
#![allow(clippy::needless_lifetimes)]
#![allow(clippy::precedence)]
#![allow(clippy::doc_overindented_list_items)]
#![allow(clippy::uninlined_format_args)]
#![allow(mismatched_lifetime_syntaxes)]
#![deny(clippy::needless_pass_by_ref_mut)]
#![deny(missing_docs)]
#[macro_use]
extern crate tracing;
use bwe::{Bwe, BweKind};
use change::{DirectApi, SdpApi};
use rtp::RawPacket;
use std::fmt;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Instant;
use str0m_proto::Pii;
use streams::RtpPacket;
use streams::StreamPaused;
use util::InstantExt;
pub mod crypto;
use crypto::Fingerprint;
mod dtls;
use crate::crypto::dtls::DtlsOutput;
use crate::crypto::{CryptoProvider, DtlsError, from_feature_flags};
use crate::dtls::is_would_block;
use dtls::Dtls;
use is::IceAgent;
use is::IceAgentEvent;
pub use is::{Candidate, CandidateBuilder, CandidateKind, IceConnectionState, IceCreds};
#[path = "config.rs"]
mod config_mod;
pub use config_mod::RtcConfig;
pub mod config {
pub use super::crypto::dtls::{DtlsCert, DtlsVersion, KeyingMaterial};
pub use super::crypto::{CryptoProvider, Fingerprint};
}
#[doc(hidden)]
pub mod ice {
pub use is::IceCreds;
pub use is::stun::{StunMessage, StunMessageBuilder, StunPacket, TransId};
pub use is::{IceAgent, IceAgentEvent};
pub use is::{LocalPreference, default_local_preference};
}
mod io;
use io::DatagramRecvInner;
mod packet;
#[path = "rtp/mod.rs"]
mod rtp_;
use rtp_::{Bitrate, DataSize};
pub mod rtp {
pub mod rtcp {
pub use crate::rtp_::{Descriptions, ExtendedReport, Fir, Goodbye, Nack, Pli};
pub use crate::rtp_::{Dlrr, NackEntry, ReceptionReport, ReportBlock};
pub use crate::rtp_::{FirEntry, ReceiverReport, SenderInfo, SenderReport, Twcc};
pub use crate::rtp_::{ReportList, Rrtr, Rtcp, Sdes, SdesType};
}
use self::rtcp::Rtcp;
pub mod vla;
pub use crate::rtp_::{AbsCaptureTime, ExtensionValues, UserExtensionValues};
pub use crate::rtp_::{Extension, ExtensionMap, ExtensionSerializer};
pub use crate::rtp_::{RtpHeader, SeqNo, Ssrc, VideoOrientation};
pub use crate::streams::{RtpPacket, StreamPaused, StreamRx, StreamTx};
#[derive(Debug)]
pub enum RawPacket {
RtcpTx(Rtcp),
RtcpRx(Rtcp),
RtpTx(RtpHeader, Vec<u8>),
RtpRx(RtpHeader, Vec<u8>),
}
}
pub(crate) mod pacer;
#[path = "bwe/mod.rs"]
pub(crate) mod bwe_;
pub mod bwe {
pub use crate::bwe_::api::*;
}
mod sctp;
use sctp::{RtcSctp, SctpEvent, SctpInitData};
mod sdp;
pub mod format;
use format::CodecConfig;
pub mod channel;
use channel::{Channel, ChannelData, ChannelHandler, ChannelId};
pub mod media;
use media::SenderFeedback;
use media::{Direction, Media, Mid, Pt, Rid, Writer};
use media::{KeyframeRequest, KeyframeRequestKind};
use media::{MediaAdded, MediaChanged, MediaData};
pub mod change;
mod util;
use util::{Soonest, not_happening};
mod session;
use session::Session;
pub mod stats;
use stats::{CandidatePairStats, CandidateStats, MediaEgressStats, MediaIngressStats};
use stats::{PeerStats, Stats, StatsEvent, StatsSnapshot};
mod streams;
pub mod error;
pub mod net {
pub use crate::io::{DatagramRecv, DatagramSend, Protocol, Receive, TcpType, Transmit};
}
const VERSION: &str = env!("CARGO_PKG_VERSION");
pub use error::RtcError;
pub struct Rtc {
alive: bool,
ice: IceAgent,
dtls: Dtls,
dtls_connected: bool,
dtls_buf: Vec<u8>,
next_dtls_timeout: Option<Instant>,
sctp: RtcSctp,
chan: ChannelHandler,
stats: Option<Stats>,
session: Session,
remote_fingerprint: Option<Fingerprint>,
remote_addrs: Vec<SocketAddr>,
send_addr: Option<SendAddr>,
need_init_time: bool,
last_now: Instant,
peer_bytes_rx: u64,
peer_bytes_tx: u64,
change_counter: usize,
last_timeout_reason: Reason,
crypto_provider: Arc<crate::crypto::CryptoProvider>,
fingerprint_verification: bool,
}
struct SendAddr {
proto: net::Protocol,
source: SocketAddr,
destination: SocketAddr,
}
#[derive(Debug)]
#[non_exhaustive]
#[rustfmt::skip]
pub enum Event {
Connected,
IceConnectionStateChange(IceConnectionState),
MediaAdded(MediaAdded),
MediaData(MediaData),
MediaChanged(MediaChanged),
ChannelOpen(ChannelId, String),
ChannelData(ChannelData),
ChannelClose(ChannelId),
ChannelBufferedAmountLow(ChannelId),
PeerStats(PeerStats),
MediaIngressStats(MediaIngressStats),
MediaEgressStats(MediaEgressStats),
EgressBitrateEstimate(BweKind),
KeyframeRequest(KeyframeRequest),
StreamPaused(StreamPaused),
SenderFeedback(SenderFeedback),
RtpPacket(RtpPacket),
RawPacket(Box<RawPacket>),
#[cfg(feature = "_internal_test_exports")]
Probe(crate::bwe_::ProbeClusterConfig),
}
impl Event {
pub fn as_raw_packet(&self) -> Option<&RawPacket> {
if let Self::RawPacket(boxed) = &self {
Some(&**boxed)
} else {
None
}
}
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)] pub enum Input<'a> {
Timeout(Instant),
Receive(Instant, net::Receive<'a>),
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum Output {
Timeout(Instant),
Transmit(net::Transmit),
Event(Event),
}
pub use crate::pacer::PacerReason;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Reason {
#[default]
NotHappening,
DTLS,
Ice,
Sctp,
Channel,
Stats,
Feedback,
Nack,
Twcc,
PauseCheck,
SendStream,
Packetize,
Pacer(PacerReason),
BweDelayControl,
BweProbeControl,
BweProbeEstimator,
}
impl Rtc {
pub fn new(start: Instant) -> Self {
let config = RtcConfig::default();
Self::new_from_config(config, start).expect("Failed to create Rtc from default config")
}
pub fn builder() -> RtcConfig {
RtcConfig::new()
}
pub(crate) fn new_from_config(config: RtcConfig, start: Instant) -> Result<Self, RtcError> {
let crypto_provider = config
.crypto_provider
.clone()
.or_else(|| CryptoProvider::get_default().cloned().map(Arc::new))
.or_else(|| Some(Arc::new(from_feature_flags())))
.expect("a crash earlier if no crypto provider was set");
let session = Session::new(&config);
let local_creds = config.local_ice_credentials.unwrap_or_else(IceCreds::new);
let mut ice = IceAgent::with_hmac(local_creds, crypto_provider.sha1_hmac_provider);
if config.ice_lite {
ice.set_ice_lite(config.ice_lite);
}
if let Some(initial_stun_rto) = config.initial_stun_rto {
ice.set_initial_stun_rto(initial_stun_rto);
}
if let Some(max_stun_rto) = config.max_stun_rto {
ice.set_max_stun_rto(max_stun_rto);
}
if let Some(max_stun_retransmits) = config.max_stun_retransmits {
ice.set_max_stun_retransmits(max_stun_retransmits);
}
let dtls_cert = config
.dtls_cert
.or_else(|| crypto_provider.dtls_provider.generate_certificate())
.expect(
"No DTLS certificate provided and the crypto provider cannot generate one. \
Either provide a certificate via RtcConfig::set_dtls_cert or use a \
crypto provider that supports certificate generation.",
);
let mut sctp = RtcSctp::new();
if config.snap_enabled {
sctp.enable_snap();
}
Ok(Rtc {
alive: true,
ice,
dtls: Dtls::new(
&dtls_cert,
crypto_provider.dtls_provider,
crypto_provider.sha256_provider,
start,
config.dtls_version,
)
.expect("DTLS to init without problem"),
dtls_connected: false,
dtls_buf: vec![0; 2000],
next_dtls_timeout: None,
session,
sctp,
chan: ChannelHandler::default(),
stats: config.stats_interval.map(Stats::new),
remote_fingerprint: None,
remote_addrs: vec![],
send_addr: None,
need_init_time: true,
last_now: start,
peer_bytes_rx: 0,
peer_bytes_tx: 0,
change_counter: 0,
last_timeout_reason: Reason::NotHappening,
crypto_provider,
fingerprint_verification: config.fingerprint_verification,
})
}
pub fn is_alive(&self) -> bool {
self.alive
}
pub fn disconnect(&mut self) {
if self.alive {
debug!("Set alive=false");
self.alive = false;
}
}
pub fn add_local_candidate(&mut self, c: Candidate) -> Option<&Candidate> {
self.ice.add_local_candidate(c)
}
pub fn add_remote_candidate(&mut self, c: Candidate) {
self.ice.add_remote_candidate(c);
}
pub fn is_connected(&self) -> bool {
self.ice.state().is_connected() && self.dtls_connected && self.session.is_connected()
}
pub fn sdp_api(&mut self) -> SdpApi {
SdpApi::new(self)
}
pub fn direct_api(&mut self) -> DirectApi {
DirectApi::new(self)
}
pub fn writer(&mut self, mid: Mid) -> Option<Writer> {
if self.session.rtp_mode {
panic!("In rtp_mode use direct_api().stream_tx().write_rtp()");
}
self.session.media_by_mid_mut(mid)?;
Some(Writer::new(&mut self.session, mid))
}
pub fn media(&self, mid: Mid) -> Option<&Media> {
self.session.media_by_mid(mid)
}
fn init_dtls(&mut self, active: bool) -> Result<(), RtcError> {
if self.dtls.is_inited() {
return Ok(());
}
debug!("DTLS setup is: {:?}", active);
self.dtls.set_active(active);
self.dtls.handle_timeout(self.last_now)?;
if active {
let _ = self.dtls.handle_receive(&[]);
}
Ok(())
}
fn try_init_sctp(
&mut self,
client: bool,
sctp_init_data: Option<SctpInitData>,
) -> Result<(), RtcError> {
if self.sctp.is_inited() {
return Ok(());
}
self.sctp.init(client, self.last_now, sctp_init_data)?;
Ok(())
}
pub(crate) fn new_mid(&self) -> Mid {
loop {
let mid = Mid::new();
if !self.session.has_mid(mid) {
break mid;
}
}
}
pub fn poll_output(&mut self) -> Result<Output, RtcError> {
let o = self.do_poll_output()?;
match &o {
Output::Event(e) => match e {
Event::ChannelData(_)
| Event::MediaData(_)
| Event::RtpPacket(_)
| Event::SenderFeedback(_)
| Event::MediaEgressStats(_)
| Event::MediaIngressStats(_)
| Event::PeerStats(_)
| Event::ChannelBufferedAmountLow(_)
| Event::EgressBitrateEstimate(_) => {
trace!("{:?}", e)
}
_ => debug!("{:?}", e),
},
Output::Transmit(t) => {
self.peer_bytes_tx += t.contents.len() as u64;
trace!("OUT {:?}", t)
}
Output::Timeout(_t) => {}
}
Ok(o)
}
fn do_poll_output(&mut self) -> Result<Output, RtcError> {
if !self.alive {
self.last_timeout_reason = Reason::NotHappening;
return Ok(Output::Timeout(not_happening()));
}
while let Some(e) = self.ice.poll_event() {
match e {
IceAgentEvent::IceRestart(_) => {
}
IceAgentEvent::IceConnectionStateChange(v) => {
return Ok(Output::Event(Event::IceConnectionStateChange(v)));
}
IceAgentEvent::DiscoveredRecv { proto, source } => {
debug!("ICE remote address: {:?}/{:?}", Pii(source), proto);
self.remote_addrs.push(source);
while self.remote_addrs.len() > 20 {
self.remote_addrs.remove(0);
}
}
IceAgentEvent::NominatedSend {
proto,
source,
destination,
} => {
debug!(
"ICE nominated send from: {:?} to: {:?} with protocol {:?}",
Pii(source),
Pii(destination),
proto,
);
self.send_addr = Some(SendAddr {
proto,
source,
destination,
});
}
}
}
let mut just_connected = false;
loop {
match self.dtls.poll_output(&mut self.dtls_buf) {
DtlsOutput::Packet(_) => {
unreachable!("We don't expect DTLS packets here since we use poll_packet");
}
DtlsOutput::Connected => {
if !self.dtls_connected {
debug!("DTLS connected");
self.dtls_connected = true;
just_connected = true;
}
}
DtlsOutput::KeyingMaterial(km, profile) => {
use config::KeyingMaterial;
let km_bytes = km.as_ref().to_vec();
debug!("DTLS set SRTP keying material and profile: {}", profile);
let active = self.dtls.is_active().expect("DTLS must be inited by now");
self.session.set_keying_material(
KeyingMaterial::new(&km_bytes),
&self.crypto_provider,
profile,
active,
);
}
DtlsOutput::PeerCert(der) => {
debug!("DTLS verify remote fingerprint");
let fingerprint = crate::crypto::Fingerprint {
hash_func: "sha-256".to_string(),
bytes: self.crypto_provider.sha256_provider.sha256(der).to_vec(),
};
self.dtls.set_remote_fingerprint(fingerprint.clone());
if let Some(expected) = &self.remote_fingerprint {
if !self.fingerprint_verification {
debug!("DTLS fingerprint verification disabled");
} else if fingerprint != *expected {
self.disconnect();
return Err(RtcError::RemoteSdp("remote fingerprint no match".into()));
}
} else {
self.disconnect();
return Err(RtcError::RemoteSdp("no a=fingerprint before dtls".into()));
}
}
DtlsOutput::ApplicationData(data) => {
self.sctp.handle_input(self.last_now, data);
}
DtlsOutput::Timeout(t) => {
self.next_dtls_timeout = Some(t);
break;
}
other => {
return Err(RtcError::Dtls(DtlsError::Io(std::io::Error::other(
format!("Unexpected DTLS output: {other:?}"),
))));
}
}
}
if just_connected {
return Ok(Output::Event(Event::Connected));
}
while let Some(e) = self.sctp.poll() {
match e {
SctpEvent::Transmit { mut packets } => {
if let Some(v) = packets.front() {
if let Err(e) = self.dtls.handle_input(v) {
if is_would_block(&e) {
self.sctp.push_back_transmit(packets);
break;
} else {
return Err(e.into());
}
}
packets.pop_front();
if !packets.is_empty() {
self.sctp.push_back_transmit(packets);
}
return self.do_poll_output();
}
}
SctpEvent::Open { id, label } => {
self.chan.ensure_channel_id_for(id);
let id = self.chan.channel_id_by_stream_id(id).unwrap();
return Ok(Output::Event(Event::ChannelOpen(id, label)));
}
SctpEvent::Close { id } => {
let Some(id) = self.chan.channel_id_by_stream_id(id) else {
warn!("Drop ChannelClose event for id: {:?}", id);
continue;
};
self.chan.remove_channel(id, self.last_now);
return Ok(Output::Event(Event::ChannelClose(id)));
}
SctpEvent::Data { id, binary, data } => {
let Some(id) = self.chan.channel_id_by_stream_id(id) else {
warn!("Drop ChannelData event for id: {:?}", id);
continue;
};
let cd = ChannelData { id, binary, data };
return Ok(Output::Event(Event::ChannelData(cd)));
}
SctpEvent::BufferedAmountLow { id } => {
let Some(id) = self.chan.channel_id_by_stream_id(id) else {
warn!("Drop BufferedAmountLow for id: {:?}", id);
continue;
};
return Ok(Output::Event(Event::ChannelBufferedAmountLow(id)));
}
}
}
if let Some(ev) = self.session.poll_event() {
return Ok(Output::Event(ev));
}
if let Some(ev) = self.session.poll_event_fallible()? {
return Ok(Output::Event(ev));
}
if let Some(e) = self.stats.as_mut().and_then(|s| s.poll_output()) {
return Ok(match e {
StatsEvent::Peer(s) => Output::Event(Event::PeerStats(s)),
StatsEvent::MediaIngress(s) => Output::Event(Event::MediaIngressStats(s)),
StatsEvent::MediaEgress(s) => Output::Event(Event::MediaEgressStats(s)),
});
}
if let Some(v) = self.ice.poll_transmit() {
return Ok(Output::Transmit(v));
}
if let Some(send) = &self.send_addr {
let datagram = None
.or_else(|| self.dtls.poll_packet())
.or_else(|| self.session.poll_datagram(self.last_now));
if let Some(contents) = datagram {
let t = net::Transmit {
proto: send.proto,
source: send.source,
destination: send.destination,
contents,
};
return Ok(Output::Transmit(t));
}
} else {
self.session.clear_feedback();
}
let stats = self.stats.as_mut();
if let Some(timeout) = self.next_dtls_timeout {
if timeout <= self.last_now {
let _ = self.dtls.handle_timeout(self.last_now);
self.next_dtls_timeout = None;
}
}
let time_and_reason = (None, Reason::NotHappening)
.soonest((self.next_dtls_timeout, Reason::DTLS))
.soonest((self.ice.poll_timeout(), Reason::Ice))
.soonest(self.session.poll_timeout())
.soonest((self.sctp.poll_timeout(), Reason::Sctp))
.soonest((self.chan.poll_timeout(&self.sctp), Reason::Channel))
.soonest((stats.and_then(|s| s.poll_timeout()), Reason::Stats));
let time = time_and_reason.0.unwrap_or_else(not_happening);
let reason = time_and_reason.1;
let next = if time < self.last_now {
self.last_now
} else {
time
};
self.last_timeout_reason = reason;
Ok(Output::Timeout(next))
}
pub fn last_timeout_reason(&self) -> Reason {
self.last_timeout_reason
}
pub fn accepts(&self, input: &Input) -> bool {
let Input::Receive(_, r) = input else {
return true;
};
if let Some(send_addr) = &self.send_addr {
if r.source == send_addr.destination {
return true;
}
}
if let DatagramRecvInner::Stun(v) = &r.contents.inner {
return self.ice.accepts_message(v);
}
if self.ice.has_viable_remote_candidate(r.source) {
return true;
}
false
}
pub fn handle_input(&mut self, input: Input) -> Result<(), RtcError> {
if !self.alive {
return Ok(());
}
match input {
Input::Timeout(now) => self.do_handle_timeout(now)?,
Input::Receive(now, r) => {
self.do_handle_receive(now, r)?;
self.do_handle_timeout(now)?;
}
}
Ok(())
}
fn init_time(&mut self, now: Instant) {
if !self.need_init_time {
return;
}
let _ = now.to_unix_duration();
self.need_init_time = false;
}
fn do_handle_timeout(&mut self, now: Instant) -> Result<(), RtcError> {
self.init_time(now);
if now < self.last_now {
return Ok(());
}
self.last_now = now;
self.ice.handle_timeout(now);
self.sctp.handle_timeout(now);
self.chan.expire_closed_stream_ids(now);
self.chan.handle_timeout(now, &mut self.sctp);
self.session.handle_timeout(now)?;
if let Some(stats) = &mut self.stats {
if stats.wants_timeout(now) {
let mut snapshot = StatsSnapshot::new(now);
snapshot.peer_rx = self.peer_bytes_rx;
snapshot.peer_tx = self.peer_bytes_tx;
snapshot.selected_candidate_pair =
self.send_addr.as_ref().map(|s| CandidatePairStats {
protocol: s.proto,
local: CandidateStats { addr: s.source },
remote: CandidateStats {
addr: s.destination,
},
});
self.session.visit_stats(now, &mut snapshot);
stats.do_handle_timeout(&mut snapshot);
}
}
Ok(())
}
fn do_handle_receive(&mut self, recv_time: Instant, r: net::Receive) -> Result<(), RtcError> {
trace!("IN {:?}", r);
use DatagramRecvInner::*;
let bytes_rx = match r.contents.inner {
Stun(_) => 0,
Dtls(v) | Rtp(v) | Rtcp(v) => v.len(),
};
self.peer_bytes_rx += bytes_rx as u64;
match r.contents.inner {
Stun(stun) => {
let packet = is::stun::StunPacket {
proto: r.proto,
source: r.source,
destination: r.destination,
message: stun,
};
self.ice.handle_packet(recv_time, packet);
}
Dtls(dtls) => self.dtls.handle_receive(dtls)?,
Rtp(rtp) => self.session.handle_rtp_receive(recv_time, rtp),
Rtcp(rtcp) => self.session.handle_rtcp_receive(recv_time, rtcp),
}
Ok(())
}
pub fn channel(&mut self, id: ChannelId) -> Option<Channel<'_>> {
if !self.alive {
return None;
}
let sctp_stream_id = self.chan.stream_id_by_channel_id(id)?;
if !self.sctp.is_open(sctp_stream_id) {
return None;
}
Some(Channel::new(sctp_stream_id, self))
}
pub fn bwe(&mut self) -> Bwe {
Bwe(self)
}
fn is_correct_change_id(&self, change_id: usize) -> bool {
self.change_counter == change_id + 1
}
fn next_change_id(&mut self) -> usize {
let n = self.change_counter;
self.change_counter += 1;
n
}
pub fn codec_config(&self) -> &CodecConfig {
&self.session.codec_config
}
}
impl PartialEq for Event {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::IceConnectionStateChange(l0), Self::IceConnectionStateChange(r0)) => l0 == r0,
(Self::MediaAdded(m0), Self::MediaAdded(m1)) => m0 == m1,
(Self::MediaData(m1), Self::MediaData(m2)) => m1 == m2,
(Self::ChannelOpen(l0, l1), Self::ChannelOpen(r0, r1)) => l0 == r0 && l1 == r1,
(Self::ChannelData(l0), Self::ChannelData(r0)) => l0 == r0,
(Self::ChannelClose(l0), Self::ChannelClose(r0)) => l0 == r0,
_ => false,
}
}
}
impl Eq for Event {}
impl fmt::Debug for Rtc {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Rtc").finish()
}
}
macro_rules! log_stat {
($name:expr, $($arg:expr),+) => {
#[cfg(feature = "_internal_dont_use_log_stats")]
{
use std::time::SystemTime;
use std::io::{self, Write};
let now = SystemTime::now();
let since_epoch = now.duration_since(SystemTime::UNIX_EPOCH).unwrap();
let unix_time_ms = since_epoch.as_millis();
let mut lock = io::stdout().lock();
write!(lock, "{} ", $name).expect("Failed to write to stdout");
$(
write!(lock, "{},", $arg).expect("Failed to write to stdout");
)+
writeln!(lock, "{}", unix_time_ms).expect("Failed to write to stdout");
}
};
}
pub(crate) use log_stat;
#[cfg(test)]
#[doc(hidden)]
pub fn init_crypto_default() {
crate::crypto::from_feature_flags().install_process_default();
}
#[cfg(test)]
mod test {
use std::panic::UnwindSafe;
use super::*;
#[test]
fn rtc_is_send() {
fn is_send<T: Send>(_t: T) {}
fn is_sync<T: Sync>(_t: T) {}
is_send(Rtc::new(Instant::now()));
is_sync(Rtc::new(Instant::now()));
}
#[test]
fn rtc_is_unwind_safe() {
fn is_unwind_safe<T: UnwindSafe>(_t: T) {}
is_unwind_safe(Rtc::new(Instant::now()));
}
#[test]
fn event_is_reasonably_sized() {
let n = std::mem::size_of::<Event>();
assert!(n < 490); }
}
#[cfg(feature = "_internal_test_exports")]
#[allow(missing_docs)]
pub mod _internal_test_exports;
#[cfg(feature = "unversioned")]
pub mod unversioned {
pub use super::packet::{
Depacketizer, H264Depacketizer, H264Packetizer, OpusPacketizer, Packetizer,
Vp8Depacketizer, Vp8Packetizer,
};
}