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