├── .gitignore ├── package.json ├── README.md ├── style.css ├── index.html ├── favicon.svg └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webrtc-demo", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "serve": "vite preview" 8 | }, 9 | "devDependencies": { 10 | "vite": "^2.0.5" 11 | }, 12 | "dependencies": { 13 | "firebase": "^8.2.10" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Video Chat with WebRTC and Firebase 2 | 3 | Build a 1-to-1 video chat feature with WebRTC, Firestore, and JavaScript. 4 | 5 | Watch the [WebRTC Explanation on YouTube](https://youtu.be/WmR9IMUD_CY) and follow the full [WebRTC Firebase Tutorial](https://fireship.io/lessons/webrtc-firebase-video-chat) on Fireship.io. 6 | 7 | 8 | ## Usage 9 | 10 | Update the firebase project config in the main.js file. 11 | 12 | ``` 13 | git clone 14 | npm install 15 | 16 | npm run dev 17 | ``` 18 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Syne+Mono&display=swap'); 2 | 3 | body { 4 | font-family: 'Syne Mono', monospace; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | text-align: center; 8 | color: #2c3e50; 9 | margin: 80px 10px; 10 | } 11 | 12 | video { 13 | width: 40vw; 14 | height: 30vw; 15 | margin: 2rem; 16 | background: #2c3e50; 17 | } 18 | 19 | .videos { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WebRTC Demo 8 | 9 | 10 |

1. Start your Webcam

11 |
12 | 13 |

Local Stream

14 | 15 |
16 | 17 |

Remote Stream

18 | 19 |
20 | 21 | 22 |
23 | 24 | 25 |

2. Create a new Call

26 | 27 | 28 |

3. Join a Call

29 |

Answer the call from a different browser window or device

30 | 31 | 32 | 33 | 34 |

4. Hangup

35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | 3 | import firebase from 'firebase/app'; 4 | import 'firebase/firestore'; 5 | 6 | const firebaseConfig = { 7 | // your config 8 | }; 9 | 10 | if (!firebase.apps.length) { 11 | firebase.initializeApp(firebaseConfig); 12 | } 13 | const firestore = firebase.firestore(); 14 | 15 | const servers = { 16 | iceServers: [ 17 | { 18 | urls: ['stun:stun1.l.google.com:19302', 'stun:stun2.l.google.com:19302'], 19 | }, 20 | ], 21 | iceCandidatePoolSize: 10, 22 | }; 23 | 24 | // Global State 25 | const pc = new RTCPeerConnection(servers); 26 | let localStream = null; 27 | let remoteStream = null; 28 | 29 | // HTML elements 30 | const webcamButton = document.getElementById('webcamButton'); 31 | const webcamVideo = document.getElementById('webcamVideo'); 32 | const callButton = document.getElementById('callButton'); 33 | const callInput = document.getElementById('callInput'); 34 | const answerButton = document.getElementById('answerButton'); 35 | const remoteVideo = document.getElementById('remoteVideo'); 36 | const hangupButton = document.getElementById('hangupButton'); 37 | 38 | // 1. Setup media sources 39 | 40 | webcamButton.onclick = async () => { 41 | localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); 42 | remoteStream = new MediaStream(); 43 | 44 | // Push tracks from local stream to peer connection 45 | localStream.getTracks().forEach((track) => { 46 | pc.addTrack(track, localStream); 47 | }); 48 | 49 | // Pull tracks from remote stream, add to video stream 50 | pc.ontrack = (event) => { 51 | event.streams[0].getTracks().forEach((track) => { 52 | remoteStream.addTrack(track); 53 | }); 54 | }; 55 | 56 | webcamVideo.srcObject = localStream; 57 | remoteVideo.srcObject = remoteStream; 58 | 59 | callButton.disabled = false; 60 | answerButton.disabled = false; 61 | webcamButton.disabled = true; 62 | }; 63 | 64 | // 2. Create an offer 65 | callButton.onclick = async () => { 66 | // Reference Firestore collections for signaling 67 | const callDoc = firestore.collection('calls').doc(); 68 | const offerCandidates = callDoc.collection('offerCandidates'); 69 | const answerCandidates = callDoc.collection('answerCandidates'); 70 | 71 | callInput.value = callDoc.id; 72 | 73 | // Get candidates for caller, save to db 74 | pc.onicecandidate = (event) => { 75 | event.candidate && offerCandidates.add(event.candidate.toJSON()); 76 | }; 77 | 78 | // Create offer 79 | const offerDescription = await pc.createOffer(); 80 | await pc.setLocalDescription(offerDescription); 81 | 82 | const offer = { 83 | sdp: offerDescription.sdp, 84 | type: offerDescription.type, 85 | }; 86 | 87 | await callDoc.set({ offer }); 88 | 89 | // Listen for remote answer 90 | callDoc.onSnapshot((snapshot) => { 91 | const data = snapshot.data(); 92 | if (!pc.currentRemoteDescription && data?.answer) { 93 | const answerDescription = new RTCSessionDescription(data.answer); 94 | pc.setRemoteDescription(answerDescription); 95 | } 96 | }); 97 | 98 | // When answered, add candidate to peer connection 99 | answerCandidates.onSnapshot((snapshot) => { 100 | snapshot.docChanges().forEach((change) => { 101 | if (change.type === 'added') { 102 | const candidate = new RTCIceCandidate(change.doc.data()); 103 | pc.addIceCandidate(candidate); 104 | } 105 | }); 106 | }); 107 | 108 | hangupButton.disabled = false; 109 | }; 110 | 111 | // 3. Answer the call with the unique ID 112 | answerButton.onclick = async () => { 113 | const callId = callInput.value; 114 | const callDoc = firestore.collection('calls').doc(callId); 115 | const answerCandidates = callDoc.collection('answerCandidates'); 116 | const offerCandidates = callDoc.collection('offerCandidates'); 117 | 118 | pc.onicecandidate = (event) => { 119 | event.candidate && answerCandidates.add(event.candidate.toJSON()); 120 | }; 121 | 122 | const callData = (await callDoc.get()).data(); 123 | 124 | const offerDescription = callData.offer; 125 | await pc.setRemoteDescription(new RTCSessionDescription(offerDescription)); 126 | 127 | const answerDescription = await pc.createAnswer(); 128 | await pc.setLocalDescription(answerDescription); 129 | 130 | const answer = { 131 | type: answerDescription.type, 132 | sdp: answerDescription.sdp, 133 | }; 134 | 135 | await callDoc.update({ answer }); 136 | 137 | offerCandidates.onSnapshot((snapshot) => { 138 | snapshot.docChanges().forEach((change) => { 139 | console.log(change); 140 | if (change.type === 'added') { 141 | let data = change.doc.data(); 142 | pc.addIceCandidate(new RTCIceCandidate(data)); 143 | } 144 | }); 145 | }); 146 | }; 147 | --------------------------------------------------------------------------------