Skip to content

Commit 2b9c2d9

Browse files
authored
Merge pull request #14511 from nextcloud/fix/7125/audio-output-support
2 parents faadeba + f39d723 commit 2b9c2d9

File tree

1 file changed

+93
-165
lines changed

1 file changed

+93
-165
lines changed

src/components/MediaSettings/MediaDevicesSelector.vue

+93-165
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,98 @@
33
- SPDX-License-Identifier: AGPL-3.0-or-later
44
-->
55

6+
<script setup lang="ts">
7+
import { computed, ref, watch } from 'vue'
8+
9+
import IconMicrophone from 'vue-material-design-icons/Microphone.vue'
10+
import IconRefresh from 'vue-material-design-icons/Refresh.vue'
11+
import IconVideo from 'vue-material-design-icons/Video.vue'
12+
import IconVolumeHigh from 'vue-material-design-icons/VolumeHigh.vue'
13+
14+
import { t } from '@nextcloud/l10n'
15+
16+
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
17+
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
18+
19+
type NcSelectOption = { id: string | null, label: string }
20+
type MediaDeviceInfoWithFallbackLabel = MediaDeviceInfo & { fallbackLabel: string }
21+
22+
const props = withDefaults(defineProps<{
23+
kind: 'audioinput' | 'audiooutput' | 'videoinput',
24+
devices: MediaDeviceInfoWithFallbackLabel[],
25+
deviceId?: string | null,
26+
enabled?: boolean,
27+
}>(), {
28+
deviceId: undefined,
29+
enabled: true,
30+
})
31+
32+
const emit = defineEmits<{
33+
(event: 'refresh'): void
34+
(event: 'update:deviceId', value: string | null | undefined): void
35+
}>()
36+
37+
const deviceOptions = computed<NcSelectOption[]>(() => ([
38+
...props.devices.filter(device => device.kind === props.kind)
39+
.map(device => ({ id: device.deviceId, label: device.label ? device.label : device.fallbackLabel })),
40+
{ id: null, label: t('spreed', 'None') },
41+
]))
42+
const deviceOptionsAvailable = computed(() => deviceOptions.value.length > 1)
43+
44+
const deviceIcon = computed<InstanceType<typeof IconMicrophone> | null>(() => {
45+
switch (props.kind) {
46+
case 'audioinput': return IconMicrophone
47+
case 'audiooutput': return IconVolumeHigh
48+
case 'videoinput': return IconVideo
49+
default: return null
50+
}
51+
})
52+
const deviceSelectorPlaceholder = computed(() => {
53+
switch (props.kind) {
54+
case 'audioinput': return deviceOptionsAvailable.value ? t('spreed', 'Select microphone') : t('spreed', 'No microphone available')
55+
case 'audiooutput': return deviceOptionsAvailable.value ? t('spreed', 'Select speaker') : t('spreed', 'No speaker available')
56+
case 'videoinput': return deviceOptionsAvailable.value ? t('spreed', 'Select camera') : t('spreed', 'No camera available')
57+
default: return ''
58+
}
59+
})
60+
61+
const deviceSelectedOption = computed<NcSelectOption | null>({
62+
get: () => {
63+
return deviceOptions.value.find(option => option.id === props.deviceId) ?? null
64+
},
65+
set: (value) => {
66+
updateDeviceId(value?.id ?? null)
67+
}
68+
})
69+
70+
/**
71+
* Update deviceId if passes required checks
72+
* @param deviceId selected NcSelect option to update with
73+
*/
74+
function updateDeviceId(deviceId: NcSelectOption['id']) {
75+
// The deviceSelectedOption may be the same as before yet a change
76+
// could be triggered if media permissions are granted, which would
77+
// update the label.
78+
if (deviceId === props.deviceId) {
79+
return
80+
}
81+
82+
// The previous selected option changed due to the device being
83+
// disconnected, so ignore it as it was not explicitly changed by
84+
// the user.
85+
if (props.deviceId && !deviceOptions.value.find(option => option.id === props.deviceId)) {
86+
return
87+
}
88+
89+
// Ignore device change on initial loading of the settings dialog.
90+
if (typeof props.deviceId === 'undefined') {
91+
return
92+
}
93+
94+
emit('update:deviceId', deviceId)
95+
}
96+
</script>
97+
698
<template>
799
<div class="media-devices-selector">
8100
<component :is="deviceIcon"
@@ -11,7 +103,7 @@
11103
:size="16" />
12104

13105
<NcSelect v-model="deviceSelectedOption"
14-
:input-id="deviceSelectorId"
106+
:input-id="`device-selector-${props.kind}`"
15107
:options="deviceOptions"
16108
label="label"
17109
:aria-label-combobox="t('spreed', 'Select a device')"
@@ -28,170 +120,6 @@
28120
</div>
29121
</template>
30122

31-
<script>
32-
import IconMicrophone from 'vue-material-design-icons/Microphone.vue'
33-
import IconRefresh from 'vue-material-design-icons/Refresh.vue'
34-
import IconVideo from 'vue-material-design-icons/Video.vue'
35-
36-
import { t } from '@nextcloud/l10n'
37-
38-
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
39-
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
40-
41-
export default {
42-
43-
name: 'MediaDevicesSelector',
44-
45-
components: {
46-
NcButton,
47-
NcSelect,
48-
IconMicrophone,
49-
IconRefresh,
50-
IconVideo,
51-
},
52-
53-
props: {
54-
kind: {
55-
validator(value) {
56-
return ['audioinput', 'videoinput'].includes(value)
57-
},
58-
required: true,
59-
},
60-
devices: {
61-
type: Array,
62-
required: true,
63-
},
64-
deviceId: {
65-
type: String,
66-
default: undefined,
67-
},
68-
enabled: {
69-
type: Boolean,
70-
default: true,
71-
},
72-
},
73-
74-
emits: ['refresh', 'update:deviceId'],
75-
76-
data() {
77-
return {
78-
deviceSelectedOption: null,
79-
}
80-
},
81-
82-
computed: {
83-
deviceSelectorId() {
84-
return 'device-selector-' + this.kind
85-
},
86-
87-
deviceIcon() {
88-
switch (this.kind) {
89-
case 'audioinput': return IconMicrophone
90-
case 'videoinput': return IconVideo
91-
default: return null
92-
}
93-
},
94-
95-
deviceOptionsAvailable() {
96-
return this.deviceOptions.length > 1
97-
},
98-
99-
deviceSelectorPlaceholder() {
100-
if (this.kind === 'audioinput') {
101-
return this.audioInputSelectorPlaceholder
102-
}
103-
104-
if (this.kind === 'videoinput') {
105-
return this.videoInputSelectorPlaceholder
106-
}
107-
108-
return null
109-
},
110-
111-
audioInputSelectorPlaceholder() {
112-
if (!this.deviceOptionsAvailable) {
113-
return t('spreed', 'No microphone available')
114-
}
115-
116-
return t('spreed', 'Select microphone')
117-
},
118-
119-
videoInputSelectorPlaceholder() {
120-
if (!this.deviceOptionsAvailable) {
121-
return t('spreed', 'No camera available')
122-
}
123-
124-
return t('spreed', 'Select camera')
125-
},
126-
127-
deviceOptions() {
128-
const options = this.devices.filter(device => device.kind === this.kind).map(device => {
129-
return {
130-
id: device.deviceId,
131-
label: device.label ? device.label : device.fallbackLabel,
132-
}
133-
})
134-
135-
options.push({
136-
id: null,
137-
label: t('spreed', 'None'),
138-
})
139-
140-
return options
141-
},
142-
143-
deviceSelectedOptionFromDeviceId() {
144-
return this.deviceOptions.find(option => option.id === this.deviceId)
145-
},
146-
},
147-
148-
watch: {
149-
// The watcher needs to be set as "immediate" to ensure that
150-
// "deviceSelectedOption" will be set when mounted.
151-
deviceSelectedOptionFromDeviceId: {
152-
handler(deviceSelectedOptionFromDeviceId) {
153-
this.deviceSelectedOption = deviceSelectedOptionFromDeviceId
154-
},
155-
immediate: true,
156-
},
157-
158-
// The watcher should not be set as "immediate" to prevent
159-
// "update:deviceId" from being emitted when mounted with the same value
160-
// initially passed to the component.
161-
deviceSelectedOption(deviceSelectedOption, previousSelectedOption) {
162-
// The deviceSelectedOption may be the same as before yet a change
163-
// could be triggered if media permissions are granted, which would
164-
// update the label.
165-
if (deviceSelectedOption && previousSelectedOption && deviceSelectedOption.id === previousSelectedOption.id) {
166-
return
167-
}
168-
169-
// The previous selected option changed due to the device being
170-
// disconnected, so ignore it as it was not explicitly changed by
171-
// the user.
172-
if (previousSelectedOption && previousSelectedOption.id && !this.deviceOptions.find(option => option.id === previousSelectedOption.id)) {
173-
return
174-
}
175-
176-
// Ignore device change on initial loading of the settings dialog.
177-
if (typeof previousSelectedOption?.id === 'undefined') {
178-
return
179-
}
180-
181-
if (deviceSelectedOption && deviceSelectedOption.id === null) {
182-
this.$emit('update:deviceId', null)
183-
return
184-
}
185-
this.$emit('update:deviceId', deviceSelectedOption ? deviceSelectedOption.id : undefined)
186-
},
187-
},
188-
189-
methods: {
190-
t,
191-
},
192-
}
193-
</script>
194-
195123
<style lang="scss" scoped>
196124
.media-devices-selector {
197125
display: flex;

0 commit comments

Comments
 (0)