feat: allow clients to specify whether a call has video initially or not (#7740)

This commit is contained in:
Casper Zandbergen
2026-02-04 17:49:32 +01:00
committed by GitHub
parent d6bce56d18
commit 63bf4c4f33
12 changed files with 69 additions and 79 deletions

View File

@@ -19,9 +19,7 @@ use crate::tools::{normalize_text, time};
use anyhow::{Context as _, Result, ensure};
use deltachat_derive::{FromSql, ToSql};
use num_traits::FromPrimitive;
use sdp::SessionDescription;
use serde::Serialize;
use std::io::Cursor;
use std::str::FromStr;
use std::time::Duration;
use tokio::task;
@@ -126,6 +124,14 @@ impl CallInfo {
self.msg.param.exists(CALL_ACCEPTED_TIMESTAMP)
}
/// Returns true if the call is started as a video call.
pub fn has_video_initially(&self) -> bool {
self.msg
.param
.get_bool(Param::WebrtcHasVideoInitially)
.unwrap_or(false)
}
/// Returns true if the call is missed
/// because the caller canceled it
/// explicitly before ringing stopped.
@@ -185,6 +191,7 @@ impl Context {
&self,
chat_id: ChatId,
place_call_info: String,
has_video_initially: bool,
) -> Result<MsgId> {
let chat = Chat::load_from_db(self, chat_id).await?;
ensure!(
@@ -199,6 +206,8 @@ impl Context {
..Default::default()
};
call.param.set(Param::WebrtcRoom, &place_call_info);
call.param
.set_int(Param::WebrtcHasVideoInitially, has_video_initially.into());
call.id = send_msg(self, chat_id, &mut call).await?;
let wait = RINGING_SECONDS;
@@ -344,13 +353,6 @@ impl Context {
} else {
call.update_text(self, "Incoming call").await?;
self.emit_msgs_changed(call.msg.chat_id, call_id); // ringing calls are not additionally notified
let has_video = match sdp_has_video(&call.place_call_info) {
Ok(has_video) => has_video,
Err(err) => {
warn!(self, "Failed to determine if SDP offer has video: {err:#}.");
false
}
};
let can_call_me = match who_can_call_me(self).await? {
WhoCanCallMe::Contacts => ChatIdBlocked::lookup_by_contact(self, from_id)
.await?
@@ -377,7 +379,7 @@ impl Context {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
place_call_info: call.place_call_info.to_string(),
has_video,
has_video: call.has_video_initially(),
});
}
let wait = call.remaining_ring_seconds();
@@ -507,19 +509,6 @@ impl Context {
}
}
/// Returns true if SDP offer has a video.
pub fn sdp_has_video(sdp: &str) -> Result<bool> {
let mut cursor = Cursor::new(sdp);
let session_description =
SessionDescription::unmarshal(&mut cursor).context("Failed to parse SDP")?;
for media_description in &session_description.media_descriptions {
if media_description.media_name.media == "video" {
return Ok(true);
}
}
Ok(false)
}
/// State of the call for display in the message bubble.
#[derive(Debug, PartialEq, Eq)]
pub enum CallState {

View File

@@ -25,13 +25,6 @@ async fn assert_text(t: &TestContext, call_id: MsgId, text: &str) -> Result<()>
const PLACE_INFO: &str = "v=0\r\no=alice 2890844526 2890844526 IN IP4 host.anywhere.com\r\ns=-\r\nc=IN IP4 host.anywhere.com\r\nt=0 0\r\nm=audio 62986 RTP/AVP 0 4 18\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:4 G723/8000\r\na=rtpmap:18 G729/8000\r\na=inactive\r\n";
const ACCEPT_INFO: &str = "v=0\r\no=bob 2890844730 2890844731 IN IP4 host.example.com\r\ns=\r\nc=IN IP4 host.example.com\r\nt=0 0\r\nm=audio 54344 RTP/AVP 0 4\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:4 G723/8000\r\na=inactive\r\n";
/// Example from <https://datatracker.ietf.org/doc/rfc9143/>
/// with `s= ` replaced with `s=-`.
///
/// `s=` cannot be empty according to RFC 3264,
/// so it is more clear as `s=-`.
const PLACE_INFO_VIDEO: &str = "v=0\r\no=alice 2890844526 2890844526 IN IP6 2001:db8::3\r\ns=-\r\nc=IN IP6 2001:db8::3\r\nt=0 0\r\na=group:BUNDLE foo bar\r\n\r\nm=audio 10000 RTP/AVP 0 8 97\r\nb=AS:200\r\na=mid:foo\r\na=rtcp-mux\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:97 iLBC/8000\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\r\nm=video 10002 RTP/AVP 31 32\r\nb=AS:1000\r\na=mid:bar\r\na=rtcp-mux\r\na=rtpmap:31 H261/90000\r\na=rtpmap:32 MPV/90000\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\n";
async fn setup_call() -> Result<CallSetup> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
@@ -52,7 +45,7 @@ async fn setup_call() -> Result<CallSetup> {
bob2.create_chat(&alice).await;
let test_msg_id = alice
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string())
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string(), true)
.await?;
let sent1 = alice.pop_sent_msg().await;
assert_eq!(sent1.sender_msg_id, test_msg_id);
@@ -68,6 +61,7 @@ async fn setup_call() -> Result<CallSetup> {
assert!(!info.is_incoming());
assert!(!info.is_accepted());
assert_eq!(info.place_call_info, PLACE_INFO);
assert_eq!(info.has_video_initially(), true);
assert_text(t, m.id, "Outgoing call").await?;
assert_eq!(call_state(t, m.id).await?, CallState::Alerting);
}
@@ -89,6 +83,7 @@ async fn setup_call() -> Result<CallSetup> {
assert!(info.is_incoming());
assert!(!info.is_accepted());
assert_eq!(info.place_call_info, PLACE_INFO);
assert_eq!(info.has_video_initially(), true);
assert_text(t, m.id, "Incoming call").await?;
assert_eq!(call_state(t, m.id).await?, CallState::Alerting);
}
@@ -525,13 +520,6 @@ async fn test_update_call_text() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sdp_has_video() {
assert!(sdp_has_video("foobar").is_err());
assert_eq!(sdp_has_video(PLACE_INFO).unwrap(), false);
assert_eq!(sdp_has_video(PLACE_INFO_VIDEO).unwrap(), true);
}
/// Tests that calls are forwarded as text messages.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forward_call() -> Result<()> {
@@ -542,7 +530,7 @@ async fn test_forward_call() -> Result<()> {
let alice_bob_chat = alice.create_chat(bob).await;
let alice_msg_id = alice
.place_outgoing_call(alice_bob_chat.id, PLACE_INFO.to_string())
.place_outgoing_call(alice_bob_chat.id, PLACE_INFO.to_string(), true)
.await
.context("Failed to place a call")?;
let alice_call = Message::load_from_db(alice, alice_msg_id).await?;

View File

@@ -91,6 +91,7 @@ pub enum HeaderDef {
ChatDispositionNotificationTo,
ChatWebrtcRoom,
ChatWebrtcAccepted,
ChatWebrtcHasVideoInitially,
/// This message deletes the messages listed in the value by rfc724_mid.
ChatDelete,

View File

@@ -1840,6 +1840,12 @@ impl MimeFactory {
mail_builder::headers::raw::Raw::new(b_encode(answer)).into(),
));
}
if let Some(has_video) = msg.param.get(Param::WebrtcHasVideoInitially) {
headers.push((
"Chat-Webrtc-Has-Video-Initially",
mail_builder::headers::raw::Raw::new(b_encode(has_video)).into(),
))
}
if msg.viewtype == Viewtype::Voice
|| msg.viewtype == Viewtype::Audio

View File

@@ -801,6 +801,9 @@ impl MimeMessage {
let accepted = self
.get_header(HeaderDef::ChatWebrtcAccepted)
.map(|s| s.to_string());
let has_video = self
.get_header(HeaderDef::ChatWebrtcHasVideoInitially)
.map(|s| s.to_string());
if let Some(part) = self.parts.first_mut() {
if let Some(room) = room {
if content == "call" {
@@ -810,6 +813,9 @@ impl MimeMessage {
} else if let Some(accepted) = accepted {
part.param.set(Param::WebrtcAccepted, accepted);
}
if let Some(has_video) = has_video {
part.param.set(Param::WebrtcHasVideoInitially, has_video);
}
}
}

View File

@@ -148,6 +148,9 @@ pub enum Param {
/// For Messages
WebrtcAccepted = b'7',
/// For Messages
WebrtcHasVideoInitially = b'z',
/// For Messages: space-separated list of messaged IDs of forwarded copies.
///
/// This is used when a [crate::message::Message] is in the