2022-10-23 07:41:38 +00:00
import type { QueueTrack , QueueTrackSource } from '~/composables/audio/queue'
2022-11-28 23:37:06 +00:00
import type { Track , Upload } from '~/types'
2022-10-23 07:41:38 +00:00
import type { Sound } from '~/api/player'
2022-10-07 15:22:22 +00:00
2022-10-28 14:31:50 +00:00
import { createGlobalState , syncRef , useTimeoutFn , whenever } from '@vueuse/core'
2023-01-28 20:23:46 +00:00
import { computed , ref , watchEffect } from 'vue'
2022-10-07 15:22:22 +00:00
2022-10-23 07:41:38 +00:00
import { connectAudioSource } from '~/composables/audio/audio-api'
2022-10-28 07:34:24 +00:00
import { usePlayer } from '~/composables/audio/player'
2022-10-28 12:29:58 +00:00
import { useQueue } from '~/composables/audio/queue'
import { soundImplementation } from '~/api/player'
2022-10-26 22:47:53 +00:00
2022-10-28 23:11:05 +00:00
import useLRUCache from '~/composables/data/useLRUCache'
2023-09-01 12:09:58 +00:00
import useLogger from '~/composables/useLogger'
2022-10-07 15:22:22 +00:00
import store from '~/store'
2022-11-28 23:37:06 +00:00
import axios from 'axios'
2022-10-07 15:22:22 +00:00
const ALLOWED_PLAY_TYPES : ( CanPlayTypeResult | undefined ) [ ] = [ 'maybe' , 'probably' ]
const AUDIO_ELEMENT = document . createElement ( 'audio' )
2023-09-01 12:09:58 +00:00
const logger = useLogger ( )
2022-10-07 15:22:22 +00:00
const soundPromises = new Map < number , Promise < Sound > > ( )
2023-01-29 18:52:58 +00:00
const soundCache = useLRUCache < number , Sound > ( {
2023-01-31 21:31:38 +00:00
max : 3 ,
2023-01-29 18:52:58 +00:00
dispose : ( sound ) = > sound . dispose ( )
} )
2022-10-07 15:22:22 +00:00
2022-11-28 23:37:06 +00:00
export const fetchTrackSources = async ( id : number ) : Promise < QueueTrackSource [ ] > = > {
const { uploads } = await axios . get ( ` tracks/ ${ id } / ` )
. then ( response = > response . data as Track , ( ) = > ( { uploads : [ ] as Upload [ ] } ) )
return uploads . map ( upload = > ( {
uuid : upload.uuid ,
duration : upload.duration ,
mimetype : upload.mimetype ,
bitrate : upload.bitrate ,
url : store.getters [ 'instance/absoluteUrl' ] ( upload . listen_url )
} ) )
}
const getTrackSources = async ( track : QueueTrack ) : Promise < QueueTrackSource [ ] > = > {
2022-10-29 06:09:07 +00:00
const token = store . state . auth . authenticated && store . state . auth . scopedTokens . listen
const appendToken = ( url : string ) = > {
if ( token ) {
const newUrl = new URL ( url )
newUrl . searchParams . set ( 'token' , token )
return newUrl . toString ( )
}
return url
}
2022-11-28 23:37:06 +00:00
if ( track . sources . length === 0 ) {
track . sources = await fetchTrackSources ( track . id )
}
2022-10-23 07:41:38 +00:00
const sources : QueueTrackSource [ ] = track . sources
. map ( ( source ) = > ( {
. . . source ,
2022-10-29 06:09:07 +00:00
url : appendToken ( store . getters [ 'instance/absoluteUrl' ] ( source . url ) )
2022-10-07 15:22:22 +00:00
} ) )
// NOTE: Add a transcoded MP3 src at the end for browsers
// that do not support other codecs to be able to play it :)
2022-10-23 07:41:38 +00:00
if ( sources . length > 0 ) {
const original = sources [ 0 ]
const url = new URL ( original . url )
2022-10-07 15:22:22 +00:00
url . searchParams . set ( 'to' , 'mp3' )
2022-10-23 07:41:38 +00:00
const bitrate = Math . min ( 320000 , original . bitrate ? ? Infinity )
sources . push ( { uuid : 'transcoded' , mimetype : 'audio/mpeg' , url : url.toString ( ) , bitrate } )
2022-10-07 15:22:22 +00:00
}
2022-10-23 07:41:38 +00:00
return sources
2022-10-28 20:04:02 +00:00
// NOTE: Filter out repeating and unplayable media types
. filter ( ( { mimetype , bitrate } , index , array ) = > array . findIndex ( ( upload ) = > upload . mimetype + upload . bitrate === mimetype + bitrate ) === index )
. filter ( ( { mimetype } ) = > ALLOWED_PLAY_TYPES . includes ( AUDIO_ELEMENT . canPlayType ( ` ${ mimetype } ` ) ) )
2022-10-07 15:22:22 +00:00
}
2022-10-28 07:34:24 +00:00
// Use Tracks
export const useTracks = createGlobalState ( ( ) = > {
const createSound = async ( track : QueueTrack ) : Promise < Sound > = > {
if ( soundCache . has ( track . id ) ) {
return soundCache . get ( track . id ) as Sound
}
if ( soundPromises . has ( track . id ) ) {
return soundPromises . get ( track . id ) as Promise < Sound >
}
const createSoundPromise = async ( ) = > {
2022-11-28 23:37:06 +00:00
const sources = await getTrackSources ( track )
2022-10-28 16:55:57 +00:00
const { playNext } = useQueue ( )
2022-10-28 07:34:24 +00:00
const SoundImplementation = soundImplementation . value
const sound = new SoundImplementation ( sources )
2022-10-28 20:04:02 +00:00
2022-10-28 07:34:24 +00:00
sound . onSoundEnd ( ( ) = > {
2023-09-01 12:09:58 +00:00
logger . log ( 'TRACK ENDED, PLAYING NEXT' )
2022-10-28 07:34:24 +00:00
// NOTE: We push it to the end of the job queue
2022-10-28 16:55:57 +00:00
setTimeout ( ( ) = > playNext ( ) , 0 )
2022-10-28 07:34:24 +00:00
} )
2022-10-28 20:04:02 +00:00
2023-06-12 13:55:57 +00:00
// NOTE: When the sound is disposed, we need to delete it from the cache (#2157)
whenever ( sound . isDisposed , ( ) = > {
soundCache . delete ( track . id )
} )
2023-03-09 19:04:29 +00:00
// NOTE: Bump current track to ensure that it lives despite enqueueing 3 tracks as next track:
//
// In every queue we have 3 tracks that are cached, in the order, they're being played:
//
// A B C
// ^ ^ ^______ C is the next track from the queue that has been preloaded in the 'Preload next track' code section
// \ \________ B is the currently played track
// \__________ A is the previous track
//
2023-03-09 19:06:50 +00:00
// Now, let's make an assumption that caching next tracks is more valuable than caching previous tracks.
2023-03-09 19:04:29 +00:00
// To prevent track B from being disposed from the cache after enqueueing D and E tracks as 'next track' twice, we can fetch the track from the cache and bump its counter
// The cache state would be as follows:
//
// A B C --(user enqueues D as next track)-> C B D --(user enqueues E as next track)-> D B E
//
// Note that the queue would be changed as follows:
//
// A B C -> A B D C -> A B E D C
//
// This means that the currently playing track (B) is never removed from the cache (and isn't disposed prematurely) during its playback.
// However, we end up in a situation where previous track isn't cached anymore but two next tracks are.
// That implies that when user changes to the previous track ( onlybefore track B ends), a new sound instance would be created,
// which means that there might be some network requests before playback.
if ( currentTrack . value ) {
soundCache . get ( currentTrack . value . id )
}
2023-03-13 12:10:38 +00:00
// Add track to the sound cache and remove from the promise cache
2022-10-28 07:34:24 +00:00
soundCache . set ( track . id , sound )
soundPromises . delete ( track . id )
2023-03-13 12:10:38 +00:00
2022-10-28 07:34:24 +00:00
return sound
}
2023-09-01 12:09:58 +00:00
logger . log ( 'NO TRACK IN CACHE, CREATING' , track )
2022-10-28 07:34:24 +00:00
const soundPromise = createSoundPromise ( )
soundPromises . set ( track . id , soundPromise )
return soundPromise
2022-10-07 15:22:22 +00:00
}
2023-01-28 23:04:09 +00:00
// Skip when errored
const { start : soundUnplayable , stop : abortSoundUnplayableTimeout } = useTimeoutFn ( ( ) = > {
2023-03-07 21:25:44 +00:00
const { isPlaying , looping , LoopingMode , pauseReason , PauseReason } = usePlayer ( )
2023-01-28 23:04:09 +00:00
const { playNext } = useQueue ( )
if ( looping . value !== LoopingMode . LoopTrack ) {
return playNext ( )
}
isPlaying . value = false
2023-03-07 21:25:44 +00:00
pauseReason . value = PauseReason . Errored
2023-01-28 23:04:09 +00:00
} , 3000 , { immediate : false } )
2022-10-28 14:31:50 +00:00
// Preload next track
2023-01-29 11:10:02 +00:00
const { start : preload , stop : abortPreload } = useTimeoutFn ( async ( track : QueueTrack ) = > {
const sound = await createSound ( track )
2022-10-28 14:31:50 +00:00
await sound . preload ( )
} , 100 , { immediate : false } )
2022-10-28 07:34:24 +00:00
// Create track from queue
const createTrack = async ( index : number ) = > {
2023-01-28 23:04:09 +00:00
abortSoundUnplayableTimeout ( )
const { queue , currentIndex } = useQueue ( )
2022-10-28 07:34:24 +00:00
if ( queue . value . length <= index || index === - 1 ) return
2023-09-01 12:09:58 +00:00
logger . log ( 'LOADING TRACK' , index )
2022-10-28 07:34:24 +00:00
const track = queue . value [ index ]
const sound = await createSound ( track )
2022-10-28 20:04:02 +00:00
if ( ! sound . playable ) {
2023-01-28 23:04:09 +00:00
soundUnplayable ( )
2022-10-28 20:04:02 +00:00
return
}
2023-09-01 12:09:58 +00:00
logger . log ( 'CONNECTING NODE' , sound )
2022-10-28 07:34:24 +00:00
sound . audioNode . disconnect ( )
connectAudioSource ( sound . audioNode )
const { isPlaying } = usePlayer ( )
if ( isPlaying . value && index === currentIndex . value ) {
await sound . play ( )
}
2022-10-07 15:22:22 +00:00
}
2023-01-29 18:52:58 +00:00
2022-10-28 07:34:24 +00:00
const currentTrack = ref < QueueTrack > ( )
2022-10-07 15:22:22 +00:00
2022-10-28 07:34:24 +00:00
// NOTE: We want to have it called only once, hence we're using createGlobalState
const initialize = createGlobalState ( ( ) = > {
2023-01-28 20:23:46 +00:00
const { currentIndex , currentTrack : track , queue , hasNext } = useQueue ( )
2022-10-28 12:29:58 +00:00
2022-10-28 14:31:50 +00:00
whenever ( track , ( ) = > {
createTrack ( currentIndex . value )
} , { immediate : true } )
2022-12-12 11:10:31 +00:00
2023-01-28 20:23:46 +00:00
let lastTrack : QueueTrack
watchEffect ( async ( ) = > {
2023-01-29 09:41:59 +00:00
abortPreload ( )
2023-01-28 20:23:46 +00:00
if ( ! hasNext . value ) return
const nextTrack = queue . value [ currentIndex . value + 1 ]
2023-01-29 11:10:02 +00:00
if ( ! nextTrack || lastTrack === nextTrack ) return
2023-01-28 20:23:46 +00:00
lastTrack = nextTrack
// NOTE: Preload next track
2023-01-29 11:10:02 +00:00
preload ( nextTrack )
2023-01-28 20:23:46 +00:00
} )
2022-10-28 10:40:55 +00:00
syncRef ( track , currentTrack , {
direction : 'ltr'
} )
2022-10-28 07:34:24 +00:00
} )
2022-10-07 15:22:22 +00:00
2022-10-28 07:34:24 +00:00
const currentSound = computed ( ( ) = > soundCache . get ( currentTrack . value ? . id ? ? - 1 ) )
2022-10-07 15:22:22 +00:00
2022-10-28 07:34:24 +00:00
return {
initialize ,
createSound ,
createTrack ,
currentSound
2022-10-07 15:22:22 +00:00
}
2022-10-28 07:34:24 +00:00
} )