Add voice call API types, protobuf definitions, and build dependencies

Define call method interfaces in Manager, create API records (CallInfo,
CallOffer, TurnServer), and hand-coded protobuf parsers for RingRTC
signaling messages (ConnectionParametersV4, RtpDataMessage).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Shaheen Gandhi 2026-02-11 14:01:59 -08:00
parent 2885ffeee8
commit 0ea4838e01
7 changed files with 145 additions and 0 deletions

View File

@ -64,6 +64,10 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.asamk.signal.manager.api.CallInfo;
import org.asamk.signal.manager.api.CallOffer;
import org.asamk.signal.manager.api.TurnServer;
public interface Manager extends Closeable {
static boolean isValidNumber(final String e164Number, final String countryCode) {
@ -413,6 +417,30 @@ public interface Manager extends Closeable {
InputStream retrieveSticker(final StickerPackId stickerPackId, final int stickerId) throws IOException;
// --- Voice call methods ---
CallInfo startCall(RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;
CallInfo acceptCall(long callId) throws IOException;
void hangupCall(long callId) throws IOException;
void rejectCall(long callId) throws IOException;
List<CallInfo> listActiveCalls();
void sendCallOffer(RecipientIdentifier.Single recipient, CallOffer offer) throws IOException, UnregisteredRecipientException;
void sendCallAnswer(RecipientIdentifier.Single recipient, long callId, byte[] answerOpaque) throws IOException, UnregisteredRecipientException;
void sendIceUpdate(RecipientIdentifier.Single recipient, long callId, List<byte[]> iceCandidates) throws IOException, UnregisteredRecipientException;
void sendHangup(RecipientIdentifier.Single recipient, long callId, MessageEnvelope.Call.Hangup.Type type) throws IOException, UnregisteredRecipientException;
void sendBusy(RecipientIdentifier.Single recipient, long callId) throws IOException, UnregisteredRecipientException;
List<TurnServer> getTurnServerInfo() throws IOException;
@Override
void close();

View File

@ -0,0 +1,21 @@
package org.asamk.signal.manager.api;
public record CallInfo(
long callId,
State state,
RecipientAddress recipient,
String inputDeviceName,
String outputDeviceName,
boolean isOutgoing
) {
public enum State {
IDLE,
RINGING_INCOMING,
RINGING_OUTGOING,
CONNECTING,
CONNECTED,
RECONNECTING,
ENDED
}
}

View File

@ -0,0 +1,13 @@
package org.asamk.signal.manager.api;
public record CallOffer(
long callId,
Type type,
byte[] opaque
) {
public enum Type {
AUDIO,
VIDEO
}
}

View File

@ -0,0 +1,10 @@
package org.asamk.signal.manager.api;
import java.util.List;
public record TurnServer(
String username,
String password,
List<String> urls
) {
}

View File

@ -15,6 +15,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.account.AccountApi;
import org.whispersystems.signalservice.api.attachment.AttachmentApi;
import org.whispersystems.signalservice.api.calling.CallingApi;
import org.whispersystems.signalservice.api.cds.CdsApi;
import org.whispersystems.signalservice.api.certificate.CertificateApi;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
@ -76,6 +77,7 @@ public class SignalDependencies {
private StorageServiceApi storageServiceApi;
private CertificateApi certificateApi;
private AttachmentApi attachmentApi;
private CallingApi callingApi;
private MessageApi messageApi;
private KeysApi keysApi;
private GroupsV2Operations groupsV2Operations;
@ -255,6 +257,13 @@ public class SignalDependencies {
() -> attachmentApi = new AttachmentApi(getAuthenticatedSignalWebSocket(), getPushServiceSocket()));
}
public CallingApi getCallingApi() {
return getOrCreate(() -> callingApi,
() -> callingApi = new CallingApi(getAuthenticatedSignalWebSocket(),
getUnauthenticatedSignalWebSocket(),
getPushServiceSocket()));
}
public MessageApi getMessageApi() {
return getOrCreate(() -> messageApi,
() -> messageApi = new MessageApi(getAuthenticatedSignalWebSocket(),

View File

@ -0,0 +1,32 @@
// In-call control messages carried over the RTP data channel.
// signal-cli hand-codes the parsing in RtpDataProtobuf.java rather than using protoc.
syntax = "proto2";
package rtp_data;
option java_package = "org.asamk.signal.manager.calling.proto";
option java_outer_classname = "RtpDataProtos";
message Accepted {}
message Hangup {
optional uint32 id = 1;
}
message SenderStatus {
optional bool audio_enabled = 1;
optional bool video_enabled = 2;
optional bool sharing_screen = 3;
}
message Receiver {
optional uint32 id = 1;
}
// Top-level RTP data message
message Data {
optional Accepted accepted = 1;
optional Hangup hangup = 2;
optional SenderStatus sender_status = 3;
optional Receiver receiver = 4;
}

View File

@ -0,0 +1,32 @@
// RingRTC signaling protobuf definitions
// These define the structure of the opaque blobs inside Signal call Offer/Answer messages.
// signal-cli hand-codes the parsing in SignalingProtobuf.java rather than using protoc.
syntax = "proto2";
package signaling;
option java_package = "org.asamk.signal.manager.calling.proto";
option java_outer_classname = "SignalingProtos";
message VideoCodec {
enum Type {
VP8 = 0;
H264 = 1;
VP9 = 2;
}
optional Type type = 1;
optional uint32 level = 2;
}
message ConnectionParametersV4 {
optional bytes public_key = 1; // x25519 public key (32 bytes)
optional string ice_ufrag = 2;
optional string ice_pwd = 3;
repeated VideoCodec receive_video_codecs = 4;
optional uint64 max_bitrate_bps = 5;
}
// The top-level opaque blob inside an OfferMessage or AnswerMessage
message Opaque {
optional ConnectionParametersV4 connection_parameters_v4 = 1;
}