cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

fix/architecture-review-findings
goq / src-tauri / src / media / moq_receiver.rs
moq_receiver.rs 26.8K
1use std::sync::atomic::Ordering;2use std::time::Duration;34use iroh::Endpoint;5use iroh_moq::{Moq, MoqSession};6#[cfg(test)]7use moq_net::Track;8use moq_net::{BroadcastConsumer, GroupConsumer, TrackConsumer};9#[cfg(test)]10use sigil_protocol::MOQ_VIDEO_H264_TRACK;11use sigil_protocol::{12 FrameFlags, KeyframeRequestReasonV3, MAX_MEDIA_GROUP_BYTES_V3,13 MAX_MEDIA_OBJECT_DELIVERY_TIMEOUT_MS, MAX_MEDIA_OBJECT_ID_V3, MediaCodec, MediaFrame,14 decode_media_frame_object, media_moq_broadcast_name,15};16use tauri::{AppHandle, Manager};1718use crate::commands::state::AppState;19use crate::media::audio_delivery::cancel_audio_generation;20use crate::media::frame_channel::{take_generation_owned, take_generation_owned_triple};21use crate::media::moq_catalog::subscribe_goq_video_track;22#[cfg(test)]23use crate::media::object_receiver::media_object_frame;24use crate::media::transport::CLIENT_ENDPOINT_CLOSE_TIMEOUT;2526const CLIENT_MOQ_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);27const CLIENT_MOQ_SUBSCRIBE_TIMEOUT: Duration = Duration::from_secs(10);28// Match the protocol's maximum single-object delivery horizon. A publisher may29// never hold a partially delivered object open indefinitely.30const CLIENT_MOQ_OBJECT_READ_TIMEOUT: Duration =31 Duration::from_millis(MAX_MEDIA_OBJECT_DELIVERY_TIMEOUT_MS as u64);32// Sigil's external encoder can take 500 ms to reach its next configured IDR.33// Allow another 500 ms for a relay path to deliver the superseding group.34const CLIENT_MOQ_GROUP_RECOVERY_TIMEOUT: Duration = Duration::from_secs(1);3536pub(crate) async fn retire_upstream_moq_generation(37 app: &AppHandle,38 media_generation: u64,39 audio_generation: Option<u64>,40) -> bool {41 let state = app.state::<AppState>();42 let endpoint = {43 // Selection and retirement are one generation-checked transaction.44 // A stale reader must never close a replacement session whose task was45 // installed after an explicit disconnect/reconnect.46 let _connection_serial = state.client_connection_serial.lock().await;47 let media_connection = {48 let mut slot = state.media_connection.lock().await;49 take_generation_owned(&mut slot, media_generation)50 };51 let Some(media_connection) = media_connection else {52 return false;53 };5455 {56 let mut control = state.media_control.lock().await;57 let _ = take_generation_owned(&mut control, media_generation);58 }59 {60 let mut delivery = state.frame_delivery.lock().await;61 let _ = take_generation_owned(&mut delivery, media_generation);62 }63 let feedback_connection = {64 let mut feedback = state.media_feedback.lock().await;65 take_generation_owned_triple(&mut feedback, media_generation)66 .map(|(connection, _)| connection)67 };68 *state.input_send.lock().await = None;6970 let audio_connection = if let Some(audio_generation) = audio_generation {71 if let Err(error) = cancel_audio_generation(72 &state.audio_connection_generation,73 &state.audio_deliveries,74 audio_generation,75 ) {76 eprintln!(77 "[client] failed to retire audio generation after upstream MoQ ended: {error}"78 );79 }80 let mut slot = state.audio_connection.lock().await;81 take_generation_owned(&mut slot, audio_generation)82 } else {83 None84 };8586 let endpoint = state.client_endpoint.lock().await.take();87 media_connection.close(0_u32.into(), b"upstream MoQ media ended");88 if let Some(feedback_connection) = feedback_connection {89 feedback_connection.close(0_u32.into(), b"upstream MoQ media ended");90 }91 if let Some(audio_connection) = audio_connection {92 audio_connection.close(0_u32.into(), b"upstream MoQ media ended");93 }94 state95 .client_connection_active96 .store(false, Ordering::SeqCst);97 endpoint98 };99100 if let Some(endpoint) = endpoint101 && tokio::time::timeout(CLIENT_ENDPOINT_CLOSE_TIMEOUT, endpoint.close())102 .await103 .is_err()104 {105 eprintln!("[client] timed out retiring endpoint after upstream MoQ media ended");106 }107 true108}109110#[derive(Debug)]111pub(crate) enum MoqMediaReadOutcome {112 Frame {113 frame: MediaFrame,114 discontinuity: bool,115 },116 Dropped {117 reason: KeyframeRequestReasonV3,118 },119 Malformed(String),120}121122#[derive(Clone, Copy, Debug, PartialEq, Eq)]123enum MoqGroupRecovery {124 /// Sigil deliberately cancels the old GOP when a configured IDR starts a125 /// replacement. This is normal live-edge supersession, not evidence that126 /// another keyframe request is needed.127 ExpectedSupersession,128 RecoverableGap(KeyframeRequestReasonV3),129}130131struct MoqGroupCursor {132 group: GroupConsumer,133 sequence: u64,134 object_count: usize,135 object_bytes: usize,136 group_gap: bool,137 replacement_for_cancelled_group: bool,138}139140/// Own every upstream handle for as long as the Portal frame task is alive.141/// In particular, dropping `Moq` aborts its actor and dropping the consumers142/// cancels their subscriptions, so none of these may be scoped only to setup.143struct MoqMediaLifetime {144 _moq: Moq,145 _session: MoqSession,146 _broadcast: BroadcastConsumer,147}148149pub(crate) struct MoqMediaReceiver {150 _lifetime: Option<MoqMediaLifetime>,151 track: TrackConsumer,152 current_group: Option<MoqGroupCursor>,153 last_group_sequence: Option<u64>,154 last_frame_sequence: Option<u64>,155 waiting_for_keyframe: bool,156 pending_group_recovery: Option<MoqGroupRecovery>,157}158159impl MoqMediaReceiver {160 fn new(161 moq: Moq,162 session: MoqSession,163 broadcast: BroadcastConsumer,164 track: TrackConsumer,165 ) -> Self {166 Self {167 _lifetime: Some(MoqMediaLifetime {168 _moq: moq,169 _session: session,170 _broadcast: broadcast,171 }),172 track,173 current_group: None,174 last_group_sequence: None,175 last_frame_sequence: None,176 waiting_for_keyframe: true,177 pending_group_recovery: None,178 }179 }180181 pub(crate) fn last_frame_sequence(&self) -> Option<u64> {182 self.last_frame_sequence183 }184185 #[cfg(test)]186 fn for_test(track: TrackConsumer) -> Self {187 Self {188 _lifetime: None,189 track,190 current_group: None,191 last_group_sequence: None,192 last_frame_sequence: None,193 waiting_for_keyframe: true,194 pending_group_recovery: None,195 }196 }197198 pub(crate) async fn next(&mut self) -> Result<Option<MoqMediaReadOutcome>, String> {199 self.next_with_timeouts(200 CLIENT_MOQ_OBJECT_READ_TIMEOUT,201 CLIENT_MOQ_GROUP_RECOVERY_TIMEOUT,202 )203 .await204 }205206 async fn next_with_timeouts(207 &mut self,208 object_read_timeout: Duration,209 group_recovery_timeout: Duration,210 ) -> Result<Option<MoqMediaReadOutcome>, String> {211 loop {212 if self.current_group.is_none() {213 let replacement_for_cancelled_group = self.pending_group_recovery.is_some();214 let group = if replacement_for_cancelled_group {215 match tokio::time::timeout(216 group_recovery_timeout,217 self.track.next_group(),218 )219 .await220 {221 Ok(group) => group,222 Err(_) => {223 let recovery = self224 .pending_group_recovery225 .take()226 .expect("pending MoQ recovery reason was present");227 return match recovery {228 MoqGroupRecovery::ExpectedSupersession => Err(format!(229 "Timed out after {} ms waiting for the MoQ group that supersedes an expected GOP cancellation",230 group_recovery_timeout.as_millis()231 )),232 MoqGroupRecovery::RecoverableGap(reason) => {233 Ok(Some(MoqMediaReadOutcome::Dropped { reason }))234 }235 };236 }237 }238 } else {239 self.track.next_group().await240 }241 .map_err(|error| format!("Upstream MoQ video track failed: {error}"))?;242 let Some(group) = group else {243 return Ok(None);244 };245 self.pending_group_recovery = None;246 let sequence = group.sequence;247 let group_gap =248 match classify_moq_group_sequence(self.last_group_sequence, sequence) {249 Ok(group_gap) => group_gap,250 Err(error) => return Ok(Some(MoqMediaReadOutcome::Malformed(error))),251 };252 if group_gap {253 self.waiting_for_keyframe = true;254 }255 self.current_group = Some(MoqGroupCursor {256 group,257 sequence,258 object_count: 0,259 object_bytes: 0,260 group_gap,261 replacement_for_cancelled_group,262 });263 }264265 let cursor = self266 .current_group267 .as_mut()268 .expect("MoQ group cursor was initialized");269 let object =270 match tokio::time::timeout(object_read_timeout, cursor.group.read_frame()).await {271 Err(_) => {272 let sequence = cursor.sequence;273 self.last_group_sequence = Some(sequence);274 self.current_group = None;275 self.waiting_for_keyframe = true;276 return Ok(Some(MoqMediaReadOutcome::Dropped {277 reason: KeyframeRequestReasonV3::DeliveryTimeout,278 }));279 }280 Ok(Ok(object)) => object,281 Ok(Err(error)) if moq_group_error_is_recoverable(&error) => {282 let sequence = cursor.sequence;283 self.last_group_sequence = Some(sequence);284 self.current_group = None;285 self.waiting_for_keyframe = true;286 let reason = moq_group_error_reason(&error);287 self.pending_group_recovery =288 Some(if matches!(error, moq_net::Error::Cancel) {289 MoqGroupRecovery::ExpectedSupersession290 } else {291 MoqGroupRecovery::RecoverableGap(reason)292 });293 continue;294 }295 Ok(Err(error)) => {296 return Ok(Some(MoqMediaReadOutcome::Malformed(format!(297 "Upstream MoQ group {} failed: {error}",298 cursor.sequence299 ))));300 }301 };302 let Some(object) = object else {303 if cursor.object_count == 0 {304 if cursor.replacement_for_cancelled_group {305 self.last_group_sequence = Some(cursor.sequence);306 self.current_group = None;307 self.waiting_for_keyframe = true;308 return Ok(Some(MoqMediaReadOutcome::Dropped {309 reason: KeyframeRequestReasonV3::TransportGap,310 }));311 }312 return Ok(Some(MoqMediaReadOutcome::Malformed(format!(313 "Upstream MoQ group {} was empty",314 cursor.sequence315 ))));316 }317 self.last_group_sequence = Some(cursor.sequence);318 self.current_group = None;319 continue;320 };321322 let next_group_bytes = match validate_moq_object_bounds(323 cursor.sequence,324 cursor.object_count,325 cursor.object_bytes,326 object.len(),327 ) {328 Ok(next_group_bytes) => next_group_bytes,329 Err(error) => return Ok(Some(MoqMediaReadOutcome::Malformed(error))),330 };331 let frame = match decode_media_frame_object(&object) {332 Ok(frame) => frame,333 Err(error) => {334 return Ok(Some(MoqMediaReadOutcome::Malformed(format!(335 "Invalid upstream MoQ media object in group {} object {}: {error}",336 cursor.sequence, cursor.object_count337 ))));338 }339 };340341 let first_object = cursor.object_count == 0;342 let frame_contiguous = match validate_moq_group_frame(343 cursor.sequence,344 first_object,345 self.last_frame_sequence,346 &frame,347 ) {348 Ok(frame_contiguous) => frame_contiguous,349 Err(_error) if first_object && cursor.replacement_for_cancelled_group => {350 self.last_group_sequence = Some(cursor.sequence);351 self.current_group = None;352 self.waiting_for_keyframe = true;353 return Ok(Some(MoqMediaReadOutcome::Dropped {354 reason: KeyframeRequestReasonV3::TransportGap,355 }));356 }357 Err(_error) if !first_object => {358 self.last_group_sequence = Some(cursor.sequence);359 self.current_group = None;360 self.waiting_for_keyframe = true;361 return Ok(Some(MoqMediaReadOutcome::Dropped {362 reason: KeyframeRequestReasonV3::TransportGap,363 }));364 }365 Err(error) => return Ok(Some(MoqMediaReadOutcome::Malformed(error))),366 };367 let discontinuity = self.waiting_for_keyframe368 || (first_object && cursor.group_gap)369 || (first_object && !frame_contiguous)370 || frame.header.flags.contains(FrameFlags::DISCONTINUITY);371 cursor.object_count += 1;372 cursor.object_bytes = next_group_bytes;373 self.last_frame_sequence = Some(frame.header.sequence);374 self.waiting_for_keyframe = false;375 return Ok(Some(MoqMediaReadOutcome::Frame {376 frame,377 discontinuity,378 }));379 }380 }381}382383fn classify_moq_group_sequence(previous: Option<u64>, current: u64) -> Result<bool, String> {384 let Some(previous) = previous else {385 return Ok(false);386 };387 if current <= previous {388 return Err(format!(389 "Upstream MoQ group sequence did not increase: previous={previous}, current={current}"390 ));391 }392 Ok(previous.checked_add(1) != Some(current))393}394395fn validate_moq_object_bounds(396 group_sequence: u64,397 object_count: usize,398 object_bytes: usize,399 object_len: usize,400) -> Result<usize, String> {401 let max_objects = MAX_MEDIA_OBJECT_ID_V3 as usize + 1;402 if object_count >= max_objects {403 return Err(format!(404 "Upstream MoQ group {group_sequence} exceeded {max_objects} media objects"405 ));406 }407 let next_group_bytes = object_bytes408 .checked_add(object_len)409 .ok_or_else(|| "Upstream MoQ group byte count overflowed".to_string())?;410 if next_group_bytes > MAX_MEDIA_GROUP_BYTES_V3 {411 return Err(format!(412 "Upstream MoQ group {group_sequence} exceeded the {MAX_MEDIA_GROUP_BYTES_V3} byte limit"413 ));414 }415 Ok(next_group_bytes)416}417418fn validate_moq_group_frame(419 group_sequence: u64,420 first_object: bool,421 last_frame_sequence: Option<u64>,422 frame: &MediaFrame,423) -> Result<bool, String> {424 if first_object425 && !(frame.header.codec == MediaCodec::H264426 && frame.header.flags.contains(FrameFlags::KEYFRAME)427 && frame.header.flags.contains(FrameFlags::CODEC_CONFIG))428 {429 return Err(format!(430 "Upstream MoQ group {group_sequence} did not begin with a configured H.264 keyframe"431 ));432 }433 let contiguous = last_frame_sequence434 .is_none_or(|previous| previous.checked_add(1) == Some(frame.header.sequence));435 if !first_object && !contiguous {436 return Err(format!(437 "Upstream MoQ group {group_sequence} contains a non-contiguous access-unit sequence"438 ));439 }440 Ok(contiguous)441}442443fn moq_group_error_is_recoverable(error: &moq_net::Error) -> bool {444 matches!(445 error,446 moq_net::Error::Cancel447 | moq_net::Error::Old448 | moq_net::Error::Timeout449 | moq_net::Error::Dropped450 | moq_net::Error::CacheFull451 | moq_net::Error::Remote(0 | 2 | 3 | 24 | 26)452 )453}454455fn moq_group_error_reason(error: &moq_net::Error) -> KeyframeRequestReasonV3 {456 if matches!(error, moq_net::Error::Timeout | moq_net::Error::Remote(3)) {457 KeyframeRequestReasonV3::DeliveryTimeout458 } else {459 KeyframeRequestReasonV3::TransportGap460 }461}462463pub(crate) async fn open_upstream_moq_media(464 endpoint: &Endpoint,465 address: &iroh::EndpointAddr,466 session_id: u64,467) -> Result<(MoqMediaReceiver, iroh::endpoint::Connection), String> {468 let broadcast_name = media_moq_broadcast_name(session_id)469 .map_err(|error| format!("Invalid MoQ media session name: {error}"))?;470 let moq = Moq::new(endpoint.clone());471 let mut session =472 tokio::time::timeout(CLIENT_MOQ_CONNECT_TIMEOUT, moq.connect(address.clone()))473 .await474 .map_err(|_| "Timed out connecting upstream MoQ media session".to_string())?475 .map_err(|error| format!("Failed to connect upstream MoQ media session: {error:#}"))?;476 if session.remote_id() != address.id {477 session.close(1, b"remote identity mismatch");478 return Err(format!(479 "Upstream MoQ connected to unexpected peer {}; expected {}",480 session.remote_id(),481 address.id482 ));483 }484 let diagnostics_connection = session.conn().clone();485 let broadcast = tokio::time::timeout(486 CLIENT_MOQ_SUBSCRIBE_TIMEOUT,487 session.subscribe(&broadcast_name),488 )489 .await490 .map_err(|_| format!("Timed out waiting for upstream MoQ broadcast {broadcast_name}"))?491 .map_err(|error| {492 format!("Failed to subscribe to upstream MoQ broadcast {broadcast_name}: {error}")493 })?;494 let catalog = subscribe_goq_video_track(&broadcast, CLIENT_MOQ_SUBSCRIBE_TIMEOUT).await?;495 eprintln!("[client] moq catalog: {}", catalog.mode.label());496 Ok((497 MoqMediaReceiver::new(moq, session, broadcast, catalog.track),498 diagnostics_connection,499 ))500}501502#[cfg(test)]503mod tests {504 use super::*;505506 #[test]507 fn upstream_moq_group_ids_detect_only_real_transport_gaps() {508 assert!(!classify_moq_group_sequence(None, 41).unwrap());509 assert!(!classify_moq_group_sequence(Some(41), 42).unwrap());510 assert!(classify_moq_group_sequence(Some(42), 44).unwrap());511 assert!(classify_moq_group_sequence(Some(44), 44).is_err());512 assert!(classify_moq_group_sequence(Some(44), 43).is_err());513 }514515 #[test]516 fn upstream_moq_group_gap_keyframe_is_the_recovery_barrier() {517 let keyframe = media_object_frame(80, FrameFlags::KEYFRAME.union(FrameFlags::CODEC_CONFIG));518 let contiguous = validate_moq_group_frame(9, true, Some(42), &keyframe).unwrap();519 assert!(!contiguous);520521 // A native group-id gap puts the decoder into resync, but this exact522 // configured frame 0 exits it immediately. There is deliberately no523 // keyframe-request action on MoqMediaReadOutcome::Frame: requesting a524 // replacement here would recreate the grouped-v3 feedback loop.525 let group_gap = classify_moq_group_sequence(Some(7), 9).unwrap();526 let discontinuity = group_gap || !contiguous;527 assert!(discontinuity);528 }529530 #[tokio::test]531 async fn upstream_moq_idr_abort_to_next_frame_zero_has_no_feedback_loop() {532 let mut producer = Track::new(MOQ_VIDEO_H264_TRACK).produce();533 let mut receiver = MoqMediaReceiver::for_test(producer.consume());534535 let mut prior_group = producer.append_group().unwrap();536 let prior_keyframe =537 media_object_frame(40, FrameFlags::KEYFRAME.union(FrameFlags::CODEC_CONFIG));538 prior_group539 .write_frame(sigil_protocol::encode_media_frame_object(&prior_keyframe).unwrap())540 .unwrap();541 assert!(matches!(542 receiver.next().await.unwrap(),543 Some(MoqMediaReadOutcome::Frame { .. })544 ));545546 prior_group.abort(moq_net::Error::Cancel).unwrap();547 let mut replacement = producer.append_group().unwrap();548 let replacement_keyframe =549 media_object_frame(80, FrameFlags::KEYFRAME.union(FrameFlags::CODEC_CONFIG));550 replacement551 .write_frame(sigil_protocol::encode_media_frame_object(&replacement_keyframe).unwrap())552 .unwrap();553 replacement.finish().unwrap();554555 // `next` consumes the expected Cancel internally and returns the556 // replacement frame directly. A Dropped outcome here would enqueue a557 // needless keyframe request and recreate the recovery feedback loop.558 assert!(matches!(559 receiver.next().await.unwrap(),560 Some(MoqMediaReadOutcome::Frame {561 discontinuity: true,562 ..563 })564 ));565 }566567 #[tokio::test]568 async fn upstream_moq_expected_cancel_without_replacement_is_terminal_without_request() {569 let mut producer = Track::new(MOQ_VIDEO_H264_TRACK).produce();570 let mut receiver = MoqMediaReceiver::for_test(producer.consume());571 let mut group = producer.append_group().unwrap();572 let keyframe = media_object_frame(1, FrameFlags::KEYFRAME.union(FrameFlags::CODEC_CONFIG));573 group574 .write_frame(sigil_protocol::encode_media_frame_object(&keyframe).unwrap())575 .unwrap();576 assert!(matches!(577 receiver.next().await.unwrap(),578 Some(MoqMediaReadOutcome::Frame { .. })579 ));580 group.abort(moq_net::Error::Cancel).unwrap();581582 let error = receiver583 .next_with_timeouts(Duration::from_millis(25), Duration::from_millis(25))584 .await585 .unwrap_err();586 assert!(error.contains("expected GOP cancellation"));587 }588589 #[tokio::test]590 async fn upstream_moq_partial_object_read_has_protocol_bounded_deadline() {591 let mut producer = Track::new(MOQ_VIDEO_H264_TRACK).produce();592 let mut receiver = MoqMediaReceiver::for_test(producer.consume());593 let _stalled_group = producer.append_group().unwrap();594595 assert!(matches!(596 receiver597 .next_with_timeouts(Duration::from_millis(25), Duration::from_millis(25))598 .await599 .unwrap(),600 Some(MoqMediaReadOutcome::Dropped {601 reason: KeyframeRequestReasonV3::DeliveryTimeout602 })603 ));604 assert_eq!(605 CLIENT_MOQ_OBJECT_READ_TIMEOUT,606 Duration::from_millis(MAX_MEDIA_OBJECT_DELIVERY_TIMEOUT_MS as u64)607 );608 assert!(CLIENT_MOQ_GROUP_RECOVERY_TIMEOUT >= Duration::from_millis(1_000));609 }610611 #[tokio::test]612 async fn upstream_moq_invalid_cancel_replacement_requests_recovery() {613 let mut producer = Track::new(MOQ_VIDEO_H264_TRACK).produce();614 let mut receiver = MoqMediaReceiver::for_test(producer.consume());615 let mut prior = producer.append_group().unwrap();616 let keyframe = media_object_frame(10, FrameFlags::KEYFRAME.union(FrameFlags::CODEC_CONFIG));617 prior618 .write_frame(sigil_protocol::encode_media_frame_object(&keyframe).unwrap())619 .unwrap();620 assert!(matches!(621 receiver.next().await.unwrap(),622 Some(MoqMediaReadOutcome::Frame { .. })623 ));624 prior.abort(moq_net::Error::Cancel).unwrap();625626 let mut invalid_replacement = producer.append_group().unwrap();627 let delta = media_object_frame(11, FrameFlags::NONE);628 invalid_replacement629 .write_frame(sigil_protocol::encode_media_frame_object(&delta).unwrap())630 .unwrap();631 invalid_replacement.finish().unwrap();632 assert!(matches!(633 receiver.next().await.unwrap(),634 Some(MoqMediaReadOutcome::Dropped {635 reason: KeyframeRequestReasonV3::TransportGap636 })637 ));638 }639640 #[test]641 fn upstream_moq_requires_configured_keyframe_zero_and_contiguous_deltas() {642 let delta = media_object_frame(10, FrameFlags::NONE);643 assert!(validate_moq_group_frame(3, true, None, &delta).is_err());644645 let unconfigured_keyframe = media_object_frame(10, FrameFlags::KEYFRAME);646 assert!(validate_moq_group_frame(3, true, None, &unconfigured_keyframe).is_err());647648 let configured =649 media_object_frame(10, FrameFlags::KEYFRAME.union(FrameFlags::CODEC_CONFIG));650 assert!(validate_moq_group_frame(3, true, None, &configured).is_ok());651 assert!(validate_moq_group_frame(3, false, Some(8), &delta).is_err());652 assert!(validate_moq_group_frame(3, false, Some(9), &delta).is_ok());653 }654655 #[test]656 fn upstream_moq_bounds_objects_and_group_bytes_before_growth() {657 assert_eq!(validate_moq_object_bounds(1, 0, 0, 40).unwrap(), 40);658 assert!(validate_moq_object_bounds(1, MAX_MEDIA_OBJECT_ID_V3 as usize + 1, 0, 1,).is_err());659 assert!(validate_moq_object_bounds(1, 0, MAX_MEDIA_GROUP_BYTES_V3, 1).is_err());660 assert!(validate_moq_object_bounds(1, 0, usize::MAX, 1).is_err());661 }662663 #[test]664 fn upstream_moq_cancellation_is_resync_but_protocol_errors_are_terminal() {665 for recoverable in [666 moq_net::Error::Cancel,667 moq_net::Error::Old,668 moq_net::Error::Timeout,669 moq_net::Error::Dropped,670 moq_net::Error::CacheFull,671 moq_net::Error::Remote(3),672 ] {673 assert!(moq_group_error_is_recoverable(&recoverable));674 }675 assert!(!moq_group_error_is_recoverable(676 &moq_net::Error::ProtocolViolation677 ));678 assert!(!moq_group_error_is_recoverable(&moq_net::Error::WrongSize));679 assert_eq!(680 moq_group_error_reason(&moq_net::Error::Timeout),681 KeyframeRequestReasonV3::DeliveryTimeout682 );683 assert_eq!(684 moq_group_error_reason(&moq_net::Error::Cancel),685 KeyframeRequestReasonV3::TransportGap686 );687 }688}