1#[cfg(feature = "capture")]
12use std::time::Instant;
13
14use moq_mux::catalog::hang::CatalogExt;
15#[cfg(any(feature = "capture", test))]
16use moq_net::Timestamp;
17
18use crate::Error;
19#[cfg(any(feature = "capture", test))]
20use crate::Frame;
21#[cfg(feature = "capture")]
22use crate::capture;
23
24use super::Encoded;
25#[cfg(feature = "capture")]
26use super::Sink;
27#[cfg(any(feature = "capture", test))]
28use super::encoder;
29#[cfg(feature = "capture")]
30use super::encoder::Codec;
31#[cfg(feature = "capture")]
32use super::rate::{Control, Policy};
33
34#[cfg(feature = "capture")]
36const DEFAULT_FRAMERATE: u32 = 30;
37
38fn rendition_hint(rendition: hang::catalog::VideoConfig) -> moq_mux::catalog::VideoHint {
46 let mut hint = moq_mux::catalog::VideoHint::default();
47 hint.codec = Some(rendition.codec);
48 hint.coded_width = rendition.coded_width;
49 hint.coded_height = rendition.coded_height;
50 hint.display_aspect_width = rendition.display_aspect_width;
51 hint.display_aspect_height = rendition.display_aspect_height;
52 hint.framerate = rendition.framerate;
53 hint.bitrate = rendition.bitrate;
54 hint.optimize_for_latency = rendition.optimize_for_latency;
55 hint.container = rendition.container;
58 hint
59}
60
61enum Codecs<E: CatalogExt> {
64 H264 {
65 split: moq_mux::codec::h264::Split,
66 import: moq_mux::codec::h264::Import<E>,
67 },
68 H265 {
69 split: moq_mux::codec::h265::Split,
70 import: moq_mux::codec::h265::Import<E>,
71 },
72}
73
74pub struct Producer<E: CatalogExt = ()> {
85 codecs: Codecs<E>,
86}
87
88impl<E: CatalogExt> Producer<E> {
89 pub fn new(
101 mut broadcast: moq_net::broadcast::Producer,
102 catalog: moq_mux::catalog::Producer<E>,
103 rendition: hang::catalog::VideoConfig,
104 ) -> Result<Self, Error> {
105 let suffix = match &rendition.codec {
106 hang::catalog::VideoCodec::H264(_) => ".avc3",
107 hang::catalog::VideoCodec::H265(_) => ".hev1",
108 other => {
109 return Err(Error::Codec(anyhow::anyhow!(
110 "{other} is not a codec this producer can publish"
111 )));
112 }
113 };
114 let track = broadcast.unique_track(suffix, catalog.track_info())?;
115 Self::with_track(track, catalog, rendition)
116 }
117
118 pub fn with_track(
123 track: moq_net::track::Producer,
124 catalog: moq_mux::catalog::Producer<E>,
125 rendition: hang::catalog::VideoConfig,
126 ) -> Result<Self, Error> {
127 let codecs = match &rendition.codec {
128 hang::catalog::VideoCodec::H264(_) => Codecs::H264 {
129 split: moq_mux::codec::h264::Split::new(),
130 import: moq_mux::codec::h264::Import::new(track, catalog.reserve(), rendition_hint(rendition))?,
131 },
132 hang::catalog::VideoCodec::H265(_) => Codecs::H265 {
133 split: moq_mux::codec::h265::Split::new(),
134 import: moq_mux::codec::h265::Import::new(track, catalog.reserve(), rendition_hint(rendition))?,
135 },
136 other => {
138 return Err(Error::Codec(anyhow::anyhow!(
139 "{other} is not a codec this producer can publish"
140 )));
141 }
142 };
143 Ok(Self { codecs })
144 }
145
146 pub fn demand(&self) -> moq_net::track::Demand {
150 match &self.codecs {
151 Codecs::H264 { import, .. } => import.demand(),
152 Codecs::H265 { import, .. } => import.demand(),
153 }
154 }
155
156 pub fn publish(&mut self, encoded: &[Encoded]) -> Result<(), Error> {
159 for frame in encoded {
160 let timestamp = Some(frame.timestamp);
161 match &mut self.codecs {
163 Codecs::H264 { split, import } => {
164 let mut frames = split.decode(&frame.payload, timestamp)?;
165 frames.extend(split.flush(timestamp)?);
166 import.decode(frames)?;
167 }
168 Codecs::H265 { split, import } => {
169 let mut frames = split.decode(&frame.payload, timestamp)?;
170 frames.extend(split.flush(timestamp)?);
171 import.decode(frames)?;
172 }
173 }
174 }
175 Ok(())
176 }
177
178 pub fn discontinuity(&mut self) -> Result<(), Error> {
186 match &mut self.codecs {
187 Codecs::H264 { import, .. } => import.discontinuity()?,
188 Codecs::H265 { import, .. } => import.discontinuity()?,
189 }
190 Ok(())
191 }
192
193 pub fn finish(mut self) -> Result<(), Error> {
198 match &mut self.codecs {
199 Codecs::H264 { import, .. } => import.finish()?,
200 Codecs::H265 { import, .. } => import.finish()?,
201 }
202 Ok(())
203 }
204
205 pub fn abort(self, err: moq_net::Error) {
210 match self.codecs {
211 Codecs::H264 { import, .. } => import.abort(err),
212 Codecs::H265 { import, .. } => import.abort(err),
213 }
214 }
215}
216
217#[derive(Clone, Default)]
225#[non_exhaustive]
226#[cfg(feature = "capture")]
227pub struct Options {
228 pub bitrate: Option<u64>,
234 pub codec: Codec,
236 pub kind: encoder::Kind,
238 pub bandwidth: Option<moq_net::bandwidth::Consumer>,
249}
250
251#[cfg(feature = "capture")]
254impl std::fmt::Debug for Options {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 f.debug_struct("Options")
257 .field("bitrate", &self.bitrate)
258 .field("codec", &self.codec)
259 .field("kind", &self.kind)
260 .field("bandwidth", &self.bandwidth.is_some())
261 .finish()
262 }
263}
264
265#[cfg(feature = "capture")]
277pub async fn publish_capture<E: CatalogExt>(
278 broadcast: moq_net::broadcast::Producer,
279 catalog: moq_mux::catalog::Producer<E>,
280 capture: capture::Config,
281 encode: Options,
282 clock: moq_mux::Clock,
283) -> Result<(), Error> {
284 if capture.framerate == Some(0) {
287 return Err(Error::InvalidFramerate(0));
288 }
289
290 let rendition = {
295 let camera = capture::open(&capture).await?;
296 let mut probe_config = encoder::Config::new(
297 camera.width(),
298 camera.height(),
299 capture
300 .framerate
301 .or_else(|| camera.framerate())
302 .unwrap_or(DEFAULT_FRAMERATE),
303 );
304 probe_config.bitrate = encode.bitrate;
305 probe_config.codec = encode.codec;
306 probe_config.kind = encode.kind.clone();
307 probe_config.color = camera.color();
308 probe_config.probe().await?
309 };
310
311 let mut producer = Producer::new(broadcast, catalog, rendition)?;
312 let demand = producer.demand();
313
314 let result = capture_loop(&mut producer, &demand, &capture, &encode, &clock).await;
315
316 match &result {
320 Ok(()) => {
322 if let Err(err) = producer.finish() {
323 tracing::debug!(error = %err, "video track finish after capture ended");
324 }
325 }
326 Err(err) => producer.abort(moq_net::Error::Transport(err.to_string())),
328 }
329 result
330}
331
332#[cfg(all(feature = "capture", not(target_os = "macos")))]
338#[allow(dead_code)]
339fn assert_publish_capture_send(
340 broadcast: moq_net::broadcast::Producer,
341 catalog: moq_mux::catalog::Producer,
342 capture: capture::Config,
343 encode: Options,
344 clock: moq_mux::Clock,
345) {
346 fn is_send<T: Send>(_: &T) {}
347 is_send(&publish_capture(broadcast, catalog, capture, encode, clock));
348}
349
350#[cfg(feature = "capture")]
354type Rate = Option<(moq_net::bandwidth::Consumer, Control)>;
355
356#[cfg(feature = "capture")]
361async fn next_estimate(rate: &mut Rate) -> Option<Option<u64>> {
362 match rate {
363 Some((bandwidth, _)) => bandwidth.changed().await.ok(),
364 None => std::future::pending().await,
366 }
367}
368
369#[cfg(feature = "capture")]
375async fn apply_estimate(encoder: &mut Sink, rate: &mut Rate, estimate: Option<Option<u64>>) {
376 let Some((_, control)) = rate.as_mut() else { return };
377
378 let Some(estimate) = estimate else {
379 tracing::debug!("bandwidth estimate ended; holding the current encoder bitrate");
380 *rate = None;
381 return;
382 };
383
384 let Some(bitrate) = control.update(estimate, Instant::now()) else {
385 return;
386 };
387
388 match encoder.set_bitrate(bitrate).await {
389 Ok(()) => tracing::debug!(bitrate, estimate, "adjusted encoder bitrate"),
390 Err(Error::BitrateUnsupported(name)) => {
394 tracing::warn!(encoder = name, "encoder cannot follow the bandwidth estimate");
395 *rate = None;
396 }
397 Err(err) => tracing::warn!(error = %err, bitrate, "failed to adjust encoder bitrate"),
401 }
402}
403
404#[cfg(feature = "capture")]
408fn log_track_ended(err: moq_net::Error) {
409 if matches!(err, moq_net::Error::Dropped | moq_net::Error::Closed) {
410 tracing::debug!("video track no longer announced; stopping capture");
411 } else {
412 tracing::warn!(error = %err, "video track aborted; stopping capture");
413 }
414}
415
416#[cfg(any(feature = "capture", test))]
417fn capture_stopped<E: CatalogExt>(producer: &mut Producer<E>) -> Result<(), Error> {
418 producer.discontinuity()
421}
422
423#[cfg(feature = "capture")]
433async fn capture_loop<E: CatalogExt>(
434 producer: &mut Producer<E>,
435 demand: &moq_net::track::Demand,
436 capture: &capture::Config,
437 encode: &Options,
438 clock: &moq_mux::Clock,
439) -> Result<(), Error> {
440 loop {
441 if let Err(err) = demand.used().await {
445 log_track_ended(err);
446 return Ok(());
447 }
448
449 let mut camera = capture::open(capture).await?;
451 let framerate = capture
454 .framerate
455 .or_else(|| camera.framerate())
456 .unwrap_or(DEFAULT_FRAMERATE);
457 let mut encoder_config = encoder::Config::new(camera.width(), camera.height(), framerate);
458 encoder_config.bitrate = encode.bitrate;
459 encoder_config.codec = encode.codec;
460 encoder_config.kind = encode.kind.clone();
461 encoder_config.color = camera.color();
462 let mut encoder = Sink::open(&encoder_config).await?;
464 let mut force_keyframe = true;
467 tracing::info!(encoder = encoder.name(), device = camera.label(), "capturing");
468
469 let mut rate = encode
474 .bandwidth
475 .clone()
476 .map(|bandwidth| (bandwidth, Control::new(Policy::new(encoder_config.resolved_bitrate()))));
477
478 loop {
479 let frame = tokio::select! {
483 biased;
484 res = demand.unused() => {
485 if let Err(err) = res {
486 log_track_ended(err);
487 return Ok(());
488 }
489 break; }
491 estimate = next_estimate(&mut rate) => {
494 apply_estimate(&mut encoder, &mut rate, estimate).await;
495 continue;
496 }
497 frame = camera.read() => frame?,
500 };
501
502 let Some(surface) = frame else { break };
503
504 let frame = Frame::new(surface, Timestamp::from_micros(clock.micros())?);
507 if force_keyframe {
508 encoder.keyframe();
509 force_keyframe = false;
510 }
511 producer.publish(&encoder.encode(frame).await?)?;
512 }
513
514 drop(camera);
516 drop(encoder);
517 capture_stopped(producer)?;
518 tracing::info!("capture stopped; released source");
519 }
520}
521
522#[cfg(test)]
523mod tests {
524 use moq_mux::catalog::Stream as _;
525
526 use super::*;
527 use crate::encode::{Config, Encoder};
528
529 async fn roundtrip_rendition(codec: Codec, kind: encoder::Kind) -> (String, hang::catalog::VideoConfig) {
539 let mut broadcast = moq_net::broadcast::Info::new().produce();
540 let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
541
542 let mut config = Config::new(320, 240, 30);
543 config.codec = codec;
544 config.kind = kind;
545
546 let mut producer = Producer::new(broadcast, catalog.clone(), config.probe().await.unwrap()).unwrap();
547 let advertised = rendition(&catalog).expect("the rendition publishes before any frame").1;
548
549 let mut encoder = Encoder::new(&config).unwrap();
550 assert_eq!(encoder.codec(), codec);
551
552 let rgba = vec![0x80u8; 320 * 240 * 4];
553 for i in 0..10u64 {
554 let surface = crate::Surface::rgba(&rgba, crate::Size::new(320, 240)).unwrap();
555 let frame = Frame::new(surface, Timestamp::from_micros(i * 33_333).unwrap());
556 producer.publish(&encoder.encode(&frame).unwrap()).unwrap();
557 }
558 producer.publish(&encoder.finish().unwrap()).unwrap();
559
560 let (name, resolved) = rendition(&catalog).expect("the importer should have registered a video rendition");
561 let (mut before, mut after) = (advertised, resolved.clone());
563 before.jitter = None;
564 after.jitter = None;
565 assert_eq!(
566 before, after,
567 "the first keyframe should confirm the advertised rendition, not correct it"
568 );
569 (name, resolved)
570 }
571
572 fn rendition(catalog: &moq_mux::catalog::Producer) -> Option<(String, hang::catalog::VideoConfig)> {
574 let snapshot = catalog.snapshot();
575 let (name, config) = snapshot.video.renditions.iter().next()?;
576 Some((name.clone(), config.clone()))
577 }
578
579 async fn collect_groups(mut consumer: moq_net::track::Subscriber) -> Vec<usize> {
580 let mut groups = Vec::new();
581 while let Some(mut group) = consumer.recv_group().await.unwrap() {
582 let mut frames = 0;
583 while group.next_frame().await.unwrap().is_some() {
584 frames += 1;
585 }
586 groups.push(frames);
587 }
588 groups
589 }
590
591 #[tokio::test]
595 async fn idle_capture_publishes_a_discontinuity_before_resume() {
596 let mut broadcast = moq_net::broadcast::Info::new().produce();
597 let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
598 let track = broadcast.create_track("video", catalog.track_info()).unwrap();
599 let consumer = track.subscribe(None);
600
601 let mut config = Config::new(320, 240, 30);
602 config.kind = encoder::Kind::Software;
603 let mut producer = Producer::with_track(track, catalog, config.probe().await.unwrap()).unwrap();
604 let mut encoder = Encoder::new(&config).unwrap();
605 let rgba = vec![0x80u8; 320 * 240 * 4];
606
607 for timestamp in [0, 10_000_000] {
608 if timestamp > 0 {
609 capture_stopped(&mut producer).unwrap();
610 }
611 encoder.keyframe();
612 let surface = crate::Surface::rgba(&rgba, crate::Size::new(320, 240)).unwrap();
613 let frame = Frame::new(surface, Timestamp::from_micros(timestamp).unwrap());
614 producer.publish(&encoder.encode(&frame).unwrap()).unwrap();
615 }
616 producer.finish().unwrap();
617
618 assert_eq!(collect_groups(consumer).await, vec![1, 0, 1]);
619 }
620
621 #[tokio::test]
622 async fn source_resize_updates_the_published_rendition() {
623 let mut broadcast = moq_net::broadcast::Info::new().produce();
624 let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
625 let mut initial = Config::new(320, 240, 30);
626 initial.kind = encoder::Kind::Software;
627 let mut producer = Producer::new(broadcast, catalog.clone(), initial.probe().await.unwrap()).unwrap();
628
629 for (timestamp, config) in [(0, initial), (33_333, Config::new(640, 360, 30))] {
630 let mut config = config;
631 config.kind = encoder::Kind::Software;
632 let mut encoder = Encoder::new(&config).unwrap();
633 encoder.keyframe();
634 let rgba = vec![0x80u8; usize::try_from(config.width * config.height * 4).unwrap()];
635 let surface = crate::Surface::rgba(&rgba, crate::Size::new(config.width, config.height)).unwrap();
636 let frame = Frame::new(surface, Timestamp::from_micros(timestamp).unwrap());
637 producer.publish(&encoder.encode(&frame).unwrap()).unwrap();
638 capture_stopped(&mut producer).unwrap();
639 }
640
641 let (_, rendition) = rendition(&catalog).expect("the resized rendition should be published");
642 assert_eq!(rendition.coded_width, Some(640));
643 assert_eq!(rendition.coded_height, Some(360));
644 }
645
646 #[tokio::test]
652 async fn a_selected_container_survives_the_rendition_hint() {
653 let mut broadcast = moq_net::broadcast::Info::new().produce();
654 let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
655
656 let mut config = Config::new(320, 240, 30);
657 config.kind = encoder::Kind::Software;
659 let mut selected = config.probe().await.unwrap();
660 selected.container = hang::catalog::Container::Loc;
661
662 let _producer = Producer::new(broadcast, catalog.clone(), selected).unwrap();
663
664 let (_, published) = rendition(&catalog).expect("the rendition publishes before any frame");
665 assert_eq!(published.container, hang::catalog::Container::Loc);
666 }
667
668 #[tokio::test]
676 async fn the_rendition_reaches_the_wire_before_the_first_frame() {
677 let mut broadcast = moq_net::broadcast::Info::new().produce();
678 let consumer = broadcast.consume();
679 let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
680
681 let mut config = Config::new(1920, 1080, 30);
682 config.bitrate = Some(6_000_000);
683 config.kind = encoder::Kind::Software;
685 let _producer = Producer::new(broadcast, catalog, config.probe().await.unwrap()).unwrap();
686
687 let mut stream = moq_mux::catalog::Consumer::<()>::new(&consumer, moq_mux::catalog::CatalogFormat::Hang)
689 .await
690 .unwrap();
691 let snapshot = stream.next().await.unwrap().expect("a catalog before any frame");
692
693 let (name, rendition) = snapshot
694 .video
695 .renditions
696 .iter()
697 .next()
698 .expect("the track must be discoverable before it has encoded anything");
699 assert!(name.ends_with(".avc3"));
700
701 let hang::catalog::VideoCodec::H264(h264) = &rendition.codec else {
704 panic!("expected H.264, got {}", rendition.codec)
705 };
706 assert!(h264.inline, "an avc3 track carries its parameter sets in band");
707 assert_eq!(rendition.coded_width, Some(1920));
708 assert_eq!(rendition.coded_height, Some(1080));
709 assert_eq!(rendition.framerate, Some(30.0));
711 assert_eq!(rendition.bitrate, Some(6_000_000));
712 }
713
714 #[tokio::test]
715 async fn h264_roundtrip_publishes_avc3() {
716 let (name, config) = roundtrip_rendition(Codec::H264, encoder::Kind::Software).await;
719 assert!(name.ends_with(".avc3"));
720 assert_eq!(config.coded_width, Some(320));
721 assert_eq!(config.coded_height, Some(240));
722 }
723
724 #[cfg(target_os = "macos")]
727 #[tokio::test]
728 async fn h265_roundtrip_publishes_hev1() {
729 let (name, config) = roundtrip_rendition(Codec::H265, encoder::Kind::Hardware).await;
730 assert!(name.ends_with(".hev1"));
731 assert_eq!(config.coded_width, Some(320));
732 assert_eq!(config.coded_height, Some(240));
733 }
734}