Created
December 5, 2014 08:04
-
-
Save mganeko/0662e3c5f01247c81b88 to your computer and use it in GitHub Desktop.
WebRTC signaling over milkcocoa for multiple room / people
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>WebRTC over milkcocoa</title> | |
</head> | |
<body> | |
<button type="button" onclick="startVideo();">Start video</button> | |
<button type="button" onclick="stopVideo();">Stop video</button> | |
<button type="button" onclick="call();">Connect</button> | |
<button type="button" onclick="hangUp();">Hang Up</button> | |
<br /> | |
<div style="position: relative;"> | |
<video id="local-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"></video> | |
<!-- <video id="remote-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"></video> --> | |
<video id="webrtc-remote-video-0" autoplay style="position: absolute; top: 250px; left: 0px; width: 320px; height: 240px; border: 1px solid black; "></video> | |
<video id="webrtc-remote-video-1" autoplay style="position: absolute; top: 250px; left: 330px; width: 320px; height: 240px; border: 1px solid black; "></video> | |
<video id="webrtc-remote-video-2" autoplay style="position: absolute; top: 0px; left: 330px; width: 320px; height: 240px; border: 1px solid black; " ></video> | |
</div> | |
<script src="http://cdn.mlkcca.com/v0.2.2/milkcocoa.js"></script> | |
<script> | |
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; | |
window.URL = window.URL || window.webkitURL; | |
var localVideo = document.getElementById('local-video'); | |
var localStream = null; | |
var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':false, 'OfferToReceiveVideo':true }}; | |
// ---- multi people video & audio ---- | |
var videoElementsInUse = {}; | |
var videoElementsStandBy = {}; | |
pushVideoStandBy(getVideoForRemote(0)); | |
pushVideoStandBy(getVideoForRemote(1)); | |
pushVideoStandBy(getVideoForRemote(2)); | |
function getVideoForRemote(index) { | |
var elementID = 'webrtc-remote-video-' + index; | |
var element = document.getElementById(elementID); | |
return element; | |
} | |
// ---- video element management --- | |
function pushVideoStandBy(element) { | |
videoElementsStandBy[element.id] = element; | |
} | |
function popVideoStandBy() { | |
var element = null; | |
for (var id in videoElementsStandBy) { | |
element = videoElementsStandBy[id]; | |
delete videoElementsStandBy[id]; | |
return element; | |
} | |
return null; | |
} | |
function pushVideoInUse(id, element) { | |
videoElementsInUse[id] = element; | |
} | |
function popVideoInUse(id) { | |
element = videoElementsInUse[id]; | |
delete videoElementsInUse[id]; | |
return element; | |
} | |
function attachVideo(id, stream) { | |
console.log('try to attach video. id=' + id); | |
var videoElement = popVideoStandBy(); | |
if (videoElement) { | |
videoElement.src = window.URL.createObjectURL(stream); | |
console.log("videoElement.src=" + videoElement.src); | |
pushVideoInUse(id, videoElement); | |
videoElement.style.display = 'block'; | |
} | |
else { | |
console.error('--- no video element stand by.'); | |
} | |
} | |
function detachVideo(id) { | |
console.log('try to detach video. id=' + id); | |
var videoElement = popVideoInUse(id); | |
if (videoElement) { | |
videoElement.pause(); | |
videoElement.src = ""; | |
console.log("videoElement.src=" + videoElement.src); | |
pushVideoStandBy(videoElement); | |
} | |
else { | |
console.warn('warning --- no video element using with id=' + id); | |
} | |
} | |
function detachAllVideo() { | |
var element = null; | |
for (var id in videoElementsInUse) { | |
detachVideo(id); | |
} | |
} | |
function getFirstVideoInUse() { | |
var element = null; | |
for (var id in videoElementsInUse) { | |
element = videoElementsInUse[id]; | |
return element; | |
} | |
return null; | |
} | |
function getVideoCountInUse() { | |
var count = 0; | |
for (var id in videoElementsInUse) { | |
count++; | |
} | |
return count; | |
} | |
function isLocalStreamStarted() { | |
if (localStream) { | |
return true; | |
} | |
else { | |
return false; | |
} | |
} | |
// -------------- multi connections -------------------- | |
var MAX_CONNECTION_COUNT = 3; | |
var connections = {}; // Connection hash | |
function Connection() { // Connection Class | |
var self = this; | |
var id = ""; // milkcocoa.id of partner | |
var peerconnection = null; // RTCPeerConnection instance | |
} | |
function getConnection(id) { | |
var con = null; | |
con = connections[id]; | |
return con; | |
} | |
function addConnection(id, connection) { | |
connections[id] = connection; | |
} | |
function getConnectionCount() { | |
var count = 0; | |
for (var id in connections) { | |
count++; | |
} | |
console.log('getConnectionCount=' + count); | |
return count; | |
} | |
function isConnectPossible() { | |
if (getConnectionCount() < MAX_CONNECTION_COUNT) | |
return true; | |
else | |
return false; | |
} | |
function getConnectionIndex(id_to_lookup) { | |
var index = 0; | |
for (var id in connections) { | |
if (id == id_to_lookup) { | |
return index; | |
} | |
index++; | |
} | |
// not found | |
return -1; | |
} | |
function deleteConnection(id) { | |
delete connections[id]; | |
} | |
function stopAllConnections() { | |
for (var id in connections) { | |
var conn = connections[id]; | |
conn.peerconnection.close(); | |
conn.peerconnection = null; | |
delete connections[id]; | |
} | |
} | |
function stopConnection(id) { | |
var conn = connections[id]; | |
if(conn) { | |
console.log('stop and delete connection with id=' + id); | |
conn.peerconnection.close(); | |
conn.peerconnection = null; | |
delete connections[id]; | |
} | |
else { | |
console.log('try to stop connection, but not found id=' + id); | |
} | |
} | |
function isPeerStarted() { | |
if (getConnectionCount() > 0) { | |
return true; | |
} | |
else { | |
return false; | |
} | |
} | |
// ----- milkcocoa --- | |
var selfID = ''; | |
var room = getRoomName(); | |
var milkcocoa = new MilkCocoa("https://your-app-id.mlkcca.com/"); /* your-app-id must be io-xxxxxxx */ | |
var ds = milkcocoa.dataStore("signaling/" + room); | |
var socketReady = false; | |
milkcocoa.anonymous(function(err, user) { | |
if (user) { | |
selfID = user.id; | |
console.log('anonymous() id=' + selfID); | |
socketReady = true; | |
} | |
else { | |
console.error('milk anonymous() error'); | |
} | |
}); | |
// watch realtime message | |
ds.on("push", function(data) { | |
onMessage(data.value); | |
}); | |
// socket: accept connection request | |
function onMessage(evt) { | |
var id = evt.from; | |
var target = evt.sendto; | |
var conn = getConnection(id); | |
if (id === selfID) { | |
// skip message from myself | |
console.log('.... skip self message ...' + id); | |
return; | |
} | |
if( (target) && (target != '') && (target != selfID) ) { | |
// skip message to someone else | |
console.log('.... skip others message ...' + target); | |
return; | |
} | |
if (evt.type === 'call') { | |
console.log('--- got call ---'); | |
console.log(evt); | |
if (! isLocalStreamStarted()) { | |
return; | |
} | |
if (conn) { | |
return; // already connected | |
} | |
if (isConnectPossible()) { | |
ds.push({type: "response", sendto: id, from: selfID}); | |
} | |
else { | |
console.warn('max connections. so ignore call'); | |
} | |
return; | |
} | |
else if (evt.type === 'response') { | |
console.log('--- got response ---'); | |
console.log(evt); | |
sendOffer(id); | |
return; | |
} else if (evt.type === 'offer') { | |
console.log("Received offer, set offer, sending answer....") | |
onOffer(evt); | |
} else if (evt.type === 'answer' && isPeerStarted()) { // ** | |
console.log('Received answer, settinng answer SDP'); | |
onAnswer(evt); | |
} else if (evt.type === 'candidate' && isPeerStarted()) { // ** | |
console.log('Received ICE candidate...'); | |
onCandidate(evt); | |
} else if (evt.type === 'user dissconnected' && isPeerStarted()) { // ** | |
console.log("disconnected"); | |
detachVideo(id); // force detach video | |
stopConnection(id); | |
} | |
else if ( (evt.type === 'bye') && isPeerStarted() ) { | |
console.log('Received bye...'); | |
detachVideo(id); // force detach video | |
stopConnection(id); | |
} | |
} | |
function getRoomName() { // signaling_milkcocoa_multi.html?roomname | |
var url = document.location.href; | |
var args = url.split('?'); | |
if (args.length > 1) { | |
var room = args[1]; | |
if (room != "") { | |
return room; | |
} | |
} | |
return "_defaultroom"; | |
} | |
function onOffer(evt) { | |
console.log("Received offer...") | |
console.log(evt); | |
setOffer(evt); | |
sendAnswer(evt); | |
} | |
function onAnswer(evt) { | |
console.log("Received Answer...") | |
console.log(evt); | |
setAnswer(evt); | |
} | |
function onCandidate(evt) { | |
var id = evt.from; | |
var conn = getConnection(id); | |
if (! conn) { | |
console.error('peerConnection not exist!'); | |
return; | |
} | |
var candidate = new RTCIceCandidate({sdpMLineIndex:evt.sdpMLineIndex, sdpMid:evt.sdpMid, candidate:evt.candidate}); | |
console.log('Received Candidate... from=' + evt.from + ' to=' + evt.sendto) | |
console.log(candidate); | |
conn.peerconnection.addIceCandidate(candidate); | |
} | |
function sendSDP(sdp) { | |
//var text = JSON.stringify(sdp); | |
//console.log("---sending sdp text ---"); | |
//console.log(text); | |
ds.push(sdp); | |
} | |
function sendCandidate(candidate) { | |
//var text = JSON.stringify(candidate); | |
//console.log("---sending candidate text ---"); | |
//console.log(text); | |
ds.push(candidate); | |
} | |
// ---------------------- video handling ----------------------- | |
// start local video | |
function startVideo() { | |
navigator.getUserMedia({video: true, audio: true}, | |
function (stream) { // success | |
localStream = stream; | |
localVideo.src = window.URL.createObjectURL(stream); | |
localVideo.play(); | |
localVideo.volume = 0; | |
}, | |
function (error) { // error | |
console.error('An error occurred:'); | |
console.error(error); | |
return; | |
} | |
); | |
} | |
// stop local video | |
function stopVideo() { | |
localVideo.src = ""; | |
localStream.stop(); | |
} | |
// ---------------------- connection handling ----------------------- | |
function prepareNewConnection(id) { | |
var pc_config = {"iceServers":[]}; | |
var peer = null; | |
try { | |
peer = new webkitRTCPeerConnection(pc_config); | |
} catch (e) { | |
console.log("Failed to create PeerConnection, exception: " + e.message); | |
} | |
var conn = new Connection(); | |
conn.id = id; | |
conn.peerconnection = peer; | |
peer.id = id; | |
addConnection(id, conn); | |
// send any ice candidates to the other peer | |
peer.onicecandidate = function (evt) { | |
if (evt.candidate) { | |
console.log(evt.candidate); | |
sendCandidate({type: "candidate", | |
sendto: conn.id, from: selfID, | |
sdpMLineIndex: evt.candidate.sdpMLineIndex, | |
sdpMid: evt.candidate.sdpMid, | |
candidate: evt.candidate.candidate}); | |
} else { | |
console.log("empty event --- phase=" + evt.eventPhase); | |
} | |
}; | |
peer.oniceconnectionstatechange = function() { | |
console.log('ice connection status=' + peer.iceConnectionState + ' gahter=' + peer.iceGatheringState); | |
if ('completed' === peer.iceConnectionState) { | |
console.log("candidate complete"); | |
} | |
}; | |
peer.onsignalingstatechange = function() { | |
console.log('signaling status=' + peer.signalingState); | |
}; | |
console.log('Adding local stream...'); | |
peer.addStream(localStream); | |
peer.addEventListener("addstream", onRemoteStreamAdded, false); | |
peer.addEventListener("removestream", onRemoteStreamRemoved, false) | |
// when remote adds a stream, hand it on to the local video element | |
function onRemoteStreamAdded(event) { | |
console.log("Added remote stream"); | |
attachVideo(this.id, event.stream); | |
} | |
// when remote removes a stream, remove it from the local video element | |
function onRemoteStreamRemoved(event) { | |
console.log("Remove remote stream"); | |
detachVideo(this.id); | |
} | |
return conn; | |
} | |
function sendOffer(id) { | |
console.log('--- sending offer to:' + id); | |
var conn = getConnection(id); | |
if (!conn) { | |
conn = prepareNewConnection(id); | |
} | |
conn.peerconnection.createOffer(function (sessionDescription) { // in case of success | |
conn.peerconnection.setLocalDescription(sessionDescription); | |
sessionDescription.sendto = id; | |
sessionDescription.from = selfID; | |
sendSDP(sessionDescription); | |
}, function () { // in case of error | |
console.log("Create Offer failed"); | |
}, mediaConstraints); | |
} | |
function setOffer(evt) { | |
var id = evt.from; | |
var conn = getConnection(id); | |
if (! conn) { | |
conn = prepareNewConnection(id); | |
conn.peerconnection.setRemoteDescription(new RTCSessionDescription(evt)); | |
} | |
else { | |
console.error('peerConnection alreay exist!'); | |
} | |
} | |
function sendAnswer(evt) { | |
console.log('-- sending Answer. Creating remote session description... to:' + evt.from); | |
var id = evt.from; | |
var conn = getConnection(id); | |
if (! conn) { | |
console.error('peerConnection not exist!'); | |
return | |
} | |
conn.peerconnection.createAnswer(function (sessionDescription) { | |
// in case of success | |
conn.peerconnection.setLocalDescription(sessionDescription); | |
sessionDescription.sendto = id; | |
sessionDescription.from = selfID; | |
sendSDP(sessionDescription); | |
}, function () { // in case of error | |
console.log("Create Answer failed"); | |
}, mediaConstraints); | |
} | |
function setAnswer(evt) { | |
var id = evt.from; | |
var conn = getConnection(id); | |
if (! conn) { | |
console.error('peerConnection not exist!'); | |
return | |
} | |
conn.peerconnection.setRemoteDescription(new RTCSessionDescription(evt)); | |
} | |
// call others before connecting peer | |
function call() { | |
if (! isLocalStreamStarted()) { | |
alert("Local stream not running yet. Please [Start Video] or [Start Screen]."); | |
return; | |
} | |
if (! socketReady) { | |
alert("Socket is not connected to server. Please reload and try again."); | |
return; | |
} | |
// call others, in same room | |
console.log("---call others in same room, befeore offer--"); | |
ds.push({type: "call", from: selfID}); | |
console.log({type: "call", from: selfID}); | |
} | |
// stop the connection upon user request | |
function hangUp() { | |
console.log("Hang up."); | |
ds.push({type: "bye", from: selfID}); | |
detachAllVideo(); | |
stopAllConnections(); | |
} | |
</script> | |
</body> | |
</html> | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment