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 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 _turnCredential; SimpleWebSocket _socket; SessionOneToOne session; Signaling(this._host, {@required this.session}); MediaStream localStream; final List remoteStreams = []; 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 _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 _config = { 'mandatory': {}, 'optional': [ {'DtlsSrtpKeyAgreement': true}, ] }; final Map _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 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 connect() async { _socket = SimpleWebSocket('$_host/ws'); if (_turnCredential == null) { try { _turnCredential = await getTurnCredential(_host); /* { "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 createStream(String media, bool userScreen) async { final Map 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 _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, // sdpMlineIndex or 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 _createDataChannel(SessionOneToOne session, {label: 'fileTransfer'}) async { RTCDataChannelInit dataChannelDict = RTCDataChannelInit() ..maxRetransmits = 30; RTCDataChannel channel = await session.pc .createDataChannel(label, dataChannelDict); _addDataChannel(session, channel); } Future _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 _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 finishSessions() async { _closeSessionById(session.id); } void _closeSessionById(String sessionId) { if (session != null && session.id == sessionId) { _closeSession(session); onCallStateChange?.call(session, CallState.Bye); } } Future _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(); } }