[
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n\n# Next.js build output\n.next\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and *not* Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Dirk Vanbeveren\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# WebRTC Video Conferencing with simple-peer\nA simple video conferencing example using simple-peer.\nThis project allows multiple devices to connect with eachother with audio and video using webrtc.\nThe package [simple-peer](https://github.com/feross/simple-peer) is used for webrtc.\nThe implementation of the signaling server is done with [socket.io](https://socket.io/)\n\n## Demo\n[Demo on heroku](https://dirvann-webrtc-video.herokuapp.com/)\n\n## Running\n\nrun `npm install` and then `npm start` in the main directory.\n\nThen open the browser at `localhost:3012` or `[your network ip/ public dns]:3012`.\n\n\n\n## Configuration\n\nConfigurations can be found in `app.js` and `public/js/main.js`.\n\nReplace the ssl certificates `ssl/key.pem` and `ssl/cert.pem` with your own.\n\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"mywebrtc\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"dependencies\": {\n    \"express\": \"^4.17.1\",\n    \"httpolyglot\": \"^0.1.2\",\n    \"simple-peer\": \"^9.7.0\",\n    \"socket.io\": \"^2.4.0\"\n  },\n  \"devDependencies\": {},\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n    \"start\": \"node src/app.js\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n"
  },
  {
    "path": "public/index.html",
    "content": "<html>\n\n<head>\n    <script lang=\"text/javascript\" src=\"/socket.io/socket.io.js\"></script>\n    <script src=\"/simple-peer/simplepeer.min.js\"></script>\n    <style>\n        .containers {\n            display: grid;\n            grid-gap: 5px;\n            grid-template-columns: repeat(auto-fit, 1fr);\n            grid-template-rows: repeat(auto-fit, 300px);\n        }\n\n        .container {\n            display: flex;\n        }\n\n        .vid {\n            flex: 0 1 auto;\n            height: 400px;\n        }\n\n        .settings {\n            background-color: #4CAF50;\n            border: none;\n            color: white;\n            padding: 5px 10px;\n            text-align: center;\n            text-decoration: none;\n            display: inline-block;\n            font-size: 14px;\n            margin: 2px 2px;\n            cursor: pointer;\n        }\n    </style>\n</head>\n\n<body>\n    <div id=\"videos\" class=\"container\">\n        <video id=\"localVideo\" class=\"vid\" autoplay muted></video>\n    </div>\n    <br />\n    <div style=\"display:block\">\n        <button id=\"switchButton\" class=\"settings\" onclick=\"switchMedia()\">Switch\n            Camera</button>\n        <button id=\"muteButton\" class=\"settings\" onclick=\"toggleMute()\">Unmuted</button>\n        <button id=\"vidButton\" class=\"settings\" onclick=\"toggleVid()\">Video Enabled</button>\n    </div>\n</body>\n<footer>\n    <script src='/js/main.js' lang=\"text/javascript\"></script>\n\n\n</footer>\n\n</html>"
  },
  {
    "path": "public/js/main.js",
    "content": "/**\n * Socket.io socket\n */\nlet socket;\n/**\n * The stream object used to send media\n */\nlet localStream = null;\n/**\n * All peer connections\n */\nlet peers = {}\n\n// redirect if not https\nif(location.href.substr(0,5) !== 'https') \n    location.href = 'https' + location.href.substr(4, location.href.length - 4)\n\n\n//////////// CONFIGURATION //////////////////\n\n/**\n * RTCPeerConnection configuration \n */\n\nconst configuration = {\n    // Using From https://www.metered.ca/tools/openrelay/\n    \"iceServers\": [\n    {\n      urls: \"stun:openrelay.metered.ca:80\"\n    },\n    {\n      urls: \"turn:openrelay.metered.ca:80\",\n      username: \"openrelayproject\",\n      credential: \"openrelayproject\"\n    },\n    {\n      urls: \"turn:openrelay.metered.ca:443\",\n      username: \"openrelayproject\",\n      credential: \"openrelayproject\"\n    },\n    {\n      urls: \"turn:openrelay.metered.ca:443?transport=tcp\",\n      username: \"openrelayproject\",\n      credential: \"openrelayproject\"\n    }\n  ]\n}\n\n/**\n * UserMedia constraints\n */\nlet constraints = {\n    audio: true,\n    video: {\n        width: {\n            max: 300\n        },\n        height: {\n            max: 300\n        }\n    }\n}\n\n/////////////////////////////////////////////////////////\n\nconstraints.video.facingMode = {\n    ideal: \"user\"\n}\n\n// enabling the camera at startup\nnavigator.mediaDevices.getUserMedia(constraints).then(stream => {\n    console.log('Received local stream');\n\n    localVideo.srcObject = stream;\n    localStream = stream;\n\n    init()\n\n}).catch(e => alert(`getusermedia error ${e.name}`))\n\n/**\n * initialize the socket connections\n */\nfunction init() {\n    socket = io()\n\n    socket.on('initReceive', socket_id => {\n        console.log('INIT RECEIVE ' + socket_id)\n        addPeer(socket_id, false)\n\n        socket.emit('initSend', socket_id)\n    })\n\n    socket.on('initSend', socket_id => {\n        console.log('INIT SEND ' + socket_id)\n        addPeer(socket_id, true)\n    })\n\n    socket.on('removePeer', socket_id => {\n        console.log('removing peer ' + socket_id)\n        removePeer(socket_id)\n    })\n\n    socket.on('disconnect', () => {\n        console.log('GOT DISCONNECTED')\n        for (let socket_id in peers) {\n            removePeer(socket_id)\n        }\n    })\n\n    socket.on('signal', data => {\n        peers[data.socket_id].signal(data.signal)\n    })\n}\n\n/**\n * Remove a peer with given socket_id. \n * Removes the video element and deletes the connection\n * @param {String} socket_id \n */\nfunction removePeer(socket_id) {\n\n    let videoEl = document.getElementById(socket_id)\n    if (videoEl) {\n\n        const tracks = videoEl.srcObject.getTracks();\n\n        tracks.forEach(function (track) {\n            track.stop()\n        })\n\n        videoEl.srcObject = null\n        videoEl.parentNode.removeChild(videoEl)\n    }\n    if (peers[socket_id]) peers[socket_id].destroy()\n    delete peers[socket_id]\n}\n\n/**\n * Creates a new peer connection and sets the event listeners\n * @param {String} socket_id \n *                 ID of the peer\n * @param {Boolean} am_initiator \n *                  Set to true if the peer initiates the connection process.\n *                  Set to false if the peer receives the connection. \n */\nfunction addPeer(socket_id, am_initiator) {\n    peers[socket_id] = new SimplePeer({\n        initiator: am_initiator,\n        stream: localStream,\n        config: configuration\n    })\n\n    peers[socket_id].on('signal', data => {\n        socket.emit('signal', {\n            signal: data,\n            socket_id: socket_id\n        })\n    })\n\n    peers[socket_id].on('stream', stream => {\n        let newVid = document.createElement('video')\n        newVid.srcObject = stream\n        newVid.id = socket_id\n        newVid.playsinline = false\n        newVid.autoplay = true\n        newVid.className = \"vid\"\n        newVid.onclick = () => openPictureMode(newVid)\n        newVid.ontouchstart = (e) => openPictureMode(newVid)\n        videos.appendChild(newVid)\n    })\n}\n\n/**\n * Opens an element in Picture-in-Picture mode\n * @param {HTMLVideoElement} el video element to put in pip mode\n */\nfunction openPictureMode(el) {\n    console.log('opening pip')\n    el.requestPictureInPicture()\n}\n\n/**\n * Switches the camera between user and environment. It will just enable the camera 2 cameras not supported.\n */\nfunction switchMedia() {\n    if (constraints.video.facingMode.ideal === 'user') {\n        constraints.video.facingMode.ideal = 'environment'\n    } else {\n        constraints.video.facingMode.ideal = 'user'\n    }\n\n    const tracks = localStream.getTracks();\n\n    tracks.forEach(function (track) {\n        track.stop()\n    })\n\n    localVideo.srcObject = null\n    navigator.mediaDevices.getUserMedia(constraints).then(stream => {\n\n        for (let socket_id in peers) {\n            for (let index in peers[socket_id].streams[0].getTracks()) {\n                for (let index2 in stream.getTracks()) {\n                    if (peers[socket_id].streams[0].getTracks()[index].kind === stream.getTracks()[index2].kind) {\n                        peers[socket_id].replaceTrack(peers[socket_id].streams[0].getTracks()[index], stream.getTracks()[index2], peers[socket_id].streams[0])\n                        break;\n                    }\n                }\n            }\n        }\n\n        localStream = stream\n        localVideo.srcObject = stream\n\n        updateButtons()\n    })\n}\n\n/**\n * Enable screen share\n */\nfunction setScreen() {\n    navigator.mediaDevices.getDisplayMedia().then(stream => {\n        for (let socket_id in peers) {\n            for (let index in peers[socket_id].streams[0].getTracks()) {\n                for (let index2 in stream.getTracks()) {\n                    if (peers[socket_id].streams[0].getTracks()[index].kind === stream.getTracks()[index2].kind) {\n                        peers[socket_id].replaceTrack(peers[socket_id].streams[0].getTracks()[index], stream.getTracks()[index2], peers[socket_id].streams[0])\n                        break;\n                    }\n                }\n            }\n\n        }\n        localStream = stream\n\n        localVideo.srcObject = localStream\n        socket.emit('removeUpdatePeer', '')\n    })\n    updateButtons()\n}\n\n/**\n * Disables and removes the local stream and all the connections to other peers.\n */\nfunction removeLocalStream() {\n    if (localStream) {\n        const tracks = localStream.getTracks();\n\n        tracks.forEach(function (track) {\n            track.stop()\n        })\n\n        localVideo.srcObject = null\n    }\n\n    for (let socket_id in peers) {\n        removePeer(socket_id)\n    }\n}\n\n/**\n * Enable/disable microphone\n */\nfunction toggleMute() {\n    for (let index in localStream.getAudioTracks()) {\n        localStream.getAudioTracks()[index].enabled = !localStream.getAudioTracks()[index].enabled\n        muteButton.innerText = localStream.getAudioTracks()[index].enabled ? \"Unmuted\" : \"Muted\"\n    }\n}\n/**\n * Enable/disable video\n */\nfunction toggleVid() {\n    for (let index in localStream.getVideoTracks()) {\n        localStream.getVideoTracks()[index].enabled = !localStream.getVideoTracks()[index].enabled\n        vidButton.innerText = localStream.getVideoTracks()[index].enabled ? \"Video Enabled\" : \"Video Disabled\"\n    }\n}\n\n/**\n * updating text of buttons\n */\nfunction updateButtons() {\n    for (let index in localStream.getVideoTracks()) {\n        vidButton.innerText = localStream.getVideoTracks()[index].enabled ? \"Video Enabled\" : \"Video Disabled\"\n    }\n    for (let index in localStream.getAudioTracks()) {\n        muteButton.innerText = localStream.getAudioTracks()[index].enabled ? \"Unmuted\" : \"Muted\"\n    }\n}\n"
  },
  {
    "path": "src/app.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst express = require('express')\nconst app = express()\nconst httpolyglot = require('httpolyglot')\nconst https = require('https')\n\n//////// CONFIGURATION ///////////\n\n// insert your own ssl certificate and keys\nconst options = {\n    key: fs.readFileSync(path.join(__dirname,'..','ssl','key.pem'), 'utf-8'),\n    cert: fs.readFileSync(path.join(__dirname,'..','ssl','cert.pem'), 'utf-8')\n}\n\nconst port = process.env.PORT || 3012\n\n////////////////////////////\n\nrequire('./routes')(app)\n\nconst httpsServer = httpolyglot.createServer(options, app)\nconst io = require('socket.io')(httpsServer)\nrequire('./socketController')(io)\n\n\nhttpsServer.listen(port, () => {\n    console.log(`listening on port ${port}`)\n})\n\n\n\n\n\n"
  },
  {
    "path": "src/routes.js",
    "content": "const path = require('path')\nconst express = require('express')\n\nmodule.exports = (app) => {\n\n    // redirect http traffic to https traffic\n    /*app.use('*', (req, res, next) => {\n        if(!req.socket.encrypted){\n            console.log('unsecure connection: redirecting..')\n            res.redirect('https://' + req.headers.host + req.path)\n        } else {\n            next()\n        }\n    })*/\n\n    app.use(express.static(path.join(__dirname, '..','public')))\n    app.use(express.static(path.join(__dirname, '..','node_modules')))\n}"
  },
  {
    "path": "src/socketController.js",
    "content": "\npeers = {}\n\n\nmodule.exports = (io) => {\n    io.on('connect', (socket) => {\n        console.log('a client is connected')\n\n\n        // Initiate the connection process as soon as the client connects\n\n        peers[socket.id] = socket\n\n        // Asking all other clients to setup the peer connection receiver\n        for(let id in peers) {\n            if(id === socket.id) continue\n            console.log('sending init receive to ' + socket.id)\n            peers[id].emit('initReceive', socket.id)\n        }\n\n        /**\n         * relay a peerconnection signal to a specific socket\n         */\n        socket.on('signal', data => {\n            console.log('sending signal from ' + socket.id + ' to ', data)\n            if(!peers[data.socket_id])return\n            peers[data.socket_id].emit('signal', {\n                socket_id: socket.id,\n                signal: data.signal\n            })\n        })\n\n        /**\n         * remove the disconnected peer connection from all other connected clients\n         */\n        socket.on('disconnect', () => {\n            console.log('socket disconnected ' + socket.id)\n            socket.broadcast.emit('removePeer', socket.id)\n            delete peers[socket.id]\n        })\n\n        /**\n         * Send message to client to initiate a connection\n         * The sender has already setup a peer connection receiver\n         */\n        socket.on('initSend', init_socket_id => {\n            console.log('INIT SEND by ' + socket.id + ' for ' + init_socket_id)\n            peers[init_socket_id].emit('initSend', socket.id)\n        })\n    })\n}"
  },
  {
    "path": "ssl/cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIICAzCCAWwCCQDUB0G6UpqG9DANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB\nVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0\ncyBQdHkgTHRkMCAXDTIwMDUwMzE1MjA0MloYDzIyOTQwMjE2MTUyMDQyWjBFMQsw\nCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJu\nZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1\nNxC8z2k7jEQkF0YkuOSqCBtc4dsb+RgPSkXu6NFMf8g59Sg7EBK29ledRlrszF1j\n9ztpgvZCy2PwpJCWu5efCacifRpDege2fPACDre/v04vWfRCzQzgwu21kM9tBPyf\nVZlM1Z5tjcbmAKrE8/DSMWxgiBP/o343UpZkQ5VLYQIDAQABMA0GCSqGSIb3DQEB\nCwUAA4GBAK+uEcDRU4BnLkr47TZ4mYm7F9h2OXAqf/9FXrH6iwBqiVenG9j8GHop\nlLZ0uAkTGUkSKlqe7imaRogKOKWzB89BTl4M3NqheeIlR5rgb8T6h73CIZViVdyF\nnid/BTSwWY3eu9qeIL6tLf4977ExfIUZCVlL739pasTx6wXQ1h5i\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "ssl/key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIICXwIBAAKBgQC1NxC8z2k7jEQkF0YkuOSqCBtc4dsb+RgPSkXu6NFMf8g59Sg7\nEBK29ledRlrszF1j9ztpgvZCy2PwpJCWu5efCacifRpDege2fPACDre/v04vWfRC\nzQzgwu21kM9tBPyfVZlM1Z5tjcbmAKrE8/DSMWxgiBP/o343UpZkQ5VLYQIDAQAB\nAoGBAKdQplWOM51Vjvy7uHnjdM1BGvKRXlrfjMVhUFyb/dNiEB7jKjgOSRlBLff6\no60LrBbGiMaMso9Nd4MSjqV9oykYQ3zhdSV/W8W7nA5OhXzgJF4cngkrwnxWhLYA\nmqH8GJWCvz7/7A8Ckeutx8Zn1GTJWcd/CfSd9SDNGAJqJA4BAkEA3RYhdATWwjhZ\nKsWwFMVzG7fBqCvJLO/Ep9kIvpLtiaQjyNtftS8Nw91IpEVodTAtxxiVBiFRiFxS\n3yTWVUOEsQJBANHVDejleEYvT/Rc7aN0bhJVcZuhaN9qPDIXY5UIQTYR1NzgcQKA\nqCo7u0x0+vQiYJ15YqrRo8ZkdqvyCNlmnbECQQC2KrYF0rbh8WwHQjyD4O2nuRFo\ncCujSyzO4JXD8WyoLQcPSTLjJ5JAAOUJ9ebMKJaPpkGke2+i2++szb2NI8UBAkEA\nkKoBmAK0hDbUOdXjpGB+DrfHxpNmmTlF3QcRCcuSIfPzPICkiSQoTE24GMNBzRTy\nZT8tzjUQY5QZ2PvaLAA6UQJBAJXLnYMIX0ZkO1nG6mMIWoZtrLpfg+pSFa0VUsHs\n7G3iYf9v31Wd6HipMRzg/q63m90nwI5emTYe+EEo4lkGnz8=\n-----END RSA PRIVATE KEY-----\n"
  }
]