"use strict";
// Copyright 2012 United States Government, as represented by the Secretary of Defense, Under
// Secretary of Defense (Personnel & Readiness).
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
// in compliance with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions and limitations under
// the License.
/// @module vwf/view/webrtc
/// @requires vwf/view
define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ], function( module, view, utility, Color, $ ) {
return view.load( module, {
// == Module Definition ====================================================================
initialize: function( options ) {
if ( !this.state ) {
this.state = {};
}
this.state.clients = {};
this.state.instances = {};
this.local = {
"ID": undefined,
"url": undefined,
"stream": undefined,
"sharing": { audio: true, video: true }
};
// turns on logger debugger console messages
this.debugVwf = {
"creation": false,
"initializing": false,
"parenting": false,
"deleting": false,
"properties": false,
"setting": false,
"getting": false,
"calling": false
};
if ( options === undefined ) { options = {}; }
this.stereo = options.stereo !== undefined ? options.stereo : false;
this.videoElementsDiv = options.videoElementsDiv !== undefined ? options.videoElementsDiv : 'videoSurfaces';
this.videoProperties = options.videoProperties !== undefined ? options.videoProperties : {};
this.bandwidth = options.bandwidth;
this.iceServers = options.iceServers !== undefined ? options.iceServers : [ { "url": "stun:stun.l.google.com:19302" } ];
this.debug = options.debug !== undefined ? options.debug : false;
this.videosAdded = 0;
this.msgQueue = [];
},
createdNode: function( nodeID, childID, childExtendsID, childImplementsIDs,
childSource, childType, childIndex, childName, callback /* ( ready ) */ ) {
if ( this.debugVwf.creation ) {
this.kernel.logger.infox( "createdNode", nodeID, childID, childExtendsID, childImplementsIDs, childSource, childType, childName );
}
if ( childExtendsID === undefined )
return;
var self = this, node;
var protos = getPrototypes.call( self, childExtendsID )
if ( isClientInstanceDef.call( this, protos ) && childName ) {
node = {
"parentID": nodeID,
"ID": childID,
"extendsID": childExtendsID,
"implementsIDs": childImplementsIDs,
"source": childSource,
"type": childType,
"name": childName,
"prototypes": protos,
};
this.state.instances[ childID ] = node;
} else if ( isClientDefinition.call( this, protos ) && childName ) {
// check if this instance of client and if this client is for this instance
// create a login for this
node = {
"parentID": nodeID,
"ID": childID,
"moniker": undefined,
"extendsID": childExtendsID,
"implementsIDs": childImplementsIDs,
"source": childSource,
"type": childType,
"name": childName,
"prototypes": protos,
"displayName": "",
"connection": undefined,
"localUrl": undefined,
"remoteUrl": undefined,
//"color": "rgb(0,0,0)",
"createProperty": true,
"sharing": { audio: true, video: true }
};
this.state.clients[ childID ] = node;
// add the client specific locals
node.moniker = appMoniker.call( this, childName );
//console.info( "new client moniker: " + node.moniker );
node.displayName = undefined;
node.prototypes = protos;
if ( this.kernel.moniker() == node.moniker ) {
this.local.ID = childID;
if ( this.videoElementsDiv ) {
$('body').append(
"
"
);
}
}
}
},
initializedNode: function( nodeID, childID, childExtendsID, childImplementsIDs,
childSource, childType, childIndex, childName ) {
if ( this.debugVwf.initializing ) {
this.kernel.logger.infox( "initializedNode", nodeID, childID, childExtendsID, childImplementsIDs, childSource, childType, childName );
}
if ( childExtendsID === undefined )
return;
var client = this.state.clients[ childID ];
if ( client ) {
if ( this.local.ID == childID ){
// local client object
// grab access to the webcam
capture.call( this, this.local.sharing );
var remoteClient = undefined;
// existing clients
for ( var clientID in this.state.clients ) {
if ( clientID != this.local.ID ) {
// create property for this client on each existing client
remoteClient = this.state.clients[ clientID ];
if ( remoteClient.createProperty ) {
//console.info( "++ 1 ++ createProperty( "+clientID+", "+this.kernel.moniker()+" )" );
remoteClient.createProperty = false;
this.kernel.createProperty( clientID, this.kernel.moniker() );
}
}
}
} else {
// not the local client, but if the local client has logged
// in create the property for this on the new client
if ( this.local.ID ) {
if ( client.createProperty ) {
client.createProperty = false;
//console.info( "++ 2 ++ createProperty( "+childID+", "+this.kernel.moniker()+" )" );
this.kernel.createProperty( childID, this.kernel.moniker() );
}
}
}
}
},
deletedNode: function( nodeID ) {
if ( this.debugVwf.deleting ) {
this.kernel.logger.infox( "deletedNode", nodeID );
}
// debugger;
//if ( this.kernel.find( nodeID, "parent::element(*,'http://vwf.example.com/clients.vwf')" ).length > 0 ) {
//if ( this.kernel.find( nodeID ).length > 0 ) {
var moniker = nodeID.slice(-20);//this.kernel.name( nodeID );
var client = undefined;
if ( moniker == this.kernel.moniker() ) {
// this is the client that has left the converstaion
// go through the peerConnections and close the
// all current connections
var peer, peerMoniker;
for ( var peerID in this.state.clients ) {
peer = this.state.clients[ peerID ];
peerMoniker = appMoniker.call( this, peer.name )
if ( peerMoniker != this.kernel.moniker ) {
peer.connection && peer.connection.disconnect();
}
}
} else {
// this is a client who has has a peer leave the converstaion
// remove that client, and the
client = findClientByMoniker.call( this, moniker );
if ( client ) {
client.connection && client.connection.disconnect();
removeClient.call( this, client );
delete this.state.clients[ client ]
}
}
//}
},
createdProperty: function( nodeID, propertyName, propertyValue ) {
if ( this.debugVwf.properties ) {
this.kernel.logger.infox( "C === createdProperty ", nodeID, propertyName, propertyValue );
}
this.satProperty( nodeID, propertyName, propertyValue );
},
initializedProperty: function( nodeID, propertyName, propertyValue ) {
if ( this.debugVwf.properties ) {
this.kernel.logger.infox( " I === initializedProperty ", nodeID, propertyName, propertyValue );
}
this.satProperty( nodeID, propertyName, propertyValue );
},
satProperty: function( nodeID, propertyName, propertyValue ) {
if ( this.debugVwf.properties || this.debugVwf.setting ) {
this.kernel.logger.infox( " S === satProperty ", nodeID, propertyName, propertyValue );
}
var client = this.state.clients[ nodeID ];
if ( client ) {
switch( propertyName ) {
case "sharing":
if ( propertyValue ) {
client.sharing = propertyValue;
if ( nodeID == this.local.ID ) {
updateSharing.call( this, nodeID, propertyValue );
}
}
break;
case "localUrl":
if ( propertyValue ) {
if ( nodeID != this.local.ID ) {
client.localUrl = propertyValue;
}
}
break;
case "remoteUrl":
if ( propertyValue ) {
client.remoteUrl = propertyValue;
}
break;
case "displayName":
if ( propertyValue ) {
client.displayName = propertyValue;
}
break;
case "color":
var clr = new utility.color( propertyValue );
if ( clr ) {
client.color = clr.toString();
}
break;
default:
// propertyName is the moniker of the client that
// this connection supports
if ( nodeID == this.local.ID ) {
if ( propertyValue ) {
// propertyName - moniker of the client
// propertyValue - peerConnection message
handlePeerMessage.call( this, propertyName, propertyValue );
}
}
break;
}
}
},
gotProperty: function( nodeID, propertyName, propertyValue ) {
if ( this.debugVwf.properties || this.debugVwf.getting ) {
this.kernel.logger.infox( " G === gotProperty ", nodeID, propertyName, propertyValue );
}
var value = undefined;
return value;
},
calledMethod: function( nodeID, methodName, methodParameters, methodValue ) {
if ( this.debugVwf.calling ) {
this.kernel.logger.infox( " CM === calledMethod ", nodeID, methodName, methodParameters );
}
switch ( methodName ) {
case "setLocalMute":
if ( this.kernel.moniker() == this.kernel.client() ) {
methodValue = setMute.call( this, methodParameters );
}
break;
}
},
firedEvent: function( nodeID, eventName, eventParameters ) {
},
} );
function createVideoElementAsAsset(id) {
var video = document.querySelector('#' + id);
if (!video) {
video = document.createElement('video');
}
video.setAttribute('id', id);
video.setAttribute('autoplay', true);
video.setAttribute('src', '');
video.setAttribute("webkit-playsinline", true);
video.setAttribute("controls", true);
video.setAttribute("width", 640);
video.setAttribute("height", 480);
var assets = document.querySelector('a-assets');
// if (!assets) {
// assets = document.createElement('a-assets');
// document.querySelector('a-scene').appendChild(assets);
// }
if (!assets.contains(video)) {
assets.appendChild(video);
}
return video;
}
function getPrototypes( extendsID ) {
var prototypes = [];
var id = extendsID;
while ( id !== undefined ) {
prototypes.push( id );
id = this.kernel.prototype( id );
}
return prototypes;
}
function getPeer( moniker ) {
var clientNode;
for ( var id in this.state.clients ) {
if ( this.state.clients[id].moniker == moniker ) {
clientNode = this.state.clients[id];
break;
}
}
return clientNode;
}
function displayLocal( stream, name, color ) {
var id = this.kernel.moniker();
return displayVideo.call( this, id, stream, this.local.url, name, id, true, color );
}
function displayVideo( id, stream, url, name, destMoniker, muted, color ) {
var divId = undefined;
if ( this.videoProperties.create ) {
this.videosAdded++
var $container;
divId = name + this.videosAdded;
var videoId = "video-" + divId;
$container = $( "#" + this.videoElementsDiv );
if ( muted ) {
$container.append(
"" +
"" +
"
"
);
} else {
$container.append(
"" +
"" +
"
"
);
}
var videoE = $( '#'+ videoId )[0];
if ( videoE && stream ) {
navigator.attachMediaStream( videoE, stream );
if ( muted ) {
videoE.muted = true; // firefox isn't mapping the muted property correctly
}
}
var divEle = $('#'+divId);
divEle && divEle.draggable && divEle.draggable();
}
var clr = new utility.color( color );
if ( clr ) {
clr = clr.toArray();
}
this.kernel.callMethod( this.kernel.application(), "createVideo", [ {
"ID": id,
"url": url,
"name": name,
"muted": muted,
"color": clr ? clr : color
}, destMoniker ] );
let video = createVideoElementAsAsset(name);
video.srcObject = stream;
this.kernel.callMethod( 'avatar-'+id, "setVideoTexture", [name]);
return divId;
}
function removeVideo( client ) {
if ( client.videoDivID ) {
var $videoWin = $( "#" + client.videoDivID );
if ( $videoWin ) {
$videoWin.remove();
}
client.videoDivID = undefined;
}
this.kernel.callMethod( this.kernel.application(), "removeVideo", [ client.moniker ] );
}
function displayRemote( id, stream, url, name, destMoniker, color ) {
return displayVideo.call( this, id, stream, url, name, destMoniker, false, color );
}
function capture( media ) {
if ( this.local.stream === undefined && ( media.video || media.audio ) ) {
var self = this;
var constraints = {
"audio": media.audio,
"video": media.video ? { "mandatory": {}, "optional": [] } : false,
};
var successCallback = function( stream ) {
self.local.url = URL.createObjectURL( stream );
self.local.stream = stream;
self.kernel.setProperty( self.local.ID, "localUrl", self.local.url );
var localNode = self.state.clients[ self.local.ID ];
displayLocal.call( self, stream, localNode.displayName, localNode.color );
sendOffers.call( self );
};
var errorCallback = function( error ) {
console.log("failed to get video stream error: " + error);
};
try {
navigator.getUserMedia( constraints, successCallback, errorCallback );
} catch (e) {
console.log("getUserMedia: error " + e );
};
}
}
function appMoniker( name ) {
return name.substr( 7, name.length-1 );
}
function findClientByMoniker( moniker ) {
var client = undefined;
for ( var id in this.state.clients ) {
if ( client === undefined && moniker == this.state.clients[ id ].moniker ) {
client = this.state.clients[ id ];
}
}
return client;
}
function removeClient( client ) {
if ( client ) {
removeVideo.call( this, client );
}
}
function sendOffers() {
var peerNode;
for ( var id in this.state.clients ) {
if ( id != this.local.ID ) {
peerNode = this.state.clients[ id ];
// if there's a url then connect
if ( peerNode.localUrl && peerNode.localUrl != "" && peerNode.connection === undefined ) {
createPeerConnection.call( this, peerNode, true );
}
}
}
}
function updateSharing( nodeID, sharing ) {
setMute.call( this, !sharing.audio );
setPause.call( this, !sharing.video );
}
function setMute( mute ) {
if ( this.local.stream && this.local.stream.audioTracks && this.local.stream.audioTracks.length > 0 ) {
if ( mute !== undefined ) {
this.local.stream.audioTracks[0].enabled = !mute;
}
}
};
function setPause( pause ) {
if ( this.local.stream && this.local.stream.videoTracks && this.local.stream.videoTracks.length > 0 ) {
if ( pause !== undefined ) {
this.local.stream.videoTracks[0].enabled = !pause;
}
}
}
function release() {
for ( id in this.connections ) {
this.connections[id].disconnect();
}
this.connections = {};
}
function hasStream() {
return ( this.stream !== undefined );
}
function createPeerConnection( peerNode, sendOffer ) {
if ( peerNode ) {
if ( peerNode.connection === undefined ) {
peerNode.connection = new mediaConnection( this, peerNode );
peerNode.connection.connect( this.local.stream, sendOffer );
//if ( this.bandwidth !== undefined ) {
// debugger;
//}
}
}
}
function handlePeerMessage( propertyName, msg ) {
var peerNode = getPeer.call( this, propertyName )
if ( peerNode ) {
if ( peerNode.connection !== undefined ) {
peerNode.connection.processMessage( msg );
} else {
if ( msg.type === 'offer' ) {
this.msgQueue.unshift( msg );
peerNode.connection = new mediaConnection( this, peerNode );
peerNode.connection.connect( this.local.stream, false );
while ( this.msgQueue.length > 0 ) {
peerNode.connection.processMessage( this.msgQueue.shift() );
}
this.msgQueue = [];
} else {
this.msgQueue.push( msg );
}
}
}
}
function deletePeerConnection( peerID ) {
var peerNode = this.state.clients[ peerID ];
if ( peerNode ) {
peerNode.connection.disconnect();
peerNode.connection = undefined;
}
}
function getConnectionStats() {
var peerNode = undefined;
for ( var id in this.state.clients ) {
peerNode = this.state.clients[ id ];
if ( peerNode && peerNode.connection ) {
peerNode.connection.getStats();
}
}
}
function isClientDefinition( prototypes ) {
var foundClient = false;
if ( prototypes ) {
var len = prototypes.length;
for ( var i = 0; i < len && !foundClient; i++ ) {
foundClient = ( prototypes[i] == "http://vwf.example.com/aframe/avatar.vwf" );
}
}
return foundClient;
}
function isClientInstanceDef( nodeID ) {
return ( nodeID == "http://vwf.example.com/clients.vwf" );
}
function mediaConnection( view, peerNode ) {
this.view = view;
this.peerNode = peerNode;
//
this.stream = undefined;
this.url = undefined;
this.pc = undefined;
this.connected = false;
this.streamAdded = false;
this.state = "created";
// webrtc peerConnection parameters
this.pc_config = { "iceServers": this.view.iceServers };
this.pc_constraints = { "optional": [ { "DtlsSrtpKeyAgreement": true } ] };
// Set up audio and video regardless of what devices are present.
this.sdpConstraints = { 'mandatory': {
'OfferToReceiveAudio':true,
'OfferToReceiveVideo':true }};
this.connect = function( stream, sendOffer ) {
var self = this;
if ( this.pc === undefined ) {
if ( this.view.debug ) console.log("Creating PeerConnection.");
var iceCallback = function( event ) {
//console.log( "------------------------ iceCallback ------------------------" );
if ( event.candidate ) {
var sMsg = {
"type": 'candidate',
"label": event.candidate.sdpMLineIndex,
"id": event.candidate.sdpMid,
"candidate": event.candidate.candidate
};
// each client creates a property for each other
// the message value is broadcast via the property
self.view.kernel.setProperty( self.peerNode.ID, self.view.kernel.moniker(), sMsg );
} else {
if ( self.view.debug ) console.log("End of candidates.");
}
};
// if ( webrtcDetectedBrowser == "firefox" ) {
// this.pc_config = {"iceServers":[{"url":"stun:23.21.150.121"}]};
// }
try {
this.pc = new RTCPeerConnection( this.pc_config, this.pc_constraints );
this.pc.onicecandidate = iceCallback;
if ( self.view.debug ) console.log("Created RTCPeerConnnection with config \"" + JSON.stringify( this.pc_config ) + "\".");
} catch (e) {
console.log("Failed to create PeerConnection, exception: " + e.message);
alert("Cannot create RTCPeerConnection object; WebRTC is not supported by this browser.");
return;
}
this.pc.onnegotiationeeded = function( event ) {
//debugger;
//console.info( "onnegotiationeeded." );
}
this.pc.onaddstream = function( event ) {
if ( self.view.debug ) console.log("Remote stream added.");
self.stream = event.stream;
self.url = URL.createObjectURL( event.stream );
if ( self.view.debug ) console.log("Remote stream added. url: " + self.url );
var divID = displayRemote.call( self.view, self.peerNode.moniker, self.stream, self.url, self.peerNode.displayName, view.kernel.moniker(), self.peerNode.color );
if ( divID !== undefined ) {
self.peerNode.videoDivID = divID;
}
};
this.pc.onremovestream = function( event ) {
if ( self.view.debug ) console.log("Remote stream removed.");
};
this.pc.onsignalingstatechange = function() {
//console.info( "onsignalingstatechange state change." );
}
this.pc.oniceconnectionstatechange = function( ) {
if ( self && self.pc ) {
var state = self.pc.signalingState || self.pc.readyState;
//console.info( "peerConnection state change: " + state );
}
}
if ( stream ) {
if ( this.view.debug ) console.log("Adding local stream.");
this.pc.addStream( stream );
this.streamAdded = true;
}
if ( sendOffer ){
this.call();
}
}
this.connected = ( this.pc !== undefined );
};
this.setMute = function( mute ) {
if ( this.stream && this.stream.audioTracks && this.stream.audioTracks.length > 0 ) {
if ( mute !== undefined ) {
this.stream.audioTracks[0].enabled = !mute;
}
}
}
this.setPause = function( pause ) {
if ( this.stream && this.stream.videoTracks && this.stream.videoTracks.length > 0 ) {
if ( pause !== undefined ) {
this.stream.videoTracks[0].enabled = !pause;
}
}
}
this.disconnect = function() {
if ( this.view.debug ) console.log( "PC.disconnect " + this.peerID );
if ( this.pc ) {
this.pc.close();
this.pc = undefined;
}
};
this.processMessage = function( msg ) {
//var msg = JSON.parse(message);
if ( this.view.debug ) console.log('S->C: ' + JSON.stringify(msg) );
if ( this.pc ) {
if ( msg.type === 'offer') {
if ( this.view.stereo ) {
msg.sdp = addStereo( msg.sdp );
}
this.pc.setRemoteDescription( new RTCSessionDescription( msg ) );
this.answer();
} else if ( msg.type === 'answer' && this.streamAdded ) {
if ( this.view.stereo ) {
msg.sdp = addStereo( msg.sdp );
}
this.pc.setRemoteDescription( new RTCSessionDescription( msg ) );
} else if ( msg.type === 'candidate' && this.streamAdded ) {
var candidate = new RTCIceCandidate( {
"sdpMLineIndex": msg.label,
"candidate": msg.candidate
} );
this.pc.addIceCandidate( candidate );
} else if ( msg.type === 'bye' && this.streamAdded ) {
this.hangup();
}
}
};
this.answer = function() {
if ( this.view.debug ) console.log( "Send answer to peer" );
var self = this;
var answerer = function( sessionDescription ) {
// Set Opus as the preferred codec in SDP if Opus is present.
sessionDescription.sdp = self.preferOpus( sessionDescription.sdp );
sessionDescription.sdp = self.setBandwidth( sessionDescription.sdp );
self.pc.setLocalDescription( sessionDescription );
self.view.kernel.setProperty( self.peerNode.ID, self.view.kernel.moniker(), sessionDescription );
};
this.pc.createAnswer( answerer, null, this.sdpConstraints);
};
this.call = function() {
var self = this;
var constraints = {
"optional": [],
"mandatory": {}
};
// temporary measure to remove Moz* constraints in Chrome
if ( navigator.webrtcDetectedBrowser === "chrome" ) {
for ( var prop in constraints.mandatory ) {
if ( prop.indexOf("Moz") != -1 ) {
delete constraints.mandatory[ prop ];
}
}
}
constraints = this.mergeConstraints( constraints, this.sdpConstraints );
if ( this.view.debug ) console.log("Sending offer to peer, with constraints: \n" + " \"" + JSON.stringify(constraints) + "\".")
var offerer = function( sessionDescription ) {
// Set Opus as the preferred codec in SDP if Opus is present.
sessionDescription.sdp = self.preferOpus( sessionDescription.sdp );
sessionDescription.sdp = self.setBandwidth( sessionDescription.sdp );
self.pc.setLocalDescription( sessionDescription );
//sendSignalMessage.call( sessionDescription, self.peerID );
self.view.kernel.setProperty( self.peerNode.ID, self.view.kernel.moniker(), sessionDescription );
};
var onFailure = function(e) {
console.log(e)
}
this.pc.createOffer( offerer, onFailure, constraints );
};
this.setBandwidth = function( sdp ) {
// apparently this only works in chrome
if ( this.bandwidth === undefined || moz ) {
return sdp;
}
// remove existing bandwidth lines
sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, '');
if ( this.bandwidth.audio ) {
sdp = sdp.replace(/a=mid:audio\r\n/g, 'a=mid:audio\r\nb=AS:' + this.bandwidth.audio + '\r\n');
}
if ( this.bandwidth.video ) {
sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + this.bandwidth.video + '\r\n');
}
if ( this.bandwidth.data /*&& !options.preferSCTP */ ) {
sdp = sdp.replace(/a=mid:data\r\n/g, 'a=mid:data\r\nb=AS:' + this.bandwidth.data + '\r\n');
}
return sdp;
}
this.getStats = function(){
if ( this.pc && this.pc.getStats ) {
console.info( "pc.iceConnectionState = " + this.pc.iceConnectionState );
console.info( " pc.iceGatheringState = " + this.pc.iceGatheringState );
console.info( " pc.readyState = " + this.pc.readyState );
console.info( " pc.signalingState = " + this.pc.signalingState );
var consoleStats = function( obj ) {
console.info( ' Timestamp:' + obj.timestamp );
if ( obj.id ) {
console.info( ' id: ' + obj.id );
}
if ( obj.type ) {
console.info( ' type: ' + obj.type );
}
if ( obj.names ) {
var names = obj.names();
for ( var i = 0; i < names.length; ++i ) {
console.info( " "+names[ i ]+": " + obj.stat( names[ i ] ) );
}
} else {
if ( obj.stat && obj.stat( 'audioOutputLevel' ) ) {
console.info( " audioOutputLevel: " + obj.stat( 'audioOutputLevel' ) );
}
}
};
// local function
var readStats = function( stats ) {
var results = stats.result();
var bitrateText = 'No bitrate stats';
for ( var i = 0; i < results.length; ++i ) {
var res = results[ i ];
console.info( 'Report ' + i );
if ( !res.local || res.local === res ) {
consoleStats( res );
// The bandwidth info for video is in a type ssrc stats record
// with googFrameHeightReceived defined.
// Should check for mediatype = video, but this is not
// implemented yet.
if ( res.type == 'ssrc' && res.stat( 'googFrameHeightReceived' ) ) {
var bytesNow = res.stat( 'bytesReceived' );
if ( timestampPrev > 0) {
var bitRate = Math.round( ( bytesNow - bytesPrev ) * 8 / ( res.timestamp - timestampPrev ) );
bitrateText = bitRate + ' kbits/sec';
}
timestampPrev = res.timestamp;
bytesPrev = bytesNow;
}
} else {
// Pre-227.0.1445 (188719) browser
if ( res.local ) {
console.info( " Local: " );
consoleStats( res.local );
}
if ( res.remote ) {
console.info( " Remote: " );
consoleStats( res.remote );
}
}
}
console.info( " bitrate: " + bitrateText )
}
this.pc.getStats( readStats );
}
}
this.hangup = function() {
if ( this.view.debug ) console.log( "PC.hangup " + this.id );
if ( this.pc ) {
this.pc.close();
this.pc = undefined;
}
};
this.mergeConstraints = function( cons1, cons2 ) {
var merged = cons1;
for (var name in cons2.mandatory) {
merged.mandatory[ name ] = cons2.mandatory[ name ];
}
merged.optional.concat( cons2.optional );
return merged;
}
// Set Opus as the default audio codec if it's present.
this.preferOpus = function( sdp ) {
var sdpLines = sdp.split( '\r\n' );
// Search for m line.
for ( var i = 0; i < sdpLines.length; i++ ) {
if ( sdpLines[i].search( 'm=audio' ) !== -1 ) {
var mLineIndex = i;
break;
}
}
if ( mLineIndex === null ) {
return sdp;
}
// for ( var i = 0; i < sdpLines.length; i++ ) {
// if ( i == 0 ) console.info( "=============================================" );
// console.info( "sdpLines["+i+"] = " + sdpLines[i] );
// }
// If Opus is available, set it as the default in m line.
for ( var i = 0; i < sdpLines.length; i++ ) {
if ( sdpLines[i].search( 'opus/48000' ) !== -1 ) {
var opusPayload = this.extractSdp( sdpLines[i], /:(\d+) opus\/48000/i );
if ( opusPayload) {
sdpLines[ mLineIndex ] = this.setDefaultCodec( sdpLines[ mLineIndex ], opusPayload );
}
break;
}
}
// Remove CN in m line and sdp.
sdpLines = this.removeCN( sdpLines, mLineIndex );
sdp = sdpLines.join('\r\n');
return sdp;
}
// Set Opus in stereo if stereo is enabled.
function addStereo( sdp ) {
var sdpLines = sdp.split('\r\n');
// Find opus payload.
for (var i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].search('opus/48000') !== -1) {
var opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i);
break;
}
}
// Find the payload in fmtp line.
for (var i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].search('a=fmtp') !== -1) {
var payload = extractSdp(sdpLines[i], /a=fmtp:(\d+)/ );
if (payload === opusPayload) {
var fmtpLineIndex = i;
break;
}
}
}
// No fmtp line found.
if (fmtpLineIndex === null)
return sdp;
// Append stereo=1 to fmtp line.
sdpLines[fmtpLineIndex] = sdpLines[fmtpLineIndex].concat(' stereo=1');
sdp = sdpLines.join('\r\n');
return sdp;
}
// Strip CN from sdp before CN constraints is ready.
this.removeCN = function( 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 = this.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;
}
this.extractSdp = function( sdpLine, pattern ) {
var result = sdpLine.match( pattern );
return ( result && result.length == 2 ) ? result[ 1 ] : null;
}
// Set the selected codec to the first in m line.
this.setDefaultCodec = function( mLine, payload ) {
var elements = mLine.split( ' ' );
var newLine = new Array();
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( ' ' );
}
}
} );