diff --git a/media/patch-uxplay.py b/media/patch-uxplay.py index 56183f1..c23a57a 100644 --- a/media/patch-uxplay.py +++ b/media/patch-uxplay.py @@ -35,3 +35,30 @@ if 'framely_client_device' not in s: } '''+marker) p.write_text(s) + +# A GStreamer sink can survive successive audio sessions. Announce each valid +# media SETUP independently of its first decoded buffer. +s=p.read_text() +if 'framely_media_request' not in s: + marker=' unsigned char type;\n LOGI("ct=%d' + assert marker in s + s=s.replace(marker,''' // framely_media_request: a new SETUP needs fresh consent after teardown. + const char* directory=getenv("FRAMELY_CAST_DIR"),*id=getenv("FRAMELY_CAST_ID"); + if(directory&&id)FrameSink(directory,id,"AirPlay").event("request"); + unsigned char type; + LOGI("ct=%d''',1) + p.write_text(s) + +# Mirroring may have no audio stream. Its size report arrives before decoding, +# so it must also request consent independently of the decoder's first frame. +s=p.read_text() +if 'framely_mirror_request' not in s: + marker='extern "C" void video_report_size(void *cls, float *width_source, float *height_source, float *width, float *height) {' + assert marker in s + s=s.replace(marker,marker+''' + // framely_mirror_request: notify even while the video decoder is starting. + const char* directory=getenv("FRAMELY_CAST_DIR"),*id=getenv("FRAMELY_CAST_ID"); + if(directory&&id&&*width_source>0&&*width_source<=4096&&*height_source>0&&*height_source<=4096) + FrameSink(directory,id,"AirPlay").event("request",(unsigned)*width_source,(unsigned)*height_source); +''',1) + p.write_text(s) diff --git a/src/cast/receiver.rs b/src/cast/receiver.rs index 16da130..5bcdd6f 100644 --- a/src/cast/receiver.rs +++ b/src/cast/receiver.rs @@ -216,6 +216,10 @@ impl Receivers { self.sessions.insert(id.clone(), e.clone()); self.events .push_back(json!({"kind":"cast.request","session":e})); + } else if kind=="request" && e["protocol"]=="AirPlay" && self.sessions[&id]["accepted"]!=true { + // SETUP can recur on an open control connection after the + // previous consent toast expired. Renew the pending request. + self.events.push_back(json!({"kind":"cast.request","session":self.sessions[&id]})); } else if matches!(kind, "video" | "audio") { let session = self.sessions.get_mut(&id).unwrap(); if kind == "video" { @@ -500,6 +504,24 @@ fn stop_child(child: &mut Child) { mod tests { use super::*; #[test] + fn airplay_setup_requests_consent_before_frames_and_after_reconnect() { + let root=tempfile::tempdir().unwrap(); + let mut r=Receivers::new(root.path().to_owned(),root.path().join("bin")); + r.airplay_id="0123".into(); + for _ in 0..2 { + r.event(json!({"event":"request","id":"0123","protocol":"AirPlay"})); + assert_eq!(r.events.pop_front().unwrap()["kind"],"cast.request"); + assert!(r.window("0123").is_err()); + r.event(json!({"event":"request","id":"0123","protocol":"AirPlay"})); + assert_eq!(r.events.pop_front().unwrap()["kind"],"cast.request"); + r.control("0123","accept",json!({})).unwrap(); + assert_eq!(r.window("0123").unwrap()["accepted"],true); + r.event(json!({"event":"ended","id":"0123","protocol":"AirPlay"})); + assert!(!root.path().join("0123.accept").exists()); + r.events.clear(); + } + } + #[test] fn consent_blocks_playback_controls_and_rejects_stale_airplay_sessions() { let root = tempfile::tempdir().unwrap(); let mut r = Receivers::new(root.path().to_owned(), root.path().join("bin"));