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.
PatientApp-KKUMC/lib/pages/videocall-webrtc-rnd/webrtc/signaling.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();
}
}