You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
511 lines
14 KiB
Dart
511 lines
14 KiB
Dart
import 'dart:convert';
|
|
import 'dart:async';
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
|
|
|
import '../utils/websocket.dart' if (dart.library.js) '../utils/websocket_web.dart';
|
|
import '../utils/turn.dart' if (dart.library.js) '../utils/turn_web.dart';
|
|
|
|
|
|
enum SignalingState { Open, Closed, Error }
|
|
enum CallState { Calling, Ringing, Invite, Connected, Bye }
|
|
|
|
|
|
const JsonEncoder _encoder = JsonEncoder();
|
|
const JsonDecoder _decoder = JsonDecoder();
|
|
|
|
class SessionOneToOne {
|
|
String id;
|
|
SocketUser local_user;
|
|
SocketUser remote_user;
|
|
SessionOneToOne({@required this.id, @required this.local_user, @required this.remote_user});
|
|
|
|
|
|
RTCPeerConnection pc;
|
|
RTCDataChannel dc;
|
|
List<RTCIceCandidate> remoteCandidates = [];
|
|
}
|
|
|
|
class SocketUser{
|
|
String id;
|
|
String name;
|
|
String userAgent;
|
|
Map moreInfo;
|
|
|
|
SocketUser({@required this.id, @required this.name, @required this.userAgent, @required this.moreInfo});
|
|
|
|
SocketUser.from(dynamic json){
|
|
id = json['id'];
|
|
name = json['name'];
|
|
userAgent = json['user_agent'];
|
|
moreInfo = json['more_info'];
|
|
}
|
|
|
|
Map toJson() => {
|
|
"id": id,
|
|
"name": name,
|
|
"user_agent": userAgent,
|
|
"more_info": moreInfo
|
|
};
|
|
}
|
|
|
|
class Signaling {
|
|
var _host;
|
|
var _port = 8086;
|
|
var _turnCredential;
|
|
|
|
SimpleWebSocket _socket;
|
|
SessionOneToOne session;
|
|
|
|
Signaling(this._host, {@required this.session});
|
|
|
|
MediaStream localStream;
|
|
final List<MediaStream> remoteStreams = <MediaStream>[];
|
|
|
|
Function(SignalingState state) onSignalingStateChange;
|
|
Function(SessionOneToOne session, CallState state) onCallStateChange;
|
|
Function(MediaStream stream) onLocalStream;
|
|
Function(SessionOneToOne session, MediaStream stream) onAddRemoteStream;
|
|
Function(SessionOneToOne session, MediaStream stream) onRemoveRemoteStream;
|
|
Function(dynamic event) onPeersUpdate;
|
|
Function(dynamic event) onConnected;
|
|
Function(dynamic event) onRemoteConnected;
|
|
Function(SessionOneToOne session, RTCDataChannel dc, RTCDataChannelMessage data) onDataChannelMessage;
|
|
Function(SessionOneToOne session, RTCDataChannel dc) onDataChannel;
|
|
|
|
String get sdpSemantics => WebRTC.platformIsWindows ? 'plan-b' : 'unified-plan';
|
|
|
|
Map<String, dynamic> _iceServers = {
|
|
'iceServers': [
|
|
{'url': 'stun:stun.l.google.com:19302'},
|
|
/*
|
|
* turn server configuration example.
|
|
{
|
|
'url': 'turn:123.45.67.89:3478',
|
|
'username': 'change_to_real_user',
|
|
'credential': 'change_to_real_secret'
|
|
},
|
|
*/
|
|
]
|
|
};
|
|
|
|
final Map<String, dynamic> _config = {
|
|
'mandatory': {},
|
|
'optional': [
|
|
{'DtlsSrtpKeyAgreement': true},
|
|
]
|
|
};
|
|
|
|
final Map<String, dynamic> _dcConstraints = {
|
|
'mandatory': {
|
|
'OfferToReceiveAudio': false,
|
|
'OfferToReceiveVideo': false,
|
|
},
|
|
'optional': [],
|
|
};
|
|
|
|
|
|
close() async {
|
|
await finishSessions();
|
|
_socket?.close();
|
|
}
|
|
|
|
void switchCamera() {
|
|
if (localStream != null) {
|
|
Helper.switchCamera(localStream .getVideoTracks()[0]);
|
|
}
|
|
}
|
|
|
|
void muteMic() {
|
|
if (localStream != null) {
|
|
bool enabled = localStream .getAudioTracks()[0].enabled;
|
|
localStream .getAudioTracks()[0].enabled = !enabled;
|
|
}
|
|
}
|
|
|
|
|
|
callAccepted(SessionOneToOne session){
|
|
_send('call_accepted', {
|
|
'to': session.remote_user?.id,
|
|
'from': session.local_user.id,
|
|
'session_id': session.id,
|
|
});
|
|
}
|
|
|
|
void offer(String media, bool useScreen) async {
|
|
if(session == null)
|
|
return;
|
|
|
|
if (media == 'data') {
|
|
_createDataChannel(session);
|
|
}
|
|
_createOffer(session, media);
|
|
onCallStateChange?.call(session, CallState.Calling);
|
|
}
|
|
|
|
void bye(SessionOneToOne session) {
|
|
_send('bye', {
|
|
'session_id': session.id,
|
|
'from': session.local_user.id,
|
|
});
|
|
_closeSession(session);
|
|
}
|
|
|
|
void onMessage(message) async {
|
|
Map<String, dynamic> mapData = message;
|
|
var data = mapData['data'];
|
|
|
|
switch (mapData['type']) {
|
|
case 'call_accepted':
|
|
{
|
|
onRemoteConnected?.call(data);
|
|
}
|
|
break;
|
|
|
|
case 'connected':
|
|
{
|
|
if (onConnected != null) {
|
|
onConnected?.call(data);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'offer':
|
|
{
|
|
|
|
var peerId = data['from'];
|
|
var description = data['description'];
|
|
var media = data['media'];
|
|
|
|
await _initPeerConnection(session, media: media, screenSharing: false);
|
|
await session.pc?.setRemoteDescription(RTCSessionDescription(description['sdp'], description['type']));
|
|
await _createAnswer(session, media);
|
|
|
|
if (session.remoteCandidates.isNotEmpty) {
|
|
session.remoteCandidates.forEach((candidate) async {
|
|
await session.pc?.addCandidate(candidate);
|
|
});
|
|
session.remoteCandidates.clear();
|
|
}
|
|
onCallStateChange?.call(session, CallState.Calling);
|
|
}
|
|
break;
|
|
case 'answer':
|
|
{
|
|
|
|
var description = data['description'];
|
|
var sessionId = data['session_id'];
|
|
session.pc?.setRemoteDescription(
|
|
RTCSessionDescription(description['sdp'], description['type']));
|
|
}
|
|
break;
|
|
case 'candidate':
|
|
{
|
|
|
|
var peerId = data['from'];
|
|
var candidateMap = data['candidate'];
|
|
var sessionId = data['session_id'];
|
|
RTCIceCandidate candidate = RTCIceCandidate(candidateMap['candidate'],
|
|
candidateMap['sdpMid'], candidateMap['sdpMLineIndex']);
|
|
|
|
if (session != null) {
|
|
if (session.pc != null) {
|
|
await session.pc?.addCandidate(candidate);
|
|
} else {
|
|
session.remoteCandidates.add(candidate);
|
|
}
|
|
} else {
|
|
// _sessions[sessionId] = SessionOneToOne(pid: peerId, sid: sessionId)
|
|
// ..remoteCandidates.add(candidate);
|
|
}
|
|
}
|
|
break;
|
|
case 'leave':
|
|
{
|
|
var peerId = data as String;
|
|
_closeSessionById(peerId);
|
|
}
|
|
break;
|
|
case 'bye':
|
|
{
|
|
|
|
var sessionId = data['session_id'];
|
|
print('bye: ' + sessionId);
|
|
if (session != null) {
|
|
onCallStateChange?.call(session, CallState.Bye);
|
|
_closeSession(session);
|
|
}
|
|
}
|
|
break;
|
|
case 'keepalive':
|
|
{
|
|
print('keepalive response!');
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
Future<void> connect() async {
|
|
_socket = SimpleWebSocket('https://$_host:$_port/ws');
|
|
|
|
if (_turnCredential == null) {
|
|
try {
|
|
_turnCredential = await getTurnCredential(_host, _port);
|
|
/*
|
|
{
|
|
"username": "1584195784:mbzrxpgjys",
|
|
"password": "isyl6FF6nqMTB9/ig5MrMRUXqZg",
|
|
"ttl": 86400,
|
|
"uris": ["turn:127.0.0.1:19302?transport=udp"]
|
|
}
|
|
*/
|
|
_iceServers = {
|
|
'iceServers': [
|
|
{
|
|
'urls': _turnCredential['uris'][0],
|
|
'username': _turnCredential['username'],
|
|
'credential': _turnCredential['password']
|
|
},
|
|
]
|
|
};
|
|
} catch (e) {}
|
|
}
|
|
|
|
_socket?.onOpen = () {
|
|
print('onOpen');
|
|
onSignalingStateChange?.call(SignalingState.Open);
|
|
_send('connect', session.local_user.toJson());
|
|
};
|
|
|
|
_socket?.onMessage = (message) {
|
|
print('Received data: ' + message);
|
|
onMessage(_decoder.convert(message));
|
|
};
|
|
|
|
_socket?.onClose = (int code, String reason) {
|
|
print('Closed by server [$code => $reason]!');
|
|
onSignalingStateChange?.call(SignalingState.Closed);
|
|
};
|
|
|
|
await _socket?.connect();
|
|
}
|
|
|
|
Future<MediaStream> createStream(String media, bool userScreen) async {
|
|
final Map<String, dynamic> mediaConstraints = {
|
|
'audio': userScreen ? false : true,
|
|
'video': userScreen
|
|
? true
|
|
: {
|
|
'mandatory': {
|
|
'minWidth': '640', // Provide your own width, height and frame rate here
|
|
'minHeight': '480',
|
|
'minFrameRate': '30',
|
|
},
|
|
'facingMode': 'user',
|
|
'optional': [],
|
|
}
|
|
};
|
|
|
|
MediaStream stream = userScreen
|
|
? await navigator.mediaDevices.getDisplayMedia(mediaConstraints)
|
|
: await navigator.mediaDevices.getUserMedia(mediaConstraints);
|
|
onLocalStream?.call(stream);
|
|
return stream;
|
|
}
|
|
|
|
Future<SessionOneToOne> _initPeerConnection(SessionOneToOne session, {@required String media, @required bool screenSharing}) async {
|
|
|
|
if (media != 'data')
|
|
localStream = await createStream(media, screenSharing);
|
|
print(_iceServers);
|
|
RTCPeerConnection pc = await createPeerConnection({
|
|
..._iceServers,
|
|
...{'sdpSemantics': sdpSemantics}
|
|
}, _config);
|
|
if (media != 'data') {
|
|
switch (sdpSemantics) {
|
|
case 'plan-b':
|
|
pc.onAddStream = (MediaStream stream) {
|
|
onAddRemoteStream?.call(session, stream);
|
|
remoteStreams.add(stream);
|
|
};
|
|
await pc.addStream(localStream);
|
|
break;
|
|
case 'unified-plan':
|
|
// Unified-Plan
|
|
pc.onTrack = (event) {
|
|
if (event.track.kind == 'video') {
|
|
onAddRemoteStream?.call(session, event.streams[0]);
|
|
}
|
|
};
|
|
localStream .getTracks().forEach((track) {
|
|
pc.addTrack(track, localStream);
|
|
});
|
|
break;
|
|
}
|
|
|
|
// Unified-Plan: Simuclast
|
|
/*
|
|
await pc.addTransceiver(
|
|
track: _localStream.getAudioTracks()[0],
|
|
init: RTCRtpTransceiverInit(
|
|
direction: TransceiverDirection.SendOnly, streams: [_localStream]),
|
|
);
|
|
|
|
await pc.addTransceiver(
|
|
track: _localStream.getVideoTracks()[0],
|
|
init: RTCRtpTransceiverInit(
|
|
direction: TransceiverDirection.SendOnly,
|
|
streams: [
|
|
_localStream
|
|
],
|
|
sendEncodings: [
|
|
RTCRtpEncoding(rid: 'f', active: true),
|
|
RTCRtpEncoding(
|
|
rid: 'h',
|
|
active: true,
|
|
scaleResolutionDownBy: 2.0,
|
|
maxBitrate: 150000,
|
|
),
|
|
RTCRtpEncoding(
|
|
rid: 'q',
|
|
active: true,
|
|
scaleResolutionDownBy: 4.0,
|
|
maxBitrate: 100000,
|
|
),
|
|
]),
|
|
);*/
|
|
/*
|
|
var sender = pc.getSenders().find(s => s.track.kind == "video");
|
|
var parameters = sender.getParameters();
|
|
if(!parameters)
|
|
parameters = {};
|
|
parameters.encodings = [
|
|
{ rid: "h", active: true, maxBitrate: 900000 },
|
|
{ rid: "m", active: true, maxBitrate: 300000, scaleResolutionDownBy: 2 },
|
|
{ rid: "l", active: true, maxBitrate: 100000, scaleResolutionDownBy: 4 }
|
|
];
|
|
sender.setParameters(parameters);
|
|
*/
|
|
}
|
|
pc.onIceCandidate = (candidate) async {
|
|
if (candidate == null) {
|
|
print('onIceCandidate: complete!');
|
|
return;
|
|
}
|
|
// This delay is needed to allow enough time to try an ICE candidate
|
|
// before skipping to the next one. 1 second is just an heuristic value
|
|
// and should be thoroughly tested in your own environment.
|
|
await Future.delayed(
|
|
const Duration(seconds: 1),
|
|
() => _send('candidate', {
|
|
'to': session.remote_user?.id,
|
|
'from': session.local_user.id,
|
|
'candidate': {
|
|
'sdpMLineIndex': candidate.sdpMlineIndex,
|
|
'sdpMid': candidate.sdpMid,
|
|
'candidate': candidate.candidate,
|
|
},
|
|
'session_id': session.id,
|
|
}));
|
|
};
|
|
|
|
pc.onIceConnectionState = (state) {};
|
|
|
|
pc.onRemoveStream = (stream) {
|
|
onRemoveRemoteStream?.call(session, stream);
|
|
remoteStreams.removeWhere((it) {
|
|
return (it.id == stream.id);
|
|
});
|
|
};
|
|
|
|
pc.onDataChannel = (channel) {
|
|
_addDataChannel(session, channel);
|
|
};
|
|
|
|
session.pc = pc;
|
|
return session;
|
|
}
|
|
|
|
void _addDataChannel(SessionOneToOne session, RTCDataChannel channel) {
|
|
channel.onDataChannelState = (e) {};
|
|
channel.onMessage = (RTCDataChannelMessage data) {
|
|
onDataChannelMessage?.call(session, channel, data);
|
|
};
|
|
session.dc = channel;
|
|
onDataChannel?.call(session, channel);
|
|
}
|
|
|
|
Future<void> _createDataChannel(SessionOneToOne session, {label: 'fileTransfer'}) async {
|
|
RTCDataChannelInit dataChannelDict = RTCDataChannelInit()
|
|
..maxRetransmits = 30;
|
|
RTCDataChannel channel =
|
|
await session.pc .createDataChannel(label, dataChannelDict);
|
|
_addDataChannel(session, channel);
|
|
}
|
|
|
|
Future<void> _createOffer(SessionOneToOne session, String media) async {
|
|
try {
|
|
RTCSessionDescription s =
|
|
await session.pc .createOffer(media == 'data' ? _dcConstraints : {});
|
|
await session.pc .setLocalDescription(s);
|
|
_send('offer', {
|
|
'to': session.remote_user?.id,
|
|
'from': session.local_user.id,
|
|
'description': {'sdp': s.sdp, 'type': s.type},
|
|
'session_id': session.id,
|
|
'media': media,
|
|
});
|
|
} catch (e) {
|
|
print(e.toString());
|
|
}
|
|
}
|
|
|
|
Future<void> _createAnswer(SessionOneToOne session, String media) async {
|
|
try {
|
|
RTCSessionDescription s =
|
|
await session.pc .createAnswer(media == 'data' ? _dcConstraints : {});
|
|
await session.pc .setLocalDescription(s);
|
|
_send('answer', {
|
|
'to': session.remote_user?.id,
|
|
'from': session.local_user.id,
|
|
'description': {'sdp': s.sdp, 'type': s.type},
|
|
'session_id': session.id,
|
|
});
|
|
} catch (e) {
|
|
print(e.toString());
|
|
}
|
|
}
|
|
|
|
_send(event, data) {
|
|
var request = Map();
|
|
request["type"] = event;
|
|
request["data"] = data;
|
|
_socket?.send(_encoder.convert(request));
|
|
}
|
|
|
|
Future<void> finishSessions() async {
|
|
_closeSessionById(session.id);
|
|
}
|
|
|
|
void _closeSessionById(String sessionId) {
|
|
if (session != null && session.id == sessionId) {
|
|
_closeSession(session);
|
|
onCallStateChange?.call(session, CallState.Bye);
|
|
}
|
|
}
|
|
|
|
Future<void> _closeSession(SessionOneToOne session) async {
|
|
localStream?.getTracks()?.forEach((element) async {
|
|
await element.stop();
|
|
});
|
|
await localStream?.dispose();
|
|
localStream = null;
|
|
|
|
await session.pc?.close();
|
|
await session.dc?.close();
|
|
}
|
|
}
|