From 98837b22988b68c2347b5121476285452b3fec01 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 27 Sep 2021 17:12:42 +0300 Subject: [PATCH] WebRTC implementation --- lib/pages/webRTC/call_page.dart | 112 ++++++++++++--- lib/pages/webRTC/signaling.dart | 242 ++++++++++++++++++++++++++++++++ 2 files changed, 338 insertions(+), 16 deletions(-) create mode 100644 lib/pages/webRTC/signaling.dart diff --git a/lib/pages/webRTC/call_page.dart b/lib/pages/webRTC/call_page.dart index bf0895c9..4c8ac4f5 100644 --- a/lib/pages/webRTC/call_page.dart +++ b/lib/pages/webRTC/call_page.dart @@ -1,3 +1,4 @@ +import 'package:diplomaticquarterapp/pages/webRTC/signaling.dart'; import 'package:diplomaticquarterapp/widgets/others/app_scaffold_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; @@ -8,18 +9,29 @@ class CallPage extends StatefulWidget { } class _CallPageState extends State { - final _localRenderer = new RTCVideoRenderer(); + Signaling signaling = Signaling(); + RTCVideoRenderer _localRenderer = RTCVideoRenderer(); + RTCVideoRenderer _remoteRenderer = RTCVideoRenderer(); + String roomId; + TextEditingController textEditingController = TextEditingController(text: ''); @override void initState() { - initializeRenderers(); - _getUserMedia(); + _localRenderer.initialize(); + _remoteRenderer.initialize(); + + signaling.onAddRemoteStream = ((stream) { + _remoteRenderer.srcObject = stream; + setState(() {}); + }); + super.initState(); } @override void dispose() { _localRenderer.dispose(); + _remoteRenderer.dispose(); super.dispose(); } @@ -47,20 +59,88 @@ class _CallPageState extends State { showNewAppBarTitle: true, isShowDecPage: false, appBarTitle: "WebRTC Calling", - body: Container( - child: Stack( - children: [ - Positioned( - top: 0.0, - left: 0.0, - right: 0.0, - bottom: 0.0, - child: Container( - child: new RTCVideoView(_localRenderer), + body: Column( + children: [ + SizedBox(height: 8), + Wrap( + children: [ + ElevatedButton( + onPressed: () { + setState(() { + signaling.openUserMedia(_localRenderer, _remoteRenderer); + }); + }, + child: Text("Open camera & microphone"), + ), + SizedBox( + width: 8, + ), + ElevatedButton( + onPressed: () async { + roomId = await signaling.createRoom(_remoteRenderer); + textEditingController.text = roomId; + setState(() {}); + }, + child: Text("Create room"), + ), + SizedBox( + width: 8, + ), + ElevatedButton( + onPressed: () { + // Add roomId + signaling.joinRoom( + textEditingController.text, + _remoteRenderer, + ); + }, + child: Text("Join room"), + ), + SizedBox( + width: 8, + ), + ElevatedButton( + onPressed: () { + signaling.hangUp(_localRenderer); + }, + child: Text("Hangup"), + ) + ], + ), + SizedBox(height: 8), + Expanded( + child: Padding( + padding: const EdgeInsets.all(0.0), + child: Stack( + children: [ + Positioned(top: 0.0, right: 0.0, left: 0.0, bottom: 0.0, child: RTCVideoView(_remoteRenderer)), + Positioned( + top: 20.0, + right: 100.0, + left: 20.0, + bottom: 300.0, + child: RTCVideoView(_localRenderer, mirror: true), + ), + ], ), - ) - ], - ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Join the following Room: "), + Flexible( + child: TextFormField( + controller: textEditingController, + ), + ) + ], + ), + ), + SizedBox(height: 8) + ], ), ); } diff --git a/lib/pages/webRTC/signaling.dart b/lib/pages/webRTC/signaling.dart new file mode 100644 index 00000000..f987e285 --- /dev/null +++ b/lib/pages/webRTC/signaling.dart @@ -0,0 +1,242 @@ +import 'dart:convert'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; + +typedef void StreamStateCallback(MediaStream stream); + +class Signaling { + Map configuration = { + 'iceServers': [ + { + 'urls': ['stun:stun1.l.google.com:19302', 'stun:stun2.l.google.com:19302'] + } + ] + }; + + RTCPeerConnection peerConnection; + MediaStream localStream; + MediaStream remoteStream; + String roomId; + String currentRoomText; + StreamStateCallback onAddRemoteStream; + + Future createRoom(RTCVideoRenderer remoteRenderer) async { + FirebaseFirestore db = FirebaseFirestore.instance; + DocumentReference roomRef = db.collection('rooms').doc(); + + print('Create PeerConnection with configuration: $configuration'); + + peerConnection = await createPeerConnection(configuration); + + registerPeerConnectionListeners(); + + localStream.getTracks().forEach((track) { + peerConnection?.addTrack(track, localStream); + }); + + // Code for collecting ICE candidates below + var callerCandidatesCollection = roomRef.collection('callerCandidates'); + + peerConnection?.onIceCandidate = (RTCIceCandidate candidate) { + print('Got candidate: ${candidate.toMap()}'); + callerCandidatesCollection.add(candidate.toMap()); + }; + // Finish Code for collecting ICE candidate + + // Add code for creating a room + RTCSessionDescription offer = await peerConnection.createOffer(); + await peerConnection.setLocalDescription(offer); + print('Created offer: $offer'); + + Map roomWithOffer = {'offer': offer.toMap()}; + + await roomRef.set(roomWithOffer); + var roomId = roomRef.id; + print('New room created with SDK offer. Room ID: $roomId'); + currentRoomText = 'Current room is $roomId - You are the caller!'; + // Created a Room + + peerConnection?.onTrack = (RTCTrackEvent event) { + print('Got remote track: ${event.streams[0]}'); + + event.streams[0].getTracks().forEach((track) { + print('Add a track to the remoteStream $track'); + remoteStream?.addTrack(track); + }); + }; + + // Listening for remote session description below + roomRef.snapshots().listen((snapshot) async { + print('Got updated room: ${snapshot.data()}'); + + Map data = snapshot.data() as Map; + if (peerConnection?.getRemoteDescription() != null && data['answer'] != null) { + var answer = RTCSessionDescription( + data['answer']['sdp'], + data['answer']['type'], + ); + + print("Someone tried to connect"); + await peerConnection?.setRemoteDescription(answer); + } + }); + // Listening for remote session description above + + // Listen for remote Ice candidates below + roomRef.collection('calleeCandidates').snapshots().listen((snapshot) { + snapshot.docChanges.forEach((change) { + if (change.type == DocumentChangeType.added) { + Map data = change.doc.data() as Map; + print('Got new remote ICE candidate: ${jsonEncode(data)}'); + peerConnection.addCandidate( + RTCIceCandidate( + data['candidate'], + data['sdpMid'], + data['sdpMLineIndex'], + ), + ); + } + }); + }); + // Listen for remote ICE candidates above + + return roomId; + } + + Future joinRoom(String roomId, RTCVideoRenderer remoteVideo) async { + FirebaseFirestore db = FirebaseFirestore.instance; + DocumentReference roomRef = db.collection('rooms').doc('$roomId'); + var roomSnapshot = await roomRef.get(); + print('Got room ${roomSnapshot.exists}'); + + if (roomSnapshot.exists) { + print('Create PeerConnection with configuration: $configuration'); + peerConnection = await createPeerConnection(configuration); + + registerPeerConnectionListeners(); + + localStream.getTracks().forEach((track) { + peerConnection?.addTrack(track, localStream); + }); + + // Code for collecting ICE candidates below + var calleeCandidatesCollection = roomRef.collection('calleeCandidates'); + peerConnection.onIceCandidate = (RTCIceCandidate candidate) { + if (candidate == null) { + print('onIceCandidate: complete!'); + return; + } + print('onIceCandidate: ${candidate.toMap()}'); + calleeCandidatesCollection.add(candidate.toMap()); + }; + // Code for collecting ICE candidate above + + peerConnection?.onTrack = (RTCTrackEvent event) { + print('Got remote track: ${event.streams[0]}'); + event.streams[0].getTracks().forEach((track) { + print('Add a track to the remoteStream: $track'); + remoteStream?.addTrack(track); + }); + }; + + // Code for creating SDP answer below + var data = roomSnapshot.data() as Map; + print('Got offer $data'); + var offer = data['offer']; + await peerConnection?.setRemoteDescription( + RTCSessionDescription(offer['sdp'], offer['type']), + ); + var answer = await peerConnection.createAnswer(); + print('Created Answer $answer'); + + await peerConnection.setLocalDescription(answer); + + Map roomWithAnswer = { + 'answer': {'type': answer.type, 'sdp': answer.sdp} + }; + + await roomRef.update(roomWithAnswer); + // Finished creating SDP answer + + // Listening for remote ICE candidates below + // roomRef.collection('callerCandidates').snapshots().listen((snapshot) { + // snapshot.docChanges.forEach((document) { + // var data = document.doc.data() as Map; + // print(data); + // print('Got new remote ICE candidate: $data'); + // peerConnection.addCandidate( + // RTCIceCandidate( + // data['candidate'], + // data['sdpMid'], + // data['sdpMLineIndex'], + // ), + // ); + // }); + // }); + } + } + + Future openUserMedia( + RTCVideoRenderer localVideo, + RTCVideoRenderer remoteVideo, + ) async { + var stream = await navigator.mediaDevices.getUserMedia({'video': true, 'audio': false}); + + localVideo.srcObject = stream; + localStream = stream; + + remoteVideo.srcObject = await createLocalMediaStream('key'); + } + + Future hangUp(RTCVideoRenderer localVideo) async { + List tracks = localVideo.srcObject.getTracks(); + tracks.forEach((track) { + track.stop(); + }); + + if (remoteStream != null) { + remoteStream.getTracks().forEach((track) => track.stop()); + } + if (peerConnection != null) peerConnection.close(); + + if (roomId != null) { + var db = FirebaseFirestore.instance; + var roomRef = db.collection('rooms').doc(roomId); + var calleeCandidates = await roomRef.collection('calleeCandidates').get(); + calleeCandidates.docs.forEach((document) => document.reference.delete()); + + var callerCandidates = await roomRef.collection('callerCandidates').get(); + callerCandidates.docs.forEach((document) => document.reference.delete()); + + await roomRef.delete(); + } + + localStream.dispose(); + remoteStream?.dispose(); + } + + void registerPeerConnectionListeners() { + peerConnection?.onIceGatheringState = (RTCIceGatheringState state) { + print('ICE gathering state changed: $state'); + }; + + peerConnection?.onConnectionState = (RTCPeerConnectionState state) { + print('Connection state change: $state'); + }; + + peerConnection?.onSignalingState = (RTCSignalingState state) { + print('Signaling state change: $state'); + }; + + peerConnection?.onIceGatheringState = (RTCIceGatheringState state) { + print('ICE connection state change: $state'); + }; + + peerConnection?.onAddStream = (MediaStream stream) { + print("Add remote stream"); + onAddRemoteStream?.call(stream); + remoteStream = stream; + }; + } +}