Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ hmac = { workspace = true }
libc = "0.2"
metrics-exporter-prometheus = "0.18"
sha2 = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tokio = { workspace = true, features = ["full", "test-util"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { workspace = true, features = ["v4"] }
wacore = { workspace = true, features = ["test-util"] }
Expand Down
102 changes: 78 additions & 24 deletions examples/voip-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ use whatsapp_rust::voip::audio::{WaOpusDecoder, WaOpusEncoder};
use whatsapp_rust::voip::session::{MediaPipeline, MediaPipelineParams};
#[cfg(feature = "voip-opus")]
use whatsapp_rust::voip::{AudioCodec, EncodedAudioFrame};
use whatsapp_rust::voip::{AudioFormat, CallHandle, VideoState};
use whatsapp_rust::voip::{AudioFormat, CallHandle, VideoState, VideoUpgradeToken};

mod video;
use video::VideoOpts;
Expand Down Expand Up @@ -977,7 +977,7 @@ struct CallState {
#[derive(Clone, Copy, PartialEq)]
enum VideoUi {
/// The peer asked for video (`UpgradeRequestV2`); `v` accepts it.
PendingPeerRequest,
PendingPeerRequest(VideoUpgradeToken),
/// Our video plane is up; `v` downgrades.
Active,
}
Expand Down Expand Up @@ -1080,12 +1080,14 @@ impl EventHandler for CallObserver {
let audio = self.audio;
// Only answer with video when we're allowed to AND the offer is actually a video
// call; advertising `<video>` on an audio offer would be wrong.
let video = self.video && *is_video;
let initial_video = self.video && *is_video;
let auto_video = self.video;
let state = self.state.clone();
let cid = call_id.clone();
begin_call_startup(&state, &cid);
tokio::spawn(async move {
if let Err(e) = respond_to_offer(&client, &call, accept, video, audio).await
if let Err(e) =
respond_to_offer(&client, &call, accept, initial_video, audio).await
{
error!("call signaling failed: {e}");
complete_call_startup(&state, &cid);
Expand All @@ -1095,7 +1097,9 @@ impl EventHandler for CallObserver {
complete_call_startup(&state, &cid);
return;
}
match start_media(&client, &call, video, audio, &state).await {
match start_media(&client, &call, initial_video, auto_video, audio, &state)
.await
{
Ok(handle) => register_handle(&state, cid.clone(), handle).await,
Err(e) => {
let peer_ended = peer_terminated_during_startup(&state, &cid);
Expand Down Expand Up @@ -1141,15 +1145,16 @@ impl EventHandler for CallObserver {
async fn start_media(
client: &Arc<Client>,
call: &IncomingCall,
video: bool,
initial_video: bool,
auto_video: bool,
audio: AudioMode,
state: &Arc<Mutex<CallState>>,
) -> Result<Arc<CallHandle>> {
let mic = spawn_mic()?;
let speaker = spawn_speaker()?;
let event_speaker = speaker.clone();
info!("🔌 connecting media via client.voip().accept(..)…");
let video_endpoints = if video {
let video_endpoints = if initial_video {
let opts = VideoOpts::from_env().await?;
let cid = call.action.call_id();
Some((
Expand All @@ -1162,7 +1167,7 @@ async fn start_media(
if peer_terminated_during_startup(state, call.action.call_id()) {
bail!("peer ended the call during media preparation");
}
send_final_accept(client, call, video, audio).await?;
send_final_accept(client, call, initial_video, audio).await?;
let voip = client.voip();
let mut builder = match audio {
#[cfg(feature = "voip-mlow")]
Expand All @@ -1185,13 +1190,13 @@ async fn start_media(
"🎙 {} media flow live for call {} — speak into the mic.{}",
audio.name(),
handle.call_id(),
if video { " 🎥 video on." } else { "" }
if initial_video { " 🎥 video on." } else { "" }
);
let handle = Arc::new(handle);
if video {
if initial_video {
mark_video(state, handle.call_id(), Some(VideoUi::Active));
}
spawn_call_event_listener(handle.clone(), event_speaker, video, state.clone());
spawn_call_event_listener(handle.clone(), event_speaker, auto_video, state.clone());
Ok(handle)
}

Expand Down Expand Up @@ -1258,6 +1263,29 @@ fn mark_video(state: &Arc<Mutex<CallState>>, call_id: &str, ui: Option<VideoUi>)
}
}

fn clear_pending_peer_video(state: &Arc<Mutex<CallState>>, call_id: &str) {
let mut st = lock_call_state(state);
if matches!(
st.video_ui.get(call_id),
Some(VideoUi::PendingPeerRequest(_))
) {
st.video_ui.remove(call_id);
}
}

fn activate_peer_video_request(
state: &Arc<Mutex<CallState>>,
call_id: &str,
request: VideoUpgradeToken,
) {
let mut st = lock_call_state(state);
if let Some(ui) = st.video_ui.get_mut(call_id)
&& *ui == VideoUi::PendingPeerRequest(request)
{
*ui = VideoUi::Active;
}
}
Comment thread
jlucaso1 marked this conversation as resolved.

/// Surface call diagnostics and drive the video upgrade handshake.
fn spawn_call_event_listener(
handle: Arc<CallHandle>,
Expand Down Expand Up @@ -1380,25 +1408,51 @@ fn spawn_call_event_listener(
"🎥 relay-send backpressure: dropped {video_access_units} complete video AUs / {packets} packets"
);
}
CallEvent::VideoStateChanged { state: vs, .. } => match vs {
CallEvent::VideoStateChanged {
state: vs,
upgrade_token,
..
} => match vs {
VideoState::UpgradeRequest | VideoState::UpgradeRequestV2 => {
let Some(request) = upgrade_token else {
info!("🎥 concurrent video upgrade resolved automatically");
continue;
};
mark_video(
&state,
handle.call_id(),
Some(VideoUi::PendingPeerRequest(request)),
);
if auto_video {
info!("🎥 peer asked for video — auto-accepting (--video)");
if let Err(e) = accept_peer_video(&handle).await {
warn!("accepting peer video failed: {e}");
} else {
mark_video(&state, handle.call_id(), Some(VideoUi::Active));
}
let handle = handle.clone();
let state = state.clone();
tokio::spawn(async move {
if let Err(e) = accept_peer_video(&handle, request).await {
Comment on lines +1430 to +1431

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cancel stale auto-accept source setup

In --video auto-accept mode, a peer cancel/re-request while the first accept_peer_video is still waiting for ffmpeg's first IDR now starts another detached task; the stale task is not cancelled and the token is not checked until after spawn_video_source opens /dev/video0, so the new request can fail with the camera still busy. Keep the task tied to the current token/cancel event or claim the request before opening the source.

Useful? React with 👍 / 👎.

warn!("accepting peer video failed: {e}");
} else {
activate_peer_video_request(&state, handle.call_id(), request);
}
});
} else {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
info!("🎥 peer asked for video — press `v` + Enter to accept");
mark_video(&state, handle.call_id(), Some(VideoUi::PendingPeerRequest));
}
}
VideoState::UpgradeAccept | VideoState::Enabled => {
info!("🎥 peer video {vs:?} — inbound video should start flowing");
}
VideoState::Stopped | VideoState::Disabled => {
VideoState::Stopped => {
info!("🎥 peer stopped its video");
clear_pending_peer_video(&state, handle.call_id());
}
VideoState::Disabled
| VideoState::UpgradeReject
| VideoState::UpgradeRejectByTimeout
| VideoState::UpgradeCancel
| VideoState::UpgradeCancelByTimeout
| VideoState::Error => {
info!("🎥 video upgrade ended ({vs:?})");
mark_video(&state, handle.call_id(), None);
}
other => info!("🎥 peer video state: {other:?}"),
},
Expand All @@ -1410,12 +1464,12 @@ fn spawn_call_event_listener(
}

/// Fresh ffmpeg/ffplay endpoints for a mid-call upgrade/accept on `handle`.
async fn accept_peer_video(handle: &CallHandle) -> Result<()> {
async fn accept_peer_video(handle: &CallHandle, request: VideoUpgradeToken) -> Result<()> {
let opts = VideoOpts::from_env().await?;
let src = video::spawn_video_source(&opts).await?;
let sink = video::spawn_video_sink(&opts, handle.call_id()).await?;
handle
.accept_video(src, sink)
.accept_video(request, src, sink)
.await
.map_err(|e| anyhow!("accept_video: {e}"))
}
Expand Down Expand Up @@ -1580,12 +1634,12 @@ fn spawn_stdin_ui(client: Arc<Client>, state: Arc<Mutex<CallState>>) {
mark_video(&state, &cid, None);
}
}
Some(VideoUi::PendingPeerRequest) => {
Some(VideoUi::PendingPeerRequest(request)) => {
info!("🎥 accepting the peer's video request");
if let Err(e) = accept_peer_video(&handle).await {
if let Err(e) = accept_peer_video(&handle, request).await {
warn!("accept_video failed: {e}");
} else {
mark_video(&state, &cid, Some(VideoUi::Active));
activate_peer_video_request(&state, &cid, request);
}
}
None => {
Expand Down
4 changes: 4 additions & 0 deletions src/client/voip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ pub enum CallError {
#[cfg(feature = "voip-runtime")]
#[error("media offer error: {0}")]
Media(&'static str),
/// The peer cancelled or replaced the upgrade before its video source became ready.
#[cfg(feature = "voip-runtime")]
#[error("video upgrade request is no longer current")]
VideoUpgradeExpired,
/// `call(peer)` resolved zero devices for the peer (nothing to address an offer to).
#[cfg(feature = "voip-runtime")]
#[error("peer has no resolvable devices")]
Expand Down
Loading
Loading