diff --git a/res/css/_components.scss b/res/css/_components.scss
index dcc5799329..25b7c9342e 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -138,6 +138,7 @@
 @import "./views/settings/tabs/_GeneralSettingsTab.scss";
 @import "./views/settings/tabs/_PreferencesSettingsTab.scss";
 @import "./views/settings/tabs/_SettingsTab.scss";
+@import "./views/settings/tabs/_VoiceSettingsTab.scss";
 @import "./views/voip/_CallView.scss";
 @import "./views/voip/_IncomingCallbox.scss";
 @import "./views/voip/_VideoView.scss";
diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss
index a6e884e170..8de0b82554 100644
--- a/res/css/views/elements/_AccessibleButton.scss
+++ b/res/css/views/elements/_AccessibleButton.scss
@@ -27,10 +27,11 @@ limitations under the License.
 }
 
 .mx_AccessibleButton_hasKind {
-    padding: 10px 25px;
+    padding: 7px 18px;
     text-align: center;
     border-radius: 4px;
     display: inline-block;
+    font-size: 14px;
 }
 
 .mx_AccessibleButton_kind_primary {
diff --git a/res/css/views/settings/tabs/_VoiceSettingsTab.scss b/res/css/views/settings/tabs/_VoiceSettingsTab.scss
new file mode 100644
index 0000000000..345fefe3cd
--- /dev/null
+++ b/res/css/views/settings/tabs/_VoiceSettingsTab.scss
@@ -0,0 +1,28 @@
+/*
+Copyright 2019 New Vector Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_VoiceSettingsTab .mx_Field select {
+  width: 100%;
+  max-width: 100%;
+}
+
+.mx_VoiceSettingsTab .mx_Field {
+  margin-right: 100px; // align with the rest of the fields
+}
+
+.mx_VoiceSettingsTab_missingMediaPermissions {
+  margin-bottom: 15px;
+}
diff --git a/src/CallMediaHandler.js b/src/CallMediaHandler.js
index 2330f86b99..9a1c9d70b8 100644
--- a/src/CallMediaHandler.js
+++ b/src/CallMediaHandler.js
@@ -69,4 +69,16 @@ export default {
         SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
         Matrix.setMatrixCallVideoInput(deviceId);
     },
+
+    getAudioOutput: function() {
+        return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
+    },
+
+    getAudioInput: function() {
+        return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput");
+    },
+
+    getVideoInput: function() {
+        return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput");
+    },
 };
diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js
index 0c11b29456..7b09970f56 100644
--- a/src/components/views/dialogs/UserSettingsDialog.js
+++ b/src/components/views/dialogs/UserSettingsDialog.js
@@ -25,6 +25,7 @@ import SettingsStore from "../../../settings/SettingsStore";
 import LabsSettingsTab from "../settings/tabs/LabsSettingsTab";
 import NotificationSettingsTab from "../settings/tabs/NotificationSettingsTab";
 import PreferencesSettingsTab from "../settings/tabs/PreferencesSettingsTab";
+import VoiceSettingsTab from "../settings/tabs/VoiceSettingsTab";
 
 // TODO: Ditch this whole component
 export class TempTab extends React.Component {
@@ -68,7 +69,7 @@ export default class UserSettingsDialog extends React.Component {
         tabs.push(new Tab(
             _td("Voice & Video"),
             "mx_UserSettingsDialog_voiceIcon",
-            <div>Voice Test</div>,
+            <VoiceSettingsTab />,
         ));
         tabs.push(new Tab(
             _td("Security & Privacy"),
diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/VoiceSettingsTab.js
new file mode 100644
index 0000000000..f03711fb96
--- /dev/null
+++ b/src/components/views/settings/tabs/VoiceSettingsTab.js
@@ -0,0 +1,177 @@
+/*
+Copyright 2019 New Vector Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+import {_t} from "../../../../languageHandler";
+import CallMediaHandler from "../../../../CallMediaHandler";
+import Field from "../../elements/Field";
+import AccessibleButton from "../../elements/AccessibleButton";
+import {SettingLevel} from "../../../../settings/SettingsStore";
+const Modal = require("../../../../Modal");
+const sdk = require("../../../../index");
+const MatrixClientPeg = require("../../../../MatrixClientPeg");
+
+export default class VoiceSettingsTab extends React.Component {
+    constructor() {
+        super();
+
+        this.state = {
+            mediaDevices: null,
+            activeAudioOutput: null,
+            activeAudioInput: null,
+            activeVideoInput: null,
+        };
+    }
+
+    componentWillMount(): void {
+        this._refreshMediaDevices();
+    }
+
+    _refreshMediaDevices = async () => {
+        this.setState({
+            mediaDevices: await CallMediaHandler.getDevices(),
+            activeAudioOutput: CallMediaHandler.getAudioOutput(),
+            activeAudioInput: CallMediaHandler.getAudioInput(),
+            activeVideoInput: CallMediaHandler.getVideoInput(),
+        });
+    };
+
+    _requestMediaPermissions = () => {
+        const getUserMedia = (
+            window.navigator.getUserMedia || window.navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia
+        );
+        if (getUserMedia) {
+            return getUserMedia.apply(window.navigator, [
+                { video: true, audio: true },
+                this._refreshMediaDevices,
+                function() {
+                    const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
+                    Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
+                        title: _t('No media permissions'),
+                        description: _t('You may need to manually permit Riot to access your microphone/webcam'),
+                    });
+                },
+            ]);
+        }
+    };
+
+    _setAudioOutput = (e) => {
+        CallMediaHandler.setAudioOutput(e.target.value);
+    };
+
+    _setAudioInput = (e) => {
+        CallMediaHandler.setAudioInput(e.target.value);
+    };
+
+    _setVideoInput = (e) => {
+        CallMediaHandler.setVideoInput(e.target.value);
+    };
+
+    _setForceTurn = (forced) => {
+        MatrixClientPeg.get().setForceTURN(forced);
+    };
+
+    _renderDeviceOptions(devices, category) {
+        return devices.map((d) => <option key={`${category}-${d.deviceId}`} value={d.deviceId}>{d.label}</option>);
+    }
+
+    render() {
+        const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
+
+        let requestButton = null;
+        let speakerDropdown = null;
+        let microphoneDropdown = null;
+        let webcamDropdown = null;
+        if (this.state.mediaDevices === false) {
+            requestButton = (
+                <div className='mx_VoiceSettingsTab_missingMediaPermissions'>
+                    <p>{_t("Missing media permissions, click the button below to request.")}</p>
+                    <AccessibleButton onClick={this._requestMediaPermissions} kind="primary">
+                        {_t("Request media permissions")}
+                    </AccessibleButton>
+                </div>
+            );
+        } else if (this.state.mediaDevices) {
+            speakerDropdown = <p>{ _t('No Audio Outputs detected') }</p>;
+            microphoneDropdown = <p>{ _t('No Microphones detected') }</p>;
+            webcamDropdown = <p>{ _t('No Webcams detected') }</p>;
+
+            const defaultOption = {
+                deviceId: '',
+                label: _t('Default Device'),
+            };
+            const getDefaultDevice = (devices) => {
+                if (!devices.some((i) => i.deviceId === 'default')) {
+                    devices.unshift(defaultOption);
+                    return '';
+                } else {
+                    return 'default';
+                }
+            };
+
+            const audioOutputs = this.state.mediaDevices.audiooutput.slice(0);
+            if (audioOutputs.length > 0) {
+                const defaultDevice = getDefaultDevice(audioOutputs);
+                speakerDropdown = (
+                    <Field element="select" label={_t("Audio Output")} id="audioOutput"
+                           value={this.state.activeAudioOutput || defaultDevice}
+                           onChange={this._setAudioOutput}>
+                        {this._renderDeviceOptions(audioOutputs, 'audioOutput')}
+                    </Field>
+                );
+            }
+
+            const audioInputs = this.state.mediaDevices.audioinput.slice(0);
+            if (audioInputs.length > 0) {
+                const defaultDevice = getDefaultDevice(audioInputs);
+                microphoneDropdown = (
+                    <Field element="select" label={_t("Microphone")} id="audioInput"
+                           value={this.state.activeAudioInput || defaultDevice}
+                           onChange={this._setAudioInput}>
+                        {this._renderDeviceOptions(audioInputs, 'audioInput')}
+                    </Field>
+                );
+            }
+
+            const videoInputs = this.state.mediaDevices.videoinput.slice(0);
+            if (videoInputs.length > 0) {
+                const defaultDevice = getDefaultDevice(videoInputs);
+                webcamDropdown = (
+                    <Field element="select" label={_t("Camera")} id="videoInput"
+                           value={this.state.activeVideoInput || defaultDevice}
+                           onChange={this._setVideoInput}>
+                        {this._renderDeviceOptions(videoInputs, 'videoInput')}
+                    </Field>
+                );
+            }
+        }
+
+        // TODO: Make 'webRtcForceTURN' be positively worded
+        return (
+            <div className="mx_SettingsTab mx_VoiceSettingsTab">
+                <div className="mx_SettingsTab_heading">{_t("Voice & Video")}</div>
+                <div className="mx_SettingsTab_section">
+                    {requestButton}
+                    {speakerDropdown}
+                    {microphoneDropdown}
+                    {webcamDropdown}
+                    <SettingsFlag name='VideoView.flipVideoHorizontally' level={SettingLevel.ACCOUNT} />
+                    <SettingsFlag name='webRtcForceTURN' level={SettingLevel.DEVICE} onChange={this._setForceTurn} />
+                </div>
+            </div>
+        );
+    }
+}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 1ef41824d5..ab4e8b74c7 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -450,6 +450,18 @@
     "Timeline": "Timeline",
     "Advanced": "Advanced",
     "Autocomplete delay (ms)": "Autocomplete delay (ms)",
+    "No media permissions": "No media permissions",
+    "You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam",
+    "Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.",
+    "Request media permissions": "Request media permissions",
+    "No Audio Outputs detected": "No Audio Outputs detected",
+    "No Microphones detected": "No Microphones detected",
+    "No Webcams detected": "No Webcams detected",
+    "Default Device": "Default Device",
+    "Audio Output": "Audio Output",
+    "Microphone": "Microphone",
+    "Camera": "Camera",
+    "Voice & Video": "Voice & Video",
     "Cannot add any more widgets": "Cannot add any more widgets",
     "The maximum permitted number of widgets have already been added to this room.": "The maximum permitted number of widgets have already been added to this room.",
     "Add a widget": "Add a widget",
@@ -1044,7 +1056,6 @@
     "Room contains unknown devices": "Room contains unknown devices",
     "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.",
     "Unknown devices": "Unknown devices",
-    "Voice & Video": "Voice & Video",
     "Security & Privacy": "Security & Privacy",
     "Help & About": "Help & About",
     "Visit old settings": "Visit old settings",
@@ -1300,16 +1311,7 @@
     "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites",
     "Bulk Options": "Bulk Options",
     "Desktop specific": "Desktop specific",
-    "No media permissions": "No media permissions",
-    "You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam",
     "Missing Media Permissions, click here to request.": "Missing Media Permissions, click here to request.",
-    "No Audio Outputs detected": "No Audio Outputs detected",
-    "No Microphones detected": "No Microphones detected",
-    "No Webcams detected": "No Webcams detected",
-    "Default Device": "Default Device",
-    "Audio Output": "Audio Output",
-    "Microphone": "Microphone",
-    "Camera": "Camera",
     "VoIP": "VoIP",
     "Email": "Email",
     "Add email address": "Add email address",