Merge branch 'zik_webrtc_dev_v2.5' into 'development_v2.5'
Project Filter Applied on Packages & Offers See merge request Cloud_Solution/diplomatic-quarter!599merge-requests/602/merge
commit
d43d6c686b
@ -0,0 +1,44 @@
|
||||
class OfferProjectsResponseModel {
|
||||
OfferProjectsResponseModel({
|
||||
this.project,});
|
||||
|
||||
OfferProjectsResponseModel.fromJson(dynamic json) {
|
||||
if (json['stores'] != null) {
|
||||
project = [];
|
||||
json['stores'].forEach((v) {
|
||||
project.add(OfferProject.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
List<OfferProject> project;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
if (project != null) {
|
||||
map['stores'] = project.map((v) => v.toJson()).toList();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class OfferProject {
|
||||
OfferProject({
|
||||
this.name,
|
||||
this.id,});
|
||||
|
||||
OfferProject.fromJson(dynamic json) {
|
||||
name = json['name'];
|
||||
id = json['id'];
|
||||
}
|
||||
String name;
|
||||
int id;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['name'] = name;
|
||||
map['id'] = id;
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
@JS()
|
||||
library streammixer.js;
|
||||
import 'package:js/js.dart';
|
||||
|
||||
|
||||
@JS('multiStreamsMixerFlutter')
|
||||
external initMultiStreamsMixer(arrayOfMediaStreams);
|
||||
|
||||
@JS("MultiStreamsMixer")
|
||||
class MultiStreamsMixer {
|
||||
external MultiStreamsMixer(arrayOfMediaStreams, elementClass);
|
||||
|
||||
external getMixedStream();
|
||||
external start();
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
// import 'dart:async';
|
||||
// import 'dart:html' as html;
|
||||
//
|
||||
// import 'dart:typed_data';
|
||||
//
|
||||
// extension xHtmlBlob on html.Blob{
|
||||
//
|
||||
// /* File Reader --- */
|
||||
// Future<html.FileReader> _fileDataUrlReader()async{
|
||||
// html.FileReader reader = html.FileReader();
|
||||
// reader.readAsDataUrl(this);
|
||||
// return reader;
|
||||
// }
|
||||
//
|
||||
// Future<html.FileReader> _fileByteReader()async{
|
||||
// html.FileReader reader = html.FileReader();
|
||||
// reader.readAsArrayBuffer(this);
|
||||
// return reader;
|
||||
// }
|
||||
// /* --- File Reader */
|
||||
//
|
||||
// Future<dynamic> readDataUrl() async{
|
||||
// Completer completer = Completer();
|
||||
// _fileDataUrlReader().then((reader){
|
||||
// reader.onLoadEnd.listen((e) async {
|
||||
// dynamic data = reader.result;
|
||||
// completer.complete(data);
|
||||
// });
|
||||
// });
|
||||
// final data = await completer.future;
|
||||
// return data;
|
||||
// }
|
||||
//
|
||||
// Future<Uint8List> readBytes() async{
|
||||
// Completer completer = Completer();
|
||||
// _fileByteReader().then((reader){
|
||||
// reader.onLoadEnd.listen((e) async {
|
||||
// dynamic data = reader.result;
|
||||
// completer.complete(data);
|
||||
// });
|
||||
// });
|
||||
// final data = await completer.future;
|
||||
// return data;
|
||||
// }
|
||||
// }
|
||||
@ -0,0 +1,15 @@
|
||||
import 'dart:io';
|
||||
|
||||
class DeviceInfo {
|
||||
static String get label {
|
||||
return 'Flutter ' +
|
||||
Platform.operatingSystem +
|
||||
'(' +
|
||||
Platform.localHostname +
|
||||
")";
|
||||
}
|
||||
|
||||
static String get userAgent {
|
||||
return 'flutter-webrtc/' + 'Web' + '-plugin 0.0.1';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
// ignore: avoid_web_libraries_in_flutter
|
||||
|
||||
class DeviceInfo {
|
||||
static String get label {
|
||||
return 'Flutter Web';
|
||||
}
|
||||
|
||||
static String get userAgent {
|
||||
return 'flutter-webrtc/web-plugin 0.0.1 (Mobile)';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
Future<Map> getTurnCredential(String host, int port) async {
|
||||
HttpClient client = HttpClient(context: SecurityContext());
|
||||
client.badCertificateCallback =
|
||||
(X509Certificate cert, String host, int port) {
|
||||
print('getTurnCredential: Allow self-signed certificate => $host:$port. ');
|
||||
return true;
|
||||
};
|
||||
var url = 'https://$host:$port/api/turn?service=turn&username=flutter-webrtc';
|
||||
var request = await client.getUrl(Uri.parse(url));
|
||||
var response = await request.close();
|
||||
var responseBody = await response.transform(Utf8Decoder()).join();
|
||||
print('getTurnCredential:response => $responseBody.');
|
||||
Map data = JsonDecoder().convert(responseBody);
|
||||
return data;
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
Future<Map> getTurnCredential(String host, int port) async {
|
||||
var url = 'https://$host:$port/api/turn?service=turn&username=flutter-webrtc';
|
||||
final res = await http.get(Uri.parse(url));
|
||||
if (res.statusCode == 200) {
|
||||
var data = json.decode(res.body);
|
||||
print('getTurnCredential:response => $data.');
|
||||
return data;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
// import 'dart:convert';
|
||||
// import 'dart:html' as html;
|
||||
//
|
||||
// import 'package:diplomaticquarterapp/pages/videocall-webrtc-rnd/utils/blob-extension.dart';
|
||||
// import 'package:flutter/cupertino.dart';
|
||||
// import 'package:http/http.dart' as http;
|
||||
//
|
||||
// // should run on port '65482' add to additional arguments --web-port=65482
|
||||
// class UploadRecording{
|
||||
//
|
||||
// final url = "https://vcallapi.hmg.com/api/videocall/trackMeeting";
|
||||
// // final url = "https://10.20.200.186/api/videocall/trackMeeting";
|
||||
// upload(html.Blob blob, {@required Map<String, String> params, @required Function completion, @required Function(double) onProgress}) async{
|
||||
//
|
||||
// final bytes = await blob.readBytes();
|
||||
// final filename = "${DateTime.now().millisecondsSinceEpoch}.webm";
|
||||
// final file = http.MultipartFile.fromBytes('file', bytes ?? [], filename: filename);
|
||||
//
|
||||
// var request = http.MultipartRequest('POST', Uri.parse(url));
|
||||
// request.files.add(file);
|
||||
// request.fields.addAll({'fileName':filename,...params});
|
||||
//
|
||||
// print("Uploading video of '${blob.size}' bytes");
|
||||
// var response = await request.send().catchError((e){
|
||||
// print(e.toString());
|
||||
// completion(e.toString());
|
||||
// });
|
||||
//
|
||||
// print(response.statusCode);
|
||||
// if (response.statusCode == 200) {
|
||||
// final bytes = await response.stream.toBytes();
|
||||
// final jsonString = utf8.decode(bytes);
|
||||
// final json = jsonDecode(jsonString);
|
||||
// if(json == null)
|
||||
// completion('200: Invalid response format');
|
||||
// else
|
||||
// completion(json);
|
||||
//
|
||||
// }else {
|
||||
// print(response.reasonPhrase);
|
||||
// completion(response.reasonPhrase);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// _upload(html.Blob blob, {@required Map<String, String> params, @required Function completion, @required Function(double) onProgress}) async{
|
||||
// final blobBytes = await blob.readBytes();
|
||||
//
|
||||
// Map<String, dynamic> body = {};
|
||||
// body.addAll(params);
|
||||
// body['file'] = blobBytes ?? [];
|
||||
// body['fileName'] = "${DateTime.now().millisecondsSinceEpoch}.webm";
|
||||
//
|
||||
// final json = jsonEncode(body);
|
||||
//
|
||||
// var request = http.StreamedRequest('POST', Uri.parse(url));
|
||||
//
|
||||
// var totalLength = json.length;// Total length (to calculate upload progress)
|
||||
// var transferredLength = 0;// Length transferred (to calculate upload progress)
|
||||
// var uploadProgress = 0.0; // Upload progress (from 0.0 to 1.0)
|
||||
//
|
||||
// Stream.value(json).transform(utf8.encoder).listen((chunk) {
|
||||
//
|
||||
// transferredLength += chunk.length;
|
||||
// uploadProgress = transferredLength / totalLength;
|
||||
// // print("Chunk: ${chunk.length}, transferred: $transferredLength, progress: $uploadProgress");
|
||||
// request.sink.add(chunk);
|
||||
// onProgress(uploadProgress*100);
|
||||
//
|
||||
// }, onDone: () {
|
||||
// print("Done. Total: $totalLength, transferred: $transferredLength, progress: $uploadProgress");
|
||||
// request.sink.close();
|
||||
// onProgress(uploadProgress*100);
|
||||
// });
|
||||
//
|
||||
//
|
||||
// print("Uploading video of '${blob.size}' bytes");
|
||||
// var response = await request.send();
|
||||
//
|
||||
// print(response.statusCode);
|
||||
// if (response.statusCode == 200){
|
||||
//
|
||||
// final bytes = await response.stream.toBytes();
|
||||
// final jsonString = utf8.decode(bytes);
|
||||
// final json = jsonDecode(jsonString);
|
||||
// if(json == null)
|
||||
// completion('200: Invalid response format');
|
||||
// else
|
||||
// completion(json);
|
||||
//
|
||||
//
|
||||
// }else {
|
||||
// print("${response.statusCode}: ${response.reasonPhrase}");
|
||||
// completion(response.reasonPhrase);
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// }
|
||||
@ -0,0 +1,75 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
|
||||
class SimpleWebSocket {
|
||||
String _url;
|
||||
var _socket;
|
||||
Function() onOpen;
|
||||
Function(dynamic msg) onMessage;
|
||||
Function(int code, String reaso) onClose;
|
||||
SimpleWebSocket(this._url);
|
||||
|
||||
connect() async {
|
||||
try {
|
||||
//_socket = await WebSocket.connect(_url);
|
||||
print('connecting to $_url');
|
||||
_socket = await _connectForSelfSignedCert(_url);
|
||||
onOpen?.call();
|
||||
_socket.listen((data) {
|
||||
onMessage?.call(data);
|
||||
}, onDone: () {
|
||||
onClose?.call(_socket.closeCode, _socket.closeReason);
|
||||
});
|
||||
} catch (e) {
|
||||
onClose?.call(500, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
send(data) {
|
||||
if (_socket != null) {
|
||||
_socket.add(data);
|
||||
print('send: $data');
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (_socket != null) _socket.close();
|
||||
}
|
||||
|
||||
Future<WebSocket> _connectForSelfSignedCert(url) async {
|
||||
try {
|
||||
Random r = new Random();
|
||||
String key = base64.encode(List<int>.generate(8, (_) => r.nextInt(255)));
|
||||
HttpClient client = HttpClient(context: SecurityContext());
|
||||
client.badCertificateCallback =
|
||||
(X509Certificate cert, String host, int port) {
|
||||
print(
|
||||
'SimpleWebSocket: Allow self-signed certificate => $host:$port. ');
|
||||
return true;
|
||||
};
|
||||
|
||||
HttpClientRequest request =
|
||||
await client.getUrl(Uri.parse(url)); // form the correct url here
|
||||
request.headers.add('Connection', 'Upgrade');
|
||||
request.headers.add('Upgrade', 'websocket');
|
||||
request.headers.add(
|
||||
'Sec-WebSocket-Version', '13'); // insert the correct version here
|
||||
request.headers.add('Sec-WebSocket-Key', key.toLowerCase());
|
||||
|
||||
HttpClientResponse response = await request.close();
|
||||
// ignore: close_sinks
|
||||
Socket socket = await response.detachSocket();
|
||||
var webSocket = WebSocket.fromUpgradedSocket(
|
||||
socket,
|
||||
protocol: 'signaling',
|
||||
serverSide: false,
|
||||
);
|
||||
|
||||
return webSocket;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
// ignore: avoid_web_libraries_in_flutter
|
||||
import 'dart:html';
|
||||
|
||||
class SimpleWebSocket {
|
||||
String _url;
|
||||
var _socket;
|
||||
Function() onOpen;
|
||||
Function(dynamic msg) onMessage;
|
||||
Function(int code, String reason) onClose;
|
||||
|
||||
SimpleWebSocket(this._url) {
|
||||
_url = _url.replaceAll('https:', 'wss:');
|
||||
}
|
||||
|
||||
connect() async {
|
||||
try {
|
||||
_socket = WebSocket(_url);
|
||||
_socket.onOpen.listen((e) {
|
||||
onOpen?.call();
|
||||
});
|
||||
|
||||
_socket.onMessage.listen((e) {
|
||||
onMessage?.call(e.data);
|
||||
});
|
||||
|
||||
_socket.onClose.listen((e) {
|
||||
onClose?.call(e.code, e.reason);
|
||||
});
|
||||
} catch (e) {
|
||||
onClose?.call(500, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
send(data) {
|
||||
if (_socket != null && _socket.readyState == WebSocket.OPEN) {
|
||||
_socket.send(data);
|
||||
print('send: $data');
|
||||
} else {
|
||||
print('WebSocket not connected, message $data not sent');
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (_socket != null) {
|
||||
_socket.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,234 @@
|
||||
//
|
||||
// import 'dart:async';
|
||||
// import 'dart:collection';
|
||||
// import 'dart:html' as html;
|
||||
// import 'dart:io';
|
||||
// import 'dart:js' as js;
|
||||
// import 'dart:typed_data';
|
||||
// import 'package:diplomaticquarterapp/pages/videocall-webrtc-rnd/js_wrapper/multistreammixer-wrapper.dart';
|
||||
// import 'package:diplomaticquarterapp/pages/videocall-webrtc-rnd/utils/blob-extension.dart';
|
||||
// import 'package:diplomaticquarterapp/pages/videocall-webrtc-rnd/utils/upload-recording.dart';
|
||||
// import 'package:flutter/cupertino.dart';
|
||||
// import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
//
|
||||
// final _recorder_options = {
|
||||
// 'mimeType': 'video/webm;codecs=vp9,opus',
|
||||
// 'bitsPerSecond': 128000,
|
||||
// 'audioBitsPerSecond': 128000,// only for audio track
|
||||
// 'videoBitsPerSecond': 128000// only for video track
|
||||
// };
|
||||
//
|
||||
// class WebRtcStreamRecorder{
|
||||
// html.MediaStream remoteStreamWeb;
|
||||
// html.MediaStream localStreamWeb;
|
||||
//
|
||||
// html.MediaRecorder _local_recorder;
|
||||
// html.MediaRecorder _mixed_recorder;
|
||||
// html.MediaRecorder _remote_recorder;
|
||||
// Duration chunkDuration;
|
||||
//
|
||||
// WebRtcStreamRecorder({@required MediaStream remoteStream, @required MediaStream localStream}){
|
||||
// remoteStreamWeb = (remoteStream as dynamic).jsStream; // MediaStreamWeb.jsStream
|
||||
// localStreamWeb = (localStream as dynamic).jsStream; // MediaStreamWeb.jsStream
|
||||
// }
|
||||
//
|
||||
// Function(html.Blob blob, bool isLast, html.MediaRecorder recorder) _blobChunkCallback;
|
||||
//
|
||||
// onBlobChunk(Duration chunkDuration,{@required Function(html.Blob blob, bool isLast, html.MediaRecorder recorder) onBlobChunk}){
|
||||
// _blobChunkCallback = onBlobChunk;
|
||||
// this.chunkDuration = chunkDuration;
|
||||
// }
|
||||
//
|
||||
// stop(){
|
||||
// _mixed_recorder?.stop();
|
||||
// _remote_recorder?.stop();
|
||||
// _local_recorder?.stop();
|
||||
// _sliceTimer?.cancel();
|
||||
// }
|
||||
//
|
||||
// WebRtcStreamRecorder start({bool withMixedStream = true}){
|
||||
// if(withMixedStream){
|
||||
// _recordMixedStream();
|
||||
//
|
||||
// }else{
|
||||
// Future.wait<html.Blob>([_startLocal(), _startRemote()]).then((value) async{
|
||||
// final localVideoData = await value[0].readBytes();
|
||||
// final remoteVideoData = await value[1].readBytes();
|
||||
//
|
||||
// processAndSendClip(localVideoData, 'local-video');
|
||||
// processAndSendClip(remoteVideoData, 'remote-video');
|
||||
// print("\n\n\n");
|
||||
// print("------------------------------------------------------------------------------------------------------------------------");
|
||||
// print("Recordings Completed");
|
||||
// print("------------------------------------------------------------------------------------------------------------------------");
|
||||
// print(" Local: ${localVideoData == null ? 'not recorded' : 'recorded'}");
|
||||
// print("Remote: ${remoteVideoData == null ? 'not recorded' : 'recorded'}");
|
||||
// print("------------------------------------------------------------------------------------------------------------------------");
|
||||
// print("\n\n\n");
|
||||
//
|
||||
// });
|
||||
// }
|
||||
// return this;
|
||||
// }
|
||||
//
|
||||
// _recordMixedStream(){
|
||||
// final mixer = initMultiStreamsMixer([remoteStreamWeb,localStreamWeb]);
|
||||
// final jsMixedStream = mixer.getMixedStream();
|
||||
// _mixed_recorder = html.MediaRecorder(jsMixedStream, _recorder_options);
|
||||
// _startRecording(_mixed_recorder).then((blob) async {
|
||||
// final data = await blob?.readBytes();
|
||||
// // final url = await blob?.readDataUrl();
|
||||
// final params = {
|
||||
// 'requesterId':'1',
|
||||
// 'source':'ziktest4',
|
||||
// 'target':'adeltest4',
|
||||
// 'callStart':'2022-04-13 09:00',
|
||||
// 'callEnd':'2022-04-13 10:00',
|
||||
// 'archive':'1'
|
||||
// };
|
||||
//
|
||||
// print("Uploading called");
|
||||
// UploadRecording().upload(blob, params: params, completion: (response){
|
||||
// print("Response: $response");
|
||||
// }, onProgress: (progress){
|
||||
// print("Upload: ${progress.toInt()}/100");
|
||||
// });
|
||||
//
|
||||
// // String file = await FileSaver.instance.saveFile("recorded", data!, "webm", mimeType: MimeType.WEBM).catchError((e){
|
||||
// // print(e);
|
||||
// // });
|
||||
// // print(file);
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// Future<html.Blob> _startRemote() async{
|
||||
// _remote_recorder = html.MediaRecorder(remoteStreamWeb, _recorder_options);
|
||||
// return _startRecording(_remote_recorder);
|
||||
// }
|
||||
//
|
||||
// Future<html.Blob> _startLocal() async{
|
||||
// _local_recorder = html.MediaRecorder(localStreamWeb, _recorder_options);
|
||||
// return _startRecording(_local_recorder);
|
||||
// }
|
||||
//
|
||||
// processAndSendClip(Uint8List data, String filename) async{
|
||||
// if(data != null){
|
||||
// // String file = await FileSaver.instance.saveFile("$filename", data, "webm", mimeType: MimeType.OTHER).catchError((e){
|
||||
// // print(e);
|
||||
// // });
|
||||
// // print(file);
|
||||
// }else{
|
||||
//
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Timer _sliceTimer;
|
||||
// extension xWebRtcStreamRecorder on WebRtcStreamRecorder{
|
||||
//
|
||||
// @override
|
||||
// Future<html.Blob> _startRecording(html.MediaRecorder recorder) async{
|
||||
// html.Blob _blob;
|
||||
// bool isLast = false;
|
||||
// if(recorder != null){
|
||||
// final completer = Completer();
|
||||
// recorder.addEventListener('dataavailable', (html.Event event) async{
|
||||
// _blob = js.JsObject.fromBrowserObject(event)['data'];
|
||||
// isLast = recorder.state == 'inactive';
|
||||
// final data = await _blob?.readBytes();
|
||||
// print("dataavailable isLast:$isLast data:${data?.length}");
|
||||
// if(_blobChunkCallback != null)
|
||||
// _blobChunkCallback(_blob, isLast, recorder);
|
||||
//
|
||||
// if(isLast)
|
||||
// completer.complete();
|
||||
// });
|
||||
// recorder.start();
|
||||
// print("_startRecording Start");
|
||||
// _enableSliceChunks(recorder);
|
||||
//
|
||||
// await completer.future;
|
||||
// }
|
||||
//
|
||||
// print("_startRecording Finish");
|
||||
// return _blob;
|
||||
// }
|
||||
//
|
||||
// _enableSliceChunks(html.MediaRecorder recorder){
|
||||
// if(chunkDuration != null && recorder != null){
|
||||
// _sliceTimer = Timer.periodic(chunkDuration, (timer) {
|
||||
// if(recorder.stream?.active == true){
|
||||
// recorder.requestData();
|
||||
// }else{
|
||||
// timer.cancel();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // class WebRecorder {
|
||||
// // static bool isNotRecording = true;
|
||||
// // static html.MediaRecorder? recorder;
|
||||
// //
|
||||
// // final Future Function() whenRecorderStart; // Function to call when recording starts
|
||||
// // final Future Function() whenRecorderStop; // Function to call when recording finishs
|
||||
// // final Future Function(dynamic) whenReceiveData;
|
||||
// //
|
||||
// // WebRecorder({
|
||||
// // required this.whenRecorderStart,
|
||||
// // required this.whenRecorderStop,
|
||||
// // required this.whenReceiveData
|
||||
// // });
|
||||
// //
|
||||
// // WebRecorder openRecorder({MediaStream? stream}){
|
||||
// // WebRecorder.isNotRecording = !WebRecorder.isNotRecording;
|
||||
// // if(WebRecorder.isNotRecording)
|
||||
// // stopRecoring().whenComplete(whenRecorderStop);
|
||||
// // else
|
||||
// // if(stream == null){
|
||||
// // html.window.navigator
|
||||
// // .getUserMedia(audio: true)
|
||||
// // .then((stream) {
|
||||
// // recorder = html.MediaRecorder(stream);
|
||||
// // recorder?.addEventListener('dataavailable', hundlerFunctionStream);
|
||||
// // })
|
||||
// // .whenComplete((){
|
||||
// // startRecording().whenComplete(whenRecorderStart);
|
||||
// // })
|
||||
// // .catchError((e)=> print);
|
||||
// // }else{
|
||||
// // final htmlStream = html.MediaStream(stream.getTracks());
|
||||
// // recorder = html.MediaRecorder(htmlStream);
|
||||
// // recorder?.addEventListener('dataavailable', hundlerFunctionStream);
|
||||
// // }
|
||||
// //
|
||||
// // return this;
|
||||
// // }
|
||||
// //
|
||||
// // Future<void> startRecording(){
|
||||
// // WebRecorder.recorder?.start();
|
||||
// // return Future.value(true);
|
||||
// // }
|
||||
// //
|
||||
// // Future<void> stopRecoring() async{
|
||||
// // WebRecorder.recorder?.stop();
|
||||
// // return Future.value(true);
|
||||
// // }
|
||||
// //
|
||||
// // hundlerFunctionStream(event) async{
|
||||
// // html.FileReader reader = html.FileReader();
|
||||
// // html.Blob blob = js.JsObject.fromBrowserObject(event)['data'];
|
||||
// // reader.readAsArrayBuffer(blob);
|
||||
// // reader.onLoadEnd.listen((e) async {
|
||||
// // setData(reader.result);
|
||||
// // });
|
||||
// // }
|
||||
// //
|
||||
// // setData(data) => whenReceiveData(data);
|
||||
// //
|
||||
// // dispose(){
|
||||
// // WebRecorder.recorder?.removeEventListener('dataavailable', hundlerFunctionStream);
|
||||
// // WebRecorder.recorder = null;
|
||||
// // }
|
||||
// // }
|
||||
@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2016, Damon Douglas. All rights reserved. Use of this source code
|
||||
// is governed by a BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
/// Simple library for generating random ascii strings.
|
||||
///
|
||||
/// More dartdocs go here.
|
||||
///
|
||||
///
|
||||
/// A simple usage example:
|
||||
///
|
||||
/// import 'package:random_string/random_string.dart' as random;
|
||||
/// main() {
|
||||
/// print(randomBetween(10,20)); // some integer between 10 and 20
|
||||
/// print(randomNumeric(4)); // sequence of 4 random numbers i.e. 3259
|
||||
/// print(randomString(10)); // random sequence of 10 characters i.e. e~f93(4l-
|
||||
/// print(randomAlpha(5)); // random sequence of 5 alpha characters i.e. aRztC
|
||||
/// print(randomAlphaNumeric(10)); // random sequence of 10 alpha numeric i.e. aRztC1y32B
|
||||
/// }
|
||||
|
||||
library random_string;
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
const ASCII_START = 33;
|
||||
const ASCII_END = 126;
|
||||
const NUMERIC_START = 48;
|
||||
const NUMERIC_END = 57;
|
||||
const LOWER_ALPHA_START = 97;
|
||||
const LOWER_ALPHA_END = 122;
|
||||
const UPPER_ALPHA_START = 65;
|
||||
const UPPER_ALPHA_END = 90;
|
||||
|
||||
/// Generates a random integer where [from] <= [to].
|
||||
int randomBetween(int from, int to) {
|
||||
if (from > to) throw Exception('$from cannot be > $to');
|
||||
var rand = Random();
|
||||
return ((to - from) * rand.nextDouble()).toInt() + from;
|
||||
}
|
||||
|
||||
/// Generates a random string of [length] with characters
|
||||
/// between ascii [from] to [to].
|
||||
/// Defaults to characters of ascii '!' to '~'.
|
||||
String randomString(int length, {int from: ASCII_START, int to: ASCII_END}) {
|
||||
return String.fromCharCodes(
|
||||
List.generate(length, (index) => randomBetween(from, to)));
|
||||
}
|
||||
|
||||
/// Generates a random string of [length] with only numeric characters.
|
||||
String randomNumeric(int length) =>
|
||||
randomString(length, from: NUMERIC_START, to: NUMERIC_END);
|
||||
/*
|
||||
/// Generates a random string of [length] with only alpha characters.
|
||||
String randomAlpha(int length) {
|
||||
var lowerAlphaLength = randomBetween(0, length);
|
||||
var upperAlphaLength = length - lowerAlphaLength;
|
||||
var lowerAlpha = randomString(lowerAlphaLength,
|
||||
from: LOWER_ALPHA_START, to: LOWER_ALPHA_END);
|
||||
var upperAlpha = randomString(upperAlphaLength,
|
||||
from: UPPER_ALPHA_START, to: UPPER_ALPHA_END);
|
||||
return randomMerge(lowerAlpha, upperAlpha);
|
||||
}
|
||||
|
||||
/// Generates a random string of [length] with alpha-numeric characters.
|
||||
String randomAlphaNumeric(int length) {
|
||||
var alphaLength = randomBetween(0, length);
|
||||
var numericLength = length - alphaLength;
|
||||
var alpha = randomAlpha(alphaLength);
|
||||
var numeric = randomNumeric(numericLength);
|
||||
return randomMerge(alpha, numeric);
|
||||
}
|
||||
|
||||
/// Merge [a] with [b] and scramble characters.
|
||||
String randomMerge(String a, String b) {
|
||||
var mergedCodeUnits = List.from("$a$b".codeUnits);
|
||||
mergedCodeUnits.shuffle();
|
||||
return String.fromCharCodes(mergedCodeUnits);
|
||||
}*/
|
||||
@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:core';
|
||||
|
||||
class CallSettings extends StatefulWidget {
|
||||
static String tag = 'call_settings';
|
||||
|
||||
@override
|
||||
_CallSettingsState createState() => _CallSettingsState();
|
||||
}
|
||||
|
||||
class _CallSettingsState extends State<CallSettings> {
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
deactivate() {
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Settings'),
|
||||
),
|
||||
body: OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
return Center(child: Text("settings"));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,510 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,240 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:diplomaticquarterapp/pages/conference/conference_button_bar.dart';
|
||||
import 'package:diplomaticquarterapp/pages/conference/web_rtc/widgets/cam_view_widget.dart';
|
||||
import 'package:diplomaticquarterapp/pages/videocall-webrtc-rnd/utils/device_info.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:core';
|
||||
import 'signaling.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
|
||||
class StartVideoCall extends StatefulWidget {
|
||||
static String tag = 'webrtc';
|
||||
final String host;
|
||||
|
||||
String caller;
|
||||
String receiver;
|
||||
bool iAmCaller;
|
||||
StartVideoCall({@required this.caller, @required this.receiver, this.iAmCaller = false, @required this.host});
|
||||
|
||||
@override
|
||||
StartVideoCallState createState() => StartVideoCallState(receiverId: receiver, callerId: caller, iAmCaller: iAmCaller);
|
||||
}
|
||||
|
||||
class StartVideoCallState extends State<StartVideoCall> {
|
||||
Signaling _signaling;
|
||||
SessionOneToOne _session;
|
||||
|
||||
StartVideoCallState({@required String callerId, @required String receiverId, @required bool iAmCaller}){
|
||||
|
||||
final self_role = iAmCaller ? "Caller" : "Receiver";
|
||||
final self_id = iAmCaller ? callerId : receiverId;
|
||||
final self_user = SocketUser(id: self_id, name: "$self_role-$self_id", userAgent: DeviceInfo.userAgent, moreInfo: {});
|
||||
|
||||
final remote_role = !iAmCaller ? "Caller" : "Receiver";
|
||||
final remote_id = !iAmCaller ? callerId : receiverId;
|
||||
final remote_user = SocketUser(id: remote_id, name: "$remote_role-$remote_id", userAgent: DeviceInfo.userAgent, moreInfo: {});
|
||||
|
||||
final session_id = "$callerId-$receiverId";
|
||||
_session = SessionOneToOne(id: session_id, local_user: self_user, remote_user: remote_user);
|
||||
}
|
||||
|
||||
bool showNoise = true;
|
||||
final StreamController<bool> _audioButton = StreamController<bool>.broadcast();
|
||||
final StreamController<bool> _videoButton = StreamController<bool>.broadcast();
|
||||
final StreamController<bool> _onButtonBarVisibleStreamController = StreamController<bool>.broadcast();
|
||||
final StreamController<double> _onButtonBarHeightStreamController = StreamController<double>.broadcast();
|
||||
|
||||
final RTCVideoRenderer _localRenderer = RTCVideoRenderer();
|
||||
final RTCVideoRenderer _remoteRenderer = RTCVideoRenderer();
|
||||
|
||||
MediaStream get localMediaStream => _signaling.localStream;
|
||||
MediaStream get remoteMediaStream => _signaling.remoteStreams.first;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
initRenderers();
|
||||
_connect();
|
||||
}
|
||||
|
||||
initRenderers() async {
|
||||
await _localRenderer.initialize();
|
||||
await _remoteRenderer.initialize();
|
||||
}
|
||||
|
||||
@override
|
||||
deactivate() {
|
||||
super.deactivate();
|
||||
_signaling?.close();
|
||||
_localRenderer.dispose();
|
||||
_remoteRenderer.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
// appBar: AppBar(
|
||||
// title: Text('P2P Call Sample' + (_session?.local_user != null ? ' [Your ID (${_session?.local_user?.id})] ' : '')),
|
||||
// actions: <Widget>[
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.settings),
|
||||
// onPressed: (){
|
||||
// setState(() {
|
||||
// });
|
||||
// },
|
||||
// tooltip: 'setup',
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
body: videoCanvasWidgets(),
|
||||
);
|
||||
}
|
||||
|
||||
LayoutBuilder videoCanvasWidgets() {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return Stack(
|
||||
children: [
|
||||
CamViewWidget(
|
||||
localRenderer: _localRenderer,
|
||||
remoteRenderer: _remoteRenderer,
|
||||
constraints: constraints,
|
||||
onButtonBarVisibleStreamController: _onButtonBarVisibleStreamController,
|
||||
onButtonBarHeightStreamController: _onButtonBarHeightStreamController,
|
||||
),
|
||||
ConferenceButtonBar(
|
||||
audioEnabled: _audioButton.stream,
|
||||
videoEnabled: _videoButton.stream,
|
||||
onAudioEnabled: _onAudioEnable,
|
||||
onVideoEnabled: _onVideoEnabled,
|
||||
onSwitchCamera: _onSwitchCamera,
|
||||
onHangup: _onHangup,
|
||||
onPersonAdd: () {},
|
||||
onPersonRemove: () {},
|
||||
onHeight: _onHeightBar,
|
||||
onShow: _onShowBar,
|
||||
onHide: _onHideBar,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void _connect() async {
|
||||
if(_session == null)
|
||||
return;
|
||||
|
||||
_signaling ??= Signaling(widget.host, session: _session)..connect();
|
||||
|
||||
_signaling?.onSignalingStateChange = (SignalingState state) {
|
||||
switch (state) {
|
||||
case SignalingState.Closed:
|
||||
case SignalingState.Error:
|
||||
case SignalingState.Open:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
_signaling?.onCallStateChange = (SessionOneToOne session, CallState state) {
|
||||
switch (state) {
|
||||
case CallState.Calling:
|
||||
setState(() {});
|
||||
break;
|
||||
case CallState.Bye:
|
||||
setState(() {
|
||||
_localRenderer.srcObject = null;
|
||||
_remoteRenderer.srcObject = null;
|
||||
_session = null;
|
||||
});
|
||||
break;
|
||||
case CallState.Invite:
|
||||
case CallState.Connected:
|
||||
case CallState.Ringing:
|
||||
}
|
||||
};
|
||||
|
||||
_signaling?.onConnected = ((event) {
|
||||
_signaling?.callAccepted(_session);
|
||||
});
|
||||
|
||||
_signaling?.onRemoteConnected = ((event) {
|
||||
|
||||
});
|
||||
|
||||
_signaling?.onLocalStream = ((stream) {
|
||||
_localRenderer.srcObject = stream;
|
||||
setState(() {
|
||||
});
|
||||
});
|
||||
|
||||
_signaling?.onAddRemoteStream = ((_, stream) {
|
||||
_remoteRenderer.srcObject = stream;
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
_signaling?.onRemoveRemoteStream = ((_, stream) {
|
||||
setState(() {
|
||||
_remoteRenderer.srcObject = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_hangUp() async{
|
||||
if (_session != null) {
|
||||
_signaling?.bye(_session);
|
||||
}
|
||||
}
|
||||
|
||||
_switchCamera() {
|
||||
_signaling?.switchCamera();
|
||||
}
|
||||
|
||||
_muteMic() {
|
||||
_signaling?.muteMic();
|
||||
}
|
||||
|
||||
Function _onAudioEnable() {
|
||||
final audioTrack = localMediaStream.getAudioTracks()[0];
|
||||
final mute = audioTrack.muted;
|
||||
Helper.setMicrophoneMute(!mute, audioTrack);
|
||||
_audioButton.add(mute);
|
||||
}
|
||||
|
||||
Function _onVideoEnabled() {
|
||||
final videoTrack = localMediaStream.getVideoTracks()[0];
|
||||
bool videoEnabled = videoTrack.enabled;
|
||||
localMediaStream.getVideoTracks()[0].enabled = !videoEnabled;
|
||||
_videoButton.add(!videoEnabled);
|
||||
}
|
||||
|
||||
Function _onSwitchCamera() {
|
||||
Helper.switchCamera(localMediaStream.getVideoTracks()[0]);
|
||||
}
|
||||
|
||||
void _onShowBar() {
|
||||
setState(() {
|
||||
});
|
||||
_onButtonBarVisibleStreamController.add(true);
|
||||
}
|
||||
|
||||
void _onHeightBar(double height) {
|
||||
_onButtonBarHeightStreamController.add(height);
|
||||
}
|
||||
|
||||
void _onHideBar() {
|
||||
setState(() {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.bottom]);
|
||||
});
|
||||
_onButtonBarVisibleStreamController.add(false);
|
||||
}
|
||||
|
||||
Future<void> _onHangup() async {
|
||||
_signaling?.finishSessions();
|
||||
print('onHangup');
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue