queue actions for player so they don't overlap

This commit is contained in:
austinried 2021-07-06 18:00:12 +09:00
parent eb4199de37
commit b2d6840901
4 changed files with 123 additions and 89 deletions

View File

@ -2,8 +2,14 @@ import { useAppState } from '@react-native-community/hooks'
import { useAtomValue, useUpdateAtom } from 'jotai/utils' import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { View } from 'react-native' import { View } from 'react-native'
import TrackPlayer, { Event, useTrackPlayerEvents } from 'react-native-track-player' import TrackPlayer, { Event, State, useTrackPlayerEvents } from 'react-native-track-player'
import { currentTrackAtom, getQueue, getTrack, playerStateAtom, queueWriteAtom } from '../state/trackplayer' import {
currentTrackAtom,
playerStateAtom,
queueWriteAtom,
useRefreshCurrentTrack,
useRefreshQueue,
} from '../state/trackplayer'
const AppActiveResponder: React.FC<{ const AppActiveResponder: React.FC<{
update: () => void update: () => void
@ -32,23 +38,16 @@ const TrackPlayerEventResponder: React.FC<{
const CurrentTrackState = () => { const CurrentTrackState = () => {
const setCurrentTrack = useUpdateAtom(currentTrackAtom) const setCurrentTrack = useUpdateAtom(currentTrackAtom)
const refreshCurrentTrack = useRefreshCurrentTrack()
const update = async (payload?: Payload) => { const update = async (payload?: Payload) => {
if (payload?.type === Event.PlaybackQueueEnded && 'track' in payload) { const queueEnded = payload?.type === Event.PlaybackQueueEnded && 'track' in payload
const remoteStop = payload?.type === Event.RemoteStop
if (queueEnded || remoteStop) {
setCurrentTrack(undefined) setCurrentTrack(undefined)
return return
} }
await refreshCurrentTrack()
const index = await TrackPlayer.getCurrentTrack()
if (index !== null && index >= 0) {
const track = await getTrack(index)
if (track !== null) {
setCurrentTrack(track)
return
}
}
setCurrentTrack(undefined)
} }
return ( return (
@ -70,31 +69,36 @@ const PlayerState = () => {
const setPlayerState = useUpdateAtom(playerStateAtom) const setPlayerState = useUpdateAtom(playerStateAtom)
const update = async (payload?: Payload) => { const update = async (payload?: Payload) => {
if (payload?.type === Event.RemoteStop) {
setPlayerState(State.None)
return
}
setPlayerState(payload?.state || (await TrackPlayer.getState())) setPlayerState(payload?.state || (await TrackPlayer.getState()))
} }
return <TrackPlayerEventResponder events={[Event.PlaybackState]} update={update} /> return <TrackPlayerEventResponder events={[Event.PlaybackState, Event.RemoteStop]} update={update} />
} }
const QueueState = () => { const QueueState = () => {
const setQueue = useUpdateAtom(queueWriteAtom) const setQueue = useUpdateAtom(queueWriteAtom)
const refreshQueue = useRefreshQueue()
const update = async (payload?: Payload) => { const update = async (payload?: Payload) => {
if (payload) { if (payload) {
setQueue([]) setQueue([])
return return
} }
setQueue(await getQueue()) await refreshQueue()
} }
return <TrackPlayerEventResponder events={[Event.RemoteStop]} update={update} /> return <TrackPlayerEventResponder events={[Event.RemoteStop]} update={update} />
} }
const Debug = () => { const Debug = () => {
const value = useAtomValue(queueWriteAtom) const value = useAtomValue(currentTrackAtom)
useEffect(() => { useEffect(() => {
console.log(value.map(t => t.title)) // ToastAndroid.show(value?.title || 'undefined', 1)
}, [value]) }, [value])
return <></> return <></>

View File

@ -3,6 +3,7 @@ import TrackPlayer, { State, Track } from 'react-native-track-player'
import equal from 'fast-deep-equal' import equal from 'fast-deep-equal'
import { useUpdateAtom } from 'jotai/utils' import { useUpdateAtom } from 'jotai/utils'
import { Song } from '../models/music' import { Song } from '../models/music'
import { PromiseQueue } from '../util'
type TrackExt = Track & { type TrackExt = Track & {
id: string id: string
@ -36,7 +37,7 @@ export const queueAtom = atom<TrackExt[]>(get => get(_queue))
export const queueWriteAtom = atom<TrackExt[], TrackExt[]>( export const queueWriteAtom = atom<TrackExt[], TrackExt[]>(
get => get(_queue), get => get(_queue),
(get, set, update) => { (get, set, update) => {
if (get(_queue) !== update) { if (!equal(get(_queue), update)) {
set(_queue, update) set(_queue, update)
} }
}, },
@ -50,19 +51,44 @@ export const queueNameAtom = atom<string | undefined>(get => {
return undefined return undefined
}) })
export const getQueue = async (): Promise<TrackExt[]> => { const trackPlayerCommands = new PromiseQueue(1)
const getQueue = async (): Promise<TrackExt[]> => {
return ((await TrackPlayer.getQueue()) as TrackExt[]) || [] return ((await TrackPlayer.getQueue()) as TrackExt[]) || []
} }
export const getTrack = async (index: number): Promise<TrackExt> => { const getTrack = async (index: number): Promise<TrackExt> => {
return ((await TrackPlayer.getTrack(index)) as TrackExt) || undefined return ((await TrackPlayer.getTrack(index)) as TrackExt) || undefined
} }
export const useRefreshQueue = () => {
const setQueue = useUpdateAtom(queueWriteAtom)
return () =>
trackPlayerCommands.enqueue(async () => {
setQueue(await getQueue())
})
}
export const useRefreshCurrentTrack = () => {
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
return () =>
trackPlayerCommands.enqueue(async () => {
const index = await TrackPlayer.getCurrentTrack()
if (typeof index === 'number' && index >= 0) {
setCurrentTrack(await getTrack(index))
} else {
setCurrentTrack(undefined)
}
})
}
export const usePrevious = () => { export const usePrevious = () => {
const setCurrentTrack = useUpdateAtom(currentTrackAtom) const setCurrentTrack = useUpdateAtom(currentTrackAtom)
return async () => { return () =>
try { trackPlayerCommands.enqueue(async () => {
const [current, queue] = await Promise.all([await TrackPlayer.getCurrentTrack(), await getQueue()]) const [current, queue] = await Promise.all([await TrackPlayer.getCurrentTrack(), await getQueue()])
if (current > 0) { if (current > 0) {
await TrackPlayer.skipToPrevious() await TrackPlayer.skipToPrevious()
@ -71,15 +97,14 @@ export const usePrevious = () => {
await TrackPlayer.seekTo(0) await TrackPlayer.seekTo(0)
} }
await TrackPlayer.play() await TrackPlayer.play()
} catch {} })
}
} }
export const useNext = () => { export const useNext = () => {
const setCurrentTrack = useUpdateAtom(currentTrackAtom) const setCurrentTrack = useUpdateAtom(currentTrackAtom)
return async () => { return () =>
try { trackPlayerCommands.enqueue(async () => {
const [current, queue] = await Promise.all([await TrackPlayer.getCurrentTrack(), await getQueue()]) const [current, queue] = await Promise.all([await TrackPlayer.getCurrentTrack(), await getQueue()])
if (current >= queue.length - 1) { if (current >= queue.length - 1) {
await TrackPlayer.skip(0) await TrackPlayer.skip(0)
@ -90,40 +115,44 @@ export const useNext = () => {
setCurrentTrack(queue[current + 1]) setCurrentTrack(queue[current + 1])
await TrackPlayer.play() await TrackPlayer.play()
} }
} catch {} })
}
} }
export const useAdd = () => { export const useAdd = () => {
const setQueue = useUpdateAtom(queueWriteAtom) const setQueue = useUpdateAtom(queueWriteAtom)
const setCurrentTrack = useUpdateAtom(currentTrackAtom) const setCurrentTrack = useUpdateAtom(currentTrackAtom)
return async (tracks: TrackExt | TrackExt[], insertBeforeindex?: number) => { return (tracks: TrackExt | TrackExt[], insertBeforeindex?: number) =>
trackPlayerCommands.enqueue(async () => {
await TrackPlayer.add(tracks, insertBeforeindex) await TrackPlayer.add(tracks, insertBeforeindex)
const queue = await getQueue() const queue = await getQueue()
setQueue(queue) setQueue(queue)
setCurrentTrack(queue.length > 0 ? queue[await TrackPlayer.getCurrentTrack()] : undefined) setCurrentTrack(queue.length > 0 ? queue[await TrackPlayer.getCurrentTrack()] : undefined)
} })
} }
export const useReset = () => { export const useReset = (enqueue = true) => {
const setQueue = useUpdateAtom(queueWriteAtom) const setQueue = useUpdateAtom(queueWriteAtom)
const setCurrentTrack = useUpdateAtom(currentTrackAtom) const setCurrentTrack = useUpdateAtom(currentTrackAtom)
return async () => { const reset = async () => {
await TrackPlayer.reset() await TrackPlayer.reset()
setQueue([]) setQueue([])
setCurrentTrack(undefined) setCurrentTrack(undefined)
} }
return enqueue ? () => trackPlayerCommands.enqueue(reset) : reset
} }
export const useSetQueue = () => { export const useSetQueue = () => {
const setCurrentTrack = useUpdateAtom(currentTrackAtom) const setCurrentTrack = useUpdateAtom(currentTrackAtom)
const setQueue = useUpdateAtom(queueWriteAtom) const setQueue = useUpdateAtom(queueWriteAtom)
const reset = useReset(false)
return async (songs: Song[], name: string, playId?: string) => { return async (songs: Song[], name: string, playId?: string) =>
await TrackPlayer.reset() trackPlayerCommands.enqueue(async () => {
await reset()
const tracks = songs.map(s => mapSongToTrack(s, name)) const tracks = songs.map(s => mapSongToTrack(s, name))
if (playId) { if (playId) {
@ -147,7 +176,7 @@ export const useSetQueue = () => {
} }
setQueue(await getQueue()) setQueue(await getQueue())
} })
} }
function mapSongToTrack(song: Song, queueName: string): TrackExt { function mapSongToTrack(song: Song, queueName: string): TrackExt {

View File

@ -26,6 +26,7 @@ import {
} from './responses' } from './responses'
import { Server } from '../models/settings' import { Server } from '../models/settings'
import paths from '../paths' import paths from '../paths'
import { PromiseQueue } from '../util'
export class SubsonicApiError extends Error { export class SubsonicApiError extends Error {
method: string method: string
@ -42,37 +43,7 @@ export class SubsonicApiError extends Error {
} }
} }
type QueuePromise = () => Promise<any> const downloadQueue = new PromiseQueue(1)
class Queue {
maxSimultaneously: number
private active = 0
private queue: QueuePromise[] = []
constructor(maxSimultaneously = 1) {
this.maxSimultaneously = maxSimultaneously
}
async enqueue(func: QueuePromise) {
if (++this.active > this.maxSimultaneously) {
await new Promise(resolve => this.queue.push(resolve as QueuePromise))
}
try {
return await func()
} catch (err) {
throw err
} finally {
this.active--
if (this.queue.length) {
this.queue.shift()?.()
}
}
}
}
const downloadQueue = new Queue(1)
export class SubsonicApiClient { export class SubsonicApiClient {
address: string address: string

View File

@ -9,3 +9,33 @@ export function formatDuration(seconds: number): string {
} }
return time return time
} }
type QueuedPromise = () => Promise<any>
export class PromiseQueue {
maxSimultaneously: number
private active = 0
private queue: QueuedPromise[] = []
constructor(maxSimultaneously = 1) {
this.maxSimultaneously = maxSimultaneously
}
async enqueue<T>(func: () => Promise<T>) {
if (++this.active > this.maxSimultaneously) {
await new Promise(resolve => this.queue.push(resolve as QueuedPromise))
}
try {
return await func()
} catch (err) {
throw err
} finally {
this.active--
if (this.queue.length) {
this.queue.shift()?.()
}
}
}
}