xref: /MusicFree/src/core/trackPlayer/index.ts (revision adf41771e5c3ca7c27879b461cece7e444d1dc58)
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 = 10000; // 当前播放最大限制
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    musicItems.forEach((item, index) => {
271        item[timeStampSymbol] = now;
272        item[sortIndexSymbol] = index;
273    });
274
275    if (beforeIndex === undefined || beforeIndex < 0) {
276        // 1.1. 添加到歌单末尾,并过滤掉已有的歌曲
277        newPlayList = currentPlayList.concat(
278            musicItems.filter(item => !isInPlayList(item)),
279        );
280    } else {
281        // 1.2. 新的播放列表,插入
282        const indexMap = createMediaIndexMap(musicItems);
283        const beforeDraft = currentPlayList
284            .slice(0, beforeIndex)
285            .filter(item => !indexMap.has(item));
286        const afterDraft = currentPlayList
287            .slice(beforeIndex)
288            .filter(item => !indexMap.has(item));
289
290        newPlayList = [...beforeDraft, ...musicItems, ...afterDraft];
291    }
292
293    // 如果太长了
294    if (newPlayList.length > maxMusicQueueLength) {
295        newPlayList = shrinkPlayListToSize(
296            newPlayList,
297            beforeIndex ?? newPlayList.length - 1,
298        );
299    }
300
301    // 2. 如果需要随机
302    if (shouldShuffle) {
303        newPlayList = shuffle(newPlayList);
304    }
305    // 3. 设置播放列表
306    setPlayList(newPlayList);
307    const currentMusicItem = currentMusicStore.getValue();
308
309    // 4. 重置下标
310    if (currentMusicItem) {
311        currentIndex = getMusicIndex(currentMusicItem);
312    }
313
314    // TODO: 更新播放队列信息
315    // 5. 存储更新的播放列表信息
316};
317
318/** 追加到队尾 */
319const add = (
320    musicItem: IMusic.IMusicItem | IMusic.IMusicItem[],
321    beforeIndex?: number,
322) => {
323    addAll(Array.isArray(musicItem) ? musicItem : [musicItem], beforeIndex);
324};
325
326/**
327 * 下一首播放
328 * @param musicItem
329 */
330const addNext = (musicItem: IMusic.IMusicItem | IMusic.IMusicItem[]) => {
331    const shouldPlay = isPlayListEmpty();
332    add(musicItem, currentIndex + 1);
333    if (shouldPlay) {
334        play(Array.isArray(musicItem) ? musicItem[0] : musicItem);
335    }
336};
337
338const isCurrentMusic = (musicItem: IMusic.IMusicItem | null | undefined) => {
339    return isSameMediaItem(musicItem, currentMusicStore.getValue()) ?? false;
340};
341
342const remove = async (musicItem: IMusic.IMusicItem) => {
343    const playList = getPlayList();
344    let newPlayList: IMusic.IMusicItem[] = [];
345    let currentMusic: IMusic.IMusicItem | null = currentMusicStore.getValue();
346    const targetIndex = getMusicIndex(musicItem);
347    let shouldPlayCurrent: boolean | null = null;
348    if (targetIndex === -1) {
349        // 1. 这种情况应该是出错了
350        return;
351    }
352    // 2. 移除的是当前项
353    if (currentIndex === targetIndex) {
354        // 2.1 停止播放,移除当前项
355        newPlayList = produce(playList, draft => {
356            draft.splice(targetIndex, 1);
357        });
358        // 2.2 设置新的播放列表,并更新当前音乐
359        if (newPlayList.length === 0) {
360            currentMusic = null;
361            shouldPlayCurrent = false;
362        } else {
363            currentMusic = newPlayList[currentIndex % newPlayList.length];
364            try {
365                const state = (await ReactNativeTrackPlayer.getPlaybackState())
366                    .state;
367                if (musicIsPaused(state)) {
368                    shouldPlayCurrent = false;
369                } else {
370                    shouldPlayCurrent = true;
371                }
372            } catch {
373                shouldPlayCurrent = false;
374            }
375        }
376    } else {
377        // 3. 删除
378        newPlayList = produce(playList, draft => {
379            draft.splice(targetIndex, 1);
380        });
381    }
382
383    setPlayList(newPlayList);
384    setCurrentMusic(currentMusic);
385    if (shouldPlayCurrent === true) {
386        await play(currentMusic, true);
387    } else if (shouldPlayCurrent === false) {
388        await ReactNativeTrackPlayer.reset();
389    }
390};
391
392/**
393 * 设置播放模式
394 * @param mode 播放模式
395 */
396const setRepeatMode = (mode: MusicRepeatMode) => {
397    const playList = getPlayList();
398    let newPlayList;
399    const prevMode = repeatModeStore.getValue();
400    if (
401        (prevMode === MusicRepeatMode.SHUFFLE &&
402            mode !== MusicRepeatMode.SHUFFLE) ||
403        (mode === MusicRepeatMode.SHUFFLE &&
404            prevMode !== MusicRepeatMode.SHUFFLE)
405    ) {
406        if (mode === MusicRepeatMode.SHUFFLE) {
407            newPlayList = shuffle(playList);
408        } else {
409            newPlayList = sortByTimestampAndIndex(playList, true);
410        }
411        setPlayList(newPlayList);
412    }
413
414    const currentMusicItem = currentMusicStore.getValue();
415    currentIndex = getMusicIndex(currentMusicItem);
416    repeatModeStore.setValue(mode);
417    // 更新下一首歌的信息
418    ReactNativeTrackPlayer.updateMetadataForTrack(1, getFakeNextTrack());
419    // 记录
420    PersistStatus.set('music.repeatMode', mode);
421};
422
423/** 清空播放列表 */
424const clear = async () => {
425    setPlayList([]);
426    setCurrentMusic(null);
427
428    await ReactNativeTrackPlayer.reset();
429    PersistStatus.set('music.musicItem', undefined);
430    PersistStatus.set('music.progress', 0);
431};
432
433/** 暂停 */
434const pause = async () => {
435    await ReactNativeTrackPlayer.pause();
436};
437
438/** 设置音源 */
439const setTrackSource = async (track: Track, autoPlay = true) => {
440    if (!track.artwork?.trim()?.length) {
441        track.artwork = undefined;
442    }
443    await ReactNativeTrackPlayer.setQueue([track, getFakeNextTrack()]);
444    PersistStatus.set('music.musicItem', track as IMusic.IMusicItem);
445    PersistStatus.set('music.progress', 0);
446    if (autoPlay) {
447        await ReactNativeTrackPlayer.play();
448    }
449};
450
451const setCurrentMusic = (musicItem?: IMusic.IMusicItem | null) => {
452    if (!musicItem) {
453        currentIndex = -1;
454        currentMusicStore.setValue(null);
455        PersistStatus.set('music.musicItem', undefined);
456        PersistStatus.set('music.progress', 0);
457        return;
458    }
459    currentIndex = getMusicIndex(musicItem);
460    currentMusicStore.setValue(musicItem);
461};
462
463const setQuality = (quality: IMusic.IQualityKey) => {
464    qualityStore.setValue(quality);
465    PersistStatus.set('music.quality', quality);
466};
467/**
468 * 播放
469 *
470 * 当musicItem 为空时,代表暂停/播放
471 *
472 * @param musicItem
473 * @param forcePlay
474 * @returns
475 */
476const play = async (
477    musicItem?: IMusic.IMusicItem | null,
478    forcePlay?: boolean,
479) => {
480    try {
481        if (!musicItem) {
482            musicItem = currentMusicStore.getValue();
483        }
484        if (!musicItem) {
485            throw new Error(PlayFailReason.PLAY_LIST_IS_EMPTY);
486        }
487        // 1. 移动网络禁止播放
488        if (
489            Network.isCellular() &&
490            !Config.get('setting.basic.useCelluarNetworkPlay') &&
491            !LocalMusicSheet.isLocalMusic(musicItem)
492        ) {
493            await ReactNativeTrackPlayer.reset();
494            throw new Error(PlayFailReason.FORBID_CELLUAR_NETWORK_PLAY);
495        }
496
497        // 2. 如果是当前正在播放的音频
498        if (isCurrentMusic(musicItem)) {
499            const currentTrack = await ReactNativeTrackPlayer.getTrack(0);
500            // 2.1 如果当前有源
501            if (
502                currentTrack?.url &&
503                isSameMediaItem(musicItem, currentTrack as IMusic.IMusicItem)
504            ) {
505                const currentActiveIndex =
506                    await ReactNativeTrackPlayer.getActiveTrackIndex();
507                if (currentActiveIndex !== 0) {
508                    await ReactNativeTrackPlayer.skip(0);
509                }
510                if (forcePlay) {
511                    // 2.1.1 强制重新开始
512                    await ReactNativeTrackPlayer.seekTo(0);
513                }
514                const currentState = (
515                    await ReactNativeTrackPlayer.getPlaybackState()
516                ).state;
517                if (currentState === State.Stopped) {
518                    await setTrackSource(currentTrack);
519                }
520                if (currentState !== State.Playing) {
521                    // 2.1.2 恢复播放
522                    await ReactNativeTrackPlayer.play();
523                }
524                // 这种情况下,播放队列和当前歌曲都不需要变化
525                return;
526            }
527            // 2.2 其他情况:重新获取源
528        }
529
530        // 3. 如果没有在播放列表中,添加到队尾;同时更新列表状态
531        const inPlayList = isInPlayList(musicItem);
532        if (!inPlayList) {
533            add(musicItem);
534        }
535
536        // 4. 更新列表状态和当前音乐
537        setCurrentMusic(musicItem);
538        await ReactNativeTrackPlayer.reset();
539
540        // 4.1 刷新歌词信息
541        if (
542            !isSameMediaItem(
543                LyricManager.getLyricState()?.lyricParser?.musicItem,
544                musicItem,
545            )
546        ) {
547            DeviceEventEmitter.emit(EDeviceEvents.REFRESH_LYRIC, true);
548        }
549
550        // 5. 获取音源
551        let track: IMusic.IMusicItem;
552
553        // 5.1 通过插件获取音源
554        const plugin = PluginManager.getByName(musicItem.platform);
555        // 5.2 获取音质排序
556        const qualityOrder = getQualityOrder(
557            Config.get('setting.basic.defaultPlayQuality') ?? 'standard',
558            Config.get('setting.basic.playQualityOrder') ?? 'asc',
559        );
560        // 5.3 插件返回音源
561        let source: IPlugin.IMediaSourceResult | null = null;
562        for (let quality of qualityOrder) {
563            if (isCurrentMusic(musicItem)) {
564                source =
565                    (await plugin?.methods?.getMediaSource(
566                        musicItem,
567                        quality,
568                    )) ?? null;
569                // 5.3.1 获取到真实源
570                if (source) {
571                    setQuality(quality);
572                    break;
573                }
574            } else {
575                // 5.3.2 已经切换到其他歌曲了,
576                return;
577            }
578        }
579
580        if (!isCurrentMusic(musicItem)) {
581            return;
582        }
583        if (!source) {
584            // 如果有source
585            if (musicItem.source) {
586                for (let quality of qualityOrder) {
587                    if (musicItem.source[quality]?.url) {
588                        source = musicItem.source[quality]!;
589                        setQuality(quality);
590
591                        break;
592                    }
593                }
594            }
595            // 5.4 没有返回源
596            if (!source && !musicItem.url) {
597                // 插件失效的情况
598                if (Config.get('setting.basic.tryChangeSourceWhenPlayFail')) {
599                    // 重试
600                    const similarMusic = await getSimilarMusic(
601                        musicItem,
602                        'music',
603                        () => !isCurrentMusic(musicItem),
604                    );
605
606                    if (similarMusic) {
607                        const similarMusicPlugin =
608                            PluginManager.getByMedia(similarMusic);
609
610                        for (let quality of qualityOrder) {
611                            if (isCurrentMusic(musicItem)) {
612                                source =
613                                    (await similarMusicPlugin?.methods?.getMediaSource(
614                                        similarMusic,
615                                        quality,
616                                    )) ?? null;
617                                // 5.4.1 获取到真实源
618                                if (source) {
619                                    setQuality(quality);
620                                    break;
621                                }
622                            } else {
623                                // 5.4.2 已经切换到其他歌曲了,
624                                return;
625                            }
626                        }
627                    }
628
629                    if (!source) {
630                        throw new Error(PlayFailReason.INVALID_SOURCE);
631                    }
632                } else {
633                    throw new Error(PlayFailReason.INVALID_SOURCE);
634                }
635            } else {
636                source = {
637                    url: musicItem.url,
638                };
639                setQuality('standard');
640            }
641        }
642
643        // 6. 特殊类型源
644        if (getUrlExt(source.url) === '.m3u8') {
645            // @ts-ignore
646            source.type = 'hls';
647        }
648        // 7. 合并结果
649        track = mergeProps(musicItem, source) as IMusic.IMusicItem;
650
651        // 8. 新增历史记录
652        musicHistory.addMusic(musicItem);
653
654        trace('获取音源成功', track);
655        // 9. 设置音源
656        await setTrackSource(track as Track);
657
658        // 10. 获取补充信息
659        let info: Partial<IMusic.IMusicItem> | null = null;
660        try {
661            info = (await plugin?.methods?.getMusicInfo?.(musicItem)) ?? null;
662            if (
663                (typeof info?.url === 'string' && info.url.trim() === '') ||
664                (info?.url && typeof info.url !== 'string')
665            ) {
666                delete info.url;
667            }
668        } catch {}
669
670        // 11. 设置补充信息
671        if (info && isCurrentMusic(musicItem)) {
672            const mergedTrack = mergeProps(track, info);
673            currentMusicStore.setValue(mergedTrack as IMusic.IMusicItem);
674            await ReactNativeTrackPlayer.updateMetadataForTrack(
675                0,
676                mergedTrack as TrackMetadataBase,
677            );
678        }
679    } catch (e: any) {
680        const message = e?.message;
681        if (
682            message === 'The player is not initialized. Call setupPlayer first.'
683        ) {
684            await ReactNativeTrackPlayer.setupPlayer();
685            play(musicItem, forcePlay);
686        } else if (message === PlayFailReason.FORBID_CELLUAR_NETWORK_PLAY) {
687            if (getCurrentDialog()?.name !== 'SimpleDialog') {
688                showDialog('SimpleDialog', {
689                    title: '流量提醒',
690                    content:
691                        '当前非WIFI环境,侧边栏设置中打开【使用移动网络播放】功能后可继续播放',
692                });
693            }
694
695            // Toast.warn(
696            //     '当前禁止移动网络播放音乐,如需播放请去侧边栏-基本设置中修改',
697            // );
698        } else if (message === PlayFailReason.INVALID_SOURCE) {
699            trace('音源为空,播放失败');
700            await failToPlay();
701        } else if (message === PlayFailReason.PLAY_LIST_IS_EMPTY) {
702            // 队列是空的,不应该出现这种情况
703        }
704    }
705};
706
707/**
708 * 播放音乐,同时替换播放队列
709 * @param musicItem 音乐
710 * @param newPlayList 替代列表
711 */
712const playWithReplacePlayList = async (
713    musicItem: IMusic.IMusicItem,
714    newPlayList: IMusic.IMusicItem[],
715) => {
716    if (newPlayList.length !== 0) {
717        const now = Date.now();
718        if (newPlayList.length > maxMusicQueueLength) {
719            newPlayList = shrinkPlayListToSize(
720                newPlayList,
721                newPlayList.findIndex(it => isSameMediaItem(it, musicItem)),
722            );
723        }
724
725        newPlayList.forEach((it, index) => {
726            it[timeStampSymbol] = now;
727            it[sortIndexSymbol] = index;
728        });
729
730        setPlayList(
731            repeatModeStore.getValue() === MusicRepeatMode.SHUFFLE
732                ? shuffle(newPlayList)
733                : newPlayList,
734        );
735        await play(musicItem, true);
736    }
737};
738
739const skipToNext = async () => {
740    if (isPlayListEmpty()) {
741        setCurrentMusic(null);
742        return;
743    }
744
745    await play(getPlayListMusicAt(currentIndex + 1), true);
746};
747
748const skipToPrevious = async () => {
749    if (isPlayListEmpty()) {
750        setCurrentMusic(null);
751        return;
752    }
753
754    await play(
755        getPlayListMusicAt(currentIndex === -1 ? 0 : currentIndex - 1),
756        true,
757    );
758};
759
760/** 修改当前播放的音质 */
761const changeQuality = async (newQuality: IMusic.IQualityKey) => {
762    // 获取当前的音乐和进度
763    if (newQuality === qualityStore.getValue()) {
764        return true;
765    }
766
767    // 获取当前歌曲
768    const musicItem = currentMusicStore.getValue();
769    if (!musicItem) {
770        return false;
771    }
772    try {
773        const progress = await ReactNativeTrackPlayer.getProgress();
774        const plugin = PluginManager.getByMedia(musicItem);
775        const newSource = await plugin?.methods?.getMediaSource(
776            musicItem,
777            newQuality,
778        );
779        if (!newSource?.url) {
780            throw new Error(PlayFailReason.INVALID_SOURCE);
781        }
782        if (isCurrentMusic(musicItem)) {
783            const playingState = (
784                await ReactNativeTrackPlayer.getPlaybackState()
785            ).state;
786            await setTrackSource(
787                mergeProps(musicItem, newSource) as unknown as Track,
788                !musicIsPaused(playingState),
789            );
790
791            await ReactNativeTrackPlayer.seekTo(progress.position ?? 0);
792            setQuality(newQuality);
793        }
794        return true;
795    } catch {
796        // 修改失败
797        return false;
798    }
799};
800
801enum PlayFailReason {
802    /** 禁止移动网络播放 */
803    FORBID_CELLUAR_NETWORK_PLAY = 'FORBID_CELLUAR_NETWORK_PLAY',
804    /** 播放列表为空 */
805    PLAY_LIST_IS_EMPTY = 'PLAY_LIST_IS_EMPTY',
806    /** 无效源 */
807    INVALID_SOURCE = 'INVALID_SOURCE',
808    /** 非当前音乐 */
809}
810
811function useMusicState() {
812    const playbackState = usePlaybackState();
813
814    return playbackState.state;
815}
816
817function getPreviousMusic() {
818    const currentMusicItem = currentMusicStore.getValue();
819    if (!currentMusicItem) {
820        return null;
821    }
822
823    return getPlayListMusicAt(currentIndex - 1);
824}
825
826function getNextMusic() {
827    const currentMusicItem = currentMusicStore.getValue();
828    if (!currentMusicItem) {
829        return null;
830    }
831
832    return getPlayListMusicAt(currentIndex + 1);
833}
834
835const TrackPlayer = {
836    setupTrackPlayer,
837    usePlayList,
838    getPlayList,
839    addAll,
840    add,
841    addNext,
842    skipToNext,
843    skipToPrevious,
844    play,
845    playWithReplacePlayList,
846    pause,
847    remove,
848    clear,
849    useCurrentMusic: currentMusicStore.useValue,
850    getCurrentMusic: currentMusicStore.getValue,
851    useRepeatMode: repeatModeStore.useValue,
852    getRepeatMode: repeatModeStore.getValue,
853    toggleRepeatMode,
854    usePlaybackState,
855    getProgress: ReactNativeTrackPlayer.getProgress,
856    useProgress: useProgress,
857    seekTo: ReactNativeTrackPlayer.seekTo,
858    changeQuality,
859    useCurrentQuality: qualityStore.useValue,
860    getCurrentQuality: qualityStore.getValue,
861    getRate: ReactNativeTrackPlayer.getRate,
862    setRate: ReactNativeTrackPlayer.setRate,
863    useMusicState,
864    reset: ReactNativeTrackPlayer.reset,
865    getPreviousMusic,
866    getNextMusic,
867};
868
869export default TrackPlayer;
870export {MusicRepeatMode, State as MusicState};
871