Skip to content

Commit

Permalink
Merge pull request #1974 from nextcloud/feat/871/call-sound-second-de…
Browse files Browse the repository at this point in the history
…vice

feat(sounds): play call notification sound on second output device
  • Loading branch information
nickvergessen authored Jul 24, 2024
2 parents 6d13ba8 + 0f470b2 commit f233a2a
Show file tree
Hide file tree
Showing 11 changed files with 209 additions and 26 deletions.
4 changes: 2 additions & 2 deletions js/notifications-main.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/notifications-main.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions js/notifications-settings.js

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions js/notifications-settings.js.license
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,27 @@ SPDX-License-Identifier: (MPL-2.0 OR Apache-2.0)
SPDX-FileCopyrightText: escape-html developers
SPDX-FileCopyrightText: Varun A P
SPDX-FileCopyrightText: Tobias Koppers @sokra
SPDX-FileCopyrightText: Titus Wormer <tituswormer@gmail.com> (https://wooorm.com)
SPDX-FileCopyrightText: T. Jameson Little <t.jameson.little@gmail.com>
SPDX-FileCopyrightText: Roman Shtylman <shtylman@gmail.com>
SPDX-FileCopyrightText: Roeland Jago Douma
SPDX-FileCopyrightText: Paul Vorbach <paul@vorba.ch> (http://paul.vorba.ch)
SPDX-FileCopyrightText: Paul Vorbach <paul@vorb.de> (http://vorb.de)
SPDX-FileCopyrightText: Matt Zabriskie
SPDX-FileCopyrightText: John-David Dalton <john.david.dalton@gmail.com> (http://allyoucanleet.com/)
SPDX-FileCopyrightText: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
SPDX-FileCopyrightText: Joas Schilling <coding@schilljs.com>
SPDX-FileCopyrightText: Jeff Sagal <sagalbot@gmail.com>
SPDX-FileCopyrightText: James Halliday
SPDX-FileCopyrightText: Jacob Clevenger<https://github.com/wheatjs>
SPDX-FileCopyrightText: Hypercontext
SPDX-FileCopyrightText: Guillaume Chau <guillaume.b.chau@gmail.com>
SPDX-FileCopyrightText: GitHub Inc.
SPDX-FileCopyrightText: Feross Aboukhadijeh
SPDX-FileCopyrightText: Faisal Salman <f@faisalman.com> (http://faisalman.com)
SPDX-FileCopyrightText: Evan You
SPDX-FileCopyrightText: Eugene Sharygin <eush77@gmail.com>
SPDX-FileCopyrightText: Eric Norris (https://github.com/ericnorris)
SPDX-FileCopyrightText: Dr.-Ing. Mario Heiderich, Cure53 <mario@cure53.de> (https://cure53.de/)
SPDX-FileCopyrightText: David Clark
SPDX-FileCopyrightText: Christoph Wurst
Expand All @@ -34,6 +44,12 @@ This file is generated from multiple sources. Included packages:
- @nextcloud/axios
- version: 2.5.0
- license: GPL-3.0-or-later
- @nextcloud/browser-storage
- version: 0.4.0
- license: GPL-3.0-or-later
- @nextcloud/capabilities
- version: 1.2.0
- license: GPL-3.0-or-later
- @nextcloud/dialogs
- version: 5.3.5
- license: AGPL-3.0-or-later
Expand All @@ -49,12 +65,18 @@ This file is generated from multiple sources. Included packages:
- @nextcloud/router
- version: 3.0.1
- license: GPL-3.0-or-later
- @nextcloud/vue-select
- version: 3.25.0
- license: MIT
- @nextcloud/l10n
- version: 3.1.0
- license: GPL-3.0-or-later
- @nextcloud/vue
- version: 8.14.0
- license: AGPL-3.0-or-later
- @vueuse/components
- version: 10.9.0
- license: MIT
- @vueuse/core
- version: 10.9.0
- license: MIT
Expand All @@ -70,6 +92,12 @@ This file is generated from multiple sources. Included packages:
- buffer
- version: 6.0.3
- license: MIT
- charenc
- version: 0.0.2
- license: BSD-3-Clause
- crypt
- version: 0.0.2
- license: BSD-3-Clause
- css-loader
- version: 6.8.1
- license: MIT
Expand All @@ -88,9 +116,18 @@ This file is generated from multiple sources. Included packages:
- ieee754
- version: 1.2.1
- license: BSD-3-Clause
- is-buffer
- version: 1.1.6
- license: MIT
- linkify-string
- version: 4.1.1
- license: MIT
- lodash.get
- version: 4.4.2
- license: MIT
- md5
- version: 2.3.0
- license: BSD-3-Clause
- node-gettext
- version: 3.0.0
- license: MIT
Expand All @@ -100,12 +137,30 @@ This file is generated from multiple sources. Included packages:
- process
- version: 0.11.10
- license: MIT
- striptags
- version: 3.2.0
- license: MIT
- style-loader
- version: 3.3.3
- license: MIT
- toastify-js
- version: 1.12.0
- license: MIT
- ua-parser-js
- version: 1.0.38
- license: MIT
- unist-builder
- version: 4.0.0
- license: MIT
- unist-util-is
- version: 6.0.0
- license: MIT
- unist-util-visit-parents
- version: 6.0.1
- license: MIT
- unist-util-visit
- version: 5.0.0
- license: MIT
- vue
- version: 2.7.16
- license: MIT
Expand Down
2 changes: 1 addition & 1 deletion js/notifications-settings.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions js/notifications-src_NotificationsApp_vue.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/notifications-src_NotificationsApp_vue.js.map

Large diffs are not rendered by default.

25 changes: 24 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@nextcloud/router": "^3.0.1",
"@nextcloud/vue": "^8.14.0",
"howler": "^2.2.4",
"ua-parser-js": "^1.0.38",
"v-click-outside": "^3.2.0",
"vue": "^2.7.16",
"vue-material-design-icons": "^5.3.0"
Expand Down
25 changes: 19 additions & 6 deletions src/services/webNotificationsService.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { emit } from '@nextcloud/event-bus'
import { loadState } from '@nextcloud/initial-state'
import { generateFilePath } from '@nextcloud/router'
import { Howl } from 'howler'
import BrowserStorage from './BrowserStorage.js'

/**
* Create a browser notification
Expand Down Expand Up @@ -59,14 +60,26 @@ const createWebNotification = (notification) => {
const playNotificationSound = (notification) => {
if (notification.app === 'spreed' && notification.objectType === 'call') {
if (loadState('notifications', 'sound_talk')) {
const sound = new Howl({
src: [
generateFilePath('notifications', 'img', 'talk.ogg'),
],
const howlPayload = {
src: [generateFilePath('notifications', 'img', 'talk.ogg')],
html5: true, // to access HTMLAudioElement property 'sinkId'
volume: 0.5,
})

}
const sound = new Howl(howlPayload)
const primaryDeviceId = sound._sounds[0]._node.sinkId ?? ''
sound.play()

const secondarySpeakerEnabled = BrowserStorage.getItem('secondary_speaker') === 'true'
const secondaryDeviceId = JSON.parse(BrowserStorage.getItem('secondary_speaker_device'))?.id ?? null
// Play only if secondary device is enabled, selected and different from primary device
if (secondarySpeakerEnabled && secondaryDeviceId && primaryDeviceId !== secondaryDeviceId) {
const soundDuped = new Howl(howlPayload)
const audioElement = sound._sounds[0]._node // Access the underlying HTMLAudioElement
audioElement.setSinkId?.(secondaryDeviceId)
.then(() => console.debug('Audio output successfully redirected to secondary speaker'))
.catch((error) => console.error('Failed to redirect audio output:', error))
soundDuped.play()
}
}
} else if (loadState('notifications', 'sound_notification')) {
const sound = new Howl({
Expand Down
111 changes: 101 additions & 10 deletions src/views/UserSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
name="notification_reminder_batchtime"
class="notification-frequency__select"
@change="updateSettings()">
<option v-for="option in batchtime_options" :key="option.value" :value="option.value">
<option v-for="option in BATCHTIME_OPTIONS" :key="option.value" :value="option.value">
{{ option.text }}
</option>
</select>
Expand All @@ -31,16 +31,42 @@
@update:checked="updateSettings">
{{ t('notifications', 'Play sound when a call started (requires Nextcloud Talk)') }}
</NcCheckboxRadioSwitch>

<template v-if="config.sound_talk">
<NcCheckboxRadioSwitch class="additional-margin-top"
:checked.sync="storage.secondary_speaker"
:disabled="isSafari"
@update:checked="updateLocalSettings">
{{ t('notifications', 'Also repeat sound on a secondary speaker') }}
</NcCheckboxRadioSwitch>
<div v-if="isSafari" class="notification-frequency__warning">
<strong>{{ t('notifications', 'Selection of the speaker device is currently not supported by Safari') }}</strong>
</div>
<NcSelect v-if="!isSafari && storage.secondary_speaker"
v-model="storage.secondary_speaker_device"
input-id="device-selector-audio-output"
:options="devices"
label="label"
:aria-label-combobox="t('notifications', 'Select a device')"
:clearable="false"
:placeholder="t('notifications', 'Select a device')"
@open="initializeDevices"
@input="updateLocalSettings" />
</template>
</NcSettingsSection>
</template>

<script>
import UAParser from 'ua-parser-js'
import { reactive, ref } from 'vue'
import axios from '@nextcloud/axios'
import { generateOcsUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
import { showSuccess, showError } from '@nextcloud/dialogs'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
import BrowserStorage from '../services/BrowserStorage.js'

const EmailFrequency = {
EMAIL_SEND_OFF: 0,
Expand All @@ -49,24 +75,40 @@ const EmailFrequency = {
EMAIL_SEND_DAILY: 3,
EMAIL_SEND_WEEKLY: 4,
}
const BATCHTIME_OPTIONS = [
{ text: t('notifications', 'Never'), value: EmailFrequency.EMAIL_SEND_OFF },
{ text: t('notifications', '1 hour'), value: EmailFrequency.EMAIL_SEND_HOURLY },
{ text: t('notifications', '3 hours'), value: EmailFrequency.EMAIL_SEND_3HOURLY },
{ text: t('notifications', '1 day'), value: EmailFrequency.EMAIL_SEND_DAILY },
{ text: t('notifications', '1 week'), value: EmailFrequency.EMAIL_SEND_WEEKLY },
]
const EMPTY_DEVICE_OPTION = { id: null, label: t('notifications', 'None') }
const parser = new UAParser()
const browser = parser.getBrowser()
const isSafari = browser.name === 'Safari' || browser.name === 'Mobile Safari'

export default {
name: 'UserSettings',
components: {
NcCheckboxRadioSwitch,
NcSelect,
NcSettingsSection,
},

data() {
setup() {
const config = reactive(loadState('notifications', 'config'))
const storage = reactive({
secondary_speaker: BrowserStorage.getItem('secondary_speaker') === 'true',
secondary_speaker_device: JSON.parse(BrowserStorage.getItem('secondary_speaker_device')) ?? EMPTY_DEVICE_OPTION,
})
const devices = ref([])

return {
batchtime_options: [
{ text: t('notifications', 'Never'), value: EmailFrequency.EMAIL_SEND_OFF },
{ text: t('notifications', '1 hour'), value: EmailFrequency.EMAIL_SEND_HOURLY },
{ text: t('notifications', '3 hours'), value: EmailFrequency.EMAIL_SEND_3HOURLY },
{ text: t('notifications', '1 day'), value: EmailFrequency.EMAIL_SEND_DAILY },
{ text: t('notifications', '1 week'), value: EmailFrequency.EMAIL_SEND_WEEKLY },
],
config: loadState('notifications', 'config'),
BATCHTIME_OPTIONS,
isSafari,
config,
storage,
devices,
}
},

Expand All @@ -84,7 +126,56 @@ export default {
console.error(error)
}
},

updateLocalSettings() {
try {
BrowserStorage.setItem('secondary_speaker', this.storage.secondary_speaker)
if (this.storage.secondary_speaker && this.storage.secondary_speaker_device.id) {
BrowserStorage.setItem('secondary_speaker_device', JSON.stringify(this.storage.secondary_speaker_device))
} else {
BrowserStorage.removeItem('secondary_speaker_device')
}
showSuccess(t('notifications', 'Your settings have been updated.'))
} catch (error) {
showError(t('notifications', 'An error occurred while updating your settings.'))
console.error(error)
}
},

async initializeDevices() {
const isAudioSupported = !isSafari && navigator?.mediaDevices?.getUserMedia && navigator?.mediaDevices?.enumerateDevices
if (!isAudioSupported || this.devices.length > 0) {
return
}

let stream = null
try {
// Request permissions to get audio devices
stream = await navigator.mediaDevices.getUserMedia({ audio: true })
// Enumerate devices and populate NcSelect options
this.devices = (await navigator.mediaDevices.enumerateDevices() ?? [])
.filter(device => device.kind === 'audiooutput')
.map(device => ({
id: device.deviceId,
label: device.label ? device.label : device.fallbackLabel,
}))
.concat([EMPTY_DEVICE_OPTION])
} catch (error) {
showError(t('notifications', 'An error occurred while updating your settings.'))
console.error('Error while requesting or initializing audio devices: ', error)
} finally {
if (stream) {
stream.getTracks().forEach(track => track.stop())
}
}
},
},
}

</script>

<style lang="scss" scoped>
.additional-margin-top {
margin-top: 12px;
}
</style>

0 comments on commit f233a2a

Please sign in to comment.