test('extend config with iceServers', function(t) { var config; var testServers = freeice(); t.plan(2); t.ok(config = generators.config({ iceServers: testServers }), 'generated config'); t.deepEqual(config.iceServers, testServers, 'servers matched'); });
initialize() { this.connectionSettings = {'room': Settings.swarm, iceServers: Freeice(), debug: false} this.connection = QuickConnect(Settings.tracker, this.connectionSettings) this.swarm = new Swarm() //TODO create different data channels for different protocol messages this.dataChannel = this.connection.createDataChannel(Settings.swarm) this.setupListerners() }
init: (cfg) => (dispatch, getState) => { rtc = new SimpleWebRTC({ url: __TURN_SERVER__, debug: cfg.debug || false, peerConnectionConfig: freeice() }); rtc .on('connectionReady', (id) => { dispatch({type: types.CONNECTION_READY, payload: id}) }) .on('createdPeer', (peer) => { dispatch({type: types.NEW_PEER_ADDED, payload: peer}) }) .on('peerStreamAdded', (peer) => { dispatch({type: types.PEER_STREAM_ADDED, payload: peer}) }) .on('peerStreamRemoved', (peer) => { dispatch({type: types.PEER_STREAM_REMOVED, payload: peer}) }); dispatch({type: types.INIT, payload: rtc}); },
function noop(e){e&&console.error(e)}function trackStop(e){e.stop&&e.stop()}function streamStop(e){e.getTracks().forEach(trackStop)}function bufferizeCandidates(e,n){var t=[];return e.onsignalingstatechange=function(e){if("stable"===this.signalingState)for(;t.length;){var n=t.shift();this.addIceCandidate(n.candidate,n.callback,n.callback)}},function(i,o){switch(o=o||n,e.signalingState){case"closed":o(new Error("PeerConnection object is closed"));break;case"stable":if(e.remoteDescription){e.addIceCandidate(i,o,o);break}default:t.push({candidate:i,callback:o})}}}function removeFIDFromOffer(e){var n=e.indexOf("a=ssrc-group:FID");return n>0?e.slice(0,n):e}function getSimulcastInfo(e){var n=e.getVideoTracks(),t=["a=x-google-flag:conference","a=ssrc-group:SIM 1 2 3","a=ssrc:1 cname:localVideo","a=ssrc:1 msid:"+e.id+" "+n[0].id,"a=ssrc:1 mslabel:"+e.id,"a=ssrc:1 label:"+n[0].id,"a=ssrc:2 cname:localVideo","a=ssrc:2 msid:"+e.id+" "+n[0].id,"a=ssrc:2 mslabel:"+e.id,"a=ssrc:2 label:"+n[0].id,"a=ssrc:3 cname:localVideo","a=ssrc:3 msid:"+e.id+" "+n[0].id,"a=ssrc:3 mslabel:"+e.id,"a=ssrc:3 label:"+n[0].id];return t.push(""),t.join("\n")}function WebRtcPeer(e,n,t){function i(){if(a){var e=p.getRemoteStreams()[0],n=e;attachMediaStream(a,e),console.log("Remote URL:",n)}}function o(){C.emit("streamended",this)}function r(){"closed"===p.signalingState&&t('The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue'),d&&c&&C.showLocalVideo(),d&&(d.onended=function(e){o()},p.addStream(d)),l&&(l.onended=function(e){o()},p.addStream(l));var n=parser.getBrowser();"sendonly"!==e||"Chrome"!==n.name&&"Chromium"!==n.name||39!==n.major||(e="sendrecv"),t()}function s(e){void 0===e&&(e=MEDIA_CONSTRAINTS),getUserMedia(e,function(e){d=e,r()},t)}if(!(this instanceof WebRtcPeer))return new WebRtcPeer(e,n,t);WebRtcPeer.super_.call(this),n instanceof Function&&(t=n,n=void 0),n=n||{},t=(t||noop).bind(this);var c=n.localVideo,a=n.remoteVideo,d=n.videoStream,l=n.audioStream,u=n.mediaConstraints,f=n.connectionConstraints,p=n.peerConnection,v=n.sendSource||"webcam",h=uuid.v4(),g=recursive({iceServers:freeice()},n.configuration),m=n.onstreamended;m&&this.on("streamended",m);var b=n.onicecandidate;b&&this.on("icecandidate",b);var S=n.oncandidategatheringdone;S&&this.on("candidategatheringdone",S);var R=n.simulcast;p||(p=new RTCPeerConnection(g)),Object.defineProperties(this,{peerConnection:{get:function(){return p}},remoteVideo:{get:function(){return a}},localVideo:{get:function(){return c}},currentFrame:{get:function(){if(a){if(a.readyState<a.HAVE_CURRENT_DATA)throw new Error("No video stream data available");var e=document.createElement("canvas");return e.width=a.videoWidth,e.height=a.videoHeight,e.getContext("2d").drawImage(a,0,0),e}}}});var C=this,w=[],P=!1;p.onicecandidate=function(e){var n=e.candidate;EventEmitter.listenerCount(C,"icecandidate")||EventEmitter.listenerCount(C,"candidategatheringdone")?n?(C.emit("icecandidate",n),P=!1):P||(C.emit("candidategatheringdone"),P=!0):P||(w.push(n),n||(P=!0))},this.on("newListener",function(e,n){if("icecandidate"===e||"candidategatheringdone"===e)for(;w.length;){var t=w.shift();!t==("candidategatheringdone"===e)&&n(t)}});var y=bufferizeCandidates(p);this.addIceCandidate=function(e,n){var t=new RTCIceCandidate(e);console.log("ICE candidate received"),n=(n||noop).bind(this),y(t,n)},this.generateOffer=function(n){n=n.bind(this);var t=parser.getBrowser(),i=!0,o=!0;u&&(i="boolean"==typeof u.audio?u.audio:!0,o="boolean"==typeof u.video?u.video:!0);var r="Firefox"===t.name&&t.version>34?{offerToReceiveAudio:"sendonly"!==e&&i,offerToReceiveVideo:"sendonly"!==e&&o}:{mandatory:{OfferToReceiveAudio:"sendonly"!==e&&i,OfferToReceiveVideo:"sendonly"!==e&&o},optional:[{DtlsSrtpKeyAgreement:!0}]},s=recursive(r,f);console.log("constraints: "+JSON.stringify(s)),p.createOffer(function(e){console.log("Created SDP offer"),R&&("Chrome"===t.name||"Chromium"===t.name?(console.log("Adding multicast info"),e=new RTCSessionDescription({type:e.type,sdp:removeFIDFromOffer(e.sdp)+getSimulcastInfo(d)})):console.warn("Simulcast is only available in Chrome browser.")),p.setLocalDescription(e,function(){console.log("Local description set",e.sdp),n(null,e.sdp,C.processAnswer.bind(C))},n)},n,s)},this.getLocalSessionDescriptor=function(){return p.localDescription},this.getRemoteSessionDescriptor=function(){return p.remoteDescription},this.showLocalVideo=function(){attachMediaStream(c,d)},this.processAnswer=function(e,n){n=(n||noop).bind(this);var t=new RTCSessionDescription({type:"answer",sdp:e});return console.log("SDP answer received, setting remote description"),"closed"===p.signalingState?n("PeerConnection is closed"):void p.setRemoteDescription(t,function(){i(),n()},n)},this.processOffer=function(e,n){n=n.bind(this);var t=new RTCSessionDescription({type:"offer",sdp:e});return console.log("SDP offer received, setting remote description"),"closed"===p.signalingState?n("PeerConnection is closed"):void p.setRemoteDescription(t,function(){i(),p.createAnswer(function(e){console.log("Created SDP answer"),p.setLocalDescription(e,function(){console.log("Local description set",e.sdp),n(null,e.sdp)},n)},n)},n)},"recvonly"===e||d||l?setTimeout(r,0):"webcam"===v?s(u):getScreenConstraints(v,function(e,n){return e?t(e):(constraints=[u],constraints.unshift(n),void s(recursive.apply(void 0,constraints)))},h),this.on("_dispose",function(){c&&(c.pause(),c.src="",c.load()),a&&(a.pause(),a.src="",a.load()),C.removeAllListeners(),void 0!==window.cancelChooseDesktopMedia&&window.cancelChooseDesktopMedia(h)})}function createEnableDescriptor(e){var n="get"+e+"Tracks";return{enumerable:!0,get:function(){if(this.peerConnection){var e=this.peerConnection.getLocalStreams();if(e.length){for(var t,i=0;t=e[i];i++)for(var o,r=t[n](),s=0;o=r[s];s++)if(!o.enabled)return!1;return!0}}},set:function(e){function t(n){n.enabled=e}this.peerConnection.getLocalStreams().forEach(function(e){e[n]().forEach(t)})}}}function WebRtcPeerRecvonly(e,n){return this instanceof WebRtcPeerRecvonly?void WebRtcPeerRecvonly.super_.call(this,"recvonly",e,n):new WebRtcPeerRecvonly(e,n)}function WebRtcPeerSendonly(e,n){return this instanceof WebRtcPeerSendonly?void WebRtcPeerSendonly.super_.call(this,"sendonly",e,n):new WebRtcPeerSendonly(e,n)}function WebRtcPeerSendrecv(e,n){return this instanceof WebRtcPeerSendrecv?void WebRtcPeerSendrecv.super_.call(this,"sendrecv",e,n):new WebRtcPeerSendrecv(e,n)}var freeice=require("freeice"),inherits=require("inherits"),UAParser=require("ua-parser-js"),uuid=require("uuid"),EventEmitter=require("events").EventEmitter,recursive=require("merge").recursive.bind(void 0,!0);try{require("kurento-browser-extensions")}catch(error){"undefined"==typeof getScreenConstraints&&(console.warn("screen sharing is not available"),getScreenConstraints=function(e,n){n(new Error("This library is not enabled for screen sharing"))})}var MEDIA_CONSTRAINTS={audio:!0,video:{width:640,framerate:15}},ua=window&&window.navigator?window.navigator.userAgent:"",parser=new UAParser(ua);inherits(WebRtcPeer,EventEmitter),Object.defineProperties(WebRtcPeer.prototype,{enabled:{enumerable:!0,get:function(){return this.audioEnabled&&this.videoEnabled},set:function(e){this.audioEnabled=this.videoEnabled=e}},audioEnabled:createEnableDescriptor("Audio"),videoEnabled:createEnableDescriptor("Video")}),WebRtcPeer.prototype.getLocalStream=function(e){return this.peerConnection?this.peerConnection.getLocalStreams()[e||0]:void 0},WebRtcPeer.prototype.getRemoteStream=function(e){return this.peerConnection?this.peerConnection.getRemoteStreams()[e||0]:void 0},WebRtcPeer.prototype.dispose=function(){console.log("Disposing WebRtcPeer");var e=this.peerConnection;try{if(e){if("closed"===e.signalingState)return;e.getLocalStreams().forEach(streamStop),e.close()}}catch(n){console.warn("Exception disposing webrtc peer "+n)}this.emit("_dispose")},inherits(WebRtcPeerRecvonly,WebRtcPeer),inherits(WebRtcPeerSendonly,WebRtcPeer),inherits(WebRtcPeerSendrecv,WebRtcPeer),exports.bufferizeCandidates=bufferizeCandidates,exports.WebRtcPeerRecvonly=WebRtcPeerRecvonly,exports.WebRtcPeerSendonly=WebRtcPeerSendonly,exports.WebRtcPeerSendrecv=WebRtcPeerSendrecv;
connect: function() { connection = rtc_quickconnect(BEMTV_SERVER, {room: this.room, iceServers: freeice()}); console.log("[bemtv] connecting to " + this.room); this.createDataChannel(connection); },
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ function defaultOnerror(e){e&&console.error(e)}function noop(){}function WebRtcPeer(e,t,r,o,i,s,n){Object.defineProperty(this,"pc",{writable:!0}),this.localVideo=t,this.remoteVideo=r,this.onerror=i||defaultOnerror,this.stream=s,this.audioStream=n,this.mode=e,this.onsdpoffer=o||noop}var freeice=require("freeice");WebRtcPeer.prototype.start=function(){var e=this;this.pc||(this.pc=new RTCPeerConnection(this.server,this.options));var t=this.pc;this.stream&&this.localVideo&&(this.localVideo.src=URL.createObjectURL(this.stream),this.localVideo.muted=!0),this.stream&&t.addStream(this.stream),this.audioStream&&t.addStream(this.audioStream),this.constraints={mandatory:{OfferToReceiveAudio:void 0!==this.remoteVideo,OfferToReceiveVideo:void 0!==this.remoteVideo}},t.createOffer(function(r){console.log("Created SDP offer"),t.setLocalDescription(r,function(){console.log("Local description set")},e.onerror)},this.onerror,this.constraints);var r=!1;t.onicecandidate=function(o){if(o.candidate)return void(r=!1);if(!r){var i=t.localDescription.sdp;console.log("ICE negotiation completed"),e.onsdpoffer(i,e),r=!0}}},WebRtcPeer.prototype.dispose=function(){console.log("Disposing WebRtcPeer"),this.pc&&"closed"!=this.pc.signalingState&&this.pc.close(),this.localVideo&&(this.localVideo.src=""),this.remoteVideo&&(this.remoteVideo.src=""),this.stream&&(this.stream.getAudioTracks().forEach(function(e){e.stop&&e.stop()}),this.stream.getVideoTracks().forEach(function(e){e.stop&&e.stop()}))},WebRtcPeer.prototype.userMediaConstraints={audio:!0,video:{mandatory:{maxWidth:640,maxFrameRate:15,minFrameRate:15}}},WebRtcPeer.prototype.processSdpAnswer=function(e){var t=new RTCSessionDescription({type:"answer",sdp:e});console.log("SDP answer received, setting remote description");var r=this;r.pc.setRemoteDescription(t,function(){if(r.remoteVideo){var e=r.pc.getRemoteStreams()[0];r.remoteVideo.src=URL.createObjectURL(e)}},this.onerror)},WebRtcPeer.prototype.server={iceServers:freeice()},WebRtcPeer.prototype.options={optional:[{DtlsSrtpKeyAgreement:!0}]},WebRtcPeer.start=function(e,t,r,o,i,s,n,c){var a=new WebRtcPeer(e,t,r,o,i,n,c);if("recv"===a.mode||a.stream)a.start();else{var d=s?s:a.userMediaConstraints;getUserMedia(d,function(e){a.stream=e,a.start()},a.onerror)}return a},WebRtcPeer.startRecvOnly=function(e,t,r,o){return WebRtcPeer.start("recv",null,e,t,r,o)},WebRtcPeer.startSendOnly=function(e,t,r,o){return WebRtcPeer.start("send",e,null,t,r,o)},WebRtcPeer.startSendRecv=function(e,t,r,o,i){return WebRtcPeer.start("sendRecv",e,t,r,o,i)},module.exports=WebRtcPeer; },{"freeice":3}],2:[function(require,module,exports){
var freeice = require('freeice'); var quickconnect = require('rtc-quickconnect'); var opts = { room: 'qcdemo42', debug: true, OfferToReceiveAudio: false, OfferToReceiveVideo: false, iceServers: freeice() }; var connecton = quickconnect('https://switchboard.rtc.io/', opts); console.log("ID: " + connecton.id); connecton .createDataChannel('test') .on('channel:opened:test', function (id, dc) { console.log('dc open for peer: ' + id); }) .on('message:candidate', function (data) { console.warn(data) });
function noop(e){e&&console.error(e)}function trackStop(e){e.stop&&e.stop()}function streamStop(e){e.getTracks().forEach(trackStop)}function bufferizeCandidates(e,n){var t=[];return e.addEventListener("signalingstatechange",function(){if("stable"===this.signalingState)for(;t.length;){var e=t.shift();this.addIceCandidate(e.candidate,e.callback,e.callback)}}),function(i,r){switch(r=r||n,e.signalingState){case"closed":r(new Error("PeerConnection object is closed"));break;case"stable":if(e.remoteDescription){e.addIceCandidate(i,r,r);break}default:t.push({candidate:i,callback:r})}}}function WebRtcPeer(e,n,t){function i(){if(s){var e=p.getRemoteStreams()[0],n=e?URL.createObjectURL(e):"";s.pause(),s.src=n,s.load(),console.log("Remote URL:",n)}}function r(){S.emit("streamended",this)}function o(){"closed"===p.signalingState&&t('The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue'),d&&a&&S.showLocalVideo(),d&&(d.addEventListener("ended",r),p.addStream(d)),l&&(l.addEventListener("ended",r),p.addStream(l));var n=parser.getBrowser();"sendonly"!==e||"Chrome"!==n.name&&"Chromium"!==n.name||39!==n.major||(e="sendrecv"),t()}function c(e){e=Array.prototype.slice.call(arguments),e.unshift(MEDIA_CONSTRAINTS),getUserMedia(recursive.apply(void 0,e),function(e){d=e,o()},t)}if(!(this instanceof WebRtcPeer))return new WebRtcPeer(e,n,t);WebRtcPeer.super_.call(this),n instanceof Function&&(t=n,n=void 0),n=n||{},t=(t||noop).bind(this);var a=n.localVideo,s=n.remoteVideo,d=n.videoStream,l=n.audioStream,u=n.mediaConstraints,f=n.connectionConstraints,p=n.peerConnection,v=n.sendSource||"webcam",h=uuid.v4(),g=recursive({iceServers:freeice()},n.configuration),b=n.onstreamended;b&&this.on("streamended",b);var m=n.onicecandidate;m&&this.on("icecandidate",m);var R=n.oncandidategatheringdone;R&&this.on("candidategatheringdone",R),p||(p=new RTCPeerConnection(g)),Object.defineProperties(this,{peerConnection:{get:function(){return p}},remoteVideo:{get:function(){return s}},localVideo:{get:function(){return a}},currentFrame:{get:function(){if(s){if(s.readyState<s.HAVE_CURRENT_DATA)throw new Error("No video stream data available");var e=document.createElement("canvas");return e.width=s.videoWidth,e.height=s.videoHeight,e.getContext("2d").drawImage(s,0,0),e}}}});var S=this,P=[],C=!1;p.addEventListener("icecandidate",function(e){var n=e.candidate;EventEmitter.listenerCount(S,"icecandidate")||EventEmitter.listenerCount(S,"candidategatheringdone")?n?(S.emit("icecandidate",n),C=!1):C||(S.emit("candidategatheringdone"),C=!0):C||(P.push(n),n||(C=!0))}),this.on("newListener",function(e,n){if("icecandidate"===e||"candidategatheringdone"===e)for(;P.length;){var t=P.shift();!t==("candidategatheringdone"===e)&&n(t)}});var w=bufferizeCandidates(p);this.addIceCandidate=function(e,n){var t=new RTCIceCandidate(e);console.log("ICE candidate received"),n=(n||noop).bind(this),w(t,n)},this.generateOffer=function(n){n=n.bind(this);var t=parser.getBrowser(),i="Firefox"===t.name&&t.version>34?{offerToReceiveAudio:"sendonly"!==e,offerToReceiveVideo:"sendonly"!==e}:{mandatory:{OfferToReceiveAudio:"sendonly"!==e,OfferToReceiveVideo:"sendonly"!==e},optional:[{DtlsSrtpKeyAgreement:!0}]},r=recursive(i,f);console.log("constraints: "+JSON.stringify(r)),p.createOffer(function(e){console.log("Created SDP offer"),p.setLocalDescription(e,function(){console.log("Local description set",e.sdp),n(null,e.sdp,S.processAnswer.bind(S))},n)},n,r)},this.getLocalSessionDescriptor=function(){return p.localDescription},this.getRemoteSessionDescriptor=function(){return p.remoteDescription},this.showLocalVideo=function(){a.src=URL.createObjectURL(d),a.muted=!0},this.processAnswer=function(e,n){n=(n||noop).bind(this);var t=new RTCSessionDescription({type:"answer",sdp:e});return console.log("SDP answer received, setting remote description"),"closed"===p.signalingState?n("PeerConnection is closed"):void p.setRemoteDescription(t,function(){i(),n()},n)},this.processOffer=function(e,n){n=n.bind(this);var t=new RTCSessionDescription({type:"offer",sdp:e});return console.log("SDP offer received, setting remote description"),"closed"===p.signalingState?n("PeerConnection is closed"):void p.setRemoteDescription(t,function(){i(),p.createAnswer(function(e){console.log("Created SDP answer"),p.setLocalDescription(e,function(){console.log("Local description set",e.sdp),n(null,e.sdp)},n)},n)},n)},"recvonly"===e||d||l?setTimeout(o,0):"webcam"===v?c(u):getScreenConstraints(v,function(e,n){return e?t(e):void c(n,u)},h),this.on("_dispose",function(){a&&(a.pause(),a.src="",a.load()),s&&(s.pause(),s.src="",s.load()),S.removeAllListeners(),void 0!==window.cancelChooseDesktopMedia&&window.cancelChooseDesktopMedia(h)})}function createEnableDescriptor(e){var n="get"+e+"Tracks";return{enumerable:!0,get:function(){if(this.peerConnection){var e=this.peerConnection.getLocalStreams();if(e.length){for(var t,i=0;t=e[i];i++)for(var r,o=t[n](),c=0;r=o[c];c++)if(!r.enabled)return!1;return!0}}},set:function(e){function t(n){n.enabled=e}this.peerConnection.getLocalStreams().forEach(function(e){e[n]().forEach(t)})}}}function WebRtcPeerRecvonly(e,n){return this instanceof WebRtcPeerRecvonly?void WebRtcPeerRecvonly.super_.call(this,"recvonly",e,n):new WebRtcPeerRecvonly(e,n)}function WebRtcPeerSendonly(e,n){return this instanceof WebRtcPeerSendonly?void WebRtcPeerSendonly.super_.call(this,"sendonly",e,n):new WebRtcPeerSendonly(e,n)}function WebRtcPeerSendrecv(e,n){return this instanceof WebRtcPeerSendrecv?void WebRtcPeerSendrecv.super_.call(this,"sendrecv",e,n):new WebRtcPeerSendrecv(e,n)}var freeice=require("freeice"),inherits=require("inherits"),UAParser=require("ua-parser-js"),uuid=require("uuid"),EventEmitter=require("events").EventEmitter,recursive=require("merge").recursive.bind(void 0,!0);try{!function(){throw new Error("Cannot find module 'kurento-browser-extensions' from '/var/lib/jenkins/workspace/kurento-js-build-project/lib'")}()}catch(error){"undefined"==typeof getScreenConstraints&&(console.warn("screen sharing is not available"),getScreenConstraints=function(e,n){n(new Error("This library is not enabled for screen sharing"))})}var MEDIA_CONSTRAINTS={audio:!0,video:{mandatory:{maxWidth:640,maxFrameRate:15,minFrameRate:15}}},ua=window&&window.navigator?window.navigator.userAgent:"",parser=new UAParser(ua);inherits(WebRtcPeer,EventEmitter),Object.defineProperties(WebRtcPeer.prototype,{enabled:{enumerable:!0,get:function(){return this.audioEnabled&&this.videoEnabled},set:function(e){this.audioEnabled=this.videoEnabled=e}},audioEnabled:createEnableDescriptor("Audio"),videoEnabled:createEnableDescriptor("Video")}),WebRtcPeer.prototype.getLocalStream=function(e){return this.peerConnection?this.peerConnection.getLocalStreams()[e||0]:void 0},WebRtcPeer.prototype.getRemoteStream=function(e){return this.peerConnection?this.peerConnection.getRemoteStreams()[e||0]:void 0},WebRtcPeer.prototype.dispose=function(){console.log("Disposing WebRtcPeer");var e=this.peerConnection;if(e){if("closed"===e.signalingState)return;e.getLocalStreams().forEach(streamStop),e.close()}this.emit("_dispose")},inherits(WebRtcPeerRecvonly,WebRtcPeer),inherits(WebRtcPeerSendonly,WebRtcPeer),inherits(WebRtcPeerSendrecv,WebRtcPeer),exports.bufferizeCandidates=bufferizeCandidates,exports.WebRtcPeerRecvonly=WebRtcPeerRecvonly,exports.WebRtcPeerSendonly=WebRtcPeerSendonly,exports.WebRtcPeerSendrecv=WebRtcPeerSendrecv;
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.kurentoUtils=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ /* * (C) Copyright 2014 Kurento (http://kurento.org/) * * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser General Public License * (LGPL) version 2.1 which accompanies this distribution, and is available at * http://www.gnu.org/licenses/lgpl-2.1.html * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ var freeice = require('freeice'); /** * @description Default handler for error callbacks. The error messaged passed * as argument is showed in a console, a div layer which should be * previously created. * * @function defaultOnerror * * @param error - * {String} Error message */ function defaultOnerror(error) { if (error) console.error(error); } function noop() { }; /** * @classdesc Wrapper object of an RTCPeerConnection. This object is aimed to * simplify the development of WebRTC-based applications. * * @constructor module:kurentoUtils.WebRtcPeer * * @param mode - * {String} Mode in which the PeerConnection will be configured. * Valid values are: 'recv', 'send', and 'sendRecv' * @param localVideo - * {Object} Video tag for the local stream * @param remoteVideo - * {Object} Video tag for the remote stream * @param onsdpoffer - * {Function} Callback executed when a SDP offer has been generated * @param onerror - * {Function} Callback executed when an error happens generating an * SDP offer * @param videoStream - * {Object} MediaStream to be used as primary source (typically video * and audio, or only video if combined with audioStream) for * localVideo and to be added as stream to the RTCPeerConnection * @param audioStream - * {Object} MediaStream to be used as second source (typically for * audio) for localVideo and to be added as stream to the * RTCPeerConnection */ function WebRtcPeer(mode, localVideo, remoteVideo, onsdpoffer, onerror, videoStream, audioStream) { Object.defineProperty(this, 'pc', { writable : true }); this.localVideo = localVideo; this.remoteVideo = remoteVideo; this.onerror = onerror || defaultOnerror; this.stream = videoStream; this.audioStream = audioStream; this.mode = mode; this.onsdpoffer = onsdpoffer || noop; } /** * @description This method creates the RTCPeerConnection object taking into * account the properties received in the constructor. It starts * the SDP negotiation process: generates the SDP offer and invokes * the onsdpoffer callback. This callback is expected to send the * SDP offer, in order to obtain an SDP answer from another peer. * * @function module:kurentoUtils.WebRtcPeer.prototype.start * */ WebRtcPeer.prototype.start = function(server, options) { var self = this; server = server || this.server; options = options || this.seoptionsrver; if (!this.pc) { this.pc = new RTCPeerConnection(server, options); } var pc = this.pc; if (this.stream && this.localVideo) { this.localVideo.src = URL.createObjectURL(this.stream); this.localVideo.muted = true; } if (this.stream) { pc.addStream(this.stream); } if (this.audioStream) { pc.addStream(this.audioStream); } this.constraints = { mandatory : { OfferToReceiveAudio : (this.remoteVideo !== undefined), OfferToReceiveVideo : (this.remoteVideo !== undefined) } }; pc.createOffer(function(offer) { console.log('Created SDP offer'); pc.setLocalDescription(offer, function() { console.log('Local description set'); }, self.onerror); }, this.onerror, this.constraints); var ended = false; pc.onicecandidate = function(e) { // candidate exists in e.candidate if (e.candidate) { ended = false; return; } if (ended) { return; } var offerSdp = pc.localDescription.sdp; console.log('ICE negotiation completed'); self.onsdpoffer(offerSdp, self); // self.emit('sdpoffer', offerSdp); ended = true; }; } /** * @description This method frees the resources used by WebRtcPeer. * * @function module:kurentoUtils.WebRtcPeer.prototype.dispose */ WebRtcPeer.prototype.dispose = function() { console.log('Disposing WebRtcPeer'); // FIXME This is not yet implemented in firefox // if (this.stream) this.pc.removeStream(this.stream); // For old browsers, PeerConnection.close() is NOT idempotent and raise // error. We check its signaling state and don't close it if it's already // closed if (this.pc && this.pc.signalingState != 'closed') this.pc.close(); if (this.localVideo) this.localVideo.src = ''; if (this.remoteVideo) this.remoteVideo.src = ''; if (this.stream) { this.stream.getAudioTracks().forEach(function(track) { track.stop && track.stop() }) this.stream.getVideoTracks().forEach(function(track) { track.stop && track.stop() }) } }; /** * @description Default user media constraints considered when invoking the * getUserMedia function. These values are: maxWidth=640, * maxFrameRate=15, minFrameRate=15. * * @alias module:kurentoUtils.WebRtcPeer.prototype.userMediaConstraints */ WebRtcPeer.prototype.userMediaConstraints = { audio : true, video : { mandatory : { maxWidth : 640, maxFrameRate : 15, minFrameRate : 15 } } }; /** * @description Callback function invoked when and SDP answer is received. * Developers are expected to invoke this function in order to * complete the SDP negotiation. * * @function module:kurentoUtils.WebRtcPeer.prototype.processSdpAnswer * * @param sdpAnswer - * Description of sdpAnswer * @param successCallback - * Called when the remoteDescription and the remoteVideo.src have * been set successfully. */ WebRtcPeer.prototype.processSdpAnswer = function(sdpAnswer, successCallback) { var answer = new RTCSessionDescription({ type : 'answer', sdp : sdpAnswer, }); console.log('SDP answer received, setting remote description'); var self = this; self.pc.setRemoteDescription(answer, function() { if (self.remoteVideo) { var stream = self.pc.getRemoteStreams()[0]; self.remoteVideo.src = URL.createObjectURL(stream); } if (successCallback) { successCallback(); } }, this.onerror); } /** * @description Default ICE server (stun:stun.l.google.com:19302). * * @alias module:kurentoUtils.WebRtcPeer.prototype.server */ WebRtcPeer.prototype.server = { iceServers : freeice() }; /** * @description Default options (DtlsSrtpKeyAgreement=true) for * RTCPeerConnection. * * @alias module:kurentoUtils.WebRtcPeer.prototype.options */ WebRtcPeer.prototype.options = { optional : [ { DtlsSrtpKeyAgreement : true } ] }; /** * @description This method creates the WebRtcPeer object and obtain userMedia * if needed. * * @function module:kurentoUtils.WebRtcPeer.start * * @param mode - * {String} Mode in which the PeerConnection will be configured. * Valid values are: 'recv', 'send', and 'sendRecv' * @param localVideo - * {Object} Video tag for the local stream * @param remoteVideo - * {Object} Video tag for the remote stream * @param onSdp - * {Function} Callback executed when a SDP offer has been generated * @param onerror - * {Function} Callback executed when an error happens generating an * SDP offer * @param mediaConstraints - * {Object[]} Constraints used to create RTCPeerConnection * @param videoStream - * {Object} MediaStream to be used as primary source (typically video * and audio, or only video if combined with audioStream) for * localVideo and to be added as stream to the RTCPeerConnection * @param videoStream - * {Object} MediaStream to be used as primary source (typically video * and audio, or only video if combined with audioStream) for * localVideo and to be added as stream to the RTCPeerConnection * @param audioStream - * {Object} MediaStream to be used as second source (typically for * audio) for localVideo and to be added as stream to the * RTCPeerConnection * * @return {module:kurentoUtils.WebRtcPeer} */ WebRtcPeer.start = function(mode, localVideo, remoteVideo, onSdp, onerror, mediaConstraints, videoStream, audioStream, server, options) { var wp = new WebRtcPeer(mode, localVideo, remoteVideo, onSdp, onerror, videoStream, audioStream); if (wp.mode !== 'recv' && !wp.stream) { var constraints = mediaConstraints ? mediaConstraints : wp.userMediaConstraints; getUserMedia(constraints, function(userStream) { wp.stream = userStream; wp.start(server, options); }, wp.onerror); } else { wp.start(server, options); } return wp; }; /** * @description This methods creates a WebRtcPeer to receive video. * * @function module:kurentoUtils.WebRtcPeer.startRecvOnly * * @param remoteVideo - * {Object} Video tag for the remote stream * @param onSdp - * {Function} Callback executed when a SDP offer has been generated * @param onerror - * {Function} Callback executed when an error happens generating an * SDP offer * @param mediaConstraints - * {Object[]} Constraints used to create RTCPeerConnection * * @return {module:kurentoUtils.WebRtcPeer} */ WebRtcPeer.startRecvOnly = function(remoteVideo, onSdp, onError, mediaConstraints, server, options) { return WebRtcPeer.start('recv', null, remoteVideo, onSdp, onError, mediaConstraints, server, options); }; /** * @description This methods creates a WebRtcPeer to send video. * * @function module:kurentoUtils.WebRtcPeer.startSendOnly * * @param localVideo - * {Object} Video tag for the local stream * @param onSdp - * {Function} Callback executed when a SDP offer has been generated * @param onerror - * {Function} Callback executed when an error happens generating an * SDP offer * @param mediaConstraints - * {Object[]} Constraints used to create RTCPeerConnection * * @return {module:kurentoUtils.WebRtcPeer} */ WebRtcPeer.startSendOnly = function(localVideo, onSdp, onError, mediaConstraints, server, options) { return WebRtcPeer.start('send', localVideo, null, onSdp, onError, mediaConstraints, server, options); }; /** * @description This methods creates a WebRtcPeer to send and receive video. * * @function module:kurentoUtils.WebRtcPeer.startSendRecv * * @param localVideo - * {Object} Video tag for the local stream * @param remoteVideo - * {Object} Video tag for the remote stream * @param onSdp - * {Function} Callback executed when a SDP offer has been generated * @param onerror - * {Function} Callback executed when an error happens generating an * SDP offer * @param mediaConstraints - * {Object[]} Constraints used to create RTCPeerConnection * * @return {module:kurentoUtils.WebRtcPeer} */ WebRtcPeer.startSendRecv = function(localVideo, remoteVideo, onSdp, onError, mediaConstraints, server, options) { return WebRtcPeer.start('sendRecv', localVideo, remoteVideo, onSdp, onError, mediaConstraints, server, options); }; module.exports = WebRtcPeer; },{"freeice":3}],2:[function(require,module,exports){
var quickconnect = require('rtc-quickconnect'); var buffered = require('rtc-bufferedchannel'); var freeice = require('freeice'); var utils = require('./Utils.js'); /* Configuration */ BEMTV_ROOM_DISCOVER_URL = "http://server.bem.tv/room" BEMTV_SERVER = "http://server.bem.tv:8080" ICE_SERVERS = freeice(); DESIRE_TIMEOUT = 0.7; // in seconds REQ_TIMEOUT = 3; // in seconds MAX_CACHE_SIZE = 10; /* Header protocol messages */ CHUNK_REQ = "req" CHUNK_DESIRE = "des" CHUNK_DESACK = "desack" CHUNK_OFFER = "offer" /* Peer States */ PEER_IDLE = 0 PEER_WAITING = 1 PEER_UPLOADING = 2 PEER_DOWNLOADING = 3 PEER_DESIRING = 4 var BemTV = function() { this._init(); }
function noop(e){e&&console.error(e)}function trackStop(e){e.stop&&e.stop()}function streamStop(e){e.getTracks().forEach(trackStop)}function bufferizeCandidates(e,n){var t=[];return e.addEventListener("signalingstatechange",function(){if("stable"===this.signalingState)for(;t.length;){var e=t.shift();this.addIceCandidate(e.candidate,e.callback,e.callback)}}),function(i,r){switch(r=r||n,e.signalingState){case"closed":r(new Error("PeerConnection object is closed"));break;case"stable":if(e.remoteDescription){e.addIceCandidate(i,r,r);break}default:t.push({candidate:i,callback:r})}}}function removeFIDFromOffer(e){var n=e.indexOf("a=ssrc-group:FID");return n>0?e.slice(0,n):e}function getSimulcastInfo(e){var n=e.getVideoTracks();if(!n.length)return console.warn("No video tracks available in the video stream"),"";var t=["a=x-google-flag:conference","a=ssrc-group:SIM 1 2 3","a=ssrc:1 cname:localVideo","a=ssrc:1 msid:"+e.id+" "+n[0].id,"a=ssrc:1 mslabel:"+e.id,"a=ssrc:1 label:"+n[0].id,"a=ssrc:2 cname:localVideo","a=ssrc:2 msid:"+e.id+" "+n[0].id,"a=ssrc:2 mslabel:"+e.id,"a=ssrc:2 label:"+n[0].id,"a=ssrc:3 cname:localVideo","a=ssrc:3 msid:"+e.id+" "+n[0].id,"a=ssrc:3 mslabel:"+e.id,"a=ssrc:3 label:"+n[0].id];return t.push(""),t.join("\n")}function WebRtcPeer(e,n,t){function i(){if(d){var e=h.getRemoteStreams()[0],n=e?URL.createObjectURL(e):"";d.pause(),d.src=n,d.load(),console.log("Remote URL:",n)}}function r(e){return C&&("Chrome"===w.name||"Chromium"===w.name?(console.log("Adding multicast info"),e=new RTCSessionDescription({type:e.type,sdp:removeFIDFromOffer(e.sdp)+getSimulcastInfo(l)})):console.warn("Simulcast is only available in Chrome browser.")),e}function o(){P.emit("streamended",this)}function a(){"closed"===h.signalingState&&t('The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue'),l&&c&&P.showLocalVideo(),l&&(l.addEventListener("ended",o),h.addStream(l)),u&&(u.addEventListener("ended",o),h.addStream(u));var n=parser.getBrowser();"sendonly"!==e||"Chrome"!==n.name&&"Chromium"!==n.name||39!==n.major||(e="sendrecv"),t()}function s(e){void 0===e&&(e=MEDIA_CONSTRAINTS),getUserMedia(e,function(e){l=e,a()},t)}if(!(this instanceof WebRtcPeer))return new WebRtcPeer(e,n,t);WebRtcPeer.super_.call(this),n instanceof Function&&(t=n,n=void 0),n=n||{},t=(t||noop).bind(this);var c=n.localVideo,d=n.remoteVideo,l=n.videoStream,u=n.audioStream,f=n.mediaConstraints,v=n.connectionConstraints,h=n.peerConnection,p=n.sendSource||"webcam",g=uuid.v4(),b=recursive({iceServers:freeice()},n.configuration),m=n.onstreamended;m&&this.on("streamended",m);var R=n.onicecandidate;R&&this.on("icecandidate",R);var S=n.oncandidategatheringdone;S&&this.on("candidategatheringdone",S);var w=parser.getBrowser(),C=n.simulcast;h||(h=new RTCPeerConnection(b)),Object.defineProperties(this,{peerConnection:{get:function(){return h}},id:{value:n.id||g,writable:!1},remoteVideo:{get:function(){return d}},localVideo:{get:function(){return c}},currentFrame:{get:function(){if(d){if(d.readyState<d.HAVE_CURRENT_DATA)throw new Error("No video stream data available");var e=document.createElement("canvas");return e.width=d.videoWidth,e.height=d.videoHeight,e.getContext("2d").drawImage(d,0,0),e}}}});var P=this,y=[],E=!1;h.addEventListener("icecandidate",function(e){var n=e.candidate;EventEmitter.listenerCount(P,"icecandidate")||EventEmitter.listenerCount(P,"candidategatheringdone")?n?(P.emit("icecandidate",n),E=!1):E||(P.emit("candidategatheringdone"),E=!0):E||(y.push(n),n||(E=!0))}),h.onaddstream=n.onaddstream,h.onnegotiationneeded=n.onnegotiationneeded,this.on("newListener",function(e,n){if("icecandidate"===e||"candidategatheringdone"===e)for(;y.length;){var t=y.shift();!t==("candidategatheringdone"===e)&&n(t)}});var W=bufferizeCandidates(h);this.addIceCandidate=function(e,n){var t=new RTCIceCandidate(e);console.log("ICE candidate received"),n=(n||noop).bind(this),W(t,n)},this.generateOffer=function(n){n=n.bind(this);var t=!0,i=!0;f&&(t="boolean"==typeof f.audio?f.audio:!0,i="boolean"==typeof f.video?f.video:!0);var o="Firefox"===w.name&&w.version>34?{offerToReceiveAudio:"sendonly"!==e&&t,offerToReceiveVideo:"sendonly"!==e&&i}:{mandatory:{OfferToReceiveAudio:"sendonly"!==e&&t,OfferToReceiveVideo:"sendonly"!==e&&i},optional:[{DtlsSrtpKeyAgreement:!0}]},a=recursive(o,v);console.log("constraints: "+JSON.stringify(a)),h.createOffer(function(e){console.log("Created SDP offer"),e=r(e),h.setLocalDescription(e,function(){console.log("Local description set",e.sdp),n(null,e.sdp,P.processAnswer.bind(P))},n)},n,a)},this.getLocalSessionDescriptor=function(){return h.localDescription},this.getRemoteSessionDescriptor=function(){return h.remoteDescription},this.showLocalVideo=function(){c.src=URL.createObjectURL(l),c.muted=!0},this.processAnswer=function(e,n){n=(n||noop).bind(this);var t=new RTCSessionDescription({type:"answer",sdp:e});return console.log("SDP answer received, setting remote description"),"closed"===h.signalingState?n("PeerConnection is closed"):void h.setRemoteDescription(t,function(){i(),n()},n)},this.processOffer=function(e,n){n=n.bind(this);var t=new RTCSessionDescription({type:"offer",sdp:e});return console.log("SDP offer received, setting remote description"),"closed"===h.signalingState?n("PeerConnection is closed"):void h.setRemoteDescription(t,function(){i(),h.createAnswer(function(e){e=r(e),console.log("Created SDP answer"),h.setLocalDescription(e,function(){console.log("Local description set",e.sdp),n(null,e.sdp)},n)},n)},n)},"recvonly"===e||l||u?setTimeout(a,0):"webcam"===p?s(f):getScreenConstraints(p,function(e,n){return e?t(e):(constraints=[f],constraints.unshift(n),void s(recursive.apply(void 0,constraints)))},g),this.on("_dispose",function(){c&&(c.pause(),c.src="",c.load()),d&&(d.pause(),d.src="",d.load()),P.removeAllListeners(),void 0!==window.cancelChooseDesktopMedia&&window.cancelChooseDesktopMedia(g)})}function createEnableDescriptor(e){var n="get"+e+"Tracks";return{enumerable:!0,get:function(){if(this.peerConnection){var e=this.peerConnection.getLocalStreams();if(e.length){for(var t,i=0;t=e[i];i++)for(var r,o=t[n](),a=0;r=o[a];a++)if(!r.enabled)return!1;return!0}}},set:function(e){function t(n){n.enabled=e}this.peerConnection.getLocalStreams().forEach(function(e){e[n]().forEach(t)})}}}function WebRtcPeerRecvonly(e,n){return this instanceof WebRtcPeerRecvonly?void WebRtcPeerRecvonly.super_.call(this,"recvonly",e,n):new WebRtcPeerRecvonly(e,n)}function WebRtcPeerSendonly(e,n){return this instanceof WebRtcPeerSendonly?void WebRtcPeerSendonly.super_.call(this,"sendonly",e,n):new WebRtcPeerSendonly(e,n)}function WebRtcPeerSendrecv(e,n){return this instanceof WebRtcPeerSendrecv?void WebRtcPeerSendrecv.super_.call(this,"sendrecv",e,n):new WebRtcPeerSendrecv(e,n)}function harkUtils(e,n){return hark(e,n)}var freeice=require("freeice"),inherits=require("inherits"),UAParser=require("ua-parser-js"),uuid=require("uuid"),hark=require("hark"),EventEmitter=require("events").EventEmitter,recursive=require("merge").recursive.bind(void 0,!0);try{require("kurento-browser-extensions")}catch(error){"undefined"==typeof getScreenConstraints&&(console.warn("screen sharing is not available"),getScreenConstraints=function(e,n){n(new Error("This library is not enabled for screen sharing"))})}var MEDIA_CONSTRAINTS={audio:!0,video:{width:640,framerate:15}},ua=window&&window.navigator?window.navigator.userAgent:"",parser=new UAParser(ua);inherits(WebRtcPeer,EventEmitter),Object.defineProperties(WebRtcPeer.prototype,{enabled:{enumerable:!0,get:function(){return this.audioEnabled&&this.videoEnabled},set:function(e){this.audioEnabled=this.videoEnabled=e}},audioEnabled:createEnableDescriptor("Audio"),videoEnabled:createEnableDescriptor("Video")}),WebRtcPeer.prototype.getLocalStream=function(e){return this.peerConnection?this.peerConnection.getLocalStreams()[e||0]:void 0},WebRtcPeer.prototype.getRemoteStream=function(e){return this.peerConnection?this.peerConnection.getRemoteStreams()[e||0]:void 0},WebRtcPeer.prototype.dispose=function(){console.log("Disposing WebRtcPeer");var e=this.peerConnection;try{if(e){if("closed"===e.signalingState)return;e.getLocalStreams().forEach(streamStop),e.close()}}catch(n){console.warn("Exception disposing webrtc peer "+n)}this.emit("_dispose")},inherits(WebRtcPeerRecvonly,WebRtcPeer),inherits(WebRtcPeerSendonly,WebRtcPeer),inherits(WebRtcPeerSendrecv,WebRtcPeer),exports.bufferizeCandidates=bufferizeCandidates,exports.WebRtcPeerRecvonly=WebRtcPeerRecvonly,exports.WebRtcPeerSendonly=WebRtcPeerSendonly,exports.WebRtcPeerSendrecv=WebRtcPeerSendrecv,exports.hark=harkUtils;
var _getDefaultTransports = function () { var transports = [] // udp transport if (UdpTransport.isCompatibleWithRuntime()) { _log.debug('using UDP transport') transports.push(new UdpTransport()) } // tcp transport if (TcpTransport.isCompatibleWithRuntime()) { _log.debug('using TCP transport') transports.push(new TcpTransport()) } // turn transport if (config.turnAddr !== undefined && config.turnPort !== undefined && config.turnUser !== undefined && config.turnPass !== undefined && config.onetpRegistrar !== undefined ) { // turn+tcp var turnConfig = { turnServer: config.turnAddr, turnPort: parseInt(config.turnPort), turnUsername: config.turnUser, turnPassword: config.turnPass, turnProtocol: new TurnProtocols.TCP(), signaling: new WebSocketSignaling({ url: config.onetpRegistrar }) } if (TurnTransport.isCompatibleWithRuntime(turnConfig)) { _log.debug('using TURN+TCP transport') transports.push(new TurnTransport(turnConfig)) } else { // turn+udp turnConfig.turnProtocol = new TurnProtocols.UDP() if (TurnTransport.isCompatibleWithRuntime(turnConfig)) { _log.debug('using TURN+UDP transport') transports.push(new TurnTransport(turnConfig)) } } } // webrtc transport if (WebRtcTransport.isCompatibleWithRuntime() && config.onetpRegistrar !== undefined ) { // add stun servers var iceServers = freeice() // add turn servers if (config.turnAddr !== undefined && config.turnPort !== undefined && config.turnUser !== undefined && config.turnPass !== undefined ) { var turnUrl = { url: 'turn:' + config.turnAddr + ':' + config.turnPort, username: config.turnUser, credential: config.turnPass } iceServers.push(turnUrl) } var webrtcConfig = { config: { iceServers: iceServers }, signaling: new WebSocketSignaling({ url: config.onetpRegistrar }) } _log.debug('using WebRtc transport') transports.push(new WebRtcTransport(webrtcConfig)) } return transports }
/** * Wrapper object of an RTCPeerConnection. This object is aimed to simplify the * development of WebRTC-based applications. * * @constructor module:kurentoUtils.WebRtcPeer * * @param {String} mode Mode in which the PeerConnection will be configured. * Valid values are: 'recv', 'send', and 'sendRecv' * @param localVideo Video tag for the local stream * @param remoteVideo Video tag for the remote stream * @param {MediaStream} videoStream Stream to be used as primary source * (typically video and audio, or only video if combined with audioStream) for * localVideo and to be added as stream to the RTCPeerConnection * @param {MediaStream} audioStream Stream to be used as second source * (typically for audio) for localVideo and to be added as stream to the * RTCPeerConnection */ function WebRtcPeer(mode, options, callback) { if (!(this instanceof WebRtcPeer)) return new WebRtcPeer(mode, options, callback) WebRtcPeer.super_.call(this) if (options instanceof Function) { callback = options options = undefined } options = options || {} callback = (callback || noop).bind(this) var localVideo = options.localVideo; var remoteVideo = options.remoteVideo; var videoStream = options.videoStream; var audioStream = options.audioStream; var mediaConstraints = options.mediaConstraints; var connectionConstraints = options.connectionConstraints; var pc = options.peerConnection var sendSource = options.sendSource || 'webcam' var configuration = recursive({ iceServers: freeice() }, options.configuration); var onicecandidate = options.onicecandidate; if (onicecandidate) this.on('icecandidate', onicecandidate); var oncandidategatheringdone = options.oncandidategatheringdone; if (oncandidategatheringdone) this.on('candidategatheringdone', oncandidategatheringdone); // Init PeerConnection if (!pc) pc = new RTCPeerConnection(configuration); Object.defineProperty(this, 'peerConnection', { get: function () { return pc; } }); /** * @member {(external:ImageData|undefined)} currentFrame */ Object.defineProperty(this, 'currentFrame', { get: function () { // [ToDo] Find solution when we have a remote stream but we didn't set // a remoteVideo tag if (!remoteVideo) return; if (remoteVideo.readyState < remoteVideo.HAVE_CURRENT_DATA) throw new Error('No video stream data available') var canvas = document.createElement('canvas') canvas.width = remoteVideo.videoWidth canvas.height = remoteVideo.videoHeight canvas.getContext('2d').drawImage(remoteVideo, 0, 0) return canvas } }); var self = this; var candidategatheringdone = false pc.addEventListener('icecandidate', function (event) { var candidate = event.candidate if (candidate) { self.emit('icecandidate', candidate); candidategatheringdone = false } else if (!candidategatheringdone) { self.emit('candidategatheringdone'); candidategatheringdone = true } }); var candidatesQueue = [] /** * Callback function invoked when an ICE candidate is received. Developers are * expected to invoke this function in order to complete the SDP negotiation. * * @function module:kurentoUtils.WebRtcPeer.prototype.addIceCandidate * * @param iceCandidate - Literal object with the ICE candidate description * @param callback - Called when the ICE candidate has been added. */ this.addIceCandidate = function (iceCandidate, callback) { var candidate = new RTCIceCandidate(iceCandidate); console.log('ICE candidate received'); callback = (callback || noop).bind(this) switch (pc.signalingState) { case 'closed': Callback(new Error('PeerConnection object is closed')) break case 'stable': if (pc.remoteDescription) { pc.addIceCandidate(candidate, callback, callback); break; } default: candidatesQueue.push({ candidate: candidate, callback: callback }) } } pc.addEventListener('signalingstatechange', function () { if (this.signalingState == 'stable') while (candidatesQueue.length) { var entry = candidatesQueue.shift() this.addIceCandidate(entry.candidate, entry.callback, entry.callback); } }) this.generateOffer = function (callback) { callback = callback.bind(this) var constraints = recursive({ offerToReceiveAudio: (mode !== 'sendonly'), offerToReceiveVideo: (mode !== 'sendonly') }, connectionConstraints); console.log('constraints: ' + JSON.stringify(constraints)); pc.createOffer(function (offer) { console.log('Created SDP offer'); pc.setLocalDescription(offer, function () { console.log('Local description set', offer.sdp); callback(null, offer.sdp, self.processAnswer.bind(self)); }, callback); }, callback, constraints); } this.getLocalSessionDescriptor = function () { return pc.localDescription } this.getRemoteSessionDescriptor = function () { return pc.remoteDescription } function setRemoteVideo() { if (remoteVideo) { var stream = pc.getRemoteStreams()[0] var url = stream ? URL.createObjectURL(stream) : ""; remoteVideo.src = url; console.log('Remote URL:', url) } } /** * Callback function invoked when a SDP answer is received. Developers are * expected to invoke this function in order to complete the SDP negotiation. * * @function module:kurentoUtils.WebRtcPeer.prototype.processAnswer * * @param sdpAnswer - Description of sdpAnswer * @param callback - Called when the remote description has been set * successfully. */ this.processAnswer = function (sdpAnswer, callback) { callback = (callback || noop).bind(this) var answer = new RTCSessionDescription({ type: 'answer', sdp: sdpAnswer, }); console.log('SDP answer received, setting remote description'); if (pc.signalingState == 'closed') return callback('PeerConnection is closed') pc.setRemoteDescription(answer, function () { setRemoteVideo() callback(); }, callback); } /** * Callback function invoked when a SDP offer is received. Developers are * expected to invoke this function in order to complete the SDP negotiation. * * @function module:kurentoUtils.WebRtcPeer.prototype.processOffer * * @param sdpOffer - Description of sdpOffer * @param callback - Called when the remote description has been set * successfully. */ this.processOffer = function (sdpOffer, callback) { callback = callback.bind(this) var offer = new RTCSessionDescription({ type: 'offer', sdp: sdpOffer, }); console.log('SDP offer received, setting remote description'); if (pc.signalingState == 'closed') return callback('PeerConnection is closed') pc.setRemoteDescription(offer, function () { setRemoteVideo() // Generate answer var constraints = recursive({ // offerToReceiveAudio: (mode !== 'sendonly'), // offerToReceiveVideo: (mode !== 'sendonly') }, connectionConstraints); console.log('constraints: ' + JSON.stringify(constraints)); pc.createAnswer(function (answer) { console.log('Created SDP answer'); pc.setLocalDescription(answer, function () { console.log('Local description set', answer.sdp); callback(null, answer.sdp); }, callback); }, callback, constraints); }, callback); } /** * This function creates the RTCPeerConnection object taking into account the * properties received in the constructor. It starts the SDP negotiation * process: generates the SDP offer and invokes the onsdpoffer callback. This * callback is expected to send the SDP offer, in order to obtain an SDP * answer from another peer. */ function start() { if (videoStream && localVideo) { localVideo.src = URL.createObjectURL(videoStream); localVideo.muted = true; } if (videoStream) pc.addStream(videoStream); if (audioStream) pc.addStream(audioStream); // [Hack] https://code.google.com/p/chromium/issues/detail?id=443558 if (mode == 'sendonly') mode = 'sendrecv'; // // Create the offer with the required constraints // self.generateOffer(callback) callback() } if (mode !== 'recvonly' && !videoStream && !audioStream) { function getMedia(constraints) { getUserMedia(recursive(MEDIA_CONSTRAINTS, constraints), function ( stream) { videoStream = stream; start() }, callback); } if (sendSource != 'webcam' && !mediaConstraints) getScreenConstraints(sendMode, function (error, constraints) { if (error) return callback(error) getMedia(constraints) }) else getMedia(mediaConstraints) } else { setTimeout(start, 0) } this.on('_dispose', function () { if (localVideo) localVideo.src = ''; if (remoteVideo) remoteVideo.src = ''; }) }
/** * Wrapper object of an RTCPeerConnection. This object is aimed to simplify the * development of WebRTC-based applications. * * @constructor module:kurentoUtils.WebRtcPeer * * @param {String} mode Mode in which the PeerConnection will be configured. * Valid values are: 'recv', 'send', and 'sendRecv' * @param localVideo Video tag for the local stream * @param remoteVideo Video tag for the remote stream * @param {MediaStream} videoStream Stream to be used as primary source * (typically video and audio, or only video if combined with audioStream) for * localVideo and to be added as stream to the RTCPeerConnection * @param {MediaStream} audioStream Stream to be used as second source * (typically for audio) for localVideo and to be added as stream to the * RTCPeerConnection */ function WebRtcPeer(mode, options, callback) { if (!(this instanceof WebRtcPeer)) { return new WebRtcPeer(mode, options, callback) } WebRtcPeer.super_.call(this) if (options instanceof Function) { callback = options options = undefined } options = options || {} callback = (callback || noop).bind(this) var localVideo = options.localVideo var remoteVideo = options.remoteVideo var videoStream = options.videoStream var audioStream = options.audioStream var mediaConstraints = options.mediaConstraints var connectionConstraints = options.connectionConstraints var pc = options.peerConnection var sendSource = options.sendSource || 'webcam' var guid = uuid.v4() var configuration = recursive({ iceServers: freeice() }, options.configuration) var onstreamended = options.onstreamended if (onstreamended) this.on('streamended', onstreamended) var onicecandidate = options.onicecandidate if (onicecandidate) this.on('icecandidate', onicecandidate) var oncandidategatheringdone = options.oncandidategatheringdone if (oncandidategatheringdone) { this.on('candidategatheringdone', oncandidategatheringdone) } var simulcast = options.simulcast // Init PeerConnection if (!pc) pc = new RTCPeerConnection(configuration) Object.defineProperties(this, { 'peerConnection': { get: function () { return pc } }, 'remoteVideo': { get: function () { return remoteVideo } }, 'localVideo': { get: function () { return localVideo } }, /** * @member {(external:ImageData|undefined)} currentFrame */ 'currentFrame': { get: function () { // [ToDo] Find solution when we have a remote stream but we didn't set // a remoteVideo tag if (!remoteVideo) return; if (remoteVideo.readyState < remoteVideo.HAVE_CURRENT_DATA) throw new Error('No video stream data available') var canvas = document.createElement('canvas') canvas.width = remoteVideo.videoWidth canvas.height = remoteVideo.videoHeight canvas.getContext('2d').drawImage(remoteVideo, 0, 0) return canvas } } }) var self = this var candidatesQueueOut = [] var candidategatheringdone = false pc.onicecandidate = function(event){ var candidate = event.candidate if (EventEmitter.listenerCount(self, 'icecandidate') || EventEmitter.listenerCount( self, 'candidategatheringdone')) { if (candidate) { self.emit('icecandidate', candidate) candidategatheringdone = false } else if (!candidategatheringdone) { self.emit('candidategatheringdone') candidategatheringdone = true } } else if (!candidategatheringdone) { // Not listening to 'icecandidate' or 'candidategatheringdone' events, queue // the candidate until one of them is listened candidatesQueueOut.push(candidate) if (!candidate) candidategatheringdone = true } }; this.on('newListener', function (event, listener) { if (event === 'icecandidate' || event === 'candidategatheringdone') { while (candidatesQueueOut.length) { var candidate = candidatesQueueOut.shift() if (!candidate === (event === 'candidategatheringdone')) { listener(candidate) } } } }) var addIceCandidate = bufferizeCandidates(pc) /** * Callback function invoked when an ICE candidate is received. Developers are * expected to invoke this function in order to complete the SDP negotiation. * * @function module:kurentoUtils.WebRtcPeer.prototype.addIceCandidate * * @param iceCandidate - Literal object with the ICE candidate description * @param callback - Called when the ICE candidate has been added. */ this.addIceCandidate = function (iceCandidate, callback) { var candidate = new RTCIceCandidate(iceCandidate) console.log('ICE candidate received') callback = (callback || noop).bind(this) addIceCandidate(candidate, callback) } this.generateOffer = function (callback) { callback = callback.bind(this) var browser = parser.getBrowser() var offerAudio = true var offerVideo = true // Constraints must have both blocks if (mediaConstraints) { offerAudio = (typeof mediaConstraints.audio === 'boolean') ? mediaConstraints.audio : true offerVideo = (typeof mediaConstraints.video === 'boolean') ? mediaConstraints.video : true } var browserDependantConstraints = (browser.name === 'Firefox' && browser.version > 34) ? { offerToReceiveAudio: (mode !== 'sendonly' && offerAudio), offerToReceiveVideo: (mode !== 'sendonly' && offerVideo) } : { mandatory: { OfferToReceiveAudio: (mode !== 'sendonly' && offerAudio), OfferToReceiveVideo: (mode !== 'sendonly' && offerVideo) }, optional: [{ DtlsSrtpKeyAgreement: true }] } var constraints = recursive(browserDependantConstraints, connectionConstraints) console.log('constraints: ' + JSON.stringify(constraints)) pc.createOffer(function (offer) { console.log('Created SDP offer') if (simulcast) { if ((browser.name === 'Chrome' || browser.name === 'Chromium')) { console.log('Adding multicast info') offer = new RTCSessionDescription({ 'type': offer.type, 'sdp': removeFIDFromOffer(offer.sdp) + getSimulcastInfo( videoStream) }); } else { console.warn('Simulcast is only available in Chrome browser.'); } } pc.setLocalDescription(offer, function () { console.log('Local description set', offer.sdp) callback(null, offer.sdp, self.processAnswer.bind(self)) }, callback) }, callback, constraints) } this.getLocalSessionDescriptor = function () { return pc.localDescription } this.getRemoteSessionDescriptor = function () { return pc.remoteDescription } function setRemoteVideo() { if (remoteVideo) { var stream = pc.getRemoteStreams()[0]; var url = stream; attachMediaStream(remoteVideo, stream); console.log('Remote URL:', url); } } this.showLocalVideo = function () { attachMediaStream(localVideo, videoStream); } /** * Callback function invoked when a SDP answer is received. Developers are * expected to invoke this function in order to complete the SDP negotiation. * * @function module:kurentoUtils.WebRtcPeer.prototype.processAnswer * * @param sdpAnswer - Description of sdpAnswer * @param callback - Called when the remote description has been set * successfully. */ this.processAnswer = function (sdpAnswer, callback) { callback = (callback || noop).bind(this) var answer = new RTCSessionDescription({ type: 'answer', sdp: sdpAnswer }) console.log('SDP answer received, setting remote description') if (pc.signalingState === 'closed') { return callback('PeerConnection is closed') } pc.setRemoteDescription(answer, function () { setRemoteVideo() callback() }, callback) } /** * Callback function invoked when a SDP offer is received. Developers are * expected to invoke this function in order to complete the SDP negotiation. * * @function module:kurentoUtils.WebRtcPeer.prototype.processOffer * * @param sdpOffer - Description of sdpOffer * @param callback - Called when the remote description has been set * successfully. */ this.processOffer = function (sdpOffer, callback) { callback = callback.bind(this) var offer = new RTCSessionDescription({ type: 'offer', sdp: sdpOffer }) console.log('SDP offer received, setting remote description') if (pc.signalingState === 'closed') { return callback('PeerConnection is closed') } pc.setRemoteDescription(offer, function () { setRemoteVideo() // Generate answer pc.createAnswer(function (answer) { console.log('Created SDP answer') pc.setLocalDescription(answer, function () { console.log('Local description set', answer.sdp) callback(null, answer.sdp) }, callback) }, callback) }, callback) } function streamEndedListener() { self.emit('streamended', this) } /** * This function creates the RTCPeerConnection object taking into account the * properties received in the constructor. It starts the SDP negotiation * process: generates the SDP offer and invokes the onsdpoffer callback. This * callback is expected to send the SDP offer, in order to obtain an SDP * answer from another peer. */ function start() { if (pc.signalingState === 'closed') { callback( 'The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue' ) } if (videoStream && localVideo) { self.showLocalVideo() } if (videoStream) { videoStream.onended = function(event){ streamEndedListener(); }; pc.addStream(videoStream) } if (audioStream) { audioStream.onended = function(event){ streamEndedListener(); }; pc.addStream(audioStream) } // [Hack] https://code.google.com/p/chromium/issues/detail?id=443558 var browser = parser.getBrowser() if (mode === 'sendonly' && (browser.name === 'Chrome' || browser.name === 'Chromium') && browser.major === 39) { mode = 'sendrecv' } callback() } if (mode !== 'recvonly' && !videoStream && !audioStream) { function getMedia(constraints) { if (constraints === undefined) { constraints = MEDIA_CONSTRAINTS } getUserMedia(constraints, function (stream) { videoStream = stream start() }, callback) } if (sendSource === 'webcam') { getMedia(mediaConstraints) } else { getScreenConstraints(sendSource, function (error, constraints_) { if (error) return callback(error) constraints = [mediaConstraints] constraints.unshift(constraints_) getMedia(recursive.apply(undefined, constraints)) }, guid) } } else { setTimeout(start, 0) } this.on('_dispose', function () { if (localVideo) { localVideo.pause() localVideo.src = '' localVideo.load() } if (remoteVideo) { remoteVideo.pause() remoteVideo.src = '' remoteVideo.load() } self.removeAllListeners() if (window.cancelChooseDesktopMedia !== undefined) { window.cancelChooseDesktopMedia(guid) } }) }