use std::sync::Arc;
use std::time::Duration;
use broadcast_common::Unpackage;
use media_plane::trunk::{RetentionClass, Trunk, TrunkConfig};
use multimux::config::{PushFormat, ReconnectPolicy};
use multimux::push::{RtmpTransport, RtmpTransportConfig, drive_push};
use rtmp_runtime::server::{ServerEvent, ServerSession};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio_util::sync::CancellationToken;
use transmux::FlvDemux;
use transmux::ir::TrackSpec;
const FLV: &[u8] = include_bytes!("../../fixtures/flv/av.flv");
const GUARD: Duration = Duration::from_secs(20);
fn nz(n: usize) -> std::num::NonZeroUsize {
std::num::NonZeroUsize::new(n).unwrap()
}
fn opaque_spec(track_id: u32) -> TrackSpec {
TrackSpec::new(
track_id,
90_000,
transmux::CodecConfig::Data {
stream_type: 0x06,
descriptors: Vec::new(),
carriage: transmux::ir::DataCarriage::Pes,
},
)
}
#[tokio::test]
async fn drive_push_negotiates_and_carries_only_flv_compatible_tracks() {
let mut demux = FlvDemux::new();
let media = demux.unpackage(FLV).expect("demux av.flv fixture");
assert_eq!(media.tracks.len(), 2, "fixture must carry AVC + AAC");
let trunk: Arc<Trunk> = Trunk::new(TrunkConfig::new(nz(256), nz(64), nz(8), nz(64), nz(64)));
let opaque_track_id = 99;
let mut specs: Vec<TrackSpec> = media.tracks.iter().map(|t| t.spec.clone()).collect();
specs.push(opaque_spec(opaque_track_id));
let writer = trunk
.writer()
.expect("trunk has a writer (nothing else holds it yet)");
writer.set_tracks(specs);
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("local addr");
let server = tokio::spawn(async move {
let (mut sock, _) = listener.accept().await.expect("accept");
let mut session = ServerSession::with_defaults();
let mut flv_bytes: Vec<u8> = Vec::new();
let mut saw_publish = false;
let mut buf = vec![0u8; 65536];
loop {
let n = match tokio::time::timeout(GUARD, sock.read(&mut buf)).await {
Ok(Ok(0)) | Err(_) => break,
Ok(Ok(n)) => n,
Ok(Err(e)) => panic!("server read failed: {e}"),
};
let (out, events) = session
.handle_data(&buf[..n])
.expect("server-side RTMP decode must not error");
if !out.is_empty() {
sock.write_all(&out).await.expect("server write reply");
}
for ev in events {
match ev {
ServerEvent::Publish { .. } => saw_publish = true,
ServerEvent::Media { flv } => flv_bytes.extend_from_slice(&flv),
_ => {}
}
}
let carried = FlvDemux::new()
.unpackage(&flv_bytes)
.map(|m| m.tracks.len())
.unwrap_or(0);
if carried >= 2 && flv_bytes.len() > 4096 {
break;
}
}
(flv_bytes, saw_publish)
});
let cfg = RtmpTransportConfig {
app: "live".to_string(),
stream_key: "test".to_string(),
};
let url = format!("rtmp://{addr}/live/test");
let cancel = CancellationToken::new();
let push_cancel = cancel.clone();
let push_task = tokio::spawn(async move {
drive_push::<RtmpTransport>(
trunk,
url,
cfg,
PushFormat::Ts,
ReconnectPolicy {
initial_backoff_ms: 100,
max_backoff_ms: 1_000,
max_attempts: None,
},
push_cancel,
)
.await
});
let opaque_sample = transmux::ir::Sample::new(
bytes::Bytes::from_static(&[0xAA, 0xBB, 0xCC, 0xDD]),
Some(0),
Some(0),
Some(3_000),
true,
);
let publish_media = media.clone();
let publish_task = tokio::spawn(async move {
loop {
for track in &publish_media.tracks {
for sample in &track.samples {
writer.publish(track.spec.track_id, RetentionClass::Timed, sample.clone());
}
}
writer.publish(
opaque_track_id,
RetentionClass::Timed,
opaque_sample.clone(),
);
tokio::time::sleep(Duration::from_millis(20)).await;
}
});
let (flv_bytes, saw_publish) = tokio::time::timeout(GUARD, server)
.await
.expect("server task must not hang")
.expect("server task panicked");
publish_task.abort();
cancel.cancel();
let _ = tokio::time::timeout(GUARD, push_task).await;
assert!(saw_publish, "server must have accepted the publish");
assert!(!flv_bytes.is_empty(), "server must have received media");
let mut demux2 = FlvDemux::new();
let media2 = demux2
.unpackage(&flv_bytes)
.expect("re-demux the RTMP-received FLV tags");
assert_eq!(
media2.tracks.len(),
2,
"server must recover exactly AVC + AAC -- never a third, opaque track"
);
for track in &media2.tracks {
assert!(
media
.tracks
.iter()
.any(|t| t.spec.track_id == track.spec.track_id),
"every recovered track must be one of the fixture's real AVC/AAC tracks"
);
assert!(
!track.samples.is_empty(),
"each carried track must have real samples"
);
}
}