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

View File

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

View File

@ -26,6 +26,7 @@ import {
} from './responses'
import { Server } from '../models/settings'
import paths from '../paths'
import { PromiseQueue } from '../util'
export class SubsonicApiError extends Error {
method: string
@ -42,37 +43,7 @@ export class SubsonicApiError extends Error {
}
}
type QueuePromise = () => Promise<any>
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)
const downloadQueue = new PromiseQueue(1)
export class SubsonicApiClient {
address: string

View File

@ -9,3 +9,33 @@ export function formatDuration(seconds: number): string {
}
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()?.()
}
}
}
}