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