xref: /MusicFree/src/core/trackPlayer/index.ts (revision 5589cdf32b2bb0f641e5ac7bf1f6152cd6b9b70e)
1import {produce} from 'immer';
2import ReactNativeTrackPlayer, {
3    Event,
4    State,
5    Track,
6    TrackMetadataBase,
7    usePlaybackState,
8    useProgress,
9} from 'react-native-track-player';
10import shuffle from 'lodash.shuffle';
11import Config from '../config';
12import {
13    EDeviceEvents,
14    internalFakeSoundKey,
15    sortIndexSymbol,
16    timeStampSymbol,
17} from '@/constants/commonConst';
18import {GlobalState} from '@/utils/stateMapper';
19import delay from '@/utils/delay';
20import {
21    isSameMediaItem,
22    mergeProps,
23    sortByTimestampAndIndex,
24} from '@/utils/mediaItem';
25import Network from '../network';
26import LocalMusicSheet from '../localMusicSheet';
27import {SoundAsset} from '@/constants/assetsConst';
28import {getQualityOrder} from '@/utils/qualities';
29import musicHistory from '../musicHistory';
30import getUrlExt from '@/utils/getUrlExt';
31import {DeviceEventEmitter} from 'react-native';
32import LyricManager from '../lyricManager';
33import {MusicRepeatMode} from './common';
34import {
35    getMusicIndex,
36    getPlayList,
37    getPlayListMusicAt,
38    isInPlayList,
39    isPlayListEmpty,
40    setPlayList,
41    usePlayList,
42} from './internal/playList';
43import {createMediaIndexMap} from '@/utils/mediaIndexMap';
44import PluginManager from '../pluginManager';
45import {musicIsPaused} from '@/utils/trackUtils';
46import {trace} from '@/utils/log';
47import PersistStatus from '../persistStatus';
48import {getCurrentDialog, showDialog} from '@/components/dialogs/useDialog';
49import getSimilarMusic from '@/utils/getSimilarMusic';
50
51/** 当前播放 */
52const currentMusicStore = new GlobalState<IMusic.IMusicItem | null>(null);
53
54/** 播放模式 */
55const repeatModeStore = new GlobalState<MusicRepeatMode>(MusicRepeatMode.QUEUE);
56
57/** 音质 */
58const qualityStore = new GlobalState<IMusic.IQualityKey>('standard');
59
60let currentIndex = -1;
61
62// TODO: 下个版本最大限制调大一些
63const maxMusicQueueLength = 3000; // 当前播放最大限制
64const halfMaxMusicQueueLength = Math.floor(maxMusicQueueLength / 2);
65const shrinkPlayListToSize = (
66    queue: IMusic.IMusicItem[],
67    targetIndex = currentIndex,
68) => {
69    // 播放列表上限,太多无法缓存状态
70    if (queue.length > maxMusicQueueLength) {
71        if (targetIndex < halfMaxMusicQueueLength) {
72            queue = queue.slice(0, maxMusicQueueLength);
73        } else {
74            const right = Math.min(
75                queue.length,
76                targetIndex + halfMaxMusicQueueLength,
77            );
78            const left = Math.max(0, right - maxMusicQueueLength);
79            queue = queue.slice(left, right);
80        }
81    }
82    return queue;
83};
84
85let hasSetupListener = false;
86
87// TODO: 删除
88function migrate() {
89    const config = Config.get('status.music');
90    if (!config) {
91        return;
92    }
93    const {rate, repeatMode, musicQueue, progress, track} = config;
94    PersistStatus.set('music.rate', rate);
95    PersistStatus.set('music.repeatMode', repeatMode);
96    PersistStatus.set('music.playList', musicQueue);
97    PersistStatus.set('music.progress', progress);
98    PersistStatus.set('music.musicItem', track);
99    Config.set('status.music', undefined);
100}
101
102async function setupTrackPlayer() {
103    migrate();
104
105    const rate = PersistStatus.get('music.rate');
106    const musicQueue = PersistStatus.get('music.playList');
107    const repeatMode = PersistStatus.get('music.repeatMode');
108    const progress = PersistStatus.get('music.progress');
109    const track = PersistStatus.get('music.musicItem');
110    const quality =
111        PersistStatus.get('music.quality') ||
112        Config.get('setting.basic.defaultPlayQuality') ||
113        'standard';
114
115    // 状态恢复
116    if (rate) {
117        ReactNativeTrackPlayer.setRate(+rate / 100);
118    }
119    if (repeatMode) {
120        repeatModeStore.setValue(repeatMode as MusicRepeatMode);
121    }
122
123    if (musicQueue && Array.isArray(musicQueue)) {
124        addAll(musicQueue, undefined, repeatMode === MusicRepeatMode.SHUFFLE);
125    }
126
127    if (track && isInPlayList(track)) {
128        if (!Config.get('setting.basic.autoPlayWhenAppStart')) {
129            track.isInit = true;
130        }
131
132        // 异步
133        PluginManager.getByMedia(track)
134            ?.methods.getMediaSource(track, quality, 0)
135            .then(async newSource => {
136                track.url = newSource?.url || track.url;
137                track.headers = newSource?.headers || track.headers;
138
139                if (isSameMediaItem(currentMusicStore.getValue(), track)) {
140                    await setTrackSource(track as Track, false);
141                }
142            });
143        setCurrentMusic(track);
144
145        if (progress) {
146            // 异步
147            ReactNativeTrackPlayer.seekTo(progress);
148        }
149    }
150
151    if (!hasSetupListener) {
152        ReactNativeTrackPlayer.addEventListener(
153            Event.PlaybackActiveTrackChanged,
154            async evt => {
155                if (
156                    evt.index === 1 &&
157                    evt.lastIndex === 0 &&
158                    evt.track?.$ === internalFakeSoundKey
159                ) {
160                    trace('队列末尾,播放下一首');
161                    if (repeatModeStore.getValue() === MusicRepeatMode.SINGLE) {
162                        await play(null, true);
163                    } else {
164                        // 当前生效的歌曲是下一曲的标记
165                        await skipToNext();
166                    }
167                }
168            },
169        );
170
171        ReactNativeTrackPlayer.addEventListener(
172            Event.PlaybackError,
173            async e => {
174                // WARNING: 不稳定,报错的时候有可能track已经变到下一首歌去了
175                const currentTrack =
176                    await ReactNativeTrackPlayer.getActiveTrack();
177                if (currentTrack?.isInit) {
178                    // HACK: 避免初始失败的情况
179                    ReactNativeTrackPlayer.updateMetadataForTrack(0, {
180                        ...currentTrack,
181                        // @ts-ignore
182                        isInit: undefined,
183                    });
184                    return;
185                }
186
187                if (
188                    (await ReactNativeTrackPlayer.getActiveTrackIndex()) ===
189                        0 &&
190                    e.message &&
191                    e.message !== 'android-io-file-not-found'
192                ) {
193                    trace('播放出错', {
194                        message: e.message,
195                        code: e.code,
196                    });
197
198                    failToPlay();
199                }
200            },
201        );
202
203        hasSetupListener = true;
204    }
205}
206
207/**
208 * 获取自动播放的下一个track
209 */
210const getFakeNextTrack = () => {
211    let track: Track | undefined;
212    const repeatMode = repeatModeStore.getValue();
213    if (repeatMode === MusicRepeatMode.SINGLE) {
214        // 单曲循环
215        track = getPlayListMusicAt(currentIndex) as Track;
216    } else {
217        // 下一曲
218        track = getPlayListMusicAt(currentIndex + 1) as Track;
219    }
220
221    if (track) {
222        return produce(track, _ => {
223            _.url = SoundAsset.fakeAudio;
224            _.$ = internalFakeSoundKey;
225            if (!_.artwork?.trim()?.length) {
226                _.artwork = undefined;
227            }
228        });
229    } else {
230        // 只有列表长度为0时才会出现的特殊情况
231        return {url: SoundAsset.fakeAudio, $: internalFakeSoundKey} as Track;
232    }
233};
234
235/** 播放失败时的情况 */
236async function failToPlay() {
237    // 如果自动跳转下一曲, 500s后自动跳转
238    if (!Config.get('setting.basic.autoStopWhenError')) {
239        await ReactNativeTrackPlayer.reset();
240        await delay(500);
241        await skipToNext();
242    }
243}
244
245// 播放模式相关
246const _toggleRepeatMapping = {
247    [MusicRepeatMode.SHUFFLE]: MusicRepeatMode.SINGLE,
248    [MusicRepeatMode.SINGLE]: MusicRepeatMode.QUEUE,
249    [MusicRepeatMode.QUEUE]: MusicRepeatMode.SHUFFLE,
250};
251/** 切换下一个模式 */
252const toggleRepeatMode = () => {
253    setRepeatMode(_toggleRepeatMapping[repeatModeStore.getValue()]);
254};
255
256/**
257 * 添加到播放列表
258 * @param musicItems 目标歌曲
259 * @param beforeIndex 在第x首歌曲前添加
260 * @param shouldShuffle 随机排序
261 */
262const addAll = (
263    musicItems: Array<IMusic.IMusicItem> = [],
264    beforeIndex?: number,
265    shouldShuffle?: boolean,
266) => {
267    const now = Date.now();
268    let newPlayList: IMusic.IMusicItem[] = [];
269    let currentPlayList = getPlayList();
270    const _musicItems = musicItems.map((item, index) =>
271        produce(item, draft => {
272            draft[timeStampSymbol] = now;
273            draft[sortIndexSymbol] = index;
274        }),
275    );
276    if (beforeIndex === undefined || beforeIndex < 0) {
277        // 1.1. 添加到歌单末尾,并过滤掉已有的歌曲
278        newPlayList = currentPlayList.concat(
279            _musicItems.filter(item => !isInPlayList(item)),
280        );
281    } else {
282        // 1.2. 新的播放列表,插入
283        const indexMap = createMediaIndexMap(_musicItems);
284        const beforeDraft = currentPlayList
285            .slice(0, beforeIndex)
286            .filter(item => !indexMap.has(item));
287        const afterDraft = currentPlayList
288            .slice(beforeIndex)
289            .filter(item => !indexMap.has(item));
290
291        newPlayList = [...beforeDraft, ..._musicItems, ...afterDraft];
292    }
293
294    // 如果太长了
295    if (newPlayList.length > maxMusicQueueLength) {
296        newPlayList = shrinkPlayListToSize(
297            newPlayList,
298            beforeIndex ?? newPlayList.length - 1,
299        );
300    }
301
302    // 2. 如果需要随机
303    if (shouldShuffle) {
304        newPlayList = shuffle(newPlayList);
305    }
306    // 3. 设置播放列表
307    setPlayList(newPlayList);
308    const currentMusicItem = currentMusicStore.getValue();
309
310    // 4. 重置下标
311    if (currentMusicItem) {
312        currentIndex = getMusicIndex(currentMusicItem);
313    }
314
315    // TODO: 更新播放队列信息
316    // 5. 存储更新的播放列表信息
317};
318
319/** 追加到队尾 */
320const add = (
321    musicItem: IMusic.IMusicItem | IMusic.IMusicItem[],
322    beforeIndex?: number,
323) => {
324    addAll(Array.isArray(musicItem) ? musicItem : [musicItem], beforeIndex);
325};
326
327/**
328 * 下一首播放
329 * @param musicItem
330 */
331const addNext = (musicItem: IMusic.IMusicItem | IMusic.IMusicItem[]) => {
332    const shouldPlay = isPlayListEmpty();
333    add(musicItem, currentIndex + 1);
334    if (shouldPlay) {
335        play(Array.isArray(musicItem) ? musicItem[0] : musicItem);
336    }
337};
338
339const isCurrentMusic = (musicItem: IMusic.IMusicItem | null | undefined) => {
340    return isSameMediaItem(musicItem, currentMusicStore.getValue()) ?? false;
341};
342
343const remove = async (musicItem: IMusic.IMusicItem) => {
344    const playList = getPlayList();
345    let newPlayList: IMusic.IMusicItem[] = [];
346    let currentMusic: IMusic.IMusicItem | null = currentMusicStore.getValue();
347    const targetIndex = getMusicIndex(musicItem);
348    let shouldPlayCurrent: boolean | null = null;
349    if (targetIndex === -1) {
350        // 1. 这种情况应该是出错了
351        return;
352    }
353    // 2. 移除的是当前项
354    if (currentIndex === targetIndex) {
355        // 2.1 停止播放,移除当前项
356        newPlayList = produce(playList, draft => {
357            draft.splice(targetIndex, 1);
358        });
359        // 2.2 设置新的播放列表,并更新当前音乐
360        if (newPlayList.length === 0) {
361            currentMusic = null;
362            shouldPlayCurrent = false;
363        } else {
364            currentMusic = newPlayList[currentIndex % newPlayList.length];
365            try {
366                const state = (await ReactNativeTrackPlayer.getPlaybackState())
367                    .state;
368                if (musicIsPaused(state)) {
369                    shouldPlayCurrent = false;
370                } else {
371                    shouldPlayCurrent = true;
372                }
373            } catch {
374                shouldPlayCurrent = false;
375            }
376        }
377    } else {
378        // 3. 删除
379        newPlayList = produce(playList, draft => {
380            draft.splice(targetIndex, 1);
381        });
382    }
383
384    setPlayList(newPlayList);
385    setCurrentMusic(currentMusic);
386    if (shouldPlayCurrent === true) {
387        await play(currentMusic, true);
388    } else if (shouldPlayCurrent === false) {
389        await ReactNativeTrackPlayer.reset();
390    }
391};
392
393/**
394 * 设置播放模式
395 * @param mode 播放模式
396 */
397const setRepeatMode = (mode: MusicRepeatMode) => {
398    const playList = getPlayList();
399    let newPlayList;
400    const prevMode = repeatModeStore.getValue();
401
402    if (
403        (prevMode === MusicRepeatMode.SHUFFLE &&
404            mode !== MusicRepeatMode.SHUFFLE) ||
405        (mode === MusicRepeatMode.SHUFFLE &&
406            prevMode !== MusicRepeatMode.SHUFFLE)
407    ) {
408        if (mode === MusicRepeatMode.SHUFFLE) {
409            newPlayList = shuffle(playList);
410        } else {
411            newPlayList = sortByTimestampAndIndex(playList, true);
412        }
413        setPlayList(newPlayList);
414    }
415
416    const currentMusicItem = currentMusicStore.getValue();
417    currentIndex = getMusicIndex(currentMusicItem);
418    repeatModeStore.setValue(mode);
419    // 更新下一首歌的信息
420    ReactNativeTrackPlayer.updateMetadataForTrack(1, getFakeNextTrack());
421    // 记录
422    PersistStatus.set('music.repeatMode', mode);
423};
424
425/** 清空播放列表 */
426const clear = async () => {
427    setPlayList([]);
428    setCurrentMusic(null);
429
430    await ReactNativeTrackPlayer.reset();
431    PersistStatus.set('music.musicItem', undefined);
432    PersistStatus.set('music.progress', 0);
433};
434
435/** 暂停 */
436const pause = async () => {
437    await ReactNativeTrackPlayer.pause();
438};
439
440/** 设置音源 */
441const setTrackSource = async (track: Track, autoPlay = true) => {
442    if (!track.artwork?.trim()?.length) {
443        track.artwork = undefined;
444    }
445    await ReactNativeTrackPlayer.setQueue([track, getFakeNextTrack()]);
446    PersistStatus.set('music.musicItem', track as IMusic.IMusicItem);
447    PersistStatus.set('music.progress', 0);
448    if (autoPlay) {
449        await ReactNativeTrackPlayer.play();
450    }
451};
452
453const setCurrentMusic = (musicItem?: IMusic.IMusicItem | null) => {
454    if (!musicItem) {
455        currentIndex = -1;
456        currentMusicStore.setValue(null);
457        PersistStatus.set('music.musicItem', undefined);
458        PersistStatus.set('music.progress', 0);
459        return;
460    }
461    currentIndex = getMusicIndex(musicItem);
462    currentMusicStore.setValue(musicItem);
463};
464
465const setQuality = (quality: IMusic.IQualityKey) => {
466    qualityStore.setValue(quality);
467    PersistStatus.set('music.quality', quality);
468};
469/**
470 * 播放
471 *
472 * 当musicItem 为空时,代表暂停/播放
473 *
474 * @param musicItem
475 * @param forcePlay
476 * @returns
477 */
478const play = async (
479    musicItem?: IMusic.IMusicItem | null,
480    forcePlay?: boolean,
481) => {
482    try {
483        if (!musicItem) {
484            musicItem = currentMusicStore.getValue();
485        }
486        if (!musicItem) {
487            throw new Error(PlayFailReason.PLAY_LIST_IS_EMPTY);
488        }
489        // 1. 移动网络禁止播放
490        if (
491            Network.isCellular() &&
492            !Config.get('setting.basic.useCelluarNetworkPlay') &&
493            !LocalMusicSheet.isLocalMusic(musicItem)
494        ) {
495            await ReactNativeTrackPlayer.reset();
496            throw new Error(PlayFailReason.FORBID_CELLUAR_NETWORK_PLAY);
497        }
498
499        // 2. 如果是当前正在播放的音频
500        if (isCurrentMusic(musicItem)) {
501            const currentTrack = await ReactNativeTrackPlayer.getTrack(0);
502            // 2.1 如果当前有源
503            if (
504                currentTrack?.url &&
505                isSameMediaItem(musicItem, currentTrack as IMusic.IMusicItem)
506            ) {
507                const currentActiveIndex =
508                    await ReactNativeTrackPlayer.getActiveTrackIndex();
509                if (currentActiveIndex !== 0) {
510                    await ReactNativeTrackPlayer.skip(0);
511                }
512                if (forcePlay) {
513                    // 2.1.1 强制重新开始
514                    await ReactNativeTrackPlayer.seekTo(0);
515                }
516                const currentState = (
517                    await ReactNativeTrackPlayer.getPlaybackState()
518                ).state;
519                if (currentState === State.Stopped) {
520                    await setTrackSource(currentTrack);
521                }
522                if (currentState !== State.Playing) {
523                    // 2.1.2 恢复播放
524                    await ReactNativeTrackPlayer.play();
525                }
526                // 这种情况下,播放队列和当前歌曲都不需要变化
527                return;
528            }
529            // 2.2 其他情况:重新获取源
530        }
531
532        // 3. 如果没有在播放列表中,添加到队尾;同时更新列表状态
533        const inPlayList = isInPlayList(musicItem);
534        if (!inPlayList) {
535            add(musicItem);
536        }
537
538        // 4. 更新列表状态和当前音乐
539        setCurrentMusic(musicItem);
540        await ReactNativeTrackPlayer.reset();
541
542        // 4.1 刷新歌词信息
543        if (
544            !isSameMediaItem(
545                LyricManager.getLyricState()?.lyricParser?.musicItem,
546                musicItem,
547            )
548        ) {
549            DeviceEventEmitter.emit(EDeviceEvents.REFRESH_LYRIC, true);
550        }
551
552        // 5. 获取音源
553        let track: IMusic.IMusicItem;
554
555        // 5.1 通过插件获取音源
556        const plugin = PluginManager.getByName(musicItem.platform);
557        // 5.2 获取音质排序
558        const qualityOrder = getQualityOrder(
559            Config.get('setting.basic.defaultPlayQuality') ?? 'standard',
560            Config.get('setting.basic.playQualityOrder') ?? 'asc',
561        );
562        // 5.3 插件返回音源
563        let source: IPlugin.IMediaSourceResult | null = null;
564        for (let quality of qualityOrder) {
565            if (isCurrentMusic(musicItem)) {
566                source =
567                    (await plugin?.methods?.getMediaSource(
568                        musicItem,
569                        quality,
570                    )) ?? null;
571                // 5.3.1 获取到真实源
572                if (source) {
573                    setQuality(quality);
574                    break;
575                }
576            } else {
577                // 5.3.2 已经切换到其他歌曲了,
578                return;
579            }
580        }
581
582        if (!isCurrentMusic(musicItem)) {
583            return;
584        }
585        if (!source) {
586            // 如果有source
587            if (musicItem.source) {
588                for (let quality of qualityOrder) {
589                    if (musicItem.source[quality]?.url) {
590                        source = musicItem.source[quality]!;
591                        setQuality(quality);
592
593                        break;
594                    }
595                }
596            }
597            // 5.4 没有返回源
598            if (!source && !musicItem.url) {
599                // 插件失效的情况
600                if (Config.get('setting.basic.tryChangeSourceWhenPlayFail')) {
601                    // 重试
602                    const similarMusic = await getSimilarMusic(
603                        musicItem,
604                        'music',
605                        () => !isCurrentMusic(musicItem),
606                    );
607
608                    if (similarMusic) {
609                        const similarMusicPlugin =
610                            PluginManager.getByMedia(similarMusic);
611
612                        for (let quality of qualityOrder) {
613                            if (isCurrentMusic(musicItem)) {
614                                source =
615                                    (await similarMusicPlugin?.methods?.getMediaSource(
616                                        similarMusic,
617                                        quality,
618                                    )) ?? null;
619                                // 5.4.1 获取到真实源
620                                if (source) {
621                                    setQuality(quality);
622                                    break;
623                                }
624                            } else {
625                                // 5.4.2 已经切换到其他歌曲了,
626                                return;
627                            }
628                        }
629                    }
630
631                    if (!source) {
632                        throw new Error(PlayFailReason.INVALID_SOURCE);
633                    }
634                } else {
635                    throw new Error(PlayFailReason.INVALID_SOURCE);
636                }
637            } else {
638                source = {
639                    url: musicItem.url,
640                };
641                setQuality('standard');
642            }
643        }
644
645        // 6. 特殊类型源
646        if (getUrlExt(source.url) === '.m3u8') {
647            // @ts-ignore
648            source.type = 'hls';
649        }
650        // 7. 合并结果
651        track = mergeProps(musicItem, source) as IMusic.IMusicItem;
652
653        // 8. 新增历史记录
654        musicHistory.addMusic(musicItem);
655
656        trace('获取音源成功', track);
657        // 9. 设置音源
658        await setTrackSource(track as Track);
659
660        // 10. 获取补充信息
661        let info: Partial<IMusic.IMusicItem> | null = null;
662        try {
663            info = (await plugin?.methods?.getMusicInfo?.(musicItem)) ?? null;
664            if (
665                (typeof info?.url === 'string' && info.url.trim() === '') ||
666                (info?.url && typeof info.url !== 'string')
667            ) {
668                delete info.url;
669            }
670        } catch {}
671
672        // 11. 设置补充信息
673        if (info && isCurrentMusic(musicItem)) {
674            const mergedTrack = mergeProps(track, info);
675            currentMusicStore.setValue(mergedTrack as IMusic.IMusicItem);
676            await ReactNativeTrackPlayer.updateMetadataForTrack(
677                0,
678                mergedTrack as TrackMetadataBase,
679            );
680        }
681    } catch (e: any) {
682        const message = e?.message;
683        if (
684            message === 'The player is not initialized. Call setupPlayer first.'
685        ) {
686            await ReactNativeTrackPlayer.setupPlayer();
687            play(musicItem, forcePlay);
688        } else if (message === PlayFailReason.FORBID_CELLUAR_NETWORK_PLAY) {
689            if (getCurrentDialog()?.name !== 'SimpleDialog') {
690                showDialog('SimpleDialog', {
691                    title: '流量提醒',
692                    content:
693                        '当前非WIFI环境,侧边栏设置中打开【使用移动网络播放】功能后可继续播放',
694                });
695            }
696
697            // Toast.warn(
698            //     '当前禁止移动网络播放音乐,如需播放请去侧边栏-基本设置中修改',
699            // );
700        } else if (message === PlayFailReason.INVALID_SOURCE) {
701            trace('音源为空,播放失败');
702            await failToPlay();
703        } else if (message === PlayFailReason.PLAY_LIST_IS_EMPTY) {
704            // 队列是空的,不应该出现这种情况
705        }
706    }
707};
708
709/**
710 * 播放音乐,同时替换播放队列
711 * @param musicItem 音乐
712 * @param newPlayList 替代列表
713 */
714const playWithReplacePlayList = async (
715    musicItem: IMusic.IMusicItem,
716    newPlayList: IMusic.IMusicItem[],
717) => {
718    if (newPlayList.length !== 0) {
719        const now = Date.now();
720        if (newPlayList.length > maxMusicQueueLength) {
721            newPlayList = shrinkPlayListToSize(
722                newPlayList,
723                newPlayList.findIndex(it => isSameMediaItem(it, musicItem)),
724            );
725        }
726        const playListItems = newPlayList.map((item, index) =>
727            produce(item, draft => {
728                draft[timeStampSymbol] = now;
729                draft[sortIndexSymbol] = index;
730            }),
731        );
732        setPlayList(
733            repeatModeStore.getValue() === MusicRepeatMode.SHUFFLE
734                ? shuffle(playListItems)
735                : playListItems,
736        );
737        await play(musicItem, true);
738    }
739};
740
741const skipToNext = async () => {
742    if (isPlayListEmpty()) {
743        setCurrentMusic(null);
744        return;
745    }
746
747    await play(getPlayListMusicAt(currentIndex + 1), true);
748};
749
750const skipToPrevious = async () => {
751    if (isPlayListEmpty()) {
752        setCurrentMusic(null);
753        return;
754    }
755
756    await play(
757        getPlayListMusicAt(currentIndex === -1 ? 0 : currentIndex - 1),
758        true,
759    );
760};
761
762/** 修改当前播放的音质 */
763const changeQuality = async (newQuality: IMusic.IQualityKey) => {
764    // 获取当前的音乐和进度
765    if (newQuality === qualityStore.getValue()) {
766        return true;
767    }
768
769    // 获取当前歌曲
770    const musicItem = currentMusicStore.getValue();
771    if (!musicItem) {
772        return false;
773    }
774    try {
775        const progress = await ReactNativeTrackPlayer.getProgress();
776        const plugin = PluginManager.getByMedia(musicItem);
777        const newSource = await plugin?.methods?.getMediaSource(
778            musicItem,
779            newQuality,
780        );
781        if (!newSource?.url) {
782            throw new Error(PlayFailReason.INVALID_SOURCE);
783        }
784        if (isCurrentMusic(musicItem)) {
785            const playingState = (
786                await ReactNativeTrackPlayer.getPlaybackState()
787            ).state;
788            await setTrackSource(
789                mergeProps(musicItem, newSource) as unknown as Track,
790                !musicIsPaused(playingState),
791            );
792
793            await ReactNativeTrackPlayer.seekTo(progress.position ?? 0);
794            setQuality(newQuality);
795        }
796        return true;
797    } catch {
798        // 修改失败
799        return false;
800    }
801};
802
803enum PlayFailReason {
804    /** 禁止移动网络播放 */
805    FORBID_CELLUAR_NETWORK_PLAY = 'FORBID_CELLUAR_NETWORK_PLAY',
806    /** 播放列表为空 */
807    PLAY_LIST_IS_EMPTY = 'PLAY_LIST_IS_EMPTY',
808    /** 无效源 */
809    INVALID_SOURCE = 'INVALID_SOURCE',
810    /** 非当前音乐 */
811}
812
813function useMusicState() {
814    const playbackState = usePlaybackState();
815
816    return playbackState.state;
817}
818
819function getPreviousMusic() {
820    const currentMusicItem = currentMusicStore.getValue();
821    if (!currentMusicItem) {
822        return null;
823    }
824
825    return getPlayListMusicAt(currentIndex - 1);
826}
827
828function getNextMusic() {
829    const currentMusicItem = currentMusicStore.getValue();
830    if (!currentMusicItem) {
831        return null;
832    }
833
834    return getPlayListMusicAt(currentIndex + 1);
835}
836
837const TrackPlayer = {
838    setupTrackPlayer,
839    usePlayList,
840    getPlayList,
841    addAll,
842    add,
843    addNext,
844    skipToNext,
845    skipToPrevious,
846    play,
847    playWithReplacePlayList,
848    pause,
849    remove,
850    clear,
851    useCurrentMusic: currentMusicStore.useValue,
852    getCurrentMusic: currentMusicStore.getValue,
853    useRepeatMode: repeatModeStore.useValue,
854    getRepeatMode: repeatModeStore.getValue,
855    toggleRepeatMode,
856    usePlaybackState,
857    getProgress: ReactNativeTrackPlayer.getProgress,
858    useProgress: useProgress,
859    seekTo: ReactNativeTrackPlayer.seekTo,
860    changeQuality,
861    useCurrentQuality: qualityStore.useValue,
862    getCurrentQuality: qualityStore.getValue,
863    getRate: ReactNativeTrackPlayer.getRate,
864    setRate: ReactNativeTrackPlayer.setRate,
865    useMusicState,
866    reset: ReactNativeTrackPlayer.reset,
867    getPreviousMusic,
868    getNextMusic,
869};
870
871export default TrackPlayer;
872export {MusicRepeatMode, State as MusicState};
873