├── .firebaserc ├── database.rules.json ├── README.md ├── firebase.json ├── .gitignore └── public ├── main.css ├── index.html └── app.js /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "fir-rtc-89b99" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | /* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */ 3 | "rules": { 4 | ".read": false, 5 | ".write": false 6 | } 7 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firebase + WebRTC Codelab 2 | ### Full code solution can be found under the branch: _solution_ 3 | This is the GitHub repo for the FirebaseRTC codelab. This will teach you how 4 | to use Firebase Cloud Firestore for signalling in a WebRTC video chat application. 5 | 6 | The solution to this codelab can be seen in the _solution_ branch. 7 | 8 | See http://webrtc.org for details. 9 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "hosting": { 6 | "public": "public", 7 | "ignore": [ 8 | "firebase.json", 9 | "**/.*", 10 | "**/node_modules/**" 11 | ], 12 | "rewrites": [ 13 | { 14 | "source": "**", 15 | "destination": "/index.html" 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | 9 | # Firebase cache 10 | .firebase/ 11 | 12 | # Firebase config 13 | 14 | # Uncomment this if you'd like others to create their own Firebase project. 15 | # For a team working on the same Firebase project(s), it is recommended to leave 16 | # it commented so all members can deploy to the same project(s) in .firebaserc. 17 | # .firebaserc 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (http://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | 67 | .idea 68 | -------------------------------------------------------------------------------- /public/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #ECEFF1; 3 | color: rgba(0, 0, 0, 0.87); 4 | font-family: Roboto, Helvetica, Arial, sans-serif; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | #message { 10 | background: white; 11 | max-width: 360px; 12 | margin: 100px auto 16px; 13 | padding: 32px 24px; 14 | border-radius: 3px; 15 | } 16 | 17 | #message h2 { 18 | color: #ffa100; 19 | font-weight: bold; 20 | font-size: 16px; 21 | margin: 0 0 8px; 22 | } 23 | 24 | #message h1 { 25 | font-size: 22px; 26 | font-weight: 300; 27 | color: rgba(0, 0, 0, 0.6); 28 | margin: 0 0 16px; 29 | } 30 | 31 | #message p { 32 | line-height: 140%; 33 | margin: 16px 0 24px; 34 | font-size: 14px; 35 | } 36 | 37 | #message a { 38 | display: block; 39 | text-align: center; 40 | background: #039be5; 41 | text-transform: uppercase; 42 | text-decoration: none; 43 | color: white; 44 | padding: 16px; 45 | border-radius: 4px; 46 | } 47 | 48 | #message, #message a { 49 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 50 | } 51 | 52 | #load { 53 | color: rgba(0, 0, 0, 0.4); 54 | text-align: center; 55 | font-size: 13px; 56 | } 57 | 58 | @media (max-width: 600px) { 59 | body, #message { 60 | margin-top: 0; 61 | background: white; 62 | box-shadow: none; 63 | } 64 | 65 | body { 66 | border-top: 16px solid #ffa100; 67 | } 68 | } 69 | 70 | body { 71 | margin: 1em; 72 | } 73 | 74 | button { 75 | margin: 0.2em 0.1em; 76 | } 77 | 78 | div#videos { 79 | display: flex; 80 | flex-direction: row; 81 | flex-wrap: wrap; 82 | } 83 | 84 | div#videos > video { 85 | background: black; 86 | width: 640px; 87 | height: 100%; 88 | display: block; 89 | margin: 1em; 90 | } 91 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Welcome to FirebaseRTC 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |

Welcome to FirebaseRTC!

22 |
23 | 27 | 31 | 35 | 39 |
40 |
41 | 42 |
43 |
44 | 45 | 46 |
47 |
53 |
54 |
55 |

Join room

56 |
57 | Enter ID for room to join: 58 |
59 | 60 | 61 |
62 |
63 |
64 |
65 | 68 | 72 |
73 |
74 |
75 |
76 |
77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /public/app.js: -------------------------------------------------------------------------------- 1 | mdc.ripple.MDCRipple.attachTo(document.querySelector('.mdc-button')); 2 | 3 | // DEfault configuration - Change these if you have a different STUN or TURN server. 4 | const configuration = { 5 | iceServers: [ 6 | { 7 | urls: [ 8 | 'stun:stun1.l.google.com:19302', 9 | 'stun:stun2.l.google.com:19302', 10 | ], 11 | }, 12 | ], 13 | iceCandidatePoolSize: 10, 14 | }; 15 | 16 | let peerConnection = null; 17 | let localStream = null; 18 | let remoteStream = null; 19 | let roomDialog = null; 20 | let roomId = null; 21 | 22 | function init() { 23 | document.querySelector('#cameraBtn').addEventListener('click', openUserMedia); 24 | document.querySelector('#hangupBtn').addEventListener('click', hangUp); 25 | document.querySelector('#createBtn').addEventListener('click', createRoom); 26 | document.querySelector('#joinBtn').addEventListener('click', joinRoom); 27 | roomDialog = new mdc.dialog.MDCDialog(document.querySelector('#room-dialog')); 28 | } 29 | 30 | async function createRoom() { 31 | document.querySelector('#createBtn').disabled = true; 32 | document.querySelector('#joinBtn').disabled = true; 33 | const db = firebase.firestore(); 34 | 35 | console.log('Create PeerConnection with configuration: ', configuration); 36 | peerConnection = new RTCPeerConnection(configuration); 37 | 38 | registerPeerConnectionListeners(); 39 | 40 | // Add code for creating a room here 41 | 42 | // Code for creating room above 43 | 44 | localStream.getTracks().forEach(track => { 45 | peerConnection.addTrack(track, localStream); 46 | }); 47 | 48 | // Code for creating a room below 49 | 50 | // Code for creating a room above 51 | 52 | // Code for collecting ICE candidates below 53 | 54 | // Code for collecting ICE candidates above 55 | 56 | peerConnection.addEventListener('track', event => { 57 | console.log('Got remote track:', event.streams[0]); 58 | event.streams[0].getTracks().forEach(track => { 59 | console.log('Add a track to the remoteStream:', track); 60 | remoteStream.addTrack(track); 61 | }); 62 | }); 63 | 64 | // Listening for remote session description below 65 | 66 | // Listening for remote session description above 67 | 68 | // Listen for remote ICE candidates below 69 | 70 | // Listen for remote ICE candidates above 71 | } 72 | 73 | function joinRoom() { 74 | document.querySelector('#createBtn').disabled = true; 75 | document.querySelector('#joinBtn').disabled = true; 76 | 77 | document.querySelector('#confirmJoinBtn'). 78 | addEventListener('click', async () => { 79 | roomId = document.querySelector('#room-id').value; 80 | console.log('Join room: ', roomId); 81 | document.querySelector( 82 | '#currentRoom').innerText = `Current room is ${roomId} - You are the callee!`; 83 | await joinRoomById(roomId); 84 | }, {once: true}); 85 | roomDialog.open(); 86 | } 87 | 88 | async function joinRoomById(roomId) { 89 | const db = firebase.firestore(); 90 | const roomRef = db.collection('rooms').doc(`${roomId}`); 91 | const roomSnapshot = await roomRef.get(); 92 | console.log('Got room:', roomSnapshot.exists); 93 | 94 | if (roomSnapshot.exists) { 95 | console.log('Create PeerConnection with configuration: ', configuration); 96 | peerConnection = new RTCPeerConnection(configuration); 97 | registerPeerConnectionListeners(); 98 | localStream.getTracks().forEach(track => { 99 | peerConnection.addTrack(track, localStream); 100 | }); 101 | 102 | // Code for collecting ICE candidates below 103 | 104 | // Code for collecting ICE candidates above 105 | 106 | peerConnection.addEventListener('track', event => { 107 | console.log('Got remote track:', event.streams[0]); 108 | event.streams[0].getTracks().forEach(track => { 109 | console.log('Add a track to the remoteStream:', track); 110 | remoteStream.addTrack(track); 111 | }); 112 | }); 113 | 114 | // Code for creating SDP answer below 115 | 116 | // Code for creating SDP answer above 117 | 118 | // Listening for remote ICE candidates below 119 | 120 | // Listening for remote ICE candidates above 121 | } 122 | } 123 | 124 | async function openUserMedia(e) { 125 | const stream = await navigator.mediaDevices.getUserMedia( 126 | {video: true, audio: true}); 127 | document.querySelector('#localVideo').srcObject = stream; 128 | localStream = stream; 129 | remoteStream = new MediaStream(); 130 | document.querySelector('#remoteVideo').srcObject = remoteStream; 131 | 132 | console.log('Stream:', document.querySelector('#localVideo').srcObject); 133 | document.querySelector('#cameraBtn').disabled = true; 134 | document.querySelector('#joinBtn').disabled = false; 135 | document.querySelector('#createBtn').disabled = false; 136 | document.querySelector('#hangupBtn').disabled = false; 137 | } 138 | 139 | async function hangUp(e) { 140 | const tracks = document.querySelector('#localVideo').srcObject.getTracks(); 141 | tracks.forEach(track => { 142 | track.stop(); 143 | }); 144 | 145 | if (remoteStream) { 146 | remoteStream.getTracks().forEach(track => track.stop()); 147 | } 148 | 149 | if (peerConnection) { 150 | peerConnection.close(); 151 | } 152 | 153 | document.querySelector('#localVideo').srcObject = null; 154 | document.querySelector('#remoteVideo').srcObject = null; 155 | document.querySelector('#cameraBtn').disabled = false; 156 | document.querySelector('#joinBtn').disabled = true; 157 | document.querySelector('#createBtn').disabled = true; 158 | document.querySelector('#hangupBtn').disabled = true; 159 | document.querySelector('#currentRoom').innerText = ''; 160 | 161 | // Delete room on hangup 162 | if (roomId) { 163 | const db = firebase.firestore(); 164 | const roomRef = db.collection('rooms').doc(roomId); 165 | const calleeCandidates = await roomRef.collection('calleeCandidates').get(); 166 | calleeCandidates.forEach(async candidate => { 167 | await candidate.delete(); 168 | }); 169 | const callerCandidates = await roomRef.collection('callerCandidates').get(); 170 | callerCandidates.forEach(async candidate => { 171 | await candidate.delete(); 172 | }); 173 | await roomRef.delete(); 174 | } 175 | 176 | document.location.reload(true); 177 | } 178 | 179 | function registerPeerConnectionListeners() { 180 | peerConnection.addEventListener('icegatheringstatechange', () => { 181 | console.log( 182 | `ICE gathering state changed: ${peerConnection.iceGatheringState}`); 183 | }); 184 | 185 | peerConnection.addEventListener('connectionstatechange', () => { 186 | console.log(`Connection state change: ${peerConnection.connectionState}`); 187 | }); 188 | 189 | peerConnection.addEventListener('signalingstatechange', () => { 190 | console.log(`Signaling state change: ${peerConnection.signalingState}`); 191 | }); 192 | 193 | peerConnection.addEventListener('iceconnectionstatechange ', () => { 194 | console.log( 195 | `ICE connection state change: ${peerConnection.iceConnectionState}`); 196 | }); 197 | } 198 | 199 | init(); 200 | --------------------------------------------------------------------------------