Add media session controls

This commit is contained in:
lucast committed 2026-09-26 22:18:33 +02:00
1 parent 5e3f3ef6a2
commit b5f7aa1372
2 files changed
+139 -19

No files matched your search

+7
View File
@@ -15,11 +15,18 @@ onMounted(() => {
mouseMovedRecently.value = false mouseMovedRecently.value = false
}, 2000) }, 2000)
} }
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === "Escape" && fullscreen.value) {
closeFullscreen()
}
}
window.addEventListener("mousemove", handleMouseMove) window.addEventListener("mousemove", handleMouseMove)
window.addEventListener("keydown", handleKeydown)
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener("mousemove", handleMouseMove) window.removeEventListener("mousemove", handleMouseMove)
window.removeEventListener("keydown", handleKeydown)
}) })
}) })
+132 -19
View File
@@ -59,40 +59,114 @@ export default defineNuxtPlugin((nuxtApp) => {
}, },
}) })
const duration = ref(0) const duration = ref(0)
let lastPositionUpdate = 0
let mediaSessionSongId: string | null = null
const aniFrame = ref<number | null>(null) const aniFrame = ref<number | null>(null)
// Helpers // Helpers
const updateMediaSession = async () => { const updateMediaSession = () => {
if (!import.meta.client) return if (!import.meta.client) return
if (!navigator.mediaSession) { const mediaSession = navigator.mediaSession
console.warn("Media Session API is not supported") if (!mediaSession) return
if (!currentSong.value) {
mediaSession.metadata = null
mediaSession.playbackState = "none"
mediaSessionSongId = null
return return
} }
if (!currentSong.value) return if (mediaSessionSongId !== currentSong.value._id) {
mediaSession.metadata = new MediaMetadata({
title: currentSong.value.name,
artist: currentSong.value.artists[0]?.name,
album: currentSong.value.album.name,
artwork: [
{
src: GET_FILE(currentSong.value.album.file),
sizes: "512x512",
},
],
})
mediaSessionSongId = currentSong.value._id
}
navigator.mediaSession.metadata = new MediaMetadata({ if (duration.value > 0) {
title: currentSong.value.name, try {
artist: currentSong.value.artists[0]?.name, mediaSession.setPositionState({
album: currentSong.value.album.name, duration: duration.value,
artwork: [ position: Math.min(secondsPlayed.value, duration.value),
{ })
src: GET_FILE(currentSong.value.album.file), } catch {
sizes: "512x512", // Position state can be unavailable while media metadata is loading.
}, }
], }
}) }
navigator.mediaSession.setPositionState({ const registerMediaSessionActions = () => {
duration: duration.value, if (!import.meta.client || !navigator.mediaSession) return
position: secondsPlayed.value,
}) const actions: MediaSessionAction[] = [
"play",
"pause",
"nexttrack",
"previoustrack",
"seekto",
"seekbackward",
"seekforward",
]
for (const action of actions) {
try {
navigator.mediaSession.setActionHandler(action, (event) => {
switch (action) {
case "play":
void play()
break
case "pause":
pause()
break
case "nexttrack":
void next()
break
case "previoustrack":
void prev()
break
case "seekto":
if (event.seekTime !== undefined)
secondsPlayed.value = event.seekTime
updateMediaSession()
break
case "seekbackward":
secondsPlayed.value = Math.max(
0,
secondsPlayed.value - (event.seekOffset ?? 10),
)
updateMediaSession()
break
case "seekforward":
secondsPlayed.value = Math.min(
duration.value,
secondsPlayed.value + (event.seekOffset ?? 10),
)
updateMediaSession()
}
})
} catch {
// Browsers may support Media Session without supporting every action.
}
}
} }
const startTracking = () => { const startTracking = () => {
const update = () => { const update = () => {
tick.value++ tick.value++
const now = Date.now()
if (now - lastPositionUpdate >= 1000) {
lastPositionUpdate = now
updateMediaSession()
}
aniFrame.value = requestAnimationFrame(update) aniFrame.value = requestAnimationFrame(update)
} }
@@ -116,6 +190,7 @@ export default defineNuxtPlugin((nuxtApp) => {
onload: () => { onload: () => {
duration.value = sound.value?.duration() || 0 duration.value = sound.value?.duration() || 0
sound.value?.volume(storedVolume.value) sound.value?.volume(storedVolume.value)
updateMediaSession()
}, },
onend: () => { onend: () => {
void closeFullscreen() void closeFullscreen()
@@ -225,6 +300,44 @@ export default defineNuxtPlugin((nuxtApp) => {
currentSong.value = null currentSong.value = null
sound.value?.stop() sound.value?.stop()
sound.value = null sound.value = null
updateMediaSession()
}
if (import.meta.client) {
registerMediaSessionActions()
window.addEventListener("keydown", (event) => {
if (
event.defaultPrevented ||
event.repeat ||
event.ctrlKey ||
event.metaKey ||
event.altKey
) return
const target = event.target
if (
target instanceof HTMLElement &&
target.closest(
"input, textarea, select, button, [contenteditable='true'], [role='slider']",
)
) return
switch (event.code) {
case "Space":
event.preventDefault()
if (isPlaying.value) pause()
else void play()
break
case "ArrowRight":
event.preventDefault()
void next()
break
case "ArrowLeft":
event.preventDefault()
void prev()
}
})
} }
const shuffle = () => { const shuffle = () => {