use std::collections::VecDeque;
use bytes::Bytes;
use media_plane::egress::{NegotiationOutcome, PushEgress, TrackSelection};
use media_plane::trunk::SampleCursorItem;
use transmux::ir::{Media, Track, TrackSpec};
use crate::push::{PushTransport, SendMediaError};
pub struct PushTransportEgress<T: PushTransport> {
transport: T,
selection: TrackSelection,
specs: Vec<TrackSpec>,
committed: bool,
unsatisfiable_reason: &'static str,
outbound: VecDeque<Bytes>,
}
impl<T: PushTransport> PushTransportEgress<T> {
pub fn new(transport: T, unsatisfiable_reason: &'static str) -> Self {
PushTransportEgress {
transport,
selection: TrackSelection::new(Vec::new()),
specs: Vec::new(),
committed: false,
unsatisfiable_reason,
outbound: VecDeque::new(),
}
}
pub fn selected_tracks(&self) -> &[TrackSpec] {
&self.specs
}
pub fn transport_mut(&mut self) -> &mut T {
&mut self.transport
}
pub async fn flush_transmit(&mut self) -> Result<(), SendMediaError> {
while let Some(message) = PushEgress::poll_transmit(self) {
self.transport
.write_message(&message)
.await
.map_err(|e| SendMediaError::Transport(Box::new(e)))?;
}
Ok(())
}
fn carriable(&self, tracks: &[TrackSpec]) -> Vec<TrackSpec> {
tracks
.iter()
.filter(|t| self.transport.supports_codec(&t.config))
.cloned()
.collect()
}
}
impl<T: PushTransport> PushEgress for PushTransportEgress<T> {
type Error = SendMediaError;
fn negotiate(&mut self, tracks: &[TrackSpec]) -> NegotiationOutcome<Self::Error> {
let carriable = self.carriable(tracks);
if carriable.is_empty() {
return NegotiationOutcome::Error(SendMediaError::Mux(
self.unsatisfiable_reason.to_string(),
));
}
self.selection = TrackSelection::new(carriable.iter().map(|t| t.track_id).collect());
self.specs = carriable;
self.committed = true;
NegotiationOutcome::Accepted(self.selection.clone())
}
fn renegotiate(&mut self, tracks: &[TrackSpec]) -> NegotiationOutcome<Self::Error> {
let carriable = self.carriable(tracks);
if carriable.is_empty() {
return NegotiationOutcome::Error(SendMediaError::Mux(
self.unsatisfiable_reason.to_string(),
));
}
let proposed_ids: Vec<u32> = carriable.iter().map(|t| t.track_id).collect();
if self.committed && proposed_ids != self.selection.track_ids {
return NegotiationOutcome::Refused {
reason: "already publishing; cannot change the carried track set mid-session",
};
}
self.selection = TrackSelection::new(proposed_ids);
self.specs = carriable;
self.committed = true;
NegotiationOutcome::Accepted(self.selection.clone())
}
fn send(&mut self, item: &SampleCursorItem) -> Result<(), Self::Error> {
let (track_id, sample) = match item {
SampleCursorItem::Timed { track_id, sample }
| SampleCursorItem::Sparse { track_id, sample } => (*track_id, sample.clone()),
SampleCursorItem::Lagged { skipped } => {
tracing::warn!(skipped, "push egress cursor lagged; samples were dropped");
return Ok(());
}
SampleCursorItem::Degraded { skipped } => {
tracing::warn!(
skipped,
"push egress cursor degraded (sparse-ring eviction); consumer state may be stale"
);
return Ok(());
}
_ => return Ok(()),
};
let Some(spec) = self.specs.iter().find(|s| s.track_id == track_id).cloned() else {
return Ok(());
};
let timescale = spec.timescale;
let media = Media::new(vec![Track::new(spec, vec![sample])], timescale);
let messages = self.transport.encode_media(&media)?;
self.outbound.extend(messages);
Ok(())
}
fn poll_transmit(&mut self) -> Option<Bytes> {
self.outbound.pop_front()
}
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use transmux::CodecConfig;
#[derive(Default)]
struct FakeTransport {
sent: Vec<Bytes>,
restrict_to_avc_aac: bool,
}
#[derive(Debug, thiserror::Error)]
#[error("fake transport error")]
struct FakeError;
#[async_trait]
impl PushTransport for FakeTransport {
type Config = ();
type Error = FakeError;
async fn connect(_url: &str, _config: &Self::Config) -> Result<Self, Self::Error> {
Ok(Self::default())
}
async fn send(&mut self, data: &[u8]) -> Result<(), Self::Error> {
self.sent.push(Bytes::copy_from_slice(data));
Ok(())
}
fn supports_codec(&self, config: &CodecConfig) -> bool {
if self.restrict_to_avc_aac {
matches!(config, CodecConfig::Avc { .. } | CodecConfig::Aac { .. })
} else {
true
}
}
fn close(&mut self) {}
}
fn avc_spec(track_id: u32) -> TrackSpec {
TrackSpec::new(
track_id,
90_000,
CodecConfig::Avc {
config: transmux::AVCConfigurationBox::new(
transmux::AVCDecoderConfigurationRecord {
configuration_version: 1,
profile_indication: 0x42,
profile_compatibility: 0,
level_indication: 0x1f,
length_size_minus_one: 3,
sps: Vec::new(),
pps: Vec::new(),
chroma_format: None,
bit_depth_luma_minus8: None,
bit_depth_chroma_minus8: None,
sps_ext: Vec::new(),
},
),
width: 0,
height: 0,
},
)
}
fn opaque_spec(track_id: u32) -> TrackSpec {
TrackSpec::new(
track_id,
90_000,
CodecConfig::Data {
stream_type: 0x06,
descriptors: Vec::new(),
carriage: transmux::ir::DataCarriage::Pes,
},
)
}
fn sample() -> transmux::ir::Sample {
let nal: &[u8] = &[0, 0, 0, 1, 0x65];
transmux::ir::Sample::new(
bytes::Bytes::copy_from_slice(nal),
Some(0),
Some(0),
Some(3_000),
true,
)
}
#[test]
fn unrestricted_transport_accepts_every_track() {
let mut egress = PushTransportEgress::new(FakeTransport::default(), "unreachable");
match egress.negotiate(&[avc_spec(1), opaque_spec(2)]) {
NegotiationOutcome::Accepted(sel) => {
assert_eq!(sel.track_ids, vec![1, 2]);
}
other => panic!("expected Accepted, got {other:?}"),
}
}
#[test]
fn restricted_transport_errors_when_nothing_is_carriable() {
let transport = FakeTransport {
restrict_to_avc_aac: true,
..Default::default()
};
let mut egress = PushTransportEgress::new(transport, "no AVC video or AAC audio track");
match egress.negotiate(&[opaque_spec(9)]) {
NegotiationOutcome::Error(SendMediaError::Mux(reason)) => {
assert_eq!(reason, "no AVC video or AAC audio track");
}
other => panic!("expected Error(Mux(..)), got {other:?}"),
}
}
#[test]
fn restricted_transport_selects_only_the_carriable_subset() {
let transport = FakeTransport {
restrict_to_avc_aac: true,
..Default::default()
};
let mut egress = PushTransportEgress::new(transport, "no AVC video or AAC audio track");
match egress.negotiate(&[avc_spec(1), opaque_spec(2)]) {
NegotiationOutcome::Accepted(sel) => assert_eq!(sel.track_ids, vec![1]),
other => panic!("expected Accepted([1]), got {other:?}"),
}
assert_eq!(egress.selected_tracks().len(), 1);
}
#[test]
fn renegotiate_refuses_a_track_set_change_once_committed() {
let mut egress = PushTransportEgress::new(FakeTransport::default(), "unreachable");
egress.negotiate(&[avc_spec(1)]);
match egress.renegotiate(&[avc_spec(1), avc_spec(2)]) {
NegotiationOutcome::Refused { reason } => assert!(!reason.is_empty()),
other => panic!("expected Refused, got {other:?}"),
}
assert_eq!(egress.selected_tracks().len(), 1);
}
#[test]
fn send_for_an_unselected_track_is_a_silent_no_op() {
let transport = FakeTransport {
restrict_to_avc_aac: true,
..Default::default()
};
let mut egress = PushTransportEgress::new(transport, "no AVC video or AAC audio track");
egress.negotiate(&[avc_spec(1)]);
egress
.send(&SampleCursorItem::Timed {
track_id: 2, sample: sample(),
})
.expect("must not error");
assert!(
PushEgress::poll_transmit(&mut egress).is_none(),
"an unselected track's sample must never reach the transport"
);
}
#[tokio::test]
async fn send_then_flush_transmit_writes_a_real_encoded_message() {
let mut egress = PushTransportEgress::new(FakeTransport::default(), "unreachable");
egress.negotiate(&[avc_spec(1)]);
egress
.send(&SampleCursorItem::Timed {
track_id: 1,
sample: sample(),
})
.expect("send must not error");
assert!(
!egress.outbound.is_empty(),
"send must have queued at least one encoded message"
);
egress.flush_transmit().await.expect("flush must not error");
assert!(
egress.outbound.is_empty(),
"flush_transmit must drain everything it queued"
);
assert!(
!egress.transport.sent.is_empty(),
"the real transport must have received the flushed bytes"
);
}
}