cat / goq
clone

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

fix/docs-activation-truth
goq / src-tauri / src / media / frame_channel.rs
frame_channel.rs 16.5K
1use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};23use serde::Serialize;4use sigil_protocol::{FrameFlags, MAX_MEDIA_PAYLOAD_LEN, MAX_VIDEO_DIMENSION, MAX_VIDEO_PIXELS};5use tauri::{AppHandle, Emitter};67use crate::commands::state::AppState;89#[derive(Serialize, Clone)]10struct FrameErrorPayload {11 generation: u64,12 error: String,13}1415pub(crate) fn emit_frame_error(app: &AppHandle, generation: u64, error: impl Into<String>) {16 let _ = app.emit(17 "frame-error",18 FrameErrorPayload {19 generation,20 error: error.into(),21 },22 );23}2425// Absorb brief webview→Rust acknowledgment jitter without allowing Tauri IPC26// to grow without bound. Four 60 fps frames cap this handoff at about 67 ms;27// WebCodecs has a separate, stricter decode-queue bound in the frontend.28pub(crate) const CLIENT_FRAME_CHANNEL_CAPACITY: usize = 4;2930const FRAME_CHANNEL_MAGIC: [u8; 4] = *b"SGFR";31const FRAME_CHANNEL_VERSION: u8 = 1;32const FRAME_CHANNEL_HEADER_LEN: usize = 40;33// The channel flag byte uses the exact sigil-protocol wire bit assignments;34// deriving them here keeps the two layers incapable of drifting apart.35// portal/frame-envelope.mjs mirrors the same values.36const FRAME_CHANNEL_FLAG_KEYFRAME: u8 = FrameFlags::KEYFRAME.bits();37const FRAME_CHANNEL_FLAG_CODEC_CONFIG: u8 = FrameFlags::CODEC_CONFIG.bits();38const FRAME_CHANNEL_FLAG_DISCONTINUITY: u8 = FrameFlags::DISCONTINUITY.bits();39const FRAME_CHANNEL_OPTIONAL_U64_NONE: u64 = u64::MAX;40const FRAME_CHANNEL_OPTIONAL_I64_NONE: i64 = i64::MIN;4142pub(crate) fn close_generation_connection<T>(43 connection: Option<(u64, T)>,44 close: impl FnOnce(T),45) -> Option<u64> {46 connection.map(|(generation, connection)| {47 close(connection);48 generation49 })50}5152pub(crate) fn take_generation_owned<T>(53 slot: &mut Option<(u64, T)>,54 expected_generation: u64,55) -> Option<T> {56 if slot57 .as_ref()58 .is_some_and(|(generation, _)| *generation == expected_generation)59 {60 slot.take().map(|(_, value)| value)61 } else {62 None63 }64}6566pub(crate) fn take_generation_owned_triple<T, U>(67 slot: &mut Option<(u64, T, U)>,68 expected_generation: u64,69) -> Option<(T, U)> {70 if slot71 .as_ref()72 .is_some_and(|(generation, _, _)| *generation == expected_generation)73 {74 slot.take().map(|(_, value, companion)| (value, companion))75 } else {76 None77 }78}7980pub(crate) struct FrameEnvelopeMetadata<'a> {81 pub(crate) width: u32,82 pub(crate) height: u32,83 pub(crate) codec: &'a str,84 pub(crate) keyframe: bool,85 pub(crate) discontinuity: bool,86 pub(crate) codec_config: bool,87 pub(crate) sequence: Option<u64>,88 pub(crate) capture_timestamp_micros: Option<u64>,89 pub(crate) pts_micros: Option<i64>,90}9192pub(crate) fn encode_frame_envelope(93 metadata: FrameEnvelopeMetadata<'_>,94 payload: &[u8],95) -> Result<Vec<u8>, String> {96 validate_legacy_media_header(metadata.width, metadata.height, payload.len())?;97 if metadata.codec_config && !metadata.keyframe {98 return Err("Frame codec configuration requires a keyframe".to_string());99 }100 if metadata.sequence == Some(FRAME_CHANNEL_OPTIONAL_U64_NONE) {101 return Err("Frame sequence collides with the channel sentinel".to_string());102 }103 if metadata.capture_timestamp_micros == Some(FRAME_CHANNEL_OPTIONAL_U64_NONE) {104 return Err("Capture timestamp collides with the channel sentinel".to_string());105 }106 if metadata.pts_micros == Some(FRAME_CHANNEL_OPTIONAL_I64_NONE) {107 return Err("Frame PTS collides with the channel sentinel".to_string());108 }109 let width = u16::try_from(metadata.width).map_err(|_| {110 format!(111 "Frame width does not fit channel envelope: {}",112 metadata.width113 )114 })?;115 let height = u16::try_from(metadata.height).map_err(|_| {116 format!(117 "Frame height does not fit channel envelope: {}",118 metadata.height119 )120 })?;121 let payload_len = u32::try_from(payload.len()).map_err(|_| {122 format!(123 "Frame payload does not fit channel envelope: {}",124 payload.len()125 )126 })?;127 let codec = match metadata.codec {128 "h264" => 1,129 other => return Err(format!("Unsupported frame channel codec: {other}")),130 };131 let mut flags = 0_u8;132 if metadata.keyframe {133 flags |= FRAME_CHANNEL_FLAG_KEYFRAME;134 }135 if metadata.discontinuity {136 flags |= FRAME_CHANNEL_FLAG_DISCONTINUITY;137 }138 if metadata.codec_config {139 flags |= FRAME_CHANNEL_FLAG_CODEC_CONFIG;140 }141142 let mut envelope = Vec::with_capacity(FRAME_CHANNEL_HEADER_LEN + payload.len());143 envelope.extend_from_slice(&FRAME_CHANNEL_MAGIC);144 envelope.push(FRAME_CHANNEL_VERSION);145 envelope.push(codec);146 envelope.push(flags);147 envelope.push(0); // Reserved; the parser rejects non-zero values.148 envelope.extend_from_slice(&width.to_be_bytes());149 envelope.extend_from_slice(&height.to_be_bytes());150 envelope.extend_from_slice(&payload_len.to_be_bytes());151 envelope.extend_from_slice(152 &metadata153 .sequence154 .unwrap_or(FRAME_CHANNEL_OPTIONAL_U64_NONE)155 .to_be_bytes(),156 );157 envelope.extend_from_slice(158 &metadata159 .capture_timestamp_micros160 .unwrap_or(FRAME_CHANNEL_OPTIONAL_U64_NONE)161 .to_be_bytes(),162 );163 envelope.extend_from_slice(164 &metadata165 .pts_micros166 .unwrap_or(FRAME_CHANNEL_OPTIONAL_I64_NONE)167 .to_be_bytes(),168 );169 envelope.extend_from_slice(payload);170 debug_assert_eq!(envelope.len(), FRAME_CHANNEL_HEADER_LEN + payload.len());171 Ok(envelope)172}173174pub(crate) fn try_reserve_frame_channel_slot(in_flight: &AtomicUsize) -> bool {175 in_flight176 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| {177 (current < CLIENT_FRAME_CHANNEL_CAPACITY).then_some(current + 1)178 })179 .is_ok()180}181182pub(crate) fn release_frame_channel_slot(in_flight: &AtomicUsize) {183 let _ = in_flight.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| {184 current.checked_sub(1)185 });186}187188fn release_frame_channel_slot_for_generation(189 in_flight: &AtomicUsize,190 current_generation: u64,191 generation: u64,192) -> bool {193 if generation == 0 || current_generation != generation {194 return false;195 }196 release_frame_channel_slot(in_flight);197 true198}199200pub(crate) async fn acknowledge_frame_delivery(201 state: &AppState,202 generation: u64,203) -> Result<bool, String> {204 // Serialize selection of the generation-owned counter against connect and205 // disconnect. Each media task keeps its own counter, so an old callback can206 // never consume a permit reserved by a replacement session.207 let _connection_serial = state.client_connection_serial.lock().await;208 let delivery = state.frame_delivery.lock().await;209 let Some((current_generation, in_flight)) = delivery.as_ref() else {210 return Ok(false);211 };212 Ok(release_frame_channel_slot_for_generation(213 in_flight,214 *current_generation,215 generation,216 ))217}218219pub(crate) fn next_media_generation(counter: &AtomicU64) -> Result<u64, String> {220 counter221 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| {222 current.checked_add(1)223 })224 .map(|previous| previous + 1)225 .map_err(|_| "Media connection generation overflowed".to_string())226}227228pub(crate) fn validate_legacy_media_header(229 width: u32,230 height: u32,231 payload_len: usize,232) -> Result<(), String> {233 if width == 0234 || height == 0235 || width > u32::from(MAX_VIDEO_DIMENSION)236 || height > u32::from(MAX_VIDEO_DIMENSION)237 || width.saturating_mul(height) > MAX_VIDEO_PIXELS238 {239 return Err(format!("Invalid legacy media dimensions: {width}x{height}"));240 }241 if payload_len == 0 || payload_len > MAX_MEDIA_PAYLOAD_LEN {242 return Err(format!(243 "Invalid legacy media payload length: {payload_len}"244 ));245 }246 Ok(())247}248249#[cfg(test)]250mod tests {251 use super::*;252253 #[test]254 fn media_generations_are_nonzero_monotonic_and_checked_for_overflow() {255 let counter = AtomicU64::new(0);256 assert_eq!(next_media_generation(&counter).unwrap(), 1);257 assert_eq!(next_media_generation(&counter).unwrap(), 2);258259 let exhausted = AtomicU64::new(u64::MAX);260 assert!(next_media_generation(&exhausted).is_err());261 }262263 #[test]264 fn binary_frame_envelope_has_exact_stable_layout() {265 let payload = [0, 0, 0, 1, 0x65];266 let envelope = encode_frame_envelope(267 FrameEnvelopeMetadata {268 width: 1280,269 height: 800,270 codec: "h264",271 keyframe: true,272 discontinuity: true,273 codec_config: true,274 sequence: Some(42),275 capture_timestamp_micros: Some(123_456),276 pts_micros: Some(98_765),277 },278 &payload,279 )280 .unwrap();281282 assert_eq!(envelope.len(), FRAME_CHANNEL_HEADER_LEN + payload.len());283 assert_eq!(&envelope[0..4], b"SGFR");284 assert_eq!(envelope[4], 1);285 assert_eq!(envelope[5], 1);286 assert_eq!(envelope[6], 0b111);287 assert_eq!(envelope[7], 0);288 assert_eq!(&envelope[8..10], &1280_u16.to_be_bytes());289 assert_eq!(&envelope[10..12], &800_u16.to_be_bytes());290 assert_eq!(&envelope[12..16], &5_u32.to_be_bytes());291 assert_eq!(&envelope[16..24], &42_u64.to_be_bytes());292 assert_eq!(&envelope[24..32], &123_456_u64.to_be_bytes());293 assert_eq!(&envelope[32..40], &98_765_i64.to_be_bytes());294 assert_eq!(&envelope[40..], payload);295 }296297 #[test]298 fn channel_flag_bits_match_protocol_wire_assignments() {299 let envelope = encode_frame_envelope(300 FrameEnvelopeMetadata {301 width: 1,302 height: 1,303 codec: "h264",304 keyframe: false,305 discontinuity: true,306 codec_config: false,307 sequence: None,308 capture_timestamp_micros: None,309 pts_micros: None,310 },311 &[1],312 )313 .unwrap();314 assert_eq!(envelope[6], FrameFlags::DISCONTINUITY.bits());315 assert_eq!(envelope[6], 0b100);316 }317318 #[test]319 fn binary_frame_envelope_uses_explicit_optional_sentinels() {320 let envelope = encode_frame_envelope(321 FrameEnvelopeMetadata {322 width: 1,323 height: 1,324 codec: "h264",325 keyframe: false,326 discontinuity: false,327 codec_config: false,328 sequence: None,329 capture_timestamp_micros: None,330 pts_micros: None,331 },332 &[1],333 )334 .unwrap();335 assert_eq!(envelope[5], 1);336 assert_eq!(&envelope[16..24], &u64::MAX.to_be_bytes());337 assert_eq!(&envelope[24..32], &u64::MAX.to_be_bytes());338 assert_eq!(&envelope[32..40], &i64::MIN.to_be_bytes());339 }340341 #[test]342 fn binary_frame_envelope_rejects_invalid_metadata_before_sending() {343 let metadata = |codec| FrameEnvelopeMetadata {344 width: 1280,345 height: 800,346 codec,347 keyframe: false,348 discontinuity: false,349 codec_config: false,350 sequence: None,351 capture_timestamp_micros: None,352 pts_micros: None,353 };354 assert!(encode_frame_envelope(metadata("h265"), &[1]).is_err());355 assert!(encode_frame_envelope(metadata("av1"), &[1]).is_err());356 assert!(encode_frame_envelope(metadata("vp9"), &[1]).is_err());357 assert!(encode_frame_envelope(metadata("h264"), &[]).is_err());358 assert!(359 encode_frame_envelope(360 FrameEnvelopeMetadata {361 codec_config: true,362 ..metadata("h264")363 },364 &[1]365 )366 .is_err()367 );368 assert!(369 encode_frame_envelope(370 FrameEnvelopeMetadata {371 sequence: Some(u64::MAX),372 ..metadata("h264")373 },374 &[1]375 )376 .is_err()377 );378 assert!(379 encode_frame_envelope(380 FrameEnvelopeMetadata {381 pts_micros: Some(i64::MIN),382 ..metadata("h264")383 },384 &[1]385 )386 .is_err()387 );388 assert!(389 encode_frame_envelope(390 FrameEnvelopeMetadata {391 width: u32::from(MAX_VIDEO_DIMENSION) + 1,392 ..metadata("h264")393 },394 &[1]395 )396 .is_err()397 );398 }399400 #[test]401 fn frame_channel_slots_are_bounded_and_cannot_underflow() {402 let in_flight = AtomicUsize::new(0);403 release_frame_channel_slot(&in_flight);404 assert_eq!(in_flight.load(Ordering::SeqCst), 0);405406 for expected in 1..=CLIENT_FRAME_CHANNEL_CAPACITY {407 assert!(try_reserve_frame_channel_slot(&in_flight));408 assert_eq!(in_flight.load(Ordering::SeqCst), expected);409 }410 assert!(!try_reserve_frame_channel_slot(&in_flight));411 assert_eq!(412 in_flight.load(Ordering::SeqCst),413 CLIENT_FRAME_CHANNEL_CAPACITY414 );415416 for expected in (0..CLIENT_FRAME_CHANNEL_CAPACITY).rev() {417 release_frame_channel_slot(&in_flight);418 assert_eq!(in_flight.load(Ordering::SeqCst), expected);419 }420 release_frame_channel_slot(&in_flight);421 assert_eq!(in_flight.load(Ordering::SeqCst), 0);422 }423424 #[test]425 fn frame_acknowledgments_release_only_the_matching_generation() {426 let in_flight = AtomicUsize::new(2);427 let generation = 9;428429 assert!(!release_frame_channel_slot_for_generation(430 &in_flight, generation, 8431 ));432 assert!(!release_frame_channel_slot_for_generation(433 &in_flight, generation, 0434 ));435 assert_eq!(in_flight.load(Ordering::SeqCst), 2);436 assert!(release_frame_channel_slot_for_generation(437 &in_flight, generation, 9438 ));439 assert_eq!(in_flight.load(Ordering::SeqCst), 1);440 }441442 #[test]443 fn global_frame_events_serialize_their_media_generation() {444 let error = serde_json::to_value(FrameErrorPayload {445 generation: 17,446 error: "closed".to_string(),447 })448 .unwrap();449450 assert_eq!(error["generation"], 17);451 assert_eq!(error["error"], "closed");452 }453454 #[test]455 fn generation_connection_close_retires_exactly_one_owned_handle() {456 let closes = AtomicUsize::new(0);457 let retired = close_generation_connection(Some((7, "media")), |connection| {458 assert_eq!(connection, "media");459 closes.fetch_add(1, Ordering::SeqCst);460 });461 assert_eq!(retired, Some(7));462 assert_eq!(closes.load(Ordering::SeqCst), 1);463464 let absent: Option<(u64, &str)> = None;465 assert_eq!(466 close_generation_connection(absent, |_| {467 closes.fetch_add(1, Ordering::SeqCst);468 }),469 None470 );471 assert_eq!(closes.load(Ordering::SeqCst), 1);472 }473474 #[test]475 fn generation_owned_retirement_cannot_take_a_replacement_session() {476 let mut slot = Some((12, "replacement"));477 assert_eq!(take_generation_owned(&mut slot, 11), None);478 assert_eq!(slot, Some((12, "replacement")));479 assert_eq!(take_generation_owned(&mut slot, 12), Some("replacement"));480 assert_eq!(slot, None);481482 let mut feedback = Some((22, "replacement connection", "replacement sender"));483 assert_eq!(take_generation_owned_triple(&mut feedback, 21), None);484 assert_eq!(485 feedback,486 Some((22, "replacement connection", "replacement sender"))487 );488 assert_eq!(489 take_generation_owned_triple(&mut feedback, 22),490 Some(("replacement connection", "replacement sender"))491 );492 assert_eq!(feedback, None);493 }494495 #[test]496 fn legacy_media_header_is_bounded_before_allocation() {497 assert!(validate_legacy_media_header(1280, 800, 1024).is_ok());498 assert!(validate_legacy_media_header(0, 800, 1024).is_err());499 assert!(validate_legacy_media_header(1280, 800, 0).is_err());500 assert!(validate_legacy_media_header(1280, 800, MAX_MEDIA_PAYLOAD_LEN + 1).is_err());501 assert!(validate_legacy_media_header(u32::MAX, u32::MAX, 1024).is_err());502 }503}