2022-07-07 18:05:46 +00:00
|
|
|
|
import isMobile from 'ismobilejs'
|
2022-04-15 14:24:30 +00:00
|
|
|
|
import plyr from 'plyr'
|
2022-10-30 23:13:57 +00:00
|
|
|
|
import { watch } from 'vue'
|
2022-07-07 18:05:46 +00:00
|
|
|
|
import { shuffle, throttle } from 'lodash'
|
2022-04-24 17:58:12 +00:00
|
|
|
|
|
2022-04-15 14:24:30 +00:00
|
|
|
|
import {
|
2022-04-24 08:50:45 +00:00
|
|
|
|
commonStore,
|
|
|
|
|
preferenceStore as preferences,
|
2022-04-15 14:24:30 +00:00
|
|
|
|
queueStore,
|
|
|
|
|
recentlyPlayedStore,
|
2022-04-24 08:50:45 +00:00
|
|
|
|
songStore,
|
|
|
|
|
userStore
|
2022-04-15 14:24:30 +00:00
|
|
|
|
} from '@/stores'
|
2022-04-24 17:58:12 +00:00
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
import { arrayify, eventBus, getPlayableProp, isAudioContextSupported, isEpisode, isSong, logger } from '@/utils'
|
2024-01-01 11:40:21 +00:00
|
|
|
|
import { audioService, http, socketService, volumeManager } from '@/services'
|
2024-05-19 05:49:42 +00:00
|
|
|
|
import { useEpisodeProgressTracking } from '@/composables'
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The number of seconds before the current song ends to start preload the next one.
|
|
|
|
|
*/
|
|
|
|
|
const PRELOAD_BUFFER = 30
|
|
|
|
|
|
2022-07-25 18:23:30 +00:00
|
|
|
|
class PlaybackService {
|
2022-10-30 23:13:57 +00:00
|
|
|
|
public player!: Plyr
|
2022-07-25 18:23:30 +00:00
|
|
|
|
private repeatModes: RepeatMode[] = ['NO_REPEAT', 'REPEAT_ALL', 'REPEAT_ONE']
|
|
|
|
|
private initialized = false
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
2024-04-23 21:01:27 +00:00
|
|
|
|
public get isTranscoding () {
|
|
|
|
|
return isMobile.any && preferences.transcode_on_mobile
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The next song in the queue.
|
|
|
|
|
* If we're in REPEAT_ALL mode and there's no next song, just get the first song.
|
|
|
|
|
*/
|
|
|
|
|
public get next () {
|
|
|
|
|
if (queueStore.next) {
|
|
|
|
|
return queueStore.next
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (preferences.repeat_mode === 'REPEAT_ALL') {
|
|
|
|
|
return queueStore.first
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The previous song in the queue.
|
|
|
|
|
* If we're in REPEAT_ALL mode and there's no prev song, get the last song.
|
|
|
|
|
*/
|
|
|
|
|
public get previous () {
|
|
|
|
|
if (queueStore.previous) {
|
|
|
|
|
return queueStore.previous
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (preferences.repeat_mode === 'REPEAT_ALL') {
|
|
|
|
|
return queueStore.last
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-30 23:13:57 +00:00
|
|
|
|
public init (plyrWrapper: HTMLElement) {
|
|
|
|
|
if (this.initialized) return
|
2022-07-25 18:23:30 +00:00
|
|
|
|
|
2022-10-30 23:13:57 +00:00
|
|
|
|
this.player = plyr.setup(plyrWrapper, { controls: [] })[0]
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
|
|
|
|
this.listenToMediaEvents(this.player.media)
|
2022-10-30 23:13:57 +00:00
|
|
|
|
this.setMediaSessionActionHandlers()
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
2022-10-30 23:13:57 +00:00
|
|
|
|
watch(volumeManager.volume, volume => this.player.setVolume(volume), { immediate: true })
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
2022-10-30 23:13:57 +00:00
|
|
|
|
this.initialized = true
|
2022-07-25 18:23:30 +00:00
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
public registerPlay (playable: Playable) {
|
|
|
|
|
recentlyPlayedStore.add(playable)
|
|
|
|
|
songStore.registerPlay(playable)
|
|
|
|
|
playable.play_count_registered = true
|
2022-07-25 18:23:30 +00:00
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
public preload (playable: Playable) {
|
2022-04-15 14:24:30 +00:00
|
|
|
|
const audioElement = document.createElement('audio')
|
2024-05-19 05:49:42 +00:00
|
|
|
|
audioElement.setAttribute('src', songStore.getSourceUrl(playable))
|
2022-04-15 14:24:30 +00:00
|
|
|
|
audioElement.setAttribute('preload', 'auto')
|
|
|
|
|
audioElement.load()
|
2024-05-19 05:49:42 +00:00
|
|
|
|
playable.preloaded = true
|
2022-07-25 18:23:30 +00:00
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Play a song. Because
|
|
|
|
|
*
|
|
|
|
|
* So many adventures couldn't happen today,
|
|
|
|
|
* So many songs we forgot to play
|
|
|
|
|
* So many dreams swinging out of the blue
|
|
|
|
|
* We'll let them come true
|
|
|
|
|
*/
|
2024-05-19 05:49:42 +00:00
|
|
|
|
public async play (playable: Playable, position = 0) {
|
|
|
|
|
if (isEpisode(playable)) {
|
|
|
|
|
useEpisodeProgressTracking().trackEpisode(playable)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
queueStore.queueIfNotQueued(playable)
|
|
|
|
|
|
2022-09-15 09:07:25 +00:00
|
|
|
|
// If for any reason (most likely a bug), the requested song has been deleted, just attempt the next song.
|
2024-05-19 05:49:42 +00:00
|
|
|
|
if (isSong(playable) && playable.deleted) {
|
|
|
|
|
logger.warn('Attempted to play a deleted song', playable)
|
2022-09-15 09:07:25 +00:00
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
if (this.next && this.next.id !== playable.id) {
|
2022-09-15 09:07:25 +00:00
|
|
|
|
await this.playNext()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-15 14:24:30 +00:00
|
|
|
|
if (queueStore.current) {
|
2022-06-10 10:47:46 +00:00
|
|
|
|
queueStore.current.playback_state = 'Stopped'
|
2022-04-15 14:24:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
playable.playback_state = 'Playing'
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
await this.setNowPlayingMeta(playable)
|
2024-01-01 11:40:21 +00:00
|
|
|
|
|
2022-04-15 14:24:30 +00:00
|
|
|
|
// Manually set the `src` attribute of the audio to prevent plyr from resetting
|
|
|
|
|
// the audio media object and cause our equalizer to malfunction.
|
2024-05-19 05:49:42 +00:00
|
|
|
|
this.player.media.src = songStore.getSourceUrl(playable)
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
if (position === 0) {
|
|
|
|
|
// We'll just "restart" playing the song, which will handle notification, scrobbling etc.
|
|
|
|
|
// Fixes #898
|
|
|
|
|
await this.restart()
|
|
|
|
|
} else {
|
|
|
|
|
this.player.seek(position)
|
|
|
|
|
await this.resume()
|
|
|
|
|
}
|
2024-06-07 12:53:24 +00:00
|
|
|
|
|
|
|
|
|
this.setMediaSessionActionHandlers()
|
2024-01-01 11:40:21 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
public showNotification (playable: Playable) {
|
|
|
|
|
if (!isSong(playable) && !isEpisode(playable)) {
|
|
|
|
|
throw 'Invalid playable type.'
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-23 22:50:50 +00:00
|
|
|
|
if (preferences.show_now_playing_notification) {
|
2022-10-31 14:55:24 +00:00
|
|
|
|
try {
|
2024-05-19 05:49:42 +00:00
|
|
|
|
const notification = new window.Notification(`♫ ${playable.title}`, {
|
|
|
|
|
icon: getPlayableProp(playable, 'album_cover', 'episode_image'),
|
|
|
|
|
body: isSong(playable)
|
|
|
|
|
? `${playable.album_name} – ${playable.artist_name}`
|
|
|
|
|
: playable.title
|
2022-10-31 14:55:24 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
notification.onclick = () => window.focus()
|
|
|
|
|
|
|
|
|
|
window.setTimeout(() => notification.close(), 5000)
|
2024-04-23 11:24:29 +00:00
|
|
|
|
} catch (error: unknown) {
|
2022-10-31 14:55:24 +00:00
|
|
|
|
// Notification fails.
|
|
|
|
|
// @link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification
|
2024-04-23 11:24:29 +00:00
|
|
|
|
logger.error(error)
|
2022-10-31 14:55:24 +00:00
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-10-31 14:55:24 +00:00
|
|
|
|
if (!navigator.mediaSession) return
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
2022-04-30 10:36:35 +00:00
|
|
|
|
navigator.mediaSession.metadata = new MediaMetadata({
|
2024-05-19 05:49:42 +00:00
|
|
|
|
title: playable.title,
|
|
|
|
|
artist: getPlayableProp(playable, 'artist_name', 'podcast_author'),
|
|
|
|
|
album: getPlayableProp(playable, 'album_name', 'podcast_title'),
|
2022-10-31 14:55:24 +00:00
|
|
|
|
artwork: [48, 64, 96, 128, 192, 256, 384, 512].map(d => ({
|
2024-05-19 05:49:42 +00:00
|
|
|
|
src: getPlayableProp(playable, 'album_cover', 'episode_image'),
|
2022-10-31 14:55:24 +00:00
|
|
|
|
sizes: `${d}x${d}`,
|
|
|
|
|
type: 'image/png'
|
|
|
|
|
}))
|
2022-04-30 10:36:35 +00:00
|
|
|
|
})
|
2022-07-25 18:23:30 +00:00
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
2024-01-01 11:40:21 +00:00
|
|
|
|
public async restart () {
|
2024-05-19 05:49:42 +00:00
|
|
|
|
const playable = queueStore.current!
|
2024-01-01 11:40:21 +00:00
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
this.recordStartTime(playable)
|
|
|
|
|
this.broadcastSong(playable)
|
2024-01-01 11:40:21 +00:00
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
http.silently.put('queue/playback-status', {
|
2024-05-19 05:49:42 +00:00
|
|
|
|
song: playable.id,
|
2024-01-01 11:40:21 +00:00
|
|
|
|
position: 0
|
|
|
|
|
})
|
2024-04-23 11:24:29 +00:00
|
|
|
|
} catch (error: unknown) {
|
2024-01-13 17:57:24 +00:00
|
|
|
|
logger.error(error)
|
2024-01-01 11:40:21 +00:00
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
2022-07-25 18:23:30 +00:00
|
|
|
|
this.player.restart()
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
|
|
|
|
try {
|
2022-07-25 18:23:30 +00:00
|
|
|
|
await this.player.media.play()
|
2022-10-31 14:55:24 +00:00
|
|
|
|
navigator.mediaSession && (navigator.mediaSession.playbackState = 'playing')
|
2024-05-19 05:49:42 +00:00
|
|
|
|
this.showNotification(playable)
|
2024-04-23 11:24:29 +00:00
|
|
|
|
} catch (error: unknown) {
|
2022-04-15 14:24:30 +00:00
|
|
|
|
// convert this into a warning, as an error will cause Cypress to fail the tests entirely
|
2022-07-20 08:00:02 +00:00
|
|
|
|
logger.warn(error)
|
2022-04-15 14:24:30 +00:00
|
|
|
|
}
|
2022-07-25 18:23:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-03-18 17:56:17 +00:00
|
|
|
|
public rotateRepeatMode () {
|
2024-01-23 22:50:50 +00:00
|
|
|
|
let index = this.repeatModes.indexOf(preferences.repeat_mode) + 1
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
|
|
|
|
if (index >= this.repeatModes.length) {
|
|
|
|
|
index = 0
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-23 22:50:50 +00:00
|
|
|
|
preferences.repeat_mode = this.repeatModes[index]
|
2022-07-25 18:23:30 +00:00
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Play the prev song in the queue, if one is found.
|
|
|
|
|
* If the prev song is not found and the current mode is NO_REPEAT, we stop completely.
|
|
|
|
|
*/
|
2022-07-25 18:23:30 +00:00
|
|
|
|
public async playPrev () {
|
2022-10-31 14:55:24 +00:00
|
|
|
|
// If the song's duration is greater than 5 seconds, and we've passed 5 seconds into it,
|
2022-04-15 14:24:30 +00:00
|
|
|
|
// restart playing instead.
|
2022-07-25 18:23:30 +00:00
|
|
|
|
if (this.player.media.currentTime > 5 && queueStore.current!.length > 5) {
|
|
|
|
|
this.player.restart()
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-23 22:50:50 +00:00
|
|
|
|
if (!this.previous && preferences.repeat_mode === 'NO_REPEAT') {
|
2022-06-10 10:47:46 +00:00
|
|
|
|
await this.stop()
|
2022-04-15 14:24:30 +00:00
|
|
|
|
} else {
|
2022-06-10 10:47:46 +00:00
|
|
|
|
this.previous && await this.play(this.previous)
|
2022-04-15 14:24:30 +00:00
|
|
|
|
}
|
2022-07-25 18:23:30 +00:00
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Play the next song in the queue, if one is found.
|
|
|
|
|
* If the next song is not found and the current mode is NO_REPEAT, we stop completely.
|
|
|
|
|
*/
|
2022-07-25 18:23:30 +00:00
|
|
|
|
public async playNext () {
|
2024-01-23 22:50:50 +00:00
|
|
|
|
if (!this.next && preferences.repeat_mode === 'NO_REPEAT') {
|
2022-06-10 10:47:46 +00:00
|
|
|
|
await this.stop() // Nothing lasts forever, even cold November rain.
|
2022-04-15 14:24:30 +00:00
|
|
|
|
} else {
|
2022-06-10 10:47:46 +00:00
|
|
|
|
this.next && await this.play(this.next)
|
2022-04-15 14:24:30 +00:00
|
|
|
|
}
|
2022-07-25 18:23:30 +00:00
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
2022-07-25 18:23:30 +00:00
|
|
|
|
public async stop () {
|
2022-05-07 08:12:16 +00:00
|
|
|
|
document.title = 'Koel'
|
2022-07-25 18:23:30 +00:00
|
|
|
|
this.player.pause()
|
|
|
|
|
this.player.seek(0)
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
2022-10-31 14:55:24 +00:00
|
|
|
|
queueStore.current && (queueStore.current.playback_state = 'Stopped')
|
|
|
|
|
navigator.mediaSession && (navigator.mediaSession.playbackState = 'none')
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
2022-04-24 08:50:45 +00:00
|
|
|
|
socketService.broadcast('SOCKET_PLAYBACK_STOPPED')
|
2022-07-25 18:23:30 +00:00
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
2022-07-25 18:23:30 +00:00
|
|
|
|
public pause () {
|
|
|
|
|
this.player.pause()
|
2022-10-31 14:55:24 +00:00
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
|
queueStore.current!.playback_state = 'Paused'
|
2022-10-31 14:55:24 +00:00
|
|
|
|
navigator.mediaSession && (navigator.mediaSession.playbackState = 'paused')
|
|
|
|
|
|
2022-07-24 10:53:49 +00:00
|
|
|
|
socketService.broadcast('SOCKET_SONG', queueStore.current)
|
2022-07-25 18:23:30 +00:00
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
2022-07-25 18:23:30 +00:00
|
|
|
|
public async resume () {
|
2024-01-01 11:40:21 +00:00
|
|
|
|
const song = queueStore.current!
|
|
|
|
|
|
|
|
|
|
if (!this.player.media.src) {
|
|
|
|
|
// on first load when the queue is loaded from saved state, the player's src is empty
|
|
|
|
|
// we need to properly set it as well as any kind of playback metadata
|
|
|
|
|
this.player.media.src = songStore.getSourceUrl(song)
|
|
|
|
|
this.player.seek(commonStore.state.queue_state.playback_position);
|
|
|
|
|
|
|
|
|
|
await this.setNowPlayingMeta(queueStore.current!)
|
|
|
|
|
this.recordStartTime(song)
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-15 14:24:30 +00:00
|
|
|
|
try {
|
2022-07-25 18:23:30 +00:00
|
|
|
|
await this.player.media.play()
|
2024-04-23 11:24:29 +00:00
|
|
|
|
} catch (error: unknown) {
|
2022-07-20 08:00:02 +00:00
|
|
|
|
logger.error(error)
|
2022-04-15 14:24:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
|
queueStore.current!.playback_state = 'Playing'
|
2022-10-31 14:55:24 +00:00
|
|
|
|
navigator.mediaSession && (navigator.mediaSession.playbackState = 'playing')
|
|
|
|
|
|
2024-01-01 11:40:21 +00:00
|
|
|
|
this.broadcastSong(song)
|
2022-07-25 18:23:30 +00:00
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
2022-07-25 18:23:30 +00:00
|
|
|
|
public async toggle () {
|
2022-04-15 14:24:30 +00:00
|
|
|
|
if (!queueStore.current) {
|
|
|
|
|
await this.playFirstInQueue()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
|
if (queueStore.current.playback_state !== 'Playing') {
|
2022-04-15 14:24:30 +00:00
|
|
|
|
await this.resume()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.pause()
|
2022-07-25 18:23:30 +00:00
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
2024-03-18 17:51:51 +00:00
|
|
|
|
public seekBy (seconds: number) {
|
|
|
|
|
if (this.player.media.duration) {
|
|
|
|
|
this.player.media.currentTime += seconds
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-15 14:24:30 +00:00
|
|
|
|
/**
|
2024-05-19 05:49:42 +00:00
|
|
|
|
* Queue up playables (replace them into the queue) and start playing right away.
|
2022-04-15 14:24:30 +00:00
|
|
|
|
*/
|
2024-05-19 05:49:42 +00:00
|
|
|
|
public async queueAndPlay (playables: MaybeArray<Playable>, shuffled = false) {
|
|
|
|
|
playables = arrayify(playables)
|
2022-08-01 11:40:52 +00:00
|
|
|
|
|
2022-04-15 14:24:30 +00:00
|
|
|
|
if (shuffled) {
|
2024-05-19 05:49:42 +00:00
|
|
|
|
playables = shuffle(playables)
|
2022-04-15 14:24:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
|
await this.stop()
|
2024-05-19 05:49:42 +00:00
|
|
|
|
queueStore.replaceQueueWith(playables)
|
2022-04-15 14:24:30 +00:00
|
|
|
|
await this.play(queueStore.first)
|
2022-07-25 18:23:30 +00:00
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
2022-07-25 18:23:30 +00:00
|
|
|
|
public async playFirstInQueue () {
|
2022-06-10 10:47:46 +00:00
|
|
|
|
queueStore.all.length && await this.play(queueStore.first)
|
2022-04-15 14:24:30 +00:00
|
|
|
|
}
|
2022-10-08 10:54:25 +00:00
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
private async setNowPlayingMeta (playable: Playable) {
|
|
|
|
|
document.title = `${playable.title} ♫ Koel`
|
|
|
|
|
this.player.media.setAttribute(
|
|
|
|
|
'title',
|
|
|
|
|
isSong(playable) ? `${playable.artist_name} - ${playable.title}` : playable.title
|
|
|
|
|
)
|
2024-04-23 21:01:27 +00:00
|
|
|
|
|
|
|
|
|
if (isAudioContextSupported) {
|
|
|
|
|
await audioService.context.resume()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Record the UNIX timestamp the song starts playing, for scrobbling purpose
|
2024-05-19 05:49:42 +00:00
|
|
|
|
private recordStartTime (song: Playable) {
|
|
|
|
|
if (!isSong(song)) return
|
|
|
|
|
|
2024-04-23 21:01:27 +00:00
|
|
|
|
song.play_start_time = Math.floor(Date.now() / 1000)
|
|
|
|
|
song.play_count_registered = false
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
private broadcastSong (playable: Playable) {
|
|
|
|
|
socketService.broadcast('SOCKET_SONG', playable)
|
2024-04-23 21:01:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-10-08 10:54:25 +00:00
|
|
|
|
private setMediaSessionActionHandlers () {
|
2022-10-31 14:55:24 +00:00
|
|
|
|
if (!navigator.mediaSession) return
|
2022-10-08 10:54:25 +00:00
|
|
|
|
|
|
|
|
|
navigator.mediaSession.setActionHandler('play', () => this.resume())
|
|
|
|
|
navigator.mediaSession.setActionHandler('pause', () => this.pause())
|
2022-10-31 14:55:24 +00:00
|
|
|
|
navigator.mediaSession.setActionHandler('stop', () => this.stop())
|
2022-10-08 10:54:25 +00:00
|
|
|
|
navigator.mediaSession.setActionHandler('previoustrack', () => this.playPrev())
|
|
|
|
|
navigator.mediaSession.setActionHandler('nexttrack', () => this.playNext())
|
2022-10-31 14:55:24 +00:00
|
|
|
|
|
2024-07-08 21:30:42 +00:00
|
|
|
|
if (!isMobile.apple) {
|
|
|
|
|
navigator.mediaSession.setActionHandler('seekbackward', details => {
|
|
|
|
|
this.player.media.currentTime -= (details.seekOffset || 10)
|
|
|
|
|
})
|
2022-10-31 14:55:24 +00:00
|
|
|
|
|
2024-07-08 21:30:42 +00:00
|
|
|
|
navigator.mediaSession.setActionHandler('seekforward', details => {
|
|
|
|
|
this.player.media.currentTime += (details.seekOffset || 10)
|
|
|
|
|
})
|
|
|
|
|
}
|
2022-10-31 14:55:24 +00:00
|
|
|
|
|
|
|
|
|
navigator.mediaSession.setActionHandler('seekto', details => {
|
|
|
|
|
if (details.fastSeek && 'fastSeek' in this.player.media) {
|
|
|
|
|
this.player.media.fastSeek(details.seekTime || 0)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.player.media.currentTime = details.seekTime || 0
|
|
|
|
|
})
|
2022-10-08 10:54:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-10-30 23:13:57 +00:00
|
|
|
|
private listenToMediaEvents (media: HTMLMediaElement) {
|
|
|
|
|
media.addEventListener('error', () => this.playNext(), true)
|
2022-10-08 10:54:25 +00:00
|
|
|
|
|
2022-10-30 23:13:57 +00:00
|
|
|
|
media.addEventListener('ended', () => {
|
2024-05-19 05:49:42 +00:00
|
|
|
|
if (
|
|
|
|
|
isSong(queueStore.current!)
|
|
|
|
|
&& commonStore.state.uses_last_fm
|
|
|
|
|
&& userStore.current.preferences!.lastfm_session_key
|
|
|
|
|
) {
|
2022-10-08 10:54:25 +00:00
|
|
|
|
songStore.scrobble(queueStore.current!)
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-23 22:50:50 +00:00
|
|
|
|
preferences.repeat_mode === 'REPEAT_ONE' ? this.restart() : this.playNext()
|
2022-10-08 10:54:25 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
let timeUpdateHandler = () => {
|
|
|
|
|
const currentSong = queueStore.current
|
|
|
|
|
|
|
|
|
|
if (!currentSong) return
|
|
|
|
|
|
|
|
|
|
if (!currentSong.play_count_registered && !this.isTranscoding) {
|
|
|
|
|
// if we've passed 25% of the song, it's safe to say the song has been "played".
|
|
|
|
|
// Refer to https://github.com/koel/koel/issues/1087
|
2022-10-30 23:13:57 +00:00
|
|
|
|
if (!media.duration || media.currentTime * 4 >= media.duration) {
|
2022-10-08 10:54:25 +00:00
|
|
|
|
this.registerPlay(currentSong)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-01 11:40:21 +00:00
|
|
|
|
if (Math.ceil(media.currentTime) % 5 === 0) {
|
2024-05-19 05:49:42 +00:00
|
|
|
|
// every 5 seconds, we save the current playback position to the server
|
2024-01-01 11:40:21 +00:00
|
|
|
|
try {
|
|
|
|
|
http.silently.put('queue/playback-status', {
|
|
|
|
|
song: currentSong.id,
|
|
|
|
|
position: Math.ceil(media.currentTime)
|
|
|
|
|
})
|
2024-04-23 11:24:29 +00:00
|
|
|
|
} catch (error: unknown) {
|
2024-01-13 17:57:24 +00:00
|
|
|
|
logger.error(error)
|
2024-01-01 11:40:21 +00:00
|
|
|
|
}
|
2024-05-19 05:49:42 +00:00
|
|
|
|
|
|
|
|
|
// if the current song is an episode, we emit an event to update the progress on the client side as well
|
|
|
|
|
if (isEpisode(currentSong)) {
|
|
|
|
|
eventBus.emit('EPISODE_PROGRESS_UPDATED', currentSong, Math.ceil(media.currentTime))
|
|
|
|
|
}
|
2024-01-01 11:40:21 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-10-08 10:54:25 +00:00
|
|
|
|
const nextSong = queueStore.next
|
|
|
|
|
|
|
|
|
|
if (!nextSong || nextSong.preloaded || this.isTranscoding) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-30 23:13:57 +00:00
|
|
|
|
if (media.duration && media.currentTime + PRELOAD_BUFFER > media.duration) {
|
2022-10-08 10:54:25 +00:00
|
|
|
|
this.preload(nextSong)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
|
|
|
timeUpdateHandler = throttle(timeUpdateHandler, 1000)
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-30 23:13:57 +00:00
|
|
|
|
media.addEventListener('timeupdate', timeUpdateHandler)
|
2022-10-08 10:54:25 +00:00
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
}
|
2022-07-25 18:23:30 +00:00
|
|
|
|
|
|
|
|
|
export const playbackService = new PlaybackService()
|