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