diff --git a/front/package.json b/front/package.json index dd8f9a0ce..0b7a0cf88 100644 --- a/front/package.json +++ b/front/package.json @@ -22,9 +22,9 @@ "@sentry/tracing": "7.17.2", "@sentry/vue": "7.17.2", "@vue/runtime-core": "3.2.41", - "@vueuse/core": "9.1.1", - "@vueuse/integrations": "9.1.1", - "@vueuse/router": "9.1.1", + "@vueuse/core": "9.3.0", + "@vueuse/integrations": "9.3.0", + "@vueuse/router": "9.3.0", "axios": "0.27.2", "axios-auth-refresh": "3.3.4", "diff": "5.1.0", @@ -36,13 +36,13 @@ "lodash-es": "4.17.21", "moment": "2.29.4", "qs": "6.11.0", - "sass": "1.54.9", "showdown": "2.1.0", - "standardized-audio-context": "^25.3.32", + "standardized-audio-context": "25.3.32", "text-clipper": "2.2.0", "transliteration": "2.3.5", "universal-cookie": "4.0.4", "vue": "3.2.41", + "vue": "3.2.40", "vue-gettext": "2.1.12", "vue-router": "4.1.6", "vue-upload-component": "3.1.2", @@ -74,24 +74,25 @@ "@vue/test-utils": "2.2.1", "@vue/tsconfig": "0.1.3", "easygettext": "2.17.0", - "eslint": "8.23.1", + "eslint": "8.24.0", "eslint-config-standard": "17.0.0", "eslint-plugin-html": "7.1.0", "eslint-plugin-import": "2.26.0", - "eslint-plugin-n": "15.2.5", + "eslint-plugin-n": "15.3.0", "eslint-plugin-node": "11.1.0", "eslint-plugin-promise": "6.0.1", - "eslint-plugin-vue": "9.4.0", + "eslint-plugin-vue": "9.6.0", "jsdom": "20.0.1", "moxios": "0.4.0", + "sass": "1.55.0", "sinon": "14.0.1", "typescript": "4.8.4", "utility-types": "3.10.0", - "vite": "3.0.9", - "vite-plugin-pwa": "0.12.4", + "vite": "3.1.6", + "vite-plugin-pwa": "0.13.1", "vite-plugin-vue-inspector": "1.1.3", - "vitest": "0.22.1", - "vue-tsc": "0.40.13", + "vitest": "0.24.0", + "vue-tsc": "1.0.0", "workbox-core": "6.5.4", "workbox-precaching": "6.5.4", "workbox-routing": "6.5.4", diff --git a/front/src/api/index.ts b/front/src/api/index.ts new file mode 100644 index 000000000..189149b4b --- /dev/null +++ b/front/src/api/index.ts @@ -0,0 +1,5 @@ +import { registerSoundImplementation } from '~/api/player' + +window.funkwhale = { + registerSoundImplementation +} diff --git a/front/src/api/player.ts b/front/src/api/player.ts new file mode 100644 index 000000000..30f9bb8dd --- /dev/null +++ b/front/src/api/player.ts @@ -0,0 +1,113 @@ +import type { IAudioContext, IAudioNode } from 'standardized-audio-context' + +import { createAudioSource } from '~/composables/audio/audio-api' +import { reactive, ref, type Ref } from 'vue' +import { createEventHook, refDefault, type EventHookOn } from '@vueuse/shared' +import { useEventListener } from '@vueuse/core' + +export interface SoundSource { + uuid: string + mimetype: string + url: string +} + +export interface Sound { + preload(): void | Promise + dispose(): void + + readonly audioNode: IAudioNode + readonly isLoaded: Ref + readonly currentTime: number + readonly duration: number + readonly buffered: number + + play(): void | Promise + pause(): void | Promise + + seekTo(seconds: number): void | Promise + seekBy(seconds: number): void | Promise + + onSoundEnd: EventHookOn +} + +export const soundImplementations = reactive(new Set>()) + +export const registerSoundImplementation = >(implementation: T) => { + soundImplementations.add(implementation) + return implementation +} + +// Default Sound implementation +@registerSoundImplementation +export class HTMLSound implements Sound { + #audio = new Audio() + #soundEndEventHook = createEventHook() + + readonly isLoaded = ref(false) + + audioNode = createAudioSource(this.#audio) + onSoundEnd: EventHookOn + + constructor (sources: SoundSource[]) { + // TODO: Quality picker + this.#audio.src = sources[0].url + this.#audio.preload = 'auto' + + useEventListener(this.#audio, 'ended', () => this.#soundEndEventHook.trigger(this)) + useEventListener(this.#audio, 'loadeddata', () => { + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState + this.isLoaded.value = this.#audio.readyState >= 2 + }) + + this.onSoundEnd = this.#soundEndEventHook.on + } + + preload () { + this.#audio.load() + } + + dispose () { + this.audioNode.disconnect() + } + + async play () { + this.#audio.play() + } + + async pause () { + this.#audio.pause() + } + + async seekTo (seconds: number) { + this.#audio.currentTime = seconds + } + + async seekBy (seconds: number) { + this.#audio.currentTime += seconds + } + + get duration () { + const { duration } = this.#audio + return isNaN(duration) ? 0 : duration + } + + get buffered () { + // https://developer.mozilla.org/en-US/docs/Web/Guide/Audio_and_video_delivery/buffering_seeking_time_ranges#creating_our_own_buffering_feedback + if (this.duration > 0) { + const { length } = this.#audio.buffered + for (let i = 0; i < length; i++) { + if (this.#audio.buffered.start(length - 1 - i) < this.#audio.currentTime) { + return this.#audio.buffered.end(length - 1 - i) + } + } + } + + return 0 + } + + get currentTime () { + return this.#audio.currentTime + } +} + +export const soundImplementation = refDefault(ref>(), HTMLSound) diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue index a8d9c75b2..776e158b1 100644 --- a/front/src/components/audio/Player.vue +++ b/front/src/components/audio/Player.vue @@ -1,13 +1,29 @@