{"id":26320893,"url":"https://github.com/clarklindev/webrtc-robbertbunch-starter","last_synced_at":"2026-05-06T22:37:17.462Z","repository":{"id":282499182,"uuid":"948781408","full_name":"clarklindev/webrtc-robbertbunch-starter","owner":"clarklindev","description":"webrtc-starter - Robbert Bunch youtube tutorial. p2p video webapp - starter files on \"webrtc in 80 minutes\" server setup starts @32min - coding starts at 42min.","archived":false,"fork":false,"pushed_at":"2025-03-15T02:04:58.000Z","size":19,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-15T02:30:46.066Z","etag":null,"topics":["js","nodejs","webapp","webrtc","webrtc-video"],"latest_commit_sha":null,"homepage":"https://youtu.be/g42yNO_dxWQ?si=Yt7kbJ0iutcXMAZS","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/clarklindev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-03-15T00:24:03.000Z","updated_at":"2025-03-15T02:05:02.000Z","dependencies_parsed_at":"2025-03-15T05:45:07.598Z","dependency_job_id":null,"html_url":"https://github.com/clarklindev/webrtc-robbertbunch-starter","commit_stats":null,"previous_names":["clarklindev/webrtc-robbertbunch-starter"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/clarklindev/webrtc-robbertbunch-starter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clarklindev%2Fwebrtc-robbertbunch-starter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clarklindev%2Fwebrtc-robbertbunch-starter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clarklindev%2Fwebrtc-robbertbunch-starter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clarklindev%2Fwebrtc-robbertbunch-starter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/clarklindev","download_url":"https://codeload.github.com/clarklindev/webrtc-robbertbunch-starter/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clarklindev%2Fwebrtc-robbertbunch-starter/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32715426,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-06T19:35:05.142Z","status":"ssl_error","status_checked_at":"2026-05-06T19:35:03.996Z","response_time":117,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["js","nodejs","webapp","webrtc","webrtc-video"],"created_at":"2025-03-15T16:15:20.910Z","updated_at":"2026-05-06T22:37:17.448Z","avatar_url":"https://github.com/clarklindev.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# WebRTC - youtube - Robbert Bunch - webrtc starter\n\n[0:00](https://www.youtube.com/watch?v=g42yNO_dxWQ) - How the video is organized  \n[5:00](https://www.youtube.com/watch?v=g42yNO_dxWQ\u0026t=300s) - The. 2 Parts of WebRTC  \n[9:20](https://www.youtube.com/watch?v=g42yNO_dxWQ\u0026t=560s) - UDP vs. TCP  \n\n## WHITEBOARDING  \n[12:12](https://www.youtube.com/watch?v=g42yNO_dxWQ\u0026t=732s) - [White-boarding the whole process](#white-boarding-the-whole-process)  \n[23:25](https://www.youtube.com/watch?v=g42yNO_dxWQ\u0026t=1405s) - White-Boarding Cont. - Signaling  \n\n## SERVER SETUP  \n[32:10](https://www.youtube.com/watch?v=g42yNO_dxWQ\u0026t=1930s) - GitHub/Project Instructions  \n[35:36](https://www.youtube.com/watch?v=g42yNO_dxWQ\u0026t=2136s) - [Setting Up Local HTTPS](#setting-up-local-https)  \n[39:00](https://www.youtube.com/watch?v=g42yNO_dxWQ\u0026t=2340s) - [Running On Local IP](#running-on-local-ip)  \n[43:05](https://www.youtube.com/watch?v=g42yNO_dxWQ\u0026t=2585s) - [index.html](#indexhtml)  \n[45:24](https://www.youtube.com/watch?v=g42yNO_dxWQ\u0026t=2724s) - [connect to socket.io](#connect-to-socketio)  \n\n## WEBRTC  \n[51:03](https://www.youtube.com/watch?v=g42yNO_dxWQ\u0026t=3063s) - [call button](#callbutton) and [fetchUserMedia()](#fetchusermedia)  \n[57:04](https://www.youtube.com/watch?v=g42yNO_dxWQ\u0026t=3424s) - createPeerConnection()  \n[63:39](https://www.youtube.com/watch?v=g42yNO_dxWQ\u0026t=3819s) - createOffer()  \n[66:54](https://www.youtube.com/watch?v=g42yNO_dxWQ\u0026t=4500s) - signaling server gets offer  \n[69:50](https://www.youtube.com/watch?v=g42yNO_dxWQ\u0026t=4170s) - client handles newOffer and sends answer   \n[74:49](https://www.youtube.com/watch?v=g42yNO_dxWQ\u0026t=4489s) - server handles new answer  \n[77:47](https://www.youtube.com/watch?v=g42yNO_dxWQ\u0026t=4667s) - client handles new answer  \n[78:40](https://www.youtube.com/watch?v=g42yNO_dxWQ\u0026t=4720s) - ice candidate passing  \n\n---\n---\n\n## White-boarding the whole process\n\n\u003cimg\nsrc='public/section04-38-webrtc-process-review-webrtc-flow.jpg'\nalt='section04-38-webrtc-process-review-webrtc-flow.jpg'\nwidth=1000\n/\u003e\n\n## Setting Up Local HTTPS\n1. npm install mkcert -g\n2. mkcert create-ca\n3. mkcert create-cert\n4. OPTIONAL: to run it locally, update the files with your local IP\n    - if using a phone or another computer (ipconfig to get local ip)\n\n## Running On Local IP\n### setup server.js\n- create our socket.io server\n- add local ip address to cors origins\n- also update to connect to local ip address -\u003e see [connect to socket.io](#connect-to-socketio)  \n\n```js\n//server.js\nconst fs = require('fs');\nconst https = require('https');\nconst express = require('express');\nconst app = express();\nconst socketio = require('socket.io');\napp.use(express.static(__dirname)); //handle static files -\u003e tells express that if you find any file in current folder, serve it up (eg. html/css etc)\n\nconst key = fs.readFileSync('cert.key'); //get contents\nconst cert = fs.readFileSync('cert.crt'); //get contents\nconst expressServer = https.createServer({key, cert}, app);\n\nconst io = socketio(expressServer,{\n    cors: {\n        origin: [\n            'https://localhost:8181',\n            'https://192.168.1.103:8181' //if using a phone or another computer (ipconfig to get local ip)\n        ],\n        methods: [\"GET\", \"POST\"]\n    }\n});\n```\n\n## talking through the code\n\n### index.html\n- starts at 43:09 index.html\n    - `\u003cvideo id=\"local-video\"\u003e\u003c/video\u003e`\n    - `\u003cvideo id=\"remote-video\"\u003e\u003c/video\u003e`\n\n- note with: `\u003cscript src=\"/socket.io/socket.io.js\"\u003e\u003c/script\u003e` this is how to include `socket.io.js` when putting it in html\n- gives access to `io` in the code eg. `io.connect()`\n\n### socketListeners.js\n- `socketListeners.js` - socket related\n\n### server.js\n- `server.js` \n    - `const express = require('express');`\n    - `const socketio = require('socket.io');`\n    - fs module toget contents of cert.key and cert.crt (used for creating the server)\n    - cors origin lists valid domains you can visit the server from (NOTE: port NOT included here)\n        - https://localhost\n        - https://192.168.1.44\n    - get access to passed in username + password \n\n    ```js\n    const userName = socket.handshank.auth.userName;\n    const password = socket.handshank.auth.password;\n\n    //validate the password\n    if(password !== \"\"){\n        socket.disconnect(true);\n    }\n    ```\n\n### scripts.js\n- `scripts.js` - webrtc related\n\n- connect to socket.io\n- send userName and password on connecting to socket server\n- `localStream` is what we store in localVideoEl\n- `remoteStream` is what we store in remoteVideoEl\n- `peerConfiguration` - where we put stun servers\n\n```js\n// scripts.js\n\n//if trying it on a phone, use this instead...\nconst socket = io.connect('https://192.168.1.103:8181',{\n// const socket = io.connect('https://localhost:8181',{\n    auth: {\n        userName,password\n    }\n})\n\nconst localVideoEl = document.querySelector('#local-video');\nconst remoteVideoEl = document.querySelector('#remote-video');\n\nlet localStream; //a var to hold the local video stream\nlet remoteStream; //a var to hold the remote video stream\nlet peerConnection; //the peerConnection that the two clients use to talk\nlet didIOffer = false;\n\nlet peerConfiguration = {\n    iceServers:[\n        {\n            urls:[\n              'stun:stun.l.google.com:19302',\n              'stun:stun1.l.google.com:19302'\n            ]\n        }\n    ]\n}\n\nconst call = async e=\u003e{}\nconst answerOffer = async(offerObj)=\u003e{}\nconst addAnswer = async(offerObj)=\u003e{}\nconst fetchUserMedia = ()=\u003e{}\nconst createPeerConnection = (offerObj)=\u003e{\n    peerConnection.addEventListener(\"signalingstatechange\", (event) =\u003e {});\n    peerConnection.addEventListener('icecandidate',e=\u003e{});\n    peerConnection.addEventListener('track',e=\u003e{});\n    if(offerObj){\n        //this won't be set when called from call();\n        //will be set when we call from answerOffer()\n        await peerConnection.setRemoteDescription(offerObj.offer)\n    }\n}\nconst addNewIceCandidate = iceCandidate=\u003e{}\n\ndocument.querySelector('#call').addEventListener('click',call)\n\n```\n\n## detailed functions\n\n### callButton\n- `document.querySelector('#call').addEventListener('click',call)`\n\n### fetchUserMedia\n\n- OUTCOME: \n    - `localVideoEl` has video feed \n    - have a stream (`localStream`)\n\n-  `const stream = await navigator.mediaDevices.getUserMedia();` prompts you to use your camera/microphone \n- if user selects \"allow\"\n- then you set: \n    - `localVideoEl.srcObject = stream;`\n    - `localStream = stream;`\n\n### createPeerConnection\n- call calls createPeerConnection()\n    - `const createPeerConnection = (offerObj)=\u003e{}` initial call doesnt send through offerObj (whoever initiates call doesnt send offerObj)\n- TODO: create a RTCPeerConnection, it can receive stun servers (see scripts.js when we set `peerConfiguration`)\n    - WebRTC -\u003e `peerConnection = await new RTCPeerConnection(peerConfiguration)`\n\n#### create remoteStream and set remoteVideoEl\n- TODO: set remoteStream -\u003e create a NEW MediaStream \n    - `remoteStream = new MediaStream();`\n- TODO: set remoteVideoEl `remoteVideoEl.srcObject = remoteStream` (html) \n- OUTCOME -\u003e other video feed has loading spinner \n\n#### add localStream tracks (from GetUserMedia) to peer connection\n- TODO: `localStream.getTracks()` for each track,\n- TODO: addTrack() adds new media tracks to peerConnection -\u003e ie. associate client1 feed with peerConnection -\u003e `peerConnection.addTrack(track,localStream);`\n    - props (track, stream to add it to)\n\n```js\nlocalStream.getTracks().forEach(track=\u003e{\n    //add localtracks so that they can be sent once the connection is established\n    peerConnection.addTrack(track,localStream);\n})\n\n```\n\n#### icecandidate event listener\n- `peerConnection.addEventListener('icecandidate',e=\u003e{});`\n- triggers because we createPeerConnection() creates `peerConnection = await new RTCPeerConnection(peerConfiguration)` and that returns ice candidates\n- if there are ice candidates, send it up to socket server `socket.emit('sendIceCandidateToSignalingServer')`\n\n#### track event listener\n- `track` event triggers from other side..\n- for each track in stream `remoteStream.addTrack(track, remoteStream)`;\n- the remote stream (`remoteStream`) is playing inside `\u003cvideo id=\"remote-video\"\u003e`\n\n#### \ncreatePeerConnection(offerObj) if you are NOT the call initiator, then it receives offerObj\n\n```js\nif(offerObj){\n    //this won't be set when called from call();\n    //will be set when we call from answerOffer()\n    await peerConnection.setRemoteDescription(offerObj.offer)\n}\n```\n\n### create the offer\n- @63min39sec / step5 in taskList.md\n- `const offer = await peerConnection.createOffer();`\n- `peerConnection.setLocalDescription(offer);`\n- set `didIOffer = true;`\n- `socket.emit('newOffer',offer);` //send offer to signalingServer\n\n```js\n//create offer time!\ntry{\n    console.log(\"Creating offer...\")\n    const offer = await peerConnection.createOffer();\n    console.log(offer); //sdp + type\n    peerConnection.setLocalDescription(offer);\n    didIOffer = true;\n    socket.emit('newOffer',offer); //send offer to signalingServer\n}catch(err){\n    console.log(err)\n}\n```\n\n```js\n//scripts.js\n\n//when a client initiates a call\nconst call = async e=\u003e{\n    //@51min03sec\n    await fetchUserMedia();\n\n    //peerConnection is all set with our STUN servers sent over\n    //@57min04sec\n    await createPeerConnection();\n\n    //create offer time!\n    try{\n        console.log(\"Creating offer...\")\n        const offer = await peerConnection.createOffer();\n        console.log(offer);\n        peerConnection.setLocalDescription(offer);\n        didIOffer = true;\n        socket.emit('newOffer',offer); //send offer to signalingServer\n    }catch(err){\n        console.log(err)\n    }\n\n}\n\nconst fetchUserMedia = ()=\u003e{\n    return new Promise(async(resolve, reject)=\u003e{\n        try{\n            const stream = await navigator.mediaDevices.getUserMedia({\n                video: true,\n                // audio: true,\n            });\n            localVideoEl.srcObject = stream;\n            localStream = stream;    \n            resolve();    \n        }catch(err){\n            console.log(err);\n            reject()\n        }\n    })\n}\n\nconst createPeerConnection = (offerObj)=\u003e{\n    return new Promise(async(resolve, reject)=\u003e{\n        //RTCPeerConnection is the thing that creates the connection\n        //we can pass a config object, and that config object can contain stun servers\n        //which will fetch us ICE candidates\n        peerConnection = await new RTCPeerConnection(peerConfiguration)\n        remoteStream = new MediaStream()\n        remoteVideoEl.srcObject = remoteStream;\n\n\n        localStream.getTracks().forEach(track=\u003e{\n            //add localtracks so that they can be sent once the connection is established\n            peerConnection.addTrack(track,localStream);\n        })\n\n        peerConnection.addEventListener(\"signalingstatechange\", (event) =\u003e {\n            console.log(event);\n            console.log(peerConnection.signalingState)\n        });\n\n        peerConnection.addEventListener('icecandidate',e=\u003e{\n            console.log('........Ice candidate found!......')\n            console.log(e)\n            if(e.candidate){\n                socket.emit('sendIceCandidateToSignalingServer',{\n                    iceCandidate: e.candidate,\n                    iceUserName: userName,\n                    didIOffer,\n                })    \n            }\n        })\n        \n        peerConnection.addEventListener('track',e=\u003e{\n            console.log(\"Got a track from the other peer!! How excting\")\n            console.log(e)\n            e.streams[0].getTracks().forEach(track=\u003e{\n                remoteStream.addTrack(track,remoteStream);\n                console.log(\"Here's an exciting moment... fingers cross\")\n            })\n        })\n\n        if(offerObj){\n            //this won't be set when called from call();\n            //will be set when we call from answerOffer()\n            // console.log(peerConnection.signalingState) //should be stable because no setDesc has been run yet\n            await peerConnection.setRemoteDescription(offerObj.offer)\n            // console.log(peerConnection.signalingState) //should be have-remote-offer, because client2 has setRemoteDesc on the offer\n        }\n        resolve();\n    })\n}\n```\n\n## server.js\n- `io.on('connection',(socket)=\u003e{})` socket refers to whoever connected\n- socket.on('newOffer',newOffer=\u003e{}) we push onto offers an object {}\n- initially when we push onto `offers`, we only have `offer`'s value `newOffer`\n\n```js\n// server.js\n//offers will contain {}\nconst offers = [\n    // offererUserName\n    // offer\n    // offerIceCandidates\n    // answererUserName\n    // answer\n    // answererIceCandidates\n];\nconst connectedSockets = [\n    //username, socketId\n]\n\nio.on('connection',(socket)=\u003e{\n    socket.on('newOffer',newOffer=\u003e{\n        offers.push({\n            offererUserName: userName,\n            offer: newOffer,\n            offerIceCandidates: [],\n            answererUserName: null,\n            answer: null,\n            answererIceCandidates: []\n        })\n\n    });\n    socket.on('newAnswer',(offerObj,ackFunction)=\u003e{});\n    socket.on('sendIceCandidateToSignalingServer',iceCandidateObj=\u003e{});\n});\n```\n\n### detailed code\n\n```js\n//server.js\nio.on('connection',(socket)=\u003e{\n    // console.log(\"Someone has connected\");\n    const userName = socket.handshake.auth.userName;\n    const password = socket.handshake.auth.password;\n\n    if(password !== \"x\"){\n        socket.disconnect(true);\n        return;\n    }\n    connectedSockets.push({\n        socketId: socket.id,\n        userName\n    })\n\n    //a new client has joined. If there are any offers available,\n    //emit them out\n    if(offers.length){\n        socket.emit('availableOffers',offers);\n    }\n    \n    socket.on('newOffer',newOffer=\u003e{\n        offers.push({\n            offererUserName: userName,\n            offer: newOffer,\n            offerIceCandidates: [],\n            answererUserName: null,\n            answer: null,\n            answererIceCandidates: []\n        })\n        // console.log(newOffer.sdp.slice(50))\n        //send out to all connected sockets EXCEPT the caller\n        socket.broadcast.emit('newOfferAwaiting',offers.slice(-1))\n    })\n\n    socket.on('newAnswer',(offerObj,ackFunction)=\u003e{\n        console.log(offerObj);\n        //emit this answer (offerObj) back to CLIENT1\n        //in order to do that, we need CLIENT1's socketid\n        const socketToAnswer = connectedSockets.find(s=\u003es.userName === offerObj.offererUserName)\n        if(!socketToAnswer){\n            console.log(\"No matching socket\")\n            return;\n        }\n        //we found the matching socket, so we can emit to it!\n        const socketIdToAnswer = socketToAnswer.socketId;\n        //we find the offer to update so we can emit it\n        const offerToUpdate = offers.find(o=\u003eo.offererUserName === offerObj.offererUserName)\n        if(!offerToUpdate){\n            console.log(\"No OfferToUpdate\")\n            return;\n        }\n        //send back to the answerer all the iceCandidates we have already collected\n        ackFunction(offerToUpdate.offerIceCandidates);\n        offerToUpdate.answer = offerObj.answer\n        offerToUpdate.answererUserName = userName\n        //socket has a .to() which allows emiting to a \"room\"\n        //every socket has it's own room\n        socket.to(socketIdToAnswer).emit('answerResponse',offerToUpdate)\n    })\n\n    socket.on('sendIceCandidateToSignalingServer',iceCandidateObj=\u003e{\n        const { didIOffer, iceUserName, iceCandidate } = iceCandidateObj;\n        // console.log(iceCandidate);\n        if(didIOffer){\n            //this ice is coming from the offerer. Send to the answerer\n            const offerInOffers = offers.find(o=\u003eo.offererUserName === iceUserName);\n            if(offerInOffers){\n                offerInOffers.offerIceCandidates.push(iceCandidate)\n                // 1. When the answerer answers, all existing ice candidates are sent\n                // 2. Any candidates that come in after the offer has been answered, will be passed through\n                if(offerInOffers.answererUserName){\n                    //pass it through to the other socket\n                    const socketToSendTo = connectedSockets.find(s=\u003es.userName === offerInOffers.answererUserName);\n                    if(socketToSendTo){\n                        socket.to(socketToSendTo.socketId).emit('receivedIceCandidateFromServer',iceCandidate)\n                    }else{\n                        console.log(\"Ice candidate recieved but could not find answere\")\n                    }\n                }\n            }\n        }else{\n            //this ice is coming from the answerer. Send to the offerer\n            //pass it through to the other socket\n            const offerInOffers = offers.find(o=\u003eo.answererUserName === iceUserName);\n            const socketToSendTo = connectedSockets.find(s=\u003es.userName === offerInOffers.offererUserName);\n            if(socketToSendTo){\n                socket.to(socketToSendTo.socketId).emit('receivedIceCandidateFromServer',iceCandidate)\n            }else{\n                console.log(\"Ice candidate recieved but could not find offerer\")\n            }\n        }\n        // console.log(offers)\n    })\n\n})\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclarklindev%2Fwebrtc-robbertbunch-starter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fclarklindev%2Fwebrtc-robbertbunch-starter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclarklindev%2Fwebrtc-robbertbunch-starter/lists"}