From 44617740fd499c819071339c708f7b7bf276d858 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Tue, 17 Aug 2021 10:58:11 +0900 Subject: [PATCH] rebuild queue with new urls on net state change --- .../java/com/subsonify/MainApplication.java | 1 + android/settings.gradle | 2 + app/hooks/trackplayer.ts | 11 ++- app/models/error.ts | 5 ++ app/playbackservice.ts | 15 ++++ app/screens/NowPlayingView.tsx | 2 +- app/state/musicmap.ts | 13 +-- app/state/trackplayer.ts | 88 +++++++++++++++++-- ios/Podfile | 2 + package.json | 1 + yarn.lock | 5 ++ 11 files changed, 124 insertions(+), 21 deletions(-) create mode 100644 app/models/error.ts diff --git a/android/app/src/main/java/com/subsonify/MainApplication.java b/android/app/src/main/java/com/subsonify/MainApplication.java index 59ded7e..5b1bd5c 100644 --- a/android/app/src/main/java/com/subsonify/MainApplication.java +++ b/android/app/src/main/java/com/subsonify/MainApplication.java @@ -4,6 +4,7 @@ import android.app.Application; import android.content.Context; import com.facebook.react.PackageList; import com.facebook.react.ReactApplication; +import com.reactnativecommunity.netinfo.NetInfoPackage; import com.oblador.vectoricons.VectorIconsPackage; import com.rnfs.RNFSPackage; import com.facebook.react.ReactInstanceManager; diff --git a/android/settings.gradle b/android/settings.gradle index 63da5eb..1a8e691 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,4 +1,6 @@ rootProject.name = 'SubSonify' +include ':@react-native-community_netinfo' +project(':@react-native-community_netinfo').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/netinfo/android') include ':react-native-vector-icons' project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android') include ':react-native-fs' diff --git a/app/hooks/trackplayer.ts b/app/hooks/trackplayer.ts index 28dcaf6..083119c 100644 --- a/app/hooks/trackplayer.ts +++ b/app/hooks/trackplayer.ts @@ -187,11 +187,12 @@ export const useSetQueue = () => { const setCurrentTrackIdx = useStore(selectTrackPlayer.setCurrentTrackIdx) const setQueue = useStore(selectTrackPlayer.setQueue) const setShuffleOrder = useStore(selectTrackPlayer.setShuffleOrder) - const setQueueName = useStore(selectTrackPlayer.setName) + const setQueueName = useStore(selectTrackPlayer.setQueueName) const getQueueShuffled = useCallback(() => !!useStore.getState().shuffleOrder, []) const setQueueContextType = useStore(selectTrackPlayer.setQueueContextType) const setQueueContextId = useStore(selectTrackPlayer.setQueueContextId) const fetchCoverArtFilePath = useStore(selectCache.fetchCoverArtFilePath) + const buildStreamUri = useStore(selectTrackPlayer.buildStreamUri) return async ( songs: Song[], @@ -222,6 +223,14 @@ export const useSetQueue = () => { let queue = songs.map(s => mapSongToTrack(s, coverArtPaths)) + try { + for (const t of queue) { + t.url = buildStreamUri(t.id) + } + } catch { + return + } + if (shuffled) { const { tracks, shuffleOrder } = shuffleTracks(queue, playTrack) setShuffleOrder(shuffleOrder) diff --git a/app/models/error.ts b/app/models/error.ts new file mode 100644 index 0000000..a8562fa --- /dev/null +++ b/app/models/error.ts @@ -0,0 +1,5 @@ +export class NoClientError extends Error { + constructor() { + super('no client in state') + } +} diff --git a/app/playbackservice.ts b/app/playbackservice.ts index 3e1602e..bc08f74 100644 --- a/app/playbackservice.ts +++ b/app/playbackservice.ts @@ -2,6 +2,7 @@ import { getCurrentTrack, getPlayerState, TrackExt, trackPlayerCommands } from ' import TrackPlayer, { Event, State } from 'react-native-track-player' import { useStore } from './state/store' import { unstable_batchedUpdates } from 'react-native' +import NetInfo, { NetInfoStateType } from '@react-native-community/netinfo' const reset = () => { unstable_batchedUpdates(() => { @@ -21,6 +22,12 @@ const setCurrentTrackIdx = (idx?: number) => { }) } +const setNetState = (netState: 'mobile' | 'wifi') => { + unstable_batchedUpdates(() => { + useStore.getState().setNetState(netState) + }) +} + let serviceCreated = false const createService = async () => { useStore.subscribe( @@ -33,6 +40,14 @@ const createService = async () => { (prev, next) => prev?.id === next?.id, ) + NetInfo.addEventListener(state => { + const currentType = useStore.getState().netState + const newType = state.type === NetInfoStateType.cellular ? 'mobile' : 'wifi' + if (currentType !== newType) { + setNetState(newType) + } + }) + TrackPlayer.addEventListener(Event.RemoteStop, () => { reset() trackPlayerCommands.enqueue(TrackPlayer.destroy) diff --git a/app/screens/NowPlayingView.tsx b/app/screens/NowPlayingView.tsx index 0a38405..9680361 100644 --- a/app/screens/NowPlayingView.tsx +++ b/app/screens/NowPlayingView.tsx @@ -49,7 +49,7 @@ function getContextName(type?: QueueContextType) { const NowPlayingHeader = React.memo<{ track?: TrackExt }>(({ track }) => { - const queueName = useStore(selectTrackPlayer.name) + const queueName = useStore(selectTrackPlayer.queueName) const queueContextType = useStore(selectTrackPlayer.queueContextType) if (!track) { diff --git a/app/state/musicmap.ts b/app/state/musicmap.ts index 9365831..18adf2c 100644 --- a/app/state/musicmap.ts +++ b/app/state/musicmap.ts @@ -35,19 +35,8 @@ export type MusicMapSlice = { mapPlaylistWithSongs: (playlist: PlaylistWithSongsElement) => Promise } -class NoClientError extends Error { - constructor() { - super('no client in state') - } -} - export const createMusicMapSlice = (set: SetState, get: GetState): MusicMapSlice => ({ mapChildToSong: async child => { - const client = get().client - if (!client) { - throw new NoClientError() - } - return { itemType: 'song', id: child.id, @@ -60,7 +49,7 @@ export const createMusicMapSlice = (set: SetState, get: GetState): duration: child.duration, starred: child.starred, coverArt: await get().getAlbumCoverArt(child.albumId), - streamUri: client.streamUri({ id: child.id }), + streamUri: get().buildStreamUri(child.id), } }, diff --git a/app/state/trackplayer.ts b/app/state/trackplayer.ts index 0b1ff95..8dce40c 100644 --- a/app/state/trackplayer.ts +++ b/app/state/trackplayer.ts @@ -1,5 +1,7 @@ +import { NoClientError } from '@app/models/error' import PromiseQueue from '@app/util/PromiseQueue' import produce from 'immer' +import { ToastAndroid } from 'react-native' import TrackPlayer, { RepeatMode, State, Track } from 'react-native-track-player' import { GetState, SetState } from 'zustand' import { Store } from './store' @@ -20,8 +22,8 @@ export type Progress = { export type QueueContextType = 'album' | 'playlist' | 'song' | 'artist' export type TrackPlayerSlice = { - name?: string - setName: (name?: string) => void + queueName?: string + setQueueName: (name?: string) => void queueContextType?: QueueContextType setQueueContextType: (queueContextType?: QueueContextType) => void @@ -50,12 +52,17 @@ export type TrackPlayerSlice = { scrobbleTrack: (id: string) => Promise + netState: 'mobile' | 'wifi' + setNetState: (netState: 'mobile' | 'wifi') => Promise + + buildStreamUri: (id: string) => string + reset: () => void } export const selectTrackPlayer = { - name: (store: TrackPlayerSlice) => store.name, - setName: (store: TrackPlayerSlice) => store.setName, + queueName: (store: TrackPlayerSlice) => store.queueName, + setQueueName: (store: TrackPlayerSlice) => store.setQueueName, queueContextType: (store: TrackPlayerSlice) => store.queueContextType, setQueueContextType: (store: TrackPlayerSlice) => store.setQueueContextType, @@ -85,14 +92,17 @@ export const selectTrackPlayer = { scrobbleTrack: (store: TrackPlayerSlice) => store.scrobbleTrack, + setNetState: (store: TrackPlayerSlice) => store.setNetState, + buildStreamUri: (store: TrackPlayerSlice) => store.buildStreamUri, + reset: (store: TrackPlayerSlice) => store.reset, } export const trackPlayerCommands = new PromiseQueue(1) export const createTrackPlayerSlice = (set: SetState, get: GetState): TrackPlayerSlice => ({ - name: undefined, - setName: name => set({ name }), + queueName: undefined, + setQueueName: name => set({ queueName: name }), queueContextType: undefined, setQueueContextType: queueContextType => set({ queueContextType }), @@ -141,9 +151,73 @@ export const createTrackPlayerSlice = (set: SetState, get: GetState { + if (netState === get().netState) { + return + } + set({ netState }) + ToastAndroid.show('switched netState to ' + netState, ToastAndroid.SHORT) + + await trackPlayerCommands.enqueue(async () => { + const queue = await getQueue() + if (!queue.length) { + return + } + + const currentTrack = await getCurrentTrack() + const state = await getPlayerState() + const position = (await TrackPlayer.getPosition()) || 0 + + const queueName = get().queueName + const queueContextId = get().queueContextId + const queueContextType = get().queueContextType + + await TrackPlayer.reset() + + try { + for (const track of queue) { + track.url = get().buildStreamUri(track.id) + } + } catch { + return + } + + set({ + queue, + queueName, + queueContextId, + queueContextType, + }) + get().setCurrentTrackIdx(currentTrack) + + await TrackPlayer.add(queue) + if (currentTrack) { + await TrackPlayer.skip(currentTrack) + } + await TrackPlayer.seekTo(position) + if (state === State.Playing) { + await TrackPlayer.play() + } + }) + }, + + buildStreamUri: id => { + const client = get().client + if (!client) { + throw new NoClientError() + } + + return client.streamUri({ + id, + estimateContentLength: get().settings.estimateContentLength, + maxBitRate: get().netState === 'mobile' ? get().settings.maxBitrateMobile : get().settings.maxBitrateWifi, + }) + }, + reset: () => { set({ - name: undefined, + queueName: undefined, queueContextType: undefined, queueContextId: undefined, shuffleOrder: undefined, diff --git a/ios/Podfile b/ios/Podfile index e73d64d..146aec8 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -16,6 +16,8 @@ target 'SubSonify' do pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons' + pod 'react-native-netinfo', :path => '../node_modules/@react-native-community/netinfo' + target 'SubSonifyTests' do inherit! :complete # Pods for testing diff --git a/package.json b/package.json index 600487e..f513df9 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@react-native-async-storage/async-storage": "^1.15.5", "@react-native-community/hooks": "^2.6.0", "@react-native-community/masked-view": "^0.1.11", + "@react-native-community/netinfo": "^6.0.0", "@react-native-community/slider": "^3.0.3", "@react-navigation/bottom-tabs": "^5.11.11", "@react-navigation/material-top-tabs": "^5.3.15", diff --git a/yarn.lock b/yarn.lock index ca0a54f..abb55f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1118,6 +1118,11 @@ resolved "https://registry.yarnpkg.com/@react-native-community/masked-view/-/masked-view-0.1.11.tgz#2f4c6e10bee0786abff4604e39a37ded6f3980ce" integrity sha512-rQfMIGSR/1r/SyN87+VD8xHHzDYeHaJq6elOSCAD+0iLagXkSI2pfA0LmSXP21uw5i3em7GkkRjfJ8wpqWXZNw== +"@react-native-community/netinfo@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-6.0.0.tgz#2a4d7190b508dd0c2293656c9c1aa068f6f60a71" + integrity sha512-Z9M8VGcF2IZVOo2x+oUStvpCW/8HjIRi4+iQCu5n+PhC7OqCQX58KYAzdBr///alIfRXiu6oMb+lK+rXQH1FvQ== + "@react-native-community/slider@^3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@react-native-community/slider/-/slider-3.0.3.tgz#830167fd757ba70ac638747ba3169b2dbae60330"