2443 lines
65 KiB
TypeScript
2443 lines
65 KiB
TypeScript
//
|
|
// Copyright 2019-2021 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import { GumVideoCaptureOptions, VideoPixelFormatEnum } from './VideoSupport';
|
|
|
|
/* tslint:disable max-classes-per-file */
|
|
|
|
import * as os from 'os';
|
|
import * as process from 'process';
|
|
|
|
// tslint:disable-next-line no-var-requires no-require-imports
|
|
const Native = require('../../build/' +
|
|
os.platform() +
|
|
'/libringrtc-' +
|
|
process.arch +
|
|
'.node');
|
|
|
|
class Config {
|
|
use_new_audio_device_module: boolean = false;
|
|
}
|
|
|
|
// tslint:disable-next-line no-unnecessary-class
|
|
class NativeCallManager {
|
|
// Read by Rust
|
|
private readonly observer: CallManagerCallbacks;
|
|
constructor(observer: CallManagerCallbacks) {
|
|
this.observer = observer;
|
|
this.createCallEndpoint(new Config());
|
|
}
|
|
|
|
setConfig(config: Config) {
|
|
this.createCallEndpoint(config);
|
|
}
|
|
|
|
private createCallEndpoint(config: Config) {
|
|
const callEndpoint = Native.createCallEndpoint(
|
|
this,
|
|
config.use_new_audio_device_module
|
|
);
|
|
Object.defineProperty(this, Native.callEndpointPropertyKey, {
|
|
value: callEndpoint,
|
|
configurable: true, // allows it to be changed
|
|
});
|
|
}
|
|
}
|
|
|
|
// Mirror methods onto NativeCallManager.
|
|
// This is done through direct assignment rather than wrapper methods to avoid indirection.
|
|
(NativeCallManager.prototype as any).setSelfUuid = Native.cm_setSelfUuid;
|
|
(NativeCallManager.prototype as any).createOutgoingCall =
|
|
Native.cm_createOutgoingCall;
|
|
(NativeCallManager.prototype as any).proceed = Native.cm_proceed;
|
|
(NativeCallManager.prototype as any).accept = Native.cm_accept;
|
|
(NativeCallManager.prototype as any).ignore = Native.cm_ignore;
|
|
(NativeCallManager.prototype as any).hangup = Native.cm_hangup;
|
|
(NativeCallManager.prototype as any).cancelGroupRing =
|
|
Native.cm_cancelGroupRing;
|
|
(NativeCallManager.prototype as any).signalingMessageSent =
|
|
Native.cm_signalingMessageSent;
|
|
(NativeCallManager.prototype as any).signalingMessageSendFailed =
|
|
Native.cm_signalingMessageSendFailed;
|
|
(NativeCallManager.prototype as any).updateBandwidthMode =
|
|
Native.cm_updateBandwidthMode;
|
|
(NativeCallManager.prototype as any).receivedOffer = Native.cm_receivedOffer;
|
|
(NativeCallManager.prototype as any).receivedAnswer = Native.cm_receivedAnswer;
|
|
(NativeCallManager.prototype as any).receivedIceCandidates =
|
|
Native.cm_receivedIceCandidates;
|
|
(NativeCallManager.prototype as any).receivedHangup = Native.cm_receivedHangup;
|
|
(NativeCallManager.prototype as any).receivedBusy = Native.cm_receivedBusy;
|
|
(NativeCallManager.prototype as any).receivedCallMessage =
|
|
Native.cm_receivedCallMessage;
|
|
(NativeCallManager.prototype as any).receivedHttpResponse =
|
|
Native.cm_receivedHttpResponse;
|
|
(NativeCallManager.prototype as any).httpRequestFailed =
|
|
Native.cm_httpRequestFailed;
|
|
(NativeCallManager.prototype as any).setOutgoingAudioEnabled =
|
|
Native.cm_setOutgoingAudioEnabled;
|
|
(NativeCallManager.prototype as any).setOutgoingVideoEnabled =
|
|
Native.cm_setOutgoingVideoEnabled;
|
|
(NativeCallManager.prototype as any).setOutgoingVideoIsScreenShare =
|
|
Native.cm_setOutgoingVideoIsScreenShare;
|
|
(NativeCallManager.prototype as any).sendVideoFrame = Native.cm_sendVideoFrame;
|
|
(NativeCallManager.prototype as any).receiveVideoFrame =
|
|
Native.cm_receiveVideoFrame;
|
|
(NativeCallManager.prototype as any).receiveGroupCallVideoFrame =
|
|
Native.cm_receiveGroupCallVideoFrame;
|
|
(NativeCallManager.prototype as any).createGroupCallClient =
|
|
Native.cm_createGroupCallClient;
|
|
(NativeCallManager.prototype as any).deleteGroupCallClient =
|
|
Native.cm_deleteGroupCallClient;
|
|
(NativeCallManager.prototype as any).connect = Native.cm_connect;
|
|
(NativeCallManager.prototype as any).join = Native.cm_join;
|
|
(NativeCallManager.prototype as any).leave = Native.cm_leave;
|
|
(NativeCallManager.prototype as any).disconnect = Native.cm_disconnect;
|
|
(NativeCallManager.prototype as any).groupRing = Native.cm_groupRing;
|
|
(NativeCallManager.prototype as any).setOutgoingAudioMuted =
|
|
Native.cm_setOutgoingAudioMuted;
|
|
(NativeCallManager.prototype as any).setOutgoingVideoMuted =
|
|
Native.cm_setOutgoingVideoMuted;
|
|
(NativeCallManager.prototype as any).setOutgoingGroupCallVideoIsScreenShare =
|
|
Native.cm_setOutgoingGroupCallVideoIsScreenShare;
|
|
(NativeCallManager.prototype as any).setPresenting = Native.cm_setPresenting;
|
|
(NativeCallManager.prototype as any).resendMediaKeys =
|
|
Native.cm_resendMediaKeys;
|
|
(NativeCallManager.prototype as any).setBandwidthMode =
|
|
Native.cm_setBandwidthMode;
|
|
(NativeCallManager.prototype as any).requestVideo = Native.cm_requestVideo;
|
|
(NativeCallManager.prototype as any).setGroupMembers =
|
|
Native.cm_setGroupMembers;
|
|
(NativeCallManager.prototype as any).setMembershipProof =
|
|
Native.cm_setMembershipProof;
|
|
(NativeCallManager.prototype as any).peekGroupCall = Native.cm_peekGroupCall;
|
|
(NativeCallManager.prototype as any).getAudioInputs = Native.cm_getAudioInputs;
|
|
(NativeCallManager.prototype as any).setAudioInput = Native.cm_setAudioInput;
|
|
(NativeCallManager.prototype as any).getAudioOutputs =
|
|
Native.cm_getAudioOutputs;
|
|
(NativeCallManager.prototype as any).setAudioOutput = Native.cm_setAudioOutput;
|
|
(NativeCallManager.prototype as any).processEvents = Native.cm_processEvents;
|
|
|
|
type GroupId = Buffer;
|
|
type GroupCallUserId = Buffer;
|
|
|
|
export class PeekDeviceInfo {
|
|
demuxId: number;
|
|
userId?: GroupCallUserId;
|
|
|
|
constructor(demuxId: number, userId: GroupCallUserId | undefined) {
|
|
this.demuxId = demuxId;
|
|
this.userId = userId;
|
|
}
|
|
}
|
|
|
|
export class PeekInfo {
|
|
devices: Array<PeekDeviceInfo>;
|
|
creator?: GroupCallUserId;
|
|
eraId?: string;
|
|
maxDevices?: number;
|
|
deviceCount: number;
|
|
|
|
constructor() {
|
|
this.devices = [];
|
|
this.deviceCount = 0;
|
|
}
|
|
}
|
|
|
|
// In sync with WebRTC's PeerConnection.AdapterType.
|
|
// Despite how it looks, this is not an option set.
|
|
// A network adapter type can only be one of the listed values.
|
|
// And there are a few oddities to note:
|
|
// - Cellular means we don't know if it's 2G, 3G, 4G, 5G, ...
|
|
// If we know, it will be one of those corresponding enum values.
|
|
// This means to know if something is cellular or not, you must
|
|
// check all of those values.
|
|
// - Default means we don't know the adapter type (like Unknown)
|
|
// but it's because we bound to the default IP address (0.0.0.0)
|
|
// so it's probably the default adapter (wifi if available, for example)
|
|
// This is unlikely to happen in practice.
|
|
enum NetworkAdapterType {
|
|
Unknown = 0,
|
|
Ethernet = 1 << 0,
|
|
Wifi = 1 << 1,
|
|
Cellular = 1 << 2,
|
|
Vpn = 1 << 3,
|
|
Loopback = 1 << 4,
|
|
Default = 1 << 5,
|
|
Cellular2G = 1 << 6,
|
|
Cellular3G = 1 << 7,
|
|
Cellular4G = 1 << 8,
|
|
Cellular5G = 1 << 9,
|
|
}
|
|
|
|
// Information about the network route being used for sending audio/video/data
|
|
export class NetworkRoute {
|
|
localAdapterType: NetworkAdapterType;
|
|
|
|
constructor() {
|
|
this.localAdapterType = NetworkAdapterType.Unknown;
|
|
}
|
|
}
|
|
|
|
// Range of 0-32767 where 0 is silence.
|
|
export type RawAudioLevel = number;
|
|
// Range of 0-1 where 0 is silence.
|
|
export type NormalizedAudioLevel = number;
|
|
|
|
export class ReceivedAudioLevel {
|
|
demuxId: number; // UInt32
|
|
level: RawAudioLevel;
|
|
|
|
constructor(demuxId: number, level: RawAudioLevel) {
|
|
this.demuxId = demuxId;
|
|
this.level = level;
|
|
}
|
|
}
|
|
|
|
function normalizeAudioLevel(raw: RawAudioLevel): NormalizedAudioLevel {
|
|
return raw / 32767;
|
|
}
|
|
|
|
class Requests<T> {
|
|
private _resolveById: Map<number, (response: T) => void> = new Map();
|
|
private _nextId: number = 1;
|
|
|
|
add(): [number, Promise<T>] {
|
|
const id = this._nextId++;
|
|
const promise = new Promise<T>((resolve, _reject) => {
|
|
this._resolveById.set(id, resolve);
|
|
});
|
|
return [id, promise];
|
|
}
|
|
|
|
resolve(id: number, response: T): boolean {
|
|
const resolve = this._resolveById.get(id);
|
|
if (!resolve) {
|
|
return false;
|
|
}
|
|
resolve(response);
|
|
this._resolveById.delete(id);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
class CallInfo {
|
|
isVideoCall: boolean;
|
|
receivedAtCounter: number;
|
|
|
|
constructor(isVideoCall: boolean, receivedAtCounter: number) {
|
|
this.isVideoCall = isVideoCall;
|
|
this.receivedAtCounter = receivedAtCounter;
|
|
}
|
|
}
|
|
|
|
export class RingRTCType {
|
|
private readonly callManager: CallManager;
|
|
private _call: Call | null;
|
|
private _groupCallByClientId: Map<GroupCallClientId, GroupCall>;
|
|
private _peekRequests: Requests<PeekInfo>;
|
|
|
|
// A map to hold call information not maintained in RingRTC.
|
|
private _callInfoByCallId: Map<String, CallInfo>;
|
|
|
|
private getCallInfoKey(callId: CallId): String {
|
|
// CallId is u64 so use a string key instead.
|
|
return callId.high.toString() + callId.low.toString();
|
|
}
|
|
|
|
// Set by UX
|
|
handleOutgoingSignaling:
|
|
| ((remoteUserId: UserId, message: CallingMessage) => Promise<boolean>)
|
|
| null = null;
|
|
handleIncomingCall: ((call: Call) => Promise<CallSettings | null>) | null =
|
|
null;
|
|
handleAutoEndedIncomingCallRequest:
|
|
| ((
|
|
callId: CallId,
|
|
remoteUserId: UserId,
|
|
reason: CallEndedReason,
|
|
ageSec: number,
|
|
wasVideoCall: boolean,
|
|
receivedAtCounter: number | undefined
|
|
) => void)
|
|
| null = null;
|
|
handleLogMessage:
|
|
| ((
|
|
level: CallLogLevel,
|
|
fileName: string,
|
|
line: number,
|
|
message: string
|
|
) => void)
|
|
| null = null;
|
|
handleSendHttpRequest:
|
|
| ((
|
|
requestId: number,
|
|
url: string,
|
|
method: HttpMethod,
|
|
headers: { [name: string]: string },
|
|
body: Buffer | undefined
|
|
) => void)
|
|
| null = null;
|
|
handleSendCallMessage:
|
|
| ((
|
|
recipientUuid: Buffer,
|
|
message: Buffer,
|
|
urgency: CallMessageUrgency
|
|
) => void)
|
|
| null = null;
|
|
handleSendCallMessageToGroup:
|
|
| ((groupId: Buffer, message: Buffer, urgency: CallMessageUrgency) => void)
|
|
| null = null;
|
|
handleGroupCallRingUpdate:
|
|
| ((
|
|
groupId: Buffer,
|
|
ringId: bigint,
|
|
sender: Buffer,
|
|
update: RingUpdate
|
|
) => void)
|
|
| null = null;
|
|
|
|
constructor() {
|
|
this.callManager = new NativeCallManager(this) as unknown as CallManager;
|
|
this._call = null;
|
|
this._groupCallByClientId = new Map();
|
|
this._peekRequests = new Requests<PeekInfo>();
|
|
this._callInfoByCallId = new Map();
|
|
}
|
|
|
|
setConfig(config: Config) {
|
|
this.callManager.setConfig(config);
|
|
}
|
|
|
|
// Called by UX
|
|
setSelfUuid(uuid: Buffer): void {
|
|
this.callManager.setSelfUuid(uuid);
|
|
}
|
|
|
|
// Called by UX
|
|
startOutgoingCall(
|
|
remoteUserId: UserId,
|
|
isVideoCall: boolean,
|
|
localDeviceId: DeviceId,
|
|
settings: CallSettings
|
|
): Call {
|
|
const callId = this.callManager.createOutgoingCall(
|
|
remoteUserId,
|
|
isVideoCall,
|
|
localDeviceId
|
|
);
|
|
const isIncoming = false;
|
|
const call = new Call(
|
|
this.callManager,
|
|
remoteUserId,
|
|
callId,
|
|
isIncoming,
|
|
isVideoCall,
|
|
settings,
|
|
CallState.Prering
|
|
);
|
|
this._call = call;
|
|
// We won't actually send anything until the remote side accepts.
|
|
call.outgoingAudioEnabled = true;
|
|
call.outgoingVideoEnabled = isVideoCall;
|
|
return call;
|
|
}
|
|
|
|
// Called by UX
|
|
cancelGroupRing(
|
|
groupId: GroupId,
|
|
ringId: bigint,
|
|
reason: RingCancelReason | null
|
|
): void {
|
|
silly_deadlock_protection(() => {
|
|
this.callManager.cancelGroupRing(groupId, ringId.toString(), reason);
|
|
});
|
|
}
|
|
|
|
// Called by Rust
|
|
onStartOutgoingCall(remoteUserId: UserId, callId: CallId): void {
|
|
const call = this._call;
|
|
if (!call || call.remoteUserId !== remoteUserId || !call.settings) {
|
|
return;
|
|
}
|
|
|
|
call.callId = callId;
|
|
this.proceed(callId, call.settings);
|
|
}
|
|
|
|
// Called by Rust
|
|
onStartIncomingCall(
|
|
remoteUserId: UserId,
|
|
callId: CallId,
|
|
isVideoCall: boolean
|
|
): void {
|
|
// Temporary: Force hangup in all glare scenarios until handled gracefully.
|
|
// In case of a glare loser, an incoming call will be generated right
|
|
// after the outgoing call is ended. In that case, ignore it once.
|
|
if (
|
|
this._call &&
|
|
(this._call.endedReason === CallEndedReason.Glare ||
|
|
this._call.endedReason === CallEndedReason.ReCall)
|
|
) {
|
|
this._call.endedReason = undefined;
|
|
// EVIL HACK: We are the "loser" of a glare collision and have ended the outgoing call
|
|
// and are now receiving the incoming call from the remote side (the "winner").
|
|
// However, the Desktop client has a bug where it re-orders the events so that
|
|
// instead of seeing ("outgoing call ended", "incoming call"), it sees
|
|
// ("incoming call", "call ended") and it gets messed up.
|
|
// The solution? Delay processing the incoming call.
|
|
setTimeout(() => {
|
|
this.onStartIncomingCall(remoteUserId, callId, isVideoCall);
|
|
}, 500);
|
|
return;
|
|
}
|
|
|
|
const isIncoming = true;
|
|
const call = new Call(
|
|
this.callManager,
|
|
remoteUserId,
|
|
callId,
|
|
isIncoming,
|
|
isVideoCall,
|
|
null,
|
|
CallState.Prering
|
|
);
|
|
// Callback to UX not set
|
|
const handleIncomingCall = this.handleIncomingCall;
|
|
if (!handleIncomingCall) {
|
|
call.ignore();
|
|
return;
|
|
}
|
|
this._call = call;
|
|
|
|
// tslint:disable no-floating-promises
|
|
(async () => {
|
|
const settings = await handleIncomingCall(call);
|
|
if (!settings) {
|
|
call.ignore();
|
|
return;
|
|
}
|
|
|
|
call.settings = settings;
|
|
this.proceed(callId, settings);
|
|
})();
|
|
}
|
|
|
|
private proceed(callId: CallId, settings: CallSettings): void {
|
|
silly_deadlock_protection(() => {
|
|
this.callManager.proceed(
|
|
callId,
|
|
settings.iceServer.username || '',
|
|
settings.iceServer.password || '',
|
|
settings.iceServer.urls,
|
|
settings.hideIp,
|
|
settings.bandwidthMode,
|
|
settings.audioLevelsIntervalMillis || 0
|
|
);
|
|
});
|
|
}
|
|
|
|
// Called by Rust
|
|
onCallState(remoteUserId: UserId, state: CallState): void {
|
|
const call = this._call;
|
|
if (!call || call.remoteUserId !== remoteUserId) {
|
|
return;
|
|
}
|
|
call.state = state;
|
|
}
|
|
|
|
// Called by Rust
|
|
onCallEnded(
|
|
remoteUserId: UserId,
|
|
callId: CallId,
|
|
reason: CallEndedReason,
|
|
ageSec: number
|
|
) {
|
|
let callInfo = this._callInfoByCallId.get(this.getCallInfoKey(callId));
|
|
const { isVideoCall, receivedAtCounter } = callInfo || {
|
|
isVideoCall: false,
|
|
receivedAtCounter: undefined,
|
|
};
|
|
this._callInfoByCallId.delete(this.getCallInfoKey(callId));
|
|
|
|
const call = this._call;
|
|
if (call && reason == CallEndedReason.ReceivedOfferWithGlare) {
|
|
// The current call is the outgoing call.
|
|
// The ended call is the incoming call.
|
|
// We're the "winner", so ignore the incoming call and keep going with the outgoing call.
|
|
return;
|
|
}
|
|
|
|
if (
|
|
call &&
|
|
(reason === CallEndedReason.Glare || reason === CallEndedReason.ReCall)
|
|
) {
|
|
// The current call is the outgoing call.
|
|
// The ended call is the outgoing call.
|
|
// We're the "loser", so end the outgoing/current call and wait for a new incoming call.
|
|
// (proceeded down to the code below)
|
|
}
|
|
|
|
// If there is no call or the remoteUserId doesn't match that of the current
|
|
// call, or if one of the "receive offer while already in a call or because
|
|
// it expired" reasons are provided, don't end the current call, because
|
|
// there isn't one for this Ended notification, just update the call history.
|
|
// If the incoming call ends while in the prering state, also immediately
|
|
// update the call history because it is just a replay of messages.
|
|
if (
|
|
!call ||
|
|
call.remoteUserId !== remoteUserId ||
|
|
reason === CallEndedReason.ReceivedOfferWhileActive ||
|
|
reason === CallEndedReason.ReceivedOfferExpired ||
|
|
(call.state === CallState.Prering && call.isIncoming)
|
|
) {
|
|
if (this.handleAutoEndedIncomingCallRequest) {
|
|
this.handleAutoEndedIncomingCallRequest(
|
|
callId,
|
|
remoteUserId,
|
|
reason,
|
|
ageSec,
|
|
isVideoCall,
|
|
receivedAtCounter
|
|
);
|
|
}
|
|
|
|
if (call && call.state === CallState.Prering && call.isIncoming) {
|
|
// Set the state to Ended without triggering a state update since we
|
|
// already notified the client.
|
|
call.endedReason = reason;
|
|
call.setCallEnded();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Send the end reason first because setting the state triggers
|
|
// call.handleStateChanged, which may look at call.endedReason.
|
|
call.endedReason = reason;
|
|
call.state = CallState.Ended;
|
|
}
|
|
|
|
onRemoteVideoEnabled(remoteUserId: UserId, enabled: boolean): void {
|
|
const call = this._call;
|
|
if (!call || call.remoteUserId !== remoteUserId) {
|
|
return;
|
|
}
|
|
|
|
call.remoteVideoEnabled = enabled;
|
|
if (call.handleRemoteVideoEnabled) {
|
|
call.handleRemoteVideoEnabled();
|
|
}
|
|
}
|
|
|
|
onRemoteSharingScreen(remoteUserId: UserId, enabled: boolean): void {
|
|
const call = this._call;
|
|
if (!call || call.remoteUserId !== remoteUserId) {
|
|
return;
|
|
}
|
|
|
|
call.remoteSharingScreen = enabled;
|
|
if (call.handleRemoteSharingScreen) {
|
|
call.handleRemoteSharingScreen();
|
|
}
|
|
}
|
|
|
|
onNetworkRouteChanged(
|
|
remoteUserId: UserId,
|
|
localNetworkAdapterType: NetworkAdapterType
|
|
): void {
|
|
const call = this._call;
|
|
if (!call || call.remoteUserId !== remoteUserId) {
|
|
return;
|
|
}
|
|
|
|
call.networkRoute.localAdapterType = localNetworkAdapterType;
|
|
if (call.handleNetworkRouteChanged) {
|
|
call.handleNetworkRouteChanged();
|
|
}
|
|
}
|
|
|
|
onAudioLevels(
|
|
remoteUserId: UserId,
|
|
capturedLevel: RawAudioLevel,
|
|
receivedLevel: RawAudioLevel
|
|
): void {
|
|
const call = this._call;
|
|
if (!call || call.remoteUserId !== remoteUserId) {
|
|
return;
|
|
}
|
|
|
|
call.outgoingAudioLevel = normalizeAudioLevel(capturedLevel);
|
|
call.remoteAudioLevel = normalizeAudioLevel(receivedLevel);
|
|
if (call.handleAudioLevels) {
|
|
call.handleAudioLevels();
|
|
}
|
|
}
|
|
|
|
renderVideoFrame(width: number, height: number, buffer: Buffer): void {
|
|
const call = this._call;
|
|
if (!call) {
|
|
return;
|
|
}
|
|
|
|
if (!!this._call?.renderVideoFrame) {
|
|
this._call?.renderVideoFrame(width, height, buffer);
|
|
}
|
|
}
|
|
|
|
// Called by Rust
|
|
onSendOffer(
|
|
remoteUserId: UserId,
|
|
remoteDeviceId: DeviceId,
|
|
callId: CallId,
|
|
broadcast: boolean,
|
|
offerType: OfferType,
|
|
opaque: Buffer
|
|
): void {
|
|
const message = new CallingMessage();
|
|
message.offer = new OfferMessage();
|
|
message.offer.callId = callId;
|
|
message.offer.type = offerType;
|
|
message.offer.opaque = opaque;
|
|
this.sendSignaling(
|
|
remoteUserId,
|
|
remoteDeviceId,
|
|
callId,
|
|
broadcast,
|
|
message
|
|
);
|
|
}
|
|
|
|
// Called by Rust
|
|
onSendAnswer(
|
|
remoteUserId: UserId,
|
|
remoteDeviceId: DeviceId,
|
|
callId: CallId,
|
|
broadcast: boolean,
|
|
opaque: Buffer
|
|
): void {
|
|
const message = new CallingMessage();
|
|
message.answer = new AnswerMessage();
|
|
message.answer.callId = callId;
|
|
message.answer.opaque = opaque;
|
|
this.sendSignaling(
|
|
remoteUserId,
|
|
remoteDeviceId,
|
|
callId,
|
|
broadcast,
|
|
message
|
|
);
|
|
}
|
|
|
|
// Called by Rust
|
|
onSendIceCandidates(
|
|
remoteUserId: UserId,
|
|
remoteDeviceId: DeviceId,
|
|
callId: CallId,
|
|
broadcast: boolean,
|
|
candidates: Array<Buffer>
|
|
): void {
|
|
const message = new CallingMessage();
|
|
message.iceCandidates = [];
|
|
for (const candidate of candidates) {
|
|
const copy = new IceCandidateMessage();
|
|
copy.callId = callId;
|
|
copy.opaque = candidate;
|
|
message.iceCandidates.push(copy);
|
|
}
|
|
this.sendSignaling(
|
|
remoteUserId,
|
|
remoteDeviceId,
|
|
callId,
|
|
broadcast,
|
|
message
|
|
);
|
|
}
|
|
|
|
// Called by Rust
|
|
onSendHangup(
|
|
remoteUserId: UserId,
|
|
remoteDeviceId: DeviceId,
|
|
callId: CallId,
|
|
broadcast: boolean,
|
|
hangupType: HangupType,
|
|
deviceId: DeviceId | null
|
|
): void {
|
|
const message = new CallingMessage();
|
|
message.hangup = new HangupMessage();
|
|
message.hangup.callId = callId;
|
|
message.hangup.type = hangupType;
|
|
message.hangup.deviceId = deviceId || 0;
|
|
this.sendSignaling(
|
|
remoteUserId,
|
|
remoteDeviceId,
|
|
callId,
|
|
broadcast,
|
|
message
|
|
);
|
|
}
|
|
|
|
// Called by Rust
|
|
onSendBusy(
|
|
remoteUserId: UserId,
|
|
remoteDeviceId: DeviceId,
|
|
callId: CallId,
|
|
broadcast: boolean
|
|
): void {
|
|
const message = new CallingMessage();
|
|
message.busy = new BusyMessage();
|
|
message.busy.callId = callId;
|
|
this.sendSignaling(
|
|
remoteUserId,
|
|
remoteDeviceId,
|
|
callId,
|
|
broadcast,
|
|
message
|
|
);
|
|
}
|
|
|
|
private sendSignaling(
|
|
remoteUserId: UserId,
|
|
remoteDeviceId: DeviceId,
|
|
callId: CallId,
|
|
broadcast: boolean,
|
|
message: CallingMessage
|
|
): void {
|
|
message.supportsMultiRing = true;
|
|
if (!broadcast) {
|
|
message.destinationDeviceId = remoteDeviceId;
|
|
}
|
|
|
|
(async () => {
|
|
if (this.handleOutgoingSignaling) {
|
|
const signalingResult = await this.handleOutgoingSignaling(
|
|
remoteUserId,
|
|
message
|
|
);
|
|
if (signalingResult) {
|
|
this.callManager.signalingMessageSent(callId);
|
|
} else {
|
|
this.callManager.signalingMessageSendFailed(callId);
|
|
}
|
|
} else {
|
|
this.callManager.signalingMessageSendFailed(callId);
|
|
}
|
|
})();
|
|
}
|
|
|
|
receivedHttpResponse(requestId: number, status: number, body: Buffer): void {
|
|
silly_deadlock_protection(() => {
|
|
try {
|
|
this.callManager.receivedHttpResponse(requestId, status, body);
|
|
} catch {
|
|
// We may not have an active connection any more.
|
|
// In which case it doesn't matter
|
|
}
|
|
});
|
|
}
|
|
|
|
httpRequestFailed(requestId: number, debugInfo: string | undefined): void {
|
|
silly_deadlock_protection(() => {
|
|
try {
|
|
this.callManager.httpRequestFailed(requestId, debugInfo);
|
|
} catch {
|
|
// We may not have an active connection any more.
|
|
// In which case it doesn't matter
|
|
}
|
|
});
|
|
}
|
|
|
|
// Group Calls
|
|
|
|
// Called by UX
|
|
getGroupCall(
|
|
groupId: Buffer,
|
|
sfuUrl: string,
|
|
hkdfExtraInfo: Buffer,
|
|
audioLevelsIntervalMillis: number | undefined,
|
|
observer: GroupCallObserver
|
|
): GroupCall | undefined {
|
|
const groupCall = new GroupCall(
|
|
this.callManager,
|
|
groupId,
|
|
sfuUrl,
|
|
hkdfExtraInfo,
|
|
audioLevelsIntervalMillis,
|
|
observer
|
|
);
|
|
|
|
this._groupCallByClientId.set(groupCall.clientId, groupCall);
|
|
|
|
return groupCall;
|
|
}
|
|
|
|
// Called by UX
|
|
// Returns a list of user IDs
|
|
peekGroupCall(
|
|
sfuUrl: string,
|
|
membershipProof: Buffer,
|
|
groupMembers: Array<GroupMemberInfo>
|
|
): Promise<PeekInfo> {
|
|
let [requestId, promise] = this._peekRequests.add();
|
|
// Response comes back via handlePeekResponse
|
|
silly_deadlock_protection(() => {
|
|
this.callManager.peekGroupCall(
|
|
requestId,
|
|
sfuUrl,
|
|
membershipProof,
|
|
groupMembers
|
|
);
|
|
});
|
|
return promise;
|
|
}
|
|
|
|
// Called by Rust
|
|
requestMembershipProof(clientId: GroupCallClientId): void {
|
|
silly_deadlock_protection(() => {
|
|
let groupCall = this._groupCallByClientId.get(clientId);
|
|
if (!groupCall) {
|
|
let error = new Error();
|
|
this.logError('requestMembershipProof(): GroupCall not found in map!');
|
|
return;
|
|
}
|
|
|
|
groupCall.requestMembershipProof();
|
|
});
|
|
}
|
|
|
|
// Called by Rust
|
|
requestGroupMembers(clientId: GroupCallClientId): void {
|
|
silly_deadlock_protection(() => {
|
|
let groupCall = this._groupCallByClientId.get(clientId);
|
|
if (!groupCall) {
|
|
let error = new Error();
|
|
this.logError('requestGroupMembers(): GroupCall not found in map!');
|
|
return;
|
|
}
|
|
|
|
groupCall.requestGroupMembers();
|
|
});
|
|
}
|
|
|
|
// Called by Rust
|
|
handleConnectionStateChanged(
|
|
clientId: GroupCallClientId,
|
|
connectionState: ConnectionState
|
|
): void {
|
|
silly_deadlock_protection(() => {
|
|
let groupCall = this._groupCallByClientId.get(clientId);
|
|
if (!groupCall) {
|
|
let error = new Error();
|
|
this.logError(
|
|
'handleConnectionStateChanged(): GroupCall not found in map!'
|
|
);
|
|
return;
|
|
}
|
|
|
|
groupCall.handleConnectionStateChanged(connectionState);
|
|
});
|
|
}
|
|
|
|
// Called by Rust
|
|
handleJoinStateChanged(
|
|
clientId: GroupCallClientId,
|
|
joinState: JoinState,
|
|
demuxId: number | undefined
|
|
): void {
|
|
silly_deadlock_protection(() => {
|
|
let groupCall = this._groupCallByClientId.get(clientId);
|
|
if (!groupCall) {
|
|
let error = new Error();
|
|
this.logError('handleJoinStateChanged(): GroupCall not found in map!');
|
|
return;
|
|
}
|
|
|
|
groupCall.handleJoinStateChanged(joinState, demuxId);
|
|
});
|
|
}
|
|
|
|
// Called by Rust
|
|
handleNetworkRouteChanged(
|
|
clientId: GroupCallClientId,
|
|
localNetworkAdapterType: NetworkAdapterType
|
|
): void {
|
|
silly_deadlock_protection(() => {
|
|
let groupCall = this._groupCallByClientId.get(clientId);
|
|
if (!groupCall) {
|
|
this.logError(
|
|
'handleNetworkRouteChanged(): GroupCall not found in map!'
|
|
);
|
|
return;
|
|
}
|
|
|
|
groupCall.handleNetworkRouteChanged(localNetworkAdapterType);
|
|
});
|
|
}
|
|
|
|
// Called by Rust
|
|
handleAudioLevels(
|
|
clientId: GroupCallClientId,
|
|
capturedLevel: RawAudioLevel,
|
|
receivedLevels: Array<ReceivedAudioLevel>
|
|
): void {
|
|
silly_deadlock_protection(() => {
|
|
let groupCall = this._groupCallByClientId.get(clientId);
|
|
if (!!groupCall) {
|
|
groupCall.handleAudioLevels(capturedLevel, receivedLevels);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Called by Rust
|
|
handleRemoteDevicesChanged(
|
|
clientId: GroupCallClientId,
|
|
remoteDeviceStates: Array<RemoteDeviceState>
|
|
): void {
|
|
silly_deadlock_protection(() => {
|
|
let groupCall = this._groupCallByClientId.get(clientId);
|
|
if (!groupCall) {
|
|
let error = new Error();
|
|
this.logError(
|
|
'handleRemoteDevicesChanged(): GroupCall not found in map!'
|
|
);
|
|
return;
|
|
}
|
|
|
|
groupCall.handleRemoteDevicesChanged(remoteDeviceStates);
|
|
});
|
|
}
|
|
|
|
// Called by Rust
|
|
handlePeekChanged(clientId: GroupCallClientId, info: PeekInfo): void {
|
|
silly_deadlock_protection(() => {
|
|
let groupCall = this._groupCallByClientId.get(clientId);
|
|
if (!groupCall) {
|
|
let error = new Error();
|
|
this.logError('handlePeekChanged(): GroupCall not found in map!');
|
|
return;
|
|
}
|
|
|
|
groupCall.handlePeekChanged(info);
|
|
});
|
|
}
|
|
|
|
// Called by Rust
|
|
handlePeekResponse(request_id: number, info: PeekInfo): void {
|
|
silly_deadlock_protection(() => {
|
|
if (!this._peekRequests.resolve(request_id, info)) {
|
|
this.logWarn(
|
|
`Invalid request ID for handlePeekResponse: ${request_id}`
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Called by Rust
|
|
handleEnded(clientId: GroupCallClientId, reason: GroupCallEndReason): void {
|
|
silly_deadlock_protection(() => {
|
|
let groupCall = this._groupCallByClientId.get(clientId);
|
|
if (!groupCall) {
|
|
let error = new Error();
|
|
this.logError('handleEnded(): GroupCall not found in map!');
|
|
return;
|
|
}
|
|
|
|
this._groupCallByClientId.delete(clientId);
|
|
|
|
groupCall.handleEnded(reason);
|
|
});
|
|
}
|
|
|
|
// Called by Rust
|
|
groupCallRingUpdate(
|
|
groupId: GroupId,
|
|
ringIdString: string,
|
|
sender: GroupCallUserId,
|
|
state: RingUpdate
|
|
): void {
|
|
silly_deadlock_protection(() => {
|
|
if (this.handleGroupCallRingUpdate) {
|
|
const ringId = BigInt(ringIdString);
|
|
this.handleGroupCallRingUpdate(groupId, ringId, sender, state);
|
|
} else {
|
|
this.logError('RingRTC.handleGroupCallRingUpdate is not set!');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Called by Rust
|
|
onLogMessage(
|
|
level: number,
|
|
fileName: string,
|
|
line: number,
|
|
message: string
|
|
): void {
|
|
if (this.handleLogMessage) {
|
|
this.handleLogMessage(level, fileName, line, message);
|
|
}
|
|
}
|
|
|
|
// Called from here
|
|
logError(message: string) {
|
|
this.onLogMessage(CallLogLevel.Error, 'Service.ts', 0, message);
|
|
}
|
|
|
|
// Called from here
|
|
logWarn(message: string) {
|
|
this.onLogMessage(CallLogLevel.Warn, 'Service.ts', 0, message);
|
|
}
|
|
|
|
// Called from here
|
|
logInfo(message: string) {
|
|
this.onLogMessage(CallLogLevel.Info, 'Service.ts', 0, message);
|
|
}
|
|
|
|
// Called by MessageReceiver
|
|
// tslint:disable-next-line cyclomatic-complexity
|
|
handleCallingMessage(
|
|
remoteUserId: UserId,
|
|
remoteUuid: Buffer | null,
|
|
remoteDeviceId: DeviceId,
|
|
localDeviceId: DeviceId,
|
|
messageAgeSec: number,
|
|
messageReceivedAtCounter: number,
|
|
message: CallingMessage,
|
|
senderIdentityKey: Buffer,
|
|
receiverIdentityKey: Buffer
|
|
): void {
|
|
if (
|
|
message.destinationDeviceId &&
|
|
message.destinationDeviceId !== localDeviceId
|
|
) {
|
|
// Drop the message as it isn't for this device, handleIgnoredCall() is not needed.
|
|
return;
|
|
}
|
|
|
|
if (message.offer && message.offer.callId) {
|
|
const callId = message.offer.callId;
|
|
const opaque = to_buffer(message.offer.opaque);
|
|
|
|
// opaque is required. sdp is obsolete, but it might still come with opaque.
|
|
if (!opaque) {
|
|
// TODO: Remove once the proto is updated to only support opaque and require it.
|
|
this.logError(
|
|
'handleCallingMessage(): opaque not received for offer, remote should update'
|
|
);
|
|
return;
|
|
}
|
|
|
|
const offerType = message.offer.type || OfferType.AudioCall;
|
|
|
|
// Save the call details for later when the call is ended.
|
|
let callInfo = new CallInfo(
|
|
offerType === OfferType.VideoCall,
|
|
messageReceivedAtCounter
|
|
);
|
|
this._callInfoByCallId.set(this.getCallInfoKey(callId), callInfo);
|
|
|
|
this.callManager.receivedOffer(
|
|
remoteUserId,
|
|
remoteDeviceId,
|
|
localDeviceId,
|
|
messageAgeSec,
|
|
callId,
|
|
offerType,
|
|
opaque,
|
|
senderIdentityKey,
|
|
receiverIdentityKey
|
|
);
|
|
}
|
|
if (message.answer && message.answer.callId) {
|
|
const callId = message.answer.callId;
|
|
const opaque = to_buffer(message.answer.opaque);
|
|
|
|
// opaque is required. sdp is obsolete, but it might still come with opaque.
|
|
if (!opaque) {
|
|
// TODO: Remove once the proto is updated to only support opaque and require it.
|
|
this.logError(
|
|
'handleCallingMessage(): opaque not received for answer, remote should update'
|
|
);
|
|
return;
|
|
}
|
|
|
|
this.callManager.receivedAnswer(
|
|
remoteUserId,
|
|
remoteDeviceId,
|
|
callId,
|
|
opaque,
|
|
senderIdentityKey,
|
|
receiverIdentityKey
|
|
);
|
|
}
|
|
if (message.iceCandidates && message.iceCandidates.length > 0) {
|
|
// We assume they all have the same .callId
|
|
let callId = message.iceCandidates[0].callId;
|
|
// We have to copy them to do the .toArrayBuffer() thing.
|
|
const candidates: Array<Buffer> = [];
|
|
for (const candidate of message.iceCandidates) {
|
|
const copy = to_buffer(candidate.opaque);
|
|
if (copy) {
|
|
candidates.push(copy);
|
|
} else {
|
|
// TODO: Remove once the proto is updated to only support opaque and require it.
|
|
this.logError(
|
|
'handleCallingMessage(): opaque not received for ice candidate, remote should update'
|
|
);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (candidates.length == 0) {
|
|
this.logWarn(
|
|
'handleCallingMessage(): No ice candidates in ice message, remote should update'
|
|
);
|
|
return;
|
|
}
|
|
|
|
this.callManager.receivedIceCandidates(
|
|
remoteUserId,
|
|
remoteDeviceId,
|
|
callId,
|
|
candidates
|
|
);
|
|
}
|
|
if (message.hangup && message.hangup.callId) {
|
|
const callId = message.hangup.callId;
|
|
const hangupType = message.hangup.type || HangupType.Normal;
|
|
const hangupDeviceId = message.hangup.deviceId || null;
|
|
this.callManager.receivedHangup(
|
|
remoteUserId,
|
|
remoteDeviceId,
|
|
callId,
|
|
hangupType,
|
|
hangupDeviceId
|
|
);
|
|
}
|
|
if (message.legacyHangup && message.legacyHangup.callId) {
|
|
const callId = message.legacyHangup.callId;
|
|
const hangupType = message.legacyHangup.type || HangupType.Normal;
|
|
const hangupDeviceId = message.legacyHangup.deviceId || null;
|
|
this.callManager.receivedHangup(
|
|
remoteUserId,
|
|
remoteDeviceId,
|
|
callId,
|
|
hangupType,
|
|
hangupDeviceId
|
|
);
|
|
}
|
|
if (message.busy && message.busy.callId) {
|
|
const callId = message.busy.callId;
|
|
this.callManager.receivedBusy(remoteUserId, remoteDeviceId, callId);
|
|
}
|
|
if (message.opaque) {
|
|
if (remoteUuid == null) {
|
|
this.logError(
|
|
'handleCallingMessage(): opaque message received without UUID!'
|
|
);
|
|
return;
|
|
}
|
|
const data = to_buffer(message.opaque.data);
|
|
if (data == undefined) {
|
|
this.logError(
|
|
'handleCallingMessage(): opaque message received without data!'
|
|
);
|
|
return;
|
|
}
|
|
this.callManager.receivedCallMessage(
|
|
remoteUuid,
|
|
remoteDeviceId,
|
|
localDeviceId,
|
|
data,
|
|
messageAgeSec
|
|
);
|
|
}
|
|
}
|
|
|
|
// Called by Rust
|
|
sendHttpRequest(
|
|
requestId: number,
|
|
url: string,
|
|
method: HttpMethod,
|
|
headers: { [name: string]: string },
|
|
body: Buffer | undefined
|
|
) {
|
|
if (this.handleSendHttpRequest) {
|
|
this.handleSendHttpRequest(requestId, url, method, headers, body);
|
|
} else {
|
|
this.logError('RingRTC.handleSendHttpRequest is not set!');
|
|
}
|
|
}
|
|
|
|
// Called by Rust
|
|
sendCallMessage(
|
|
recipientUuid: Buffer,
|
|
message: Buffer,
|
|
urgency: CallMessageUrgency
|
|
): void {
|
|
if (this.handleSendCallMessage) {
|
|
this.handleSendCallMessage(recipientUuid, message, urgency);
|
|
} else {
|
|
this.logError('RingRTC.handleSendCallMessage is not set!');
|
|
}
|
|
}
|
|
|
|
// Called by Rust
|
|
sendCallMessageToGroup(
|
|
groupId: Buffer,
|
|
message: Buffer,
|
|
urgency: CallMessageUrgency
|
|
): void {
|
|
if (this.handleSendCallMessageToGroup) {
|
|
this.handleSendCallMessageToGroup(groupId, message, urgency);
|
|
} else {
|
|
this.logError('RingRTC.handleSendCallMessageToGroup is not set!');
|
|
}
|
|
}
|
|
|
|
// These are convenience methods. One could use the Call class instead.
|
|
get call(): Call | null {
|
|
return this._call;
|
|
}
|
|
|
|
getCall(callId: CallId): Call | null {
|
|
const { call } = this;
|
|
|
|
if (
|
|
call &&
|
|
call.callId.high === callId.high &&
|
|
call.callId.low === call.callId.low
|
|
) {
|
|
return call;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
accept(callId: CallId, asVideoCall: boolean) {
|
|
const call = this.getCall(callId);
|
|
if (!call) {
|
|
return;
|
|
}
|
|
|
|
call.accept();
|
|
call.outgoingAudioEnabled = true;
|
|
call.outgoingVideoEnabled = asVideoCall;
|
|
}
|
|
|
|
decline(callId: CallId) {
|
|
const call = this.getCall(callId);
|
|
if (!call) {
|
|
return;
|
|
}
|
|
|
|
call.decline();
|
|
}
|
|
|
|
ignore(callId: CallId) {
|
|
const call = this.getCall(callId);
|
|
if (!call) {
|
|
return;
|
|
}
|
|
|
|
call.ignore();
|
|
}
|
|
|
|
hangup(callId: CallId) {
|
|
const call = this.getCall(callId);
|
|
if (!call) {
|
|
return;
|
|
}
|
|
|
|
call.hangup();
|
|
}
|
|
|
|
setOutgoingAudio(callId: CallId, enabled: boolean) {
|
|
const call = this.getCall(callId);
|
|
if (!call) {
|
|
return;
|
|
}
|
|
|
|
call.outgoingAudioEnabled = enabled;
|
|
}
|
|
|
|
setOutgoingVideo(callId: CallId, enabled: boolean) {
|
|
const call = this.getCall(callId);
|
|
if (!call) {
|
|
return;
|
|
}
|
|
|
|
call.outgoingVideoEnabled = enabled;
|
|
}
|
|
|
|
setOutgoingVideoIsScreenShare(callId: CallId, isScreenShare: boolean) {
|
|
const call = this.getCall(callId);
|
|
if (!call) {
|
|
return;
|
|
}
|
|
|
|
call.outgoingVideoIsScreenShare = isScreenShare;
|
|
}
|
|
|
|
setVideoCapturer(callId: CallId, capturer: VideoCapturer | null) {
|
|
const call = this.getCall(callId);
|
|
if (!call) {
|
|
return;
|
|
}
|
|
|
|
call.videoCapturer = capturer;
|
|
}
|
|
|
|
setVideoRenderer(callId: CallId, renderer: VideoRenderer | null) {
|
|
const call = this.getCall(callId);
|
|
if (!call) {
|
|
return;
|
|
}
|
|
|
|
call.videoRenderer = renderer;
|
|
}
|
|
|
|
getAudioInputs(): AudioDevice[] {
|
|
return this.callManager.getAudioInputs();
|
|
}
|
|
|
|
setAudioInput(index: number): void {
|
|
this.callManager.setAudioInput(index);
|
|
}
|
|
|
|
getAudioOutputs(): AudioDevice[] {
|
|
return this.callManager.getAudioOutputs();
|
|
}
|
|
|
|
setAudioOutput(index: number): void {
|
|
this.callManager.setAudioOutput(index);
|
|
}
|
|
}
|
|
|
|
export interface CallSettings {
|
|
iceServer: IceServer;
|
|
hideIp: boolean;
|
|
bandwidthMode: BandwidthMode;
|
|
audioLevelsIntervalMillis?: number;
|
|
}
|
|
|
|
interface IceServer {
|
|
username?: string;
|
|
password?: string;
|
|
urls: Array<string>;
|
|
}
|
|
|
|
// Describes an audio input or output device.
|
|
export interface AudioDevice {
|
|
// Device name.
|
|
name: string;
|
|
// Index of this device, starting from 0.
|
|
index: number;
|
|
// A unique and somewhat stable identifier of this device.
|
|
uniqueId: string;
|
|
// If present, the identifier of a localized string to substitute for the device name.
|
|
i18nKey?: string;
|
|
}
|
|
|
|
export interface VideoCapturer {
|
|
enableCapture(): void;
|
|
enableCaptureAndSend(
|
|
call: Call,
|
|
captureOptions?: GumVideoCaptureOptions
|
|
): void;
|
|
disable(): void;
|
|
}
|
|
|
|
export interface VideoRenderer {
|
|
enable(call: Call): void;
|
|
disable(): void;
|
|
}
|
|
export class Call {
|
|
// The calls' info and state.
|
|
private readonly _callManager: CallManager;
|
|
private readonly _remoteUserId: UserId;
|
|
// We can have a null CallId while we're waiting for RingRTC to give us one.
|
|
callId: CallId;
|
|
private readonly _isIncoming: boolean;
|
|
private readonly _isVideoCall: boolean;
|
|
// We can have a null CallSettings while we're waiting for the UX to give us one.
|
|
settings: CallSettings | null;
|
|
private _state: CallState;
|
|
private _outgoingAudioEnabled: boolean = false;
|
|
private _outgoingVideoEnabled: boolean = false;
|
|
private _outgoingVideoIsScreenShare: boolean = false;
|
|
private _remoteVideoEnabled: boolean = false;
|
|
outgoingAudioLevel: NormalizedAudioLevel = 0;
|
|
remoteAudioLevel: NormalizedAudioLevel = 0;
|
|
remoteSharingScreen: boolean = false;
|
|
networkRoute: NetworkRoute = new NetworkRoute();
|
|
private _videoCapturer: VideoCapturer | null = null;
|
|
private _videoRenderer: VideoRenderer | null = null;
|
|
endedReason?: CallEndedReason;
|
|
|
|
// These callbacks should be set by the UX code.
|
|
handleStateChanged?: () => void;
|
|
handleRemoteVideoEnabled?: () => void;
|
|
handleRemoteSharingScreen?: () => void;
|
|
handleNetworkRouteChanged?: () => void;
|
|
handleAudioLevels?: () => void;
|
|
|
|
// This callback should be set by the VideoCapturer,
|
|
// But could also be set by the UX.
|
|
renderVideoFrame?: (width: number, height: number, buffer: Buffer) => void;
|
|
|
|
constructor(
|
|
callManager: CallManager,
|
|
remoteUserId: UserId,
|
|
callId: CallId,
|
|
isIncoming: boolean,
|
|
isVideoCall: boolean,
|
|
settings: CallSettings | null,
|
|
state: CallState
|
|
) {
|
|
this._callManager = callManager;
|
|
this._remoteUserId = remoteUserId;
|
|
this.callId = callId;
|
|
this._isIncoming = isIncoming;
|
|
this._isVideoCall = isVideoCall;
|
|
this.settings = settings;
|
|
this._state = state;
|
|
}
|
|
|
|
get remoteUserId(): UserId {
|
|
return this._remoteUserId;
|
|
}
|
|
|
|
get isIncoming(): boolean {
|
|
return this._isIncoming;
|
|
}
|
|
|
|
get isVideoCall(): boolean {
|
|
return this._isVideoCall;
|
|
}
|
|
|
|
get state(): CallState {
|
|
return this._state;
|
|
}
|
|
|
|
set state(state: CallState) {
|
|
if (state == this._state) {
|
|
return;
|
|
}
|
|
this._state = state;
|
|
this.enableOrDisableCapturer();
|
|
this.enableOrDisableRenderer();
|
|
if (!!this.handleStateChanged) {
|
|
this.handleStateChanged();
|
|
}
|
|
}
|
|
|
|
setCallEnded() {
|
|
this._state = CallState.Ended;
|
|
}
|
|
|
|
set videoCapturer(capturer: VideoCapturer | null) {
|
|
this._videoCapturer = capturer;
|
|
this.enableOrDisableCapturer();
|
|
}
|
|
|
|
set videoRenderer(renderer: VideoRenderer | null) {
|
|
this._videoRenderer = renderer;
|
|
this.enableOrDisableRenderer();
|
|
}
|
|
|
|
accept(): void {
|
|
this._callManager.accept(this.callId);
|
|
}
|
|
|
|
decline(): void {
|
|
this.hangup();
|
|
}
|
|
|
|
ignore(): void {
|
|
this._callManager.ignore(this.callId);
|
|
}
|
|
|
|
hangup(): void {
|
|
// This is a little faster than waiting for the
|
|
// change in call state to come back.
|
|
if (this._videoCapturer) {
|
|
this._videoCapturer.disable();
|
|
}
|
|
if (this._videoRenderer) {
|
|
this._videoRenderer.disable();
|
|
}
|
|
// This assumes we only have one active call.
|
|
silly_deadlock_protection(() => {
|
|
this._callManager.hangup();
|
|
});
|
|
}
|
|
|
|
get outgoingAudioEnabled(): boolean {
|
|
return this._outgoingAudioEnabled;
|
|
}
|
|
|
|
set outgoingAudioEnabled(enabled: boolean) {
|
|
this._outgoingAudioEnabled = enabled;
|
|
// This assumes we only have one active call.
|
|
silly_deadlock_protection(() => {
|
|
this._callManager.setOutgoingAudioEnabled(enabled);
|
|
});
|
|
}
|
|
|
|
get outgoingVideoEnabled(): boolean {
|
|
return this._outgoingVideoEnabled;
|
|
}
|
|
|
|
set outgoingVideoEnabled(enabled: boolean) {
|
|
this._outgoingVideoEnabled = enabled;
|
|
this.enableOrDisableCapturer();
|
|
}
|
|
|
|
set outgoingVideoIsScreenShare(isScreenShare: boolean) {
|
|
// This assumes we only have one active call.
|
|
this._outgoingVideoIsScreenShare = isScreenShare;
|
|
silly_deadlock_protection(() => {
|
|
this._callManager.setOutgoingVideoIsScreenShare(isScreenShare);
|
|
});
|
|
}
|
|
|
|
get remoteVideoEnabled(): boolean {
|
|
return this._remoteVideoEnabled;
|
|
}
|
|
|
|
set remoteVideoEnabled(enabled: boolean) {
|
|
this._remoteVideoEnabled = enabled;
|
|
this.enableOrDisableRenderer();
|
|
}
|
|
|
|
// With this method, a Call is a VideoFrameSender
|
|
sendVideoFrame(
|
|
width: number,
|
|
height: number,
|
|
format: VideoPixelFormatEnum,
|
|
buffer: Buffer
|
|
): void {
|
|
// This assumes we only have one active all.
|
|
this._callManager.sendVideoFrame(width, height, format, buffer);
|
|
}
|
|
|
|
// With this method, a Call is a VideoFrameSource
|
|
receiveVideoFrame(buffer: Buffer): [number, number] | undefined {
|
|
// This assumes we only have one active all.
|
|
return this._callManager.receiveVideoFrame(buffer);
|
|
}
|
|
|
|
private enableOrDisableCapturer(): void {
|
|
if (!this._videoCapturer) {
|
|
return;
|
|
}
|
|
if (!this.outgoingVideoEnabled) {
|
|
this._videoCapturer.disable();
|
|
if (this.state === CallState.Accepted) {
|
|
this.setOutgoingVideoEnabled(false);
|
|
}
|
|
return;
|
|
}
|
|
switch (this.state) {
|
|
case CallState.Prering:
|
|
case CallState.Ringing:
|
|
this._videoCapturer.enableCapture();
|
|
break;
|
|
case CallState.Accepted:
|
|
this._videoCapturer.enableCaptureAndSend(this);
|
|
this.setOutgoingVideoEnabled(true);
|
|
if (this._outgoingVideoIsScreenShare) {
|
|
// Make sure the status gets sent.
|
|
this.outgoingVideoIsScreenShare = true;
|
|
}
|
|
break;
|
|
case CallState.Reconnecting:
|
|
this._videoCapturer.enableCaptureAndSend(this);
|
|
// Don't send status until we're reconnected.
|
|
break;
|
|
case CallState.Ended:
|
|
this._videoCapturer.disable();
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
|
|
private setOutgoingVideoEnabled(enabled: boolean) {
|
|
silly_deadlock_protection(() => {
|
|
try {
|
|
this._callManager.setOutgoingVideoEnabled(enabled);
|
|
} catch {
|
|
// We may not have an active connection any more.
|
|
// In which case it doesn't matter
|
|
}
|
|
});
|
|
}
|
|
|
|
updateBandwidthMode(bandwidthMode: BandwidthMode) {
|
|
silly_deadlock_protection(() => {
|
|
try {
|
|
this._callManager.updateBandwidthMode(bandwidthMode);
|
|
} catch {
|
|
// We may not have an active connection any more.
|
|
// In which case it doesn't matter
|
|
}
|
|
});
|
|
}
|
|
|
|
private enableOrDisableRenderer(): void {
|
|
if (!this._videoRenderer) {
|
|
return;
|
|
}
|
|
if (!this.remoteVideoEnabled) {
|
|
this._videoRenderer.disable();
|
|
return;
|
|
}
|
|
switch (this.state) {
|
|
case CallState.Prering:
|
|
case CallState.Ringing:
|
|
this._videoRenderer.disable();
|
|
break;
|
|
case CallState.Accepted:
|
|
case CallState.Reconnecting:
|
|
this._videoRenderer.enable(this);
|
|
break;
|
|
case CallState.Ended:
|
|
this._videoRenderer.disable();
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
// Group Calls
|
|
|
|
export type GroupCallClientId = number;
|
|
|
|
// Represents the connection state to a media server for a group call.
|
|
export enum ConnectionState {
|
|
NotConnected = 0,
|
|
Connecting = 1,
|
|
Connected = 2,
|
|
Reconnecting = 3,
|
|
}
|
|
|
|
// Represents whether or not a user is joined to a group call and can exchange media.
|
|
export enum JoinState {
|
|
NotJoined = 0,
|
|
Joining = 1,
|
|
Joined = 2,
|
|
}
|
|
|
|
// If not ended purposely by the user, gives the reason why a group call ended.
|
|
export enum GroupCallEndReason {
|
|
// Normal events
|
|
DeviceExplicitlyDisconnected = 0,
|
|
ServerExplicitlyDisconnected = 1,
|
|
|
|
// Things that can go wrong
|
|
CallManagerIsBusy = 2,
|
|
SfuClientFailedToJoin = 3,
|
|
FailedToCreatePeerConnectionFactory = 4,
|
|
FailedToNegotiateSrtpKeys = 5,
|
|
FailedToCreatePeerConnection = 6,
|
|
FailedToStartPeerConnection = 7,
|
|
FailedToUpdatePeerConnection = 8,
|
|
FailedToSetMaxSendBitrate = 9,
|
|
IceFailedWhileConnecting = 10,
|
|
IceFailedAfterConnected = 11,
|
|
ServerChangedDemuxId = 12,
|
|
HasMaxDevices = 13,
|
|
}
|
|
|
|
export enum CallMessageUrgency {
|
|
Droppable = 0,
|
|
HandleImmediately,
|
|
}
|
|
|
|
export enum RingUpdate {
|
|
/// The sender is trying to ring this user.
|
|
Requested = 0,
|
|
/// The sender tried to ring this user, but it's been too long.
|
|
ExpiredRequest,
|
|
/// Call was accepted elsewhere by a different device.
|
|
AcceptedOnAnotherDevice,
|
|
/// Call was declined elsewhere by a different device.
|
|
DeclinedOnAnotherDevice,
|
|
/// This device is currently on a different call.
|
|
BusyLocally,
|
|
/// A different device is currently on a different call.
|
|
BusyOnAnotherDevice,
|
|
/// The sender cancelled the ring request.
|
|
CancelledByRinger,
|
|
}
|
|
|
|
// HTTP request methods.
|
|
export enum HttpMethod {
|
|
Get = 0,
|
|
Put = 1,
|
|
Post = 2,
|
|
Delete = 3,
|
|
}
|
|
|
|
// The local device state for a group call.
|
|
export class LocalDeviceState {
|
|
connectionState: ConnectionState;
|
|
joinState: JoinState;
|
|
// Set after joined
|
|
demuxId?: number;
|
|
audioMuted: boolean;
|
|
videoMuted: boolean;
|
|
audioLevel: NormalizedAudioLevel;
|
|
presenting: boolean;
|
|
sharingScreen: boolean;
|
|
networkRoute: NetworkRoute;
|
|
|
|
constructor() {
|
|
this.connectionState = ConnectionState.NotConnected;
|
|
this.joinState = JoinState.NotJoined;
|
|
// By default audio and video are muted.
|
|
this.audioMuted = true;
|
|
this.videoMuted = true;
|
|
this.audioLevel = 0;
|
|
this.presenting = false;
|
|
this.sharingScreen = false;
|
|
this.networkRoute = new NetworkRoute();
|
|
}
|
|
}
|
|
|
|
// All remote devices in a group call and their associated state.
|
|
export class RemoteDeviceState {
|
|
demuxId: number; // UInt32
|
|
userId: Buffer;
|
|
mediaKeysReceived: boolean;
|
|
|
|
audioMuted: boolean | undefined;
|
|
videoMuted: boolean | undefined;
|
|
audioLevel: NormalizedAudioLevel;
|
|
presenting: boolean | undefined;
|
|
sharingScreen: boolean | undefined;
|
|
videoAspectRatio: number | undefined; // Float
|
|
addedTime: string | undefined; // unix millis (to be converted to a numeric type)
|
|
speakerTime: string | undefined; // unix millis; 0 if they've never spoken (to be converted to a numeric type)
|
|
forwardingVideo: boolean | undefined;
|
|
isHigherResolutionPending: boolean;
|
|
|
|
constructor(demuxId: number, userId: Buffer, mediaKeysReceived: boolean) {
|
|
this.demuxId = demuxId;
|
|
this.userId = userId;
|
|
this.mediaKeysReceived = mediaKeysReceived;
|
|
this.audioLevel = 0;
|
|
this.isHigherResolutionPending = false;
|
|
}
|
|
}
|
|
|
|
// Used to communicate the group membership to RingRTC for a group call.
|
|
export class GroupMemberInfo {
|
|
userId: Buffer;
|
|
userIdCipherText: Buffer;
|
|
|
|
constructor(userId: Buffer, userIdCipherText: Buffer) {
|
|
this.userId = userId;
|
|
this.userIdCipherText = userIdCipherText;
|
|
}
|
|
}
|
|
|
|
// Used for the application to communicate the actual resolutions of
|
|
// each device in a group call to RingRTC and the SFU.
|
|
export class VideoRequest {
|
|
demuxId: number; // UInt32
|
|
width: number; // UInt16
|
|
height: number; // UInt16
|
|
framerate: number | undefined; // UInt16
|
|
|
|
constructor(
|
|
demuxId: number,
|
|
width: number,
|
|
height: number,
|
|
framerate: number | undefined
|
|
) {
|
|
this.demuxId = demuxId;
|
|
this.width = width;
|
|
this.height = height;
|
|
this.framerate = framerate;
|
|
}
|
|
}
|
|
|
|
export interface GroupCallObserver {
|
|
requestMembershipProof(groupCall: GroupCall): void;
|
|
requestGroupMembers(groupCall: GroupCall): void;
|
|
onLocalDeviceStateChanged(groupCall: GroupCall): void;
|
|
onRemoteDeviceStatesChanged(groupCall: GroupCall): void;
|
|
onAudioLevels(groupCall: GroupCall): void;
|
|
onPeekChanged(groupCall: GroupCall): void;
|
|
onEnded(groupCall: GroupCall, reason: GroupCallEndReason): void;
|
|
}
|
|
|
|
export class GroupCall {
|
|
private readonly _callManager: CallManager;
|
|
private readonly _observer: GroupCallObserver;
|
|
|
|
private readonly _clientId: GroupCallClientId;
|
|
public get clientId(): GroupCallClientId {
|
|
return this._clientId;
|
|
}
|
|
|
|
private _localDeviceState: LocalDeviceState;
|
|
private _remoteDeviceStates: Array<RemoteDeviceState> | undefined;
|
|
|
|
private _peekInfo: PeekInfo | undefined; // uuid
|
|
|
|
// Called by UI via RingRTC object
|
|
constructor(
|
|
callManager: CallManager,
|
|
groupId: Buffer,
|
|
sfuUrl: string,
|
|
hkdfExtraInfo: Buffer,
|
|
audioLevelsIntervalMillis: number | undefined,
|
|
observer: GroupCallObserver
|
|
) {
|
|
this._callManager = callManager;
|
|
this._observer = observer;
|
|
|
|
this._localDeviceState = new LocalDeviceState();
|
|
|
|
this._clientId = this._callManager.createGroupCallClient(
|
|
groupId,
|
|
sfuUrl,
|
|
hkdfExtraInfo,
|
|
audioLevelsIntervalMillis || 0
|
|
);
|
|
}
|
|
|
|
// Called by UI
|
|
connect(): void {
|
|
this._callManager.connect(this._clientId);
|
|
}
|
|
|
|
// Called by UI
|
|
join(): void {
|
|
this._callManager.join(this._clientId);
|
|
}
|
|
|
|
// Called by UI
|
|
leave(): void {
|
|
this._callManager.leave(this._clientId);
|
|
}
|
|
|
|
// Called by UI
|
|
disconnect(): void {
|
|
this._callManager.disconnect(this._clientId);
|
|
}
|
|
|
|
// Called by UI
|
|
getLocalDeviceState(): LocalDeviceState {
|
|
return this._localDeviceState;
|
|
}
|
|
|
|
// Called by UI
|
|
getRemoteDeviceStates(): Array<RemoteDeviceState> | undefined {
|
|
return this._remoteDeviceStates;
|
|
}
|
|
|
|
// Called by UI
|
|
getPeekInfo(): PeekInfo | undefined {
|
|
return this._peekInfo;
|
|
}
|
|
|
|
// Called by UI
|
|
setOutgoingAudioMuted(muted: boolean): void {
|
|
this._localDeviceState.audioMuted = muted;
|
|
this._callManager.setOutgoingAudioMuted(this._clientId, muted);
|
|
this._observer.onLocalDeviceStateChanged(this);
|
|
}
|
|
|
|
// Called by UI
|
|
setOutgoingVideoMuted(muted: boolean): void {
|
|
this._localDeviceState.videoMuted = muted;
|
|
this._callManager.setOutgoingVideoMuted(this._clientId, muted);
|
|
this._observer.onLocalDeviceStateChanged(this);
|
|
}
|
|
|
|
// Called by UI
|
|
setPresenting(presenting: boolean): void {
|
|
this._localDeviceState.presenting = presenting;
|
|
this._callManager.setPresenting(this._clientId, presenting);
|
|
this._observer.onLocalDeviceStateChanged(this);
|
|
}
|
|
|
|
// Called by UI
|
|
setOutgoingVideoIsScreenShare(isScreenShare: boolean): void {
|
|
this._localDeviceState.sharingScreen = isScreenShare;
|
|
this._callManager.setOutgoingGroupCallVideoIsScreenShare(
|
|
this._clientId,
|
|
isScreenShare
|
|
);
|
|
this._observer.onLocalDeviceStateChanged(this);
|
|
}
|
|
|
|
// Called by UI
|
|
ringAll(): void {
|
|
this._callManager.groupRing(this._clientId, undefined);
|
|
}
|
|
|
|
// Called by UI
|
|
resendMediaKeys(): void {
|
|
this._callManager.resendMediaKeys(this._clientId);
|
|
}
|
|
|
|
// Called by UI
|
|
setBandwidthMode(bandwidthMode: BandwidthMode): void {
|
|
this._callManager.setBandwidthMode(this._clientId, bandwidthMode);
|
|
}
|
|
|
|
// Called by UI
|
|
requestVideo(resolutions: Array<VideoRequest>, activeSpeakerHeight: number): void {
|
|
this._callManager.requestVideo(this._clientId, resolutions, activeSpeakerHeight);
|
|
}
|
|
|
|
// Called by UI
|
|
setGroupMembers(members: Array<GroupMemberInfo>): void {
|
|
this._callManager.setGroupMembers(this._clientId, members);
|
|
}
|
|
|
|
// Called by UI
|
|
setMembershipProof(proof: Buffer): void {
|
|
this._callManager.setMembershipProof(this._clientId, proof);
|
|
}
|
|
|
|
// Called by Rust via RingRTC object
|
|
requestMembershipProof(): void {
|
|
this._observer.requestMembershipProof(this);
|
|
}
|
|
|
|
// Called by Rust via RingRTC object
|
|
requestGroupMembers(): void {
|
|
this._observer.requestGroupMembers(this);
|
|
}
|
|
|
|
// Called by Rust via RingRTC object
|
|
handleConnectionStateChanged(connectionState: ConnectionState): void {
|
|
this._localDeviceState.connectionState = connectionState;
|
|
|
|
this._observer.onLocalDeviceStateChanged(this);
|
|
}
|
|
|
|
// Called by Rust via RingRTC object
|
|
handleJoinStateChanged(
|
|
joinState: JoinState,
|
|
demuxId: number | undefined
|
|
): void {
|
|
this._localDeviceState.joinState = joinState;
|
|
|
|
// Don't set to undefined after we leave so we can still know the demuxId after we leave.
|
|
if (demuxId != undefined) {
|
|
this._localDeviceState.demuxId = demuxId;
|
|
}
|
|
|
|
this._observer.onLocalDeviceStateChanged(this);
|
|
}
|
|
|
|
// Called by Rust via RingRTC object
|
|
handleNetworkRouteChanged(localNetworkAdapterType: NetworkAdapterType): void {
|
|
this._localDeviceState.networkRoute.localAdapterType =
|
|
localNetworkAdapterType;
|
|
|
|
this._observer.onLocalDeviceStateChanged(this);
|
|
}
|
|
|
|
handleAudioLevels(
|
|
capturedLevel: RawAudioLevel,
|
|
receivedLevels: Array<ReceivedAudioLevel>
|
|
) {
|
|
this._localDeviceState.audioLevel = normalizeAudioLevel(capturedLevel);
|
|
if (this._remoteDeviceStates != undefined) {
|
|
for (const received of receivedLevels) {
|
|
for (let remoteDeviceState of this._remoteDeviceStates) {
|
|
if (remoteDeviceState.demuxId == received.demuxId) {
|
|
remoteDeviceState.audioLevel = normalizeAudioLevel(received.level);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this._observer.onAudioLevels(this);
|
|
}
|
|
|
|
// Called by Rust via RingRTC object
|
|
handleRemoteDevicesChanged(
|
|
remoteDeviceStates: Array<RemoteDeviceState>
|
|
): void {
|
|
// We don't get aspect ratios from RingRTC, so make sure to copy them over.
|
|
for (const noo of remoteDeviceStates) {
|
|
const old = this._remoteDeviceStates?.find(
|
|
old => old.demuxId == noo.demuxId
|
|
);
|
|
noo.videoAspectRatio = old?.videoAspectRatio;
|
|
}
|
|
|
|
this._remoteDeviceStates = remoteDeviceStates;
|
|
|
|
this._observer.onRemoteDeviceStatesChanged(this);
|
|
}
|
|
|
|
// Called by Rust via RingRTC object
|
|
handlePeekChanged(info: PeekInfo): void {
|
|
this._peekInfo = info;
|
|
|
|
this._observer.onPeekChanged(this);
|
|
}
|
|
|
|
// Called by Rust via RingRTC object
|
|
handleEnded(reason: GroupCallEndReason): void {
|
|
this._observer.onEnded(this, reason);
|
|
|
|
this._callManager.deleteGroupCallClient(this._clientId);
|
|
}
|
|
|
|
// With this, a GroupCall is a VideoFrameSender
|
|
sendVideoFrame(
|
|
width: number,
|
|
height: number,
|
|
format: VideoPixelFormatEnum,
|
|
buffer: Buffer
|
|
): void {
|
|
// This assumes we only have one active all.
|
|
this._callManager.sendVideoFrame(width, height, format, buffer);
|
|
}
|
|
|
|
// With this, a GroupCall can provide a VideoFrameSource for each remote device.
|
|
getVideoSource(remoteDemuxId: number): GroupCallVideoFrameSource {
|
|
return new GroupCallVideoFrameSource(
|
|
this._callManager,
|
|
this,
|
|
remoteDemuxId
|
|
);
|
|
}
|
|
|
|
// Called by the GroupCallVideoFrameSource when it receives a video frame.
|
|
setRemoteAspectRatio(remoteDemuxId: number, aspectRatio: number) {
|
|
const remoteDevice = this._remoteDeviceStates?.find(
|
|
device => device.demuxId == remoteDemuxId
|
|
);
|
|
if (!!remoteDevice && remoteDevice.videoAspectRatio != aspectRatio) {
|
|
remoteDevice.videoAspectRatio = aspectRatio;
|
|
this._observer.onRemoteDeviceStatesChanged(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Implements VideoSource for use in CanvasVideoRenderer
|
|
class GroupCallVideoFrameSource {
|
|
private readonly _callManager: CallManager;
|
|
private readonly _groupCall: GroupCall;
|
|
private readonly _remoteDemuxId: number; // Uint32
|
|
|
|
constructor(
|
|
callManager: CallManager,
|
|
groupCall: GroupCall,
|
|
remoteDemuxId: number // Uint32
|
|
) {
|
|
this._callManager = callManager;
|
|
this._groupCall = groupCall;
|
|
this._remoteDemuxId = remoteDemuxId;
|
|
}
|
|
|
|
receiveVideoFrame(buffer: Buffer): [number, number] | undefined {
|
|
// This assumes we only have one active all.
|
|
const frame = this._callManager.receiveGroupCallVideoFrame(
|
|
this._groupCall.clientId,
|
|
this._remoteDemuxId,
|
|
buffer
|
|
);
|
|
if (!!frame) {
|
|
const [width, height] = frame;
|
|
this._groupCall.setRemoteAspectRatio(this._remoteDemuxId, width / height);
|
|
}
|
|
return frame;
|
|
}
|
|
}
|
|
|
|
// When sending, we just set an Buffer.
|
|
// When receiving, we call .toArrayBuffer().
|
|
type ProtobufBuffer = Buffer | { toArrayBuffer: () => ArrayBuffer };
|
|
|
|
function to_buffer(pbab: ProtobufBuffer | undefined): Buffer | undefined {
|
|
if (!pbab) {
|
|
return pbab;
|
|
}
|
|
if (pbab instanceof Buffer) {
|
|
return pbab;
|
|
}
|
|
return Buffer.from(pbab.toArrayBuffer());
|
|
}
|
|
|
|
export type UserId = string;
|
|
|
|
export type DeviceId = number;
|
|
|
|
export type CallId = any;
|
|
|
|
export class CallingMessage {
|
|
offer?: OfferMessage;
|
|
answer?: AnswerMessage;
|
|
iceCandidates?: Array<IceCandidateMessage>;
|
|
legacyHangup?: HangupMessage;
|
|
busy?: BusyMessage;
|
|
hangup?: HangupMessage;
|
|
opaque?: OpaqueMessage;
|
|
supportsMultiRing?: boolean;
|
|
destinationDeviceId?: DeviceId;
|
|
}
|
|
|
|
export class OfferMessage {
|
|
callId?: CallId;
|
|
type?: OfferType;
|
|
opaque?: ProtobufBuffer;
|
|
sdp?: string;
|
|
}
|
|
|
|
export enum OfferType {
|
|
AudioCall = 0,
|
|
VideoCall = 1,
|
|
}
|
|
|
|
export class AnswerMessage {
|
|
callId?: CallId;
|
|
opaque?: ProtobufBuffer;
|
|
sdp?: string;
|
|
}
|
|
|
|
export class IceCandidateMessage {
|
|
callId?: CallId;
|
|
mid?: string;
|
|
line?: number;
|
|
opaque?: ProtobufBuffer;
|
|
sdp?: string;
|
|
}
|
|
|
|
export class BusyMessage {
|
|
callId?: CallId;
|
|
}
|
|
|
|
export class HangupMessage {
|
|
callId?: CallId;
|
|
type?: HangupType;
|
|
deviceId?: DeviceId;
|
|
}
|
|
|
|
export class OpaqueMessage {
|
|
data?: ProtobufBuffer;
|
|
}
|
|
|
|
export enum HangupType {
|
|
Normal = 0,
|
|
Accepted = 1,
|
|
Declined = 2,
|
|
Busy = 3,
|
|
NeedPermission = 4,
|
|
}
|
|
|
|
export enum BandwidthMode {
|
|
VeryLow = 0,
|
|
Low = 1,
|
|
Normal = 2,
|
|
}
|
|
|
|
/// Describes why a ring was cancelled.
|
|
export enum RingCancelReason {
|
|
/// The user explicitly clicked "Decline".
|
|
DeclinedByUser = 0,
|
|
/// The device is busy with another call.
|
|
Busy,
|
|
}
|
|
|
|
export interface CallManager {
|
|
setConfig(config: Config): void;
|
|
setSelfUuid(uuid: Buffer): void;
|
|
createOutgoingCall(
|
|
remoteUserId: UserId,
|
|
isVideoCall: boolean,
|
|
localDeviceId: DeviceId
|
|
): CallId;
|
|
proceed(
|
|
callId: CallId,
|
|
iceServerUsername: string,
|
|
iceServerPassword: string,
|
|
iceServerUrls: Array<string>,
|
|
hideIp: boolean,
|
|
bandwidthMode: BandwidthMode,
|
|
audioLevelsIntervalMillis: number
|
|
): void;
|
|
accept(callId: CallId): void;
|
|
ignore(callId: CallId): void;
|
|
hangup(): void;
|
|
cancelGroupRing(
|
|
groupId: GroupId,
|
|
ringId: string,
|
|
reason: RingCancelReason | null
|
|
): void;
|
|
signalingMessageSent(callId: CallId): void;
|
|
signalingMessageSendFailed(callId: CallId): void;
|
|
setOutgoingAudioEnabled(enabled: boolean): void;
|
|
setOutgoingVideoEnabled(enabled: boolean): void;
|
|
setOutgoingVideoIsScreenShare(enabled: boolean): void;
|
|
updateBandwidthMode(bandwidthMode: BandwidthMode): void;
|
|
sendVideoFrame(
|
|
width: number,
|
|
height: number,
|
|
format: VideoPixelFormatEnum,
|
|
buffer: Buffer
|
|
): void;
|
|
receiveVideoFrame(buffer: Buffer): [number, number] | undefined;
|
|
receivedOffer(
|
|
remoteUserId: UserId,
|
|
remoteDeviceId: DeviceId,
|
|
messageAgeSec: number,
|
|
callId: CallId,
|
|
offerType: OfferType,
|
|
localDeviceId: DeviceId,
|
|
opaque: Buffer,
|
|
senderIdentityKey: Buffer,
|
|
receiverIdentityKey: Buffer
|
|
): void;
|
|
receivedAnswer(
|
|
remoteUserId: UserId,
|
|
remoteDeviceId: DeviceId,
|
|
callId: CallId,
|
|
opaque: Buffer,
|
|
senderIdentityKey: Buffer,
|
|
receiverIdentityKey: Buffer
|
|
): void;
|
|
receivedIceCandidates(
|
|
remoteUserId: UserId,
|
|
remoteDeviceId: DeviceId,
|
|
callId: CallId,
|
|
candidates: Array<Buffer>
|
|
): void;
|
|
receivedHangup(
|
|
remoteUserId: UserId,
|
|
remoteDeviceId: DeviceId,
|
|
callId: CallId,
|
|
hangupType: HangupType,
|
|
hangupDeviceId: DeviceId | null
|
|
): void;
|
|
receivedBusy(
|
|
remoteUserId: UserId,
|
|
remoteDeviceId: DeviceId,
|
|
callId: CallId
|
|
): void;
|
|
receivedCallMessage(
|
|
remoteUserId: Buffer,
|
|
remoteDeviceId: DeviceId,
|
|
localDeviceId: DeviceId,
|
|
data: Buffer,
|
|
messageAgeSec: number
|
|
): void;
|
|
|
|
receivedHttpResponse(requestId: number, status: number, body: Buffer): void;
|
|
httpRequestFailed(requestId: number, debugInfo: string | undefined): void;
|
|
|
|
// Group Calls
|
|
|
|
createGroupCallClient(
|
|
groupId: Buffer,
|
|
sfuUrl: string,
|
|
hkdfExtraInfo: Buffer,
|
|
audioLevelsIntervalMillis: number
|
|
): GroupCallClientId;
|
|
deleteGroupCallClient(clientId: GroupCallClientId): void;
|
|
connect(clientId: GroupCallClientId): void;
|
|
join(clientId: GroupCallClientId): void;
|
|
leave(clientId: GroupCallClientId): void;
|
|
disconnect(clientId: GroupCallClientId): void;
|
|
setOutgoingAudioMuted(clientId: GroupCallClientId, muted: boolean): void;
|
|
setOutgoingVideoMuted(clientId: GroupCallClientId, muted: boolean): void;
|
|
setPresenting(clientId: GroupCallClientId, presenting: boolean): void;
|
|
setOutgoingGroupCallVideoIsScreenShare(
|
|
clientId: GroupCallClientId,
|
|
isScreenShare: boolean
|
|
): void;
|
|
groupRing(clientId: GroupCallClientId, recipient: Buffer | undefined): void;
|
|
resendMediaKeys(clientId: GroupCallClientId): void;
|
|
setBandwidthMode(
|
|
clientId: GroupCallClientId,
|
|
bandwidthMode: BandwidthMode
|
|
): void;
|
|
requestVideo(
|
|
clientId: GroupCallClientId,
|
|
resolutions: Array<VideoRequest>,
|
|
activeSpeakerHeight: number
|
|
): void;
|
|
setGroupMembers(
|
|
clientId: GroupCallClientId,
|
|
members: Array<GroupMemberInfo>
|
|
): void;
|
|
setMembershipProof(clientId: GroupCallClientId, proof: Buffer): void;
|
|
// Same as receiveVideoFrame, but with a specific GroupCallClientId and remoteDemuxId.
|
|
receiveGroupCallVideoFrame(
|
|
clientId: GroupCallClientId,
|
|
remoteDemuxId: number,
|
|
buffer: Buffer
|
|
): [number, number] | undefined;
|
|
// Response comes back via handlePeekResponse
|
|
peekGroupCall(
|
|
requestId: number,
|
|
sfu_url: string,
|
|
membership_proof: Buffer,
|
|
group_members: Array<GroupMemberInfo>
|
|
): Promise<PeekInfo>;
|
|
|
|
getAudioInputs(): AudioDevice[];
|
|
setAudioInput(index: number): void;
|
|
getAudioOutputs(): AudioDevice[];
|
|
setAudioOutput(index: number): void;
|
|
}
|
|
|
|
export interface CallManagerCallbacks {
|
|
onStartOutgoingCall(remoteUserId: UserId, callId: CallId): void;
|
|
onStartIncomingCall(
|
|
remoteUserId: UserId,
|
|
callId: CallId,
|
|
isVideoCall: boolean
|
|
): void;
|
|
onCallState(remoteUserId: UserId, state: CallState): void;
|
|
onCallEnded(
|
|
remoteUserId: UserId,
|
|
callId: CallId,
|
|
endedReason: CallEndedReason,
|
|
ageSec: number
|
|
): void;
|
|
onRemoteVideoEnabled(remoteUserId: UserId, enabled: boolean): void;
|
|
onRemoteSharingScreen(remoteUserId: UserId, enabled: boolean): void;
|
|
onSendOffer(
|
|
remoteUserId: UserId,
|
|
remoteDeviceId: DeviceId,
|
|
callId: CallId,
|
|
broadcast: boolean,
|
|
mediaType: number,
|
|
opaque: Buffer
|
|
): void;
|
|
onSendAnswer(
|
|
remoteUserId: UserId,
|
|
remoteDeviceId: DeviceId,
|
|
callId: CallId,
|
|
broadcast: boolean,
|
|
opaque: Buffer
|
|
): void;
|
|
onSendIceCandidates(
|
|
remoteUserId: UserId,
|
|
remoteDeviceId: DeviceId,
|
|
callId: CallId,
|
|
broadcast: boolean,
|
|
candidates: Array<Buffer>
|
|
): void;
|
|
onSendHangup(
|
|
remoteUserId: UserId,
|
|
remoteDeviceId: DeviceId,
|
|
callId: CallId,
|
|
broadcast: boolean,
|
|
HangupType: HangupType,
|
|
hangupDeviceId: DeviceId | null
|
|
): void;
|
|
onSendBusy(
|
|
remoteUserId: UserId,
|
|
remoteDeviceId: DeviceId,
|
|
callId: CallId,
|
|
broadcast: boolean
|
|
): void;
|
|
sendCallMessage(
|
|
recipientUuid: Buffer,
|
|
message: Buffer,
|
|
urgency: CallMessageUrgency
|
|
): void;
|
|
sendCallMessageToGroup(
|
|
groupId: Buffer,
|
|
message: Buffer,
|
|
urgency: CallMessageUrgency
|
|
): void;
|
|
sendHttpRequest(
|
|
requestId: number,
|
|
url: string,
|
|
method: HttpMethod,
|
|
headers: { [name: string]: string },
|
|
body: Buffer | undefined
|
|
): void;
|
|
|
|
// Group Calls
|
|
|
|
requestMembershipProof(clientId: GroupCallClientId): void;
|
|
requestGroupMembers(clientId: GroupCallClientId): void;
|
|
handleConnectionStateChanged(
|
|
clientId: GroupCallClientId,
|
|
connectionState: ConnectionState
|
|
): void;
|
|
handleJoinStateChanged(
|
|
clientId: GroupCallClientId,
|
|
joinState: JoinState,
|
|
demuxId: number | undefined
|
|
): void;
|
|
handleRemoteDevicesChanged(
|
|
clientId: GroupCallClientId,
|
|
remoteDeviceStates: Array<RemoteDeviceState>
|
|
): void;
|
|
handlePeekChanged(clientId: GroupCallClientId, info: PeekInfo): void;
|
|
handlePeekResponse(request_id: number, info: PeekInfo): void;
|
|
handleEnded(clientId: GroupCallClientId, reason: GroupCallEndReason): void;
|
|
|
|
onLogMessage(
|
|
level: number,
|
|
fileName: string,
|
|
line: number,
|
|
message: string
|
|
): void;
|
|
}
|
|
|
|
export enum CallState {
|
|
Prering = 'idle',
|
|
Ringing = 'ringing',
|
|
Accepted = 'connected',
|
|
Reconnecting = 'connecting',
|
|
Ended = 'ended',
|
|
}
|
|
|
|
export enum CallEndedReason {
|
|
LocalHangup = 'LocalHangup',
|
|
RemoteHangup = 'RemoteHangup',
|
|
RemoteHangupNeedPermission = 'RemoteHangupNeedPermission',
|
|
Declined = 'Declined',
|
|
Busy = 'Busy',
|
|
Glare = 'Glare',
|
|
ReCall = 'ReCall',
|
|
ReceivedOfferExpired = 'ReceivedOfferExpired',
|
|
ReceivedOfferWhileActive = 'ReceivedOfferWhileActive',
|
|
ReceivedOfferWithGlare = 'ReceivedOfferWithGlare',
|
|
SignalingFailure = 'SignalingFailure',
|
|
GlareFailure = 'GlareFailure',
|
|
ConnectionFailure = 'ConnectionFailure',
|
|
InternalFailure = 'InternalFailure',
|
|
Timeout = 'Timeout',
|
|
AcceptedOnAnotherDevice = 'AcceptedOnAnotherDevice',
|
|
DeclinedOnAnotherDevice = 'DeclinedOnAnotherDevice',
|
|
BusyOnAnotherDevice = 'BusyOnAnotherDevice',
|
|
}
|
|
|
|
export enum CallLogLevel {
|
|
Off,
|
|
Error,
|
|
Warn,
|
|
Info,
|
|
Debug,
|
|
Trace,
|
|
}
|
|
|
|
function silly_deadlock_protection(f: () => void) {
|
|
// tslint:disable no-floating-promises
|
|
(async () => {
|
|
// This is a silly way of preventing a deadlock.
|
|
// tslint:disable-next-line await-promise
|
|
await 0;
|
|
|
|
f();
|
|
})();
|
|
}
|