Merge pull request #1649 from famedly/td/callsLifetime
fix: ignore calls with age older than lifetime
This commit is contained in:
commit
e1d4af80ae
|
|
@ -2142,6 +2142,22 @@ class Client extends MatrixApi {
|
|||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
final age = callEvent.unsigned?.tryGet<int>('age') ??
|
||||
(DateTime.now().millisecondsSinceEpoch -
|
||||
callEvent.originServerTs.millisecondsSinceEpoch);
|
||||
|
||||
callEvents.removeWhere((element) {
|
||||
if (callEvent.type == EventTypes.CallInvite &&
|
||||
age >
|
||||
(callEvent.content.tryGet<int>('lifetime') ??
|
||||
CallTimeouts.callInviteLifetime.inMilliseconds)) {
|
||||
Logs().v(
|
||||
'Ommiting invite event ${callEvent.eventId} as age was older than lifetime');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,18 +30,18 @@ import 'package:matrix/src/utils/cached_stream_controller.dart';
|
|||
/// version 1
|
||||
const String voipProtoVersion = '1';
|
||||
|
||||
class Timeouts {
|
||||
class CallTimeouts {
|
||||
/// The default life time for call events, in millisecond.
|
||||
static const lifetimeMs = 10 * 1000;
|
||||
static const defaultCallEventLifetime = Duration(seconds: 10);
|
||||
|
||||
/// The length of time a call can be ringing for.
|
||||
static const callTimeoutSec = 60;
|
||||
static const callInviteLifetime = Duration(seconds: 60);
|
||||
|
||||
/// The delay for ice gathering.
|
||||
static const iceGatheringDelayMs = 200;
|
||||
static const iceGatheringDelay = Duration(milliseconds: 200);
|
||||
|
||||
/// Delay before createOffer.
|
||||
static const delayBeforeOfferMs = 100;
|
||||
static const delayBeforeOffer = Duration(milliseconds: 100);
|
||||
}
|
||||
|
||||
extension RTCIceCandidateExt on RTCIceCandidate {
|
||||
|
|
@ -504,7 +504,7 @@ class CallSession {
|
|||
|
||||
setCallState(CallState.kRinging);
|
||||
|
||||
ringingTimer = Timer(Duration(seconds: 30), () {
|
||||
ringingTimer = Timer(CallTimeouts.callInviteLifetime, () {
|
||||
if (state == CallState.kRinging) {
|
||||
Logs().v('[VOIP] Call invite has expired. Hanging up.');
|
||||
hangupParty = CallParty.kRemote; // effectively
|
||||
|
|
@ -621,8 +621,7 @@ class CallSession {
|
|||
}
|
||||
|
||||
/// Send select_answer event.
|
||||
await sendSelectCallAnswer(
|
||||
opts.room, callId, Timeouts.lifetimeMs, localPartyId, remotePartyId!);
|
||||
await sendSelectCallAnswer(opts.room, callId, localPartyId, remotePartyId!);
|
||||
}
|
||||
|
||||
Future<void> onNegotiateReceived(
|
||||
|
|
@ -659,7 +658,11 @@ class CallSession {
|
|||
}
|
||||
|
||||
await sendCallNegotiate(
|
||||
room, callId, Timeouts.lifetimeMs, localPartyId, answer.sdp!,
|
||||
room,
|
||||
callId,
|
||||
CallTimeouts.defaultCallEventLifetime.inMilliseconds,
|
||||
localPartyId,
|
||||
answer.sdp!,
|
||||
type: answer.type!);
|
||||
await pc!.setLocalDescription(answer);
|
||||
}
|
||||
|
|
@ -983,7 +986,7 @@ class CallSession {
|
|||
if (localUserMediaStream != null && localUserMediaStream!.stream != null) {
|
||||
final stream = await _getUserMedia(CallType.kVideo);
|
||||
if (stream != null) {
|
||||
Logs().e('[VOIP] running replaceTracks() on stream: ${stream.id}');
|
||||
Logs().d('[VOIP] running replaceTracks() on stream: ${stream.id}');
|
||||
_setTracksEnabled(stream.getVideoTracks(), true);
|
||||
// replace local tracks
|
||||
for (final track in localUserMediaStream!.stream!.getTracks()) {
|
||||
|
|
@ -1083,7 +1086,7 @@ class CallSession {
|
|||
return callOnHold;
|
||||
}
|
||||
|
||||
Future<void> answer() async {
|
||||
Future<void> answer({String? txid}) async {
|
||||
if (inviteOrAnswerSent) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1121,10 +1124,16 @@ class CallSession {
|
|||
// Allow a short time for initial candidates to be gathered
|
||||
await Future.delayed(Duration(milliseconds: 200));
|
||||
|
||||
final res = await sendAnswerCall(room, callId, answer.sdp!, localPartyId,
|
||||
type: answer.type!,
|
||||
capabilities: callCapabilities,
|
||||
metadata: metadata);
|
||||
final res = await sendAnswerCall(
|
||||
room,
|
||||
callId,
|
||||
answer.sdp!,
|
||||
localPartyId,
|
||||
type: answer.type!,
|
||||
capabilities: callCapabilities,
|
||||
metadata: metadata,
|
||||
txid: txid,
|
||||
);
|
||||
Logs().v('[VOIP] answer res => $res');
|
||||
|
||||
inviteOrAnswerSent = true;
|
||||
|
|
@ -1145,8 +1154,7 @@ class CallSession {
|
|||
Logs().d('[VOIP] Rejecting call: $callId');
|
||||
await terminate(CallParty.kLocal, CallErrorCode.UserHangup, shouldEmit);
|
||||
if (shouldEmit) {
|
||||
await sendCallReject(
|
||||
room, callId, Timeouts.lifetimeMs, localPartyId, reason);
|
||||
await sendCallReject(room, callId, localPartyId, reason);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1257,8 +1265,7 @@ class CallSession {
|
|||
if (pc!.iceGatheringState ==
|
||||
RTCIceGatheringState.RTCIceGatheringStateGathering) {
|
||||
// Allow a short time for initial candidates to be gathered
|
||||
await Future.delayed(
|
||||
Duration(milliseconds: Timeouts.iceGatheringDelayMs));
|
||||
await Future.delayed(CallTimeouts.iceGatheringDelay);
|
||||
}
|
||||
|
||||
if (callHasEnded) return;
|
||||
|
|
@ -1271,8 +1278,14 @@ class CallSession {
|
|||
Logs().d('[glare] new invite sent about to be called');
|
||||
|
||||
await sendInviteToCall(
|
||||
room, callId, Timeouts.lifetimeMs, localPartyId, null, offer.sdp!,
|
||||
capabilities: callCapabilities, metadata: metadata);
|
||||
room,
|
||||
callId,
|
||||
CallTimeouts.callInviteLifetime.inMilliseconds,
|
||||
localPartyId,
|
||||
null,
|
||||
offer.sdp!,
|
||||
capabilities: callCapabilities,
|
||||
metadata: metadata);
|
||||
// just incase we ended the call but already sent the invite
|
||||
if (state == CallState.kEnded) {
|
||||
await hangup(CallErrorCode.Replaced, false);
|
||||
|
|
@ -1287,7 +1300,7 @@ class CallSession {
|
|||
|
||||
setCallState(CallState.kInviteSent);
|
||||
|
||||
inviteTimer = Timer(Duration(seconds: Timeouts.callTimeoutSec), () {
|
||||
inviteTimer = Timer(CallTimeouts.callInviteLifetime, () {
|
||||
if (state == CallState.kInviteSent) {
|
||||
hangup(CallErrorCode.InviteTimeout);
|
||||
}
|
||||
|
|
@ -1296,7 +1309,11 @@ class CallSession {
|
|||
});
|
||||
} else {
|
||||
await sendCallNegotiate(
|
||||
room, callId, Timeouts.lifetimeMs, localPartyId, offer.sdp!,
|
||||
room,
|
||||
callId,
|
||||
CallTimeouts.defaultCallEventLifetime.inMilliseconds,
|
||||
localPartyId,
|
||||
offer.sdp!,
|
||||
type: offer.type!,
|
||||
capabilities: callCapabilities,
|
||||
metadata: metadata);
|
||||
|
|
@ -1311,7 +1328,7 @@ class CallSession {
|
|||
// onNegotiationNeeded, which causes creatOffer to only include
|
||||
// audio m-line, add delay and wait for video track to be added,
|
||||
// then createOffer can get audio/video m-line correctly.
|
||||
await Future.delayed(Duration(milliseconds: Timeouts.delayBeforeOfferMs));
|
||||
await Future.delayed(CallTimeouts.delayBeforeOffer);
|
||||
final offer = await pc!.createOffer({});
|
||||
await _gotLocalOffer(offer);
|
||||
} catch (e) {
|
||||
|
|
@ -1703,8 +1720,8 @@ class CallSession {
|
|||
/// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
|
||||
/// [party_id] The party ID for call, Can be set to client.deviceId.
|
||||
/// [selected_party_id] The party ID for the selected answer.
|
||||
Future<String?> sendSelectCallAnswer(Room room, String callId, int lifetime,
|
||||
String party_id, String selected_party_id,
|
||||
Future<String?> sendSelectCallAnswer(
|
||||
Room room, String callId, String party_id, String selected_party_id,
|
||||
{String version = voipProtoVersion, String? txid}) async {
|
||||
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
|
|
@ -1713,7 +1730,6 @@ class CallSession {
|
|||
'party_id': party_id,
|
||||
if (groupCallId != null) 'conf_id': groupCallId,
|
||||
'version': version,
|
||||
'lifetime': lifetime,
|
||||
'selected_party_id': selected_party_id,
|
||||
};
|
||||
|
||||
|
|
@ -1730,7 +1746,7 @@ class CallSession {
|
|||
/// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
|
||||
/// [party_id] The party ID for call, Can be set to client.deviceId.
|
||||
Future<String?> sendCallReject(
|
||||
Room room, String callId, int lifetime, String party_id, String? reason,
|
||||
Room room, String callId, String party_id, String? reason,
|
||||
{String version = voipProtoVersion, String? txid}) async {
|
||||
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
|
|
@ -1740,7 +1756,6 @@ class CallSession {
|
|||
if (groupCallId != null) 'conf_id': groupCallId,
|
||||
if (reason != null) 'reason': reason,
|
||||
'version': version,
|
||||
'lifetime': lifetime,
|
||||
};
|
||||
|
||||
return await _sendContent(
|
||||
|
|
@ -1972,6 +1987,9 @@ class CallSession {
|
|||
}) async {
|
||||
txid ??= client.generateUniqueTransactionId();
|
||||
final mustEncrypt = room.encrypted && client.encryptionEnabled;
|
||||
|
||||
// opponentDeviceId is only set for a few events during group calls,
|
||||
// therefore only group calls use to-device messages for some events
|
||||
if (opponentDeviceId != null) {
|
||||
final toDeviceSeq = this.toDeviceSeq++;
|
||||
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ class GroupCall {
|
|||
WrappedMediaStream? localUserMediaStream;
|
||||
WrappedMediaStream? localScreenshareStream;
|
||||
String? localDesktopCapturerSourceId;
|
||||
List<CallSession> calls = [];
|
||||
List<CallSession> callSessions = [];
|
||||
List<User> participants = [];
|
||||
List<WrappedMediaStream> userMediaStreams = [];
|
||||
List<WrappedMediaStream> screenshareStreams = [];
|
||||
|
|
@ -404,7 +404,7 @@ class GroupCall {
|
|||
final stream =
|
||||
await voip.delegate.mediaDevices.getUserMedia({'audio': true});
|
||||
final audioTrack = stream.getAudioTracks().first;
|
||||
for (final call in calls) {
|
||||
for (final call in callSessions) {
|
||||
await call.updateAudioDevice(audioTrack);
|
||||
}
|
||||
}
|
||||
|
|
@ -441,7 +441,7 @@ class GroupCall {
|
|||
|
||||
_callSubscription = voip.onIncomingCall.stream.listen(onIncomingCall);
|
||||
|
||||
for (final call in calls) {
|
||||
for (final call in callSessions) {
|
||||
await onIncomingCall(call);
|
||||
}
|
||||
|
||||
|
|
@ -478,7 +478,7 @@ class GroupCall {
|
|||
|
||||
await removeMemberStateEvent();
|
||||
|
||||
final callsCopy = calls.toList();
|
||||
final callsCopy = callSessions.toList();
|
||||
|
||||
for (final call in callsCopy) {
|
||||
await removeCall(call, CallErrorCode.UserHangup);
|
||||
|
|
@ -553,7 +553,7 @@ class GroupCall {
|
|||
setTracksEnabled(localUserMediaStream!.stream!.getAudioTracks(), !muted);
|
||||
}
|
||||
|
||||
for (final call in calls) {
|
||||
for (final call in callSessions) {
|
||||
await call.setMicrophoneMuted(muted);
|
||||
}
|
||||
|
||||
|
|
@ -571,7 +571,7 @@ class GroupCall {
|
|||
setTracksEnabled(localUserMediaStream!.stream!.getVideoTracks(), !muted);
|
||||
}
|
||||
|
||||
for (final call in calls) {
|
||||
for (final call in callSessions) {
|
||||
await call.setLocalVideoMuted(muted);
|
||||
}
|
||||
|
||||
|
|
@ -620,7 +620,7 @@ class GroupCall {
|
|||
await localScreenshareStream!.initialize();
|
||||
|
||||
onGroupCallEvent.add(GroupCallEvent.LocalScreenshareStateChanged);
|
||||
for (final call in calls) {
|
||||
for (final call in callSessions) {
|
||||
await call.addLocalStream(
|
||||
await localScreenshareStream!.stream!.clone(),
|
||||
localScreenshareStream!.purpose);
|
||||
|
|
@ -637,7 +637,7 @@ class GroupCall {
|
|||
return false;
|
||||
}
|
||||
} else {
|
||||
for (final call in calls) {
|
||||
for (final call in callSessions) {
|
||||
await call.removeLocalStream(call.localScreenSharingStream!);
|
||||
}
|
||||
|
||||
|
|
@ -935,7 +935,7 @@ class GroupCall {
|
|||
}
|
||||
|
||||
CallSession? getCallByUserId(String userId) {
|
||||
final value = calls.where((item) => item.remoteUser!.id == userId);
|
||||
final value = callSessions.where((item) => item.remoteUser!.id == userId);
|
||||
if (value.isNotEmpty) {
|
||||
return value.first;
|
||||
}
|
||||
|
|
@ -943,7 +943,7 @@ class GroupCall {
|
|||
}
|
||||
|
||||
Future<void> addCall(CallSession call) async {
|
||||
calls.add(call);
|
||||
callSessions.add(call);
|
||||
await initCall(call);
|
||||
onGroupCallEvent.add(GroupCallEvent.CallsChanged);
|
||||
}
|
||||
|
|
@ -951,14 +951,14 @@ class GroupCall {
|
|||
Future<void> replaceCall(
|
||||
CallSession existingCall, CallSession replacementCall) async {
|
||||
final existingCallIndex =
|
||||
calls.indexWhere((element) => element == existingCall);
|
||||
callSessions.indexWhere((element) => element == existingCall);
|
||||
|
||||
if (existingCallIndex == -1) {
|
||||
throw Exception('Couldn\'t find call to replace');
|
||||
}
|
||||
|
||||
calls.removeAt(existingCallIndex);
|
||||
calls.add(replacementCall);
|
||||
callSessions.removeAt(existingCallIndex);
|
||||
callSessions.add(replacementCall);
|
||||
|
||||
await disposeCall(existingCall, CallErrorCode.Replaced);
|
||||
await initCall(replacementCall);
|
||||
|
|
@ -970,7 +970,7 @@ class GroupCall {
|
|||
Future<void> removeCall(CallSession call, String hangupReason) async {
|
||||
await disposeCall(call, hangupReason);
|
||||
|
||||
calls.removeWhere((element) => call.callId == element.callId);
|
||||
callSessions.removeWhere((element) => call.callId == element.callId);
|
||||
|
||||
onGroupCallEvent.add(GroupCallEvent.CallsChanged);
|
||||
}
|
||||
|
|
@ -1287,7 +1287,7 @@ class GroupCall {
|
|||
|
||||
onGroupCallEvent.add(GroupCallEvent.ParticipantsChanged);
|
||||
|
||||
final callsCopylist = List.from(calls);
|
||||
final callsCopylist = List.from(callSessions);
|
||||
|
||||
for (final call in callsCopylist) {
|
||||
await call.updateMuteStatus();
|
||||
|
|
@ -1305,7 +1305,7 @@ class GroupCall {
|
|||
|
||||
onGroupCallEvent.add(GroupCallEvent.ParticipantsChanged);
|
||||
|
||||
final callsCopylist = List.from(calls);
|
||||
final callsCopylist = List.from(callSessions);
|
||||
|
||||
for (final call in callsCopylist) {
|
||||
await call.updateMuteStatus();
|
||||
|
|
|
|||
|
|
@ -176,7 +176,11 @@ class VoIP {
|
|||
final String partyId = content['party_id'];
|
||||
final int lifetime = content['lifetime'];
|
||||
final String? confId = content['conf_id'];
|
||||
|
||||
// msc3401 group call invites send deviceId and senderSessionId in to device messages
|
||||
final String? deviceId = content['device_id'];
|
||||
final String? senderSessionId = content['sender_session_id'];
|
||||
|
||||
final call = calls[callId];
|
||||
|
||||
Logs().d(
|
||||
|
|
@ -232,7 +236,7 @@ class VoIP {
|
|||
newCall.remotePartyId = partyId;
|
||||
newCall.remoteUser = await room.requestUser(senderId);
|
||||
newCall.opponentDeviceId = deviceId;
|
||||
newCall.opponentSessionId = content['sender_session_id'];
|
||||
newCall.opponentSessionId = senderSessionId;
|
||||
if (!delegate.canHandleNewCall &&
|
||||
(confId == null || confId != currentGroupCID)) {
|
||||
Logs().v(
|
||||
|
|
@ -272,7 +276,10 @@ class VoIP {
|
|||
await delegate.handleNewCall(newCall);
|
||||
}
|
||||
|
||||
onIncomingCall.add(newCall);
|
||||
if (confId != null) {
|
||||
// the stream is used to monitor incoming peer calls in a mesh call
|
||||
onIncomingCall.add(newCall);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onCallAnswer(
|
||||
|
|
@ -494,6 +501,19 @@ class VoIP {
|
|||
'Ignoring call negotiation for room $roomId claiming to be for call in room ${call.room.id}');
|
||||
return;
|
||||
}
|
||||
if (content['party_id'] != call.remotePartyId) {
|
||||
Logs().w('Ignoring call negotiation, wrong partyId detected');
|
||||
return;
|
||||
}
|
||||
if (content['party_id'] == call.localPartyId) {
|
||||
Logs().w('Ignoring call negotiation echo');
|
||||
return;
|
||||
}
|
||||
|
||||
// ideally you also check the lifetime here and discard negotiation events
|
||||
// if age of the event was older than the lifetime but as to device events
|
||||
// do not have a unsigned age nor a origin_server_ts there's no easy way to
|
||||
// override this one function atm
|
||||
|
||||
final description = content['description'];
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'fake_client.dart';
|
||||
import 'webrtc_stub.dart';
|
||||
|
||||
void main() {
|
||||
late Client matrix;
|
||||
late Room room;
|
||||
|
||||
group('Call Tests', () {
|
||||
Logs().level = Level.info;
|
||||
|
||||
test('Login', () async {
|
||||
matrix = await getClient();
|
||||
});
|
||||
|
||||
test('Create from json', () async {
|
||||
final id = '!localpart:server.abc';
|
||||
final membership = Membership.join;
|
||||
|
||||
room = Room(
|
||||
client: matrix,
|
||||
id: id,
|
||||
membership: membership,
|
||||
prev_batch: '',
|
||||
);
|
||||
});
|
||||
|
||||
test('Test call methods', () async {
|
||||
final call = CallSession(CallOptions()..room = room);
|
||||
await call.sendInviteToCall(room, '1234', 1234, '4567', '7890', 'sdp',
|
||||
txid: '1234');
|
||||
await call.sendAnswerCall(room, '1234', 'sdp', '4567', txid: '1234');
|
||||
await call.sendCallCandidates(room, '1234', '4567', [], txid: '1234');
|
||||
await call.sendSelectCallAnswer(room, '1234', '4567', '6789',
|
||||
txid: '1234');
|
||||
await call.sendCallReject(room, '1234', '4567', 'busy', txid: '1234');
|
||||
await call.sendCallNegotiate(room, '1234', 1234, '4567', 'sdp',
|
||||
txid: '1234');
|
||||
await call.sendHangupCall(room, '1234', '4567', 'user_hangup',
|
||||
txid: '1234');
|
||||
await call.sendAssertedIdentity(
|
||||
room,
|
||||
'1234',
|
||||
'4567',
|
||||
AssertedIdentity()
|
||||
..displayName = 'name'
|
||||
..id = 'some_id',
|
||||
txid: '1234');
|
||||
await call.sendCallReplaces(room, '1234', '4567', CallReplaces(),
|
||||
txid: '1234');
|
||||
await call.sendSDPStreamMetadataChanged(
|
||||
room, '1234', '4567', SDPStreamMetadata({}),
|
||||
txid: '1234');
|
||||
});
|
||||
test('Test call lifetime', () async {
|
||||
final voip = VoIP(matrix, MockWebRTCDelegate());
|
||||
expect(voip.currentCID, null);
|
||||
// persist normal room messages
|
||||
await matrix.handleSync(SyncUpdate(
|
||||
nextBatch: 'something',
|
||||
rooms: RoomsUpdate(join: {
|
||||
room.id: JoinedRoomUpdate(
|
||||
timeline: TimelineUpdate(events: [
|
||||
MatrixEvent(
|
||||
type: 'm.call.invite',
|
||||
content: {
|
||||
'lifetime': 60000,
|
||||
'call_id': '1702472924955oq1uQbNAfU7wAaEA',
|
||||
'party_id': 'DPCIPPBGPO',
|
||||
'offer': {'type': 'offer', 'sdp': 'sdp'}
|
||||
},
|
||||
senderId: '@alice:testing.com',
|
||||
eventId: 'newevent',
|
||||
originServerTs: DateTime.utc(1969),
|
||||
)
|
||||
]))
|
||||
})));
|
||||
await Future.delayed(Duration(seconds: 3));
|
||||
// confirm that no call got created after 3 seconds, which is
|
||||
// expected in this case because the originTs was old asf
|
||||
expect(voip.currentCID, null);
|
||||
|
||||
// persist normal room messages
|
||||
await matrix.handleSync(SyncUpdate(
|
||||
nextBatch: 'something',
|
||||
rooms: RoomsUpdate(join: {
|
||||
room.id: JoinedRoomUpdate(
|
||||
timeline: TimelineUpdate(events: [
|
||||
MatrixEvent(
|
||||
unsigned: {'age': 60001},
|
||||
type: 'm.call.invite',
|
||||
content: {
|
||||
'lifetime': 60000,
|
||||
'call_id': 'unsignedTsInvalidCall',
|
||||
'party_id': 'DPCIPPBGPO',
|
||||
'offer': {'type': 'offer', 'sdp': 'sdp'}
|
||||
},
|
||||
senderId: '@alice:testing.com',
|
||||
eventId: 'newevent',
|
||||
originServerTs: DateTime.now(),
|
||||
)
|
||||
]))
|
||||
})));
|
||||
await Future.delayed(Duration(seconds: 3));
|
||||
// confirm that no call got created after 3 seconds, which is
|
||||
// expected in this case because age was older than lifetime
|
||||
expect(voip.currentCID, null);
|
||||
// persist normal room messages
|
||||
await matrix.handleSync(SyncUpdate(
|
||||
nextBatch: 'something',
|
||||
rooms: RoomsUpdate(join: {
|
||||
room.id: JoinedRoomUpdate(
|
||||
timeline: TimelineUpdate(events: [
|
||||
MatrixEvent(
|
||||
type: 'm.call.invite',
|
||||
content: {
|
||||
'lifetime': 60000,
|
||||
'call_id': 'originTsValidCall',
|
||||
'party_id': 'DPCIPPBGPO',
|
||||
'offer': {'type': 'offer', 'sdp': 'sdp'}
|
||||
},
|
||||
senderId: '@alice:testing.com',
|
||||
eventId: 'newevent',
|
||||
originServerTs: DateTime.now(),
|
||||
)
|
||||
]))
|
||||
})));
|
||||
while (voip.currentCID != 'originTsValidCall') {
|
||||
// call invite looks valid, call should be created now :D
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
Logs().d('Waiting for currentCID to update');
|
||||
}
|
||||
expect(voip.currentCID, 'originTsValidCall');
|
||||
final call = voip.calls[voip.currentCID]!;
|
||||
await call.answer(txid: '1234');
|
||||
expect(call.state, CallState.kConnecting);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1052,35 +1052,6 @@ void main() {
|
|||
expect(room.pushRuleState, PushRuleState.dontNotify);
|
||||
});
|
||||
|
||||
test('Test call methods', () async {
|
||||
final call = CallSession(CallOptions()..room = room);
|
||||
await call.sendInviteToCall(room, '1234', 1234, '4567', '7890', 'sdp',
|
||||
txid: '1234');
|
||||
await call.sendAnswerCall(room, '1234', 'sdp', '4567', txid: '1234');
|
||||
await call.sendCallCandidates(room, '1234', '4567', [], txid: '1234');
|
||||
await call.sendSelectCallAnswer(room, '1234', 1234, '4567', '6789',
|
||||
txid: '1234');
|
||||
await call.sendCallReject(room, '1234', 1234, '4567', 'busy',
|
||||
txid: '1234');
|
||||
await call.sendCallNegotiate(room, '1234', 1234, '4567', 'sdp',
|
||||
txid: '1234');
|
||||
await call.sendHangupCall(room, '1234', '4567', 'user_hangup',
|
||||
txid: '1234');
|
||||
await call.sendAssertedIdentity(
|
||||
room,
|
||||
'1234',
|
||||
'4567',
|
||||
AssertedIdentity()
|
||||
..displayName = 'name'
|
||||
..id = 'some_id',
|
||||
txid: '1234');
|
||||
await call.sendCallReplaces(room, '1234', '4567', CallReplaces(),
|
||||
txid: '1234');
|
||||
await call.sendSDPStreamMetadataChanged(
|
||||
room, '1234', '4567', SDPStreamMetadata({}),
|
||||
txid: '1234');
|
||||
});
|
||||
|
||||
test('enableEncryption', () async {
|
||||
await room.enableEncryption();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,824 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:webrtc_interface/webrtc_interface.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class MockWebRTCDelegate implements WebRTCDelegate {
|
||||
@override
|
||||
// TODO: implement canHandleNewCall
|
||||
bool get canHandleNewCall => true;
|
||||
|
||||
@override
|
||||
Future<RTCPeerConnection> createPeerConnection(
|
||||
Map<String, dynamic> configuration, [
|
||||
Map<String, dynamic> constraints = const {},
|
||||
]) async =>
|
||||
MockRTCPeerConnection();
|
||||
|
||||
@override
|
||||
VideoRenderer createRenderer() {
|
||||
return MockVideoRenderer();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> handleCallEnded(CallSession session) async {
|
||||
Logs().i('handleCallEnded called in MockWebRTCDelegate');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> handleGroupCallEnded(GroupCall groupCall) async {
|
||||
Logs().i('handleGroupCallEnded called in MockWebRTCDelegate');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> handleMissedCall(CallSession session) async {
|
||||
Logs().i('handleMissedCall called in MockWebRTCDelegate');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> handleNewCall(CallSession session) async {
|
||||
Logs().i('handleNewCall called in MockWebRTCDelegate');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> handleNewGroupCall(GroupCall groupCall) async {
|
||||
Logs().i('handleNewGroupCall called in MockWebRTCDelegate');
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isWeb => false;
|
||||
|
||||
@override
|
||||
MediaDevices get mediaDevices => MockMediaDevices();
|
||||
|
||||
@override
|
||||
Future<void> playRingtone() async {
|
||||
Logs().i('playRingtone called in MockWebRTCDelegate');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopRingtone() async {
|
||||
Logs().i('stopRingtone called in MockWebRTCDelegate');
|
||||
}
|
||||
}
|
||||
|
||||
class MockMediaDevices implements MediaDevices {
|
||||
@override
|
||||
Function(dynamic event)? ondevicechange;
|
||||
|
||||
@override
|
||||
Future<List<MediaDeviceInfo>> enumerateDevices() {
|
||||
// TODO: implement enumerateDevices
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MediaStream> getDisplayMedia(Map<String, dynamic> mediaConstraints) {
|
||||
// TODO: implement getDisplayMedia
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List> getSources() {
|
||||
// TODO: implement getSources
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
MediaTrackSupportedConstraints getSupportedConstraints() {
|
||||
// TODO: implement getSupportedConstraints
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MediaStream> getUserMedia(
|
||||
Map<String, dynamic> mediaConstraints) async {
|
||||
return MockMediaStream('', '');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MediaDeviceInfo> selectAudioOutput([AudioOutputOptions? options]) {
|
||||
// TODO: implement selectAudioOutput
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
class MockRTCPeerConnection implements RTCPeerConnection {
|
||||
@override
|
||||
Function(RTCSignalingState state)? onSignalingState;
|
||||
|
||||
@override
|
||||
Function(RTCPeerConnectionState state)? onConnectionState;
|
||||
|
||||
@override
|
||||
Function(RTCIceGatheringState state)? onIceGatheringState;
|
||||
|
||||
@override
|
||||
Function(RTCIceConnectionState state)? onIceConnectionState;
|
||||
|
||||
@override
|
||||
Function(RTCIceCandidate candidate)? onIceCandidate;
|
||||
|
||||
@override
|
||||
Function(MediaStream stream)? onAddStream;
|
||||
|
||||
@override
|
||||
Function(MediaStream stream)? onRemoveStream;
|
||||
|
||||
@override
|
||||
Function(MediaStream stream, MediaStreamTrack track)? onAddTrack;
|
||||
|
||||
@override
|
||||
Function(MediaStream stream, MediaStreamTrack track)? onRemoveTrack;
|
||||
|
||||
@override
|
||||
Function(RTCDataChannel channel)? onDataChannel;
|
||||
|
||||
@override
|
||||
Function()? onRenegotiationNeeded;
|
||||
|
||||
@override
|
||||
Function(RTCTrackEvent event)? onTrack;
|
||||
|
||||
@override
|
||||
RTCSignalingState? get signalingState => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<RTCSignalingState?> getSignalingState() async {
|
||||
return signalingState;
|
||||
}
|
||||
|
||||
@override
|
||||
RTCIceGatheringState? get iceGatheringState => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<RTCIceGatheringState?> getIceGatheringState() async {
|
||||
return iceGatheringState;
|
||||
}
|
||||
|
||||
@override
|
||||
RTCIceConnectionState? get iceConnectionState => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<RTCIceConnectionState?> getIceConnectionState() async {
|
||||
return iceConnectionState;
|
||||
}
|
||||
|
||||
@override
|
||||
RTCPeerConnectionState? get connectionState => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<RTCPeerConnectionState?> getConnectionState() async {
|
||||
return connectionState;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
// Mock implementation for disposing the connection
|
||||
Logs().i('Mock: Disposing RTCPeerConnection');
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> get getConfiguration => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<void> setConfiguration(Map<String, dynamic> configuration) async {
|
||||
// Mock implementation for setting configuration
|
||||
Logs().i('Mock: Setting RTCPeerConnection configuration');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RTCSessionDescription> createOffer(
|
||||
[Map<String, dynamic>? constraints]) {
|
||||
// Mock implementation for creating an offer
|
||||
Logs().i('Mock: Creating offer');
|
||||
return Future.value(RTCSessionDescription('', ''));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RTCSessionDescription> createAnswer(
|
||||
[Map<String, dynamic>? constraints]) {
|
||||
// Mock implementation for creating an answer
|
||||
Logs().i('Mock: Creating answer');
|
||||
return Future.value(RTCSessionDescription('', ''));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addStream(MediaStream stream) async {
|
||||
// Mock implementation for adding a stream
|
||||
Logs().i('Mock: Adding stream');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeStream(MediaStream stream) async {
|
||||
// Mock implementation for removing a stream
|
||||
Logs().i('Mock: Removing stream');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RTCSessionDescription?> getLocalDescription() async {
|
||||
// Mock implementation for getting local description
|
||||
Logs().i('Mock: Getting local description');
|
||||
return RTCSessionDescription('', '');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setLocalDescription(RTCSessionDescription description) async {
|
||||
// Mock implementation for setting local description
|
||||
Logs().i('Mock: Setting local description');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RTCSessionDescription?> getRemoteDescription() async {
|
||||
// Mock implementation for getting remote description
|
||||
Logs().i('Mock: Getting remote description');
|
||||
return RTCSessionDescription('', '');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setRemoteDescription(RTCSessionDescription description) async {
|
||||
// Mock implementation for setting remote description
|
||||
Logs().i('Mock: Setting remote description');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addCandidate(RTCIceCandidate candidate) async {
|
||||
// Mock implementation for adding a candidate
|
||||
Logs().i('Mock: Adding ICE candidate');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<StatsReport>> getStats([MediaStreamTrack? track]) async {
|
||||
// Mock implementation for getting stats
|
||||
Logs().i('Mock: Getting stats');
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
List<MediaStream?> getLocalStreams() {
|
||||
// Mock implementation for getting local streams
|
||||
Logs().i('Mock: Getting local streams');
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
List<MediaStream?> getRemoteStreams() {
|
||||
// Mock implementation for getting remote streams
|
||||
Logs().i('Mock: Getting remote streams');
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RTCDataChannel> createDataChannel(
|
||||
String label, RTCDataChannelInit dataChannelDict) async {
|
||||
// Mock implementation for creating a data channel
|
||||
Logs().i('Mock: Creating data channel');
|
||||
return MockRTCDataChannel();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> restartIce() async {
|
||||
// Mock implementation for restarting ICE
|
||||
Logs().i('Mock: Restarting ICE');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
// Mock implementation for closing the connection
|
||||
Logs().i('Mock: Closing RTCPeerConnection');
|
||||
}
|
||||
|
||||
@override
|
||||
RTCDTMFSender createDtmfSender(MediaStreamTrack track) {
|
||||
// Mock implementation for creating a DTMF sender
|
||||
Logs().i('Mock: Creating DTMF sender');
|
||||
return MockRTCDTMFSender();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<RTCRtpSender>> getSenders() async {
|
||||
// Mock implementation for getting senders
|
||||
Logs().i('Mock: Getting senders');
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<RTCRtpReceiver>> getReceivers() async {
|
||||
// Mock implementation for getting receivers
|
||||
Logs().i('Mock: Getting receivers');
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<RTCRtpTransceiver>> getTransceivers() async {
|
||||
// Mock implementation for getting transceivers
|
||||
Logs().i('Mock: Getting transceivers');
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RTCRtpSender> addTrack(MediaStreamTrack track,
|
||||
[MediaStream? stream]) async {
|
||||
// Mock implementation for adding a track
|
||||
Logs().i('Mock: Adding track');
|
||||
return MockRTCRtpSender();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> removeTrack(RTCRtpSender sender) async {
|
||||
// Mock implementation for removing a track
|
||||
Logs().i('Mock: Removing track');
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RTCRtpTransceiver> addTransceiver(
|
||||
{MediaStreamTrack? track,
|
||||
RTCRtpMediaType? kind,
|
||||
RTCRtpTransceiverInit? init}) async {
|
||||
// Mock implementation for adding a transceiver
|
||||
Logs().i('Mock: Adding transceiver');
|
||||
return MockRTCRtpTransceiver();
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement receivers
|
||||
Future<List<RTCRtpReceiver>> get receivers => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
// TODO: implement senders
|
||||
Future<List<RTCRtpSender>> get senders => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
// TODO: implement transceivers
|
||||
Future<List<RTCRtpTransceiver>> get transceivers =>
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
class MockRTCRtpTransceiver implements RTCRtpTransceiver {
|
||||
@override
|
||||
Future<TransceiverDirection?> getCurrentDirection() async {
|
||||
// Mock implementation for getting current direction
|
||||
Logs().i('Mock: Getting current direction');
|
||||
return TransceiverDirection.SendRecv;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setDirection(TransceiverDirection direction) async {
|
||||
// Mock implementation for setting direction
|
||||
Logs().i('Mock: Setting direction');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TransceiverDirection> getDirection() async {
|
||||
// Mock implementation for getting direction
|
||||
Logs().i('Mock: Getting direction');
|
||||
return TransceiverDirection.SendRecv;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setCodecPreferences(List<RTCRtpCodecCapability> codecs) async {
|
||||
// Mock implementation for setting codec preferences
|
||||
Logs().i('Mock: Setting codec preferences');
|
||||
}
|
||||
|
||||
@override
|
||||
String get mid => 'mock_mid';
|
||||
|
||||
@override
|
||||
RTCRtpSender get sender => MockRTCRtpSender();
|
||||
|
||||
@override
|
||||
RTCRtpReceiver get receiver => MockRTCRtpReceiver();
|
||||
|
||||
bool get stopped => false;
|
||||
|
||||
@override
|
||||
String get transceiverId => 'mock_transceiver_id';
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
// Mock implementation for stopping transceiver
|
||||
Logs().i('Mock: Stopping transceiver');
|
||||
}
|
||||
|
||||
@override
|
||||
TransceiverDirection get currentDirection {
|
||||
// Deprecated method, should be replaced with `await getCurrentDirection`
|
||||
throw UnimplementedError(
|
||||
'Need to be call asynchronously from native SDK, so the method is deprecated');
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement stoped
|
||||
bool get stoped => throw UnimplementedError();
|
||||
}
|
||||
|
||||
class MockRTCRtpSender implements RTCRtpSender {
|
||||
@override
|
||||
Future<void> dispose() {
|
||||
// TODO: implement dispose
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement dtmfSender
|
||||
RTCDTMFSender get dtmfSender => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<List<StatsReport>> getStats() {
|
||||
// TODO: implement getStats
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement ownsTrack
|
||||
bool get ownsTrack => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
// TODO: implement parameters
|
||||
RTCRtpParameters get parameters => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<void> replaceTrack(MediaStreamTrack? track) {
|
||||
// TODO: implement replaceTrack
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement senderId
|
||||
String get senderId => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<bool> setParameters(RTCRtpParameters parameters) {
|
||||
// TODO: implement setParameters
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setStreams(List<MediaStream> streams) {
|
||||
// TODO: implement setStreams
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setTrack(MediaStreamTrack? track, {bool takeOwnership = true}) {
|
||||
// TODO: implement setTrack
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement track
|
||||
MediaStreamTrack? get track => throw UnimplementedError();
|
||||
// Mock implementation for RTCRtpSender
|
||||
}
|
||||
|
||||
class MockRTCRtpReceiver implements RTCRtpReceiver {
|
||||
@override
|
||||
Function(RTCRtpReceiver rtpReceiver, RTCRtpMediaType mediaType)?
|
||||
onFirstPacketReceived;
|
||||
|
||||
@override
|
||||
Future<List<StatsReport>> getStats() {
|
||||
// TODO: implement getStats
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement parameters
|
||||
RTCRtpParameters get parameters => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
// TODO: implement receiverId
|
||||
String get receiverId => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
// TODO: implement track
|
||||
MediaStreamTrack? get track => throw UnimplementedError();
|
||||
// Mock implementation for RTCRtpReceiver
|
||||
}
|
||||
|
||||
typedef StreamTrackCallback = void Function();
|
||||
|
||||
class MockMediaStreamTrack implements MediaStreamTrack {
|
||||
@override
|
||||
String? get id => 'mock_id';
|
||||
|
||||
@override
|
||||
String? get label => 'mock_label';
|
||||
|
||||
@override
|
||||
String? get kind => 'mock_kind';
|
||||
|
||||
@override
|
||||
StreamTrackCallback? onMute;
|
||||
|
||||
@override
|
||||
StreamTrackCallback? onUnMute;
|
||||
|
||||
@override
|
||||
StreamTrackCallback? onEnded;
|
||||
|
||||
@override
|
||||
bool get enabled => true;
|
||||
|
||||
@override
|
||||
set enabled(bool b) {
|
||||
// Mock implementation for setting enable state
|
||||
Logs().i('Mock: Setting MediaStreamTrack enable state');
|
||||
}
|
||||
|
||||
@override
|
||||
bool? get muted => false;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> getConstraints() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> applyConstraints([Map<String, dynamic>? constraints]) async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MediaStreamTrack> clone() async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
// Mock implementation for stopping the track
|
||||
Logs().i('Mock: Stopping MediaStreamTrack');
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> getSettings() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> switchCamera() async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> adaptRes(int width, int height) async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
void enableSpeakerphone(bool enable) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ByteBuffer> captureFrame() async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> hasTorch() async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setTorch(bool torch) async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
@Deprecated('use stop() instead')
|
||||
Future<void> dispose() async {
|
||||
// Mock implementation for disposing the track
|
||||
Logs().i('Mock: Disposing MediaStreamTrack');
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Track(id: $id, kind: $kind, label: $label, enabled: $enabled, muted: $muted)';
|
||||
}
|
||||
}
|
||||
|
||||
class MockRTCDTMFSender implements RTCDTMFSender {
|
||||
@override
|
||||
Future<void> insertDTMF(String tones,
|
||||
{int duration = 100, int interToneGap = 70}) async {
|
||||
// Mock implementation for inserting DTMF tones
|
||||
Logs().i(
|
||||
'Mock: Inserting DTMF tones: $tones, Duration: $duration, InterToneGap: $interToneGap');
|
||||
}
|
||||
|
||||
@override
|
||||
@Deprecated('Use method insertDTMF instead')
|
||||
Future<void> sendDtmf(String tones,
|
||||
{int duration = 100, int interToneGap = 70}) async {
|
||||
return insertDTMF(tones, duration: duration, interToneGap: interToneGap);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> canInsertDtmf() async {
|
||||
// Mock implementation for checking if DTMF can be inserted
|
||||
Logs().i('Mock: Checking if DTMF can be inserted');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class MockRTCDataChannel implements RTCDataChannel {
|
||||
@override
|
||||
Function(RTCDataChannelState state)? onDataChannelState;
|
||||
|
||||
@override
|
||||
Function(RTCDataChannelMessage data)? onMessage;
|
||||
|
||||
@override
|
||||
Function(int currentAmount, int changedAmount)? onBufferedAmountChange;
|
||||
|
||||
@override
|
||||
Function(int currentAmount)? onBufferedAmountLow;
|
||||
|
||||
@override
|
||||
RTCDataChannelState? get state => RTCDataChannelState.RTCDataChannelOpen;
|
||||
|
||||
@override
|
||||
int? get id => 1;
|
||||
|
||||
@override
|
||||
String? get label => 'mock_label';
|
||||
|
||||
@override
|
||||
int? get bufferedAmount => 0;
|
||||
|
||||
@override
|
||||
int? bufferedAmountLowThreshold;
|
||||
|
||||
@override
|
||||
late Stream<RTCDataChannelState> stateChangeStream;
|
||||
|
||||
@override
|
||||
late Stream<RTCDataChannelMessage> messageStream;
|
||||
|
||||
@override
|
||||
Future<void> send(RTCDataChannelMessage message) async {
|
||||
// Mock implementation for sending a message
|
||||
Logs().i('Mock: Sending RTCDataChannelMessage: $message');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
// Mock implementation for closing the data channel
|
||||
Logs().i('Mock: Closing RTCDataChannel');
|
||||
}
|
||||
}
|
||||
|
||||
class MockMediaStream implements MediaStream {
|
||||
final String _id;
|
||||
final String _ownerTag;
|
||||
bool _isActive = true; // Initially set as active
|
||||
|
||||
MockMediaStream(this._id, this._ownerTag);
|
||||
|
||||
@override
|
||||
Function(MediaStreamTrack track)? onAddTrack;
|
||||
|
||||
@override
|
||||
Function(MediaStreamTrack track)? onRemoveTrack;
|
||||
|
||||
@override
|
||||
String get id => _id;
|
||||
|
||||
@override
|
||||
String get ownerTag => _ownerTag;
|
||||
|
||||
@override
|
||||
bool? get active => _isActive;
|
||||
|
||||
@override
|
||||
Future<void> getMediaTracks() async {
|
||||
// Mock implementation for getting media tracks
|
||||
Logs().i('Mock: Getting media tracks');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addTrack(MediaStreamTrack track,
|
||||
{bool addToNative = true}) async {
|
||||
// Mock implementation for adding a track
|
||||
Logs().i('Mock: Adding track to MediaStream: $track');
|
||||
onAddTrack?.call(track);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeTrack(MediaStreamTrack track,
|
||||
{bool removeFromNative = true}) async {
|
||||
// Mock implementation for removing a track
|
||||
Logs().i('Mock: Removing track from MediaStream: $track');
|
||||
onRemoveTrack?.call(track);
|
||||
}
|
||||
|
||||
@override
|
||||
List<MediaStreamTrack> getTracks() {
|
||||
// Mock implementation for getting all tracks
|
||||
Logs().i('Mock: Getting all tracks');
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
List<MediaStreamTrack> getAudioTracks() {
|
||||
// Mock implementation for getting audio tracks
|
||||
Logs().i('Mock: Getting audio tracks');
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
List<MediaStreamTrack> getVideoTracks() {
|
||||
// Mock implementation for getting video tracks
|
||||
Logs().i('Mock: Getting video tracks');
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
MediaStreamTrack? getTrackById(String trackId) {
|
||||
// Mock implementation for getting a track by ID
|
||||
Logs().i('Mock: Getting track by ID: $trackId');
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MediaStream> clone() async {
|
||||
// Mock implementation for cloning the media stream
|
||||
Logs().i('Mock: Cloning MediaStream');
|
||||
return MockMediaStream('${_id}_clone', _ownerTag);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
// Mock implementation for disposing the media stream
|
||||
Logs().i('Mock: Disposing MediaStream');
|
||||
_isActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
class MockVideoRenderer implements VideoRenderer {
|
||||
@override
|
||||
Function? onResize;
|
||||
@override
|
||||
Function? onFirstFrameRendered;
|
||||
final int _videoWidth = 0;
|
||||
final int _videoHeight = 0;
|
||||
bool _muted = false;
|
||||
final bool _renderVideo = true;
|
||||
int? _textureId;
|
||||
MediaStream? _srcObject;
|
||||
|
||||
@override
|
||||
int get videoWidth => _videoWidth;
|
||||
|
||||
@override
|
||||
int get videoHeight => _videoHeight;
|
||||
|
||||
@override
|
||||
bool get muted => _muted;
|
||||
|
||||
@override
|
||||
set muted(bool mute) {
|
||||
_muted = mute;
|
||||
// Mock implementation for muting/unmuting
|
||||
Logs().i('Mock: Setting mute state: $_muted');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> audioOutput(String deviceId) async {
|
||||
// Mock implementation for changing audio output
|
||||
Logs().i('Mock: Changing audio output to device ID: $deviceId');
|
||||
return true; // Mocking successful audio output change
|
||||
}
|
||||
|
||||
@override
|
||||
bool get renderVideo => _renderVideo;
|
||||
|
||||
@override
|
||||
int? get textureId => _textureId;
|
||||
|
||||
@override
|
||||
Future<void> initialize() async {
|
||||
// Mock implementation for initialization
|
||||
Logs().i('Mock: Initializing VideoRenderer');
|
||||
}
|
||||
|
||||
@override
|
||||
MediaStream? get srcObject => _srcObject;
|
||||
|
||||
@override
|
||||
set srcObject(MediaStream? stream) {
|
||||
_srcObject = stream;
|
||||
// Mock implementation for setting source object
|
||||
Logs().i('Mock: Setting source object for VideoRenderer');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
// Mock implementation for disposing VideoRenderer
|
||||
Logs().i('Mock: Disposing VideoRenderer');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue