/*
* Famedly Matrix SDK
* Copyright (C) 2021 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
import 'dart:async';
import 'dart:core';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:webrtc_interface/webrtc_interface.dart';
import 'package:matrix/matrix.dart';
import 'package:matrix/src/utils/cached_stream_controller.dart';
/// https://github.com/matrix-org/matrix-doc/pull/2746
/// version 1
const String voipProtoVersion = '1';
class CallTimeouts {
/// The default life time for call events, in millisecond.
static const defaultCallEventLifetime = Duration(seconds: 10);
/// The length of time a call can be ringing for.
static const callInviteLifetime = Duration(seconds: 60);
/// The delay for ice gathering.
static const iceGatheringDelay = Duration(milliseconds: 200);
/// Delay before createOffer.
static const delayBeforeOffer = Duration(milliseconds: 100);
}
extension RTCIceCandidateExt on RTCIceCandidate {
bool get isValid =>
sdpMLineIndex != null &&
sdpMid != null &&
candidate != null &&
candidate!.isNotEmpty;
}
/// Wrapped MediaStream, used to adapt Widget to display
class WrappedMediaStream {
MediaStream? stream;
final String userId;
final Room room;
/// Current stream type, usermedia or screen-sharing
String purpose;
bool audioMuted;
bool videoMuted;
final Client client;
VideoRenderer renderer;
final bool isWeb;
final bool isGroupCall;
final RTCPeerConnection? pc;
/// for debug
String get title => '$displayName:$purpose:a[$audioMuted]:v[$videoMuted]';
bool stopped = false;
final CachedStreamController onMuteStateChanged =
CachedStreamController();
void Function(MediaStream stream)? onNewStream;
WrappedMediaStream(
{this.stream,
this.pc,
required this.renderer,
required this.room,
required this.userId,
required this.purpose,
required this.client,
required this.audioMuted,
required this.videoMuted,
required this.isWeb,
required this.isGroupCall});
/// Initialize the video renderer
Future initialize() async {
await renderer.initialize();
renderer.srcObject = stream;
renderer.onResize = () {
Logs().i(
'onResize [${stream!.id.substring(0, 8)}] ${renderer.videoWidth} x ${renderer.videoHeight}');
};
}
Future dispose() async {
renderer.srcObject = null;
/// libwebrtc does not provide a way to clone MediaStreams. So stopping the
/// local stream here would break calls with all other participants if anyone
/// leaves. The local stream is manually disposed when user leaves. On web
/// streams are actually cloned.
if (!isGroupCall || isWeb) {
await stopMediaStream(stream);
}
stream = null;
await renderer.dispose();
}
Future disposeRenderer() async {
renderer.srcObject = null;
await renderer.dispose();
}
Uri? get avatarUrl => getUser().avatarUrl;
String get avatarName =>
getUser().calcDisplayname(mxidLocalPartFallback: false);
String? get displayName => getUser().displayName;
User getUser() {
return room.unsafeGetUserFromMemoryOrFallback(userId);
}
bool isLocal() {
return userId == client.userID;
}
bool isAudioMuted() {
return (stream != null && stream!.getAudioTracks().isEmpty) || audioMuted;
}
bool isVideoMuted() {
return (stream != null && stream!.getVideoTracks().isEmpty) || videoMuted;
}
void setNewStream(MediaStream newStream) {
stream = newStream;
renderer.srcObject = stream;
if (onNewStream != null) {
onNewStream?.call(stream!);
}
}
void setAudioMuted(bool muted) {
audioMuted = muted;
onMuteStateChanged.add(this);
}
void setVideoMuted(bool muted) {
videoMuted = muted;
onMuteStateChanged.add(this);
}
}
// Call state
enum CallState {
/// The call is inilalized but not yet started
kFledgling,
/// The first time an invite is sent, the local has createdOffer
kInviteSent,
/// getUserMedia or getDisplayMedia has been called,
/// but MediaStream has not yet been returned
kWaitLocalMedia,
/// The local has createdOffer
kCreateOffer,
/// Received a remote offer message and created a local Answer
kCreateAnswer,
/// Answer sdp is set, but ice is not connected
kConnecting,
/// WebRTC media stream is connected
kConnected,
/// The call was received, but no processing has been done yet.
kRinging,
/// End of call
kEnded,
}
class CallErrorCode {
/// The user chose to end the call
static String UserHangup = 'user_hangup';
/// An error code when the local client failed to create an offer.
static String LocalOfferFailed = 'local_offer_failed';
/// An error code when there is no local mic/camera to use. This may be because
/// the hardware isn't plugged in, or the user has explicitly denied access.
static String NoUserMedia = 'no_user_media';
/// Error code used when a call event failed to send
/// because unknown devices were present in the room
static String UnknownDevices = 'unknown_devices';
/// Error code used when we fail to send the invite
/// for some reason other than there being unknown devices
static String SendInvite = 'send_invite';
/// An answer could not be created
static String CreateAnswer = 'create_answer';
/// Error code used when we fail to send the answer
/// for some reason other than there being unknown devices
static String SendAnswer = 'send_answer';
/// The session description from the other side could not be set
static String SetRemoteDescription = 'set_remote_description';
/// The session description from this side could not be set
static String SetLocalDescription = 'set_local_description';
/// A different device answered the call
static String AnsweredElsewhere = 'answered_elsewhere';
/// No media connection could be established to the other party
static String IceFailed = 'ice_failed';
/// The invite timed out whilst waiting for an answer
static String InviteTimeout = 'invite_timeout';
/// The call was replaced by another call
static String Replaced = 'replaced';
/// Signalling for the call could not be sent (other than the initial invite)
static String SignallingFailed = 'signalling_timeout';
/// The remote party is busy
static String UserBusy = 'user_busy';
/// We transferred the call off to somewhere else
static String Transfered = 'transferred';
}
class CallError extends Error {
final String code;
final String msg;
final dynamic err;
CallError(this.code, this.msg, this.err);
@override
String toString() {
return '[$code] $msg, err: ${err.toString()}';
}
}
enum CallEvent {
/// The call was hangup by the local|remote user.
kHangup,
/// The call state has changed
kState,
/// The call got some error.
kError,
/// Call transfer
kReplaced,
/// The value of isLocalOnHold() has changed
kLocalHoldUnhold,
/// The value of isRemoteOnHold() has changed
kRemoteHoldUnhold,
/// Feeds have changed
kFeedsChanged,
/// For sip calls. support in the future.
kAssertedIdentityChanged,
}
enum CallType { kVoice, kVideo }
enum CallDirection { kIncoming, kOutgoing }
enum CallParty { kLocal, kRemote }
/// Initialization parameters of the call session.
class CallOptions {
late String callId;
String? groupCallId;
late CallType type;
late CallDirection dir;
late String localPartyId;
late VoIP voip;
late Room room;
late List