webrtc-scalable-broadcast
Version:
Scalable WebRTC peer-to-peer broadcasting demo.
319 lines (278 loc) • 12.5 kB
HTML
<title>WebRTC Scalable Broadcast using RTCMultiConnection</title>
<h1><a href="https://github.com/muaz-khan/WebRTC-Scalable-Broadcast">WebRTC Scalable Broadcast</a> using <a href="https://github.com/muaz-khan/RTCMultiConnection">RTCMultiConnection</a></h1>
<hr>
This module simply initializes socket.io and configures it in a way that single file can be shared/relayed over unlimited users without any <a href="https://www.webrtc-experiment.com/docs/RTP-usage.html">bandwidth/CPU usage issues</a>. Everything happens peer-to-peer!
<hr>
<input type="text" id="broadcast-id" placeholder="broadcast-id" value="room-xyz">
<button id="open-or-join">Open or Join Broadcast</button>
<hr>
<input type="file" disabled>
<div id="files-container"></div>
<style>
button, input, select {
font-family: Myriad, Arial, Verdana;
font-weight: normal;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
border-bottom-left-radius: 3px;
padding: 4px 12px;
text-decoration: none;
color: rgb(27, 26, 26);
display: inline-block;
box-shadow: rgb(255, 255, 255) 1px 1px 0px 0px inset;
text-shadow: none;
background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0.05, rgb(241, 241, 241)), to(rgb(230, 230, 230)));
font-size: 20px;
border: 1px solid red;
outline:none;
}
button:active, input:active, select:active, button:focus, input:focus, select:focus {
background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(5%, rgb(221, 221, 221)), to(rgb(250, 250, 250)));
border: 1px solid rgb(142, 142, 142);
}
button[disabled], iput[disabled], select[disabled] {
background: rgb(249, 249, 249);
border: 1px solid rgb(218, 207, 207);
color: rgb(197, 189, 189);
}
input, input:focus, input:active {
background: white;
}
</style>
<script src="/socket.io/socket.io.js"></script>
<script src="/RTCMultiConnection.js"></script>
<script>
var socket = io.connect();
// using single socket for RTCMultiConnection signaling
var onMessageCallbacks = {};
socket.on('message', function(data) {
if (data.sender == connection.userid) return;
if (onMessageCallbacks[data.channel]) {
onMessageCallbacks[data.channel](data.message);
};
});
// initializing RTCMultiConnection constructor.
function initRTCMultiConnection(userid) {
var connection = new RTCMultiConnection();
connection.body = connection.filesContainer = document.getElementById('files-container');
connection.channel = connection.sessionid = connection.userid = userid || connection.userid;
connection.sdpConstraints.mandatory = {
OfferToReceiveAudio: false,
OfferToReceiveVideo: false
};
// using socket.io for signaling
connection.openSignalingChannel = function(config) {
var channel = config.channel || this.channel;
onMessageCallbacks[channel] = config.onmessage;
if (config.onopen) setTimeout(config.onopen, 1000);
return {
send: function(message) {
socket.emit('message', {
sender: connection.userid,
channel: channel,
message: message
});
},
channel: channel
};
};
connection.onMediaError = function(error) {
alert(JSON.stringify(error));
};
return connection;
}
// this RTCMultiConnection object is used to connect with existing users
var connection = initRTCMultiConnection();
connection.getExternalIceServers = false;
connection.onopen = function(event) {
document.querySelector('h1').innerHTML = 'Remote user <b>' + event.userid + '</b> is connected.';
if(connection.isInitiator) {
document.querySelector('input[type=file]').disabled = false;
}
if (connection.isInitiator == false && !connection.broadcastingConnection) {
// "connection.broadcastingConnection" global-level object is used
// instead of using a closure object, i.e. "privateConnection"
// because sometimes out of browser-specific bugs, browser
// can emit "onaddstream" event even if remote user didn't attach any stream.
// such bugs happen often in chrome.
// "connection.broadcastingConnection" prevents multiple initializations.
// if current user is broadcast viewer
// he should create a separate RTCMultiConnection object as well.
// because node.js server can allot him other viewers for
// remote-stream-broadcasting.
connection.broadcastingConnection = initRTCMultiConnection(connection.userid);
connection.broadcastingConnection.onopen = function() {
document.querySelector('h1').innerHTML = 'Remote user <b>' + event.userid + '</b> is connected.';
// share old received files with new users!
if(connection.broadcastingConnection.isInitiator && connection.lastFile) {
setTimeout(function() {
connection.broadcastingConnection.send(connection.lastFile);
}, 2000);
}
};
connection.broadcastingConnection.body = connection.broadcastingConnection.filesContainer = document.getElementById('files-container');
FileProgressBarHandler.handle(connection.broadcastingConnection);
connection.broadcastingConnection.session = connection.session;
// forwarder should always use this!
connection.broadcastingConnection.sdpConstraints.mandatory = {
OfferToReceiveVideo: false,
OfferToReceiveAudio: false
};
connection.broadcastingConnection.open({
dontTransmit: true
});
}
};
// ask node.js server to look for a broadcast
// if broadcast is available, simply join it. i.e. "join-broadcaster" event should be emitted.
// if broadcast is absent, simply create it. i.e. "start-broadcasting" event should be fired.
document.getElementById('open-or-join').onclick = function() {
var broadcastid = document.getElementById('broadcast-id').value;
if (broadcastid.replace(/^\s+|\s+$/g, '').length <= 0) {
alert('Please enter broadcast-id');
document.getElementById('broadcast-id').focus();
return;
}
this.disabled = true;
connection.session = {
data: true,
oneway: true
};
socket.emit('join-broadcast', {
broadcastid: broadcastid,
userid: connection.userid,
typeOfStreams: connection.session
});
};
// this event is emitted when a broadcast is already created.
socket.on('join-broadcaster', function(broadcaster, typeOfStreams) {
connection.session = typeOfStreams;
connection.channel = connection.sessionid = broadcaster.userid;
connection.sdpConstraints.mandatory = {
OfferToReceiveVideo: !!connection.session.video,
OfferToReceiveAudio: !!connection.session.audio
};
connection.join({
sessionid: broadcaster.userid,
userid: broadcaster.userid,
extra: {},
session: connection.session
});
});
// this event is emitted when a broadcast is absent.
socket.on('start-broadcasting', function(typeOfStreams) {
// host i.e. sender should always use this!
connection.sdpConstraints.mandatory = {
OfferToReceiveVideo: false,
OfferToReceiveAudio: false
};
connection.session = typeOfStreams;
connection.open({
dontTransmit: true
});
if (connection.broadcastingConnection) {
// if new person is given the initiation/host/moderation control
connection.broadcastingConnection.close();
connection.broadcastingConnection = null;
}
});
window.onbeforeunload = function() {
// Firefox is weird!
document.getElementById('open-or-join').disabled = false;
document.querySelector('input[type=file]').disabled = true;
};
var FileProgressBarHandler = (function() {
function handle(connection) {
var progressHelper = {};
// www.RTCMultiConnection.org/docs/onFileStart/
connection.onFileStart = function(file) {
var div = document.createElement('div');
div.title = file.name;
div.innerHTML = '<label>0%</label> <progress></progress>';
if (file.remoteUserId) {
div.innerHTML += ' (Sharing with:' + file.remoteUserId + ')';
}
connection.filesContainer.insertBefore(div, connection.filesContainer.firstChild);
if (!file.remoteUserId) {
progressHelper[file.uuid] = {
div: div,
progress: div.querySelector('progress'),
label: div.querySelector('label')
};
progressHelper[file.uuid].progress.max = file.maxChunks;
return;
}
if (!progressHelper[file.uuid]) {
progressHelper[file.uuid] = {};
}
progressHelper[file.uuid][file.remoteUserId] = {
div: div,
progress: div.querySelector('progress'),
label: div.querySelector('label')
};
progressHelper[file.uuid][file.remoteUserId].progress.max = file.maxChunks;
};
// www.RTCMultiConnection.org/docs/onFileProgress/
connection.onFileProgress = function(chunk) {
var helper = progressHelper[chunk.uuid];
if (!helper) {
return;
}
if (chunk.remoteUserId) {
helper = progressHelper[chunk.uuid][chunk.remoteUserId];
if (!helper) {
return;
}
}
helper.progress.value = chunk.currentPosition || chunk.maxChunks || helper.progress.max;
updateLabel(helper.progress, helper.label);
};
// www.RTCMultiConnection.org/docs/onFileEnd/
connection.onFileEnd = function(file) {
var helper = progressHelper[file.uuid];
if (!helper) {
console.error('No such progress-helper element exists.', file);
return;
}
if (file.remoteUserId) {
helper = progressHelper[file.uuid][file.remoteUserId];
if (!helper) {
return;
}
}
var div = helper.div;
if (file.type.indexOf('image') != -1) {
div.innerHTML = '<a href="' + file.url + '" download="' + file.name + '">Download <strong style="color:red;">' + file.name + '</strong> </a><br /><img src="' + file.url + '" title="' + file.name + '" style="max-width: 80%;">';
} else if (file.type.indexOf('video/') != -1) {
div.innerHTML = '<a href="' + file.url + '" download="' + file.name + '">Download <strong style="color:red;">' + file.name + '</strong> </a><br /><video src="' + file.url + '" title="' + file.name + '" style="max-width: 80%;" controls></video>';
} else if (file.type.indexOf('audio/') != -1) {
div.innerHTML = '<a href="' + file.url + '" download="' + file.name + '">Download <strong style="color:red;">' + file.name + '</strong> </a><br /><audio src="' + file.url + '" title="' + file.name + '" style="max-width: 80%;" controls></audio>';
} else {
div.innerHTML = '<a href="' + file.url + '" download="' + file.name + '">Download <strong style="color:red;">' + file.name + '</strong> </a><br /><iframe src="' + file.url + '" title="' + file.name + '" style="width: 80%;border: 0;height: inherit;margin-top:1em;"></iframe>';
}
if(connection.broadcastingConnection && connection.broadcastingConnection.numberOfConnectedUsers > 0) {
connection.broadcastingConnection.send(file);
}
connection.lastFile = file;
};
function updateLabel(progress, label) {
if (progress.position === -1) {
return;
}
var position = +progress.position.toFixed(2).split('.')[1] || 100;
label.innerHTML = position + '%';
}
}
return {
handle: handle
};
})();
FileProgressBarHandler.handle(connection);
document.querySelector('input[type=file]').onchange = function() {
var file = this.files[0];
if(connection.isInitiator && connection.numberOfConnectedUsers > 0) {
connection.send(file);
}
};
</script>