chore: Use webrtc interface to build the voip module.
This commit is contained in:
parent
56d9ba7d4a
commit
93b623f2d5
|
|
@ -1,11 +1,57 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:core';
|
import 'dart:core';
|
||||||
|
|
||||||
import 'voip_abstract.dart';
|
import 'package:webrtc_interface/webrtc_interface.dart';
|
||||||
import 'package:sdp_transform/sdp_transform.dart' as sdp_transform;
|
import 'package:sdp_transform/sdp_transform.dart' as sdp_transform;
|
||||||
|
|
||||||
import '../matrix.dart';
|
import '../matrix.dart';
|
||||||
|
|
||||||
|
MediaDevices mediaDevices = Null as MediaDevices;
|
||||||
|
RTCFactory factory = Null as RTCFactory;
|
||||||
|
|
||||||
|
class RTCVideoRenderer extends VideoRenderer {
|
||||||
|
RTCVideoRenderer() : super() {
|
||||||
|
muted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
late bool muted;
|
||||||
|
|
||||||
|
@override
|
||||||
|
MediaStream? srcObject;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> audioOutput(String deviceId) {
|
||||||
|
// TODO: implement audioOutput
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> initialize() {
|
||||||
|
// TODO: implement initialize
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
// TODO: implement renderVideo
|
||||||
|
bool get renderVideo => throw UnimplementedError();
|
||||||
|
|
||||||
|
@override
|
||||||
|
// TODO: implement textureId
|
||||||
|
int? get textureId => throw UnimplementedError();
|
||||||
|
|
||||||
|
@override
|
||||||
|
// TODO: implement videoHeight
|
||||||
|
int get videoHeight => throw UnimplementedError();
|
||||||
|
|
||||||
|
@override
|
||||||
|
// TODO: implement videoWidth
|
||||||
|
int get videoWidth => throw UnimplementedError();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() => throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
/// The default life time for call events, in millisecond.
|
/// The default life time for call events, in millisecond.
|
||||||
const lifetimeMs = 10 * 1000;
|
const lifetimeMs = 10 * 1000;
|
||||||
|
|
||||||
|
|
@ -393,7 +439,7 @@ class CallSession {
|
||||||
try {
|
try {
|
||||||
await pc!.setRemoteDescription(description);
|
await pc!.setRemoteDescription(description);
|
||||||
if (description.type == 'offer') {
|
if (description.type == 'offer') {
|
||||||
final answer = await pc!.createAnswer();
|
final answer = await pc!.createAnswer({});
|
||||||
await room.sendCallNegotiate(
|
await room.sendCallNegotiate(
|
||||||
callId, lifetimeMs, localPartyId, answer.sdp!,
|
callId, lifetimeMs, localPartyId, answer.sdp!,
|
||||||
type: answer.type!);
|
type: answer.type!);
|
||||||
|
|
@ -555,7 +601,8 @@ class CallSession {
|
||||||
|
|
||||||
if (purpose == SDPStreamMetadataPurpose.Usermedia) {
|
if (purpose == SDPStreamMetadataPurpose.Usermedia) {
|
||||||
speakerOn = type == CallType.kVideo;
|
speakerOn = type == CallType.kVideo;
|
||||||
if (!kIsWeb && !voip.background) {
|
//TODO: Confirm that the platform is not Web.
|
||||||
|
if (/*!kIsWeb && */ !voip.background) {
|
||||||
final audioTrack = stream.getAudioTracks()[0];
|
final audioTrack = stream.getAudioTracks()[0];
|
||||||
audioTrack.enableSpeakerphone(speakerOn);
|
audioTrack.enableSpeakerphone(speakerOn);
|
||||||
}
|
}
|
||||||
|
|
@ -658,13 +705,16 @@ class CallSession {
|
||||||
speakerOn = !speakerOn;
|
speakerOn = !speakerOn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: move to the app.
|
||||||
Future<void> switchCamera() async {
|
Future<void> switchCamera() async {
|
||||||
if (localUserMediaStream != null) {
|
if (localUserMediaStream != null) {
|
||||||
|
/*
|
||||||
await Helper.switchCamera(
|
await Helper.switchCamera(
|
||||||
localUserMediaStream!.stream!.getVideoTracks()[0]);
|
localUserMediaStream!.stream!.getVideoTracks()[0]);
|
||||||
if (kIsMobile) {
|
if (kIsMobile) {
|
||||||
facingMode == 'user' ? facingMode = 'environment' : facingMode = 'user';
|
facingMode == 'user' ? facingMode = 'environment' : facingMode = 'user';
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -960,7 +1010,7 @@ class CallSession {
|
||||||
: false,
|
: false,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
return await navigator.mediaDevices.getUserMedia(mediaConstraints);
|
return await mediaDevices.getUserMedia(mediaConstraints);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_getUserMediaFailed(e);
|
_getUserMediaFailed(e);
|
||||||
}
|
}
|
||||||
|
|
@ -973,7 +1023,7 @@ class CallSession {
|
||||||
'video': true,
|
'video': true,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
return await navigator.mediaDevices.getDisplayMedia(mediaConstraints);
|
return await mediaDevices.getDisplayMedia(mediaConstraints);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_getUserMediaFailed(e);
|
_getUserMediaFailed(e);
|
||||||
}
|
}
|
||||||
|
|
@ -985,7 +1035,7 @@ class CallSession {
|
||||||
'iceServers': opts.iceServers,
|
'iceServers': opts.iceServers,
|
||||||
'sdpSemantics': 'unified-plan'
|
'sdpSemantics': 'unified-plan'
|
||||||
};
|
};
|
||||||
final pc = await createPeerConnection(configuration);
|
final pc = await factory.createPeerConnection(configuration);
|
||||||
pc.onTrack = (RTCTrackEvent event) {
|
pc.onTrack = (RTCTrackEvent event) {
|
||||||
if (event.streams.isNotEmpty) {
|
if (event.streams.isNotEmpty) {
|
||||||
final stream = event.streams[0];
|
final stream = event.streams[0];
|
||||||
|
|
@ -1115,47 +1165,16 @@ class CallSession {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class VoIP with WidgetsBindingObserver {
|
class VoIP {
|
||||||
TurnServerCredentials? _turnServerCredentials;
|
TurnServerCredentials? _turnServerCredentials;
|
||||||
Map<String, CallSession> calls = <String, CallSession>{};
|
Map<String, CallSession> calls = <String, CallSession>{};
|
||||||
String? currentCID;
|
String? currentCID;
|
||||||
//ConnectivityResult _currentConnectivity;
|
Function(CallSession session)? onNewCall;
|
||||||
Function(CallSession session)? onIncomingCall;
|
Function(CallSession session)? onCallEnded;
|
||||||
OverlayEntry? overlayEntry;
|
|
||||||
String? get localPartyId => client.deviceID;
|
String? get localPartyId => client.deviceID;
|
||||||
bool background = false;
|
bool background = false;
|
||||||
Client client;
|
Client client;
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
||||||
Logs().v('AppLifecycleState = $state');
|
|
||||||
background = !(state != AppLifecycleState.detached &&
|
|
||||||
state != AppLifecycleState.paused);
|
|
||||||
}
|
|
||||||
|
|
||||||
void addCallingOverlay(
|
|
||||||
BuildContext context, String callId, CallSession call) {
|
|
||||||
if (overlayEntry != null) {
|
|
||||||
Logs().w('[VOIP] addCallingOverlay: The call session already exists?');
|
|
||||||
overlayEntry?.remove();
|
|
||||||
}
|
|
||||||
/* TODO:
|
|
||||||
final overlay = Overlay.of(context);
|
|
||||||
overlayEntry = OverlayEntry(
|
|
||||||
builder: (_) => Calling(
|
|
||||||
context: famedly.context,
|
|
||||||
client: client,
|
|
||||||
callId: callId,
|
|
||||||
call: call,
|
|
||||||
onClear: () {
|
|
||||||
overlayEntry?.remove();
|
|
||||||
overlayEntry = null;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
overlay.insert(overlayEntry);
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
VoIP(this.client) : super() {
|
VoIP(this.client) : super() {
|
||||||
client.onCallInvite.stream.listen(onCallInvite);
|
client.onCallInvite.stream.listen(onCallInvite);
|
||||||
client.onCallAnswer.stream.listen(onCallAnswer);
|
client.onCallAnswer.stream.listen(onCallAnswer);
|
||||||
|
|
@ -1168,19 +1187,19 @@ class VoIP with WidgetsBindingObserver {
|
||||||
client.onSDPStreamMetadataChangedReceived.stream
|
client.onSDPStreamMetadataChangedReceived.stream
|
||||||
.listen(onSDPStreamMetadataChangedReceived);
|
.listen(onSDPStreamMetadataChangedReceived);
|
||||||
client.onAssertedIdentityReceived.stream.listen(onAssertedIdentityReceived);
|
client.onAssertedIdentityReceived.stream.listen(onAssertedIdentityReceived);
|
||||||
/*
|
|
||||||
|
/* TODO: implement this in the fanedly-app.
|
||||||
Connectivity().onConnectivityChanged.listen(_handleNetworkChanged);
|
Connectivity().onConnectivityChanged.listen(_handleNetworkChanged);
|
||||||
Connectivity()
|
Connectivity()
|
||||||
.checkConnectivity()
|
.checkConnectivity()
|
||||||
.then((result) => _currentConnectivity = result)
|
.then((result) => _currentConnectivity = result)
|
||||||
.catchError((e) => _currentConnectivity = ConnectivityResult.none);
|
.catchError((e) => _currentConnectivity = ConnectivityResult.none);
|
||||||
*/
|
|
||||||
|
|
||||||
if (!kIsWeb) {
|
if (!kIsWeb) {
|
||||||
final wb = WidgetsBinding.instance;
|
final wb = WidgetsBinding.instance;
|
||||||
wb!.addObserver(this);
|
wb!.addObserver(this);
|
||||||
didChangeAppLifecycleState(wb.lifecycleState!);
|
didChangeAppLifecycleState(wb.lifecycleState!);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onCallInvite(Event event) async {
|
Future<void> onCallInvite(Event event) async {
|
||||||
|
|
@ -1254,8 +1273,7 @@ class VoIP with WidgetsBindingObserver {
|
||||||
.then((_) {
|
.then((_) {
|
||||||
// Popup CallingPage for incoming call.
|
// Popup CallingPage for incoming call.
|
||||||
if (!background) {
|
if (!background) {
|
||||||
//TODO:
|
onNewCall?.call(newCall);
|
||||||
//addCallingOverlay(famedly.context, callId, newCall);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
currentCID = callId;
|
currentCID = callId;
|
||||||
|
|
@ -1264,8 +1282,7 @@ class VoIP with WidgetsBindingObserver {
|
||||||
/// Forced to enable signaling synchronization until the end of the call.
|
/// Forced to enable signaling synchronization until the end of the call.
|
||||||
client.backgroundSync = true;
|
client.backgroundSync = true;
|
||||||
|
|
||||||
/// Handle incoming call for callkeep plugin.
|
///TODO: notify the callkeep that the call is incoming.
|
||||||
onIncomingCall?.call(newCall);
|
|
||||||
}
|
}
|
||||||
// Play ringtone
|
// Play ringtone
|
||||||
playRingtone();
|
playRingtone();
|
||||||
|
|
@ -1352,9 +1369,7 @@ class VoIP with WidgetsBindingObserver {
|
||||||
// hangup in any case, either if the other party hung up or we did on another device
|
// hangup in any case, either if the other party hung up or we did on another device
|
||||||
call.terminate(CallParty.kRemote,
|
call.terminate(CallParty.kRemote,
|
||||||
event.content['reason'] ?? CallErrorCode.UserHangup, true);
|
event.content['reason'] ?? CallErrorCode.UserHangup, true);
|
||||||
|
onCallEnded?.call(call);
|
||||||
overlayEntry?.remove();
|
|
||||||
overlayEntry = null;
|
|
||||||
} else {
|
} else {
|
||||||
Logs().v('[VOIP] onCallHangup: Session [$callId] not found!');
|
Logs().v('[VOIP] onCallHangup: Session [$callId] not found!');
|
||||||
}
|
}
|
||||||
|
|
@ -1534,8 +1549,7 @@ class VoIP with WidgetsBindingObserver {
|
||||||
currentCID = callId;
|
currentCID = callId;
|
||||||
await newCall.initOutboundCall(type).then((_) {
|
await newCall.initOutboundCall(type).then((_) {
|
||||||
if (!background) {
|
if (!background) {
|
||||||
//TODO:
|
onNewCall?.call(newCall);
|
||||||
//addCallingOverlay(famdly.context, callId, newCall);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
currentCID = callId;
|
currentCID = callId;
|
||||||
|
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
class MediaStreamTrack {
|
|
||||||
bool get enabled => false;
|
|
||||||
set enabled(bool value) {}
|
|
||||||
String get kind => throw UnimplementedError();
|
|
||||||
Future<void> stop() async {}
|
|
||||||
Future<void> enableSpeakerphone(bool enable) async {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MediaStream {
|
|
||||||
String get id => throw UnimplementedError();
|
|
||||||
List<MediaStreamTrack> getAudioTracks() => throw UnimplementedError();
|
|
||||||
List<MediaStreamTrack> getVideoTracks() => throw UnimplementedError();
|
|
||||||
List<MediaStreamTrack> getTracks() => throw UnimplementedError();
|
|
||||||
Future<void> dispose() async {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RTCPeerConnection {
|
|
||||||
Function(RTCTrackEvent event)? onTrack;
|
|
||||||
Function()? onRenegotiationNeeded;
|
|
||||||
Function(RTCIceCandidate)? onIceCandidate;
|
|
||||||
Function(dynamic state)? onIceGatheringState;
|
|
||||||
Function(dynamic state)? onIceConnectionState;
|
|
||||||
|
|
||||||
Future<RTCSessionDescription> createOffer(Map<String, dynamic> constraints) {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<RTCSessionDescription> createAnswer(Map<String, dynamic> constraints) {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setRemoteDescription(RTCSessionDescription description) async {}
|
|
||||||
Future<void> setLocalDescription(RTCSessionDescription description) async {}
|
|
||||||
|
|
||||||
Future<RTCRtpSender> addTrack(
|
|
||||||
MediaStreamTrack track, MediaStream stream) async {
|
|
||||||
return RTCRtpSender();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> removeTrack(RTCRtpSender sender) async {}
|
|
||||||
|
|
||||||
Future<void> close() async {}
|
|
||||||
|
|
||||||
Future<void> dispose() async {}
|
|
||||||
|
|
||||||
Future<void> addIceCandidate(RTCIceCandidate candidate) async {}
|
|
||||||
|
|
||||||
Future<void> addStream(MediaStream stream) async {}
|
|
||||||
|
|
||||||
Future<void> removeStream(MediaStream stream) async {}
|
|
||||||
|
|
||||||
Future<List<dynamic>> getTransceivers() async {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<RTCRtpSender>> getSenders() async {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> addCandidate(RTCIceCandidate candidate) async {}
|
|
||||||
|
|
||||||
dynamic get signalingState => throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
class RTCIceCandidate {
|
|
||||||
String get candidate => throw UnimplementedError();
|
|
||||||
String get sdpMid => throw UnimplementedError();
|
|
||||||
int get sdpMLineIndex => throw UnimplementedError();
|
|
||||||
Map<String, dynamic> toMap() => throw UnimplementedError();
|
|
||||||
RTCIceCandidate(String candidate, String sdpMid, int sdpMLineIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
class RTCRtpSender {
|
|
||||||
MediaStreamTrack? get track => throw UnimplementedError();
|
|
||||||
DtmfSender get dtmfSender => throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
class RTCSessionDescription {
|
|
||||||
late String type;
|
|
||||||
late String sdp;
|
|
||||||
RTCSessionDescription(this.sdp, this.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
class RTCTrackEvent {
|
|
||||||
late List<MediaStream> streams;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum TransceiverDirection {
|
|
||||||
SendRecv,
|
|
||||||
SendOnly,
|
|
||||||
RecvOnly,
|
|
||||||
Inactive,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum RTCSignalingState { RTCSignalingStateStable }
|
|
||||||
|
|
||||||
class RTCVideoRenderer {}
|
|
||||||
|
|
||||||
const kIsWeb = false;
|
|
||||||
|
|
||||||
bool get kIsMobile => !kIsWeb && (Platform.isAndroid || Platform.isIOS);
|
|
||||||
|
|
||||||
class Helper {
|
|
||||||
static Future<void> switchCamera(MediaStreamTrack track) async {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DtmfSender {
|
|
||||||
Future<void> insertDTMF(String tones) async {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<MediaStream> createPeerConnection(
|
|
||||||
Map<String, dynamic> constraints) async {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
@ -23,6 +23,7 @@ dependencies:
|
||||||
slugify: ^2.0.0
|
slugify: ^2.0.0
|
||||||
html: ^0.15.0
|
html: ^0.15.0
|
||||||
collection: ^1.15.0
|
collection: ^1.15.0
|
||||||
|
webrtc_interface: ^1.0.0
|
||||||
sdp_transform: ^0.3.2
|
sdp_transform: ^0.3.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue