|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
class Player { |
|
constructor() { |
|
this.playlist = []; |
|
this.index = -1; |
|
this._loop_mode = 0; |
|
this._media_uri_list = {}; |
|
this.playedFrom = 0; |
|
this.mode = 'background'; |
|
this.skipTime = 15; |
|
} |
|
|
|
setMode(newMode) { |
|
this.mode = newMode; |
|
} |
|
|
|
setRefreshRate(rate = 10) { |
|
clearInterval(this.refreshTimer); |
|
this.refreshTimer = setInterval(() => { |
|
if (this.playing) { |
|
this.sendFrameUpdate(); |
|
} |
|
}, 1000 / rate); |
|
} |
|
|
|
get currentAudio() { |
|
return this.playlist[this.index]; |
|
} |
|
|
|
get currentHowl() { |
|
return this.currentAudio && this.currentAudio.howl; |
|
} |
|
|
|
get playing() { |
|
return this.currentHowl ? this.currentHowl.playing() : false; |
|
} |
|
|
|
|
|
get muted() { |
|
return !!Howler._muted; |
|
} |
|
|
|
insertAudio(audio, idx) { |
|
if (this.playlist.find((i) => audio.id === i.id)) return; |
|
|
|
const audioData = { |
|
...audio, |
|
disabled: false, |
|
howl: null, |
|
}; |
|
if (idx) { |
|
this.playlist.splice(idx, 0, [audio]); |
|
} else { |
|
this.playlist.push(audioData); |
|
} |
|
this.sendPlaylistEvent(); |
|
this.sendLoadEvent(); |
|
} |
|
|
|
static array_move(arr, old_index, new_index) { |
|
|
|
if (new_index >= arr.length) { |
|
let k = new_index - arr.length + 1; |
|
while (k > 0) { |
|
k -= 1; |
|
arr.push(undefined); |
|
} |
|
} |
|
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]); |
|
return arr; |
|
} |
|
|
|
insertAudioByDirection(audio, to_audio, direction) { |
|
const originTrack = this.playlist[this.index]; |
|
const index = this.playlist.findIndex((i) => i.id === audio.id); |
|
let insertIndex = this.playlist.findIndex((i) => i.id === to_audio.id); |
|
if (index === insertIndex) { |
|
return; |
|
} |
|
if (insertIndex > index) { |
|
insertIndex -= 1; |
|
} |
|
const offset = direction === 'top' ? 0 : 1; |
|
this.playlist = Player.array_move( |
|
this.playlist, |
|
index, |
|
insertIndex + offset |
|
); |
|
const foundOriginTrackIndex = this.playlist.findIndex( |
|
(i) => i.id === originTrack.id |
|
); |
|
if (foundOriginTrackIndex >= 0) { |
|
this.index = foundOriginTrackIndex; |
|
} |
|
|
|
this.sendPlaylistEvent(); |
|
this.sendLoadEvent(); |
|
} |
|
|
|
removeAudio(idx) { |
|
if (!this.playlist[idx]) { |
|
return; |
|
} |
|
|
|
const isPlaying = this.playing; |
|
const { id: trackId } = this.currentAudio; |
|
|
|
if (isPlaying && this.playlist[idx].id === trackId) { |
|
this.pause(); |
|
} |
|
|
|
this.playlist.splice(idx, 1); |
|
const newIndex = this.playlist.findIndex((i) => i.id === trackId); |
|
if (newIndex >= 0) { |
|
this.index = newIndex; |
|
} else { |
|
|
|
if (idx >= this.playlist.length) { |
|
this.index = this.playlist.length - 1; |
|
} else { |
|
this.index = idx; |
|
} |
|
if (isPlaying) { |
|
this.play(); |
|
} |
|
} |
|
|
|
this.sendPlaylistEvent(); |
|
this.sendLoadEvent(); |
|
} |
|
|
|
appendAudioList(list) { |
|
if (!Array.isArray(list)) { |
|
return; |
|
} |
|
list.forEach((audio) => { |
|
this.insertAudio(audio); |
|
}); |
|
} |
|
|
|
clearPlaylist() { |
|
this.playlist = []; |
|
this.stopAll(); |
|
Howler.unload(); |
|
this.sendPlaylistEvent(); |
|
this.sendLoadEvent(); |
|
} |
|
|
|
stopAll() { |
|
this.playlist.forEach((i) => { |
|
if (i.howl) { |
|
i.howl.stop(); |
|
} |
|
}); |
|
} |
|
|
|
setNewPlaylist(list) { |
|
if (list.length) { |
|
|
|
this.stopAll(); |
|
Howler.unload(); |
|
|
|
this.playlist = list.map((audio) => ({ |
|
...audio, |
|
howl: null, |
|
})); |
|
|
|
this.index = 0; |
|
this.load(0); |
|
} |
|
this.sendPlaylistEvent(); |
|
} |
|
|
|
playById(id) { |
|
const idx = this.playlist.findIndex((audio) => audio.id === id); |
|
this.play(idx); |
|
} |
|
|
|
loadById(id) { |
|
const idx = this.playlist.findIndex((audio) => audio.id === id); |
|
this.load(idx); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
play(idx) { |
|
this.load(idx); |
|
|
|
const data = this.playlist[this.index]; |
|
if (!data.howl || !this._media_uri_list[data.id]) { |
|
this.retrieveMediaUrl(this.index, true); |
|
} else { |
|
this.finishLoad(this.index, true); |
|
} |
|
} |
|
|
|
retrieveMediaUrl(index, playNow) { |
|
const msg = { |
|
type: 'BG_PLAYER:RETRIEVE_URL', |
|
data: { |
|
...this.playlist[index], |
|
howl: undefined, |
|
index, |
|
playNow, |
|
}, |
|
}; |
|
|
|
MediaService.bootstrapTrack( |
|
msg.data, |
|
(bootinfo) => { |
|
msg.type = 'BG_PLAYER:RETRIEVE_URL_SUCCESS'; |
|
|
|
msg.data = { ...msg.data, ...bootinfo }; |
|
|
|
this.playlist[index].bitrate = bootinfo.bitrate; |
|
this.playlist[index].platform = bootinfo.platform; |
|
|
|
this.setMediaURI(msg.data.url, msg.data.id); |
|
this.setAudioDisabled(false, msg.data.index); |
|
this.finishLoad(msg.data.index, playNow); |
|
playerSendMessage(this.mode, msg); |
|
}, |
|
() => { |
|
msg.type = 'BG_PLAYER:RETRIEVE_URL_FAIL'; |
|
|
|
this.setAudioDisabled(true, msg.data.index); |
|
playerSendMessage(this.mode, msg); |
|
|
|
this.skip('next'); |
|
} |
|
); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
load(idx) { |
|
let index = typeof idx === 'number' ? idx : this.index; |
|
if (index < 0) return; |
|
if (!this.playlist[index]) { |
|
index = 0; |
|
} |
|
|
|
if (index !== this.index) { |
|
Howler.unload(); |
|
} |
|
this.index = index; |
|
|
|
this.sendLoadEvent(); |
|
} |
|
|
|
finishLoad(index, playNow) { |
|
const data = this.playlist[index]; |
|
|
|
|
|
|
|
const self = this; |
|
if (!data.howl) { |
|
data.howl = new Howl({ |
|
src: [self._media_uri_list[data.url || data.id]], |
|
volume: 1, |
|
mute: self.muted, |
|
html5: true, |
|
onplay() { |
|
if ('mediaSession' in navigator) { |
|
const { mediaSession } = navigator; |
|
mediaSession.playbackState = 'playing'; |
|
mediaSession.metadata = new MediaMetadata({ |
|
title: self.currentAudio.title, |
|
artist: self.currentAudio.artist, |
|
album: `Listen1 • ${( |
|
self.currentAudio.album || '<???>' |
|
).padEnd(100)}`, |
|
artwork: [ |
|
{ |
|
src: self.currentAudio.img_url, |
|
sizes: '300x300', |
|
}, |
|
], |
|
}); |
|
} |
|
self.currentAudio.disabled = false; |
|
self.playedFrom = Date.now(); |
|
self.sendPlayingEvent('Playing'); |
|
}, |
|
onload() { |
|
self.currentAudio.disabled = false; |
|
self.sendPlayingEvent('Loaded'); |
|
}, |
|
onend() { |
|
switch (self.loop_mode) { |
|
case 2: |
|
self.skip('random'); |
|
break; |
|
|
|
case 1: |
|
self.play(); |
|
break; |
|
|
|
case 0: |
|
default: |
|
self.skip('next'); |
|
break; |
|
} |
|
self.sendPlayingEvent('Ended'); |
|
}, |
|
onpause() { |
|
navigator.mediaSession.playbackState = 'paused'; |
|
self.sendPlayingEvent('Paused'); |
|
}, |
|
onstop() { |
|
self.sendPlayingEvent('Stopped'); |
|
}, |
|
onseek() {}, |
|
onvolume() {}, |
|
onloaderror(id, err) { |
|
playerSendMessage(this.mode, { |
|
type: 'BG_PLAYER:PLAY_FAILED', |
|
data: err, |
|
}); |
|
self.currentAudio.disabled = true; |
|
self.sendPlayingEvent('err'); |
|
self.currentHowl.unload(); |
|
delete self._media_uri_list[data.id]; |
|
}, |
|
onplayerror(id, err) { |
|
playerSendMessage(this.mode, { |
|
type: 'BG_PLAYER:PLAY_FAILED', |
|
data: err, |
|
}); |
|
self.currentAudio.disabled = true; |
|
self.sendPlayingEvent('err'); |
|
}, |
|
}); |
|
} |
|
|
|
if (playNow) { |
|
if (this.playing && index === this.index) { |
|
return; |
|
} |
|
this.playlist.forEach((i) => { |
|
if (i.howl && i.howl !== this.currentHowl) { |
|
i.howl.stop(); |
|
} |
|
}); |
|
this.currentHowl.play(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
pause() { |
|
if (!this.currentHowl) return; |
|
|
|
|
|
this.currentHowl.pause(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
skip(direction) { |
|
Howler.unload(); |
|
|
|
let nextIndexFn = null; |
|
if (this._loop_mode === 2 || direction === 'random') { |
|
|
|
nextIndexFn = () => Math.floor(Math.random() * this.playlist.length); |
|
} else if (direction === 'prev') { |
|
nextIndexFn = (idx) => (idx - 1) % this.playlist.length; |
|
} else if (direction === 'next') { |
|
nextIndexFn = (idx) => (idx + 1) % this.playlist.length; |
|
} |
|
this.index = nextIndexFn(this.index); |
|
|
|
let tryCount = 0; |
|
while (tryCount < this.playlist.length) { |
|
if (!this.playlist[this.index].disabled) { |
|
this.play(this.index); |
|
return; |
|
} |
|
this.index = nextIndexFn(this.index); |
|
tryCount += 1; |
|
} |
|
playerSendMessage(this.mode, { |
|
type: 'BG_PLAYER:RETRIEVE_URL_FAIL_ALL', |
|
}); |
|
this.sendLoadEvent(); |
|
} |
|
|
|
set loop_mode(input) { |
|
const LOOP_MODE = { |
|
all: 0, |
|
one: 1, |
|
shuffle: 2, |
|
}; |
|
let myMode = 0; |
|
if (typeof input === 'string') { |
|
myMode = LOOP_MODE[input]; |
|
} else { |
|
myMode = input; |
|
} |
|
if (!Object.values(LOOP_MODE).includes(myMode)) { |
|
return; |
|
} |
|
this._loop_mode = myMode; |
|
} |
|
|
|
get loop_mode() { |
|
return this._loop_mode; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
set volume(val) { |
|
|
|
if (typeof val === 'number') { |
|
Howler.volume(val); |
|
this.sendVolumeEvent(); |
|
this.sendFrameUpdate(); |
|
} |
|
} |
|
|
|
|
|
get volume() { |
|
return Howler.volume(); |
|
} |
|
|
|
adjustVolume(inc) { |
|
this.volume = inc |
|
? Math.min(this.volume + 0.1, 1) |
|
: Math.max(this.volume - 0.1, 0); |
|
this.sendVolumeEvent(); |
|
this.sendFrameUpdate(); |
|
} |
|
|
|
mute() { |
|
Howler.mute(true); |
|
playerSendMessage(this.mode, { |
|
type: 'BG_PLAYER:MUTE', |
|
data: true, |
|
}); |
|
} |
|
|
|
unmute() { |
|
Howler.mute(false); |
|
playerSendMessage(this.mode, { |
|
type: 'BG_PLAYER:MUTE', |
|
data: false, |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
seek(per) { |
|
if (!this.currentHowl) return; |
|
|
|
|
|
const audio = this.currentHowl; |
|
|
|
|
|
|
|
|
|
audio.seek(audio.duration() * per); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
seekTime(seconds) { |
|
if (!this.currentHowl) return; |
|
const audio = this.currentHowl; |
|
audio.seek(seconds); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
static formatTime(secs) { |
|
const minutes = Math.floor(secs / 60) || 0; |
|
const seconds = secs - minutes * 60 || 0; |
|
|
|
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; |
|
} |
|
|
|
setMediaURI(uri, url) { |
|
if (url) { |
|
this._media_uri_list[url] = uri; |
|
} |
|
} |
|
|
|
setAudioDisabled(disabled, idx) { |
|
if (this.playlist[idx]) { |
|
this.playlist[idx].disabled = disabled; |
|
} |
|
} |
|
|
|
async sendFrameUpdate() { |
|
const data = { |
|
id: this.currentAudio ? this.currentAudio.id : 0, |
|
duration: this.currentHowl ? this.currentHowl.duration() : 0, |
|
pos: this.currentHowl ? this.currentHowl.seek() : 0, |
|
playedFrom: this.playedFrom, |
|
playing: this.playing, |
|
}; |
|
if ('setPositionState' in navigator.mediaSession) { |
|
navigator.mediaSession.setPositionState({ |
|
duration: this.currentHowl ? this.currentHowl.duration() : 0, |
|
playbackRate: this.currentHowl ? this.currentHowl.rate() : 1, |
|
position: this.currentHowl ? this.currentHowl.seek() : 0, |
|
}); |
|
} |
|
|
|
playerSendMessage(this.mode, { |
|
type: 'BG_PLAYER:FRAME_UPDATE', |
|
data, |
|
}); |
|
} |
|
|
|
async sendPlayingEvent(reason = 'UNKNOWN') { |
|
playerSendMessage(this.mode, { |
|
type: 'BG_PLAYER:PLAY_STATE', |
|
data: { |
|
isPlaying: this.playing, |
|
reason, |
|
}, |
|
}); |
|
} |
|
|
|
async sendLoadEvent() { |
|
playerSendMessage(this.mode, { |
|
type: 'BG_PLAYER:LOAD', |
|
data: { |
|
...this.currentAudio, |
|
howl: undefined, |
|
}, |
|
}); |
|
} |
|
|
|
async sendVolumeEvent() { |
|
playerSendMessage(this.mode, { |
|
type: 'BG_PLAYER:VOLUME', |
|
data: this.volume * 100, |
|
}); |
|
} |
|
|
|
async sendPlaylistEvent() { |
|
playerSendMessage(this.mode, { |
|
type: 'BG_PLAYER:PLAYLIST', |
|
data: this.playlist.map((audio) => ({ ...audio, howl: undefined })), |
|
}); |
|
} |
|
} |
|
|
|
|
|
|
|
const threadPlayer = new Player(); |
|
threadPlayer.setRefreshRate(); |
|
window.threadPlayer = threadPlayer; |
|
|
|
if ('mediaSession' in navigator) { |
|
const { mediaSession } = navigator; |
|
mediaSession.setActionHandler('play', () => { |
|
threadPlayer.play(); |
|
}); |
|
mediaSession.setActionHandler('pause', () => { |
|
threadPlayer.pause(); |
|
}); |
|
mediaSession.setActionHandler('seekforward', (details) => { |
|
|
|
const { currentHowl } = threadPlayer; |
|
const skipTime = details.seekOffset || threadPlayer.skipTime; |
|
const newTime = Math.min( |
|
currentHowl.seek() + skipTime, |
|
currentHowl.duration() |
|
); |
|
threadPlayer.seekTime(newTime); |
|
threadPlayer.sendFrameUpdate(); |
|
}); |
|
mediaSession.setActionHandler('seekbackward', (details) => { |
|
|
|
const { currentHowl } = threadPlayer; |
|
const skipTime = details.seekOffset || threadPlayer.skipTime; |
|
const newTime = Math.max(currentHowl.seek() - skipTime, 0); |
|
threadPlayer.seekTime(newTime); |
|
threadPlayer.sendFrameUpdate(); |
|
}); |
|
mediaSession.setActionHandler('seekto', (details) => { |
|
const { seekTime } = details; |
|
threadPlayer.seekTime(seekTime); |
|
threadPlayer.sendFrameUpdate(); |
|
}); |
|
mediaSession.setActionHandler('nexttrack', () => { |
|
threadPlayer.skip('next'); |
|
threadPlayer.sendFrameUpdate(); |
|
}); |
|
mediaSession.setActionHandler('previoustrack', () => { |
|
threadPlayer.skip('prev'); |
|
threadPlayer.sendFrameUpdate(); |
|
}); |
|
} |
|
playerSendMessage(this.mode, { |
|
type: 'BG_PLAYER:READY', |
|
}); |
|
} |
|
|