JavaScript MediaSession在更改HTMLAudioElement的’src’属性时停止工作。

huangapple go评论72阅读模式
英文:

JavaScript MediaSession stops working when changing the 'src' attribute of HTMLAudioElement

问题

我正在创建一个音乐播放器网络应用程序,几乎所有的东西都已经处理好了,但是我遇到了一个重大问题。

首先,这是有效的部分。当音乐开始播放时,MediaSession 开始工作。这会在用户的手机上显示通知,并允许他们使用蓝牙音频设备来控制播放。当用户驾车时,后者尤其重要,因为如果用户已将手机与车辆配对,他们可以使用车辆的音频控制来控制播放,而不必低头看手机。

现在,问题出在这里。为了切换歌曲,HTMLAudioElementsrc 属性会被更改,它会自动从服务器开始加载新的歌曲文件。如果新歌曲立即开始播放,一切都正常,但如果播放被暂停或在加载歌曲时出现错误,那么MediaSession 将停止工作。通知消失,操作处理程序不再对来自蓝牙设备的输入做出反应。

以下是我在Vue的Pinia store中的相关代码的简化摘录:

export const useMusicStore = defineStore('music', {

  state: () => ({
    player: new Audio(),
    queue: [],
    currentQueueId: null,
    isPaused: true,
  }),

  getters: {
    currentSong() {
      // ...
    },
  },

  actions: {

    initialize() {
      this.addEventListeners()
      this.setActionHandlers()
      this.loadLastSession()
    },

    addEventListeners() {
      this.player.addEventListener('play', () => this.updatePlaybackState('playing'))
      this.player.addEventListener('pause', () => this.updatePlaybackState('paused'))
      this.player.addEventListener('timeupdate', () => this.updatePositionState())
      this.player.addEventListener('loadedmetadata', () => this.updatePositionState())
      this.player.addEventListener('canplay', () => {
        if (!this.isPaused) {
          this.play()
        }
      })
      this.player.addEventListener('ended', () => {
        this.nextSong()
        this.play()
      })
      this.player.addEventListener('error', (el, err) => {
        console.error(err)
        Notify.create("Error encounted while attempting to play song.")
      })
    },


    setActionHandlers() {
      const actionHandlers = {
        play: () => this.play(),
        pause: () => this.pause(),
        stop: () => this.stop(),
        seekto: (details) => this.seek(details.seekTime),
        seekbackward: (details) => this.skipBack(details.seekOffset ?? 10),
        seekforward: (details) => this.skipForward(details.seekOffset ?? 10),
        previoustrack: () => this.skipBack(),
        nexttrack: () => this.skipForward(),
      }

      if ('mediaSession' in navigator) {
        for (const [action, handler] of Object.entries(actionHandlers)) {
          try {
            navigator.mediaSession.setActionHandler(action, handler)
          } catch {
            console.log(`Action '${action}' not supported.`)
          }
        }
      }
    },

    loadCurrentSong() {
      if (this.currentSong) {
        this.player.src = this.getSongUrl(this.currentSong)
      }
    },

  }
}

在上面的代码中,您可以看到当 loadCurrentSong() 运行时,它会更改 HTMLAudioElementsrc 属性,这会自动开始加载歌曲文件。一旦文件加载了足够多的内容,HTMLAudioElement 会触发一个 "canplay" 事件。如果用户没有暂停播放,相关事件监听器会开始播放歌曲。

有没有办法确保在切换歌曲时 MediaSession 保持 "活动"?我可以考虑使用库,但前提是能够处理用户界面(在Vue中)。我尝试过 Howler.js,但仍然遇到了完全相同的问题。

英文:

I'm creating a music player web app, and I've got almost everything worked out, but I've run into a major problem.

First, here's the part that works. When music begins playing, MediaSession kicks in. This causes a notification to appear on the user's phone and let's them control playback with a Bluetooth audio device. The latter is especially important when driving, because if the user has paired their phone with their car, they can control playback with their car's audio controls instead of having to look down at their phone.

Now, here's where the problem comes in. In order to change songs, the src attribute of HTMLAudioElement gets changed, and it automatically begins loading the new song file from the server. If the new song starts playing right away, everything works fine, but if playback is paused or if an error is encountered while loading the song, then MediaSession stops working. The notification disappears and the action handlers no longer react to input from the Bluetooth device.

Here's a simplified excerpt of the relevant code from my Pinia store in Vue:

export const useMusicStore = defineStore('music', {
state: () => ({
player: new Audio(),
queue: [],
currentQueueId: null,
isPaused: true,
}),
getters: {
currentSong() {
// ...
},
},
actions: {
initialize() {
this.addEventListeners()
this.setActionHandlers()
this.loadLastSession()
},
addEventListeners() {
this.player.addEventListener('play', () => this.updatePlaybackState('playing'))
this.player.addEventListener('pause', () => this.updatePlaybackState('paused'))
this.player.addEventListener('timeupdate', () => this.updatePositionState())
this.player.addEventListener('loadedmetadata', () => this.updatePositionState())
this.player.addEventListener('canplay', () => {
if (!this.isPaused) {
this.play()
}
})
this.player.addEventListener('ended', () => {
this.nextSong()
this.play()
})
this.player.addEventListener('error', (el, err) => {
console.error(err)
Notify.create("Error encounted while attempting to play song.")
})
},
setActionHandlers() {
const actionHandlers = {
play: () => this.play(),
pause: () => this.pause(),
stop: () => this.stop(),
seekto: (details) => this.seek(details.seekTime),
seekbackward: (details) => this.skipBack(details.seekOffset ?? 10),
seekforward: (details) => this.skipForward(details.seekOffset ?? 10),
previoustrack: () => this.skipBack(),
nexttrack: () => this.skipForward(),
}
if ('mediaSession' in navigator) {
for (const [action, handler] of Object.entries(actionHandlers)) {
try {
navigator.mediaSession.setActionHandler(action, handler)
} catch {
console.log(`Action '${action}' not supported.`)
}
}
}
},
loadCurrentSong() {
if (this.currentSong) {
this.player.src = this.getSongUrl(this.currentSong)
}
},
}
}

In the above code, you can see that when loadCurrentSong() runs, it changes the src attribute of the HTMLAudioElement, which automatically begins loading the song file. When enough of the file has been loaded, HTMLAudioElement fires a "canplay" event. This gets heard by the relevant event listener, which begins playing the song only if the user didn't have playback paused.

Is there a way of ensuring that MediaSession stays "alive" when switching songs? I'm open to using a library, but only if lets me handle the user interface (in Vue). I tried Howler.js, but I still ran into the exact same problem.

答案1

得分: 2

我修复了它!出于某种原因,MediaSession 会在 src 属性更改时关闭,除非随后立即开始音频播放。我不得不想出某种方法来适应这个“特性”,同时仍然保留用户在暂停模式下跳过曲目的能力。在阅读了 这个问题这个问题 后,我想出了一个解决方案。我创建了一个第二个音频元素,硬编码了 10 秒的静音,我从 这个存储库 下载了它。在 loadCurrentSong() 函数的末尾,在主音频元素的 src 更改后,静音播放然后立即暂停。由于它是同步发生的,用户永远不会注意到它,但足以保持 MediaSession 保持活动状态。以下是修改后代码的相关部分:

export const useMusicStore = defineStore('music', {

  state: () => ({
    player: new Audio(),
    silence: new Audio('10-seconds-of-silence.mp3'),
    // ...
  }),

  actions: {

    loadCurrentSong() {
      if (this.currentSong) {
        this.player.src = this.getSongUrl(this.currentSong)
        this.player.load()
        this.updateMetadata()
        this.updatePositionState()
        this.silence.play()
        this.silence.pause()
      }
    },

  }
}
英文:

I fixed it! For some reason, MediaSession will shut down any time the src attribute changes, unless audio playback starts immediately afterward. I had to figure out some way to accommodate this "feature" while still preserving the ability for users to skip tracks in pause mode. I came up with a solution after reading this question and this question. I created a second audio element hardcoded with 10 seconds of silence, which I downloaded from this repository. At the end of the loadCurrentSong() function, after the src of the main audio element is changed, the silence plays and then immediately pauses. Since it happens synchronously, the user will never notice it, but it's enough to keep MediaSession alive. Here's the relevant bits of modified code:

export const useMusicStore = defineStore('music', {
state: () => ({
player: new Audio(),
silence: new Audio('10-seconds-of-silence.mp3'),
// ...
}),
actions: {
loadCurrentSong() {
if (this.currentSong) {
this.player.src = this.getSongUrl(this.currentSong)
this.player.load()
this.updateMetadata()
this.updatePositionState()
this.silence.play()
this.silence.pause()
}
},
}
}

huangapple
  • 本文由 发表于 2023年5月29日 11:29:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/76354522.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定