mirror of
https://github.com/moparisthebest/kaiwa
synced 2024-11-25 02:32:26 -05:00
658 lines
18 KiB
JavaScript
658 lines
18 KiB
JavaScript
;(function () {
|
|
|
|
var logger = {
|
|
log: function (){},
|
|
warn: function (){},
|
|
error: function (){}
|
|
};
|
|
|
|
// normalize environment
|
|
var RTCPeerConnection = null,
|
|
getUserMedia = null,
|
|
attachMediaStream = null,
|
|
reattachMediaStream = null,
|
|
browser = null,
|
|
screenSharingSupport = false;
|
|
webRTCSupport = true;
|
|
|
|
if (navigator.mozGetUserMedia) {
|
|
logger.log("This appears to be Firefox");
|
|
|
|
browser = "firefox";
|
|
|
|
// The RTCPeerConnection object.
|
|
RTCPeerConnection = mozRTCPeerConnection;
|
|
|
|
// The RTCSessionDescription object.
|
|
RTCSessionDescription = mozRTCSessionDescription;
|
|
|
|
// The RTCIceCandidate object.
|
|
RTCIceCandidate = mozRTCIceCandidate;
|
|
|
|
// Get UserMedia (only difference is the prefix).
|
|
// Code from Adam Barth.
|
|
getUserMedia = navigator.mozGetUserMedia.bind(navigator);
|
|
|
|
// Attach a media stream to an element.
|
|
attachMediaStream = function(element, stream) {
|
|
element.mozSrcObject = stream;
|
|
element.play();
|
|
};
|
|
|
|
reattachMediaStream = function(to, from) {
|
|
to.mozSrcObject = from.mozSrcObject;
|
|
to.play();
|
|
};
|
|
|
|
// Fake get{Video,Audio}Tracks
|
|
MediaStream.prototype.getVideoTracks = function() {
|
|
return [];
|
|
};
|
|
|
|
MediaStream.prototype.getAudioTracks = function() {
|
|
return [];
|
|
};
|
|
} else if (navigator.webkitGetUserMedia) {
|
|
browser = "chrome";
|
|
|
|
screenSharingSupport = navigator.userAgent.match('Chrome') && parseInt(navigator.userAgent.match(/Chrome\/(.*) /)[1]) >= 26
|
|
|
|
// The RTCPeerConnection object.
|
|
RTCPeerConnection = webkitRTCPeerConnection;
|
|
|
|
// Get UserMedia (only difference is the prefix).
|
|
// Code from Adam Barth.
|
|
getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
|
|
|
|
// Attach a media stream to an element.
|
|
attachMediaStream = function(element, stream) {
|
|
element.autoplay = true;
|
|
element.src = webkitURL.createObjectURL(stream);
|
|
};
|
|
|
|
reattachMediaStream = function(to, from) {
|
|
to.src = from.src;
|
|
};
|
|
|
|
// The representation of tracks in a stream is changed in M26.
|
|
// Unify them for earlier Chrome versions in the coexisting period.
|
|
if (!webkitMediaStream.prototype.getVideoTracks) {
|
|
webkitMediaStream.prototype.getVideoTracks = function() {
|
|
return this.videoTracks;
|
|
};
|
|
webkitMediaStream.prototype.getAudioTracks = function() {
|
|
return this.audioTracks;
|
|
};
|
|
}
|
|
|
|
// New syntax of getXXXStreams method in M26.
|
|
if (!webkitRTCPeerConnection.prototype.getLocalStreams) {
|
|
webkitRTCPeerConnection.prototype.getLocalStreams = function() {
|
|
return this.localStreams;
|
|
};
|
|
webkitRTCPeerConnection.prototype.getRemoteStreams = function() {
|
|
return this.remoteStreams;
|
|
};
|
|
}
|
|
} else {
|
|
webRTCSupport = false;
|
|
throw new Error("Browser does not appear to be WebRTC-capable");
|
|
}
|
|
|
|
|
|
// emitter that we use as a base
|
|
function WildEmitter() {
|
|
this.callbacks = {};
|
|
}
|
|
|
|
// Listen on the given `event` with `fn`. Store a group name if present.
|
|
WildEmitter.prototype.on = function (event, groupName, fn) {
|
|
var hasGroup = (arguments.length === 3),
|
|
group = hasGroup ? arguments[1] : undefined,
|
|
func = hasGroup ? arguments[2] : arguments[1];
|
|
func._groupName = group;
|
|
(this.callbacks[event] = this.callbacks[event] || []).push(func);
|
|
return this;
|
|
};
|
|
|
|
// Adds an `event` listener that will be invoked a single
|
|
// time then automatically removed.
|
|
WildEmitter.prototype.once = function (event, fn) {
|
|
var self = this;
|
|
function on() {
|
|
self.off(event, on);
|
|
fn.apply(this, arguments);
|
|
}
|
|
this.on(event, on);
|
|
return this;
|
|
};
|
|
|
|
// Unbinds an entire group
|
|
WildEmitter.prototype.releaseGroup = function (groupName) {
|
|
var item, i, len, handlers;
|
|
for (item in this.callbacks) {
|
|
handlers = this.callbacks[item];
|
|
for (i = 0, len = handlers.length; i < len; i++) {
|
|
if (handlers[i]._groupName === groupName) {
|
|
handlers.splice(i, 1);
|
|
i--;
|
|
len--;
|
|
}
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
// Remove the given callback for `event` or all
|
|
// registered callbacks.
|
|
WildEmitter.prototype.off = function (event, fn) {
|
|
var callbacks = this.callbacks[event],
|
|
i;
|
|
|
|
if (!callbacks) return this;
|
|
|
|
// remove all handlers
|
|
if (arguments.length === 1) {
|
|
delete this.callbacks[event];
|
|
return this;
|
|
}
|
|
|
|
// remove specific handler
|
|
i = callbacks.indexOf(fn);
|
|
callbacks.splice(i, 1);
|
|
return this;
|
|
};
|
|
|
|
// Emit `event` with the given args.
|
|
// also calls any `*` handlers
|
|
WildEmitter.prototype.emit = function (event) {
|
|
var args = [].slice.call(arguments, 1),
|
|
callbacks = this.callbacks[event],
|
|
specialCallbacks = this.getWildcardCallbacks(event),
|
|
i,
|
|
len,
|
|
item;
|
|
|
|
if (callbacks) {
|
|
for (i = 0, len = callbacks.length; i < len; ++i) {
|
|
callbacks[i].apply(this, args);
|
|
}
|
|
}
|
|
|
|
if (specialCallbacks) {
|
|
for (i = 0, len = specialCallbacks.length; i < len; ++i) {
|
|
specialCallbacks[i].apply(this, [event].concat(args));
|
|
}
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
// Helper for for finding special wildcard event handlers that match the event
|
|
WildEmitter.prototype.getWildcardCallbacks = function (eventName) {
|
|
var item,
|
|
split,
|
|
result = [];
|
|
|
|
for (item in this.callbacks) {
|
|
split = item.split('*');
|
|
if (item === '*' || (split.length === 2 && eventName.slice(0, split[1].length) === split[1])) {
|
|
result = result.concat(this.callbacks[item]);
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
|
|
function WebRTC(opts) {
|
|
var self = this,
|
|
options = opts || {},
|
|
config = this.config = {
|
|
url: 'http://signaling.simplewebrtc.com:8888',
|
|
log: false,
|
|
localVideoEl: '',
|
|
remoteVideosEl: '',
|
|
autoRequestMedia: false,
|
|
// makes the entire PC config overridable
|
|
peerConnectionConfig: {
|
|
iceServers: browser == 'firefox' ? [{"url":"stun:124.124.124.2"}] : [{"url": "stun:stun.l.google.com:19302"}]
|
|
},
|
|
peerConnectionContraints: {
|
|
optional: [{"DtlsSrtpKeyAgreement": true}]
|
|
},
|
|
media: {
|
|
audio:true,
|
|
video: {
|
|
mandatory: {},
|
|
optional: []
|
|
}
|
|
}
|
|
},
|
|
item,
|
|
connection;
|
|
|
|
// check for support
|
|
if (!webRTCSupport) {
|
|
console.error('Your browser doesn\'t seem to support WebRTC');
|
|
}
|
|
|
|
// expose screensharing check
|
|
this.screenSharingSupport = screenSharingSupport;
|
|
|
|
// set options
|
|
for (item in options) {
|
|
this.config[item] = options[item];
|
|
}
|
|
|
|
// log if configured to
|
|
if (this.config.log) logger = console;
|
|
|
|
// where we'll store our peer connections
|
|
this.peers = [];
|
|
|
|
// our socket.io connection
|
|
connection = this.connection = io.connect(this.config.url);
|
|
|
|
connection.on('connect', function () {
|
|
self.emit('ready', connection.socket.sessionid);
|
|
self.sessionReady = true;
|
|
self.testReadiness();
|
|
});
|
|
|
|
connection.on('message', function (message) {
|
|
var peers = self.getPeers(message.from, message.roomType),
|
|
peer;
|
|
|
|
if (message.type === 'offer') {
|
|
peer = self.createPeer({
|
|
id: message.from,
|
|
type: message.roomType,
|
|
sharemyscreen: message.roomType === 'screen' && !message.broadcaster
|
|
});
|
|
peer.handleMessage(message);
|
|
} else if (peers.length) {
|
|
peers.forEach(function (peer) {
|
|
peer.handleMessage(message);
|
|
});
|
|
}
|
|
});
|
|
|
|
connection.on('remove', function (room) {
|
|
if (room.id !== self.connection.socket.sessionid) {
|
|
self.removeForPeerSession(room.id, room.type);
|
|
}
|
|
});
|
|
|
|
WildEmitter.call(this);
|
|
|
|
// log events
|
|
this.on('*', function (event, val1, val2) {
|
|
logger.log('event:', event, val1, val2);
|
|
});
|
|
|
|
// auto request if configured
|
|
if (this.config.autoRequestMedia) this.startLocalVideo();
|
|
}
|
|
|
|
WebRTC.prototype = Object.create(WildEmitter.prototype, {
|
|
constructor: {
|
|
value: WebRTC
|
|
}
|
|
});
|
|
|
|
WebRTC.prototype.getEl = function (idOrEl) {
|
|
if (typeof idOrEl == 'string') {
|
|
return document.getElementById(idOrEl);
|
|
} else {
|
|
return idOrEl;
|
|
}
|
|
};
|
|
|
|
// this accepts either element ID or element
|
|
// and either the video tag itself or a container
|
|
// that will be used to put the video tag into.
|
|
WebRTC.prototype.getLocalVideoContainer = function () {
|
|
var el = this.getEl(this.config.localVideoEl);
|
|
if (el && el.tagName === 'VIDEO') {
|
|
return el;
|
|
} else {
|
|
var video = document.createElement('video');
|
|
el.appendChild(video);
|
|
return video;
|
|
}
|
|
};
|
|
|
|
WebRTC.prototype.getRemoteVideoContainer = function () {
|
|
return this.getEl(this.config.remoteVideosEl);
|
|
};
|
|
|
|
WebRTC.prototype.createPeer = function (opts) {
|
|
var peer;
|
|
opts.parent = this;
|
|
peer = new Peer(opts);
|
|
this.peers.push(peer);
|
|
return peer;
|
|
};
|
|
|
|
WebRTC.prototype.createRoom = function (name, cb) {
|
|
if (arguments.length === 2) {
|
|
this.connection.emit('create', name, cb);
|
|
} else {
|
|
this.connection.emit('create', name);
|
|
}
|
|
};
|
|
|
|
WebRTC.prototype.joinRoom = function (name) {
|
|
var self = this;
|
|
this.roomName = name;
|
|
this.connection.emit('join', name, function (roomDescription) {
|
|
var id,
|
|
client,
|
|
type,
|
|
peer;
|
|
for (id in roomDescription) {
|
|
client = roomDescription[id];
|
|
for (type in client) {
|
|
if (client[type]) {
|
|
peer = self.createPeer({
|
|
id: id,
|
|
type: type
|
|
});
|
|
peer.start();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
WebRTC.prototype.leaveRoom = function () {
|
|
if (this.roomName) {
|
|
this.connection.emit('leave', this.roomName);
|
|
this.peers.forEach(function (peer) {
|
|
peer.end();
|
|
});
|
|
}
|
|
};
|
|
|
|
WebRTC.prototype.testReadiness = function () {
|
|
var self = this;
|
|
if (this.localStream && this.sessionReady) {
|
|
// This timeout is a workaround for the strange no-audio bug
|
|
// as described here: https://code.google.com/p/webrtc/issues/detail?id=1525
|
|
// remove timeout when this is fixed.
|
|
setTimeout(function () {
|
|
self.emit('readyToCall', self.connection.socket.sessionid);
|
|
}, 1000);
|
|
}
|
|
};
|
|
|
|
WebRTC.prototype.startLocalVideo = function (element) {
|
|
var self = this;
|
|
getUserMedia(this.config.media, function (stream) {
|
|
attachMediaStream(element || self.getLocalVideoContainer(), stream);
|
|
self.localStream = stream;
|
|
self.testReadiness();
|
|
}, function () {
|
|
throw new Error('Failed to get access to local media.');
|
|
});
|
|
};
|
|
|
|
// Audio controls
|
|
WebRTC.prototype.mute = function () {
|
|
this._audioEnabled(false);
|
|
this.emit('audioOff');
|
|
};
|
|
WebRTC.prototype.unmute = function () {
|
|
this._audioEnabled(true);
|
|
this.emit('audioOn');
|
|
};
|
|
|
|
// Video controls
|
|
WebRTC.prototype.pauseVideo = function () {
|
|
this._videoEnabled(false);
|
|
this.emit('videoOff');
|
|
};
|
|
WebRTC.prototype.resumeVideo = function () {
|
|
this._videoEnabled(true);
|
|
this.emit('videoOn');
|
|
};
|
|
|
|
// Combined controls
|
|
WebRTC.prototype.pause = function () {
|
|
this.mute();
|
|
this.pauseVideo();
|
|
};
|
|
WebRTC.prototype.resume = function () {
|
|
this.unmute();
|
|
this.resumeVideo();
|
|
};
|
|
|
|
// Internal methods for enabling/disabling audio/video
|
|
WebRTC.prototype._audioEnabled = function (bool) {
|
|
this.localStream.getAudioTracks().forEach(function (track) {
|
|
track.enabled = !!bool;
|
|
});
|
|
};
|
|
WebRTC.prototype._videoEnabled = function (bool) {
|
|
this.localStream.getVideoTracks().forEach(function (track) {
|
|
track.enabled = !!bool;
|
|
});
|
|
};
|
|
|
|
WebRTC.prototype.shareScreen = function () {
|
|
var self = this,
|
|
peer;
|
|
if (screenSharingSupport) {
|
|
getUserMedia({
|
|
video: {
|
|
mandatory: {
|
|
chromeMediaSource: 'screen'
|
|
}
|
|
}
|
|
}, function (stream) {
|
|
var item,
|
|
el = document.createElement('video'),
|
|
container = self.getRemoteVideoContainer();
|
|
|
|
self.localScreen = stream;
|
|
|
|
el.id = 'localScreen';
|
|
attachMediaStream(el, stream);
|
|
if (container) {
|
|
container.appendChild(el);
|
|
}
|
|
self.emit('videoAdded', el);
|
|
self.connection.emit('shareScreen');
|
|
self.peers.forEach(function (existingPeer) {
|
|
var peer;
|
|
if (existingPeer.type === 'video') {
|
|
peer = self.createPeer({
|
|
id: existingPeer.id,
|
|
type: 'screen',
|
|
sharemyscreen: true
|
|
});
|
|
peer.start();
|
|
}
|
|
});
|
|
}, function () {
|
|
throw new Error('Failed to access to screen media.');
|
|
});
|
|
}
|
|
};
|
|
|
|
WebRTC.prototype.stopScreenShare = function () {
|
|
this.connection.emit('unshareScreen');
|
|
var videoEl = document.getElementById('localScreen'),
|
|
container = this.getRemoteVideoContainer(),
|
|
stream = this.localScreen;
|
|
|
|
if (container && videoEl) {
|
|
container.removeChild(videoEl);
|
|
}
|
|
this.localScreen.stop();
|
|
this.peers.forEach(function (peer) {
|
|
if (peer.broadcaster) {
|
|
peer.end();
|
|
// a hack to emit the event the removes the video
|
|
// element that we want
|
|
peer.emit('videoRemoved', videoEl);
|
|
}
|
|
});
|
|
delete this.localScreen;
|
|
};
|
|
|
|
WebRTC.prototype.removeForPeerSession = function (id, type) {
|
|
this.getPeers(id, type).forEach(function (peer) {
|
|
peer.end();
|
|
});
|
|
};
|
|
|
|
// fetches all Peer objects by session id and/or type
|
|
WebRTC.prototype.getPeers = function (sessionId, type) {
|
|
return this.peers.filter(function (peer) {
|
|
return (!sessionId || peer.id === sessionId) && (!type || peer.type === type);
|
|
});
|
|
};
|
|
|
|
|
|
function Peer(options) {
|
|
var self = this;
|
|
|
|
this.id = options.id;
|
|
this.parent = options.parent;
|
|
this.type = options.type || 'video';
|
|
this.oneway = options.oneway || false;
|
|
this.sharemyscreen = options.sharemyscreen || false;
|
|
this.stream = options.stream;
|
|
// Create an RTCPeerConnection via the polyfill
|
|
this.pc = new RTCPeerConnection(this.parent.config.peerConnectionConfig, this.parent.config.peerConnectionContraints);
|
|
this.pc.onicecandidate = this.onIceCandidate.bind(this);
|
|
if (options.type === 'screen') {
|
|
if (this.parent.localScreen && this.sharemyscreen) {
|
|
logger.log('adding local screen stream to peer connection')
|
|
this.pc.addStream(this.parent.localScreen);
|
|
this.broadcaster = this.parent.connection.socket.sessionid;
|
|
}
|
|
} else {
|
|
this.pc.addStream(this.parent.localStream);
|
|
}
|
|
this.pc.onaddstream = this.handleRemoteStreamAdded.bind(this);
|
|
this.pc.onremovestream = this.handleStreamRemoved.bind(this);
|
|
// for re-use
|
|
this.mediaConstraints = {
|
|
mandatory: {
|
|
OfferToReceiveAudio: true,
|
|
OfferToReceiveVideo: true
|
|
}
|
|
};
|
|
WildEmitter.call(this);
|
|
|
|
// proxy events to parent
|
|
this.on('*', function (name, value) {
|
|
self.parent.emit(name, value, self);
|
|
});
|
|
}
|
|
|
|
Peer.prototype = Object.create(WildEmitter.prototype, {
|
|
constructor: {
|
|
value: Peer
|
|
}
|
|
});
|
|
|
|
Peer.prototype.handleMessage = function (message) {
|
|
if (message.type === 'offer') {
|
|
logger.log('setting remote description');
|
|
this.pc.setRemoteDescription(new RTCSessionDescription(message.payload));
|
|
this.answer();
|
|
} else if (message.type === 'answer') {
|
|
logger.log('setting answer');
|
|
this.pc.setRemoteDescription(new RTCSessionDescription(message.payload));
|
|
} else if (message.type === 'candidate') {
|
|
var candidate = new RTCIceCandidate({
|
|
sdpMLineIndex: message.payload.label,
|
|
candidate: message.payload.candidate
|
|
});
|
|
this.pc.addIceCandidate(candidate);
|
|
}
|
|
};
|
|
|
|
Peer.prototype.send = function (type, payload) {
|
|
this.parent.connection.emit('message', {
|
|
to: this.id,
|
|
broadcaster: this.broadcaster,
|
|
roomType: this.type,
|
|
type: type,
|
|
payload: payload
|
|
});
|
|
};
|
|
|
|
Peer.prototype.onIceCandidate = function (event) {
|
|
if (this.closed) return;
|
|
if (event.candidate) {
|
|
this.send('candidate', {
|
|
label: event.candidate.sdpMLineIndex,
|
|
id: event.candidate.sdpMid,
|
|
candidate: event.candidate.candidate
|
|
});
|
|
} else {
|
|
logger.log("End of candidates.");
|
|
}
|
|
};
|
|
|
|
Peer.prototype.start = function () {
|
|
var self = this;
|
|
this.pc.createOffer(function (sessionDescription) {
|
|
logger.log('setting local description');
|
|
self.pc.setLocalDescription(sessionDescription);
|
|
logger.log('sending offer', sessionDescription);
|
|
self.send('offer', sessionDescription);
|
|
}, null, this.mediaConstraints);
|
|
};
|
|
|
|
Peer.prototype.end = function () {
|
|
this.pc.close();
|
|
this.handleStreamRemoved();
|
|
};
|
|
|
|
Peer.prototype.answer = function () {
|
|
var self = this;
|
|
logger.log('answer called');
|
|
this.pc.createAnswer(function (sessionDescription) {
|
|
logger.log('setting local description');
|
|
self.pc.setLocalDescription(sessionDescription);
|
|
logger.log('sending answer', sessionDescription);
|
|
self.send('answer', sessionDescription);
|
|
}, null, this.mediaConstraints);
|
|
};
|
|
|
|
Peer.prototype.handleRemoteStreamAdded = function (event) {
|
|
var stream = this.stream = event.stream,
|
|
el = document.createElement('video'),
|
|
container = this.parent.getRemoteVideoContainer();
|
|
|
|
el.id = this.getDomId();
|
|
attachMediaStream(el, stream);
|
|
if (container) container.appendChild(el);
|
|
this.emit('videoAdded', el);
|
|
};
|
|
|
|
Peer.prototype.handleStreamRemoved = function () {
|
|
var video = document.getElementById(this.getDomId()),
|
|
container = this.parent.getRemoteVideoContainer();
|
|
if (video && container) {
|
|
container.removeChild(video);
|
|
this.emit('videoRemoved', video);
|
|
}
|
|
this.parent.peers.splice(this.parent.peers.indexOf(this), 1);
|
|
this.closed = true;
|
|
};
|
|
|
|
Peer.prototype.getDomId = function () {
|
|
return [this.id, this.type, this.broadcaster ? 'broadcasting' : 'incoming'].join('_');
|
|
};
|
|
|
|
// expose WebRTC
|
|
window.WebRTC = WebRTC;
|
|
|
|
}());
|