1pub mod av1;
12pub mod h264;
13pub mod h265;
14pub mod opus;
15pub mod vp8;
16pub mod vp9;
17
18use bytes::Bytes;
19use hang::catalog::VideoConfig;
20use str0m::format::Codec;
21
22use crate::Result;
23
24#[derive(Clone, Debug)]
30pub struct Frame {
31 pub timestamp_us: u64,
32 pub payload: Bytes,
33}
34
35pub trait Bridge: Send {
43 fn push(&mut self, frame: Frame) -> Result<()>;
44}
45
46#[derive(Clone, Debug)]
52pub struct PacketizedFrame {
53 pub timestamp_us: u64,
54 pub payload: Bytes,
55}
56
57pub struct Track {
64 consumer: moq_mux::container::Consumer<moq_mux::catalog::hang::Container>,
65 codec: Codec,
66 convert: TrackConvert,
67}
68
69enum TrackConvert {
71 Passthrough,
75 LengthPrefixed { length_size: usize, keyframe_prefix: Bytes },
81}
82
83impl Track {
84 pub async fn opus(broadcast: &moq_net::BroadcastConsumer, name: &str) -> Result<Self> {
86 let container = moq_mux::catalog::hang::Container::Legacy;
87 let track = broadcast.subscribe_track(&moq_net::Track::new(name))?;
91 let consumer = moq_mux::container::Consumer::new(track, container);
92 Ok(Self {
93 consumer,
94 codec: Codec::Opus,
95 convert: TrackConvert::Passthrough,
96 })
97 }
98
99 pub async fn video(broadcast: &moq_net::BroadcastConsumer, name: &str, config: &VideoConfig) -> Result<Self> {
103 let container: moq_mux::catalog::hang::Container = (&config.container).try_into()?;
104 let track = broadcast.subscribe_track(&moq_net::Track::new(name))?;
107 let consumer = moq_mux::container::Consumer::new(track, container);
108
109 let (codec, convert) = match &config.codec {
110 hang::catalog::VideoCodec::VP8 => (Codec::Vp8, TrackConvert::Passthrough),
111 hang::catalog::VideoCodec::VP9(_) => (Codec::Vp9, TrackConvert::Passthrough),
112 hang::catalog::VideoCodec::AV1(_) => (Codec::Av1, TrackConvert::Passthrough),
113 hang::catalog::VideoCodec::H264(_) => (Codec::H264, h264_convert(config)?),
114 hang::catalog::VideoCodec::H265(_) => (Codec::H265, h265_convert(config)?),
115 other => return Err(crate::Error::UnsupportedCodec(format!("{other:?}"))),
116 };
117
118 Ok(Self {
119 consumer,
120 codec,
121 convert,
122 })
123 }
124
125 pub fn codec(&self) -> Codec {
126 self.codec
127 }
128
129 pub async fn next(&mut self) -> Result<Option<PacketizedFrame>> {
131 loop {
132 let Some(frame) = self.consumer.read().await? else {
133 return Ok(None);
134 };
135 let payload = match &self.convert {
136 TrackConvert::Passthrough => frame.payload,
137 TrackConvert::LengthPrefixed {
138 length_size,
139 keyframe_prefix,
140 } => {
141 let prefix = frame.keyframe.then(|| keyframe_prefix.as_ref());
142 moq_mux::codec::annexb::from_length_prefixed(&frame.payload, *length_size, prefix)
143 .map_err(|err| crate::Error::Other(anyhow::anyhow!("annexb: {err}")))?
144 }
145 };
146 if payload.is_empty() {
147 continue;
148 }
149 return Ok(Some(PacketizedFrame {
150 timestamp_us: frame.timestamp.as_micros() as u64,
151 payload,
152 }));
153 }
154 }
155}
156
157fn h264_convert(config: &VideoConfig) -> Result<TrackConvert> {
163 let Some(avcc) = config.description.as_ref().filter(|d| !d.is_empty()) else {
164 return Ok(TrackConvert::Passthrough);
165 };
166 let params = moq_mux::codec::h264::Avcc::parse(avcc)
167 .map_err(|err| crate::Error::Other(anyhow::anyhow!("avcc parse: {err}")))?;
168 if params.sps.is_empty() || params.pps.is_empty() {
172 return Err(crate::Error::Other(anyhow::anyhow!(
173 "avc1 avcC is missing parameter sets (sps={}, pps={})",
174 params.sps.len(),
175 params.pps.len()
176 )));
177 }
178 let keyframe_prefix = moq_mux::codec::annexb::build_prefix(params.sps.iter().chain(params.pps.iter()));
179 Ok(TrackConvert::LengthPrefixed {
180 length_size: params.length_size,
181 keyframe_prefix,
182 })
183}
184
185fn h265_convert(config: &VideoConfig) -> Result<TrackConvert> {
191 let Some(hvcc) = config.description.as_ref().filter(|d| !d.is_empty()) else {
192 return Ok(TrackConvert::Passthrough);
193 };
194 let params = moq_mux::codec::h265::Hvcc::parse(hvcc)
195 .map_err(|err| crate::Error::Other(anyhow::anyhow!("hvcc parse: {err}")))?;
196 if params.vps.is_empty() || params.sps.is_empty() || params.pps.is_empty() {
199 return Err(crate::Error::Other(anyhow::anyhow!(
200 "hvc1 hvcC is missing parameter sets (vps={}, sps={}, pps={})",
201 params.vps.len(),
202 params.sps.len(),
203 params.pps.len()
204 )));
205 }
206 let keyframe_prefix =
207 moq_mux::codec::annexb::build_prefix(params.vps.iter().chain(params.sps.iter()).chain(params.pps.iter()));
208 Ok(TrackConvert::LengthPrefixed {
209 length_size: params.length_size,
210 keyframe_prefix,
211 })
212}
213
214#[cfg(test)]
215mod tests {
216 use hang::catalog::{H264, H265, VideoConfig};
217
218 use super::*;
219
220 fn config(codec: impl Into<hang::catalog::VideoCodec>, description: Option<Bytes>) -> VideoConfig {
221 let mut config = VideoConfig::new(codec);
222 config.description = description;
223 config
224 }
225
226 fn h264(inline: bool) -> H264 {
227 H264 {
228 inline,
229 profile: 0x42,
230 constraints: 0,
231 level: 0x1f,
232 }
233 }
234
235 fn h265(in_band: bool) -> H265 {
236 H265 {
237 in_band,
238 profile_space: 0,
239 profile_idc: 1,
240 profile_compatibility_flags: [0; 4],
241 tier_flag: false,
242 level_idc: 0x5d,
243 constraint_flags: [0; 6],
244 }
245 }
246
247 fn build_avcc(sps: &[u8], pps: &[u8]) -> Bytes {
249 let mut v = vec![1, sps[1], sps[2], sps[3], 0xff, 0xe1];
250 v.extend_from_slice(&(sps.len() as u16).to_be_bytes());
251 v.extend_from_slice(sps);
252 v.push(1);
253 v.extend_from_slice(&(pps.len() as u16).to_be_bytes());
254 v.extend_from_slice(pps);
255 Bytes::from(v)
256 }
257
258 fn build_hvcc(vps: &[u8], sps: &[u8], pps: &[u8]) -> Bytes {
261 let mut v = vec![0u8; 21];
262 v.push(0xff); v.push(3); for (nal_type, nal) in [(32u8, vps), (33, sps), (34, pps)] {
265 v.push(nal_type); v.extend_from_slice(&1u16.to_be_bytes()); v.extend_from_slice(&(nal.len() as u16).to_be_bytes());
268 v.extend_from_slice(nal);
269 }
270 Bytes::from(v)
271 }
272
273 #[test]
274 fn h264_avc3_passthrough() {
275 let cfg = config(h264(true), None);
276 assert!(matches!(h264_convert(&cfg).unwrap(), TrackConvert::Passthrough));
277 }
278
279 #[test]
280 fn h264_avc1_length_prefixed() {
281 let sps: &[u8] = &[0x67, 0x42, 0xc0, 0x1f, 0xde];
282 let pps: &[u8] = &[0x68, 0xce, 0x3c, 0x80];
283 let cfg = config(h264(false), Some(build_avcc(sps, pps)));
284
285 let TrackConvert::LengthPrefixed {
286 length_size,
287 keyframe_prefix,
288 } = h264_convert(&cfg).unwrap()
289 else {
290 panic!("expected LengthPrefixed");
291 };
292 assert_eq!(length_size, 4);
293 assert!(keyframe_prefix.starts_with(&[0, 0, 0, 1]), "Annex-B start code");
294 assert!(keyframe_prefix.windows(sps.len()).any(|w| w == sps), "SPS in prefix");
295 assert!(keyframe_prefix.windows(pps.len()).any(|w| w == pps), "PPS in prefix");
296 }
297
298 #[test]
299 fn h265_hev1_passthrough() {
300 let cfg = config(h265(true), None);
301 assert!(matches!(h265_convert(&cfg).unwrap(), TrackConvert::Passthrough));
302 }
303
304 #[test]
305 fn h265_hvc1_length_prefixed() {
306 let vps: &[u8] = &[0x40, 0x01, 0x0c, 0x01];
307 let sps: &[u8] = &[0x42, 0x01, 0x01, 0x01];
308 let pps: &[u8] = &[0x44, 0x01, 0xc0, 0xf7];
309 let cfg = config(h265(false), Some(build_hvcc(vps, sps, pps)));
310
311 let TrackConvert::LengthPrefixed {
312 length_size,
313 keyframe_prefix,
314 } = h265_convert(&cfg).unwrap()
315 else {
316 panic!("expected LengthPrefixed");
317 };
318 assert_eq!(length_size, 4);
319 let v = keyframe_prefix.windows(vps.len()).position(|w| w == vps).expect("VPS");
321 let s = keyframe_prefix.windows(sps.len()).position(|w| w == sps).expect("SPS");
322 let p = keyframe_prefix.windows(pps.len()).position(|w| w == pps).expect("PPS");
323 assert!(v < s && s < p, "VPS < SPS < PPS order in prefix");
324 }
325
326 #[test]
329 fn h264_avc1_missing_param_sets_errors() {
330 let avcc = Bytes::from(vec![1, 0x42, 0, 0x1f, 0xff, 0xe0, 0x00]);
332 let cfg = config(h264(false), Some(avcc));
333 assert!(h264_convert(&cfg).is_err(), "missing SPS/PPS must error");
334 }
335}