1mod amf;
39mod chunk;
40mod flv;
41mod handshake;
42mod relay;
43
44pub use relay::RtmpRelay;
45
46use crate::auth::{token_from_query, Credentials, EgressGate};
47use crate::bus::{PlaybackRegistry, PublishRegistry, StreamHandle};
48use crate::{MediaFrame, Result, StreamError, StreamKey};
49use amf::Amf0Value;
50use async_trait::async_trait;
51use chunk::{ChunkReader, ChunkWriter, RtmpMessage};
52use std::net::SocketAddr;
53use std::sync::Arc;
54use tokio::net::TcpStream;
55use tokio_util::sync::CancellationToken;
56use tracing::{debug, info, warn};
57
58fn stream_token_of(name: Option<&Amf0Value>) -> Option<String> {
63 crate::auth::token_from_query(name.and_then(Amf0Value::as_str)?)
64}
65
66pub const DEFAULT_RTMP_PORT: u16 = 1935;
68const OUT_CHUNK_SIZE: usize = 4096;
71
72const MSG_SET_CHUNK_SIZE: u8 = 1;
74const MSG_ACK: u8 = 3;
75const MSG_USER_CONTROL: u8 = 4;
76const MSG_WINDOW_ACK_SIZE: u8 = 5;
77const MSG_SET_PEER_BANDWIDTH: u8 = 6;
78const MSG_AUDIO: u8 = 8;
79const MSG_VIDEO: u8 = 9;
80const MSG_DATA_AMF3: u8 = 15;
81const MSG_DATA_AMF0: u8 = 18;
82const MSG_COMMAND_AMF3: u8 = 17;
83const MSG_COMMAND_AMF0: u8 = 20;
84
85const CSID_CONTROL: u8 = 2;
87const CSID_COMMAND: u8 = 3;
88const CSID_AUDIO: u8 = 4;
89const CSID_VIDEO: u8 = 6;
90
91const STREAM_ID: u32 = 1;
94
95pub struct RtmpHandler {
106 addr: SocketAddr,
107 max_connections: usize,
108 playback: Option<Arc<dyn PlaybackRegistry>>,
109 gate: Option<EgressGate>,
110}
111
112impl RtmpHandler {
113 pub fn new(addr: SocketAddr) -> Self {
115 Self {
116 addr,
117 max_connections: 1024,
118 playback: None,
119 gate: None,
120 }
121 }
122
123 pub fn max_connections(mut self, max: usize) -> Self {
125 self.max_connections = max;
126 self
127 }
128
129 pub fn with_playback(mut self, playback: Arc<dyn PlaybackRegistry>) -> Self {
132 self.playback = Some(playback);
133 self
134 }
135
136 pub fn with_gate(mut self, gate: EgressGate) -> Self {
142 self.gate = Some(gate);
143 self
144 }
145}
146
147#[async_trait]
155impl crate::inbound::InboundProtocol for RtmpHandler {
156 fn name(&self) -> &'static str {
157 "rtmp"
158 }
159
160 async fn serve(
161 &self,
162 ctx: crate::inbound::IngestContext,
163 shutdown: CancellationToken,
164 ) -> Result<()> {
165 info!(addr = %self.addr, "rtmp handler listening");
166 let registry = Arc::clone(ctx.registry());
171 let playback = self.playback.clone();
172 let gate = self.gate.clone();
173 crate::protocol::run_tcp_ingest_server(
174 self.addr,
175 self.max_connections,
176 shutdown,
177 move |sock, peer| {
178 let registry = Arc::clone(®istry);
179 let playback = playback.clone();
180 let gate = gate.clone();
181 async move {
182 if let Err(e) = handle_connection(sock, peer, registry, playback, gate).await {
183 warn!(%peer, error = %e, "rtmp session ended with error");
184 }
185 }
186 },
187 )
188 .await
189 }
190}
191
192async fn handle_connection(
194 mut sock: TcpStream,
195 peer: SocketAddr,
196 registry: Arc<dyn PublishRegistry>,
197 playback: Option<Arc<dyn PlaybackRegistry>>,
198 gate: Option<EgressGate>,
199) -> Result<()> {
200 handshake::accept(&mut sock).await?;
201 debug!(%peer, "rtmp handshake complete");
202 let (read_half, write_half) = sock.into_split();
203 let mut session = Session::new(
204 ChunkReader::new(read_half),
205 ChunkWriter::new(write_half),
206 registry,
207 playback,
208 gate,
209 Some(peer),
210 );
211 session.run().await
212}
213
214#[derive(PartialEq)]
216enum Role {
217 Pending,
218 Publishing,
219 Playing,
220}
221
222struct Session<R, W> {
224 reader: ChunkReader<R>,
225 writer: ChunkWriter<W>,
226 registry: Arc<dyn PublishRegistry>,
227 playback: Option<Arc<dyn PlaybackRegistry>>,
228 gate: Option<EgressGate>,
229 peer: Option<SocketAddr>,
231 app: Option<String>,
232 role: Role,
233 publish_key: Option<StreamKey>,
234 handle: Option<StreamHandle>,
235 avc: Option<flv::AvcConfig>,
236 aac: Option<flv::AudioConfig>,
237 audio_seq_sent: bool,
239 stop: bool,
240}
241
242impl<R, W> Session<R, W>
243where
244 R: tokio::io::AsyncRead + Unpin,
245 W: tokio::io::AsyncWrite + Unpin,
246{
247 fn new(
248 reader: ChunkReader<R>,
249 writer: ChunkWriter<W>,
250 registry: Arc<dyn PublishRegistry>,
251 playback: Option<Arc<dyn PlaybackRegistry>>,
252 gate: Option<EgressGate>,
253 peer: Option<SocketAddr>,
254 ) -> Self {
255 Self {
256 reader,
257 writer,
258 registry,
259 playback,
260 gate,
261 peer,
262 app: None,
263 role: Role::Pending,
264 publish_key: None,
265 handle: None,
266 avc: None,
267 aac: None,
268 audio_seq_sent: false,
269 stop: false,
270 }
271 }
272
273 async fn run(&mut self) -> Result<()> {
275 let result = self.event_loop().await;
276 if let Some(key) = self.publish_key.take() {
277 let _ = self.registry.end_publish(&key).await;
278 }
279 match result {
280 Ok(()) => Ok(()),
281 Err(e) if is_disconnect(&e) => Ok(()),
282 Err(e) => Err(e),
283 }
284 }
285
286 async fn event_loop(&mut self) -> Result<()> {
287 while !self.stop {
288 let msg = self.reader.read_message().await?;
289 self.process(msg).await?;
290 }
291 Ok(())
292 }
293
294 async fn process(&mut self, msg: RtmpMessage) -> Result<()> {
295 match msg.type_id {
296 MSG_SET_CHUNK_SIZE => {
297 if let Ok(b) = <[u8; 4]>::try_from(&msg.payload[..msg.payload.len().min(4)]) {
298 self.reader.set_chunk_size(u32::from_be_bytes(b) as usize);
299 }
300 }
301 MSG_COMMAND_AMF0 => self.handle_command(&msg.payload).await?,
302 MSG_COMMAND_AMF3 => {
303 self.handle_command(&msg.payload[1.min(msg.payload.len())..])
306 .await?
307 }
308 MSG_VIDEO if self.role == Role::Publishing => self.on_video(&msg)?,
309 MSG_AUDIO if self.role == Role::Publishing => self.on_audio(&msg)?,
310 MSG_DATA_AMF0 | MSG_DATA_AMF3 => { }
311 MSG_ACK | MSG_WINDOW_ACK_SIZE | MSG_SET_PEER_BANDWIDTH | MSG_USER_CONTROL => {}
312 other => debug!(
313 type_id = other,
314 stream = msg.msg_stream_id,
315 "ignoring RTMP message"
316 ),
317 }
318 Ok(())
319 }
320
321 async fn handle_command(&mut self, payload: &[u8]) -> Result<()> {
324 let values = amf::decode_all(payload);
325 let Some(name) = values.first().and_then(Amf0Value::as_str) else {
326 return Ok(());
327 };
328 let txn = values.get(1).and_then(Amf0Value::as_number).unwrap_or(0.0);
329 match name {
330 "connect" => self.on_connect(txn, values.get(2)).await,
331 "createStream" => self.on_create_stream(txn).await,
332 "publish" => self.on_publish(values.get(3)).await,
333 "play" => self.on_play(values.get(3)).await,
334 "releaseStream" | "FCPublish" | "FCUnpublish" | "deleteStream" | "closeStream"
335 | "FCSubscribe" => Ok(()),
336 other => {
337 debug!(command = other, "unhandled RTMP command");
338 Ok(())
339 }
340 }
341 }
342
343 async fn on_connect(&mut self, txn: f64, cmd_obj: Option<&Amf0Value>) -> Result<()> {
344 let app = cmd_obj
345 .and_then(|o| o.get("app"))
346 .and_then(Amf0Value::as_str)
347 .unwrap_or("")
348 .split('?')
349 .next()
350 .unwrap_or("")
351 .to_string();
352 self.app = Some(app);
353
354 self.send_control(MSG_WINDOW_ACK_SIZE, &2_500_000u32.to_be_bytes())
356 .await?;
357 let mut bw = Vec::from(2_500_000u32.to_be_bytes());
358 bw.push(2); self.send_control(MSG_SET_PEER_BANDWIDTH, &bw).await?;
360 self.send_control(MSG_SET_CHUNK_SIZE, &(OUT_CHUNK_SIZE as u32).to_be_bytes())
361 .await?;
362 self.writer.set_chunk_size(OUT_CHUNK_SIZE);
363
364 let props = amf::object(vec![
365 ("fmsVer", amf::string("FMS/3,0,1,123")),
366 ("capabilities", Amf0Value::Number(31.0)),
367 ]);
368 let info = amf::object(vec![
369 ("level", amf::string("status")),
370 ("code", amf::string("NetConnection.Connect.Success")),
371 ("description", amf::string("Connection succeeded.")),
372 ("objectEncoding", Amf0Value::Number(0.0)),
373 ]);
374 self.send_command(
375 0,
376 &[amf::string("_result"), Amf0Value::Number(txn), props, info],
377 )
378 .await
379 }
380
381 async fn on_create_stream(&mut self, txn: f64) -> Result<()> {
382 self.send_command(
383 0,
384 &[
385 amf::string("_result"),
386 Amf0Value::Number(txn),
387 Amf0Value::Null,
388 Amf0Value::Number(STREAM_ID as f64),
389 ],
390 )
391 .await
392 }
393
394 async fn on_publish(&mut self, name: Option<&Amf0Value>) -> Result<()> {
395 let key = self.stream_key(name)?;
396 let mut creds = match stream_token_of(name) {
400 Some(t) => Credentials::token(t),
401 None => Credentials::default(),
402 };
403 creds.addr = self.peer;
406 let handle = self.registry.start_publish_checked(&key, &creds).await?;
407 info!(stream = %key, "rtmp publish started");
408 self.handle = Some(handle);
409 self.publish_key = Some(key);
410 self.role = Role::Publishing;
411 self.send_status("status", "NetStream.Publish.Start", "Publishing started.")
412 .await
413 }
414
415 async fn on_play(&mut self, name: Option<&Amf0Value>) -> Result<()> {
416 let key = self.stream_key(name)?;
417 let Some(playback) = self.playback.clone() else {
418 self.send_status("error", "NetStream.Play.Failed", "Playback not enabled.")
419 .await?;
420 self.stop = true;
421 return Ok(());
422 };
423 if let Some(gate) = self.gate.clone() {
428 let token = name.and_then(Amf0Value::as_str).and_then(token_from_query);
429 if !gate(key.clone(), token, self.peer).await {
430 self.send_status("error", "NetStream.Play.Failed", "Playback not authorized.")
431 .await?;
432 self.stop = true;
433 return Ok(());
434 }
435 }
436 let handle = playback.get_stream(&key)?;
437 info!(stream = %key, "rtmp play started");
438 self.role = Role::Playing;
439
440 let mut begin = Vec::from(0u16.to_be_bytes()); begin.extend_from_slice(&STREAM_ID.to_be_bytes());
443 self.send_control(MSG_USER_CONTROL, &begin).await?;
444 self.send_status("status", "NetStream.Play.Reset", "Playing and resetting.")
445 .await?;
446 self.send_status("status", "NetStream.Play.Start", "Started playing.")
447 .await?;
448
449 self.serve_play(handle).await?;
450 self.stop = true;
451 Ok(())
452 }
453
454 fn on_video(&mut self, msg: &RtmpMessage) -> Result<()> {
457 let Some(header) = flv::parse_video_header(&msg.payload) else {
458 return Ok(()); };
460 let body = &msg.payload[header.body_offset..];
461 let ts = msg.timestamp as i64;
462
463 if header.avc_packet_type == flv::PKT_SEQUENCE_HEADER {
464 let cfg = flv::AvcConfig::parse(body)
465 .ok_or_else(|| StreamError::protocol("bad AVCDecoderConfigurationRecord"))?;
466 let mut frame =
467 MediaFrame::new_video(ts, ts, cfg.to_annexb(), crate::CodecId::H264, false);
468 frame.flags |= crate::FrameFlags::CONFIG;
469 self.avc = Some(cfg);
470 self.publish(frame);
471 } else if header.avc_packet_type == flv::PKT_RAW {
472 let Some(cfg) = self.avc.as_ref() else {
473 return Ok(()); };
475 let annexb = cfg
476 .avcc_to_annexb(body, header.is_keyframe)
477 .ok_or_else(|| StreamError::protocol("malformed AVCC NAL"))?;
478 let pts = ts + header.composition_time as i64;
479 let frame =
480 MediaFrame::new_video(pts, ts, annexb, crate::CodecId::H264, header.is_keyframe);
481 self.publish(frame);
482 }
483 Ok(())
484 }
485
486 fn on_audio(&mut self, msg: &RtmpMessage) -> Result<()> {
487 let payload = &msg.payload;
488 let Some(&b0) = payload.first() else {
489 return Ok(());
490 };
491 if b0 >> 4 != flv::AUDIO_FORMAT_AAC {
492 return Ok(()); }
494 let aac_packet_type = *payload.get(1).unwrap_or(&0);
495 let body = &payload[2.min(payload.len())..];
496 let ts = msg.timestamp as i64;
497
498 if aac_packet_type == flv::PKT_SEQUENCE_HEADER {
499 let cfg = flv::AudioConfig::parse(body)
500 .ok_or_else(|| StreamError::protocol("bad AudioSpecificConfig"))?;
501 self.aac = Some(cfg);
502 let mut frame =
503 MediaFrame::new_audio(ts, bytes::Bytes::copy_from_slice(body), crate::CodecId::AAC);
504 frame.flags |= crate::FrameFlags::CONFIG;
505 self.publish(frame);
506 } else if let Some(cfg) = self.aac.as_ref() {
507 let adts = cfg.to_adts(body);
508 self.publish(MediaFrame::new_audio(ts, adts, crate::CodecId::AAC));
509 }
510 Ok(())
511 }
512
513 fn publish(&self, frame: MediaFrame) {
514 if let Some(handle) = self.handle.as_ref() {
515 let _ = handle.publish_frame(frame);
516 }
517 }
518
519 async fn serve_play(&mut self, handle: StreamHandle) -> Result<()> {
522 for frame in handle.replay_buffer() {
524 self.send_media(&frame).await?;
525 }
526 let mut sub = handle.subscribe_resilient();
527 while let Some(frame) = sub.recv().await {
528 self.send_media(&frame).await?;
529 }
530 Ok(())
531 }
532
533 async fn send_media(&mut self, frame: &MediaFrame) -> Result<()> {
534 let is_config = frame.flags.contains(crate::FrameFlags::CONFIG);
535 match frame.codec {
536 crate::CodecId::H264 => {
537 let ts = frame.dts.max(0) as u32;
538 let tag = if is_config {
539 let cfg = annexb_to_avc_config(&frame.data);
540 flv::build_video_tag(false, flv::PKT_SEQUENCE_HEADER, 0, &cfg.to_avc_record())
541 } else {
542 let avcc = crate::codec::h264::annexb_to_avcc(&frame.data);
543 let cts = (frame.pts - frame.dts) as i32;
544 flv::build_video_tag(frame.is_keyframe(), flv::PKT_RAW, cts, &avcc)
545 };
546 self.writer
547 .write_message(CSID_VIDEO, MSG_VIDEO, ts, STREAM_ID, &tag)
548 .await
549 }
550 crate::CodecId::AAC => {
551 let ts = frame.pts.max(0) as u32;
552 if is_config {
553 self.audio_seq_sent = true;
554 let tag = flv::build_audio_tag(flv::PKT_SEQUENCE_HEADER, &frame.data);
555 return self
556 .writer
557 .write_message(CSID_AUDIO, MSG_AUDIO, ts, STREAM_ID, &tag)
558 .await;
559 }
560 if !self.audio_seq_sent {
564 if let Some(cfg) = flv::AudioConfig::from_adts(&frame.data) {
565 let seq = flv::build_audio_tag(flv::PKT_SEQUENCE_HEADER, &cfg.to_asc());
566 self.writer
567 .write_message(CSID_AUDIO, MSG_AUDIO, ts, STREAM_ID, &seq)
568 .await?;
569 self.audio_seq_sent = true;
570 }
571 }
572 let raw = frame.data.get(7..).unwrap_or(&[]);
574 let tag = flv::build_audio_tag(flv::PKT_RAW, raw);
575 self.writer
576 .write_message(CSID_AUDIO, MSG_AUDIO, ts, STREAM_ID, &tag)
577 .await
578 }
579 _ => Ok(()), }
581 }
582
583 fn stream_key(&self, name: Option<&Amf0Value>) -> Result<StreamKey> {
587 let app = self
588 .app
589 .clone()
590 .ok_or_else(|| StreamError::protocol("stream command before connect"))?;
591 let stream = name
592 .and_then(Amf0Value::as_str)
593 .ok_or_else(|| StreamError::protocol("missing stream name"))?
594 .split('?')
595 .next()
596 .unwrap_or("")
597 .to_string();
598 Ok(StreamKey::new(app, stream))
599 }
600
601 async fn send_control(&mut self, type_id: u8, payload: &[u8]) -> Result<()> {
602 self.writer
603 .write_message(CSID_CONTROL, type_id, 0, 0, payload)
604 .await
605 }
606
607 async fn send_command(&mut self, msg_stream_id: u32, values: &[Amf0Value]) -> Result<()> {
608 let mut buf = bytes::BytesMut::new();
609 for v in values {
610 v.encode(&mut buf);
611 }
612 self.writer
613 .write_message(CSID_COMMAND, MSG_COMMAND_AMF0, 0, msg_stream_id, &buf)
614 .await
615 }
616
617 async fn send_status(&mut self, level: &str, code: &str, description: &str) -> Result<()> {
618 let info = amf::object(vec![
619 ("level", amf::string(level)),
620 ("code", amf::string(code)),
621 ("description", amf::string(description)),
622 ]);
623 self.send_command(
624 STREAM_ID,
625 &[
626 amf::string("onStatus"),
627 Amf0Value::Number(0.0),
628 Amf0Value::Null,
629 info,
630 ],
631 )
632 .await
633 }
634}
635
636fn annexb_to_avc_config(annexb: &[u8]) -> flv::AvcConfig {
638 let mut cfg = flv::AvcConfig {
639 nal_length_size: 4,
640 ..Default::default()
641 };
642 for nal in crate::codec::h264::iter_nals_annexb(annexb) {
643 match nal.first().map(|b| b & 0x1F) {
644 Some(7) => cfg.sps.push(nal.to_vec()),
645 Some(8) => cfg.pps.push(nal.to_vec()),
646 _ => {}
647 }
648 }
649 cfg
650}
651
652fn is_disconnect(e: &StreamError) -> bool {
654 matches!(e, StreamError::Io(io) if matches!(
655 io.kind(),
656 std::io::ErrorKind::UnexpectedEof
657 | std::io::ErrorKind::ConnectionReset
658 | std::io::ErrorKind::BrokenPipe
659 | std::io::ErrorKind::ConnectionAborted
660 ))
661}
662
663#[cfg(test)]
664mod tests {
665 use super::*;
666 use crate::{AppSpec, Engine};
667 use bytes::Bytes;
668
669 fn avcc_idr() -> Vec<u8> {
671 let slice = [0x65u8, 0x88, 0x84, 0x00];
672 let mut v = (slice.len() as u32).to_be_bytes().to_vec();
673 v.extend_from_slice(&slice);
674 v
675 }
676
677 fn avc_decoder_config() -> Vec<u8> {
678 let sps = [0x67u8, 0x42, 0x00, 0x1F, 0xAA];
679 let pps = [0x68u8, 0xCE, 0x3C, 0x80];
680 let mut r = vec![1, sps[1], sps[2], sps[3], 0xFF, 0xE1];
681 r.extend_from_slice(&(sps.len() as u16).to_be_bytes());
682 r.extend_from_slice(&sps);
683 r.push(1);
684 r.extend_from_slice(&(pps.len() as u16).to_be_bytes());
685 r.extend_from_slice(&pps);
686 r
687 }
688
689 async fn publishing_session() -> (Session<tokio::io::Empty, tokio::io::Sink>, Arc<Engine>) {
692 let engine = Engine::builder()
693 .application(AppSpec::new("live").gop_cache(64))
694 .build();
695 let key = StreamKey::new("live", "cam");
696 let handle = engine.start_publish(&key).await.unwrap();
697
698 let mut session = Session::new(
699 ChunkReader::new(tokio::io::empty()),
700 ChunkWriter::new(tokio::io::sink()),
701 engine.clone(),
702 Some(engine.clone()),
703 None,
704 None,
705 );
706 session.app = Some("live".into());
707 session.handle = Some(handle);
708 session.role = Role::Publishing;
709 (session, engine)
710 }
711
712 #[tokio::test]
713 async fn publish_video_seq_header_then_keyframe_reaches_gop_cache() {
714 let (mut session, _engine) = publishing_session().await;
715
716 let seq = flv::build_video_tag(false, flv::PKT_SEQUENCE_HEADER, 0, &avc_decoder_config());
718 session
719 .on_video(&RtmpMessage {
720 type_id: MSG_VIDEO,
721 timestamp: 0,
722 msg_stream_id: STREAM_ID,
723 payload: seq,
724 })
725 .unwrap();
726
727 let kf = flv::build_video_tag(true, flv::PKT_RAW, 0, &avcc_idr());
729 session
730 .on_video(&RtmpMessage {
731 type_id: MSG_VIDEO,
732 timestamp: 40,
733 msg_stream_id: STREAM_ID,
734 payload: kf,
735 })
736 .unwrap();
737
738 let handle = session.handle.as_ref().unwrap();
739 let (vcfg, _) = handle.cached_configs();
740 let vcfg = vcfg.expect("video config cached");
741 assert!(vcfg.flags.contains(crate::FrameFlags::CONFIG));
742 assert_eq!(&vcfg.data[..5], &[0, 0, 0, 1, 0x67]);
744
745 let replay = handle.replay_buffer();
748 let key = replay
749 .iter()
750 .find(|f| f.is_keyframe())
751 .expect("keyframe in GOP");
752 assert_eq!(&key.data[..5], &[0, 0, 0, 1, 0x67]);
753 }
754
755 #[tokio::test]
756 async fn publish_aac_builds_adts_frames() {
757 let (mut session, _engine) = publishing_session().await;
758 let asc = [0x12u8, 0x10];
760 let seq = flv::build_audio_tag(flv::PKT_SEQUENCE_HEADER, &asc);
761 session
762 .on_audio(&RtmpMessage {
763 type_id: MSG_AUDIO,
764 timestamp: 0,
765 msg_stream_id: STREAM_ID,
766 payload: seq,
767 })
768 .unwrap();
769
770 let raw = flv::build_audio_tag(flv::PKT_RAW, &[0x01, 0x02, 0x03]);
771 session
772 .on_audio(&RtmpMessage {
773 type_id: MSG_AUDIO,
774 timestamp: 23,
775 msg_stream_id: STREAM_ID,
776 payload: raw,
777 })
778 .unwrap();
779
780 let handle = session.handle.as_ref().unwrap();
782 let (_, acfg) = handle.cached_configs();
783 assert_eq!(&acfg.unwrap().data[..], &asc);
784 }
785
786 #[tokio::test]
787 async fn full_publish_session_drives_real_chunk_reader() {
788 use tokio::io::AsyncReadExt;
789
790 let engine = Engine::builder()
791 .application(AppSpec::new("live").gop_cache(64))
792 .build();
793
794 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
798 let addr = listener.local_addr().unwrap();
799 let engine_for_server = engine.clone();
800 let server = tokio::spawn(async move {
801 let (sock, _) = listener.accept().await.unwrap();
802 let (r, w) = sock.into_split();
803 let mut session = Session::new(
804 ChunkReader::new(r),
805 ChunkWriter::new(w),
806 engine_for_server,
807 None,
808 None,
809 None,
810 );
811 session.run().await.unwrap(); let handle = session.handle.as_ref().expect("publish handle");
813 let has_config = handle.cached_configs().0.is_some();
814 let has_keyframe = handle.replay_buffer().iter().any(|f| f.is_keyframe());
815 (has_config, has_keyframe)
816 });
817
818 let client = tokio::spawn(async move {
819 let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
820 let (mut read_half, write_half) = stream.into_split();
821 tokio::spawn(async move {
823 let mut buf = [0u8; 4096];
824 while let Ok(n) = read_half.read(&mut buf).await {
825 if n == 0 {
826 break;
827 }
828 }
829 });
830
831 let cmd = |values: &[Amf0Value]| {
832 let mut b = bytes::BytesMut::new();
833 for v in values {
834 v.encode(&mut b);
835 }
836 b
837 };
838 let mut w = ChunkWriter::new(write_half);
839 let connect = cmd(&[
840 amf::string("connect"),
841 Amf0Value::Number(1.0),
842 amf::object(vec![("app", amf::string("live"))]),
843 ]);
844 w.write_message(CSID_COMMAND, MSG_COMMAND_AMF0, 0, 0, &connect)
845 .await
846 .unwrap();
847 let create = cmd(&[
848 amf::string("createStream"),
849 Amf0Value::Number(2.0),
850 Amf0Value::Null,
851 ]);
852 w.write_message(CSID_COMMAND, MSG_COMMAND_AMF0, 0, 0, &create)
853 .await
854 .unwrap();
855 let publish = cmd(&[
856 amf::string("publish"),
857 Amf0Value::Number(3.0),
858 Amf0Value::Null,
859 amf::string("cam"),
860 amf::string("live"),
861 ]);
862 w.write_message(CSID_COMMAND, MSG_COMMAND_AMF0, 0, 0, &publish)
863 .await
864 .unwrap();
865
866 let seq =
868 flv::build_video_tag(false, flv::PKT_SEQUENCE_HEADER, 0, &avc_decoder_config());
869 w.write_message(CSID_VIDEO, MSG_VIDEO, 0, STREAM_ID, &seq)
870 .await
871 .unwrap();
872 let kf = flv::build_video_tag(true, flv::PKT_RAW, 0, &avcc_idr());
873 w.write_message(CSID_VIDEO, MSG_VIDEO, 40, STREAM_ID, &kf)
874 .await
875 .unwrap();
876 });
878
879 client.await.unwrap();
880 let (has_config, has_keyframe) = server.await.unwrap();
881 assert!(has_config, "video config reached the engine over the wire");
882 assert!(has_keyframe, "keyframe reached the GOP cache over the wire");
883 }
884
885 #[test]
886 fn annexb_config_splits_sps_and_pps() {
887 let annexb = Bytes::from_static(&[0, 0, 0, 1, 0x67, 0x42, 0x00, 1, 0, 0, 0, 1, 0x68, 0xCE]);
888 let cfg = annexb_to_avc_config(&annexb);
889 assert_eq!(cfg.sps.len(), 1);
890 assert_eq!(cfg.pps.len(), 1);
891 assert_eq!(cfg.sps[0][0] & 0x1F, 7);
892 assert_eq!(cfg.pps[0][0] & 0x1F, 8);
893 }
894}