diff --git a/package.json b/package.json index de75c506c4e599fadf047bc6e6f588e9a39c5a2d..fabfffc6f99b4a325a5fa5c0fe912c37b72f16fd 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "filetransfer": "^2.0.4", "localmedia": "^4.0.0", "rtcpeerconnection": "^8.0.0", - "webrtcsupport": "^2.2.0", "wildemitter": "^1.2.0", "socket.io-client": "1.3.7", "attachmediastream": "^2.0.0", diff --git a/src/liowebrtc.js b/src/liowebrtc.js index 9b24ba57255b700f5f5c0d4053dba695063baee6..e27620747753da09415a4a5def3d713dd1952d8a 100644 --- a/src/liowebrtc.js +++ b/src/liowebrtc.js @@ -54,7 +54,8 @@ class LioWebRTC extends WildEmitter { this.config[o] = options[o]; }); - if (this.config.dataOnly) { + if (options.dataOnly) { + console.log('data only'); this.config.media.video = false; this.config.media.audio = false; } diff --git a/src/localmedia.js b/src/localmedia.js new file mode 100644 index 0000000000000000000000000000000000000000..c271ea69a488554da0a55443697cc1fdfdbdb740 --- /dev/null +++ b/src/localmedia.js @@ -0,0 +1,329 @@ +import util from 'util'; +import hark from 'hark'; +import getScreenMedia from 'getscreenmedia'; +import WildEmitter from 'wildemitter'; +import mockconsole from 'mockconsole'; + +function isAllTracksEnded(stream) { + let isAllTracksEnded = true; + stream.getTracks().forEach(t => { + isAllTracksEnded = t.readyState === 'ended' && isAllTracksEnded; + }); + return isAllTracksEnded; +} + +function shouldWorkAroundFirefoxStopStream() { + if (typeof window === 'undefined') { + return false; + } + if (!window.navigator.mozGetUserMedia) { + return false; + } + const match = window.navigator.userAgent.match(/Firefox\/(\d+)\./); + const version = match && match.length >= 1 && parseInt(match[1], 10); + return version < 50; +} + +class LocalMedia extends WildEmitter { + constructor(opts) { + super(); + const config = this.config = { + detectSpeakingEvents: false, + audioFallback: false, + media: { + audio: true, + video: true + }, + harkOptions: null, + logger: mockconsole + }; + + let 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)); + } + + start(mediaConstraints, cb) { + const self = this; + const constraints = mediaConstraints || this.config.media; + + this.emit('localStreamRequested', constraints); + + navigator.mediaDevices.getUserMedia(constraints).then(stream => { + if (constraints.audio && self.config.detectSpeakingEvents) { + self._setupAudioMonitor(stream, self.config.harkOptions); + } + self.localStreams.push(stream); + + stream.getTracks().forEach(track => { + track.addEventListener('ended', () => { + if (isAllTracksEnded(stream)) { + self._removeStream(stream); + } + }); + }); + + self.emit('localStream', stream); + + if (cb) { + return cb(null, stream); + } + }).catch(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); + } + }); + } + + stop(stream) { + this.stopStream(stream); + this.stopScreenShare(stream); + } + + stopStream(stream) { + const self = this; + + if (stream) { + const idx = this.localStreams.indexOf(stream); + if (idx > -1) { + stream.getTracks().forEach(track => { track.stop(); }); + + //Half-working fix for Firefox, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1208373 + if (shouldWorkAroundFirefoxStopStream()) { + this._removeStream(stream); + } + } + } else { + this.localStreams.forEach(stream => { + stream.getTracks().forEach(track => { track.stop(); }); + + //Half-working fix for Firefox, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1208373 + if (shouldWorkAroundFirefoxStopStream()) { + self._removeStream(stream); + } + }); + } + } + + startScreenShare(constraints, cb) { + const self = this; + + this.emit('localScreenRequested'); + + if (typeof constraints === 'function' && !cb) { + cb = constraints; + constraints = null; + } + + getScreenMedia(constraints, (err, stream) => { + if (!err) { + self.localScreens.push(stream); + + stream.getTracks().forEach(track => { + track.addEventListener('ended', () => { + let isAllTracksEnded = true; + stream.getTracks().forEach(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); + } + }); + } + + stopScreenShare(stream) { + const self = this; + + if (stream) { + const idx = this.localScreens.indexOf(stream); + if (idx > -1) { + stream.getTracks().forEach(track => { track.stop(); }); + + //Half-working fix for Firefox, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1208373 + if (shouldWorkAroundFirefoxStopStream()) { + this._removeStream(stream); + } + } + } else { + this.localScreens.forEach(stream => { + stream.getTracks().forEach(track => { track.stop(); }); + + //Half-working fix for Firefox, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1208373 + if (shouldWorkAroundFirefoxStopStream()) { + self._removeStream(stream); + } + }); + } + } + + // Audio controls + mute() { + this._audioEnabled(false); + this.emit('audioOff'); + } + + unmute() { + this._audioEnabled(true); + this.emit('audioOn'); + } + + // Video controls + pauseVideo() { + this._videoEnabled(false); + this.emit('videoOff'); + } + + resumeVideo() { + this._videoEnabled(true); + this.emit('videoOn'); + } + + // Combined controls + pause() { + this.mute(); + this.pauseVideo(); + } + + resume() { + this.unmute(); + this.resumeVideo(); + } + + // Internal methods for enabling/disabling audio/video + _audioEnabled(bool) { + this.localStreams.forEach(stream => { + stream.getAudioTracks().forEach(track => { + track.enabled = !!bool; + }); + }); + } + + _videoEnabled(bool) { + this.localStreams.forEach(stream => { + stream.getVideoTracks().forEach(track => { + track.enabled = !!bool; + }); + }); + } + + // check if all audio streams are enabled + isAudioEnabled() { + let enabled = true; + this.localStreams.forEach(stream => { + stream.getAudioTracks().forEach(track => { + enabled = enabled && track.enabled; + }); + }); + return enabled; + } + + // check if all video streams are enabled + isVideoEnabled() { + let enabled = true; + this.localStreams.forEach(stream => { + stream.getVideoTracks().forEach(track => { + enabled = enabled && track.enabled; + }); + }); + return enabled; + } + + _removeStream(stream) { + let 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); + } + } + } + + _setupAudioMonitor(stream, harkOptions) { + this._log('Setup audio'); + const audio = hark(stream, harkOptions); + const self = this; + let timeout; + + audio.on('speaking', () => { + self.emit('speaking'); + }); + + audio.on('stopped_speaking', () => { + if (timeout) { + clearTimeout(timeout); + } + + timeout = setTimeout(() => { + self.emit('stoppedSpeaking'); + }, 1000); + }); + audio.on('volume_change', (volume, threshold) => { + self.emit('volumeChange', volume, threshold); + }); + + this._audioMonitors.push({audio, stream}); + } + + _stopAudioMonitor(stream) { + let idx = -1; + this._audioMonitors.forEach((monitors, i) => { + if (monitors.stream === stream) { + idx = i; + } + }); + + if (idx > -1) { + this._audioMonitors[idx].audio.stop(); + this._audioMonitors.splice(idx, 1); + } + } +} + +util.inherits(LocalMedia, WildEmitter); + + +export default LocalMedia; diff --git a/src/webrtc.js b/src/webrtc.js index c34d4e85fb307937cf8869e7ca37e17eb6b66dc9..970339a90d27ceafce4ff1810333f0f1b5d115a6 100644 --- a/src/webrtc.js +++ b/src/webrtc.js @@ -1,11 +1,12 @@ import util from 'util'; import mockconsole from 'mockconsole'; -import localMedia from 'localmedia'; +import LocalMedia from './localmedia'; import Peer from './peer'; import webrtcSupport from './webrtcsupport'; -class WebRTC { +class WebRTC extends LocalMedia { constructor(opts) { + super(opts); const self = this; const options = opts || {}; const config = this.config = { @@ -52,7 +53,7 @@ class WebRTC { this.peers = []; // call localMedia constructor - localMedia.call(this, this.config); + // localMedia.call(this, this.config); this.on('speaking', () => { if (!self.hardMuted) { @@ -150,6 +151,4 @@ class WebRTC { } } -util.inherits(WebRTC, localMedia); - export default WebRTC;