From 6636fa32f67a592eb18208e89b2ae2ce226c00ac Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Sat, 26 May 2018 17:22:23 +0100
Subject: [PATCH] Allow selecting audio output for WebRTC Audio/Video calls

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/CallMediaHandler.js                   | 22 +++++++++++-----
 src/components/structures/UserSettings.js | 32 +++++++++++++++++++++--
 src/settings/Settings.js                  |  4 +++
 3 files changed, 50 insertions(+), 8 deletions(-)

diff --git a/src/CallMediaHandler.js b/src/CallMediaHandler.js
index cdc5c61921..2330f86b99 100644
--- a/src/CallMediaHandler.js
+++ b/src/CallMediaHandler.js
@@ -22,34 +22,44 @@ export default {
         // Only needed for Electron atm, though should work in modern browsers
         // once permission has been granted to the webapp
         return navigator.mediaDevices.enumerateDevices().then(function(devices) {
-            const audioIn = [];
-            const videoIn = [];
+            const audiooutput = [];
+            const audioinput = [];
+            const videoinput = [];
 
             if (devices.some((device) => !device.label)) return false;
 
             devices.forEach((device) => {
                 switch (device.kind) {
-                    case 'audioinput': audioIn.push(device); break;
-                    case 'videoinput': videoIn.push(device); break;
+                    case 'audiooutput': audiooutput.push(device); break;
+                    case 'audioinput': audioinput.push(device); break;
+                    case 'videoinput': videoinput.push(device); break;
                 }
             });
 
             // console.log("Loaded WebRTC Devices", mediaDevices);
             return {
-                audioinput: audioIn,
-                videoinput: videoIn,
+                audiooutput,
+                audioinput,
+                videoinput,
             };
         }, (error) => { console.log('Unable to refresh WebRTC Devices: ', error); });
     },
 
     loadDevices: function() {
+        const audioOutDeviceId = SettingsStore.getValue("webrtc_audiooutput");
         const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
         const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
 
+        Matrix.setMatrixCallAudioOutput(audioOutDeviceId);
         Matrix.setMatrixCallAudioInput(audioDeviceId);
         Matrix.setMatrixCallVideoInput(videoDeviceId);
     },
 
+    setAudioOutput: function(deviceId) {
+        SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
+        Matrix.setMatrixCallAudioOutput(deviceId);
+    },
+
     setAudioInput: function(deviceId) {
         SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
         Matrix.setMatrixCallAudioInput(deviceId);
diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js
index c8ce79905d..0561071fce 100644
--- a/src/components/structures/UserSettings.js
+++ b/src/components/structures/UserSettings.js
@@ -292,6 +292,7 @@ module.exports = React.createClass({
             if (this._unmounted) return;
             this.setState({
                 mediaDevices,
+                activeAudioOutput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audiooutput'),
                 activeAudioInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audioinput'),
                 activeVideoInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_videoinput'),
             });
@@ -970,6 +971,11 @@ module.exports = React.createClass({
         return devices.map((device) => <span key={device.deviceId}>{ device.label }</span>);
     },
 
+    _setAudioOutput: function(deviceId) {
+        this.setState({activeAudioOutput: deviceId});
+        CallMediaHandler.setAudioOutput(deviceId);
+    },
+
     _setAudioInput: function(deviceId) {
         this.setState({activeAudioInput: deviceId});
         CallMediaHandler.setAudioInput(deviceId);
@@ -1010,6 +1016,7 @@ module.exports = React.createClass({
 
         const Dropdown = sdk.getComponent('elements.Dropdown');
 
+        let speakerDropdown = <p>{ _t('No Audio Outputs detected') }</p>;
         let microphoneDropdown = <p>{ _t('No Microphones detected') }</p>;
         let webcamDropdown = <p>{ _t('No Webcams detected') }</p>;
 
@@ -1018,6 +1025,26 @@ module.exports = React.createClass({
             label: _t('Default Device'),
         };
 
+        const audioOutputs = this.state.mediaDevices.audiooutput.slice(0);
+        if (audioOutputs.length > 0) {
+            let defaultOutput = '';
+            if (!audioOutputs.some((input) => input.deviceId === 'default')) {
+                audioOutputs.unshift(defaultOption);
+            } else {
+                defaultOutput = 'default';
+            }
+
+            speakerDropdown = <div>
+                <h4>{ _t('Audio Output') }</h4>
+                <Dropdown
+                    className="mx_UserSettings_webRtcDevices_dropdown"
+                    value={this.state.activeAudioOutput || defaultOutput}
+                    onOptionChange={this._setAudioOutput}>
+                    { this._mapWebRtcDevicesToSpans(audioOutputs) }
+                </Dropdown>
+            </div>;
+        }
+
         const audioInputs = this.state.mediaDevices.audioinput.slice(0);
         if (audioInputs.length > 0) {
             let defaultInput = '';
@@ -1059,8 +1086,9 @@ module.exports = React.createClass({
         }
 
         return <div>
-                { microphoneDropdown }
-                { webcamDropdown }
+            { speakerDropdown }
+            { microphoneDropdown }
+            { webcamDropdown }
         </div>;
     },
 
diff --git a/src/settings/Settings.js b/src/settings/Settings.js
index b1bc4161fd..a222c29dda 100644
--- a/src/settings/Settings.js
+++ b/src/settings/Settings.js
@@ -201,6 +201,10 @@ export const SETTINGS = {
         displayName: _td('Disable Peer-to-Peer for 1:1 calls'),
         default: false,
     },
+    "webrtc_audiooutput": {
+        supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
+        default: null,
+    },
     "webrtc_audioinput": {
         supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
         default: null,