Browse Source

Added ability to switch audio output device

dev1
Kostiantyn Tsaregradskyi 9 years ago
parent
commit
4a17d57fc6

+ 25
- 0
JitsiMeetJS.js View File

@@ -147,6 +147,31 @@ var LibJitsiMeet = {
147 147
     isDeviceChangeAvailable: function () {
148 148
         return RTC.isDeviceChangeAvailable();
149 149
     },
150
+    /**
151
+     * Returns true if changing the audio output of media elements is supported
152
+     * and false if not.
153
+     */
154
+    isAudioOutputDeviceChangeAvailable: function () {
155
+        return RTC.isAudioOutputDeviceChangeAvailable();
156
+    },
157
+    /**
158
+     * Returns currently used audio output device id, '' stands for default
159
+     * device
160
+     * @returns {string}
161
+     */
162
+    getAudioOutputDevice: function () {
163
+        return RTC.getAudioOutputDevice();
164
+    },
165
+    /**
166
+     * Sets current audio output device.
167
+     * @param {string} deviceId - id of 'audiooutput' device from
168
+     *      navigator.mediaDevices.enumerateDevices()
169
+     * @returns {Promise} - resolves when audio output is changed, is rejected
170
+     *      otherwise
171
+     */
172
+    setAudioOutputDevice: function (deviceId) {
173
+        return RTC.setAudioOutputDevice(deviceId);
174
+    },
150 175
     enumerateDevices: function (callback) {
151 176
         RTC.enumerateDevices(callback);
152 177
     },

+ 5
- 1
JitsiTrackEvents.js View File

@@ -14,7 +14,11 @@ var JitsiTrackEvents = {
14 14
     /**
15 15
      * The video type("camera" or "desktop") of the track was changed.
16 16
      */
17
-     TRACK_VIDEOTYPE_CHANGED: "track.videoTypeChanged"
17
+    TRACK_VIDEOTYPE_CHANGED: "track.videoTypeChanged",
18
+    /**
19
+     * The audio output of the track was changed.
20
+     */
21
+    TRACK_AUDIO_OUTPUT_CHANGED: "track.audioOutputChanged"
18 22
 };
19 23
 
20 24
 module.exports = JitsiTrackEvents;

+ 29
- 0
doc/example/example.js View File

@@ -36,6 +36,10 @@ function onLocalTracks(tracks)
36 36
             function () {
37 37
                 console.log("local track stoped");
38 38
             });
39
+        localTracks[i].addEventListener(JitsiMeetJS.events.track.TRACK_AUDIO_OUTPUT_CHANGED,
40
+            function (deviceId) {
41
+                console.log("track audio output device was changed to " + deviceId);
42
+            });
39 43
         if(localTracks[i].getType() == "video") {
40 44
             $("body").append("<video autoplay='1' id='localVideo" + i + "' />");
41 45
             localTracks[i].attach($("#localVideo" + i)[0]);
@@ -71,6 +75,10 @@ function onRemoteTrack(track) {
71 75
         function () {
72 76
             console.log("remote track stoped");
73 77
         });
78
+    track.addEventListener(JitsiMeetJS.events.track.TRACK_AUDIO_OUTPUT_CHANGED,
79
+        function (deviceId) {
80
+            console.log("track audio output device was changed to " + deviceId);
81
+        });
74 82
     var id = participant + track.getType() + idx;
75 83
     if(track.getType() == "video") {
76 84
         $("body").append("<video autoplay='1' id='" + participant + "video" + idx + "' />");
@@ -180,6 +188,10 @@ function switchVideo() {
180 188
         });
181 189
 }
182 190
 
191
+function changeAudioOutput(selected) {
192
+    JitsiMeetJS.setAudioOutputDevice(selected.value);
193
+}
194
+
183 195
 $(window).bind('beforeunload', unload);
184 196
 $(window).bind('unload', unload);
185 197
 
@@ -228,6 +240,23 @@ JitsiMeetJS.init(initOptions).then(function(){
228 240
     console.log(error);
229 241
 });
230 242
 
243
+
244
+if (JitsiMeetJS.isAudioOutputDeviceChangeAvailable()) {
245
+    JitsiMeetJS.enumerateDevices(function(devices) {
246
+        var audioOutputDevices = devices.filter(function(d) { return d.kind === 'audiooutput'; });
247
+
248
+        if (audioOutputDevices.length > 1) {
249
+            $('#audioOutputSelect').html(
250
+                audioOutputDevices.map(function (d) {
251
+                    return '<option value="' + d.deviceId + '">' + d.label + '</option>';
252
+                }).join('\n')
253
+            );
254
+
255
+            $('#audioOutputSelectWrapper').show();
256
+        }
257
+    })
258
+}
259
+
231 260
 var connection = null;
232 261
 var room = null;
233 262
 var localTracks = [];

+ 4
- 0
doc/example/index.html View File

@@ -13,6 +13,10 @@
13 13
 <body>
14 14
     <a href="#" onclick="unload()">Unload</a>
15 15
     <a href="#" onclick="switchVideo()">switchVideo</a>
16
+    <div id="audioOutputSelectWrapper" style="display: none;">
17
+        Change audio output device
18
+        <select id="audioOutputSelect" onchange="changeAudioOutput(this)"></select>
19
+    </div>
16 20
     <!-- <video id="localVideo" autoplay="true"></video> -->
17 21
     <!--<audio id="localAudio" autoplay="true" muted="true"></audio>-->
18 22
 </body>

+ 44
- 30
modules/RTC/JitsiTrack.js View File

@@ -76,6 +76,8 @@ function JitsiTrack(rtc, stream, track, streamInactiveHandler, trackMediaType,
76 76
         }
77 77
         addMediaStreamInactiveHandler(stream, streamInactiveHandler);
78 78
     }
79
+
80
+    RTCUtils.addListener(RTCEvents.AUDIO_OUTPUT_DEVICE_CHANGED, this.setAudioOutput.bind(this));
79 81
 }
80 82
 
81 83
 /**
@@ -190,36 +192,6 @@ JitsiTrack.prototype.attach = function (container) {
190 192
     return container;
191 193
 };
192 194
 
193
-JitsiTrack.prototype.changeAudioOutput = function (audioOutputDeviceId) {
194
-    return Promise.all(this.containers.map(function(element) {
195
-        if (typeof element.setSinkId !== 'undefined') {
196
-            try {
197
-                return element.setSinkId(audioOutputDeviceId)
198
-                    .then(function () {
199
-                        console.log('Audio output device changed on element ' + element);
200
-                    })
201
-                    .catch(function (error) {
202
-                        var errorMessage = error;
203
-
204
-                        if (error.name === 'SecurityError') {
205
-                            errorMessage = 'You need to use HTTPS for selecting audio output device: ' + error;
206
-                        }
207
-
208
-                        console.error('Failed to change audio output device on element ' + element + ': ' + errorMessage);
209
-
210
-                        return Promise.resolve();
211
-                    });
212
-            } catch(ex) {
213
-                console.error(ex);
214
-                return Promise.resolve();
215
-            }
216
-        } else {
217
-            console.warn('Browser does not support output device selection.');
218
-            return Promise.resolve();
219
-        }
220
-    }));
221
-};
222
-
223 195
 /**
224 196
  * Removes the track from the passed HTML container.
225 197
  * @param container the HTML container. If <tt>null</tt> all containers are removed.
@@ -340,4 +312,46 @@ JitsiTrack.prototype.getMSID = function () {
340 312
     return (streamId && trackId) ? (streamId + " " + trackId) : null;
341 313
 };
342 314
 
315
+/**
316
+ * Set new audio output device for track's DOM elements.
317
+ * @param {string} audioOutputDeviceId - id of 'audiooutput' device from
318
+ *      navigator.mediaDevices.enumerateDevices()
319
+ * @emits JitsiTrackEvents.TRACK_AUDIO_OUTPUT_CHANGED
320
+ * @returns {Promise}
321
+ */
322
+JitsiTrack.prototype.setAudioOutput = function (audioOutputDeviceId) {
323
+    var self = this;
324
+
325
+    if (!RTCUtils.isAudioOutputDeviceChangeAvailable()) {
326
+        return Promise.reject(
327
+            new Error('Audio output device change is not supported'));
328
+    }
329
+
330
+    return Promise.all(this.containers.map(function(element) {
331
+        return element.setSinkId(audioOutputDeviceId)
332
+            .catch(function (error) {
333
+                console.error('Failed to change audio output device on element',
334
+                    element, error);
335
+
336
+                // TODO: for some reason 'AbortError' is raised on video
337
+                // elements with local track blobs. Maybe this is something
338
+                // similar to https://goo.gl/TKLiqx. Ignoring this error for
339
+                // now. Spec says that "If the device identified by the given
340
+                // sinkId cannot be used due to a unspecified error, throw a
341
+                // DOMException whose name is AbortError."
342
+                // In any case, all audio communication is done via separate
343
+                // audio elements, so maybe it doesn't make sense to change
344
+                // sinkId for <video> elements at all.
345
+                if (!(self.isVideoTrack() && self.isLocal && self.isLocal() &&
346
+                    error.name === 'AbortError')) {
347
+                    throw error;
348
+                }
349
+            });
350
+    }))
351
+    .then(function () {
352
+        self.eventEmitter.emit(JitsiTrackEvents.TRACK_AUDIO_OUTPUT_CHANGED,
353
+            audioOutputDeviceId);
354
+    });
355
+};
356
+
343 357
 module.exports = JitsiTrack;

+ 28
- 0
modules/RTC/RTC.js View File

@@ -314,6 +314,34 @@ RTC.isDeviceChangeAvailable = function () {
314 314
     return RTCUtils.isDeviceChangeAvailable();
315 315
 };
316 316
 
317
+/**
318
+ * Returns true if changing the audio output of media elements is supported
319
+ * and false if not.
320
+ */
321
+RTC.isAudioOutputDeviceChangeAvailable = function () {
322
+    return RTCUtils.isAudioOutputDeviceChangeAvailable();
323
+};
324
+
325
+/**
326
+ * Returns currently used audio output device id, '' stands for default
327
+ * device
328
+ * @returns {string}
329
+ */
330
+RTC.getAudioOutputDevice = function () {
331
+    return RTCUtils.getAudioOutputDevice();
332
+};
333
+
334
+/**
335
+ * Sets current audio output device.
336
+ * @param {string} deviceId - id of 'audiooutput' device from
337
+ *      navigator.mediaDevices.enumerateDevices()
338
+ * @returns {Promise} - resolves when audio output is changed, is rejected
339
+ *      otherwise
340
+ */
341
+RTC.setAudioOutputDevice = function (deviceId) {
342
+    return RTCUtils.setAudioOutputDevice(deviceId);
343
+};
344
+
317 345
 /**
318 346
  * Returns <tt>true<tt/> if given WebRTC MediaStream is considered a valid
319 347
  * "user" stream which means that it's not a "receive only" stream nor a "mixed"

+ 77
- 11
modules/RTC/RTCUtils.js View File

@@ -24,6 +24,10 @@ var devices = {
24 24
     video: true
25 25
 };
26 26
 
27
+var audioOuputDeviceId = ''; // default device
28
+
29
+var featureDetectionVideoEl = document.createElement('video');
30
+
27 31
 var rtcReady = false;
28 32
 
29 33
 function setResolutionConstraints(constraints, resolution) {
@@ -303,7 +307,8 @@ function wrapEnumerateDevices(enumerateDevices) {
303 307
                 //add auto devices
304 308
                 devices.unshift(
305 309
                     createAutoDeviceInfo('audioinput'),
306
-                    createAutoDeviceInfo('videoinput')
310
+                    createAutoDeviceInfo('videoinput'),
311
+                    createAutoDeviceInfo('audiooutput')
307 312
                 );
308 313
 
309 314
                 callback(devices);
@@ -311,8 +316,11 @@ function wrapEnumerateDevices(enumerateDevices) {
311 316
                 console.error('cannot enumerate devices: ', err);
312 317
 
313 318
                 // return only auto devices
314
-                callback([createAutoDeviceInfo('audioinput'),
315
-                          createAutoDeviceInfo('videoinput')]);
319
+                callback([
320
+                    createAutoDeviceInfo('audioinput'),
321
+                    createAutoDeviceInfo('videoinput'),
322
+                    createAutoDeviceInfo('audiooutput')
323
+                ]);
316 324
             });
317 325
         });
318 326
     };
@@ -341,7 +349,8 @@ function enumerateDevicesThroughMediaStreamTrack (callback) {
341 349
         //add auto devices
342 350
         devices.unshift(
343 351
             createAutoDeviceInfo('audioinput'),
344
-            createAutoDeviceInfo('videoinput')
352
+            createAutoDeviceInfo('videoinput'),
353
+            createAutoDeviceInfo('audiooutput')
345 354
         );
346 355
         callback(devices);
347 356
     });
@@ -443,6 +452,28 @@ function handleLocalStream(streams, resolution) {
443 452
     return res;
444 453
 }
445 454
 
455
+/**
456
+ * Wraps original attachMediaStream function to set current audio output device
457
+ * if this is supported.
458
+ * @param {Function} origAttachMediaStream
459
+ * @returns {Function}
460
+ */
461
+function wrapAttachMediaStream(origAttachMediaStream) {
462
+    return function(element, stream) {
463
+        var res = origAttachMediaStream.apply(RTCUtils, arguments);
464
+
465
+        if (RTCUtils.isAudioOutputDeviceChangeAvailable()) {
466
+            element.setSinkId(RTCUtils.getAudioOutputDevice())
467
+                .catch(function (ex) {
468
+                    console.error('Failed to set audio output on element',
469
+                        element, ex);
470
+                });
471
+        }
472
+
473
+        return res;
474
+    }
475
+}
476
+
446 477
 //Options parameter is to pass config options. Currently uses only "useIPv6".
447 478
 var RTCUtils = {
448 479
     init: function (options) {
@@ -463,7 +494,7 @@ var RTCUtils = {
463 494
                     navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices)
464 495
                 );
465 496
                 this.pc_constraints = {};
466
-                this.attachMediaStream = function (element, stream) {
497
+                this.attachMediaStream = wrapAttachMediaStream(function (element, stream) {
467 498
                     //  srcObject is being standardized and FF will eventually
468 499
                     //  support that unprefixed. FF also supports the
469 500
                     //  "element.src = URL.createObjectURL(...)" combo, but that
@@ -477,7 +508,7 @@ var RTCUtils = {
477 508
                     element.play();
478 509
 
479 510
                     return element;
480
-                };
511
+                });
481 512
                 this.getStreamID = function (stream) {
482 513
                     var id = stream.id;
483 514
                     if (!id) {
@@ -512,7 +543,7 @@ var RTCUtils = {
512 543
                     this.getUserMedia = getUserMedia;
513 544
                     this.enumerateDevices = enumerateDevicesThroughMediaStreamTrack;
514 545
                 }
515
-                this.attachMediaStream = function (element, stream) {
546
+                this.attachMediaStream = wrapAttachMediaStream(function (element, stream) {
516 547
 
517 548
                     // saves the created url for the stream, so we can reuse it
518 549
                     // and not keep creating urls
@@ -524,7 +555,7 @@ var RTCUtils = {
524 555
                     element.src = stream.jitsiObjectURL;
525 556
 
526 557
                     return element;
527
-                };
558
+                });
528 559
                 this.getStreamID = function (stream) {
529 560
                     // streams from FF endpoints have the characters '{' and '}'
530 561
                     // that make jQuery choke.
@@ -575,7 +606,7 @@ var RTCUtils = {
575 606
                     self.peerconnection = RTCPeerConnection;
576 607
                     self.getUserMedia = window.getUserMedia;
577 608
                     self.enumerateDevices = enumerateDevicesThroughMediaStreamTrack;
578
-                    self.attachMediaStream = function (element, stream) {
609
+                    self.attachMediaStream = wrapAttachMediaStream(function (element, stream) {
579 610
 
580 611
                         if (stream.id === "dummyAudio" || stream.id === "dummyVideo") {
581 612
                             return;
@@ -587,7 +618,7 @@ var RTCUtils = {
587 618
                         }
588 619
 
589 620
                         return attachMediaStream(element, stream);
590
-                    };
621
+                    });
591 622
                     self.getStreamID = function (stream) {
592 623
                         return SDPUtil.filter_special_chars(stream.label);
593 624
                     };
@@ -825,6 +856,13 @@ var RTCUtils = {
825 856
             RTCBrowserType.isOpera() ||
826 857
             RTCBrowserType.isTemasysPluginUsed();
827 858
     },
859
+    /**
860
+     * Returns true if changing the audio output of media elements is supported
861
+     * and false if not.
862
+     */
863
+    isAudioOutputDeviceChangeAvailable: function () {
864
+        return typeof featureDetectionVideoEl.setSinkId !== 'undefined';
865
+    },
828 866
     /**
829 867
      * A method to handle stopping of the stream.
830 868
      * One point to handle the differences in various implementations.
@@ -854,8 +892,36 @@ var RTCUtils = {
854 892
      */
855 893
     isDesktopSharingEnabled: function () {
856 894
         return screenObtainer.isSupported();
857
-    }
895
+    },
896
+    /**
897
+     * Sets current audio output device.
898
+     * @param {string} deviceId - id of 'audiooutput' device from
899
+     *      navigator.mediaDevices.enumerateDevices()
900
+     * @returns {Promise} - resolves when audio output is changed, is rejected
901
+     *      otherwise
902
+     */
903
+    setAudioOutputDevice: function (deviceId) {
904
+        if (!this.isAudioOutputDeviceChangeAvailable()) {
905
+            Promise.reject(
906
+                new Error('Audio output device change is not supported'));
907
+        }
908
+        
909
+        return featureDetectionVideoEl.setSinkId(deviceId)
910
+            .then(function() {
911
+                audioOuputDeviceId = deviceId;
858 912
 
913
+                eventEmitter.emit(RTCEvents.AUDIO_OUTPUT_DEVICE_CHANGED,
914
+                    deviceId);
915
+            });
916
+    },
917
+    /**
918
+     * Returns currently used audio output device id, '' stands for default
919
+     * device
920
+     * @returns {string}
921
+     */
922
+    getAudioOutputDevice: function () {
923
+        return audioOuputDeviceId;
924
+    }
859 925
 };
860 926
 
861 927
 module.exports = RTCUtils;

+ 2
- 1
service/RTC/RTCEvents.js View File

@@ -6,7 +6,8 @@ var RTCEvents = {
6 6
     LASTN_ENDPOINT_CHANGED: "rtc.lastn_endpoint_changed",
7 7
     AVAILABLE_DEVICES_CHANGED: "rtc.available_devices_changed",
8 8
     FAKE_VIDEO_TRACK_CREATED: "rtc.fake_video_track_created",
9
-    TRACK_ATTACHED: "rtc.track_attached"
9
+    TRACK_ATTACHED: "rtc.track_attached",
10
+    AUDIO_OUTPUT_DEVICE_CHANGED: "rtc.audio_output_device_changed"
10 11
 };
11 12
 
12 13
 module.exports = RTCEvents;

Loading…
Cancel
Save