Repository navigation
fix(portal): 修复 iPhone 锁屏冷连接无法自动解锁的问题 #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
VallierDev
merged 2 commits into
VallierDev:main
from
wanglz111:codex/ios-locked-bootstrap
Oct 17, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| //! Black video keeps locked Portal initialization reachable by the iPhone viewer. | ||
| use std::time::Duration; | ||
| use bytes::Bytes; | ||
| use webrtc::{media::Sample, peer_connection::{RTCPeerConnection, peer_connection_state::RTCPeerConnectionState}}; | ||
| use crate::{capture_setting::VideoCodec, desktop_session::PortalDesktop, video_track::VideoTrack}; | ||
|
|
||
| async fn black_frame(codec: VideoCodec) -> Result<Bytes, String> { | ||
| let mut command = crate::video::ffmpeg_command(); | ||
| command.args(["-hide_banner", "-loglevel", "error", "-nostdin", | ||
| "-f", "lavfi", "-i", "color=c=black:s=1280x720:r=1", "-frames:v", "1", | ||
| "-an", "-pix_fmt", "yuv420p", "-threads", "1", "-preset", "ultrafast", "-tune", "zerolatency"]); | ||
| match codec { | ||
| VideoCodec::H264 => { command.args(["-c:v", "libx264", "-profile:v", "baseline", | ||
| "-x264-params", "keyint=1:repeat-headers=1:aud=1", "-f", "h264"]); }, | ||
| VideoCodec::H265 => { command.args(["-c:v", "libx265", | ||
| "-x265-params", "keyint=1:repeat-headers=1:aud=1:pools=1:frame-threads=1:log-level=error", "-f", "hevc"]); }, | ||
| } | ||
| command.arg("pipe:1").kill_on_drop(true); | ||
| let output = tokio::time::timeout(Duration::from_secs(5), command.output()).await | ||
| .map_err(|_| "Locked desktop video encoder timed out".to_string())? | ||
| .map_err(|error| format!("Locked desktop video encoder: {error}"))?; | ||
| if !output.status.success() || output.stdout.is_empty() { | ||
| return Err(format!("Locked desktop video encoder failed: status={}", output.status)); | ||
| } | ||
| Ok(output.stdout.into()) | ||
| } | ||
|
|
||
| pub async fn run(pc: &RTCPeerConnection, track: &VideoTrack, desktop: &PortalDesktop) -> Result<(), String> { | ||
| run_with_encoder(pc, track, desktop, black_frame).await | ||
| } | ||
|
|
||
| async fn run_with_encoder<F, Fut>(pc: &RTCPeerConnection, track: &VideoTrack, desktop: &PortalDesktop, mut encode: F) -> Result<(), String> | ||
| where F: FnMut(VideoCodec) -> Fut, Fut: std::future::Future<Output = Result<Bytes, String>> { | ||
| // #29: iPhone 4.42.0 cold ON/OFF/ON tests only sent action15 after black video. | ||
| // Keep CONTROL permissions truthful and use the existing video track until real capture is ready. | ||
| let mut updates = desktop.subscribe(); | ||
| let mut locked = desktop.lock_state(); | ||
| let mut tick = tokio::time::interval(Duration::from_secs(1)); | ||
| tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); | ||
| let mut frame = None; | ||
| let mut sent = 0u64; | ||
| let mut attempts = 0u64; | ||
| loop { | ||
| let state = updates.borrow_and_update().clone(); | ||
| let _ = locked.borrow_and_update(); | ||
| if state.ready() || !state.waiting || crate::video::transport_closed(pc) { | ||
| if attempts > 0 { log::info!("Locked desktop video ended: sent={sent} attempts={attempts}"); } | ||
| return Ok(()); | ||
| } | ||
| tokio::select! { | ||
| biased; | ||
| changed = updates.changed() => if changed.is_err() { return Ok(()); }, | ||
| changed = locked.changed() => if changed.is_err() { return Ok(()); }, | ||
| _ = tick.tick() => { | ||
| if !updates.borrow().waiting || *locked.borrow() != Some(true) | ||
| || pc.connection_state() != RTCPeerConnectionState::Connected { continue; } | ||
| if frame.is_none() { | ||
| let codec = crate::capture_setting::preferred_codec( | ||
| track.supports(VideoCodec::H264), track.supports(VideoCodec::H265)); | ||
| let encoded = tokio::select! { | ||
| biased; | ||
| _ = updates.changed() => continue, | ||
| _ = locked.changed() => continue, | ||
| result = encode(codec) => result?, | ||
| }; | ||
| frame = Some((codec, encoded)); | ||
| } | ||
| if crate::video::transport_closed(pc) { return Ok(()); } | ||
| // Encoding can finish in the same poll that publishes unlock/readiness. | ||
| if !updates.borrow().waiting || *locked.borrow() != Some(true) | ||
| || pc.connection_state() != RTCPeerConnectionState::Connected { continue; } | ||
| let (codec, data) = frame.as_ref().unwrap(); | ||
| let sample = Sample { data: data.clone(), duration: Duration::from_secs(1), ..Default::default() }; | ||
| let extensions = [crate::rtp_metadata::capture_index(0), crate::rtp_metadata::new_frame()]; | ||
| let revision = track.revision(); | ||
| // No monitor inventory or permission claims: 1280x720 is only this synthetic bitstream. | ||
| tokio::select! { | ||
| biased; | ||
| _ = updates.changed() => continue, | ||
| _ = locked.changed() => continue, | ||
| result = tokio::time::timeout(Duration::from_secs(2), | ||
| track.write_sample_with_extensions(*codec, &sample, &extensions)) => { | ||
| if crate::video::transport_closed(pc) { return Ok(()); } | ||
| result.map_err(|_| "Locked desktop video send timed out".to_string())? | ||
| .map_err(|error| format!("Locked desktop video send: {error}"))?; | ||
| }, | ||
| } | ||
| attempts += 1; | ||
| if track.revision() > revision { sent += 1; } | ||
| tick.reset(); | ||
| if attempts == 1 || attempts % 10 == 0 { | ||
| log::info!("Locked desktop video: sent={sent} codec={codec:?} synthetic_size=1280x720 bytes={} attempts={attempts}", data.len()); | ||
| } | ||
| }, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| #[path = "locked_video_tests.rs"] | ||
| mod tests; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a lock/readiness notification arrives after a backpressured send has begun, either
changed()arm wins and dropswrite_sample_with_extensionsin the middle of an access unit (the two-second timeout can do the same).VideoTrack::write_sample_with_extensionsadvances the shared timestamp up front and the sequence before each awaited packet write (src/video_track.rs:64-70), so the handoff to live capture can start after a partial, unmarked IDR and with a sequence gap, causing loss/NACKs or delayed first video on exactly the slow path this timeout covers. Make this access-unit write cancellation-safe, or allow the in-flight frame to finish before reacting to state changes.Useful? React with 👍 / 👎.