123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547 |
- //CLIENT
- // Fallbacks for vendor-specific variables until the spec is finalized.
- var PeerConnection = (window.PeerConnection || window.webkitPeerConnection00 || window.webkitRTCPeerConnection || window.mozRTCPeerConnection);
- var URL = (window.URL || window.webkitURL || window.msURL || window.oURL);
- var getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);
- var nativeRTCIceCandidate = (window.mozRTCIceCandidate || window.RTCIceCandidate);
- var nativeRTCSessionDescription = (window.mozRTCSessionDescription || window.RTCSessionDescription); // order is very important: "RTCSessionDescription" defined in Nighly but useless
- var sdpConstraints = {
- 'mandatory': {
- 'OfferToReceiveAudio': true,
- 'OfferToReceiveVideo': true
- }
- };
- if (navigator.webkitGetUserMedia) {
- 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;
- };
- }
- }
- (function() {
- var rtc;
- if ('undefined' === typeof module) {
- rtc = this.rtc = {};
- } else {
- rtc = module.exports = {};
- }
- // Holds a connection to the server.
- rtc._socket = null;
- // Holds identity for the client
- rtc._me = null;
- // Holds callbacks for certain events.
- rtc._events = {};
- rtc.on = function(eventName, callback) {
- rtc._events[eventName] = rtc._events[eventName] || [];
- rtc._events[eventName].push(callback);
- };
- rtc.fire = function(eventName, _) {
- var events = rtc._events[eventName];
- var args = Array.prototype.slice.call(arguments, 1);
- if (!events) {
- return;
- }
- for (var i = 0, len = events.length; i < len; i++) {
- events[i].apply(null, args);
- }
- };
- // Holds the STUN/ICE server to use for PeerConnections.
- rtc.SERVER = function() {
- if (navigator.mozGetUserMedia) {
- return {
- "iceServers": [{
- "url": "stun:23.21.150.121"
- }]
- };
- }
- return {
- "iceServers": [{
- "url": "stun:stun.l.google.com:19302"
- }]
- };
- };
- // Reference to the lone PeerConnection instance.
- rtc.peerConnections = {};
- // Array of known peer socket ids
- rtc.connections = [];
- // Stream-related variables.
- rtc.streams = [];
- rtc.numStreams = 0;
- rtc.initializedStreams = 0;
- // Reference to the data channels
- rtc.dataChannels = {};
- // PeerConnection datachannel configuration
- rtc.dataChannelConfig = {
- "optional": [{
- "RtpDataChannels": true
- }, {
- "DtlsSrtpKeyAgreement": true
- }]
- };
- rtc.pc_constraints = {
- "optional": [{
- "DtlsSrtpKeyAgreement": true
- }]
- };
- // check whether data channel is supported.
- rtc.checkDataChannelSupport = function() {
- try {
- // raises exception if createDataChannel is not supported
- var pc = new PeerConnection(rtc.SERVER(), rtc.dataChannelConfig);
- var channel = pc.createDataChannel('supportCheck', {
- reliable: false
- });
- channel.close();
- return true;
- } catch (e) {
- return false;
- }
- };
- rtc.dataChannelSupport = rtc.checkDataChannelSupport();
- /**
- * Connects to the websocket server.
- */
- rtc.connect = function(server, room) {
- room = room || ""; // by default, join a room called the blank string
- rtc._socket = new WebSocket(server);
- rtc._socket.onopen = function() {
- rtc._socket.send(JSON.stringify({
- "eventName": "join_room",
- "data": {
- "room": room
- }
- }));
- rtc._socket.onmessage = function(msg) {
- var json = JSON.parse(msg.data);
- rtc.fire(json.eventName, json.data);
- };
- rtc._socket.onerror = function(err) {
- console.error('onerror');
- console.error(err);
- };
- rtc._socket.onclose = function(data) {
- rtc.fire('disconnect stream', rtc._socket.id);
- delete rtc.peerConnections[rtc._socket.id];
- };
- rtc.on('get_peers', function(data) {
- rtc.connections = data.connections;
- rtc._me = data.you;
- // fire connections event and pass peers
- rtc.fire('connections', rtc.connections);
- });
- rtc.on('receive_ice_candidate', function(data) {
- var candidate = new nativeRTCIceCandidate(data);
- rtc.peerConnections[data.socketId].addIceCandidate(candidate);
- rtc.fire('receive ice candidate', candidate);
- });
- rtc.on('new_peer_connected', function(data) {
- rtc.connections.push(data.socketId);
- var pc = rtc.createPeerConnection(data.socketId);
- for (var i = 0; i < rtc.streams.length; i++) {
- var stream = rtc.streams[i];
- pc.addStream(stream);
- }
- });
- rtc.on('remove_peer_connected', function(data) {
- rtc.fire('disconnect stream', data.socketId);
- delete rtc.peerConnections[data.socketId];
- });
- rtc.on('receive_offer', function(data) {
- rtc.receiveOffer(data.socketId, data.sdp);
- rtc.fire('receive offer', data);
- });
- rtc.on('receive_answer', function(data) {
- rtc.receiveAnswer(data.socketId, data.sdp);
- rtc.fire('receive answer', data);
- });
- rtc.fire('connect');
- };
- };
- rtc.sendOffers = function() {
- for (var i = 0, len = rtc.connections.length; i < len; i++) {
- var socketId = rtc.connections[i];
- rtc.sendOffer(socketId);
- }
- };
- rtc.onClose = function(data) {
- rtc.on('close_stream', function() {
- rtc.fire('close_stream', data);
- });
- };
- rtc.createPeerConnections = function() {
- for (var i = 0; i < rtc.connections.length; i++) {
- rtc.createPeerConnection(rtc.connections[i]);
- }
- };
- rtc.createPeerConnection = function(id) {
- var config = rtc.pc_constraints;
- if (rtc.dataChannelSupport) config = rtc.dataChannelConfig;
- var pc = rtc.peerConnections[id] = new PeerConnection(rtc.SERVER(), config);
- pc.onicecandidate = function(event) {
- if (event.candidate) {
- rtc._socket.send(JSON.stringify({
- "eventName": "send_ice_candidate",
- "data": {
- "label": event.candidate.sdpMLineIndex,
- "candidate": event.candidate.candidate,
- "socketId": id
- }
- }));
- }
- rtc.fire('ice candidate', event.candidate);
- };
- pc.onopen = function() {
- // TODO: Finalize this API
- rtc.fire('peer connection opened');
- };
- pc.onaddstream = function(event) {
- // TODO: Finalize this API
- rtc.fire('add remote stream', event.stream, id);
- };
- if (rtc.dataChannelSupport) {
- pc.ondatachannel = function(evt) {
- console.log('data channel connecting ' + id);
- rtc.addDataChannel(id, evt.channel);
- };
- }
- return pc;
- };
- rtc.sendOffer = function(socketId) {
- var pc = rtc.peerConnections[socketId];
- var constraints = {
- "optional": [],
- "mandatory": {
- "MozDontOfferDataChannel": true
- }
- };
- // temporary measure to remove Moz* constraints in Chrome
- if (navigator.webkitGetUserMedia) {
- for (var prop in constraints.mandatory) {
- if (prop.indexOf("Moz") != -1) {
- delete constraints.mandatory[prop];
- }
- }
- }
- constraints = mergeConstraints(constraints, sdpConstraints);
- pc.createOffer(function(session_description) {
- session_description.sdp = preferOpus(session_description.sdp);
- pc.setLocalDescription(session_description);
- rtc._socket.send(JSON.stringify({
- "eventName": "send_offer",
- "data": {
- "socketId": socketId,
- "sdp": session_description
- }
- }));
- }, null, sdpConstraints);
- };
- rtc.receiveOffer = function(socketId, sdp) {
- var pc = rtc.peerConnections[socketId];
- rtc.sendAnswer(socketId, sdp);
- };
- rtc.sendAnswer = function(socketId, sdp) {
- var pc = rtc.peerConnections[socketId];
- pc.setRemoteDescription(new nativeRTCSessionDescription(sdp));
- pc.createAnswer(function(session_description) {
- pc.setLocalDescription(session_description);
- rtc._socket.send(JSON.stringify({
- "eventName": "send_answer",
- "data": {
- "socketId": socketId,
- "sdp": session_description
- }
- }));
- //TODO Unused variable!?
- var offer = pc.remoteDescription;
- }, null, sdpConstraints);
- };
- rtc.receiveAnswer = function(socketId, sdp) {
- var pc = rtc.peerConnections[socketId];
- pc.setRemoteDescription(new nativeRTCSessionDescription(sdp));
- };
- rtc.createStream = function(opt, onSuccess, onFail) {
- var options;
- onSuccess = onSuccess || function() {};
- onFail = onFail || function() {};
- options = {
- video: !! opt.video,
- audio: !! opt.audio
- };
- if (getUserMedia) {
- rtc.numStreams++;
- getUserMedia.call(navigator, options, function(stream) {
- rtc.streams.push(stream);
- rtc.initializedStreams++;
- onSuccess(stream);
- if (rtc.initializedStreams === rtc.numStreams) {
- rtc.fire('ready');
- }
- }, function() {
- alert("Could not connect stream.");
- onFail();
- });
- } else {
- alert('Your Browser does not support WebRTC (Video Chat). Try Firefox, Chrome, or Opera');
- }
- };
- rtc.addStreams = function() {
- for (var i = 0; i < rtc.streams.length; i++) {
- var stream = rtc.streams[i];
- for (var connection in rtc.peerConnections) {
- rtc.peerConnections[connection].addStream(stream);
- }
- }
- };
- rtc.attachStream = function(stream, domId) {
- var element = document.getElementById(domId);
- if (navigator.mozGetUserMedia) {
- console.log("Attaching media stream");
- element.mozSrcObject = stream;
- element.play();
- } else {
- element.src = webkitURL.createObjectURL(stream);
- }
- };
- rtc.createDataChannel = function(pcOrId, label) {
- if (!rtc.dataChannelSupport) {
- //TODO this should be an exception
- alert('webRTC data channel is not yet supported in this browser,' +
- ' or you must turn on experimental flags');
- return;
- }
- var id, pc;
- if (typeof(pcOrId) === 'string') {
- id = pcOrId;
- pc = rtc.peerConnections[pcOrId];
- } else {
- pc = pcOrId;
- id = undefined;
- for (var key in rtc.peerConnections) {
- if (rtc.peerConnections[key] === pc) id = key;
- }
- }
- if (!id) throw new Error('attempt to createDataChannel with unknown id');
- if (!pc || !(pc instanceof PeerConnection)) throw new Error('attempt to createDataChannel without peerConnection');
- // need a label
- label = label || 'fileTransfer' || String(id);
- // chrome only supports reliable false atm.
- var options = {
- reliable: false
- };
- var channel;
- try {
- console.log('createDataChannel ' + id);
- channel = pc.createDataChannel(label, options);
- } catch (error) {
- console.log('seems that DataChannel is NOT actually supported!');
- throw error;
- }
- return rtc.addDataChannel(id, channel);
- };
- rtc.addDataChannel = function(id, channel) {
- channel.onopen = function() {
- console.log('data stream open ' + id);
- rtc.fire('data stream open', channel);
- };
- channel.onclose = function(event) {
- delete rtc.dataChannels[id];
- console.log('data stream close ' + id);
- rtc.fire('data stream close', channel);
- };
- channel.onmessage = function(message) {
- console.log('data stream message ' + id);
- console.log(message);
- rtc.fire('data stream data', channel, message.data);
- };
- channel.onerror = function(err) {
- console.log('data stream error ' + id + ': ' + err);
- rtc.fire('data stream error', channel, err);
- };
- // track dataChannel
- rtc.dataChannels[id] = channel;
- return channel;
- };
- rtc.addDataChannels = function() {
- if (!rtc.dataChannelSupport) return;
- for (var connection in rtc.peerConnections)
- rtc.createDataChannel(connection);
- };
- rtc.on('ready', function() {
- rtc.createPeerConnections();
- rtc.addStreams();
- rtc.addDataChannels();
- rtc.sendOffers();
- });
- }).call(this);
- function preferOpus(sdp) {
- var sdpLines = sdp.split('\r\n');
- var mLineIndex = null;
- // Search for m line.
- for (var i = 0; i < sdpLines.length; i++) {
- if (sdpLines[i].search('m=audio') !== -1) {
- mLineIndex = i;
- break;
- }
- }
- if (mLineIndex === null) return sdp;
- // If Opus is available, set it as the default in m line.
- for (var j = 0; j < sdpLines.length; j++) {
- if (sdpLines[j].search('opus/48000') !== -1) {
- var opusPayload = extractSdp(sdpLines[j], /:(\d+) opus\/48000/i);
- if (opusPayload) sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], opusPayload);
- break;
- }
- }
- // Remove CN in m line and sdp.
- sdpLines = removeCN(sdpLines, mLineIndex);
- sdp = sdpLines.join('\r\n');
- return sdp;
- }
- function extractSdp(sdpLine, pattern) {
- var result = sdpLine.match(pattern);
- return (result && result.length == 2) ? result[1] : null;
- }
- function setDefaultCodec(mLine, payload) {
- var elements = mLine.split(' ');
- var newLine = [];
- var index = 0;
- for (var i = 0; i < elements.length; i++) {
- if (index === 3) // Format of media starts from the fourth.
- newLine[index++] = payload; // Put target payload to the first.
- if (elements[i] !== payload) newLine[index++] = elements[i];
- }
- return newLine.join(' ');
- }
- function removeCN(sdpLines, mLineIndex) {
- var mLineElements = sdpLines[mLineIndex].split(' ');
- // Scan from end for the convenience of removing an item.
- for (var i = sdpLines.length - 1; i >= 0; i--) {
- var payload = extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i);
- if (payload) {
- var cnPos = mLineElements.indexOf(payload);
- if (cnPos !== -1) {
- // Remove CN payload from m line.
- mLineElements.splice(cnPos, 1);
- }
- // Remove CN line in sdp
- sdpLines.splice(i, 1);
- }
- }
- sdpLines[mLineIndex] = mLineElements.join(' ');
- return sdpLines;
- }
- function mergeConstraints(cons1, cons2) {
- var merged = cons1;
- for (var name in cons2.mandatory) {
- merged.mandatory[name] = cons2.mandatory[name];
- }
- merged.optional.concat(cons2.optional);
- return merged;
- }
|