xref: /MusicFree/src/core/trackPlayer/index.ts (revision 15900d057ad4df766b2f9ea5b48f92a8ce2664db)
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';
46
47/** 当前播放 */
48const currentMusicStore = new GlobalState<IMusic.IMusicItem | null>(null);
49
50/** 播放模式 */
51const repeatModeStore = new GlobalState<MusicRepeatMode>(MusicRepeatMode.QUEUE);
52
53/** 音质 */
54const qualityStore = new GlobalState<IMusic.IQualityKey>('standard');
55
56let currentIndex = -1;
57
58// TODO: 下个版本最大限制调大一些
59const maxMusicQueueLength = 1500; // 当前播放最大限制
60const halfMaxMusicQueueLength = Math.floor(maxMusicQueueLength / 2);
61const shrinkPlayListToSize = (
62    queue: IMusic.IMusicItem[],
63    targetIndex = currentIndex,
64) => {
65    // 播放列表上限,太多无法缓存状态
66    if (queue.length > maxMusicQueueLength) {
67        if (targetIndex < halfMaxMusicQueueLength) {
68            queue = queue.slice(0, maxMusicQueueLength);
69        } else {
70            const right = Math.min(
71                queue.length,
72                targetIndex + halfMaxMusicQueueLength,
73            );
74            const left = Math.max(0, right - maxMusicQueueLength);
75            queue = queue.slice(left, right);
76        }
77    }
78    return queue;
79};
80
81async function setupTrackPlayer() {
82    const config = Config.get('status.music') ?? {};
83    console.log('config!!', config);
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        currentMusicStore.setValue(null);
380        currentIndex = -1;
381    }
382
383    currentMusicStore.setValue(musicItem!);
384    currentIndex = getMusicIndex(musicItem);
385};
386
387/**
388 * 播放
389 *
390 * 当musicItem 为空时,代表暂停/播放
391 *
392 * @param musicItem
393 * @param forcePlay
394 * @returns
395 */
396const play = async (
397    musicItem?: IMusic.IMusicItem | null,
398    forcePlay?: boolean,
399) => {
400    try {
401        if (!musicItem) {
402            musicItem = currentMusicStore.getValue();
403        }
404        if (!musicItem) {
405            throw new Error(PlayFailReason.PLAY_LIST_IS_EMPTY);
406        }
407        // 1. 移动网络禁止播放
408        if (
409            Network.isCellular() &&
410            !Config.get('setting.basic.useCelluarNetworkPlay') &&
411            !LocalMusicSheet.isLocalMusic(musicItem)
412        ) {
413            await ReactNativeTrackPlayer.reset();
414            throw new Error(PlayFailReason.FORBID_CELLUAR_NETWORK_PLAY);
415        }
416
417        // 2. 如果是当前正在播放的音频
418        if (isCurrentMusic(musicItem)) {
419            const currentTrack = await ReactNativeTrackPlayer.getTrack(0);
420            // 2.1 如果当前有源
421            if (
422                currentTrack?.url &&
423                isSameMediaItem(musicItem, currentTrack as IMusic.IMusicItem)
424            ) {
425                const currentActiveIndex =
426                    await ReactNativeTrackPlayer.getActiveTrackIndex();
427                if (currentActiveIndex !== 0) {
428                    await ReactNativeTrackPlayer.skip(0);
429                }
430                if (forcePlay) {
431                    // 2.1.1 强制重新开始
432                    await ReactNativeTrackPlayer.seekTo(0);
433                } else if (
434                    (await ReactNativeTrackPlayer.getPlaybackState()).state !==
435                    State.Playing
436                ) {
437                    // 2.1.2 恢复播放
438                    await ReactNativeTrackPlayer.play();
439                }
440                // 这种情况下,播放队列和当前歌曲都不需要变化
441                return;
442            }
443            // 2.2 其他情况:重新获取源
444        }
445
446        // 3. 如果没有在播放列表中,添加到队尾;同时更新列表状态
447        const inPlayList = isInPlayList(musicItem);
448        if (!inPlayList) {
449            add(musicItem);
450        }
451
452        // 4. 更新列表状态和当前音乐
453        setCurrentMusic(musicItem);
454
455        // 5. 获取音源
456        let track: IMusic.IMusicItem;
457
458        // 5.1 通过插件获取音源
459        const plugin = PluginManager.getByName(musicItem.platform);
460        // 5.2 获取音质排序
461        const qualityOrder = getQualityOrder(
462            Config.get('setting.basic.defaultPlayQuality') ?? 'standard',
463            Config.get('setting.basic.playQualityOrder') ?? 'asc',
464        );
465        // 5.3 插件返回音源
466        let source: IPlugin.IMediaSourceResult | null = null;
467        for (let quality of qualityOrder) {
468            if (isCurrentMusic(musicItem)) {
469                source =
470                    (await plugin?.methods?.getMediaSource(
471                        musicItem,
472                        quality,
473                    )) ?? null;
474                // 5.3.1 获取到真实源
475                if (source) {
476                    qualityStore.setValue(quality);
477                    break;
478                }
479            } else {
480                // 5.3.2 已经切换到其他歌曲了,
481                return;
482            }
483        }
484
485        if (!isCurrentMusic(musicItem)) {
486            return;
487        }
488
489        if (!source) {
490            // 5.4 没有返回源
491            if (!musicItem.url) {
492                throw new Error(PlayFailReason.INVALID_SOURCE);
493            }
494            source = {
495                url: musicItem.url,
496            };
497            qualityStore.setValue('standard');
498        }
499
500        // 6. 特殊类型源
501        if (getUrlExt(source.url) === '.m3u8') {
502            // @ts-ignore
503            source.type = 'hls';
504        }
505        // 7. 合并结果
506        track = mergeProps(musicItem, source) as IMusic.IMusicItem;
507
508        // 8. 新增历史记录
509        musicHistory.addMusic(musicItem);
510
511        // 9. 设置音源
512        await setTrackSource(track as Track);
513
514        // 10. 获取补充信息
515        let info: Partial<IMusic.IMusicItem> | null = null;
516        try {
517            info = (await plugin?.methods?.getMusicInfo?.(musicItem)) ?? null;
518        } catch {}
519
520        // 11. 设置补充信息
521        if (info && isCurrentMusic(musicItem)) {
522            const mergedTrack = mergeProps(track, info);
523            currentMusicStore.setValue(mergedTrack as IMusic.IMusicItem);
524            await ReactNativeTrackPlayer.updateMetadataForTrack(
525                0,
526                mergedTrack as TrackMetadataBase,
527            );
528        }
529
530        // 12. 刷新歌词信息
531        if (
532            !isSameMediaItem(
533                LyricManager.getLyricState()?.lyricParser?.getCurrentMusicItem?.(),
534                musicItem,
535            )
536        ) {
537            DeviceEventEmitter.emit(EDeviceEvents.REFRESH_LYRIC, true);
538        }
539    } catch (e: any) {
540        const message = e?.message;
541        if (
542            message === 'The player is not initialized. Call setupPlayer first.'
543        ) {
544            await ReactNativeTrackPlayer.setupPlayer();
545            play(musicItem, forcePlay);
546        } else if (message === PlayFailReason.FORBID_CELLUAR_NETWORK_PLAY) {
547        } else if (message === PlayFailReason.INVALID_SOURCE) {
548            await failToPlay('无效源');
549        } else if (message === PlayFailReason.PLAY_LIST_IS_EMPTY) {
550            // 队列是空的,不应该出现这种情况
551        }
552    }
553};
554
555/**
556 * 播放音乐,同时替换播放队列
557 * @param musicItem 音乐
558 * @param newPlayList 替代列表
559 */
560const playWithReplacePlayList = async (
561    musicItem: IMusic.IMusicItem,
562    newPlayList: IMusic.IMusicItem[],
563) => {
564    if (newPlayList.length !== 0) {
565        const now = Date.now();
566        if (newPlayList.length > maxMusicQueueLength) {
567            newPlayList = shrinkPlayListToSize(
568                newPlayList,
569                newPlayList.findIndex(it => isSameMediaItem(it, musicItem)),
570            );
571        }
572        const playListItems = newPlayList.map((item, index) =>
573            produce(item, draft => {
574                draft[timeStampSymbol] = now;
575                draft[sortIndexSymbol] = index;
576            }),
577        );
578        setPlayList(
579            repeatModeStore.getValue() === MusicRepeatMode.SHUFFLE
580                ? shuffle(playListItems)
581                : playListItems,
582        );
583        await play(musicItem, true);
584    }
585};
586
587const skipToNext = async (reason?: string) => {
588    console.log(
589        'SkipToNext',
590        reason,
591        await ReactNativeTrackPlayer.getActiveTrack(),
592    );
593    if (isPlayListEmpty()) {
594        setCurrentMusic(null);
595        return;
596    }
597
598    await play(getPlayListMusicAt(currentIndex + 1), true);
599};
600
601const skipToPrevious = async () => {
602    if (isPlayListEmpty()) {
603        setCurrentMusic(null);
604        return;
605    }
606
607    await play(getPlayListMusicAt(currentIndex === -1 ? 0 : currentIndex - 1));
608};
609
610/** 修改当前播放的音质 */
611const changeQuality = async (newQuality: IMusic.IQualityKey) => {
612    // 获取当前的音乐和进度
613    if (newQuality === qualityStore.getValue()) {
614        return true;
615    }
616
617    // 获取当前歌曲
618    const musicItem = currentMusicStore.getValue();
619    if (!musicItem) {
620        return false;
621    }
622    try {
623        const progress = await ReactNativeTrackPlayer.getProgress();
624        const plugin = PluginManager.getByMedia(musicItem);
625        const newSource = await plugin?.methods?.getMediaSource(
626            musicItem,
627            newQuality,
628        );
629        if (!newSource?.url) {
630            throw new Error(PlayFailReason.INVALID_SOURCE);
631        }
632        if (isCurrentMusic(musicItem)) {
633            const playingState = (
634                await ReactNativeTrackPlayer.getPlaybackState()
635            ).state;
636            await setTrackSource(
637                mergeProps(musicItem, newSource) as unknown as Track,
638                !musicIsPaused(playingState),
639            );
640
641            await ReactNativeTrackPlayer.seekTo(progress.position ?? 0);
642            qualityStore.setValue(newQuality);
643        }
644        return true;
645    } catch {
646        // 修改失败
647        return false;
648    }
649};
650
651enum PlayFailReason {
652    /** 禁止移动网络播放 */
653    FORBID_CELLUAR_NETWORK_PLAY = 'FORBID_CELLUAR_NETWORK_PLAY',
654    /** 播放列表为空 */
655    PLAY_LIST_IS_EMPTY = 'PLAY_LIST_IS_EMPTY',
656    /** 无效源 */
657    INVALID_SOURCE = 'INVALID_SOURCE',
658    /** 非当前音乐 */
659}
660
661function useMusicState() {
662    const playbackState = usePlaybackState();
663
664    return playbackState.state;
665}
666
667const TrackPlayer = {
668    setupTrackPlayer,
669    usePlayList,
670    getPlayList,
671    addAll,
672    add,
673    addNext,
674    skipToNext,
675    skipToPrevious,
676    play,
677    playWithReplacePlayList,
678    pause,
679    remove,
680    clear,
681    useCurrentMusic: currentMusicStore.useValue,
682    getCurrentMusic: currentMusicStore.getValue,
683    useRepeatMode: repeatModeStore.useValue,
684    getRepeatMode: repeatModeStore.getValue,
685    toggleRepeatMode,
686    usePlaybackState,
687    getProgress: ReactNativeTrackPlayer.getProgress,
688    useProgress: useProgress,
689    seekTo: ReactNativeTrackPlayer.seekTo,
690    changeQuality,
691    useCurrentQuality: qualityStore.useValue,
692    getCurrentQuality: qualityStore.getValue,
693    getRate: ReactNativeTrackPlayer.getRate,
694    setRate: ReactNativeTrackPlayer.setRate,
695    useMusicState,
696    reset: ReactNativeTrackPlayer.reset,
697};
698
699export default TrackPlayer;
700export {MusicRepeatMode, State as MusicState};
701