├── .gitignore ├── examples ├── audio-video-loopback │ ├── server.js │ └── client.js ├── ping-pong │ ├── server.js │ └── client.js ├── broadcaster │ ├── server.js │ └── client.js ├── viewer │ ├── server.js │ └── client.js ├── sine-wave │ ├── server.js │ └── client.js ├── pitch-detector │ ├── server.js │ └── client.js ├── sine-wave-stereo │ ├── server.js │ └── client.js ├── record-audio-video-stream │ ├── client.js │ └── server.js ├── video-compositing │ ├── client.js │ └── server.js └── datachannel-buffer-limits │ ├── server.js │ └── client.js ├── lib ├── server │ ├── connections │ │ ├── connection.js │ │ ├── webrtcconnectionmanager.js │ │ ├── connectionmanager.js │ │ └── webrtcconnection.js │ ├── rest │ │ └── connectionsapi.js │ └── webrtc │ │ └── rtcaudiosourcesinewave.js ├── browser │ ├── webaudio │ │ ├── refcountedaudiocontext.js │ │ └── webaudiooscillatornodesinewave.js │ ├── example.js │ └── startstopbutton.js ├── common │ └── pitchdetector.js └── client │ └── index.js ├── test ├── integration │ ├── server │ │ ├── webrtc │ │ │ └── rtcaudiosourcesinewave.js │ │ └── rest │ │ │ └── connectionsapi.js │ └── client │ │ └── index.js ├── unit │ └── server │ │ └── connections │ │ ├── webrtcconnectionmanager.js │ │ ├── connectionmanager.js │ │ └── webrtcconnection.js └── lib │ └── testrtcpeerconnection.js ├── package.json ├── html └── index.html ├── index.js ├── .eslintrc └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /examples/audio-video-loopback/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function beforeOffer(peerConnection) { 4 | const audioTransceiver = peerConnection.addTransceiver('audio'); 5 | const videoTransceiver = peerConnection.addTransceiver('video'); 6 | return Promise.all([ 7 | audioTransceiver.sender.replaceTrack(audioTransceiver.receiver.track), 8 | videoTransceiver.sender.replaceTrack(videoTransceiver.receiver.track) 9 | ]); 10 | } 11 | 12 | module.exports = { beforeOffer }; 13 | -------------------------------------------------------------------------------- /lib/server/connections/connection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EventEmitter = require('events'); 4 | 5 | class Connection extends EventEmitter { 6 | constructor(id) { 7 | super(); 8 | this.id = id; 9 | this.state = 'open'; 10 | } 11 | 12 | close() { 13 | this.state = 'closed'; 14 | this.emit('closed'); 15 | } 16 | 17 | toJSON() { 18 | return { 19 | id: this.id, 20 | state: this.state 21 | }; 22 | } 23 | } 24 | 25 | module.exports = Connection; 26 | -------------------------------------------------------------------------------- /lib/browser/webaudio/refcountedaudiocontext.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let audioContext = null; 4 | let refCount = 0; 5 | 6 | function acquireAudioContext() { 7 | refCount++; 8 | if (refCount && !audioContext) { 9 | audioContext = new AudioContext(); 10 | } 11 | return audioContext; 12 | } 13 | 14 | function releaseAudioContext() { 15 | refCount--; 16 | if (!refCount && audioContext) { 17 | audioContext.close(); 18 | audioContext = null; 19 | } 20 | } 21 | 22 | exports.acquireAudioContext = acquireAudioContext; 23 | exports.releaseAudioContext = releaseAudioContext; 24 | -------------------------------------------------------------------------------- /examples/ping-pong/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function beforeOffer(peerConnection) { 4 | const dataChannel = peerConnection.createDataChannel('ping-pong'); 5 | 6 | function onMessage({ data }) { 7 | if (data === 'ping') { 8 | dataChannel.send('pong'); 9 | } 10 | } 11 | 12 | dataChannel.addEventListener('message', onMessage); 13 | 14 | // NOTE(mroberts): This is a hack so that we can get a callback when the 15 | // RTCPeerConnection is closed. In the future, we can subscribe to 16 | // "connectionstatechange" events. 17 | const { close } = peerConnection; 18 | peerConnection.close = function() { 19 | dataChannel.removeEventListener('message', onMessage); 20 | return close.apply(this, arguments); 21 | }; 22 | } 23 | 24 | module.exports = { beforeOffer }; 25 | -------------------------------------------------------------------------------- /examples/broadcaster/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { EventEmitter } = require('events'); 4 | 5 | const broadcaster = new EventEmitter(); 6 | const { on } = broadcaster; 7 | 8 | function beforeOffer(peerConnection) { 9 | const audioTrack = broadcaster.audioTrack = peerConnection.addTransceiver('audio').receiver.track; 10 | const videoTrack = broadcaster.videoTrack = peerConnection.addTransceiver('video').receiver.track; 11 | 12 | broadcaster.emit('newBroadcast', { 13 | audioTrack, 14 | videoTrack 15 | }); 16 | 17 | const { close } = peerConnection; 18 | peerConnection.close = function() { 19 | audioTrack.stop() 20 | videoTrack.stop() 21 | return close.apply(this, arguments); 22 | }; 23 | } 24 | 25 | module.exports = { 26 | beforeOffer, 27 | broadcaster 28 | }; 29 | -------------------------------------------------------------------------------- /examples/viewer/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { broadcaster } = require('../broadcaster/server') 4 | 5 | function beforeOffer(peerConnection) { 6 | const audioTransceiver = peerConnection.addTransceiver('audio'); 7 | const videoTransceiver = peerConnection.addTransceiver('video'); 8 | 9 | function onNewBroadcast({ audioTrack, videoTrack }) { 10 | audioTransceiver.sender.replaceTrack(audioTrack), 11 | videoTransceiver.sender.replaceTrack(videoTrack) 12 | } 13 | 14 | broadcaster.on('newBroadcast', onNewBroadcast) 15 | 16 | if (broadcaster.audioTrack && broadcaster.videoTrack) { 17 | onNewBroadcast(broadcaster); 18 | } 19 | 20 | const { close } = peerConnection; 21 | peerConnection.close = function() { 22 | broadcaster.removeListener('newBroadcast', onNewBroadcast); 23 | return close.apply(this, arguments); 24 | } 25 | } 26 | 27 | module.exports = { beforeOffer }; 28 | -------------------------------------------------------------------------------- /test/integration/server/webrtc/rtcaudiosourcesinewave.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tape = require('tape'); 4 | const { RTCAudioSink } = require('wrtc').nonstandard; 5 | 6 | const PitchDetector = require('../../../../lib/common/pitchdetector'); 7 | const RTCAudioSourceSineWave = require('../../../../lib/server/webrtc/rtcaudiosourcesinewave'); 8 | 9 | tape('RTCAudioSinkFrequencyDetector', t => { 10 | t.test('it works', t => { 11 | const source = new RTCAudioSourceSineWave(); 12 | const track = source.createTrack(); 13 | const sink = new RTCAudioSink(track); 14 | const pitchDetector = new PitchDetector(track); 15 | const e = 1; 16 | sink.ondata = data => { 17 | const frequency = pitchDetector.onData(data); 18 | if (source.frequency - e <= frequency && frequency <= source.frequency + e) { 19 | sink.stop(); 20 | track.stop(); 21 | source.close(); 22 | t.end(); 23 | } 24 | }; 25 | }); 26 | t.end(); 27 | }); 28 | -------------------------------------------------------------------------------- /lib/browser/example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const createStartStopButton = require('./startstopbutton'); 4 | const ConnectionClient = require('../client'); 5 | 6 | function createExample(name, description, options) { 7 | const nameTag = document.createElement('h2'); 8 | nameTag.innerText = name; 9 | document.body.appendChild(nameTag); 10 | 11 | const descriptionTag = document.createElement('p'); 12 | descriptionTag.innerHTML = description; 13 | document.body.appendChild(descriptionTag); 14 | 15 | const clickStartTag = document.createElement('p'); 16 | clickStartTag.innerHTML = 'Click “Start” to begin.'; 17 | document.body.appendChild(clickStartTag); 18 | 19 | const connectionClient = new ConnectionClient(); 20 | 21 | let peerConnection = null; 22 | 23 | createStartStopButton(async () => { 24 | peerConnection = await connectionClient.createConnection(options); 25 | window.peerConnection = peerConnection; 26 | }, () => { 27 | peerConnection.close(); 28 | }); 29 | } 30 | 31 | module.exports = createExample; 32 | -------------------------------------------------------------------------------- /lib/browser/startstopbutton.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function createStartStopButton(onStart, onStop) { 4 | const startButton = document.createElement('button'); 5 | startButton.innerText = 'Start'; 6 | document.body.appendChild(startButton); 7 | 8 | const stopButton = document.createElement('button'); 9 | stopButton.innerText = 'Stop'; 10 | stopButton.disabled = true; 11 | document.body.appendChild(stopButton); 12 | 13 | startButton.addEventListener('click', async () => { 14 | startButton.disabled = true; 15 | try { 16 | await onStart(); 17 | stopButton.disabled = false; 18 | } catch (error) { 19 | startButton.disabled = false; 20 | throw error; 21 | } 22 | }); 23 | 24 | stopButton.addEventListener('click', async () => { 25 | stopButton.disabled = true; 26 | try { 27 | await onStop(); 28 | startButton.disabled = false; 29 | } catch (error) { 30 | stopButton.disabled = false; 31 | throw error; 32 | } 33 | }); 34 | } 35 | 36 | module.exports = createStartStopButton; 37 | -------------------------------------------------------------------------------- /examples/sine-wave/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RTCAudioSourceSineWave = require('../../lib/server/webrtc/rtcaudiosourcesinewave'); 4 | 5 | function beforeOffer(peerConnection) { 6 | const source = new RTCAudioSourceSineWave(); 7 | const track = source.createTrack(); 8 | peerConnection.addTrack(track); 9 | 10 | const dataChannel = peerConnection.createDataChannel('frequency'); 11 | 12 | function onMessage({ data }) { 13 | const frequency = Number.parseFloat(data); 14 | source.setFrequency(frequency); 15 | } 16 | 17 | dataChannel.addEventListener('message', onMessage); 18 | 19 | // NOTE(mroberts): This is a hack so that we can get a callback when the 20 | // RTCPeerConnection is closed. In the future, we can subscribe to 21 | // "connectionstatechange" events. 22 | const { close } = peerConnection; 23 | peerConnection.close = function() { 24 | dataChannel.removeEventListener('message', onMessage); 25 | track.stop(); 26 | source.close(); 27 | return close.apply(this, arguments); 28 | }; 29 | } 30 | 31 | module.exports = { beforeOffer }; 32 | -------------------------------------------------------------------------------- /examples/pitch-detector/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { RTCAudioSink } = require('wrtc').nonstandard; 4 | 5 | const PitchDetector = require('../../lib/common/pitchdetector'); 6 | 7 | function beforeOffer(peerConnection) { 8 | const { track } = peerConnection.addTransceiver('audio').receiver; 9 | const sink = new RTCAudioSink(track); 10 | const pitchDetector = new PitchDetector(); 11 | 12 | const dataChannel = peerConnection.createDataChannel('frequency'); 13 | 14 | function onData(data) { 15 | const frequency = pitchDetector.onData(data); 16 | if (frequency && dataChannel.readyState === 'open') { 17 | dataChannel.send(JSON.stringify(frequency)); 18 | } 19 | } 20 | 21 | sink.ondata = onData; 22 | 23 | // NOTE(mroberts): This is a hack so that we can get a callback when the 24 | // RTCPeerConnection is closed. In the future, we can subscribe to 25 | // "connectionstatechange" events. 26 | const { close } = peerConnection; 27 | peerConnection.close = function() { 28 | sink.stop(); 29 | return close.apply(this, arguments); 30 | }; 31 | } 32 | 33 | module.exports = { beforeOffer }; 34 | -------------------------------------------------------------------------------- /examples/sine-wave-stereo/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RTCAudioSourceSineWave = require('../../lib/server/webrtc/rtcaudiosourcesinewave'); 4 | 5 | function beforeOffer(peerConnection) { 6 | const source = new RTCAudioSourceSineWave({ 7 | channelCount: 2, 8 | panning: 50 9 | }); 10 | const track = source.createTrack(); 11 | peerConnection.addTrack(track); 12 | 13 | const dataChannel = peerConnection.createDataChannel('panning'); 14 | 15 | function onMessage({ data }) { 16 | const panning = Number.parseFloat(data); 17 | source.setPanning(panning); 18 | } 19 | 20 | dataChannel.addEventListener('message', onMessage); 21 | 22 | // NOTE(mroberts): This is a hack so that we can get a callback when the 23 | // RTCPeerConnection is closed. In the future, we can subscribe to 24 | // "connectionstatechange" events. 25 | const { close } = peerConnection; 26 | peerConnection.close = function() { 27 | dataChannel.removeEventListener('message', onMessage); 28 | track.stop(); 29 | source.close(); 30 | return close.apply(this, arguments); 31 | }; 32 | } 33 | 34 | module.exports = { beforeOffer }; 35 | -------------------------------------------------------------------------------- /lib/browser/webaudio/webaudiooscillatornodesinewave.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { acquireAudioContext, releaseAudioContext } = require('./refcountedaudiocontext'); 4 | 5 | class WebAudioOscillatorNodeSineWave { 6 | constructor(options = {}) { 7 | options = { 8 | audioContext: null, 9 | frequency: 440, 10 | ...options 11 | }; 12 | 13 | const isUsingDefaultAudioContext = !options.audioContext; 14 | const audioContext = options.audioContext || acquireAudioContext(); 15 | 16 | const oscillatorNode = audioContext.createOscillator(); 17 | oscillatorNode.frequency.setValueAtTime(options.frequency, audioContext.currentTime); 18 | oscillatorNode.connect(audioContext.destination); 19 | oscillatorNode.start(); 20 | 21 | this.node = oscillatorNode; 22 | 23 | this.close = () => { 24 | oscillatorNode.stop(); 25 | if (isUsingDefaultAudioContext) { 26 | releaseAudioContext(); 27 | } 28 | }; 29 | 30 | this.setFrequency = frequency => { 31 | oscillatorNode.frequency.setValueAtTime(frequency, audioContext.currentTime); 32 | }; 33 | } 34 | } 35 | 36 | module.exports = WebAudioOscillatorNodeSineWave; 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-webrtc-examples", 3 | "version": "0.1.0", 4 | "description": "This project presents a few example applications using node-webrtc.", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "lint": "eslint index.js examples lib test", 9 | "start": "node index.js", 10 | "test": "npm run test:unit && npm run test:integration", 11 | "test:unit": "tape 'test/unit/**/*.js'", 12 | "test:integration": "tape 'test/integration/**/*.js'" 13 | }, 14 | "keywords": [ 15 | "Web", 16 | "Audio" 17 | ], 18 | "author": "Mark Andrus Roberts ", 19 | "license": "BSD-3-Clause", 20 | "dependencies": { 21 | "@ffmpeg-installer/ffmpeg": "^1.0.20", 22 | "Scope": "github:kevincennis/Scope", 23 | "body-parser": "^1.18.3", 24 | "browserify-middleware": "^8.1.1", 25 | "canvas": "^2.4.1", 26 | "color-space": "^1.16.0", 27 | "express": "^4.16.4", 28 | "fluent-ffmpeg": "^2.1.2", 29 | "fluent-ffmpeg-multistream": "^1.0.0", 30 | "node-fetch": "^2.3.0", 31 | "uuid": "^3.3.2", 32 | "wrtc": "^0.4.3" 33 | }, 34 | "devDependencies": { 35 | "eslint": "^5.15.1", 36 | "tape": "^4.10.0" 37 | } 38 | } -------------------------------------------------------------------------------- /lib/server/connections/webrtcconnectionmanager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ConnectionManager = require('./connectionmanager'); 4 | const WebRtcConnection = require('./webrtcconnection'); 5 | 6 | class WebRtcConnectionManager { 7 | constructor(options = {}) { 8 | options = { 9 | Connection: WebRtcConnection, 10 | ...options 11 | }; 12 | 13 | const connectionManager = new ConnectionManager(options); 14 | 15 | this.createConnection = async () => { 16 | const connection = connectionManager.createConnection(); 17 | await connection.doOffer(); 18 | return connection; 19 | }; 20 | 21 | this.getConnection = id => { 22 | return connectionManager.getConnection(id); 23 | }; 24 | 25 | this.getConnections = () => { 26 | return connectionManager.getConnections(); 27 | }; 28 | } 29 | 30 | toJSON() { 31 | return this.getConnections().map(connection => connection.toJSON()); 32 | } 33 | } 34 | 35 | WebRtcConnectionManager.create = function create(options) { 36 | return new WebRtcConnectionManager({ 37 | Connection: function(id) { 38 | return new WebRtcConnection(id, options); 39 | } 40 | }); 41 | }; 42 | 43 | module.exports = WebRtcConnectionManager; 44 | -------------------------------------------------------------------------------- /examples/viewer/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const createExample = require('../../lib/browser/example'); 4 | 5 | const description = 'View a broadcast. You should have already started the \ 6 | broadcast example. Although you can prototype such a system with node-webrtc, \ 7 | you should consider using an \ 8 | SFU.'; 9 | 10 | const remoteVideo = document.createElement('video'); 11 | remoteVideo.autoplay = true; 12 | 13 | async function beforeAnswer(peerConnection) { 14 | const remoteStream = new MediaStream(peerConnection.getReceivers().map(receiver => receiver.track)); 15 | remoteVideo.srcObject = remoteStream; 16 | 17 | // NOTE(mroberts): This is a hack so that we can get a callback when the 18 | // RTCPeerConnection is closed. In the future, we can subscribe to 19 | // "connectionstatechange" events. 20 | const { close } = peerConnection; 21 | peerConnection.close = function() { 22 | remoteVideo.srcObject = null; 23 | return close.apply(this, arguments); 24 | }; 25 | } 26 | 27 | createExample('viewer', description, { beforeAnswer }); 28 | 29 | const videos = document.createElement('div'); 30 | videos.className = 'grid'; 31 | videos.appendChild(remoteVideo); 32 | document.body.appendChild(videos); 33 | -------------------------------------------------------------------------------- /examples/record-audio-video-stream/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const createExample = require('../../lib/browser/example'); 4 | 5 | const description = 'Transcode and record audio and video into different video resolutions and then merge into single file.'; 6 | 7 | const localVideo = document.createElement('video'); 8 | localVideo.autoplay = true; 9 | localVideo.muted = true; 10 | 11 | async function beforeAnswer(peerConnection) { 12 | const localStream = await window.navigator.mediaDevices.getUserMedia({ 13 | audio: true, 14 | video: true 15 | }); 16 | 17 | localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream)); 18 | 19 | localVideo.srcObject = localStream; 20 | 21 | // NOTE(mroberts): This is a hack so that we can get a callback when the 22 | // RTCPeerConnection is closed. In the future, we can subscribe to 23 | // "connectionstatechange" events. 24 | const { close } = peerConnection; 25 | peerConnection.close = function() { 26 | localVideo.srcObject = null; 27 | 28 | localStream.getTracks().forEach(track => track.stop()); 29 | 30 | return close.apply(this, arguments); 31 | }; 32 | } 33 | 34 | createExample('record-audio-video-stream', description, { beforeAnswer }); 35 | 36 | const videos = document.createElement('div'); 37 | videos.className = 'grid'; 38 | videos.appendChild(localVideo); 39 | document.body.appendChild(videos); 40 | -------------------------------------------------------------------------------- /test/integration/client/index.js: -------------------------------------------------------------------------------- 1 | /* eslint no-process-env:0 */ 2 | 'use strict'; 3 | 4 | const bodyParser = require('body-parser'); 5 | const express = require('express'); 6 | const tape = require('tape'); 7 | 8 | const ConnectionClient = require('../../../lib/client'); 9 | const WebRtcConnectionManager = require('../../../lib/server/connections/webrtcconnectionmanager'); 10 | const connectionsApi = require('../../../lib/server/rest/connectionsapi'); 11 | 12 | tape('ConnectionsClient', t => { 13 | t.test('typical usage', t => { 14 | const app = express(); 15 | 16 | app.use(bodyParser.json()); 17 | 18 | const connectionManager = WebRtcConnectionManager.create({ 19 | beforeOffer(peerConnection) { 20 | peerConnection.createDataChannel('test'); 21 | }, 22 | timeToReconnected: 0 23 | }); 24 | 25 | connectionsApi(app, connectionManager); 26 | 27 | const server = app.listen(3000, async () => { 28 | const connectionClient = new ConnectionClient({ 29 | host: 'http://localhost:3000', 30 | prefix: '/v1' 31 | }); 32 | 33 | const peerConnection = await connectionClient.createConnection(); 34 | 35 | peerConnection.close(); 36 | 37 | connectionManager.getConnections().forEach(connection => connection.close()); 38 | 39 | server.close(); 40 | 41 | t.end(); 42 | }); 43 | }); 44 | 45 | t.end(); 46 | }); 47 | -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 38 | 39 | node-webrtc Example 40 | 41 |

node-webrtc examples

42 |