Browse Source

Merge pulll request #138 from longsleep/renegotiation

* renegotiation:
  Disable enableRenegotiationSupport per default to make the stuff mergeable.
  Add pc registry to usermedia to trigger media updates to all pcs where the usermedia stream is used.
  Implemented negotiation workaround for Firefox.
  Implemented offer/answer creation on renegotiationneeded event.
  Implement workaround for FF not supporting onnegotiationneeded.
  Changed offer generation to generate offer when negotiation is required and signaling state is stable.
pull/112/head
Simon Eisenmann 12 years ago
parent
commit
2228d6bcee
  1. 75
      static/js/directives/audiovideo.js
  2. 20
      static/js/mediastream/peercall.js
  3. 7
      static/js/mediastream/peerconference.js
  4. 31
      static/js/mediastream/peerconnection.js
  5. 187
      static/js/mediastream/usermedia.js
  6. 104
      static/js/mediastream/webrtc.js
  7. 28
      static/js/services/videolayout.js
  8. 2
      static/partials/audiovideopeer.html

75
static/js/directives/audiovideo.js

@ -26,8 +26,12 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) { var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
var peers = {}; var streams = {};
var events = $({}); var getStreamId = function(stream, currentcall) {
var id = currentcall.id + "-" + stream.id;
console.log("Created stream ID", id);
return id;
};
$scope.container = $element.get(0); $scope.container = $element.get(0);
$scope.layoutparent = $element.parent(); $scope.layoutparent = $element.parent();
@ -39,6 +43,9 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
$scope.hasUsermedia = false; $scope.hasUsermedia = false;
$scope.isActive = false; $scope.isActive = false;
$scope.haveStreams = false;
$scope.peersTalking = {};
$scope.rendererName = $scope.defaultRendererName = "democrazy"; $scope.rendererName = $scope.defaultRendererName = "democrazy";
@ -46,31 +53,32 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
$scope.addRemoteStream = function(stream, currentcall) { $scope.addRemoteStream = function(stream, currentcall) {
//console.log("Add remote stream to scope", pc.id, stream); var id = getStreamId(stream, currentcall);
if (streams.hasOwnProperty(id)) {
console.warn("Cowardly refusing to add stream id twice", id, currentcall);
return;
}
//console.log("Add remote stream to scope", stream.id, stream, currentcall);
// Create scope. // Create scope.
var subscope = $scope.$new(true); var subscope = $scope.$new();
var peerid = subscope.peerid = currentcall.id; var peerid = subscope.peerid = currentcall.id;
buddyData.push(peerid); buddyData.push(peerid);
subscope.withvideo = false; subscope.withvideo = false;
subscope.onlyaudio = false; subscope.onlyaudio = false;
subscope.talking = false;
subscope.destroyed = false; subscope.destroyed = false;
subscope.applyTalking = function(talking) {
subscope.talking = !! talking;
safeApply(subscope);
};
subscope.$on("active", function() { subscope.$on("active", function() {
console.log("Stream scope is now active", peerid); console.log("Stream scope is now active", id, peerid);
events.triggerHandler("active." + peerid, [subscope, currentcall, stream]);
}); });
subscope.$on("$destroy", function() { subscope.$on("$destroy", function() {
console.log("Destroyed scope for audiovideo", subscope); console.log("Destroyed scope for stream", id, peerid);
subscope.destroyed = true; subscope.destroyed = true;
}); });
console.log("Created stream scope", peerid); console.log("Created stream scope", id, peerid);
// Add created scope. // Add created scope.
peers[peerid] = subscope; streams[id] = subscope;
// Render template. // Render template.
peerTemplate(subscope, function(clonedElement, scope) { peerTemplate(subscope, function(clonedElement, scope) {
@ -118,10 +126,13 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
$scope.removeRemoteStream = function(stream, currentcall) { $scope.removeRemoteStream = function(stream, currentcall) {
var subscope = peers[currentcall.id]; //console.log("remove stream", stream, stream.id, currentcall);
var id = getStreamId(stream, currentcall);
var subscope = streams[id];
if (subscope) { if (subscope) {
buddyData.pop(currentcall.id); buddyData.pop(currentcall.id);
delete peers[currentcall.id]; delete streams[id];
//console.log("remove scope", subscope); //console.log("remove scope", subscope);
if (subscope.element) { if (subscope.element) {
subscope.element.remove(); subscope.element.remove();
@ -134,17 +145,9 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
// Talking updates receiver. // Talking updates receiver.
mediaStream.api.e.on("received.talking", function(event, id, from, talking) { mediaStream.api.e.on("received.talking", function(event, id, from, talking) {
var scope = peers[from]; $scope.$apply(function(scope) {
//console.log("received.talking", talking, scope); scope.peersTalking[from] = !!talking;
if (scope) {
scope.applyTalking(talking);
} else {
console.log("Received talking state without scope -> adding event.", from, talking);
events.one("active." + from, function(event, scope) {
console.log("Applying previously received talking state", from, talking);
scope.applyTalking(talking);
}); });
}
}); });
$scope.$on("active", function(currentcall) { $scope.$on("active", function(currentcall) {
@ -178,6 +181,13 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
mediaStream.webrtc.e.on("usermedia", function(event, usermedia) { mediaStream.webrtc.e.on("usermedia", function(event, usermedia) {
//console.log("XXXX XXXXXXXXXXXXXXXXXXXXX usermedia event", usermedia); //console.log("XXXX XXXXXXXXXXXXXXXXXXXXX usermedia event", usermedia);
if ($scope.haveStreams) {
usermedia.attachMediaStream($scope.miniVideo);
$scope.redraw();
} else {
$scope.hasUsermedia = true; $scope.hasUsermedia = true;
usermedia.attachMediaStream($scope.localVideo); usermedia.attachMediaStream($scope.localVideo);
var count = 0; var count = 0;
@ -199,12 +209,15 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
}; };
waitForLocalVideo(); waitForLocalVideo();
}
}); });
mediaStream.webrtc.e.on("done", function() { mediaStream.webrtc.e.on("done", function() {
$scope.hasUsermedia = false; $scope.hasUsermedia = false;
$scope.isActive = false; $scope.isActive = false;
$scope.peersTalking = {};
if (BigScreen.enabled) { if (BigScreen.enabled) {
BigScreen.exit(); BigScreen.exit();
} }
@ -220,20 +233,22 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
$scope.localVideo.style.opacity = 0; $scope.localVideo.style.opacity = 0;
$scope.remoteVideos.style.opacity = 0; $scope.remoteVideos.style.opacity = 0;
$element.removeClass('active'); $element.removeClass('active');
_.each(peers, function(scope, k) { _.each(streams, function(scope, k) {
scope.$destroy(); scope.$destroy();
delete peers[k]; delete streams[k];
}); });
$scope.rendererName = $scope.defaultRendererName; $scope.rendererName = $scope.defaultRendererName;
$scope.haveStreams = false;
}); });
mediaStream.webrtc.e.on("streamadded", function(event, stream, currentcall) { mediaStream.webrtc.e.on("streamadded", function(event, stream, currentcall) {
console.log("Remote stream added.", stream, currentcall); console.log("Remote stream added.", stream, currentcall);
if (_.isEmpty(peers)) { if (!$scope.haveStreams) {
//console.log("First stream"); //console.log("First stream");
$window.reattachMediaStream($scope.miniVideo, $scope.localVideo); $window.reattachMediaStream($scope.miniVideo, $scope.localVideo);
$scope.haveStreams = true;
} }
$scope.addRemoteStream(stream, currentcall); $scope.addRemoteStream(stream, currentcall);
@ -247,7 +262,7 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
}); });
return { return {
peers: peers streams: streams
}; };
}]; }];

20
static/js/mediastream/peercall.js

@ -138,7 +138,7 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection
// after the remote SDP was set successfully. // after the remote SDP was set successfully.
_.defer(_.bind(function() { _.defer(_.bind(function() {
_.each(peerconnection.getRemoteStreams(), _.bind(function(stream) { _.each(peerconnection.getRemoteStreams(), _.bind(function(stream) {
if (!this.streams.hasOwnProperty(stream) && (stream.getAudioTracks().length > 0 || stream.getVideoTracks().length > 0)) { if (!this.streams.hasOwnProperty(stream.id) && (stream.getAudioTracks().length > 0 || stream.getVideoTracks().length > 0)) {
// NOTE(longsleep): Add stream here when it has at least one audio or video track, to avoid FF >= 33 to add it multiple times. // NOTE(longsleep): Add stream here when it has at least one audio or video track, to avoid FF >= 33 to add it multiple times.
console.log("Adding stream after remote SDP success.", stream); console.log("Adding stream after remote SDP success.", stream);
this.onRemoteStreamAdded(stream); this.onRemoteStreamAdded(stream);
@ -182,7 +182,11 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection
PeerCall.prototype.onRemoteStreamAdded = function(stream) { PeerCall.prototype.onRemoteStreamAdded = function(stream) {
this.streams[stream] = true; var id = stream.id;
if (this.streams.hasOwnProperty(id)) {
return;
}
this.streams[id] = stream;
this.e.triggerHandler("remoteStreamAdded", [stream, this]); this.e.triggerHandler("remoteStreamAdded", [stream, this]);
}; };
@ -191,16 +195,17 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection
this.e.triggerHandler("remoteStreamRemoved", [stream, this]); this.e.triggerHandler("remoteStreamRemoved", [stream, this]);
if (stream) { if (stream) {
delete this.streams[stream]; delete this.streams[stream.id];
} }
}; };
PeerCall.prototype.onNegotiationNeeded = function(peerconnection) { PeerCall.prototype.onNegotiationNeeded = function() {
if (!this.negotiationNeeded) { if (!this.negotiationNeeded) {
this.negotiationNeeded = true; this.negotiationNeeded = true;
console.log("Negotiation needed.", this); console.log("Negotiation needed.", this);
this.e.triggerHandler("negotiationNeeded", [this]);
} }
}; };
@ -298,13 +303,18 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection
datachannel.close(); datachannel.close();
}); });
this.datachannels = {}; this.datachannels = {};
this.streams = {};
if (this.peerconnection) { if (this.peerconnection) {
this.peerconnection.close(); this.peerconnection.close();
this.peerconnection = null; this.peerconnection = null;
} }
// Trigger event for all previously added streams.
_.each(this.streams, _.bind(function(stream, id) {
this.e.triggerHandler("remoteStreamRemoved", [stream, this]);
}, this));
this.streams = {};
console.log("Peercall close", this); console.log("Peercall close", this);
this.e.triggerHandler("closed", [this]); this.e.triggerHandler("closed", [this]);

7
static/js/mediastream/peerconference.js

@ -92,13 +92,16 @@ define(['underscore', 'mediastream/peercall'], function(_, PeerCall) {
console.log("Creating PeerConnection", call); console.log("Creating PeerConnection", call);
call.createPeerConnection(_.bind(function(peerconnection) { call.createPeerConnection(_.bind(function(peerconnection) {
// Success call. // Success call.
call.e.on("negotiationNeeded", _.bind(function(event, extracall) {
this.webrtc.sendOfferWhenNegotiationNeeded(extracall);
}, this));
if (this.webrtc.usermedia) { if (this.webrtc.usermedia) {
this.webrtc.usermedia.addToPeerConnection(peerconnection); this.webrtc.usermedia.addToPeerConnection(peerconnection);
} }
call.createOffer(_.bind(function(sessionDescription, extracall) { /*call.createOffer(_.bind(function(sessionDescription, extracall) {
console.log("Sending offer with sessionDescription", sessionDescription, extracall.id); console.log("Sending offer with sessionDescription", sessionDescription, extracall.id);
this.webrtc.api.sendOffer(extracall.id, sessionDescription); this.webrtc.api.sendOffer(extracall.id, sessionDescription);
}, this)); }, this));*/
}, this), _.bind(function() { }, this), _.bind(function() {
// Error call. // Error call.
console.error("Failed to create peer connection for conference call."); console.error("Failed to create peer connection for conference call.");

31
static/js/mediastream/peerconnection.js

@ -36,7 +36,7 @@ define(['jquery', 'underscore', 'webrtc.adapter'], function($, _) {
this.createPeerConnection(currentcall); this.createPeerConnection(currentcall);
} }
} };
PeerConnection.prototype.createPeerConnection = function(currentcall) { PeerConnection.prototype.createPeerConnection = function(currentcall) {
@ -70,11 +70,22 @@ define(['jquery', 'underscore', 'webrtc.adapter'], function($, _) {
// for example https://bugzilla.mozilla.org/show_bug.cgi?id=998546. // for example https://bugzilla.mozilla.org/show_bug.cgi?id=998546.
pc.onaddstream = _.bind(this.onRemoteStreamAdded, this); pc.onaddstream = _.bind(this.onRemoteStreamAdded, this);
pc.onremovestream = _.bind(this.onRemoteStreamRemoved, this); pc.onremovestream = _.bind(this.onRemoteStreamRemoved, this);
if (webrtcDetectedBrowser === "firefox") {
// NOTE(longsleep): onnegotiationneeded is not supported by Firefox. We trigger it
// manually when a stream is added or removed.
// https://bugzilla.mozilla.org/show_bug.cgi?id=840728
this.negotiationNeeded = _.bind(function() {
if (this.currentcall.initiate) {
// Trigger onNegotiationNeeded once for Firefox.
console.log("Negotiation needed.");
this.onNegotiationNeeded({target: this.pc});
}
}, this);
} else {
pc.onnegotiationneeded = _.bind(this.onNegotiationNeeded, this); pc.onnegotiationneeded = _.bind(this.onNegotiationNeeded, this);
}
pc.ondatachannel = _.bind(this.onDatachannel, this); pc.ondatachannel = _.bind(this.onDatachannel, this);
pc.onsignalingstatechange = function(event) { pc.onsignalingstatechange = function(event) {
// XXX(longsleep): Remove this or handle it in a real function.
// XXX(longsleep): Firefox 25 does send event as a string (like stable).
console.debug("Signaling state changed", pc.signalingState); console.debug("Signaling state changed", pc.signalingState);
}; };
// NOTE(longsleep): // NOTE(longsleep):
@ -111,6 +122,10 @@ define(['jquery', 'underscore', 'webrtc.adapter'], function($, _) {
}; };
PeerConnection.prototype.negotiationNeeded = function() {
// Per default this does nothing as the browser is expected to handle this.
};
PeerConnection.prototype.createDatachannel = function(label, init) { PeerConnection.prototype.createDatachannel = function(label, init) {
if (!label) { if (!label) {
@ -224,13 +239,9 @@ define(['jquery', 'underscore', 'webrtc.adapter'], function($, _) {
PeerConnection.prototype.onNegotiationNeeded = function(event) { PeerConnection.prototype.onNegotiationNeeded = function(event) {
// XXX(longsleep): Renegotiation seems to break video streams on Chrome 31.
// XXX(longsleep): Renegotiation can happen from both sides, meaning this
// could switch offer/answer side - oh crap.
var peerconnection = event.target; var peerconnection = event.target;
if (peerconnection === this.pc) { if (peerconnection === this.pc) {
//console.log("Negotiation needed.", peerconnection.remoteDescription, peerconnection.iceConnectionState, peerconnection.signalingState, this); this.currentcall.onNegotiationNeeded();
this.currentcall.onNegotiationNeeded(this);
} }
}; };
@ -244,8 +255,6 @@ define(['jquery', 'underscore', 'webrtc.adapter'], function($, _) {
this.pc.close(); this.pc.close();
} }
this.currentcall.onRemoteStreamRemoved(null);
this.datachannel = null; this.datachannel = null;
this.pc = null; this.pc = null;
@ -271,12 +280,14 @@ define(['jquery', 'underscore', 'webrtc.adapter'], function($, _) {
PeerConnection.prototype.addStream = function() { PeerConnection.prototype.addStream = function() {
_.defer(this.negotiationNeeded);
return this.pc.addStream.apply(this.pc, arguments); return this.pc.addStream.apply(this.pc, arguments);
}; };
PeerConnection.prototype.removeStream = function() { PeerConnection.prototype.removeStream = function() {
_.defer(this.negotiationNeeded);
return this.pc.removeStream.apply(this.pc, arguments); return this.pc.removeStream.apply(this.pc, arguments);
}; };

187
static/js/mediastream/usermedia.js

@ -22,6 +22,11 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _
// Create AudioContext singleton, if supported. // Create AudioContext singleton, if supported.
var context = AudioContext ? new AudioContext() : null; var context = AudioContext ? new AudioContext() : null;
var peerconnections = {};
// Disabled for now until browser support matures. If enabled this totally breaks
// Firefox and Chrome with Firefox interop.
var enableRenegotiationSupport = false;
var UserMedia = function(options) { var UserMedia = function(options) {
@ -32,9 +37,14 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _
this.started = false; this.started = false;
this.delay = 0; this.delay = 0;
this.audioMute = false;
this.videoMute = false;
this.mediaConstraints = null;
// Audio level. // Audio level.
this.audioLevel = 0; this.audioLevel = 0;
if (!this.options.noaudio && context && context.createScriptProcessor) { if (!this.options.noaudio && context && context.createScriptProcessor) {
this.audioSource = null; this.audioSource = null;
this.audioProcessor = context.createScriptProcessor(2048, 1, 1); this.audioProcessor = context.createScriptProcessor(2048, 1, 1);
this.audioProcessor.onaudioprocess = _.bind(function(event) { this.audioProcessor.onaudioprocess = _.bind(function(event) {
@ -54,8 +64,34 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _
this.audioLevel = rms; this.audioLevel = rms;
//console.log("this.audioLevel", this.audioLevel); //console.log("this.audioLevel", this.audioLevel);
}, this); }, this);
// Connect stream to audio processor if supported.
if (context.createMediaStreamSource) {
this.e.on("localstream", _.bind(function(event, stream) {
if (this.audioSource) {
this.audioSource.disconnect();
}
// Connect to audioProcessor.
this.audioSource = context.createMediaStreamSource(stream);
//console.log("got source", this.audioSource);
this.audioSource.connect(this.audioProcessor);
this.audioProcessor.connect(context.destination);
}, this));
} }
}
this.e.on("localstream", _.bind(function(event, stream, oldstream) {
// Update stream support.
if (oldstream) {
_.each(peerconnections, function(pc) {
pc.removeStream(oldstream);
pc.addStream(stream);
console.log("Updated usermedia stream at peer connection", pc, stream);
});
}
}, this));
}; };
// Static. // Static.
@ -112,11 +148,30 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _
if (!mediaConstraints) { if (!mediaConstraints) {
mediaConstraints = currentcall.mediaConstraints; mediaConstraints = currentcall.mediaConstraints;
} }
this.mediaConstraints = mediaConstraints;
return this.doGetUserMediaWithConstraints(mediaConstraints);
};
UserMedia.prototype.doGetUserMediaWithConstraints = function(mediaConstraints) {
if (!mediaConstraints) {
mediaConstraints = this.mediaConstraints;
}
var constraints = $.extend(true, {}, mediaConstraints);
if (this.audioMute) {
constraints.audio = false;
}
if (this.videoMute) {
constraints.video = false;
}
try { try {
console.log('Requesting access to local media with mediaConstraints:\n' + console.log('Requesting access to local media with mediaConstraints:\n' +
' \'' + JSON.stringify(mediaConstraints) + '\'', mediaConstraints); ' \'' + JSON.stringify(constraints) + '\'', constraints);
getUserMedia(mediaConstraints, _.bind(this.onUserMediaSuccess, this), _.bind(this.onUserMediaError, this)); getUserMedia(constraints, _.bind(this.onUserMediaSuccess, this), _.bind(this.onUserMediaError, this));
this.started = true; this.started = true;
return true; return true;
} catch (e) { } catch (e) {
@ -134,27 +189,7 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _
return; return;
} }
// Get notified of end events. this.onLocalStream(stream);
stream.onended = _.bind(function(event) {
console.log("User media stream ended.");
if (this.started) {
this.stop();
}
}, this);
if (this.audioProcessor && context.createMediaStreamSource) {
// Connect to audioProcessor.
this.audioSource = context.createMediaStreamSource(stream);
//console.log("got source", this.audioSource);
this.audioSource.connect(this.audioProcessor);
this.audioProcessor.connect(context.destination);
}
this.localStream = stream;
// Let webrtc handle the rest.
setTimeout(_.bind(function() {
this.e.triggerHandler("mediasuccess", [this]);
}, this), this.delay);
}; };
@ -170,6 +205,36 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _
}; };
UserMedia.prototype.onLocalStream = function(stream) {
var oldStream = this.localStream;
if (oldStream) {
oldStream.onended = function() {};
oldStream.stop();
setTimeout(_.bind(function() {
this.e.triggerHandler("mediachanged", [this]);
}, this), 0);
} else {
// Let webrtc handle the rest.
setTimeout(_.bind(function() {
this.e.triggerHandler("mediasuccess", [this]);
}, this), this.delay);
}
// Get notified of end events.
stream.onended = _.bind(function(event) {
console.log("User media stream ended.");
if (this.started) {
this.stop();
}
}, this);
// Set new stream.
this.localStream = stream;
this.e.triggerHandler("localstream", [stream, oldStream, this]);
};
UserMedia.prototype.stop = function() { UserMedia.prototype.stop = function() {
this.started = false; this.started = false;
@ -186,6 +251,9 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _
this.audioProcessor.disconnect() this.audioProcessor.disconnect()
} }
this.audioLevel = 0; this.audioLevel = 0;
this.audioMute = false;
this.videoMute = false;
this.mediaConstraints = null;
console.log("Stopped user media."); console.log("Stopped user media.");
this.e.triggerHandler("stopped", [this]); this.e.triggerHandler("stopped", [this]);
@ -198,6 +266,13 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _
UserMedia.prototype.applyAudioMute = function(mute) { UserMedia.prototype.applyAudioMute = function(mute) {
var m = !!mute;
if (!enableRenegotiationSupport) {
// Disable streams only - does not require renegotiation but keeps mic
// active and the stream will transmit silence.
if (this.localStream) { if (this.localStream) {
var audioTracks = this.localStream.getAudioTracks(); var audioTracks = this.localStream.getAudioTracks();
@ -206,45 +281,82 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _
return; return;
} }
for (i = 0; i < audioTracks.length; i++) { for (var i = 0; i < audioTracks.length; i++) {
audioTracks[i].enabled = !mute; audioTracks[i].enabled = !mute;
} }
if (mute) { if (mute) {
console.log("Local audio muted.") console.log("Local audio muted by disabling audio tracks.");
} else {
console.log("Local audio unmuted by enabling audio tracks.");
}
}
} else {
// Remove audio stream, by creating a new stream and doing renegotiation. This
// is the way to go to disable the mic when audio is muted.
if (this.localStream) {
if (this.audioMute !== m) {
this.audioMute = m;
this.doGetUserMediaWithConstraints();
}
} else { } else {
console.log("Local audio unmuted.") this.audioMute = m;
} }
} }
return mute; return m;
}; };
UserMedia.prototype.applyVideoMute = function(mute) { UserMedia.prototype.applyVideoMute = function(mute) {
if (this.localStream) { var m = !!mute;
if (!enableRenegotiationSupport) {
// Disable streams only - does not require renegotiation but keeps camera
// active and the stream will transmit black.
if (this.localStream) {
var videoTracks = this.localStream.getVideoTracks(); var videoTracks = this.localStream.getVideoTracks();
if (videoTracks.length === 0) { if (videoTracks.length === 0) {
//console.log('No local video available.'); //console.log('No local video available.');
return; return;
} }
for (i = 0; i < videoTracks.length; i++) { for (var i = 0; i < videoTracks.length; i++) {
videoTracks[i].enabled = !mute; videoTracks[i].enabled = !mute;
} }
if (mute) { if (mute) {
console.log("Local video muted.") console.log("Local video muted by disabling video tracks.");
} else { } else {
console.log("Local video unmuted.") console.log("Local video unmuted by enabling video tracks.");
}
}
} else {
// Removevideo stream, by creating a new stream and doing renegotiation. This
// is the way to go to disable the camera when video is muted.
if (this.localStream) {
if (this.videoMute !== m) {
this.videoMute = m;
this.doGetUserMediaWithConstraints();
}
} else {
this.videoMute = m;
} }
} }
return mute; return m;
}; };
@ -253,6 +365,13 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _
console.log("Add usermedia stream to peer connection", pc, this.localStream); console.log("Add usermedia stream to peer connection", pc, this.localStream);
if (this.localStream) { if (this.localStream) {
pc.addStream(this.localStream); pc.addStream(this.localStream);
var id = pc.id;
if (!peerconnections.hasOwnProperty(id)) {
peerconnections[id] = pc;
pc.currentcall.e.one("closed", function() {
delete peerconnections[id];
});
}
} }
}; };
@ -262,13 +381,15 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _
console.log("Remove usermedia stream from peer connection", pc, this.localStream); console.log("Remove usermedia stream from peer connection", pc, this.localStream);
if (this.localStream) { if (this.localStream) {
pc.removeStream(this.localStream); pc.removeStream(this.localStream);
if (peerconnections.hasOwnProperty(pc.id)) {
delete peerconnections[pc.id];
}
} }
}; };
UserMedia.prototype.attachMediaStream = function(video) { UserMedia.prototype.attachMediaStream = function(video) {
//console.log("attach", video, this.localStream);
attachMediaStream(video, this.localStream); attachMediaStream(video, this.localStream);
}; };

104
static/js/mediastream/webrtc.js

@ -117,6 +117,10 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
// Start always, no matter what. // Start always, no matter what.
this.maybeStart(); this.maybeStart();
}, this)); }, this));
this.usermedia.e.on("mediachanged", _.bind(function() {
// Propagate media change events.
this.e.triggerHandler("usermedia", [this.usermedia]);
}, this));
}; };
@ -226,32 +230,36 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
switch (type) { switch (type) {
case "Offer": case "Offer":
var busy = false; console.log("Offer process.");
var conference = null; if (this.settings.stereo) {
if (this.currentcall.from !== from) { data.sdp = utils.addStereo(data.sdp);
}
targetcall = this.findTargetCall(from);
if (targetcall) {
// Hey we know this call.
targetcall.setRemoteDescription(new RTCSessionDescription(data), _.bind(function(sessionDescription, currentcall) {
if (currentcall === this.currentcall) {
// Main call.
this.e.triggerHandler("peercall", [this.currentcall]);
}
currentcall.createAnswer(_.bind(function(sessionDescription, currentcall) {
console.log("Sending answer", sessionDescription, currentcall.id);
this.api.sendAnswer(currentcall.id, sessionDescription);
}, this));
}, this));
} else {
// No target call. Check conference auto answer support.
if (this.currentconference && this.currentconference.id === data._conference) { if (this.currentconference && this.currentconference.id === data._conference) {
console.log("Received conference Offer -> auto.", from, data._conference); console.log("Received conference Offer -> auto.", from, data._conference);
conference = data._conference; // Clean own internal data before feeding into browser.
// clean own internal data before feeding into browser.
delete data._conference; delete data._conference;
} else { this.currentconference.autoAnswer(from, new RTCSessionDescription(data));
console.log("Received Offer from unknown id -> busy.", from, this.currentconference); break;
busy = true;
}
} }
if (busy) { // Cannot do anything with this offer, reply with busy.
console.log("Received Offer from unknown id -> busy.", from);
this.api.sendBye(from, "busy"); this.api.sendBye(from, "busy");
this.e.triggerHandler("busy", [from, to2, to]); this.e.triggerHandler("busy", [from, to2, to]);
return;
}
console.log("Offer process.");
if (this.settings.stereo) {
data.sdp = utils.addStereo(data.sdp);
}
if (conference) {
this.currentconference.autoAnswer(from, new RTCSessionDescription(data));
} else {
this.currentcall.setRemoteDescription(new RTCSessionDescription(data), _.bind(this.doAnswer, this));
} }
break; break;
case "Candidate": case "Candidate":
@ -280,7 +288,10 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
} }
// TODO(longsleep): In case of negotiation this could switch offer and answer // TODO(longsleep): In case of negotiation this could switch offer and answer
// and result in a offer sdp sent as answer data. We need to handle this. // and result in a offer sdp sent as answer data. We need to handle this.
targetcall.setRemoteDescription(new RTCSessionDescription(data)); targetcall.setRemoteDescription(new RTCSessionDescription(data), function() {
// Received remote description as answer.
console.log("Received answer after we sent offer", data);
});
break; break;
case "Bye": case "Bye":
targetcall = this.findTargetCall(from); targetcall = this.findTargetCall(from);
@ -425,16 +436,6 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
}; };
WebRTC.prototype.doAnswer = function() {
this.e.triggerHandler("peercall", [this.currentcall]);
this.currentcall.createAnswer(_.bind(function(sessionDescription, currentcall) {
console.log("Sending answer", sessionDescription, currentcall.id);
this.api.sendAnswer(currentcall.id, sessionDescription);
}, this));
};
WebRTC.prototype.doXfer = function(id, token, options) { WebRTC.prototype.doXfer = function(id, token, options) {
var registeredToken = tokens.get(token); var registeredToken = tokens.get(token);
@ -482,12 +483,17 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
// Connect. // Connect.
xfer.setInitiate(true); xfer.setInitiate(true);
xfer.createPeerConnection(); xfer.createPeerConnection(_.bind(function() {
xfer.e.on("negotiationNeeded", _.bind(function(event, currentxfer) {
this.sendOfferWhenNegotiationNeeded(currentxfer, id);
}, this));
}, this));
/*
xfer.createOffer(_.bind(function(sessionDescription, currentxfer) { xfer.createOffer(_.bind(function(sessionDescription, currentxfer) {
console.log("Sending xfer offer with sessionDescription", sessionDescription, currentxfer.id); console.log("Sending xfer offer with sessionDescription", sessionDescription, currentxfer.id);
// TODO(longsleep): Support sending this through data channel too if we have one. // TODO(longsleep): Support sending this through data channel too if we have one.
this.api.sendOffer(id, sessionDescription); this.api.sendOffer(id, sessionDescription);
}, this)); }, this));*/
}; };
@ -553,12 +559,17 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
// Connect. // Connect.
peerscreenshare.setInitiate(true); //XXX(longsleep): This creates a data channel which is not needed. peerscreenshare.setInitiate(true); //XXX(longsleep): This creates a data channel which is not needed.
peerscreenshare.createPeerConnection(); peerscreenshare.createPeerConnection(_.bind(function() {
peerscreenshare.e.on("negotiationNeeded", _.bind(function(event, currentscreenshare) {
this.sendOfferWhenNegotiationNeeded(currentscreenshare, id);
}, this));
}, this));
/*
peerscreenshare.createOffer(_.bind(function(sessionDescription, currentscreenshare) { peerscreenshare.createOffer(_.bind(function(sessionDescription, currentscreenshare) {
console.log("Sending screen share offer with sessionDescription", sessionDescription, currentscreenshare.id); console.log("Sending screen share offer with sessionDescription", sessionDescription, currentscreenshare.id);
// TODO(longsleep): Support sending this through data channel too if we have one. // TODO(longsleep): Support sending this through data channel too if we have one.
this.api.sendOffer(id, sessionDescription); this.api.sendOffer(id, sessionDescription);
}, this)); }, this));*/
}; };
@ -637,13 +648,16 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
} }
this.started = true; this.started = true;
if (this.initiator) { if (this.initiator) {
currentcall.createOffer(_.bind(function(sessionDescription, currentcall) { /*currentcall.createOffer(_.bind(function(sessionDescription, currentcall) {
console.log("Sending offer with sessionDescription", sessionDescription, currentcall.id); console.log("Sending offer with sessionDescription", sessionDescription, currentcall.id);
this.api.sendOffer(currentcall.id, sessionDescription); this.api.sendOffer(currentcall.id, sessionDescription);
}, this)); }, this));*/
} else { } else {
this.calleeStart(); this.calleeStart();
} }
currentcall.e.on("negotiationNeeded", _.bind(function(event, currentcall) {
this.sendOfferWhenNegotiationNeeded(currentcall);
}, this));
}, this), _.bind(function() { }, this), _.bind(function() {
// Error call. // Error call.
this.e.triggerHandler("error", ["Failed to create peer connection. See log for details."]); this.e.triggerHandler("error", ["Failed to create peer connection. See log for details."]);
@ -664,6 +678,22 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
}; };
WebRTC.prototype.sendOfferWhenNegotiationNeeded = function(currentcall, to) {
// TODO(longsleep): Check if the check for stable is really required.
if (currentcall.peerconnection.pc.signalingState === "stable") {
if (!to) {
to = currentcall.id;
}
currentcall.createOffer(_.bind(function(sessionDescription, currentcall) {
console.log("Sending offer with sessionDescription", sessionDescription, to, currentcall);
// TODO(longsleep): Support sending this through data channel too if we have one.
this.api.sendOffer(to, sessionDescription);
}, this));
}
};
WebRTC.prototype.onConnectionStateChange = function(iceConnectionState, currentcall) { WebRTC.prototype.onConnectionStateChange = function(iceConnectionState, currentcall) {
// Defer this to allow native event handlers to complete before running more stuff. // Defer this to allow native event handlers to complete before running more stuff.
_.defer(_.bind(function() { _.defer(_.bind(function() {

28
static/js/services/videolayout.js

@ -23,14 +23,14 @@ define(["jquery", "underscore", "modernizr", "injectCSS"], function($, _, Modern
var dynamicCSSContainer = "audiovideo-dynamic"; var dynamicCSSContainer = "audiovideo-dynamic";
var renderers = {}; var renderers = {};
var getRemoteVideoSize = function(videos, peers) { var getRemoteVideoSize = function(videos, streams) {
var size = { var size = {
width: 1920, width: 1920,
height: 1080 height: 1080
} }
if (videos.length) { if (videos.length) {
if (videos.length === 1) { if (videos.length === 1) {
var remoteVideo = peers[videos[0]].element.find("video").get(0); var remoteVideo = streams[videos[0]].element.find("video").get(0);
if (remoteVideo) { if (remoteVideo) {
size.width = remoteVideo.videoWidth; size.width = remoteVideo.videoWidth;
size.height = remoteVideo.videoHeight; size.height = remoteVideo.videoHeight;
@ -51,7 +51,7 @@ define(["jquery", "underscore", "modernizr", "injectCSS"], function($, _, Modern
OnePeople.prototype.name = "onepeople"; OnePeople.prototype.name = "onepeople";
OnePeople.prototype.render = function(container, size, scope, videos, peers) { OnePeople.prototype.render = function(container, size, scope, videos, streams) {
if (this.closed) { if (this.closed) {
return; return;
@ -61,7 +61,7 @@ define(["jquery", "underscore", "modernizr", "injectCSS"], function($, _, Modern
var videoHeight; var videoHeight;
if (videos.length) { if (videos.length) {
var remoteSize = getRemoteVideoSize(videos, peers); var remoteSize = getRemoteVideoSize(videos, streams);
videoWidth = remoteSize.width; videoWidth = remoteSize.width;
videoHeight = remoteSize.height; videoHeight = remoteSize.height;
} }
@ -235,25 +235,25 @@ define(["jquery", "underscore", "modernizr", "injectCSS"], function($, _, Modern
}; };
ConferenceKiosk.prototype.render = function(container, size, scope, videos, peers) { ConferenceKiosk.prototype.render = function(container, size, scope, videos, streams) {
var big = this.big; var big = this.big;
if (big) { if (big) {
var currentbigpeerid = this.big.data("peerid"); var currentbigpeerid = this.big.data("peerid");
if (!peers[currentbigpeerid]) { if (!streams[currentbigpeerid]) {
console.log("Current big peer is no longer there", currentbigpeerid); console.log("Current big peer is no longer there", currentbigpeerid);
this.big = big = null; this.big = big = null;
} }
} }
if (!big) { if (!big) {
if (videos.length) { if (videos.length) {
this.makeBig(peers[videos[0]].element); this.makeBig(streams[videos[0]].element);
this.bigVideo.style.opacity = 1; this.bigVideo.style.opacity = 1;
} }
} }
var remoteSize = getRemoteVideoSize(videos, peers); var remoteSize = getRemoteVideoSize(videos, streams);
var aspectRatio = remoteSize.width / remoteSize.height; var aspectRatio = remoteSize.width / remoteSize.height;
var innerHeight = size.height - 110; var innerHeight = size.height - 110;
var innerWidth = size.width; var innerWidth = size.width;
@ -304,18 +304,18 @@ define(["jquery", "underscore", "modernizr", "injectCSS"], function($, _, Modern
Classroom.prototype = Object.create(ConferenceKiosk.prototype); Classroom.prototype = Object.create(ConferenceKiosk.prototype);
Classroom.prototype.constructor = Classroom; Classroom.prototype.constructor = Classroom;
Classroom.prototype.name = "classroom"; Classroom.prototype.name = "classroom";
Classroom.prototype.render = function(container, size, scope, videos, peers) { Classroom.prototype.render = function(container, size, scope, videos, streams) {
var big = this.big; var big = this.big;
if (big) { if (big) {
var currentbigpeerid = this.big.data("peerid"); var currentbigpeerid = this.big.data("peerid");
if (!peers[currentbigpeerid]) { if (!streams[currentbigpeerid]) {
console.log("Current big peer is no longer there", currentbigpeerid); console.log("Current big peer is no longer there", currentbigpeerid);
this.big = big = null; this.big = big = null;
} }
} }
if (!big) { if (!big) {
if (videos.length) { if (videos.length) {
this.makeBig(peers[videos[0]].element); this.makeBig(streams[videos[0]].element);
this.bigVideo.style.opacity = 1; this.bigVideo.style.opacity = 1;
} }
@ -345,8 +345,8 @@ define(["jquery", "underscore", "modernizr", "injectCSS"], function($, _, Modern
return r; return r;
}; };
var videos = _.keys(controller.peers); var videos = _.keys(controller.streams);
var peers = controller.peers; var streams = controller.streams;
var container = scope.container; var container = scope.container;
var layoutparent = scope.layoutparent; var layoutparent = scope.layoutparent;
@ -370,7 +370,7 @@ define(["jquery", "underscore", "modernizr", "injectCSS"], function($, _, Modern
} }
} }
return current.render(container, size, scope, videos, peers); return current.render(container, size, scope, videos, streams);
}, },
register: function(name, impl) { register: function(name, impl) {

2
static/partials/audiovideopeer.html

@ -1,4 +1,4 @@
<div class="remoteVideo" ng-class="{'withvideo': withvideo, 'onlyaudio': onlyaudio, 'talking': talking}"> <div class="remoteVideo" ng-class="{'withvideo': withvideo, 'onlyaudio': onlyaudio, 'talking': peersTalking[peerid]}">
<video autoplay="autoplay"></video> <video autoplay="autoplay"></video>
<div class="peerLabel">{{peerid|displayName}}</div> <div class="peerLabel">{{peerid|displayName}}</div>
<div class="peerActions"> <div class="peerActions">

Loading…
Cancel
Save