Browse Source

allow user to select camera and microphone

j8
isymchych 9 years ago
parent
commit
f65d630ad8

+ 105
- 40
conference.js View File

19
 const TrackEvents = JitsiMeetJS.events.track;
19
 const TrackEvents = JitsiMeetJS.events.track;
20
 const TrackErrors = JitsiMeetJS.errors.track;
20
 const TrackErrors = JitsiMeetJS.errors.track;
21
 
21
 
22
-let room, connection, localTracks, localAudio, localVideo, roomLocker;
22
+let room, connection, localAudio, localVideo, roomLocker;
23
 
23
 
24
 /**
24
 /**
25
  * Known custom conference commands.
25
  * Known custom conference commands.
120
         // copy array to avoid mutations inside library
120
         // copy array to avoid mutations inside library
121
         devices: devices.slice(0),
121
         devices: devices.slice(0),
122
         resolution: config.resolution,
122
         resolution: config.resolution,
123
+        cameraDeviceId: APP.settings.getCameraDeviceId(),
124
+        micDeviceId: APP.settings.getMicDeviceId(),
123
         // adds any ff fake device settings if any
125
         // adds any ff fake device settings if any
124
         firefox_fake_device: config.firefox_fake_device
126
         firefox_fake_device: config.firefox_fake_device
125
     }).catch(function (err) {
127
     }).catch(function (err) {
293
             ]);
295
             ]);
294
         }).then(([tracks, con]) => {
296
         }).then(([tracks, con]) => {
295
             console.log('initialized with %s local tracks', tracks.length);
297
             console.log('initialized with %s local tracks', tracks.length);
296
-            localTracks = tracks;
297
             connection = con;
298
             connection = con;
298
-            this._createRoom();
299
+            this._createRoom(tracks);
299
             this.isDesktopSharingEnabled =
300
             this.isDesktopSharingEnabled =
300
                 JitsiMeetJS.isDesktopSharingEnabled();
301
                 JitsiMeetJS.isDesktopSharingEnabled();
302
+
303
+            // update list of available devices
304
+            if (JitsiMeetJS.isDeviceListAvailable() &&
305
+                JitsiMeetJS.isDeviceChangeAvailable()) {
306
+                JitsiMeetJS.enumerateDevices((devices) => {
307
+                    this.availableDevices = devices;
308
+                    APP.UI.onAvailableDevicesChanged();
309
+                });
310
+            }
301
             // XXX The API will take care of disconnecting from the XMPP server
311
             // XXX The API will take care of disconnecting from the XMPP server
302
             // (and, thus, leaving the room) on unload.
312
             // (and, thus, leaving the room) on unload.
303
             return new Promise((resolve, reject) => {
313
             return new Promise((resolve, reject) => {
360
     listMembersIds () {
370
     listMembersIds () {
361
         return room.getParticipants().map(p => p.getId());
371
         return room.getParticipants().map(p => p.getId());
362
     },
372
     },
373
+    /**
374
+     * List of available cameras and microphones.
375
+     */
376
+    availableDevices: [],
363
     /**
377
     /**
364
      * Check if SIP is supported.
378
      * Check if SIP is supported.
365
      * @returns {boolean}
379
      * @returns {boolean}
449
     getLogs () {
463
     getLogs () {
450
         return room.getLogs();
464
         return room.getLogs();
451
     },
465
     },
452
-    _createRoom () {
466
+    _createRoom (localTracks) {
453
         room = connection.initJitsiConference(APP.conference.roomName,
467
         room = connection.initJitsiConference(APP.conference.roomName,
454
             this._getConferenceOptions());
468
             this._getConferenceOptions());
455
         this.localId = room.myUserId();
469
         this.localId = room.myUserId();
456
         localTracks.forEach((track) => {
470
         localTracks.forEach((track) => {
457
-            if(track.isAudioTrack()) {
458
-                localAudio = track;
459
-            }
460
-            else if (track.isVideoTrack()) {
461
-                localVideo = track;
462
-            }
463
             room.addTrack(track);
471
             room.addTrack(track);
464
-            APP.UI.addLocalStream(track);
472
+
473
+            if (track.isAudioTrack()) {
474
+                this.useAudioStream(track);
475
+            } else if (track.isVideoTrack()) {
476
+                this.useVideoStream(track);
477
+            }
465
         });
478
         });
466
         roomLocker = createRoomLocker(room);
479
         roomLocker = createRoomLocker(room);
467
         this._room = room; // FIXME do not use this
480
         this._room = room; // FIXME do not use this
468
-        this.localId = room.myUserId();
469
 
481
 
470
         let email = APP.settings.getEmail();
482
         let email = APP.settings.getEmail();
471
         email && sendEmail(email);
483
         email && sendEmail(email);
472
 
484
 
473
         let nick = APP.settings.getDisplayName();
485
         let nick = APP.settings.getDisplayName();
474
-        (config.useNicks && !nick) && (() => {
486
+        if (config.useNicks && !nick) {
475
             nick = APP.UI.askForNickname();
487
             nick = APP.UI.askForNickname();
476
             APP.settings.setDisplayName(nick);
488
             APP.settings.setDisplayName(nick);
477
-        })();
489
+        }
478
         nick && room.setDisplayName(nick);
490
         nick && room.setDisplayName(nick);
479
 
491
 
480
         this._setupListeners();
492
         this._setupListeners();
489
         return options;
501
         return options;
490
     },
502
     },
491
 
503
 
504
+    /**
505
+     * Start using provided video stream.
506
+     * Stops previous video stream.
507
+     * @param {JitsiLocalTrack} [stream] new stream to use or null
508
+     */
509
+    useVideoStream (stream) {
510
+        if (localVideo) {
511
+            localVideo.stop();
512
+        }
513
+        localVideo = stream;
514
+
515
+        if (stream) {
516
+            this.videoMuted = stream.isMuted();
517
+
518
+            APP.UI.addLocalStream(stream);
519
+
520
+            this.isSharingScreen = stream.videoType === 'desktop';
521
+        } else {
522
+            this.videoMuted = false;
523
+            this.isSharingScreen = false;
524
+        }
525
+
526
+        APP.UI.setVideoMuted(this.localId, this.videoMuted);
527
+
528
+        APP.UI.updateDesktopSharingButtons();
529
+    },
530
+
531
+    /**
532
+     * Start using provided audio stream.
533
+     * Stops previous audio stream.
534
+     * @param {JitsiLocalTrack} [stream] new stream to use or null
535
+     */
536
+    useAudioStream (stream) {
537
+        if (localAudio) {
538
+            localAudio.stop();
539
+        }
540
+        localAudio = stream;
541
+
542
+        if (stream) {
543
+            this.audioMuted = stream.isMuted();
544
+
545
+            APP.UI.addLocalStream(stream);
546
+        } else {
547
+            this.audioMuted = false;
548
+        }
549
+
550
+        APP.UI.setAudioMuted(this.localId, this.audioMuted);
551
+    },
552
+
492
     videoSwitchInProgress: false,
553
     videoSwitchInProgress: false,
493
     toggleScreenSharing () {
554
     toggleScreenSharing () {
494
         if (this.videoSwitchInProgress) {
555
         if (this.videoSwitchInProgress) {
507
             createLocalTracks('video').then(function ([stream]) {
568
             createLocalTracks('video').then(function ([stream]) {
508
                 return room.addTrack(stream);
569
                 return room.addTrack(stream);
509
             }).then((stream) => {
570
             }).then((stream) => {
510
-                if (localVideo) {
511
-                    localVideo.stop();
512
-                }
513
-                localVideo = stream;
514
-                this.videoMuted = stream.isMuted();
515
-                APP.UI.setVideoMuted(this.localId, this.videoMuted);
516
-
517
-                APP.UI.addLocalStream(stream);
571
+                this.useVideoStream(stream);
572
+                this.videoSwitchInProgress = false;
518
                 console.log('sharing local video');
573
                 console.log('sharing local video');
519
-            }).catch((err) => {
520
-                localVideo = null;
521
-                console.error('failed to share local video', err);
522
-            }).then(() => {
574
+            }).catch(function (err) {
575
+                this.useVideoStream(null);
523
                 this.videoSwitchInProgress = false;
576
                 this.videoSwitchInProgress = false;
524
-                this.isSharingScreen = false;
525
-                APP.UI.updateDesktopSharingButtons();
577
+                console.error('failed to share local video', err);
526
             });
578
             });
527
         } else {
579
         } else {
528
             // stop sharing video and share desktop
580
             // stop sharing video and share desktop
541
                 );
593
                 );
542
                 return room.addTrack(stream);
594
                 return room.addTrack(stream);
543
             }).then((stream) => {
595
             }).then((stream) => {
544
-                if (localVideo) {
545
-                    localVideo.stop();
546
-                }
547
-                localVideo = stream;
548
-
549
-                this.videoMuted = stream.isMuted();
550
-                APP.UI.setVideoMuted(this.localId, this.videoMuted);
551
-
552
-                APP.UI.addLocalStream(stream);
553
-
596
+                this.useVideoStream(stream);
554
                 this.videoSwitchInProgress = false;
597
                 this.videoSwitchInProgress = false;
555
-                this.isSharingScreen = true;
556
-                APP.UI.updateDesktopSharingButtons();
557
                 console.log('sharing local desktop');
598
                 console.log('sharing local desktop');
558
             }).catch((err) => {
599
             }).catch((err) => {
559
                 this.videoSwitchInProgress = false;
600
                 this.videoSwitchInProgress = false;
907
             room.pinParticipant(id);
948
             room.pinParticipant(id);
908
         });
949
         });
909
 
950
 
951
+        APP.UI.addListener(
952
+            UIEvents.VIDEO_DEVICE_CHANGED,
953
+            (cameraDeviceId) => {
954
+                APP.settings.setCameraDeviceId(cameraDeviceId);
955
+                createLocalTracks('video').then(([stream]) => {
956
+                    room.addTrack(stream);
957
+                    this.useVideoStream(stream);
958
+                    console.log('switched local video device');
959
+                });
960
+            }
961
+        );
962
+
963
+        APP.UI.addListener(
964
+            UIEvents.AUDIO_DEVICE_CHANGED,
965
+            (micDeviceId) => {
966
+                APP.settings.setMicDeviceId(micDeviceId);
967
+                createLocalTracks('audio').then(([stream]) => {
968
+                    room.addTrack(stream);
969
+                    this.useAudioStream(stream);
970
+                    console.log('switched local audio device');
971
+                });
972
+            }
973
+        );
974
+
910
         APP.UI.addListener(
975
         APP.UI.addListener(
911
             UIEvents.TOGGLE_SCREENSHARING, this.toggleScreenSharing.bind(this)
976
             UIEvents.TOGGLE_SCREENSHARING, this.toggleScreenSharing.bind(this)
912
         );
977
         );

+ 22
- 0
css/settingsmenu.css View File

1
 #settingsmenu {
1
 #settingsmenu {
2
     background: black;
2
     background: black;
3
     color: #00ccff;
3
     color: #00ccff;
4
+    overflow-y: auto;
4
 }
5
 }
5
 
6
 
6
 #settingsmenu input, select {
7
 #settingsmenu input, select {
52
 #startMutedOptions {
53
 #startMutedOptions {
53
     padding-left: 10%;
54
     padding-left: 10%;
54
     text-indent: -10%;
55
     text-indent: -10%;
56
+
57
+    /* clearfix */
58
+    overflow: auto;
59
+    zoom: 1;
55
 }
60
 }
56
 
61
 
57
 #startAudioMuted {
62
 #startAudioMuted {
66
     width: 94%;
71
     width: 94%;
67
     float: left;
72
     float: left;
68
 }
73
 }
74
+
75
+#devicesOptions {
76
+    display: none;
77
+}
78
+
79
+#devicesOptions label {
80
+    display: block;
81
+    margin-top: 15px;
82
+}
83
+
84
+#devicesOptions span {
85
+    padding-left: 10%;
86
+}
87
+
88
+#devicesOptions select {
89
+    height: 40px;
90
+}

+ 10
- 0
index.html View File

231
                     <span data-i18n="settings.startVideoMuted"></span>
231
                     <span data-i18n="settings.startVideoMuted"></span>
232
                 </label>
232
                 </label>
233
             </div>
233
             </div>
234
+            <div id="devicesOptions">
235
+                <label className="devicesOptionsLabel">
236
+                    <span data-i18n="settings.selectCamera"></span>
237
+                    <select id="selectCamera"></select>
238
+                </label>
239
+                <label className="devicesOptionsLabel">
240
+                    <span data-i18n="settings.selectMic"></span>
241
+                    <select id="selectMic"></select>
242
+                </label>
243
+            </div>
234
             <button id="updateSettings" data-i18n="settings.update"></button>
244
             <button id="updateSettings" data-i18n="settings.update"></button>
235
             <a id="downloadlog" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[data-content]downloadlogs" ><i class="fa fa-cloud-download"></i></a>
245
             <a id="downloadlog" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[data-content]downloadlogs" ><i class="fa fa-cloud-download"></i></a>
236
         </div>
246
         </div>

+ 3
- 1
lang/main.json View File

84
         "update": "Update",
84
         "update": "Update",
85
         "name": "Name",
85
         "name": "Name",
86
         "startAudioMuted": "start without audio",
86
         "startAudioMuted": "start without audio",
87
-        "startVideoMuted": "start without video"
87
+        "startVideoMuted": "start without video",
88
+        "selectCamera": "select camera",
89
+        "selectMic": "select microphone"
88
     },
90
     },
89
     "videothumbnail":
91
     "videothumbnail":
90
     {
92
     {

+ 14
- 5
modules/UI/UI.js View File

660
  */
660
  */
661
 UI.setAudioMuted = function (id, muted) {
661
 UI.setAudioMuted = function (id, muted) {
662
     VideoLayout.onAudioMute(id, muted);
662
     VideoLayout.onAudioMute(id, muted);
663
-    if(APP.conference.isLocalId(id))
664
-        UIUtil.buttonClick("#toolbar_button_mute",
665
-            "icon-microphone icon-mic-disabled");
663
+    if (APP.conference.isLocalId(id)) {
664
+        Toolbar.markAudioIconAsMuted(muted);
665
+    }
666
 };
666
 };
667
 
667
 
668
 /**
668
 /**
670
  */
670
  */
671
 UI.setVideoMuted = function (id, muted) {
671
 UI.setVideoMuted = function (id, muted) {
672
     VideoLayout.onVideoMute(id, muted);
672
     VideoLayout.onVideoMute(id, muted);
673
-    if(APP.conference.isLocalId(id))
674
-        $('#toolbar_button_camera').toggleClass("icon-camera-disabled", muted);
673
+    if (APP.conference.isLocalId(id)) {
674
+        Toolbar.markVideoIconAsMuted(muted);
675
+    }
675
 };
676
 };
676
 
677
 
677
 UI.addListener = function (type, listener) {
678
 UI.addListener = function (type, listener) {
1040
     SettingsMenu.onStartMutedChanged();
1041
     SettingsMenu.onStartMutedChanged();
1041
 };
1042
 };
1042
 
1043
 
1044
+/**
1045
+ * Update list of available physical devices.
1046
+ * @param {object[]} devices new list of available devices
1047
+ */
1048
+UI.onAvailableDevicesChanged = function (devices) {
1049
+    SettingsMenu.onAvailableDevicesChanged(devices);
1050
+};
1051
+
1043
 /**
1052
 /**
1044
  * Returns the id of the current video shown on large.
1053
  * Returns the id of the current video shown on large.
1045
  * Currently used by tests (torture).
1054
  * Currently used by tests (torture).

+ 46
- 0
modules/UI/side_pannels/settings/SettingsMenu.js View File

21
     return html + "</select>";
21
     return html + "</select>";
22
 }
22
 }
23
 
23
 
24
+function generateDevicesOptions(items, selectedId) {
25
+    return items.map(function (item) {
26
+        let attrs = {
27
+            value: item.deviceId
28
+        };
29
+
30
+        if (item.deviceId === selectedId) {
31
+            attrs.selected = 'selected';
32
+        }
33
+
34
+        let attrsStr = UIUtil.attrsToString(attrs);
35
+        return `<option ${attrsStr}>${item.label}</option>`;
36
+    }).join('\n');
37
+}
38
+
24
 
39
 
25
 export default {
40
 export default {
26
     init (emitter) {
41
     init (emitter) {
51
                     startVideoMuted
66
                     startVideoMuted
52
                 );
67
                 );
53
             }
68
             }
69
+
70
+            let cameraDeviceId = $('#selectCamera').val();
71
+            if (cameraDeviceId !== Settings.getCameraDeviceId()) {
72
+                emitter.emit(UIEvents.VIDEO_DEVICE_CHANGED, cameraDeviceId);
73
+            }
74
+
75
+            let micDeviceId = $('#selectMic').val();
76
+            if (micDeviceId !== Settings.getMicDeviceId()) {
77
+                emitter.emit(UIEvents.AUDIO_DEVICE_CHANGED, micDeviceId);
78
+            }
54
         }
79
         }
55
 
80
 
56
         let startMutedBlock = $("#startMutedOptions");
81
         let startMutedBlock = $("#startMutedOptions");
57
         startMutedBlock.before(generateLanguagesSelectBox());
82
         startMutedBlock.before(generateLanguagesSelectBox());
58
         APP.translation.translateElement($("#languages_selectbox"));
83
         APP.translation.translateElement($("#languages_selectbox"));
59
 
84
 
85
+        this.onAvailableDevicesChanged();
60
         this.onRoleChanged();
86
         this.onRoleChanged();
61
         this.onStartMutedChanged();
87
         this.onStartMutedChanged();
62
 
88
 
94
 
120
 
95
     changeAvatar (avatarUrl) {
121
     changeAvatar (avatarUrl) {
96
         $('#avatar').attr('src', avatarUrl);
122
         $('#avatar').attr('src', avatarUrl);
123
+    },
124
+
125
+    onAvailableDevicesChanged () {
126
+        let devices = APP.conference.availableDevices;
127
+        if (!devices.length) {
128
+            $('#devicesOptions').hide();
129
+            return;
130
+        }
131
+
132
+        let audio = devices.filter(device => device.kind === 'audioinput');
133
+        let video = devices.filter(device => device.kind === 'videoinput');
134
+
135
+        $('#selectCamera').html(
136
+            generateDevicesOptions(video, Settings.getCameraDeviceId())
137
+        );
138
+        $('#selectMic').html(
139
+            generateDevicesOptions(audio, Settings.getMicDeviceId())
140
+        );
141
+
142
+        $('#devicesOptions').show();
97
     }
143
     }
98
 };
144
 };

+ 16
- 0
modules/UI/toolbars/Toolbar.js View File

385
 
385
 
386
     updateRecordingState (state) {
386
     updateRecordingState (state) {
387
         setRecordingButtonState(state);
387
         setRecordingButtonState(state);
388
+    },
389
+
390
+    /**
391
+     * Marks video icon as muted or not.
392
+     * @param {boolean} muted if icon should look like muted or not
393
+     */
394
+    markVideoIconAsMuted (muted) {
395
+        $('#toolbar_button_camera').toggleClass("icon-camera-disabled", muted);
396
+    },
397
+
398
+    /**
399
+     * Marks audio icon as muted or not.
400
+     * @param {boolean} muted if icon should look like muted or not
401
+     */
402
+    markAudioIconAsMuted (muted) {
403
+        $('#toolbar_button_mute').toggleClass("icon-microphone", !muted).toggleClass("icon-mic-disabled", muted);
388
     }
404
     }
389
 };
405
 };
390
 
406
 

+ 11
- 0
modules/UI/util/UIUtil.js View File

139
          return document.fullScreen
139
          return document.fullScreen
140
              || document.mozFullScreen
140
              || document.mozFullScreen
141
              || document.webkitIsFullScreen;
141
              || document.webkitIsFullScreen;
142
+     },
143
+
144
+     /**
145
+      * Create html attributes string out of object properties.
146
+      * @param {Object} attrs object with properties
147
+      * @returns {String} string of html element attributes
148
+      */
149
+     attrsToString: function (attrs) {
150
+         return Object.keys(attrs).map(
151
+             key => ` ${key}="${attrs[key]}"`
152
+         ).join(' ');
142
      }
153
      }
143
 };
154
 };
144
 
155
 

+ 5
- 4
modules/UI/videolayout/LocalVideo.js View File

17
     this.flipX = true;
17
     this.flipX = true;
18
     this.isLocal = true;
18
     this.isLocal = true;
19
     this.emitter = emitter;
19
     this.emitter = emitter;
20
+    Object.defineProperty(this, 'id', {
21
+        get: function () {
22
+            return APP.conference.localId;
23
+        }
24
+    });
20
     SmallVideo.call(this);
25
     SmallVideo.call(this);
21
 }
26
 }
22
 
27
 
195
     stream.on(TrackEvents.TRACK_STOPPED, endedHandler);
200
     stream.on(TrackEvents.TRACK_STOPPED, endedHandler);
196
 };
201
 };
197
 
202
 
198
-LocalVideo.prototype.joined = function (id) {
199
-    this.id = id;
200
-};
201
-
202
 export default LocalVideo;
203
 export default LocalVideo;

+ 1
- 4
modules/UI/videolayout/VideoLayout.js View File

170
      * and setting them assume the id is already set.
170
      * and setting them assume the id is already set.
171
      */
171
      */
172
     mucJoined () {
172
     mucJoined () {
173
-        let id = APP.conference.localId;
174
-        localVideoThumbnail.joined(id);
175
-
176
         if (largeVideo && !largeVideo.id) {
173
         if (largeVideo && !largeVideo.id) {
177
-            this.updateLargeVideo(id, true);
174
+            this.updateLargeVideo(APP.conference.localId, true);
178
         }
175
         }
179
     },
176
     },
180
 
177
 

+ 44
- 4
modules/settings/Settings.js View File

1
 import {generateUsername} from '../util/UsernameGenerator';
1
 import {generateUsername} from '../util/UsernameGenerator';
2
 
2
 
3
-var email = '';
4
-var displayName = '';
5
-var userId;
6
-var language = null;
3
+let email = '';
4
+let displayName = '';
5
+let userId;
6
+let language = null;
7
+let cameraDeviceId = '';
8
+let micDeviceId = '';
7
 
9
 
8
 function supportsLocalStorage() {
10
 function supportsLocalStorage() {
9
     try {
11
     try {
32
     email = window.localStorage.email || '';
34
     email = window.localStorage.email || '';
33
     displayName = window.localStorage.displayname || '';
35
     displayName = window.localStorage.displayname || '';
34
     language = window.localStorage.language;
36
     language = window.localStorage.language;
37
+    cameraDeviceId = window.localStorage.cameraDeviceId || '';
38
+    micDeviceId = window.localStorage.micDeviceId || '';
35
 } else {
39
 } else {
36
     console.log("local storage is not supported");
40
     console.log("local storage is not supported");
37
     userId = generateUniqueId();
41
     userId = generateUniqueId();
86
     setLanguage: function (lang) {
90
     setLanguage: function (lang) {
87
         language = lang;
91
         language = lang;
88
         window.localStorage.language = lang;
92
         window.localStorage.language = lang;
93
+    },
94
+
95
+    /**
96
+     * Get device id of the camera which is currently in use.
97
+     * Empty string stands for default device.
98
+     * @returns {String}
99
+     */
100
+    getCameraDeviceId: function () {
101
+        return cameraDeviceId;
102
+    },
103
+    /**
104
+     * Set device id of the camera which is currently in use.
105
+     * Empty string stands for default device.
106
+     * @param {string} newId new camera device id
107
+     */
108
+    setCameraDeviceId: function (newId = '') {
109
+        cameraDeviceId = newId;
110
+        window.localStorage.cameraDeviceId = newId;
111
+    },
112
+
113
+    /**
114
+     * Get device id of the microphone which is currently in use.
115
+     * Empty string stands for default device.
116
+     * @returns {String}
117
+     */
118
+    getMicDeviceId: function () {
119
+        return micDeviceId;
120
+    },
121
+    /**
122
+     * Set device id of the microphone which is currently in use.
123
+     * Empty string stands for default device.
124
+     * @param {string} newId new microphone device id
125
+     */
126
+    setMicDeviceId: function (newId = '') {
127
+        micDeviceId = newId;
128
+        window.localStorage.micDeviceId = newId;
89
     }
129
     }
90
 };
130
 };

+ 3
- 1
service/UI/UIEvents.js View File

41
     LOGOUT: "UI.logout",
41
     LOGOUT: "UI.logout",
42
     RECORDING_TOGGLE: "UI.recording_toggle",
42
     RECORDING_TOGGLE: "UI.recording_toggle",
43
     SIP_DIAL: "UI.sip_dial",
43
     SIP_DIAL: "UI.sip_dial",
44
-    SUBEJCT_CHANGED: "UI.subject_changed"
44
+    SUBEJCT_CHANGED: "UI.subject_changed",
45
+    VIDEO_DEVICE_CHANGED: "UI.video_device_changed",
46
+    AUDIO_DEVICE_CHANGED: "UI.audio_device_changed"
45
 };
47
 };

Loading…
Cancel
Save