зеркало из https://github.com/nextcloud/spreed.git
Merge pull request #1524 from nextcloud/add-support-for-calls-in-other-browsers
Add support for calls in other browsers
This commit is contained in:
Коммит
cd90f8a43a
|
@ -1,7 +1,7 @@
|
|||
/js/admin/*
|
||||
/js/tests/*
|
||||
/js/vendor/*
|
||||
/js/simplewebrtc.js
|
||||
/js/simplewebrtc/*
|
||||
/js/**/templates.js
|
||||
/karma.conf.js
|
||||
/tests/*
|
||||
|
|
6
Makefile
6
Makefile
|
@ -34,6 +34,12 @@ install-npm-deps-dev:
|
|||
compile-handlebars-templates: dev-setup
|
||||
bash compile-handlebars-templates.sh
|
||||
|
||||
bundle-simplewebrtc: dev-setup
|
||||
# webrtc-adapter uses JavaScript features not supported by browserify,
|
||||
# so the sources need to be transformed using babel to a compatible
|
||||
# version of JavaScript.
|
||||
npx browserify --standalone SimpleWebRTC --transform [ babelify --global --presets [ @babel/env ] ] js/simplewebrtc/simplewebrtc.js > js/simplewebrtc/bundled.js
|
||||
|
||||
dev-setup: install-npm-deps-dev
|
||||
|
||||
appstore: clean install-deps
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"vendor/jshashes/hashes.min.js",
|
||||
"vendor/Caret.js/dist/jquery.caret.min.js",
|
||||
"vendor/At.js/dist/js/jquery.atwho.min.js",
|
||||
"simplewebrtc/bundled.js",
|
||||
"models/chatmessage.js",
|
||||
"models/chatmessagecollection.js",
|
||||
"models/room.js",
|
||||
|
@ -21,7 +22,6 @@
|
|||
"views/templates.js",
|
||||
"views/videoview.js",
|
||||
"views/virtuallist.js",
|
||||
"simplewebrtc.js",
|
||||
"webrtc.js",
|
||||
"signaling.js",
|
||||
"connection.js",
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"vendor/jshashes/hashes.min.js",
|
||||
"vendor/Caret.js/dist/jquery.caret.min.js",
|
||||
"vendor/At.js/dist/js/jquery.atwho.min.js",
|
||||
"simplewebrtc/bundled.js",
|
||||
"models/chatmessage.js",
|
||||
"models/chatmessagecollection.js",
|
||||
"models/localstoragemodel.js",
|
||||
|
@ -30,7 +31,6 @@
|
|||
"views/templates.js",
|
||||
"views/videoview.js",
|
||||
"views/virtuallist.js",
|
||||
"simplewebrtc.js",
|
||||
"webrtc.js",
|
||||
"signaling.js",
|
||||
"connection.js",
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"vendor/jshashes/hashes.min.js",
|
||||
"vendor/Caret.js/dist/jquery.caret.min.js",
|
||||
"vendor/At.js/dist/js/jquery.atwho.min.js",
|
||||
"simplewebrtc/bundled.js",
|
||||
"models/chatmessage.js",
|
||||
"models/chatmessagecollection.js",
|
||||
"models/localstoragemodel.js",
|
||||
|
@ -30,7 +31,6 @@
|
|||
"views/templates.js",
|
||||
"views/videoview.js",
|
||||
"views/virtuallist.js",
|
||||
"simplewebrtc.js",
|
||||
"webrtc.js",
|
||||
"signaling.js",
|
||||
"connection.js",
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"vendor/jshashes/hashes.min.js",
|
||||
"vendor/Caret.js/dist/jquery.caret.min.js",
|
||||
"vendor/At.js/dist/js/jquery.atwho.min.js",
|
||||
"simplewebrtc/bundled.js",
|
||||
"models/chatmessage.js",
|
||||
"models/chatmessagecollection.js",
|
||||
"models/room.js",
|
||||
|
@ -29,7 +30,6 @@
|
|||
"views/templates.js",
|
||||
"views/videoview.js",
|
||||
"views/virtuallist.js",
|
||||
"simplewebrtc.js",
|
||||
"webrtc.js",
|
||||
"signaling.js",
|
||||
"connection.js",
|
||||
|
|
18562
js/simplewebrtc.js
18562
js/simplewebrtc.js
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,182 @@
|
|||
// getScreenMedia helper by @HenrikJoreteg
|
||||
var getUserMedia = function(constraints, callback) {
|
||||
window.navigator.mediaDevices.getUserMedia(constraints).then(function(stream) {
|
||||
callback(null, stream);
|
||||
}).catch(function(error) {
|
||||
callback(error, null);
|
||||
});
|
||||
};
|
||||
|
||||
// cache for constraints and callback
|
||||
var cache = {};
|
||||
|
||||
module.exports = function (mode, constraints, cb) {
|
||||
var hasConstraints = arguments.length === 3;
|
||||
var callback = hasConstraints ? cb : constraints;
|
||||
var error;
|
||||
|
||||
if (typeof window === 'undefined' || window.location.protocol === 'http:') {
|
||||
error = new Error('NavigatorUserMediaError');
|
||||
error.name = 'HTTPS_REQUIRED';
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if (navigator.webkitGetUserMedia) {
|
||||
var chromever = parseInt(window.navigator.userAgent.match(/Chrome\/(\d+)\./)[1], 10);
|
||||
var maxver = 33;
|
||||
// Chrome 71 dropped support for "window.chrome.webstore;".
|
||||
var isCef = (chromever < 71) && !window.chrome.webstore;
|
||||
// "known" crash in chrome 34 and 35 on linux
|
||||
if (window.navigator.userAgent.match('Linux')) maxver = 35;
|
||||
|
||||
// check that the extension is installed by looking for a
|
||||
// sessionStorage variable that contains the extension id
|
||||
// this has to be set after installation unless the contest
|
||||
// script does that
|
||||
if (sessionStorage.getScreenMediaJSExtensionId) {
|
||||
chrome.runtime.sendMessage(sessionStorage.getScreenMediaJSExtensionId,
|
||||
{type:'getScreen', id: 1}, null,
|
||||
function (data) {
|
||||
if (!data || data.sourceId === '') { // user canceled
|
||||
var error = new Error('NavigatorUserMediaError');
|
||||
error.name = 'PERMISSION_DENIED';
|
||||
callback(error);
|
||||
} else {
|
||||
constraints = (hasConstraints && constraints) || {audio: false, video: {
|
||||
mandatory: {
|
||||
chromeMediaSource: 'desktop',
|
||||
maxWidth: window.screen.width,
|
||||
maxHeight: window.screen.height,
|
||||
maxFrameRate: 3
|
||||
}
|
||||
}};
|
||||
constraints.video.mandatory.chromeMediaSourceId = data.sourceId;
|
||||
getUserMedia(constraints, callback);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else if (window.cefGetScreenMedia) {
|
||||
//window.cefGetScreenMedia is experimental - may be removed without notice
|
||||
window.cefGetScreenMedia(function(sourceId) {
|
||||
if (!sourceId) {
|
||||
var error = new Error('cefGetScreenMediaError');
|
||||
error.name = 'CEF_GETSCREENMEDIA_CANCELED';
|
||||
callback(error);
|
||||
} else {
|
||||
constraints = (hasConstraints && constraints) || {audio: false, video: {
|
||||
mandatory: {
|
||||
chromeMediaSource: 'desktop',
|
||||
maxWidth: window.screen.width,
|
||||
maxHeight: window.screen.height,
|
||||
maxFrameRate: 3
|
||||
},
|
||||
optional: [
|
||||
{googLeakyBucket: true},
|
||||
{googTemporalLayeredScreencast: true}
|
||||
]
|
||||
}};
|
||||
constraints.video.mandatory.chromeMediaSourceId = sourceId;
|
||||
getUserMedia(constraints, callback);
|
||||
}
|
||||
});
|
||||
} else if (isCef || (chromever >= 26 && chromever <= maxver)) {
|
||||
// chrome 26 - chrome 33 way to do it -- requires bad chrome://flags
|
||||
// note: this is basically in maintenance mode and will go away soon
|
||||
constraints = (hasConstraints && constraints) || {
|
||||
video: {
|
||||
mandatory: {
|
||||
googLeakyBucket: true,
|
||||
maxWidth: window.screen.width,
|
||||
maxHeight: window.screen.height,
|
||||
maxFrameRate: 3,
|
||||
chromeMediaSource: 'screen'
|
||||
}
|
||||
}
|
||||
};
|
||||
getUserMedia(constraints, callback);
|
||||
} else {
|
||||
// chrome 34+ way requiring an extension
|
||||
var pending = window.setTimeout(function () {
|
||||
error = new Error('NavigatorUserMediaError');
|
||||
error.name = 'EXTENSION_UNAVAILABLE';
|
||||
return callback(error);
|
||||
}, 1000);
|
||||
cache[pending] = [callback, hasConstraints ? constraints : null];
|
||||
window.postMessage({ type: 'getScreen', id: pending }, '*');
|
||||
}
|
||||
} else if (window.navigator.userAgent.match('Firefox')) {
|
||||
var ffver = parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10);
|
||||
if (ffver >= 52) {
|
||||
mode = mode || 'window';
|
||||
constraints = (hasConstraints && constraints) || {
|
||||
video: {
|
||||
mozMediaSource: mode,
|
||||
mediaSource: mode
|
||||
}
|
||||
};
|
||||
getUserMedia(constraints, function (err, stream) {
|
||||
callback(err, stream);
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
// workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1045810
|
||||
var lastTime = stream.currentTime;
|
||||
var polly = window.setInterval(function () {
|
||||
if (!stream) window.clearInterval(polly);
|
||||
if (stream.currentTime == lastTime) {
|
||||
window.clearInterval(polly);
|
||||
if (stream.onended) {
|
||||
stream.onended();
|
||||
}
|
||||
}
|
||||
lastTime = stream.currentTime;
|
||||
}, 500);
|
||||
});
|
||||
} else {
|
||||
error = new Error('NavigatorUserMediaError');
|
||||
error.name = 'FF52_REQUIRED';
|
||||
return callback(error);
|
||||
}
|
||||
} else if (navigator.mediaDevices && navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) {
|
||||
navigator.mediaDevices.getDisplayMedia({video: true}).then(function(stream) {
|
||||
callback(null, stream);
|
||||
}).catch(function(error) {
|
||||
callback(error, null);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
typeof window !== 'undefined' && window.addEventListener('message', function (event) {
|
||||
if (event.origin != window.location.origin && !event.isTrusted) {
|
||||
return;
|
||||
}
|
||||
if (event.data.type == 'gotScreen' && cache[event.data.id]) {
|
||||
var data = cache[event.data.id];
|
||||
var constraints = data[1];
|
||||
var callback = data[0];
|
||||
delete cache[event.data.id];
|
||||
|
||||
if (event.data.sourceId === '') { // user canceled
|
||||
var error = new Error('NavigatorUserMediaError');
|
||||
error.name = 'PERMISSION_DENIED';
|
||||
callback(error);
|
||||
} else {
|
||||
constraints = constraints || {audio: false, video: {
|
||||
mandatory: {
|
||||
chromeMediaSource: 'desktop',
|
||||
maxWidth: window.screen.width,
|
||||
maxHeight: window.screen.height,
|
||||
maxFrameRate: 3
|
||||
},
|
||||
optional: [
|
||||
{googLeakyBucket: true},
|
||||
{googTemporalLayeredScreencast: true}
|
||||
]
|
||||
}};
|
||||
constraints.video.mandatory.chromeMediaSourceId = event.data.sourceId;
|
||||
getUserMedia(constraints, callback);
|
||||
}
|
||||
} else if (event.data.type == 'getScreenPending') {
|
||||
window.clearTimeout(event.data.id);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,337 @@
|
|||
var util = require('util');
|
||||
var hark = require('hark');
|
||||
var getScreenMedia = require('./getscreenmedia');
|
||||
var WildEmitter = require('wildemitter');
|
||||
var mockconsole = require('mockconsole');
|
||||
|
||||
function isAllTracksEnded(stream) {
|
||||
var isAllTracksEnded = true;
|
||||
stream.getTracks().forEach(function (t) {
|
||||
isAllTracksEnded = t.readyState === 'ended' && isAllTracksEnded;
|
||||
});
|
||||
return isAllTracksEnded;
|
||||
}
|
||||
|
||||
function LocalMedia(opts) {
|
||||
WildEmitter.call(this);
|
||||
|
||||
var config = this.config = {
|
||||
detectSpeakingEvents: false,
|
||||
audioFallback: false,
|
||||
media: {
|
||||
audio: true,
|
||||
video: true
|
||||
},
|
||||
harkOptions: null,
|
||||
logger: mockconsole
|
||||
};
|
||||
|
||||
var item;
|
||||
for (item in opts) {
|
||||
if (opts.hasOwnProperty(item)) {
|
||||
this.config[item] = opts[item];
|
||||
}
|
||||
}
|
||||
|
||||
this.logger = config.logger;
|
||||
this._log = this.logger.log.bind(this.logger, 'LocalMedia:');
|
||||
this._logerror = this.logger.error.bind(this.logger, 'LocalMedia:');
|
||||
|
||||
this.localStreams = [];
|
||||
this.localScreens = [];
|
||||
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
this._logerror('Your browser does not support local media capture.');
|
||||
}
|
||||
|
||||
this._audioMonitors = [];
|
||||
this.on('localStreamStopped', this._stopAudioMonitor.bind(this));
|
||||
this.on('localScreenStopped', this._stopAudioMonitor.bind(this));
|
||||
}
|
||||
|
||||
util.inherits(LocalMedia, WildEmitter);
|
||||
|
||||
|
||||
LocalMedia.prototype.start = function (mediaConstraints, cb) {
|
||||
var self = this;
|
||||
var constraints = mediaConstraints || this.config.media;
|
||||
|
||||
this.emit('localStreamRequested', constraints);
|
||||
|
||||
navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {
|
||||
// Although the promise should be resolved only if all the constraints
|
||||
// are met Edge resolves it if both audio and video are requested but
|
||||
// only audio is available.
|
||||
if (constraints.video && stream.getVideoTracks().length === 0) {
|
||||
constraints.video = false;
|
||||
self.start(constraints, cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (constraints.audio && self.config.detectSpeakingEvents) {
|
||||
self._setupAudioMonitor(stream, self.config.harkOptions);
|
||||
}
|
||||
self.localStreams.push(stream);
|
||||
|
||||
stream.getTracks().forEach(function (track) {
|
||||
track.addEventListener('ended', function () {
|
||||
if (isAllTracksEnded(stream)) {
|
||||
self._removeStream(stream);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
self.emit('localStream', stream);
|
||||
|
||||
if (cb) {
|
||||
return cb(null, stream);
|
||||
}
|
||||
}).catch(function (err) {
|
||||
// Fallback for users without a camera
|
||||
if (self.config.audioFallback && err.name === 'NotFoundError' && constraints.video !== false) {
|
||||
constraints.video = false;
|
||||
self.start(constraints, cb);
|
||||
return;
|
||||
}
|
||||
|
||||
self.emit('localStreamRequestFailed', constraints);
|
||||
|
||||
if (cb) {
|
||||
return cb(err, null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
LocalMedia.prototype.stop = function (stream) {
|
||||
this.stopStream(stream);
|
||||
this.stopScreenShare(stream);
|
||||
};
|
||||
|
||||
LocalMedia.prototype.stopStream = function (stream) {
|
||||
var self = this;
|
||||
|
||||
if (stream) {
|
||||
var idx = this.localStreams.indexOf(stream);
|
||||
if (idx > -1) {
|
||||
stream.getTracks().forEach(function (track) { track.stop(); });
|
||||
this._removeStream(stream);
|
||||
}
|
||||
} else {
|
||||
this.localStreams.forEach(function (stream) {
|
||||
stream.getTracks().forEach(function (track) { track.stop(); });
|
||||
self._removeStream(stream);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
LocalMedia.prototype.startScreenShare = function (mode, constraints, cb) {
|
||||
var self = this;
|
||||
|
||||
this.emit('localScreenRequested');
|
||||
|
||||
if (typeof constraints === 'function' && !cb) {
|
||||
cb = constraints;
|
||||
constraints = null;
|
||||
}
|
||||
|
||||
getScreenMedia(mode, constraints, function (err, stream) {
|
||||
if (!err) {
|
||||
self.localScreens.push(stream);
|
||||
|
||||
stream.getTracks().forEach(function (track) {
|
||||
track.addEventListener('ended', function () {
|
||||
var isAllTracksEnded = true;
|
||||
stream.getTracks().forEach(function (t) {
|
||||
isAllTracksEnded = t.readyState === 'ended' && isAllTracksEnded;
|
||||
});
|
||||
|
||||
if (isAllTracksEnded) {
|
||||
self._removeStream(stream);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
self.emit('localScreen', stream);
|
||||
} else {
|
||||
self.emit('localScreenRequestFailed');
|
||||
}
|
||||
|
||||
// enable the callback
|
||||
if (cb) {
|
||||
return cb(err, stream);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
LocalMedia.prototype.stopScreenShare = function (stream) {
|
||||
var self = this;
|
||||
|
||||
if (stream) {
|
||||
var idx = this.localScreens.indexOf(stream);
|
||||
if (idx > -1) {
|
||||
stream.getTracks().forEach(function (track) { track.stop(); });
|
||||
this._removeStream(stream);
|
||||
}
|
||||
} else {
|
||||
this.localScreens.forEach(function (stream) {
|
||||
stream.getTracks().forEach(function (track) { track.stop(); });
|
||||
self._removeStream(stream);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Audio controls
|
||||
LocalMedia.prototype.mute = function () {
|
||||
this._audioEnabled(false);
|
||||
this.emit('audioOff');
|
||||
};
|
||||
|
||||
LocalMedia.prototype.unmute = function () {
|
||||
this._audioEnabled(true);
|
||||
this.emit('audioOn');
|
||||
};
|
||||
|
||||
// Video controls
|
||||
LocalMedia.prototype.pauseVideo = function () {
|
||||
this._videoEnabled(false);
|
||||
this.emit('videoOff');
|
||||
};
|
||||
LocalMedia.prototype.resumeVideo = function () {
|
||||
this._videoEnabled(true);
|
||||
this.emit('videoOn');
|
||||
};
|
||||
|
||||
// Combined controls
|
||||
LocalMedia.prototype.pause = function () {
|
||||
this.mute();
|
||||
this.pauseVideo();
|
||||
};
|
||||
LocalMedia.prototype.resume = function () {
|
||||
this.unmute();
|
||||
this.resumeVideo();
|
||||
};
|
||||
|
||||
// Internal methods for enabling/disabling audio/video
|
||||
LocalMedia.prototype._audioEnabled = function (bool) {
|
||||
this.localStreams.forEach(function (stream) {
|
||||
stream.getAudioTracks().forEach(function (track) {
|
||||
track.enabled = !!bool;
|
||||
});
|
||||
});
|
||||
};
|
||||
LocalMedia.prototype._videoEnabled = function (bool) {
|
||||
this.localStreams.forEach(function (stream) {
|
||||
stream.getVideoTracks().forEach(function (track) {
|
||||
track.enabled = !!bool;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// check if all audio streams are enabled
|
||||
LocalMedia.prototype.isAudioEnabled = function () {
|
||||
var enabled = true;
|
||||
var hasAudioTracks = false;
|
||||
this.localStreams.forEach(function (stream) {
|
||||
var audioTracks = stream.getAudioTracks();
|
||||
if (audioTracks.length > 0) {
|
||||
hasAudioTracks = true;
|
||||
audioTracks.forEach(function (track) {
|
||||
enabled = enabled && track.enabled;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// If no audioTracks were found, that means there is no microphone device.
|
||||
// In that case, isAudioEnabled should return false.
|
||||
if (!hasAudioTracks) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return enabled;
|
||||
};
|
||||
|
||||
// check if all video streams are enabled
|
||||
LocalMedia.prototype.isVideoEnabled = function () {
|
||||
var enabled = true;
|
||||
var hasVideoTracks = false;
|
||||
this.localStreams.forEach(function (stream) {
|
||||
var videoTracks = stream.getVideoTracks();
|
||||
if (videoTracks.length > 0) {
|
||||
hasVideoTracks = true;
|
||||
videoTracks.forEach(function (track) {
|
||||
enabled = enabled && track.enabled;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// If no videoTracks were found, that means there is no camera device.
|
||||
// In that case, isVideoEnabled should return false.
|
||||
if (!hasVideoTracks) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return enabled;
|
||||
};
|
||||
|
||||
LocalMedia.prototype._removeStream = function (stream) {
|
||||
var idx = this.localStreams.indexOf(stream);
|
||||
if (idx > -1) {
|
||||
this.localStreams.splice(idx, 1);
|
||||
this.emit('localStreamStopped', stream);
|
||||
} else {
|
||||
idx = this.localScreens.indexOf(stream);
|
||||
if (idx > -1) {
|
||||
this.localScreens.splice(idx, 1);
|
||||
this.emit('localScreenStopped', stream);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
LocalMedia.prototype._setupAudioMonitor = function (stream, harkOptions) {
|
||||
this._log('Setup audio');
|
||||
var audio = hark(stream, harkOptions);
|
||||
var self = this;
|
||||
var timeout;
|
||||
|
||||
audio.on('speaking', function () {
|
||||
self.emit('speaking');
|
||||
});
|
||||
|
||||
audio.on('stopped_speaking', function () {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
timeout = setTimeout(function () {
|
||||
self.emit('stoppedSpeaking');
|
||||
}, 1000);
|
||||
});
|
||||
audio.on('volume_change', function (volume, threshold) {
|
||||
self.emit('volumeChange', volume, threshold);
|
||||
});
|
||||
|
||||
this._audioMonitors.push({audio: audio, stream: stream});
|
||||
};
|
||||
|
||||
LocalMedia.prototype._stopAudioMonitor = function (stream) {
|
||||
var idx = -1;
|
||||
this._audioMonitors.forEach(function (monitors, i) {
|
||||
if (monitors.stream === stream) {
|
||||
idx = i;
|
||||
}
|
||||
});
|
||||
|
||||
if (idx > -1) {
|
||||
this._audioMonitors[idx].audio.stop();
|
||||
this._audioMonitors.splice(idx, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// fallback for old .localScreen behaviour
|
||||
Object.defineProperty(LocalMedia.prototype, 'localScreen', {
|
||||
get: function () {
|
||||
return this.localScreens.length > 0 ? this.localScreens[0] : null;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = LocalMedia;
|
|
@ -0,0 +1,306 @@
|
|||
var util = require('util');
|
||||
var webrtcSupport = require('webrtcsupport');
|
||||
var WildEmitter = require('wildemitter');
|
||||
|
||||
function isAllTracksEnded(stream) {
|
||||
var isAllTracksEnded = true;
|
||||
stream.getTracks().forEach(function (t) {
|
||||
isAllTracksEnded = t.readyState === 'ended' && isAllTracksEnded;
|
||||
});
|
||||
return isAllTracksEnded;
|
||||
}
|
||||
|
||||
function Peer(options) {
|
||||
var self = this;
|
||||
|
||||
// call emitter constructor
|
||||
WildEmitter.call(this);
|
||||
|
||||
this.id = options.id;
|
||||
this.parent = options.parent;
|
||||
this.type = options.type || 'video';
|
||||
this.oneway = options.oneway || false;
|
||||
this.sharemyscreen = options.sharemyscreen || false;
|
||||
this.browserPrefix = options.prefix;
|
||||
this.stream = options.stream;
|
||||
this.enableDataChannels = options.enableDataChannels === undefined ? this.parent.config.enableDataChannels : options.enableDataChannels;
|
||||
this.receiveMedia = options.receiveMedia || this.parent.config.receiveMedia;
|
||||
this.channels = {};
|
||||
this.pendingDCMessages = []; // key (datachannel label) -> value (array[pending messages])
|
||||
this.sid = options.sid || Date.now().toString();
|
||||
this.pc = new RTCPeerConnection(this.parent.config.peerConnectionConfig);
|
||||
this.pc.addEventListener('icecandidate', this.onIceCandidate.bind(this));
|
||||
this.pc.addEventListener('endofcandidates', function (event) {
|
||||
self.send('endOfCandidates', event);
|
||||
});
|
||||
this.pc.addEventListener('addstream', this.handleRemoteStreamAdded.bind(this));
|
||||
this.pc.addEventListener('datachannel', this.handleDataChannelAdded.bind(this));
|
||||
this.pc.addEventListener('removestream', this.handleStreamRemoved.bind(this));
|
||||
// Just fire negotiation needed events for now
|
||||
// When browser re-negotiation handling seems to work
|
||||
// we can use this as the trigger for starting the offer/answer process
|
||||
// automatically. We'll just leave it be for now while this stabalizes.
|
||||
this.pc.addEventListener('negotiationneeded', this.emit.bind(this, 'negotiationNeeded'));
|
||||
this.pc.addEventListener('iceconnectionstatechange', this.emit.bind(this, 'iceConnectionStateChange'));
|
||||
this.pc.addEventListener('iceconnectionstatechange', function () {
|
||||
switch (self.pc.iceConnectionState) {
|
||||
case 'failed':
|
||||
// currently, in chrome only the initiator goes to failed
|
||||
// so we need to signal this to the peer
|
||||
if (self.pc.localDescription.type === 'offer') {
|
||||
self.parent.emit('iceFailed', self);
|
||||
self.send('connectivityError');
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
this.pc.addEventListener('signalingstatechange', this.emit.bind(this, 'signalingStateChange'));
|
||||
this.logger = this.parent.logger;
|
||||
|
||||
// handle screensharing/broadcast mode
|
||||
if (options.type === 'screen') {
|
||||
if (this.parent.localScreen && this.sharemyscreen) {
|
||||
this.logger.log('adding local screen stream to peer connection');
|
||||
this.pc.addStream(this.parent.localScreen);
|
||||
this.broadcaster = options.broadcaster;
|
||||
}
|
||||
} else {
|
||||
this.parent.localStreams.forEach(function (stream) {
|
||||
self.pc.addStream(stream);
|
||||
});
|
||||
}
|
||||
|
||||
// proxy events to parent
|
||||
this.on('*', function () {
|
||||
self.parent.emit.apply(self.parent, arguments);
|
||||
});
|
||||
}
|
||||
|
||||
util.inherits(Peer, WildEmitter);
|
||||
|
||||
Peer.prototype.offer = function(options) {
|
||||
this.pc.createOffer(options).then(function(offer) {
|
||||
this.pc.setLocalDescription(offer).then(function() {
|
||||
if (this.parent.config.nick) offer.nick = this.parent.config.nick;
|
||||
this.send('offer', offer);
|
||||
}.bind(this)).catch(function(error) {
|
||||
console.warn("setLocalDescription for offer failed: ", error);
|
||||
}.bind(this));
|
||||
}.bind(this)).catch(function(error) {
|
||||
console.warn("createOffer failed: ", error);
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
Peer.prototype.handleOffer = function (offer) {
|
||||
this.pc.setRemoteDescription(offer).then(function() {
|
||||
this.answer();
|
||||
}.bind(this)).catch(function(error) {
|
||||
console.warn("setRemoteDescription for offer failed: ", error);
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
Peer.prototype.answer = function() {
|
||||
this.pc.createAnswer().then(function(answer) {
|
||||
this.pc.setLocalDescription(answer).then(function() {
|
||||
if (this.parent.config.nick) answer.nick = this.parent.config.nick;
|
||||
this.send('answer', answer);
|
||||
}.bind(this)).catch(function(error) {
|
||||
console.warn("setLocalDescription for answer failed: ", error);
|
||||
}.bind(this));
|
||||
}.bind(this)).catch(function(error) {
|
||||
console.warn("createAnswer failed: ", error);
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
Peer.prototype.handleAnswer = function (answer) {
|
||||
this.pc.setRemoteDescription(answer).catch(function(error) {
|
||||
console.warn("setRemoteDescription for answer failed: ", error);
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
Peer.prototype.handleMessage = function (message) {
|
||||
var self = this;
|
||||
|
||||
this.logger.log('getting', message.type, message);
|
||||
|
||||
if (message.prefix) this.browserPrefix = message.prefix;
|
||||
|
||||
if (message.type === 'offer') {
|
||||
if (!this.nick) this.nick = message.payload.nick;
|
||||
delete message.payload.nick;
|
||||
this.handleOffer(message.payload);
|
||||
} else if (message.type === 'answer') {
|
||||
if (!this.nick) this.nick = message.payload.nick;
|
||||
delete message.payload.nick;
|
||||
this.handleAnswer(message.payload);
|
||||
} else if (message.type === 'candidate') {
|
||||
this.pc.addIceCandidate(message.payload.candidate);
|
||||
} else if (message.type === 'connectivityError') {
|
||||
this.parent.emit('connectivityError', self);
|
||||
} else if (message.type === 'mute') {
|
||||
this.parent.emit('mute', {id: message.from, name: message.payload.name});
|
||||
} else if (message.type === 'unmute') {
|
||||
this.parent.emit('unmute', {id: message.from, name: message.payload.name});
|
||||
} else if (message.type === 'endOfCandidates') {
|
||||
this.pc.addIceCandidate('');
|
||||
} else if (message.type === 'unshareScreen') {
|
||||
this.parent.emit('unshareScreen', {id: message.from});
|
||||
this.end();
|
||||
}
|
||||
};
|
||||
|
||||
// send via signalling channel
|
||||
Peer.prototype.send = function (messageType, payload) {
|
||||
var message = {
|
||||
to: this.id,
|
||||
sid: this.sid,
|
||||
broadcaster: this.broadcaster,
|
||||
roomType: this.type,
|
||||
type: messageType,
|
||||
payload: payload,
|
||||
prefix: webrtcSupport.prefix
|
||||
};
|
||||
this.logger.log('sending', messageType, message);
|
||||
this.parent.emit('message', message);
|
||||
};
|
||||
|
||||
// send via data channel
|
||||
// returns true when message was sent and false if channel is not open
|
||||
Peer.prototype.sendDirectly = function (channel, messageType, payload) {
|
||||
var message = {
|
||||
type: messageType,
|
||||
payload: payload
|
||||
};
|
||||
this.logger.log('sending via datachannel', channel, messageType, message);
|
||||
var dc = this.getDataChannel(channel);
|
||||
if (dc.readyState != 'open') {
|
||||
if (!this.pendingDCMessages.hasOwnProperty(channel)) {
|
||||
this.pendingDCMessages[channel] = [];
|
||||
}
|
||||
this.pendingDCMessages[channel].push(message);
|
||||
return false;
|
||||
}
|
||||
dc.send(JSON.stringify(message));
|
||||
return true;
|
||||
};
|
||||
|
||||
// Internal method registering handlers for a data channel and emitting events on the peer
|
||||
Peer.prototype._observeDataChannel = function (channel) {
|
||||
var self = this;
|
||||
channel.onclose = this.emit.bind(this, 'channelClose', channel);
|
||||
channel.onerror = this.emit.bind(this, 'channelError', channel);
|
||||
channel.onmessage = function (event) {
|
||||
self.emit('channelMessage', self, channel.label, JSON.parse(event.data), channel, event);
|
||||
};
|
||||
channel.onopen = function () {
|
||||
self.emit('channelOpen', channel);
|
||||
// Check if there are messages that could not be send
|
||||
if (self.pendingDCMessages.hasOwnProperty(channel.label)) {
|
||||
var pendingMessages = self.pendingDCMessages[channel.label];
|
||||
for (var i = 0; i < pendingMessages.length; i++) {
|
||||
self.sendDirectly(channel.label, pendingMessages[i].type, pendingMessages[i].payload);
|
||||
}
|
||||
self.pendingDCMessages[channel.label] = [];
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Fetch or create a data channel by the given name
|
||||
Peer.prototype.getDataChannel = function (name, opts) {
|
||||
if (!webrtcSupport.supportDataChannel) return this.emit('error', new Error('createDataChannel not supported'));
|
||||
var channel = this.channels[name];
|
||||
opts || (opts = {});
|
||||
if (channel) return channel;
|
||||
// if we don't have one by this label, create it
|
||||
channel = this.channels[name] = this.pc.createDataChannel(name, opts);
|
||||
this._observeDataChannel(channel);
|
||||
return channel;
|
||||
};
|
||||
|
||||
Peer.prototype.onIceCandidate = function (event) {
|
||||
var candidate = event.candidate;
|
||||
if (this.closed) return;
|
||||
if (candidate) {
|
||||
var pcConfig = this.parent.config.peerConnectionConfig;
|
||||
if (webrtcSupport.prefix === 'moz' && pcConfig && pcConfig.iceTransports &&
|
||||
candidate.candidate && candidate.candidate.candidate &&
|
||||
candidate.candidate.candidate.indexOf(pcConfig.iceTransports) < 0) {
|
||||
this.logger.log('Ignoring ice candidate not matching pcConfig iceTransports type: ', pcConfig.iceTransports);
|
||||
} else {
|
||||
// Retain legacy data structure for compatibility with
|
||||
// mobile clients.
|
||||
var expandedCandidate = {
|
||||
candidate: {
|
||||
candidate: candidate.candidate,
|
||||
sdpMid: candidate.sdpMid,
|
||||
sdpMLineIndex: candidate.sdpMLineIndex
|
||||
}
|
||||
};
|
||||
this.send('candidate', expandedCandidate);
|
||||
}
|
||||
} else {
|
||||
this.logger.log("End of candidates.");
|
||||
}
|
||||
};
|
||||
|
||||
Peer.prototype.start = function () {
|
||||
var self = this;
|
||||
|
||||
// well, the webrtc api requires that we either
|
||||
// a) create a datachannel a priori
|
||||
// b) do a renegotiation later to add the SCTP m-line
|
||||
// Let's do (a) first...
|
||||
if (this.enableDataChannels) {
|
||||
this.getDataChannel('simplewebrtc');
|
||||
}
|
||||
|
||||
this.offer(this.receiveMedia);
|
||||
};
|
||||
|
||||
Peer.prototype.icerestart = function () {
|
||||
var constraints = this.receiveMedia;
|
||||
constraints.iceRestart = true;
|
||||
this.offer(constraints);
|
||||
};
|
||||
|
||||
Peer.prototype.end = function () {
|
||||
if (this.closed) return;
|
||||
this.pc.close();
|
||||
this.handleStreamRemoved();
|
||||
};
|
||||
|
||||
Peer.prototype.handleRemoteStreamAdded = function (event) {
|
||||
var self = this;
|
||||
if (this.stream) {
|
||||
this.logger.warn('Already have a remote stream');
|
||||
} else {
|
||||
this.stream = event.stream;
|
||||
|
||||
this.stream.getTracks().forEach(function (track) {
|
||||
track.addEventListener('ended', function () {
|
||||
if (isAllTracksEnded(self.stream)) {
|
||||
self.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.parent.emit('peerStreamAdded', this);
|
||||
}
|
||||
};
|
||||
|
||||
Peer.prototype.handleStreamRemoved = function () {
|
||||
var peerIndex = this.parent.peers.indexOf(this);
|
||||
if (peerIndex > -1) {
|
||||
this.parent.peers.splice(peerIndex, 1);
|
||||
this.closed = true;
|
||||
this.parent.emit('peerStreamRemoved', this);
|
||||
}
|
||||
};
|
||||
|
||||
Peer.prototype.handleDataChannelAdded = function (event) {
|
||||
var channel = event.channel;
|
||||
this.channels[channel.label] = channel;
|
||||
this._observeDataChannel(channel);
|
||||
};
|
||||
|
||||
module.exports = Peer;
|
|
@ -0,0 +1,422 @@
|
|||
var WebRTC = require('./webrtc');
|
||||
var WildEmitter = require('wildemitter');
|
||||
var webrtcSupport = require('webrtcsupport');
|
||||
var attachMediaStream = require('attachmediastream');
|
||||
var mockconsole = require('mockconsole');
|
||||
|
||||
function SimpleWebRTC(opts) {
|
||||
var self = this;
|
||||
var options = opts || {};
|
||||
var config = this.config = {
|
||||
socketio: {/* 'force new connection':true*/},
|
||||
connection: null,
|
||||
debug: false,
|
||||
localVideoEl: '',
|
||||
remoteVideosEl: '',
|
||||
enableDataChannels: true,
|
||||
autoRequestMedia: false,
|
||||
autoRemoveVideos: true,
|
||||
adjustPeerVolume: false,
|
||||
peerVolumeWhenSpeaking: 0.25,
|
||||
media: {
|
||||
video: true,
|
||||
audio: true
|
||||
},
|
||||
receiveMedia: {
|
||||
offerToReceiveAudio: 1,
|
||||
offerToReceiveVideo: 1
|
||||
},
|
||||
localVideo: {
|
||||
autoplay: true,
|
||||
mirror: true,
|
||||
muted: true
|
||||
}
|
||||
};
|
||||
var item, connection;
|
||||
|
||||
// We also allow a 'logger' option. It can be any object that implements
|
||||
// log, warn, and error methods.
|
||||
// We log nothing by default, following "the rule of silence":
|
||||
// http://www.linfo.org/rule_of_silence.html
|
||||
this.logger = function () {
|
||||
// we assume that if you're in debug mode and you didn't
|
||||
// pass in a logger, you actually want to log as much as
|
||||
// possible.
|
||||
if (opts.debug) {
|
||||
return opts.logger || console;
|
||||
} else {
|
||||
// or we'll use your logger which should have its own logic
|
||||
// for output. Or we'll return the no-op.
|
||||
return opts.logger || mockconsole;
|
||||
}
|
||||
}();
|
||||
|
||||
// set our config from options
|
||||
for (item in options) {
|
||||
if (options.hasOwnProperty(item)) {
|
||||
this.config[item] = options[item];
|
||||
}
|
||||
}
|
||||
|
||||
// attach detected support for convenience
|
||||
this.capabilities = webrtcSupport;
|
||||
|
||||
// call WildEmitter constructor
|
||||
WildEmitter.call(this);
|
||||
|
||||
if (this.config.connection === null) {
|
||||
throw 'no connection object given in the configuration';
|
||||
} else {
|
||||
connection = this.connection = this.config.connection;
|
||||
}
|
||||
|
||||
connection.on('connect', function () {
|
||||
self.emit('connectionReady', connection.getSessionid());
|
||||
self.sessionReady = true;
|
||||
self.testReadiness();
|
||||
});
|
||||
|
||||
connection.on('message', function (message) {
|
||||
var peers = self.webrtc.getPeers(message.from, message.roomType);
|
||||
var peer;
|
||||
|
||||
if (message.type === 'offer') {
|
||||
if (peers.length) {
|
||||
peers.forEach(function (p) {
|
||||
if (p.sid == message.sid) peer = p;
|
||||
});
|
||||
//if (!peer) peer = peers[0]; // fallback for old protocol versions
|
||||
}
|
||||
if (!peer) {
|
||||
peer = self.webrtc.createPeer({
|
||||
id: message.from,
|
||||
sid: message.sid,
|
||||
type: message.roomType,
|
||||
enableDataChannels: self.config.enableDataChannels && message.roomType !== 'screen',
|
||||
sharemyscreen: message.roomType === 'screen' && !message.broadcaster,
|
||||
broadcaster: message.roomType === 'screen' && !message.broadcaster ? self.connection.getSessionid() : null
|
||||
});
|
||||
self.emit('createdPeer', peer);
|
||||
}
|
||||
peer.handleMessage(message);
|
||||
} else if (peers.length) {
|
||||
peers.forEach(function (peer) {
|
||||
if (message.sid) {
|
||||
if (peer.sid === message.sid) {
|
||||
peer.handleMessage(message);
|
||||
}
|
||||
} else {
|
||||
peer.handleMessage(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
connection.on('remove', function (room) {
|
||||
if (room.id !== self.connection.getSessionid()) {
|
||||
self.webrtc.removePeers(room.id, room.type);
|
||||
}
|
||||
});
|
||||
|
||||
// instantiate our main WebRTC helper
|
||||
// using same logger from logic here
|
||||
opts.logger = this.logger;
|
||||
opts.debug = false;
|
||||
this.webrtc = new WebRTC(opts);
|
||||
|
||||
// attach a few methods from underlying lib to simple.
|
||||
['mute', 'unmute', 'pauseVideo', 'resumeVideo', 'pause', 'resume', 'sendToAll', 'sendDirectlyToAll', 'getPeers', 'createPeer', 'removePeers'].forEach(function (method) {
|
||||
self[method] = self.webrtc[method].bind(self.webrtc);
|
||||
});
|
||||
|
||||
// proxy events from WebRTC
|
||||
this.webrtc.on('*', function () {
|
||||
self.emit.apply(self, arguments);
|
||||
});
|
||||
|
||||
// log all events in debug mode
|
||||
if (config.debug) {
|
||||
this.on('*', this.logger.log.bind(this.logger, 'SimpleWebRTC event:'));
|
||||
}
|
||||
|
||||
// check for readiness
|
||||
this.webrtc.on('localStream', function () {
|
||||
self.testReadiness();
|
||||
});
|
||||
|
||||
this.webrtc.on('message', function (payload) {
|
||||
self.connection.emit('message', payload);
|
||||
});
|
||||
|
||||
this.webrtc.on('peerStreamAdded', this.handlePeerStreamAdded.bind(this));
|
||||
this.webrtc.on('peerStreamRemoved', this.handlePeerStreamRemoved.bind(this));
|
||||
|
||||
// echo cancellation attempts
|
||||
if (this.config.adjustPeerVolume) {
|
||||
this.webrtc.on('speaking', this.setVolumeForAll.bind(this, this.config.peerVolumeWhenSpeaking));
|
||||
this.webrtc.on('stoppedSpeaking', this.setVolumeForAll.bind(this, 1));
|
||||
}
|
||||
|
||||
connection.on('stunservers', function (args) {
|
||||
// resets/overrides the config
|
||||
self.webrtc.config.peerConnectionConfig.iceServers = args;
|
||||
self.emit('stunservers', args);
|
||||
});
|
||||
connection.on('turnservers', function (args) {
|
||||
// appends to the config
|
||||
self.webrtc.config.peerConnectionConfig.iceServers = self.webrtc.config.peerConnectionConfig.iceServers.concat(args);
|
||||
self.emit('turnservers', args);
|
||||
});
|
||||
|
||||
this.webrtc.on('iceFailed', function (peer) {
|
||||
// local ice failure
|
||||
});
|
||||
this.webrtc.on('connectivityError', function (peer) {
|
||||
// remote ice failure
|
||||
});
|
||||
|
||||
|
||||
// sending mute/unmute to all peers
|
||||
this.webrtc.on('audioOn', function () {
|
||||
self.webrtc.sendToAll('unmute', {name: 'audio'});
|
||||
});
|
||||
this.webrtc.on('audioOff', function () {
|
||||
self.webrtc.sendToAll('mute', {name: 'audio'});
|
||||
});
|
||||
this.webrtc.on('videoOn', function () {
|
||||
self.webrtc.sendToAll('unmute', {name: 'video'});
|
||||
});
|
||||
this.webrtc.on('videoOff', function () {
|
||||
self.webrtc.sendToAll('mute', {name: 'video'});
|
||||
});
|
||||
|
||||
// screensharing events
|
||||
this.webrtc.on('localScreen', function (stream) {
|
||||
var item,
|
||||
el = document.createElement('video'),
|
||||
container = self.getRemoteVideoContainer();
|
||||
|
||||
el.oncontextmenu = function () { return false; };
|
||||
el.id = 'localScreen';
|
||||
attachMediaStream(stream, el);
|
||||
if (container) {
|
||||
container.appendChild(el);
|
||||
}
|
||||
|
||||
self.emit('localScreenAdded', el);
|
||||
self.connection.emit('shareScreen');
|
||||
|
||||
// NOTE: we don't create screen peers for existing video peers here,
|
||||
// this is done by the application code in "webrtc.js".
|
||||
});
|
||||
this.webrtc.on('localScreenStopped', function (stream) {
|
||||
if (self.getLocalScreen()) {
|
||||
self.stopScreenShare();
|
||||
}
|
||||
/*
|
||||
self.connection.emit('unshareScreen');
|
||||
self.webrtc.peers.forEach(function (peer) {
|
||||
if (peer.sharemyscreen) {
|
||||
peer.end();
|
||||
}
|
||||
});
|
||||
*/
|
||||
});
|
||||
|
||||
this.webrtc.on('channelMessage', function (peer, label, data) {
|
||||
if (data.type == 'volume') {
|
||||
self.emit('remoteVolumeChange', peer, data.volume);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
SimpleWebRTC.prototype = Object.create(WildEmitter.prototype, {
|
||||
constructor: {
|
||||
value: SimpleWebRTC
|
||||
}
|
||||
});
|
||||
|
||||
SimpleWebRTC.prototype.leaveCall = function () {
|
||||
if (this.roomName) {
|
||||
while (this.webrtc.peers.length) {
|
||||
this.webrtc.peers[0].end();
|
||||
}
|
||||
if (this.getLocalScreen()) {
|
||||
this.stopScreenShare();
|
||||
}
|
||||
this.emit('leftRoom', this.roomName);
|
||||
this.stopLocalVideo();
|
||||
this.roomName = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
SimpleWebRTC.prototype.disconnect = function () {
|
||||
this.connection.disconnect();
|
||||
delete this.connection;
|
||||
};
|
||||
|
||||
SimpleWebRTC.prototype.handlePeerStreamAdded = function (peer) {
|
||||
var self = this;
|
||||
var container = this.getRemoteVideoContainer();
|
||||
var video = attachMediaStream(peer.stream);
|
||||
|
||||
// At least Firefox, Opera and Edge move the video to a wrong position
|
||||
// instead of keeping it unchanged when "transform: scaleX(1)" is used
|
||||
// ("transform: scaleX(-1)" is fine); as it should have no effect the
|
||||
// transform is removed.
|
||||
if (video.style.transform === 'scaleX(1)') {
|
||||
video.style.transform = '';
|
||||
}
|
||||
|
||||
// store video element as part of peer for easy removal
|
||||
peer.videoEl = video;
|
||||
video.id = this.getDomId(peer);
|
||||
|
||||
if (container) container.appendChild(video);
|
||||
|
||||
this.emit('videoAdded', video, peer);
|
||||
|
||||
// send our mute status to new peer if we're muted
|
||||
// currently called with a small delay because it arrives before
|
||||
// the video element is created otherwise (which happens after
|
||||
// the async setRemoteDescription-createAnswer)
|
||||
window.setTimeout(function () {
|
||||
if (!self.webrtc.isAudioEnabled()) {
|
||||
peer.send('mute', {name: 'audio'});
|
||||
}
|
||||
if (!self.webrtc.isVideoEnabled()) {
|
||||
peer.send('mute', {name: 'video'});
|
||||
}
|
||||
}, 250);
|
||||
};
|
||||
|
||||
SimpleWebRTC.prototype.handlePeerStreamRemoved = function (peer) {
|
||||
var container = this.getRemoteVideoContainer();
|
||||
var videoEl = peer.videoEl;
|
||||
if (this.config.autoRemoveVideos && container && videoEl) {
|
||||
container.removeChild(videoEl);
|
||||
}
|
||||
if (videoEl) this.emit('videoRemoved', videoEl, peer);
|
||||
};
|
||||
|
||||
SimpleWebRTC.prototype.getDomId = function (peer) {
|
||||
return [peer.id, peer.type, peer.broadcaster ? 'broadcasting' : 'incoming'].join('_');
|
||||
};
|
||||
|
||||
// set volume on video tag for all peers takse a value between 0 and 1
|
||||
SimpleWebRTC.prototype.setVolumeForAll = function (volume) {
|
||||
this.webrtc.peers.forEach(function (peer) {
|
||||
if (peer.videoEl) peer.videoEl.volume = volume;
|
||||
});
|
||||
};
|
||||
|
||||
SimpleWebRTC.prototype.joinCall = function (name) {
|
||||
if (this.config.autoRequestMedia) this.startLocalVideo();
|
||||
this.roomName = name;
|
||||
this.emit('joinedRoom', name);
|
||||
};
|
||||
|
||||
SimpleWebRTC.prototype.getEl = function (idOrEl) {
|
||||
if (typeof idOrEl === 'string') {
|
||||
return document.getElementById(idOrEl);
|
||||
} else {
|
||||
return idOrEl;
|
||||
}
|
||||
};
|
||||
|
||||
SimpleWebRTC.prototype.startLocalVideo = function () {
|
||||
var self = this;
|
||||
this.webrtc.start(this.config.media, function (err, stream) {
|
||||
if (err) {
|
||||
self.emit('localMediaError', err);
|
||||
} else {
|
||||
self.emit('localMediaStarted', self.config.media);
|
||||
attachMediaStream(stream, self.getLocalVideoContainer(), self.config.localVideo);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
SimpleWebRTC.prototype.stopLocalVideo = function () {
|
||||
this.webrtc.stop();
|
||||
};
|
||||
|
||||
// this accepts either element ID or element
|
||||
// and either the video tag itself or a container
|
||||
// that will be used to put the video tag into.
|
||||
SimpleWebRTC.prototype.getLocalVideoContainer = function () {
|
||||
var el = this.getEl(this.config.localVideoEl);
|
||||
if (el && el.tagName === 'VIDEO') {
|
||||
el.oncontextmenu = function () { return false; };
|
||||
return el;
|
||||
} else if (el) {
|
||||
var video = document.createElement('video');
|
||||
video.oncontextmenu = function () { return false; };
|
||||
el.appendChild(video);
|
||||
return video;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
SimpleWebRTC.prototype.getRemoteVideoContainer = function () {
|
||||
return this.getEl(this.config.remoteVideosEl);
|
||||
};
|
||||
|
||||
SimpleWebRTC.prototype.shareScreen = function (mode, cb) {
|
||||
this.webrtc.startScreenShare(mode, cb);
|
||||
};
|
||||
|
||||
SimpleWebRTC.prototype.getLocalScreen = function () {
|
||||
return this.webrtc.localScreen;
|
||||
};
|
||||
|
||||
SimpleWebRTC.prototype.stopScreenShare = function () {
|
||||
this.connection.emit('unshareScreen');
|
||||
var videoEl = document.getElementById('localScreen');
|
||||
var container = this.getRemoteVideoContainer();
|
||||
|
||||
if (this.config.autoRemoveVideos && container && videoEl) {
|
||||
container.removeChild(videoEl);
|
||||
}
|
||||
|
||||
// a hack to emit the event the removes the video
|
||||
// element that we want
|
||||
if (videoEl) {
|
||||
this.emit('videoRemoved', videoEl);
|
||||
}
|
||||
if (this.getLocalScreen()) {
|
||||
this.webrtc.stopScreenShare();
|
||||
}
|
||||
// Notify peers were sending to.
|
||||
this.webrtc.peers.forEach(function (peer) {
|
||||
if (peer.type === 'screen' && peer.sharemyscreen) {
|
||||
peer.send('unshareScreen');
|
||||
}
|
||||
if (peer.broadcaster) {
|
||||
peer.end();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
SimpleWebRTC.prototype.testReadiness = function () {
|
||||
var self = this;
|
||||
if (this.sessionReady) {
|
||||
if (!this.config.media.video && !this.config.media.audio) {
|
||||
self.emit('readyToCall', self.connection.getSessionid());
|
||||
} else if (this.webrtc.localStreams.length > 0) {
|
||||
self.emit('readyToCall', self.connection.getSessionid());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
SimpleWebRTC.prototype.createRoom = function (name, cb) {
|
||||
this.roomName = name;
|
||||
if (arguments.length === 2) {
|
||||
this.connection.emit('create', name, cb);
|
||||
} else {
|
||||
this.connection.emit('create', name);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = SimpleWebRTC;
|
|
@ -0,0 +1,164 @@
|
|||
var util = require('util');
|
||||
var webrtcSupport = require('webrtcsupport');
|
||||
var mockconsole = require('mockconsole');
|
||||
var localMedia = require('./localmedia');
|
||||
var Peer = require('./peer');
|
||||
|
||||
|
||||
function WebRTC(opts) {
|
||||
var self = this;
|
||||
var options = opts || {};
|
||||
var config = this.config = {
|
||||
debug: false,
|
||||
// makes the entire PC config overridable
|
||||
peerConnectionConfig: {
|
||||
iceServers: []
|
||||
},
|
||||
receiveMedia: {
|
||||
offerToReceiveAudio: 1,
|
||||
offerToReceiveVideo: 1
|
||||
},
|
||||
enableDataChannels: true
|
||||
};
|
||||
var item;
|
||||
|
||||
// We also allow a 'logger' option. It can be any object that implements
|
||||
// log, warn, and error methods.
|
||||
// We log nothing by default, following "the rule of silence":
|
||||
// http://www.linfo.org/rule_of_silence.html
|
||||
this.logger = function () {
|
||||
// we assume that if you're in debug mode and you didn't
|
||||
// pass in a logger, you actually want to log as much as
|
||||
// possible.
|
||||
if (opts.debug) {
|
||||
return opts.logger || console;
|
||||
} else {
|
||||
// or we'll use your logger which should have its own logic
|
||||
// for output. Or we'll return the no-op.
|
||||
return opts.logger || mockconsole;
|
||||
}
|
||||
}();
|
||||
|
||||
// set options
|
||||
for (item in options) {
|
||||
if (options.hasOwnProperty(item)) {
|
||||
this.config[item] = options[item];
|
||||
}
|
||||
}
|
||||
|
||||
// check for support
|
||||
if (!webrtcSupport.support) {
|
||||
this.logger.error('Your browser doesn\'t seem to support WebRTC');
|
||||
}
|
||||
|
||||
// where we'll store our peer connections
|
||||
this.peers = [];
|
||||
|
||||
// call localMedia constructor
|
||||
localMedia.call(this, this.config);
|
||||
|
||||
this.on('speaking', function () {
|
||||
if (!self.hardMuted) {
|
||||
// FIXME: should use sendDirectlyToAll, but currently has different semantics wrt payload
|
||||
self.peers.forEach(function (peer) {
|
||||
if (peer.enableDataChannels) {
|
||||
var dc = peer.getDataChannel('hark');
|
||||
if (dc.readyState != 'open') return;
|
||||
dc.send(JSON.stringify({type: 'speaking'}));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
this.on('stoppedSpeaking', function () {
|
||||
if (!self.hardMuted) {
|
||||
// FIXME: should use sendDirectlyToAll, but currently has different semantics wrt payload
|
||||
self.peers.forEach(function (peer) {
|
||||
if (peer.enableDataChannels) {
|
||||
var dc = peer.getDataChannel('hark');
|
||||
if (dc.readyState != 'open') return;
|
||||
dc.send(JSON.stringify({type: 'stoppedSpeaking'}));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
this.on('volumeChange', function (volume, treshold) {
|
||||
if (!self.hardMuted) {
|
||||
// FIXME: should use sendDirectlyToAll, but currently has different semantics wrt payload
|
||||
self.peers.forEach(function (peer) {
|
||||
if (peer.enableDataChannels) {
|
||||
var dc = peer.getDataChannel('hark');
|
||||
if (dc.readyState != 'open') return;
|
||||
dc.send(JSON.stringify({type: 'volume', volume: volume }));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.on('unshareScreen', function(message) {
|
||||
// End peers we were receiving the screensharing stream from.
|
||||
var peers = self.getPeers(message.from, 'screen');
|
||||
peers.forEach(function(peer) {
|
||||
if (!peer.sharemyscreen) {
|
||||
peer.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// log events in debug mode
|
||||
if (this.config.debug) {
|
||||
this.on('*', function (event, val1, val2) {
|
||||
var logger;
|
||||
// if you didn't pass in a logger and you explicitly turning on debug
|
||||
// we're just going to assume you're wanting log output with console
|
||||
if (self.config.logger === mockconsole) {
|
||||
logger = console;
|
||||
} else {
|
||||
logger = self.logger;
|
||||
}
|
||||
logger.log('event:', event, val1, val2);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
util.inherits(WebRTC, localMedia);
|
||||
|
||||
WebRTC.prototype.createPeer = function (opts) {
|
||||
var peer;
|
||||
opts.parent = this;
|
||||
peer = new Peer(opts);
|
||||
this.peers.push(peer);
|
||||
return peer;
|
||||
};
|
||||
|
||||
// removes peers
|
||||
WebRTC.prototype.removePeers = function (id, type) {
|
||||
this.getPeers(id, type).forEach(function (peer) {
|
||||
peer.end();
|
||||
});
|
||||
};
|
||||
|
||||
// fetches all Peer objects by session id and/or type
|
||||
WebRTC.prototype.getPeers = function (sessionId, type) {
|
||||
return this.peers.filter(function (peer) {
|
||||
return (!sessionId || peer.id === sessionId) && (!type || peer.type === type);
|
||||
});
|
||||
};
|
||||
|
||||
// sends message to all
|
||||
WebRTC.prototype.sendToAll = function (message, payload) {
|
||||
this.peers.forEach(function (peer) {
|
||||
peer.send(message, payload);
|
||||
});
|
||||
};
|
||||
|
||||
// sends message to all using a datachannel
|
||||
// only sends to anyone who has an open datachannel
|
||||
WebRTC.prototype.sendDirectlyToAll = function (channel, message, payload) {
|
||||
this.peers.forEach(function (peer) {
|
||||
if (peer.enableDataChannels) {
|
||||
peer.sendDirectly(channel, message, payload);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = WebRTC;
|
23
js/webrtc.js
23
js/webrtc.js
|
@ -467,7 +467,7 @@ var spreedPeerConnectionTable = [];
|
|||
// Initialize ice restart counter for peer
|
||||
spreedPeerConnectionTable[peer.id] = 0;
|
||||
|
||||
peer.pc.on('iceConnectionStateChange', function () {
|
||||
peer.pc.addEventListener('iceconnectionstatechange', function () {
|
||||
var userId = spreedMappingTable[peer.id];
|
||||
|
||||
switch (peer.pc.iceConnectionState) {
|
||||
|
@ -528,8 +528,8 @@ var spreedPeerConnectionTable = [];
|
|||
videoView.setConnectionStatus(OCA.Talk.Views.VideoView.ConnectionStatus.DISCONNECTED_LONG);
|
||||
|
||||
if (spreedPeerConnectionTable[peer.id] < 5) {
|
||||
if (peer.pc.pc.peerconnection.localDescription.type === 'offer' &&
|
||||
peer.pc.pc.peerconnection.signalingState === 'stable') {
|
||||
if (peer.pc.localDescription.type === 'offer' &&
|
||||
peer.pc.signalingState === 'stable') {
|
||||
spreedPeerConnectionTable[peer.id] ++;
|
||||
console.log('ICE restart.');
|
||||
peer.icerestart();
|
||||
|
@ -548,8 +548,8 @@ var spreedPeerConnectionTable = [];
|
|||
if (spreedPeerConnectionTable[peer.id] < 5) {
|
||||
videoView.setConnectionStatus(OCA.Talk.Views.VideoView.ConnectionStatus.FAILED);
|
||||
|
||||
if (peer.pc.pc.peerconnection.localDescription.type === 'offer' &&
|
||||
peer.pc.pc.peerconnection.signalingState === 'stable') {
|
||||
if (peer.pc.localDescription.type === 'offer' &&
|
||||
peer.pc.signalingState === 'stable') {
|
||||
spreedPeerConnectionTable[peer.id] ++;
|
||||
console.log('ICE restart.');
|
||||
peer.icerestart();
|
||||
|
@ -570,10 +570,6 @@ var spreedPeerConnectionTable = [];
|
|||
|
||||
OCA.SpreedMe.speakers.updateVideoContainerDummyIfLatestSpeaker(peer.id);
|
||||
});
|
||||
|
||||
peer.pc.on('PeerConnectionTrace', function (event) {
|
||||
console.log('trace', event);
|
||||
});
|
||||
},
|
||||
// The nick name below the avatar is distributed through the
|
||||
// DataChannel of the PeerConnection and only sent once during
|
||||
|
@ -855,15 +851,14 @@ var spreedPeerConnectionTable = [];
|
|||
|
||||
function checkPeerMedia(peer, track, mediaType) {
|
||||
var defer = $.Deferred();
|
||||
peer.pc.pc.getStats(track, function(stats) {
|
||||
peer.pc.getStats(track).then(function(stats) {
|
||||
var result = false;
|
||||
Object.keys(stats).forEach(function(key) {
|
||||
var value = stats[key];
|
||||
if (!result && !value || value.mediaType !== mediaType || !value.hasOwnProperty('bytesReceived')) {
|
||||
stats.forEach(function(statsReport) {
|
||||
if (!result && statsReport.mediaType !== mediaType || !statsReport.hasOwnProperty('bytesReceived')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.bytesReceived > 0) {
|
||||
if (statsReport.bytesReceived > 0) {
|
||||
OCA.SpreedMe.webrtc.emit('unmute', {
|
||||
id: peer.id,
|
||||
name: mediaType
|
||||
|
|
13
package.json
13
package.json
|
@ -13,7 +13,13 @@
|
|||
"author": "",
|
||||
"license": "AGPL-3.0",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.3.4",
|
||||
"@babel/preset-env": "^7.3.4",
|
||||
"attachmediastream": "^2.1.0",
|
||||
"babelify": "^10.0.0",
|
||||
"browserify": "^16.2.3",
|
||||
"handlebars": "^4.0.12",
|
||||
"hark": "^1.2.3",
|
||||
"jasmine": "^2.5.2",
|
||||
"jasmine-ajax": "^3.2.0",
|
||||
"jasmine-core": "^2.5.2",
|
||||
|
@ -22,6 +28,11 @@
|
|||
"karma-jasmine": "^1.0.2",
|
||||
"karma-jasmine-ajax": "^0.1.13",
|
||||
"karma-phantomjs-launcher": "^1.0.2",
|
||||
"phantomjs-prebuilt": "^2.1.13"
|
||||
"mockconsole": "0.0.1",
|
||||
"phantomjs-prebuilt": "^2.1.13",
|
||||
"util": "^0.11.1",
|
||||
"webrtc-adapter": "^7.2.1",
|
||||
"webrtcsupport": "^2.2.0",
|
||||
"wildemitter": "^1.2.0"
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче