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
     isDeviceChangeAvailable: function () {
147
     isDeviceChangeAvailable: function () {
148
         return RTC.isDeviceChangeAvailable();
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
     enumerateDevices: function (callback) {
175
     enumerateDevices: function (callback) {
151
         RTC.enumerateDevices(callback);
176
         RTC.enumerateDevices(callback);
152
     },
177
     },

+ 5
- 1
JitsiTrackEvents.js View File

14
     /**
14
     /**
15
      * The video type("camera" or "desktop") of the track was changed.
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
 module.exports = JitsiTrackEvents;
24
 module.exports = JitsiTrackEvents;

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

36
             function () {
36
             function () {
37
                 console.log("local track stoped");
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
         if(localTracks[i].getType() == "video") {
43
         if(localTracks[i].getType() == "video") {
40
             $("body").append("<video autoplay='1' id='localVideo" + i + "' />");
44
             $("body").append("<video autoplay='1' id='localVideo" + i + "' />");
41
             localTracks[i].attach($("#localVideo" + i)[0]);
45
             localTracks[i].attach($("#localVideo" + i)[0]);
71
         function () {
75
         function () {
72
             console.log("remote track stoped");
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
     var id = participant + track.getType() + idx;
82
     var id = participant + track.getType() + idx;
75
     if(track.getType() == "video") {
83
     if(track.getType() == "video") {
76
         $("body").append("<video autoplay='1' id='" + participant + "video" + idx + "' />");
84
         $("body").append("<video autoplay='1' id='" + participant + "video" + idx + "' />");
180
         });
188
         });
181
 }
189
 }
182
 
190
 
191
+function changeAudioOutput(selected) {
192
+    JitsiMeetJS.setAudioOutputDevice(selected.value);
193
+}
194
+
183
 $(window).bind('beforeunload', unload);
195
 $(window).bind('beforeunload', unload);
184
 $(window).bind('unload', unload);
196
 $(window).bind('unload', unload);
185
 
197
 
228
     console.log(error);
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
 var connection = null;
260
 var connection = null;
232
 var room = null;
261
 var room = null;
233
 var localTracks = [];
262
 var localTracks = [];

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

13
 <body>
13
 <body>
14
     <a href="#" onclick="unload()">Unload</a>
14
     <a href="#" onclick="unload()">Unload</a>
15
     <a href="#" onclick="switchVideo()">switchVideo</a>
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
     <!-- <video id="localVideo" autoplay="true"></video> -->
20
     <!-- <video id="localVideo" autoplay="true"></video> -->
17
     <!--<audio id="localAudio" autoplay="true" muted="true"></audio>-->
21
     <!--<audio id="localAudio" autoplay="true" muted="true"></audio>-->
18
 </body>
22
 </body>

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

76
         }
76
         }
77
         addMediaStreamInactiveHandler(stream, streamInactiveHandler);
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
     return container;
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
  * Removes the track from the passed HTML container.
196
  * Removes the track from the passed HTML container.
225
  * @param container the HTML container. If <tt>null</tt> all containers are removed.
197
  * @param container the HTML container. If <tt>null</tt> all containers are removed.
340
     return (streamId && trackId) ? (streamId + " " + trackId) : null;
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
 module.exports = JitsiTrack;
357
 module.exports = JitsiTrack;

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

314
     return RTCUtils.isDeviceChangeAvailable();
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
  * Returns <tt>true<tt/> if given WebRTC MediaStream is considered a valid
346
  * Returns <tt>true<tt/> if given WebRTC MediaStream is considered a valid
319
  * "user" stream which means that it's not a "receive only" stream nor a "mixed"
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
     video: true
24
     video: true
25
 };
25
 };
26
 
26
 
27
+var audioOuputDeviceId = ''; // default device
28
+
29
+var featureDetectionVideoEl = document.createElement('video');
30
+
27
 var rtcReady = false;
31
 var rtcReady = false;
28
 
32
 
29
 function setResolutionConstraints(constraints, resolution) {
33
 function setResolutionConstraints(constraints, resolution) {
303
                 //add auto devices
307
                 //add auto devices
304
                 devices.unshift(
308
                 devices.unshift(
305
                     createAutoDeviceInfo('audioinput'),
309
                     createAutoDeviceInfo('audioinput'),
306
-                    createAutoDeviceInfo('videoinput')
310
+                    createAutoDeviceInfo('videoinput'),
311
+                    createAutoDeviceInfo('audiooutput')
307
                 );
312
                 );
308
 
313
 
309
                 callback(devices);
314
                 callback(devices);
311
                 console.error('cannot enumerate devices: ', err);
316
                 console.error('cannot enumerate devices: ', err);
312
 
317
 
313
                 // return only auto devices
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
         //add auto devices
349
         //add auto devices
342
         devices.unshift(
350
         devices.unshift(
343
             createAutoDeviceInfo('audioinput'),
351
             createAutoDeviceInfo('audioinput'),
344
-            createAutoDeviceInfo('videoinput')
352
+            createAutoDeviceInfo('videoinput'),
353
+            createAutoDeviceInfo('audiooutput')
345
         );
354
         );
346
         callback(devices);
355
         callback(devices);
347
     });
356
     });
443
     return res;
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
 //Options parameter is to pass config options. Currently uses only "useIPv6".
477
 //Options parameter is to pass config options. Currently uses only "useIPv6".
447
 var RTCUtils = {
478
 var RTCUtils = {
448
     init: function (options) {
479
     init: function (options) {
463
                     navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices)
494
                     navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices)
464
                 );
495
                 );
465
                 this.pc_constraints = {};
496
                 this.pc_constraints = {};
466
-                this.attachMediaStream = function (element, stream) {
497
+                this.attachMediaStream = wrapAttachMediaStream(function (element, stream) {
467
                     //  srcObject is being standardized and FF will eventually
498
                     //  srcObject is being standardized and FF will eventually
468
                     //  support that unprefixed. FF also supports the
499
                     //  support that unprefixed. FF also supports the
469
                     //  "element.src = URL.createObjectURL(...)" combo, but that
500
                     //  "element.src = URL.createObjectURL(...)" combo, but that
477
                     element.play();
508
                     element.play();
478
 
509
 
479
                     return element;
510
                     return element;
480
-                };
511
+                });
481
                 this.getStreamID = function (stream) {
512
                 this.getStreamID = function (stream) {
482
                     var id = stream.id;
513
                     var id = stream.id;
483
                     if (!id) {
514
                     if (!id) {
512
                     this.getUserMedia = getUserMedia;
543
                     this.getUserMedia = getUserMedia;
513
                     this.enumerateDevices = enumerateDevicesThroughMediaStreamTrack;
544
                     this.enumerateDevices = enumerateDevicesThroughMediaStreamTrack;
514
                 }
545
                 }
515
-                this.attachMediaStream = function (element, stream) {
546
+                this.attachMediaStream = wrapAttachMediaStream(function (element, stream) {
516
 
547
 
517
                     // saves the created url for the stream, so we can reuse it
548
                     // saves the created url for the stream, so we can reuse it
518
                     // and not keep creating urls
549
                     // and not keep creating urls
524
                     element.src = stream.jitsiObjectURL;
555
                     element.src = stream.jitsiObjectURL;
525
 
556
 
526
                     return element;
557
                     return element;
527
-                };
558
+                });
528
                 this.getStreamID = function (stream) {
559
                 this.getStreamID = function (stream) {
529
                     // streams from FF endpoints have the characters '{' and '}'
560
                     // streams from FF endpoints have the characters '{' and '}'
530
                     // that make jQuery choke.
561
                     // that make jQuery choke.
575
                     self.peerconnection = RTCPeerConnection;
606
                     self.peerconnection = RTCPeerConnection;
576
                     self.getUserMedia = window.getUserMedia;
607
                     self.getUserMedia = window.getUserMedia;
577
                     self.enumerateDevices = enumerateDevicesThroughMediaStreamTrack;
608
                     self.enumerateDevices = enumerateDevicesThroughMediaStreamTrack;
578
-                    self.attachMediaStream = function (element, stream) {
609
+                    self.attachMediaStream = wrapAttachMediaStream(function (element, stream) {
579
 
610
 
580
                         if (stream.id === "dummyAudio" || stream.id === "dummyVideo") {
611
                         if (stream.id === "dummyAudio" || stream.id === "dummyVideo") {
581
                             return;
612
                             return;
587
                         }
618
                         }
588
 
619
 
589
                         return attachMediaStream(element, stream);
620
                         return attachMediaStream(element, stream);
590
-                    };
621
+                    });
591
                     self.getStreamID = function (stream) {
622
                     self.getStreamID = function (stream) {
592
                         return SDPUtil.filter_special_chars(stream.label);
623
                         return SDPUtil.filter_special_chars(stream.label);
593
                     };
624
                     };
825
             RTCBrowserType.isOpera() ||
856
             RTCBrowserType.isOpera() ||
826
             RTCBrowserType.isTemasysPluginUsed();
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
      * A method to handle stopping of the stream.
867
      * A method to handle stopping of the stream.
830
      * One point to handle the differences in various implementations.
868
      * One point to handle the differences in various implementations.
854
      */
892
      */
855
     isDesktopSharingEnabled: function () {
893
     isDesktopSharingEnabled: function () {
856
         return screenObtainer.isSupported();
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
 module.exports = RTCUtils;
927
 module.exports = RTCUtils;

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

6
     LASTN_ENDPOINT_CHANGED: "rtc.lastn_endpoint_changed",
6
     LASTN_ENDPOINT_CHANGED: "rtc.lastn_endpoint_changed",
7
     AVAILABLE_DEVICES_CHANGED: "rtc.available_devices_changed",
7
     AVAILABLE_DEVICES_CHANGED: "rtc.available_devices_changed",
8
     FAKE_VIDEO_TRACK_CREATED: "rtc.fake_video_track_created",
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
 module.exports = RTCEvents;
13
 module.exports = RTCEvents;

Loading…
Cancel
Save