let weixinReadyPromise = new Promise((resolve, reject) => { if (!window.navigator.userAgent.toLowerCase().includes('micromessenger')) { return resolve('other') } if (!window.wx) { alert('请引入微信相关文件') } window.wx.config({ debug: false, appId: '', timestamp: 1, nonceStr: '', signature: '', jsApiList: [] }) window.wx.ready(async () => { await new Promise(res => setTimeout(res, 200)) // 立刻resolve的话,自动播放可能会失败 resolve('wx') }) }) export class PlayInfo { src = '' startTime = 0 endTime = 0 timeUpdate: (e: Event) => void = null updateStatus: (status: string) => void = null } export class AudioPromiseInner { info = new PlayInfo() audio: HTMLAudioElement = null canPlayPromise: Promise = null canPlayDefer: Defer = null playDefer: Defer = null async play(src, startTime = 0, endTime = 0, timeUpdate = f => f, updateStatus = f => f) { if (!src) throw new Error('bad audio src: ' + src) await weixinReadyPromise if (!this.audio) { this.audio = new Audio() this.audio.addEventListener("canplay", () => this.onCanPlay()) this.audio.addEventListener("ended", () => this.onEnded()) this.audio.addEventListener("timeupdate", e => this.onTimeupdate(e)) for (let e of "abort error stalled suspend waiting".split(' ')) { console.log('adding listener: ', e) this.audio.addEventListener(e, (...args) => console.log('event: ', ...args, 'for', src)) } } if (this.info.src !== src) { this.pause('', 'paused') if (src) { this.audio.src = src this.audio.load() setTimeout(() => this.onCanPlay(), 500) // redrocket项目read页面直接刷新无法播放单词,定位到onCanPlay未被调用,待进一步诊断 } this.canPlayPromise = new Promise((resolve, reject) => { this.canPlayDefer = {resolve, reject} }) } Object.assign(this.info, {src, startTime, endTime, timeUpdate, updateStatus}) console.log(`playing ${this.info.src} from ${startTime} -> ${endTime || ''}`) await this.canPlayPromise return new Promise((resolve, reject) => { // promise 返回三种状态:ended paused error this.playDefer = {resolve, reject} this.audio.currentTime = startTime let p = this.audio.play() p && p.catch && p.catch(err => { // 错误的情况也要让播放返回,低版本iphone不支持catch resolve('error') }) }) } pause(src, status = 'paused') { if (src && src !== this.info.src) return this.audio && this.audio.pause() this.info.updateStatus && this.info.updateStatus(status) if (this.playDefer) { this.playDefer.resolve(status) } } onEnded() { this.pause('', 'ended') } onTimeupdate(e) { if (this.info.endTime && +e.target.currentTime > this.info.endTime) { this.pause('', 'ended') } this.info.timeUpdate(e) } onCanPlay() { this.canPlayDefer.resolve(true) } } let singleAudio = new AudioPromiseInner() export default class AudioPromise { info = new PlayInfo() constructor(src, timeUpdate = f => f, updateStatus = f => f) { Object.assign(this.info, {src, timeUpdate, updateStatus}) } async play(startTime = 0, endTime = 0) { return singleAudio.play(this.info.src, startTime, endTime, this.info.timeUpdate, this.info.updateStatus) } async pause() { singleAudio.canPlayPromise && await singleAudio.canPlayPromise return singleAudio.pause(this.info.src) } }