otalk-media-controller
Version:
Controller for managing local and remote media streams.
549 lines (453 loc) • 15.6 kB
JavaScript
var WebRTC = require('webrtcsupport');
var getUserMedia = require('getusermedia');
var getScreenMedia = require('getscreenmedia');
var State = require('ampersand-state');
var LodashMixin = require('ampersand-collection-lodash-mixin');
var Collection = require('ampersand-collection');
var FilteredCollection = require('ampersand-filtered-subcollection');
var SubCollection = FilteredCollection.extend(LodashMixin);
var Stream = require('otalk-model-media');
var DeviceManager = require('otalk-media-devices');
module.exports = State.extend({
props: {
defaultOptionalAudioConstraints: ['array', true],
defaultOptionalVideoConstraints: ['array', true],
capturingAudio: 'boolean',
capturingVideo: 'boolean',
capturingScreen: 'boolean',
detectSpeaking: ['boolean', true, true],
audioMonitoring: {
type: 'object',
required: true,
default: function () {
return {
threshold: -50,
interval: 100,
smoothing: 0.1
};
}
},
// Holding area for a local stream before adding it
// to our collection. Allows for the creation of
// "haircheck" preview UIs to let the user approve
// of the stream before we start using it.
preview: 'state',
// The various categories of streams
localStreams: 'collection',
localScreens: 'collection',
localVideoStreams: 'collection',
localAudioOnlyStreams: 'collection',
remoteStreams: 'collection',
remoteVideoStreams: 'collection',
remoteAudioOnlyStreams: 'collection',
// For multi-party applications using remote streams,
// we track "claimed" streams which are just streams
// that have a peer assigned as an owner.
claimedRemoteStreams: 'collection',
claimedRemoteVideoStreams: 'collection',
claimedRemoteAudioOnlyStreams: 'collection'
},
session: {
_localAudioCount: 'number',
_localVideoCount: 'number',
_localScreenCount: 'number'
},
children: {
devices: DeviceManager
},
collections: {
streams: Collection.extend({ model: Stream })
},
initialize: function () {
var self = this;
this.initializeSubCollections();
this.localVideoStreams.on('add remove reset', function () {
});
this.localStreams.bind('add remove reset', function () {
var updates = {
capturingAudio: false,
capturingVideo: false,
capturingScreeen: false
};
self.localStreams.forEach(function (stream) {
if (stream.hasAudio) {
updates.capturingAudio = true;
}
if (stream.hasVideo && !stream.isScreen) {
updates.capturingVideo = true;
}
if (stream.isScreen && stream.hasVideo) {
updates.capturingScreen = true;
}
});
self.set(updates);
});
this.localScreens.bind('add remove reset', function () {
self.capturingScreen = !!self.localScreens.length;
});
this.streams.bind('change:isEnded', function (stream) {
if (stream.isEnded) {
process.nextTick(function () {
self.streams.remove(stream.id);
});
}
});
},
initializeSubCollections: function () {
this.localStreams = new SubCollection(this.streams, {
where: {
isEnded: false,
isLocal: true
}
});
this.localScreens = new SubCollection(this.streams, {
where: {
isEnded: false,
isLocal: true,
isVideo: true,
isScreen: true
}
});
this.localVideoStreams = new SubCollection(this.streams, {
where: {
isEnded: false,
isLocal: true,
isVideo: true,
isScreen: false
}
});
this.localAudioOnlyStreams = new SubCollection(this.streams, {
where: {
isEnded: false,
isLocal: true,
isAudioOnly: true
}
});
this.remoteStreams = new SubCollection(this.streams, {
where: {
isEnded: false,
isRemote: true
}
});
this.remoteVideoStreams = new SubCollection(this.streams, {
where: {
isEnded: false,
isRemote: true,
isVideo: true
}
});
this.remoteAudioOnlyStreams = new SubCollection(this.streams, {
where: {
isEnded: false,
isRemote: true,
isAudioOnly: true
}
});
this.claimedRemoteStreams = new SubCollection(this.streams, {
where: {
isEnded: false,
isRemote: true,
isClaimed: true
}
});
this.claimedRemoteVideoStreams = new SubCollection(this.streams, {
where: {
isEnded: false,
isRemote: true,
isVideo: true,
isClaimed: true
}
});
this.claimedRemoteAudioOnlyStreams = new SubCollection(this.streams, {
where: {
isEnded: false,
isRemote: true,
isAudioOnly: true,
isClaimed: true
}
});
},
addLocalStream: function (stream, isScreen, opts) {
opts = opts || {};
if (stream.isState) {
stream.origin = 'local';
stream.isScreen = isScreen || false;
stream.session = opts.session;
return this.streams.add(stream);
} else {
return this.streams.add({
origin: 'local',
stream: stream,
isScreen: isScreen || false,
session: opts.session
});
}
},
addRemoteStream: function (stream, opts) {
opts = opts || {};
if (stream.isState) {
stream.origin = 'remote';
stream.session = opts.session;
stream.peer = opts.peer;
return this.streams.add(stream);
} else {
return this.streams.add({
origin: 'remote',
stream: stream,
session: opts.session,
peer: opts.peer
});
}
},
start: function (constraints, cb) {
var self = this;
cb = cb || function () {};
this._startStream(constraints, function (err, stream) {
if (err) {
return cb(err);
}
var localStream = self.addLocalStream(stream);
if (self.detectSpeaking && localStream.hasAudio) {
localStream.startVolumeMonitor(self.audioMonitoring);
}
cb(err, stream);
});
},
startScreenShare: function (constraints, cb) {
var self = this;
if (arguments.length === 1) {
cb = constraints;
constraints = {};
}
cb = cb || function () {};
constraints = this._prepConstraints(constraints);
getScreenMedia(function (err, stream) {
if (err) {
return cb(err);
}
if (constraints.audio) {
self._startStream({
audio: constraints.audio,
video: false
}, function (err, audioStream) {
if (err) {
return cb(err);
}
stream.addTrack(audioStream.getAudioTracks()[0]);
var localStream = self.addLocalStream(stream, true);
if (self.detectSpeaking && localStream.hasAudio) {
localStream.startVolumeMonitor(self.audioMonitoring);
}
cb(null, stream);
});
} else {
self.addLocalStream(stream, true);
cb(err, stream);
}
});
},
startPreview: function (constraints, cb) {
var self = this;
cb = cb || function () {};
this.stopPreview();
this._startStream(constraints, function (err, stream) {
if (err) {
return cb(err);
}
self.preview = new Stream({
origin: 'local',
stream: stream,
isScreen: false,
});
if (self.detectSpeaking && self.preview.hasAudio) {
self.preview.startVolumeMonitor(self.audioMonitoring);
}
cb(null, stream);
});
},
stop: function (stream) {
var self = this;
if (stream) {
stream = this.streams.get(stream.id);
if (stream) {
stream.stop();
}
} else {
var streams = this.localStreams.models;
this.localStreams.models = [];
streams.forEach(function (stream) {
stream.stop();
self.streams.remove(stream);
});
this.localStreams.trigger('reset');
this.stopScreenShare();
this.stopPreview();
}
},
stopScreenShare: function (stream) {
var self = this;
if (stream) {
stream = this.streams.get(stream.id);
if (stream) {
stream.stop();
}
} else {
var streams = this.localScreens.models;
this.localScreens.models = [];
streams.forEach(function (stream) {
stream.stop();
self.streams.remove(stream);
});
this.localScreens.trigger('reset');
}
},
stopPreview: function () {
if (this.preview) {
this.preview.stop();
this.unset('preview');
}
},
acceptPreview: function () {
if (this.preview) {
this.addLocalStream(this.preview);
this.unset('preview');
}
},
muteAudio: function () {
this.localStreams.forEach(function (stream) {
stream.muteAudio();
});
},
muteVideo: function () {
this.localStreams.forEach(function (stream) {
stream.muteVideo();
});
},
resumeAudio: function () {
this.localStreams.forEach(function (stream) {
stream.playAudio();
});
},
resumeVideo: function () {
this.localStreams.forEach(function (stream) {
stream.playVideo();
});
},
findStream: function (mediaStream) {
var matches = this.streams.filter(function (stream) {
return stream.stream === mediaStream || (stream.id && stream.id === mediaStream.id);
});
return matches[0];
},
ensureLocalStreams: function (constraints, cb) {
var check = constraints || {};
var existing = false;
var wantAudio = !!check.audio;
var wantVideo = !!check.video;
for (var i = 0, len = this.localStreams.length; i < len; i++) {
var stream = this.localStreams.at(i);
if (!stream.isScreen && wantAudio === stream.hasAudio && wantVideo === stream.hasVideo) {
existing = true;
break;
}
}
if (!existing) {
this.start(constraints, cb);
} else {
process.nextTick(cb);
}
},
_prepConstraints: function (constraints) {
if (!constraints) {
constraints = {
audio: this.useAudioWhenAvailable,
video: this.useVideoWhenAvailable
};
}
// Don't override if detailed constraints were explicitly given
if (constraints.audio === true) {
constraints.audio = {
optional: JSON.parse(JSON.stringify(this.defaultOptionalAudioConstraints))
};
if (this.devices.preferredMicrophone) {
if (WebRTC.prefix === 'webkit') {
constraints.audio.optional.push({
sourceId: this.devices.preferredMicrophone
});
} else {
constraints.audio.deviceId = this.devices.preferredMicrophone;
}
}
}
// Don't override if detailed constraints were explicitly given
if (constraints.video === true) {
constraints.video = {
optional: JSON.parse(JSON.stringify(this.defaultOptionalVideoConstraints))
};
if (this.devices.preferredCamera) {
if (WebRTC.prefix === 'webkit') {
constraints.video.optional.push({
sourceId: this.devices.preferredCamera
});
} else {
constraints.video.deviceId = this.devices.preferredCamera;
}
}
}
return constraints;
},
_startStream: function (constraints, cb) {
var self = this;
constraints = this._prepConstraints(constraints);
var transaction = {};
if (!!constraints.audio) {
transaction.wantMicrophoneAccess = true;
transaction.microphoneAccess = self.devices.requestMicrophoneAccess();
}
if (!!constraints.video) {
transaction.wantCameraAccess = true;
transaction.cameraAccess = self.devices.requestCameraAccess();
}
getUserMedia(constraints, function (err, stream) {
if (err) {
return self._handleError(err, transaction, cb);
}
if (stream.getAudioTracks().length > 0) {
transaction.microphoneAccess('granted');
} else if (transaction.wantMicrophoneAccess) {
transaction.microphoneAccess('error');
}
if (stream.getVideoTracks().length > 0) {
transaction.cameraAccess('granted');
} else if (transaction.wantCameraAccess) {
transaction.cameraAccess('error');
}
cb(err, stream);
});
},
_handleError: function (err, transaction, cb) {
function handleError(response) {
if (transaction.wantCameraAccess) {
transaction.cameraAccess(response);
}
if (transaction.wantMicrophoneAccess) {
transaction.microphoneAccess(response);
}
}
switch (err.name) {
case 'PermissionDeniedError':
handleError('denied');
break;
case 'PermissionDismissedError':
handleError('dismissed');
break;
case 'DevicesNotFoundError':
case 'ConstraintNotSatisfiedError':
case 'NotSupportedError':
case 'NoMediaRequestedError':
handleError('error');
break;
}
return cb(err);
}
});