Browse Source

Switch local audio and video track when list of available devices changes

master
Kostiantyn Tsaregradskyi 9 years ago
parent
commit
f57a75b412

+ 24
- 4
JitsiMediaDevices.js View File

@@ -1,6 +1,7 @@
1 1
 var EventEmitter = require("events");
2 2
 var RTCEvents = require('./service/RTC/RTCEvents');
3 3
 var RTC = require("./modules/RTC/RTC");
4
+var MediaType = require('./service/RTC/MediaType');
4 5
 var JitsiMediaDevicesEvents = require('./JitsiMediaDevicesEvents');
5 6
 
6 7
 var eventEmitter = new EventEmitter();
@@ -28,7 +29,7 @@ var JitsiMediaDevices = {
28 29
     /**
29 30
      * Returns true if changing the input (camera / microphone) or output
30 31
      * (audio) device is supported and false if not.
31
-     * @params {string} [deviceType] - type of device to change. Default is
32
+     * @param {string} [deviceType] - type of device to change. Default is
32 33
      *      undefined or 'input', 'output' - for audio output device change.
33 34
      * @returns {boolean} true if available, false otherwise.
34 35
      */
@@ -36,8 +37,26 @@ var JitsiMediaDevices = {
36 37
         return RTC.isDeviceChangeAvailable(deviceType);
37 38
     },
38 39
     /**
39
-     * Returns currently used audio output device id, '' stands for default
40
-     * device
40
+     * Returns true if user granted permission to media devices.
41
+     * @param {'audio'|'video'} [type] - type of devices to check,
42
+     *      undefined stands for both 'audio' and 'video' together
43
+     * @returns {boolean}
44
+     */
45
+    isDevicePermissionGranted: function (type) {
46
+        var permissions = RTC.getDeviceAvailability();
47
+
48
+        switch(type) {
49
+            case MediaType.VIDEO:
50
+                return permissions.video === true;
51
+            case MediaType.AUDIO:
52
+                return permissions.audio === true;
53
+            default:
54
+                return permissions.video === true && permissions.audio === true;
55
+        }
56
+    },
57
+    /**
58
+     * Returns currently used audio output device id, 'default' stands
59
+     * for default device
41 60
      * @returns {string}
42 61
      */
43 62
     getAudioOutputDevice: function () {
@@ -46,7 +65,8 @@ var JitsiMediaDevices = {
46 65
     /**
47 66
      * Sets current audio output device.
48 67
      * @param {string} deviceId - id of 'audiooutput' device from
49
-     *      navigator.mediaDevices.enumerateDevices(), '' is for default device
68
+     *      navigator.mediaDevices.enumerateDevices(), 'default' is for
69
+     *      default device
50 70
      * @returns {Promise} - resolves when audio output is changed, is rejected
51 71
      *      otherwise
52 72
      */

+ 5
- 0
doc/API.md View File

@@ -81,6 +81,7 @@ JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.ERROR);
81 81
         - groupId - group identifier, two devices have the same group identifier if they belong to the same physical device; for example a monitor with both a built-in camera and microphone
82 82
     - ```setAudioOutputDevice(deviceId)``` - sets current audio output device. ```deviceId``` - id of 'audiooutput' device from ```JitsiMeetJS.enumerateDevices()```, '' is for default device.
83 83
     - ```getAudioOutputDevice()``` - returns currently used audio output device id, '' stands for default device.
84
+    - ```isDevicePermissionGranted(type)``` - returns true if user granted permission to media devices. ```type``` - 'audio', 'video' or ```undefined```. In case of ```undefined``` will check if both audio and video permissions were granted.
84 85
     - ```addEventListener(event, handler)``` - attaches an event handler.
85 86
     - ```removeEventListener(event, handler)``` - removes an event handler.
86 87
 
@@ -355,6 +356,10 @@ We have the following methods for controling the tracks:
355 356
    
356 357
 10. setAudioOutput(audioOutputDeviceId) - sets new audio output device for track's DOM elements. Video tracks are ignored.
357 358
 
359
+11. getDeviceId() - returns device ID associated with track (for local tracks only)
360
+
361
+12. isEnded() - returns true if track is ended
362
+
358 363
 
359 364
 Getting Started
360 365
 ==============

+ 75
- 4
modules/RTC/JitsiLocalTrack.js View File

@@ -4,6 +4,7 @@ var JitsiTrack = require("./JitsiTrack");
4 4
 var RTCBrowserType = require("./RTCBrowserType");
5 5
 var JitsiTrackEvents = require('../../JitsiTrackEvents');
6 6
 var JitsiTrackErrors = require("../../JitsiTrackErrors");
7
+var RTCEvents = require("../../service/RTC/RTCEvents");
7 8
 var RTCUtils = require("./RTCUtils");
8 9
 var VideoType = require('../../service/RTC/VideoType');
9 10
 
@@ -20,6 +21,8 @@ var VideoType = require('../../service/RTC/VideoType');
20 21
  */
21 22
 function JitsiLocalTrack(stream, track, mediaType, videoType, resolution,
22 23
                          deviceId) {
24
+    var self = this;
25
+
23 26
     JitsiTrack.call(this,
24 27
         null /* RTC */, stream, track,
25 28
         function () {
@@ -33,16 +36,72 @@ function JitsiLocalTrack(stream, track, mediaType, videoType, resolution,
33 36
     this.resolution = resolution;
34 37
     this.deviceId = deviceId;
35 38
     this.startMuted = false;
36
-    this.disposed = false;
37 39
     //FIXME: This dependacy is not necessary.
38 40
     this.conference = null;
39 41
     this.initialMSID = this.getMSID();
40 42
     this.inMuteOrUnmuteProgress = false;
43
+
44
+    // Currently there is no way to know the MediaStreamTrack ended due to to
45
+    // device disconnect in Firefox through e.g. "readyState" property. Instead
46
+    // we will compare current track's label with device labels from
47
+    // enumerateDevices() list.
48
+    this._trackEnded = false;
49
+
50
+    // Currently there is no way to determine with what device track was
51
+    // created (until getConstraints() support), however we can associate tracks
52
+    // with real devices obtained from enumerateDevices() call as soon as it's
53
+    // called.
54
+    this._realDeviceId = this.deviceId === '' ? undefined : this.deviceId;
55
+
56
+    this._onDeviceListChanged = function (devices) {
57
+        self._setRealDeviceIdFromDeviceList(devices);
58
+
59
+        // Mark track as ended for those browsers that do not support
60
+        // "readyState" property. We do not touch tracks created with default
61
+        // device ID "".
62
+        if (typeof self.getTrack().readyState === 'undefined'
63
+            && typeof self._realDeviceId !== 'undefined'
64
+            && !devices.find(function (d) {
65
+                return d.deviceId === self._realDeviceId;
66
+            })) {
67
+            self._trackEnded = true;
68
+        }
69
+    };
70
+
71
+
72
+    RTCUtils.addListener(RTCEvents.DEVICE_LIST_CHANGED,
73
+        this._onDeviceListChanged);
41 74
 }
42 75
 
43 76
 JitsiLocalTrack.prototype = Object.create(JitsiTrack.prototype);
44 77
 JitsiLocalTrack.prototype.constructor = JitsiLocalTrack;
45 78
 
79
+/**
80
+ * Returns if associated MediaStreamTrack is in the 'ended' state
81
+ * @returns {boolean}
82
+ */
83
+JitsiLocalTrack.prototype.isEnded = function () {
84
+    return  this.getTrack().readyState === 'ended' || this._trackEnded;
85
+};
86
+
87
+/**
88
+ * Sets real device ID by comparing track information with device information.
89
+ * This is temporary solution until getConstraints() method will be implemented
90
+ * in browsers.
91
+ * @param {MediaDeviceInfo[]} devices - list of devices obtained from
92
+ *  enumerateDevices() call
93
+ */
94
+JitsiLocalTrack.prototype._setRealDeviceIdFromDeviceList = function (devices) {
95
+    var track = this.getTrack(),
96
+        device = devices.find(function (d) {
97
+            return d.kind === track.kind + 'input' && d.label === track.label;
98
+        });
99
+
100
+    if (device) {
101
+        this._realDeviceId = device.deviceId;
102
+    }
103
+};
104
+
46 105
 /**
47 106
  * Mutes the track. Will reject the Promise if there is mute/unmute operation
48 107
  * in progress.
@@ -150,9 +209,9 @@ JitsiLocalTrack.prototype._setMute = function (mute, resolve, reject) {
150 209
                 resolution: self.resolution
151 210
             };
152 211
             if (isAudio) {
153
-                streamOptions['micDeviceId'] = self.deviceId;
212
+                streamOptions['micDeviceId'] = self.getDeviceId();
154 213
             } else if(self.videoType === VideoType.CAMERA) {
155
-                streamOptions['cameraDeviceId'] = self.deviceId;
214
+                streamOptions['cameraDeviceId'] = self.getDeviceId();
156 215
             }
157 216
             RTCUtils.obtainAudioAndVideoPermissions(streamOptions)
158 217
                 .then(function (streamsInfo) {
@@ -222,7 +281,11 @@ JitsiLocalTrack.prototype.dispose = function () {
222 281
         RTCUtils.stopMediaStream(this.stream);
223 282
         this.detach();
224 283
     }
225
-    this.disposed = true;
284
+
285
+    JitsiTrack.prototype.dispose.call(this);
286
+
287
+    RTCUtils.removeListener(RTCEvents.DEVICE_LIST_CHANGED,
288
+        this._onDeviceListChanged);
226 289
 
227 290
     return promise;
228 291
 };
@@ -302,4 +365,12 @@ JitsiLocalTrack.prototype.isLocal = function () {
302 365
     return true;
303 366
 };
304 367
 
368
+/**
369
+ * Returns device id associated with track.
370
+ * @returns {string}
371
+ */
372
+JitsiLocalTrack.prototype.getDeviceId = function () {
373
+    return this._realDeviceId || this.deviceId;
374
+};
375
+
305 376
 module.exports = JitsiLocalTrack;

+ 1
- 3
modules/RTC/JitsiRemoteTrack.js View File

@@ -14,7 +14,7 @@ var JitsiTrackEvents = require("../../JitsiTrackEvents");
14 14
  * @constructor
15 15
  */
16 16
 function JitsiRemoteTrack(RTC, ownerJid, stream, track, mediaType, videoType,
17
-                          ssrc, muted) {    
17
+                          ssrc, muted) {
18 18
     JitsiTrack.call(
19 19
         this, RTC, stream, track, function () {}, mediaType, videoType, ssrc);
20 20
     this.rtc = RTC;
@@ -84,6 +84,4 @@ JitsiRemoteTrack.prototype._setVideoType = function (type) {
84 84
     this.eventEmitter.emit(JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED, type);
85 85
 };
86 86
 
87
-delete JitsiRemoteTrack.prototype.dispose;
88
-
89 87
 module.exports = JitsiRemoteTrack;

+ 12
- 2
modules/RTC/JitsiTrack.js View File

@@ -70,6 +70,8 @@ function JitsiTrack(rtc, stream, track, streamInactiveHandler, trackMediaType,
70 70
     this.type = trackMediaType;
71 71
     this.track = track;
72 72
     this.videoType = videoType;
73
+    this.disposed = false;
74
+
73 75
     if(stream) {
74 76
         if (RTCBrowserType.isFirefox()) {
75 77
             implementOnEndedHandling(this);
@@ -77,7 +79,12 @@ function JitsiTrack(rtc, stream, track, streamInactiveHandler, trackMediaType,
77 79
         addMediaStreamInactiveHandler(stream, streamInactiveHandler);
78 80
     }
79 81
 
80
-    RTCUtils.addListener(RTCEvents.AUDIO_OUTPUT_DEVICE_CHANGED, this.setAudioOutput.bind(this));
82
+    this._onAudioOutputDeviceChanged = this.setAudioOutput.bind(this);
83
+
84
+    if (this.isAudioTrack()) {
85
+        RTCUtils.addListener(RTCEvents.AUDIO_OUTPUT_DEVICE_CHANGED,
86
+            this._onAudioOutputDeviceChanged);
87
+    }
81 88
 }
82 89
 
83 90
 /**
@@ -218,9 +225,12 @@ JitsiTrack.prototype.detach = function (container) {
218 225
 
219 226
 /**
220 227
  * Dispose sending the media track. And removes it from the HTML.
221
- * NOTE: Works for local tracks only.
222 228
  */
223 229
 JitsiTrack.prototype.dispose = function () {
230
+    RTCUtils.removeListener(RTCEvents.AUDIO_OUTPUT_DEVICE_CHANGED,
231
+        this._onAudioOutputDeviceChanged);
232
+
233
+    this.disposed = true;
224 234
 };
225 235
 
226 236
 /**

+ 5
- 1
modules/RTC/RTC.js View File

@@ -277,7 +277,11 @@ RTC.prototype.createRemoteTrack = function (event) {
277 277
  * @returns {JitsiRemoteTrack|null}
278 278
  */
279 279
 RTC.prototype.removeRemoteTracks = function (resource) {
280
-    if(this.remoteTracks[resource]) {
280
+    var remoteTracks = this.remoteTracks[resource];
281
+
282
+    if(remoteTracks) {
283
+        remoteTracks['audio'] && remoteTracks['audio'].dispose();
284
+        remoteTracks['video'] && remoteTracks['video'].dispose();
281 285
         delete this.remoteTracks[resource];
282 286
     }
283 287
 };

+ 89
- 70
modules/RTC/RTCUtils.js View File

@@ -22,11 +22,13 @@ var eventEmitter = new EventEmitter();
22 22
 var AVAILABLE_DEVICES_POLL_INTERVAL_TIME = 3000; // ms
23 23
 
24 24
 var devices = {
25
-    audio: true,
26
-    video: true
25
+    audio: false,
26
+    video: false
27 27
 };
28 28
 
29
-var audioOuputDeviceId = ''; // default device
29
+// Currently audio output device change is supported only in Chrome and
30
+// default output always has 'default' device ID
31
+var audioOutputDeviceId = 'default'; // default device
30 32
 
31 33
 var featureDetectionAudioEl = document.createElement('audio');
32 34
 var isAudioOutputDeviceChangeAvailable =
@@ -34,6 +36,21 @@ var isAudioOutputDeviceChangeAvailable =
34 36
 
35 37
 var currentlyAvailableMediaDevices = [];
36 38
 
39
+var rawEnumerateDevicesWithCallback = navigator.mediaDevices
40
+    && navigator.mediaDevices.enumerateDevices
41
+        ? function(callback) {
42
+            navigator.mediaDevices.enumerateDevices().then(callback, function () {
43
+                callback([]);
44
+            });
45
+        }
46
+        : (MediaStreamTrack && MediaStreamTrack.getSources)
47
+            ? function (callback) {
48
+                MediaStreamTrack.getSources(function (sources) {
49
+                    callback(sources.map(convertMediaStreamTrackSource));
50
+                });
51
+            }
52
+            : undefined;
53
+
37 54
 // TODO: currently no browser supports 'devicechange' event even in nightly
38 55
 // builds so no feature/browser detection is used at all. However in future this
39 56
 // should be changed to some expression. Progress on 'devicechange' event
@@ -262,14 +279,20 @@ function compareAvailableMediaDevices(newDevices) {
262 279
  * will be supported by browsers.
263 280
  */
264 281
 function pollForAvailableMediaDevices() {
265
-    RTCUtils.enumerateDevices(function (devices) {
266
-        if (compareAvailableMediaDevices(devices)) {
267
-            onMediaDevicesListChanged(devices);
268
-        }
282
+    // Here we use plain navigator.mediaDevices.enumerateDevices instead of
283
+    // wrapped because we just need to know the fact the devices changed, labels
284
+    // do not matter. This fixes situation when we have no devices initially,
285
+    // and then plug in a new one.
286
+    if (rawEnumerateDevicesWithCallback) {
287
+        rawEnumerateDevicesWithCallback(function (devices) {
288
+            if (compareAvailableMediaDevices(devices)) {
289
+                onMediaDevicesListChanged(devices);
290
+            }
269 291
 
270
-        window.setTimeout(pollForAvailableMediaDevices,
271
-            AVAILABLE_DEVICES_POLL_INTERVAL_TIME);
272
-    });
292
+            window.setTimeout(pollForAvailableMediaDevices,
293
+                AVAILABLE_DEVICES_POLL_INTERVAL_TIME);
294
+        });
295
+    }
273 296
 }
274 297
 
275 298
 /**
@@ -278,9 +301,35 @@ function pollForAvailableMediaDevices() {
278 301
  * @emits RTCEvents.DEVICE_LIST_CHANGED
279 302
  */
280 303
 function onMediaDevicesListChanged(devices) {
281
-    currentlyAvailableMediaDevices = devices;
304
+    currentlyAvailableMediaDevices = devices.slice(0);
305
+    logger.info('list of media devices has changed:', currentlyAvailableMediaDevices);
306
+
307
+    var videoInputDevices = currentlyAvailableMediaDevices.filter(function (d) {
308
+            return d.kind === 'videoinput';
309
+        }),
310
+        audioInputDevices = currentlyAvailableMediaDevices.filter(function (d) {
311
+            return d.kind === 'audioinput';
312
+        }),
313
+        videoInputDevicesWithEmptyLabels = videoInputDevices.filter(
314
+            function (d) {
315
+                return d.label === '';
316
+            }),
317
+        audioInputDevicesWithEmptyLabels = audioInputDevices.filter(
318
+            function (d) {
319
+                return d.label === '';
320
+            });
321
+
322
+    if (videoInputDevices.length &&
323
+        videoInputDevices.length === videoInputDevicesWithEmptyLabels.length) {
324
+        setAvailableDevices(['video'], false);
325
+    }
326
+
327
+    if (audioInputDevices.length &&
328
+        audioInputDevices.length === audioInputDevicesWithEmptyLabels.length) {
329
+        setAvailableDevices(['audio'], false);
330
+    }
331
+
282 332
     eventEmitter.emit(RTCEvents.DEVICE_LIST_CHANGED, devices);
283
-    logger.info('list of media devices has changed:', devices);
284 333
 }
285 334
 
286 335
 // In case of IE we continue from 'onReady' callback
@@ -340,22 +389,6 @@ function wrapGetUserMedia(getUserMedia) {
340 389
   };
341 390
 }
342 391
 
343
-/**
344
- * Create stub device which equals to auto selected device.
345
- * @param {string} kind if that should be `audio` or `video` device
346
- * @returns {Object} stub device description in `enumerateDevices` format
347
- */
348
-function createAutoDeviceInfo(kind) {
349
-    return {
350
-        facing: null,
351
-        label: 'Auto',
352
-        kind: kind,
353
-        deviceId: '',
354
-        groupId: ''
355
-    };
356
-}
357
-
358
-
359 392
 /**
360 393
  * Execute function after getUserMedia was executed at least once.
361 394
  * @param {Function} callback function to execute after getUserMedia
@@ -379,24 +412,10 @@ function wrapEnumerateDevices(enumerateDevices) {
379 412
         // enumerate devices only after initial getUserMedia
380 413
         afterUserMediaInitialized(function () {
381 414
 
382
-            enumerateDevices().then(function (devices) {
383
-                //add auto devices
384
-                devices.unshift(
385
-                    createAutoDeviceInfo('audioinput'),
386
-                    createAutoDeviceInfo('videoinput'),
387
-                    createAutoDeviceInfo('audiooutput')
388
-                );
389
-
390
-                callback(devices);
391
-            }, function (err) {
415
+            enumerateDevices().then(callback, function (err) {
392 416
                 console.error('cannot enumerate devices: ', err);
393 417
 
394
-                // return only auto devices
395
-                callback([
396
-                    createAutoDeviceInfo('audioinput'),
397
-                    createAutoDeviceInfo('videoinput'),
398
-                    createAutoDeviceInfo('audiooutput')
399
-                ]);
418
+                callback([]);
400 419
             });
401 420
         });
402 421
     };
@@ -409,32 +428,31 @@ function wrapEnumerateDevices(enumerateDevices) {
409 428
  */
410 429
 function enumerateDevicesThroughMediaStreamTrack (callback) {
411 430
     MediaStreamTrack.getSources(function (sources) {
412
-        var devices = sources.map(function (source) {
413
-            var kind = (source.kind || '').toLowerCase();
414
-            return {
415
-                facing: source.facing || null,
416
-                label: source.label,
417
-                // theoretically deprecated MediaStreamTrack.getSources should
418
-                // not return 'audiooutput' devices but let's handle it in any
419
-                // case
420
-                kind: kind
421
-                    ? (kind === 'audiooutput' ? kind : kind + 'input')
422
-                    : null,
423
-                deviceId: source.id,
424
-                groupId: source.groupId || null
425
-            };
426
-        });
427
-
428
-        //add auto devices
429
-        devices.unshift(
430
-            createAutoDeviceInfo('audioinput'),
431
-            createAutoDeviceInfo('videoinput'),
432
-            createAutoDeviceInfo('audiooutput')
433
-        );
434
-        callback(devices);
431
+        callback(sources.map(convertMediaStreamTrackSource));
435 432
     });
436 433
 }
437 434
 
435
+/**
436
+ * Converts MediaStreamTrack Source to enumerateDevices format.
437
+ * @param {Object} source
438
+ */
439
+function convertMediaStreamTrackSource(source) {
440
+    var kind = (source.kind || '').toLowerCase();
441
+
442
+    return {
443
+        facing: source.facing || null,
444
+        label: source.label,
445
+        // theoretically deprecated MediaStreamTrack.getSources should
446
+        // not return 'audiooutput' devices but let's handle it in any
447
+        // case
448
+        kind: kind
449
+            ? (kind === 'audiooutput' ? kind : kind + 'input')
450
+            : null,
451
+        deviceId: source.id,
452
+        groupId: source.groupId || null
453
+    };
454
+}
455
+
438 456
 function obtainDevices(options) {
439 457
     if(!options.devices || options.devices.length === 0) {
440 458
         return options.successCallback(options.streams || {});
@@ -975,7 +993,8 @@ var RTCUtils = {
975 993
     /**
976 994
      * Sets current audio output device.
977 995
      * @param {string} deviceId - id of 'audiooutput' device from
978
-     *      navigator.mediaDevices.enumerateDevices(), '' for default device
996
+     *      navigator.mediaDevices.enumerateDevices(), 'default' for default
997
+     *      device
979 998
      * @returns {Promise} - resolves when audio output is changed, is rejected
980 999
      *      otherwise
981 1000
      */
@@ -987,7 +1006,7 @@ var RTCUtils = {
987 1006
 
988 1007
         return featureDetectionAudioEl.setSinkId(deviceId)
989 1008
             .then(function() {
990
-                audioOuputDeviceId = deviceId;
1009
+                audioOutputDeviceId = deviceId;
991 1010
 
992 1011
                 logger.log('Audio output device set to ' + deviceId);
993 1012
 
@@ -1001,7 +1020,7 @@ var RTCUtils = {
1001 1020
      * @returns {string}
1002 1021
      */
1003 1022
     getAudioOutputDevice: function () {
1004
-        return audioOuputDeviceId;
1023
+        return audioOutputDeviceId;
1005 1024
     }
1006 1025
 };
1007 1026
 

Loading…
Cancel
Save