From 1ab41f33c41c2f4d7f6ed38b24d14155994bc652 Mon Sep 17 00:00:00 2001 From: canxin <69547456+canxin121@users.noreply.github.com> Date: Thu, 16 May 2024 02:01:29 +0800 Subject: [PATCH] [Refactor] Lazy Load Music (#5) * [Refactor] LazyLoad Music. * [Fix] Reduce updatePlayingMusic. * [Change] chore change * [Fix] Replace Music. --- assets/blank.mp3 | Bin 0 -> 60543 bytes assets/nature.mp3 | Bin 40491 -> 0 bytes lib/comp/play_page_comp/lyric.dart | 7 +- lib/comp/play_page_comp/music_list.dart | 22 +- lib/comp/play_page_comp/quality_time.dart | 13 +- lib/page/in_music_album.dart | 20 +- lib/page/in_music_list.dart | 122 ++++--- lib/page/in_search_music_list.dart | 14 +- lib/page/playing_music_page.dart | 4 +- lib/page/search_page.dart | 14 +- lib/types/lazy_audio_source.dart | 76 +++++ lib/types/music.dart | 220 ++++++------- lib/types/play_music_queue.dart | 62 ++-- lib/util/advanced_music_sdk.dart | 8 +- lib/util/audio_controller.dart | 369 ++++++++++++---------- lib/util/helper.dart | 4 +- lib/util/pull_down_selection.dart | 2 +- pubspec.lock | 2 +- pubspec.yaml | 1 + 19 files changed, 526 insertions(+), 434 deletions(-) create mode 100644 assets/blank.mp3 delete mode 100644 assets/nature.mp3 create mode 100644 lib/types/lazy_audio_source.dart diff --git a/assets/blank.mp3 b/assets/blank.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..e333fc78100464450cea44d70a396e67e248760e GIT binary patch literal 60543 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
)MbV+IXV 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 O7ytnDfB)Too%ufsJfe&M diff --git a/lib/comp/play_page_comp/lyric.dart b/lib/comp/play_page_comp/lyric.dart index bd51a83..9e247f3 100644 --- a/lib/comp/play_page_comp/lyric.dart +++ b/lib/comp/play_page_comp/lyric.dart @@ -22,7 +22,7 @@ class LyricDisplayState extends State { var lyricModel = LyricsModelBuilder.create().bindLyricToMain("[00:00.00]无歌词").getModel(); var lyricUI = UINetease(lyricAlign: LyricAlign.CENTER, highlight: true); - late StreamSubscription stream; + late StreamSubscription stream; @override void initState() { super.initState(); @@ -70,7 +70,7 @@ class LyricDisplayState extends State { globalAudioHandler.seek(toSeek).then((value) { confirm.call(); // 这里是考虑到在暂停状态下。需要开启播放 - if (!globalAudioHandler.isPlaying()) { + if (!globalAudioHandler.isPlaying) { globalAudioHandler.play(); } }); @@ -87,7 +87,8 @@ class LyricDisplayState extends State { ), Text( formatDuration(Duration(milliseconds: progress).inSeconds), - style: const TextStyle(color: CupertinoColors.white).useSystemChineseFont(), + style: const TextStyle(color: CupertinoColors.white) + .useSystemChineseFont(), ) ], ); diff --git a/lib/comp/play_page_comp/music_list.dart b/lib/comp/play_page_comp/music_list.dart index 8f8aba3..e913458 100644 --- a/lib/comp/play_page_comp/music_list.dart +++ b/lib/comp/play_page_comp/music_list.dart @@ -15,12 +15,12 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pull_down_button/pull_down_button.dart'; -// PlayMusicList组件 -class PlayMusicList extends StatelessWidget { +// MusicList组件 +class MusicListComp extends StatelessWidget { final double maxHeight; final EdgeInsets picPadding; final double itemHeight; - const PlayMusicList( + const MusicListComp( {super.key, required this.maxHeight, required this.picPadding, @@ -29,7 +29,7 @@ class PlayMusicList extends StatelessWidget { @override Widget build(BuildContext context) { return Obx(() { - var musics = globalAudioHandler.playMusicList; + var musics = globalAudioHandler.musicList; return Container( constraints: BoxConstraints(maxHeight: maxHeight), @@ -77,7 +77,7 @@ class PlayMusicList extends StatelessWidget { // 播放展示界面的列表中的音乐卡片的长按触发操作 List displayListMusicCardPullDown( BuildContext context, - PlayMusic music, + Music music, Future Function() onDelete, Rect position, ) => @@ -143,11 +143,8 @@ List displayListMusicCardPullDown( if (context.mounted) { await showPullDownMenu( context: context, - items: addToMusicListPullDown( - context, - musicLists, - Future.value([DisplayMusic(music.ref, info_: music.info)]), - position), + items: addToMusicListPullDown(context, musicLists, + Future.value([Music(music.ref)]), position), position: position); } }, @@ -181,9 +178,8 @@ List displayListMusicCardPullDown( ), PullDownMenuItem( onTap: () async { - globalTopUiController.updateWidget(InMusicAlbumListPage( - key: UniqueKey(), - music: DisplayMusic(music.ref, info_: music.info))); + globalTopUiController.updateWidget( + InMusicAlbumListPage(key: UniqueKey(), music: Music(music.ref))); }, title: "查看专辑", icon: CupertinoIcons.music_albums, diff --git a/lib/comp/play_page_comp/quality_time.dart b/lib/comp/play_page_comp/quality_time.dart index 5afa86b..5ce295f 100644 --- a/lib/comp/play_page_comp/quality_time.dart +++ b/lib/comp/play_page_comp/quality_time.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:app_rhyme/src/rust/api/mirror.dart'; -import 'package:app_rhyme/types/music.dart'; import 'package:app_rhyme/util/helper.dart'; import 'package:app_rhyme/util/audio_controller.dart'; import 'package:app_rhyme/util/time_parse.dart'; @@ -49,16 +48,8 @@ class QualityTimeState extends State { context: context, items: qualitySelectPullDown(context, qualityOptions, (selectQuality) async { - var playingMusic = globalAudioHandler.playingMusic.value; - if (playingMusic != null) { - var displayData = DisplayMusic(playingMusic.ref, - info_: playingMusic.info); - var newPlayMusic = - await display2PlayMusic(displayData, selectQuality); - if (newPlayMusic == null) return; - await globalAudioHandler - .replacePlayingMusic(newPlayMusic); - } + await globalAudioHandler + .replacePlayingMusic(selectQuality); }), position: details.globalPosition & Size.zero); } diff --git a/lib/page/in_music_album.dart b/lib/page/in_music_album.dart index 4efe07e..e8234eb 100644 --- a/lib/page/in_music_album.dart +++ b/lib/page/in_music_album.dart @@ -22,7 +22,7 @@ import 'package:pull_down_button/pull_down_button.dart'; import 'package:toastification/toastification.dart'; class InMusicAlbumListPage extends StatefulWidget { - final DisplayMusic music; + final Music music; const InMusicAlbumListPage({ super.key, required this.music, @@ -35,7 +35,7 @@ class InMusicAlbumListPage extends StatefulWidget { class InMusicAlbumListPageState extends State { MusicList musicList = const MusicList(name: "Album", artPic: "", desc: ""); var allowEmptyTime = 3; - var pagingController = PagingController(firstPageKey: 1); + var pagingController = PagingController(firstPageKey: 1); @override void initState() { @@ -64,13 +64,13 @@ class InMusicAlbumListPageState extends State { musicList = musicList_; }); } - var newMusics = newMusicsRefs.map((e) => DisplayMusic(e)); - List uniqueItems = []; + var newMusics = newMusicsRefs.map((e) => Music(e)); + List uniqueItems = []; if (pagingController.value.itemList != null) { for (var newMusic in newMusics) { bool exist = false; - for (DisplayMusic existItem in pagingController.value.itemList!) { + for (Music existItem in pagingController.value.itemList!) { if (existItem.info.name == newMusic.info.name && existItem.info.artist.join(",") == newMusic.info.artist.join(",")) { @@ -198,7 +198,7 @@ class InMusicAlbumListPageState extends State { music: displayMusic, onClick: () { globalAudioHandler.addMusicPlay( - displayMusic as DisplayMusic, + displayMusic as Music, ); }, onPress: (details) async { @@ -206,7 +206,7 @@ class InMusicAlbumListPageState extends State { await showPullDownMenu( context: context, items: inMusicAlbumMusicCardPullDown( - context, displayMusic as DisplayMusic, position), + context, displayMusic as Music, position), position: position); }, ), @@ -253,7 +253,7 @@ Widget _buildButton(BuildContext context, // 专辑界面的音乐卡片的长按触发操作 List inMusicAlbumMusicCardPullDown( BuildContext context, - DisplayMusic music, + Music music, Rect position, ) => [ @@ -349,7 +349,7 @@ List musicAlbumActionPullDown( BuildContext context, Future Function() fetchAllMusic, MusicList musiclist, - PagingController pagingController, + PagingController pagingController, Rect position, ) => [ @@ -448,7 +448,7 @@ List musicAlbumActionPullDown( if (pagingController.itemList != null) { return pagingController.itemList!; } - }(), // 这里传递Future> + }(), // 这里传递Future> position), position: position); } diff --git a/lib/page/in_music_list.dart b/lib/page/in_music_list.dart index 7b21f62..c41a05a 100644 --- a/lib/page/in_music_list.dart +++ b/lib/page/in_music_list.dart @@ -20,7 +20,7 @@ import 'package:pull_down_button/pull_down_button.dart'; class InMusicListPage extends StatefulWidget { final MusicList musicList; - final Future>? musicsFuture; + final Future>? musicsFuture; const InMusicListPage({ super.key, @@ -34,7 +34,7 @@ class InMusicListPage extends StatefulWidget { class InMusicListPageState extends State { late MusicList musicList; - late Future> musicsFuture; + late Future> musicsFuture; @override void initState() { @@ -43,15 +43,16 @@ class InMusicListPageState extends State { musicsFuture = widget.musicsFuture ?? getMusicsFromSQL(); } - Future> getMusicsFromSQL() async { + Future> getMusicsFromSQL() async { var results = await globalSqlMusicFactory.readMusic(musicList: musicList); - return results.map((m) => DisplayMusic(m)).toList(); + return results.map((m) => Music(m)).toList(); } @override Widget build(BuildContext context) { double screenWidth = MediaQuery.of(context).size.width; - List musics = []; + List musics = []; + List> hasCache = []; return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( padding: const EdgeInsetsDirectional.only(end: 16), @@ -70,8 +71,12 @@ class InMusicListPageState extends State { onTapDown: (details) { showPullDownMenu( context: context, - items: - musicListActionPullDown(context, musics, getMusicsFromSQL), + items: musicListActionPullDown( + context, + musics, + (index, hasCache_) => setState(() { + hasCache[index] = Future.value(hasCache_); + })), position: details.globalPosition & Size.zero); }, ), @@ -132,7 +137,7 @@ class InMusicListPageState extends State { height: 1, ), ), - FutureBuilder>( + FutureBuilder>( future: musicsFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { @@ -145,6 +150,7 @@ class InMusicListPageState extends State { ); } else if (snapshot.hasData) { musics = snapshot.data!; + hasCache = musics.map((e) => e.hasCache()).toList(); return SliverList( delegate: SliverChildBuilderDelegate( (context, index) { @@ -157,48 +163,49 @@ class InMusicListPageState extends State { music, ); }, - hasCache: music.hasCache(), + hasCache: hasCache[index], onPress: (details) async { await showPullDownMenu( context: context, items: inListMusicCardPullDown(context, music, () async { // 缓存 - var playMusic = await display2PlayMusic(music); - if (playMusic == null) return; - cacheFile( - file: playMusic.playInfo.file, - cachePath: musicCachePath, - filename: playMusic.toCacheFileName()) - .then((file) { - // 下载完成之后设置本地路径为新的播放文件 - playMusic.playInfo.file = file; + var index = globalFloatWidgetContoller + .addMsg("缓存音乐: ${music.info.name}"); + try { + if (await music.hasCache()) return; + var playInfo = await music.getPlayInfo(); + if (playInfo == null) return; + await cacheFile( + file: playInfo.file, + cachePath: musicCachePath, + filename: music.toCacheFileName()); // 如果这首歌正在播放列表中,替换他,防止继续在线播放 - globalAudioHandler.replaceMusic(playMusic); + globalAudioHandler.replaceMusic(music); // 在这里需要重新判断是否 hasCache,所以直接setState解决 - setState(() {}); - }); + setState(() { + hasCache[index] = Future.value(true); + }); + } finally { + globalFloatWidgetContoller.delMsg(index); + } }, () async { // 删除缓存 if (music.info.defaultQuality == null) return; - var result = music.toCacheFileNameAndExtra( - music.info.defaultQuality!); - if (result == null) return; - var (cacheFileName, _) = result; + var cacheFileName = music.toCacheFileName(); deleteCacheFile( file: "", cachePath: musicCachePath, filename: cacheFileName) .then((value) { // 删除缓存后刷新是否有缓存 - setState(() {}); + setState(() { + hasCache[index] = Future.value(false); + }); if (kDebugMode) { print("成功删除缓存:${music.info.name}"); } - display2PlayMusic(music).then((value) { - if (value == null) return; - globalAudioHandler.replaceMusic(value); - }); + globalAudioHandler.replaceMusic(music); }); }, () async { // 删除音乐 @@ -207,6 +214,7 @@ class InMusicListPageState extends State { ids: Int64List.fromList([music.info.id])); setState(() { musics.removeAt(index); + hasCache.removeAt(index); }); }, () async { // 编辑音乐 @@ -289,7 +297,7 @@ Widget _buildButton(BuildContext context, // 在自定义歌单内的音乐卡片的长按触发操作 List inListMusicCardPullDown( BuildContext context, - DisplayMusic music, + Music music, Future Function() onSave, Future Function() onUnSave, Future Function() onDelete, @@ -396,23 +404,33 @@ List inListMusicCardPullDown( // 资料库界面自定义歌单内部右上角的编辑触发的的长按触发操作 List musicListActionPullDown( BuildContext context, - List displayMusics, - void Function() refresh, + List displayMusics, + void Function(int index, bool hasCache) refresh, ) => [ PullDownMenuItem( title: '全部缓存', onTap: () async { + var i = 0; for (var music in displayMusics) { - if (await music.hasCache()) continue; - var playMusic = await display2PlayMusic(music); - if (playMusic != null) { - await cacheFile( - file: playMusic.playInfo.file, - cachePath: musicCachePath, - filename: playMusic.toCacheFileName()); + var index = + globalFloatWidgetContoller.addMsg("缓存歌曲: ${music.info.name}"); + try { + if (await music.hasCache()) continue; + var playInfo = await music.getPlayInfo(); + if (playInfo != null) { + await cacheFile( + file: playInfo.file, + cachePath: musicCachePath, + filename: + music.toCacheFileName(quality_: playInfo.quality)); + await globalAudioHandler.replaceMusic(music); + refresh(i, true); + } + } finally { + i++; + globalFloatWidgetContoller.delMsg(index); } - refresh(); } }, icon: CupertinoIcons.cloud_download, @@ -420,15 +438,21 @@ List musicListActionPullDown( PullDownMenuItem( title: '删除所有缓存', onTap: () async { + var i = 0; for (var music in displayMusics) { - if (music.info.defaultQuality != null) { - var result = - music.toCacheFileNameAndExtra(music.info.defaultQuality!); - if (result == null) return; - var (cacheFileName, _) = result; - await deleteCacheFile( - file: "", cachePath: musicCachePath, filename: cacheFileName); - refresh(); + try { + if (music.info.defaultQuality != null) { + if (!await music.hasCache()) continue; + var cacheFileName = music.toCacheFileName(); + await deleteCacheFile( + file: "", + cachePath: musicCachePath, + filename: cacheFileName); + await globalAudioHandler.replaceMusic(music); + refresh(i, false); + } + } finally { + i++; } } }, diff --git a/lib/page/in_search_music_list.dart b/lib/page/in_search_music_list.dart index d98a869..f9467e6 100644 --- a/lib/page/in_search_music_list.dart +++ b/lib/page/in_search_music_list.dart @@ -39,7 +39,7 @@ class InSearchMusicListPage extends StatefulWidget { class InSearchMusicListPageState extends State { late MusicList musicList; var allowEmptyTime = 3; - var pagingController = PagingController(firstPageKey: 1); + var pagingController = PagingController(firstPageKey: 1); @override void initState() { @@ -57,14 +57,14 @@ class InSearchMusicListPageState extends State { source: 'KuWo', payload: widget.payload, )) - .map((i) => DisplayMusic(i)); + .map((i) => Music(i)); - List uniqueItems = []; + List uniqueItems = []; if (pagingController.value.itemList != null) { for (var newItem in newItems) { bool exist = false; - for (DisplayMusic existItem in pagingController.value.itemList!) { + for (Music existItem in pagingController.value.itemList!) { if (existItem.info.name == newItem.info.name && existItem.info.artist.join(",") == newItem.info.artist.join(",")) { @@ -192,7 +192,7 @@ class InSearchMusicListPageState extends State { music: displayMusic, onClick: () { globalAudioHandler.addMusicPlay( - displayMusic as DisplayMusic, + displayMusic as Music, ); }, onPress: (details) async { @@ -200,7 +200,7 @@ class InSearchMusicListPageState extends State { await showPullDownMenu( context: context, items: searchMusicCardPullDown( - context, displayMusic as DisplayMusic, position), + context, displayMusic as Music, position), position: position); }, ), @@ -247,7 +247,7 @@ Widget _buildButton(BuildContext context, // 搜索界面的音乐卡片的长按触发操作 List searchMusicCardPullDown( BuildContext context, - DisplayMusic music, + Music music, Rect position, ) => [ diff --git a/lib/page/playing_music_page.dart b/lib/page/playing_music_page.dart index 632d10a..9f3476f 100644 --- a/lib/page/playing_music_page.dart +++ b/lib/page/playing_music_page.dart @@ -86,7 +86,7 @@ class SongDisplayPageState extends State { ), ), // 应当占据剩下的所有高度 - PlayMusicList( + MusicListComp( maxHeight: Platform.isIOS ? screenHeight * 0.87 - 350 : screenHeight * 0.87 - 300, @@ -152,7 +152,7 @@ class SongDisplayPageState extends State { const MusicInfo( titleHeight: 20, artistHeight: 16, - padding: EdgeInsets.only(left: 40), + padding: EdgeInsets.only(left: 40, right: 40), ), // 占据约35的高度 const ProgressSlider( diff --git a/lib/page/search_page.dart b/lib/page/search_page.dart index 324e706..0f6a952 100644 --- a/lib/page/search_page.dart +++ b/lib/page/search_page.dart @@ -120,16 +120,16 @@ class SearchController extends GetxController { source: 'KuWo', ); - List newItems = []; + List newItems = []; for (MusicW result in results) { - newItems.add(DisplayMusic(result)); + newItems.add(Music(result)); } - List uniqueItems = []; + List uniqueItems = []; if (pagingController.value.itemList != null) { - for (DisplayMusic newItem in newItems) { + for (Music newItem in newItems) { bool exist = false; - for (DisplayMusic existItem in pagingController.value.itemList!) { + for (Music existItem in pagingController.value.itemList!) { if (existItem.info.name == newItem.info.name && existItem.info.artist.join("") == newItem.info.artist.join("")) { @@ -242,7 +242,7 @@ class SearchPage extends StatelessWidget { music: displayMusic, onClick: () { globalAudioHandler.addMusicPlay( - displayMusic as DisplayMusic, + displayMusic as Music, ); }, onPress: (details) async { @@ -250,7 +250,7 @@ class SearchPage extends StatelessWidget { await showPullDownMenu( context: context, items: searchMusicCardPullDown( - context, displayMusic as DisplayMusic, position), + context, displayMusic as Music, position), position: position); }, ), diff --git a/lib/types/lazy_audio_source.dart b/lib/types/lazy_audio_source.dart new file mode 100644 index 0000000..a65b5c3 --- /dev/null +++ b/lib/types/lazy_audio_source.dart @@ -0,0 +1,76 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:just_audio/just_audio.dart'; + +typedef ResolveSoundUrl = Future Function(String uniquidId); + +class ResolvingAudioSource extends StreamAudioSource { + final String uniqueId; + final ResolveSoundUrl resolveSoundUrl; + final Map? headers; + + var _hasRequestedSoundUrl = false; + final _soundUrlCompleter = Completer(); + + Future get _soundUrl => _soundUrlCompleter.future; + + HttpClient? _httpClient; + + HttpClient get httpClient => _httpClient ?? (_httpClient = HttpClient()); + + ResolvingAudioSource( + {required this.uniqueId, + required this.resolveSoundUrl, + this.headers, + super.tag}); + + @override + Future request([int? start, int? end]) async { + if (!_hasRequestedSoundUrl) { + _hasRequestedSoundUrl = true; + final soundUrl = await resolveSoundUrl(uniqueId); + _soundUrlCompleter.complete(soundUrl); + } + final soundUrl = await _soundUrl; + if (soundUrl == null) { + return StreamAudioResponse( + sourceLength: null, + contentLength: null, + offset: null, + stream: const Stream.empty(), + contentType: ''); + } + final request = await httpClient.getUrl(soundUrl); + for (var entry in headers?.entries ?? >[]) { + request.headers.set(entry.key, entry.value); + } + if (start != null || end != null) { + request.headers + .set(HttpHeaders.rangeHeader, 'bytes=${start ?? ""}-${end ?? ""}'); + } + final response = await request.close(); + final acceptRangesHeader = + response.headers.value(HttpHeaders.acceptRangesHeader); + final contentRange = response.headers.value(HttpHeaders.contentRangeHeader); + int? offset; + if (contentRange != null) { + int offsetEnd = contentRange.indexOf('-'); + if (offsetEnd >= 6) { + offset = int.tryParse(contentRange.substring(6, offsetEnd)); + } + } + final contentLength = + response.headers.value(HttpHeaders.contentLengthHeader); + final contentType = response.headers.value(HttpHeaders.contentTypeHeader); + return StreamAudioResponse( + rangeRequestsSupported: + acceptRangesHeader != null && acceptRangesHeader != 'none', + sourceLength: null, + contentLength: + contentLength == null ? null : int.tryParse(contentLength), + offset: offset, + stream: response.asBroadcastStream(), + contentType: contentType ?? ""); + } +} diff --git a/lib/types/music.dart b/lib/types/music.dart index 61acd14..f74abbe 100644 --- a/lib/types/music.dart +++ b/lib/types/music.dart @@ -6,103 +6,9 @@ import 'package:app_rhyme/src/rust/api/mirror.dart'; import 'package:app_rhyme/src/rust/api/music_sdk.dart'; import 'package:app_rhyme/util/default.dart'; import 'package:audio_service/audio_service.dart'; +import 'package:get/get.dart'; import 'package:just_audio/just_audio.dart'; -// 是一个原本只具有展示功能的DisplayMusicTuple通过请求第三方api变成可以播放的音乐 -// 这个过程已经决定了一个音乐是否可以播放,因此本函数应该可能throw Exception -Future display2PlayMusic(DisplayMusic music, - [Quality? quality]) async { - late Quality finalQuality; - if (quality != null) { - finalQuality = quality; - } else { - if (music.info.defaultQuality != null) { - finalQuality = music.info.defaultQuality!; - } else if (music.info.qualities.isNotEmpty) { - finalQuality = music.info.qualities[0]; - talker.info("[Display2PlayMusic] 音乐无默认音质,选择音质中第一个进行播放:$finalQuality"); - } else { - talker.error("[Display2PlayMusic] 音乐没有可供播放的音质"); - return null; - } - } - - // 音乐缓存获取的逻辑 - var result = music.toCacheFileNameAndExtra(finalQuality); - if (result == null) { - return null; - } - var (cacheFileName, extra) = result; - - // 尝试获取本地缓存 - var cache = await useCacheFile( - file: "", cachePath: musicCachePath, filename: cacheFileName); - - // 有本地缓存直接返回 - if (cache != null) { - talker.info("[Display2PlayMusic] 使用本地歌曲缓存转化歌曲: ${music.info.name}"); - return PlayMusic(music.ref, music.info, PlayInfo(cache, finalQuality), - music.ref.getExtraInto(quality: finalQuality)); - } - - // 没有本地缓存,也没有第三方api,直接返回null - if (globalExternApi == null) { - talker.error("[Display2PlayMusic] 无第三方音乐源,无法获取播放信息"); - } - - // 有第三方api,使用api进行请求 - var playinfo = - await globalExternApi!.getMusicPlayInfo(music.info.source, extra); - - // 如果第三方api查找不到,直接返回null - if (playinfo == null) { - talker.error("[Display2PlayMusic] 第三方音乐源无法获取到playinfo: ${music.info.name}"); - return null; - } - - talker.info("[Display2PlayMusic] 使用第三方Api请求转化歌曲: ${music.info.name}"); - var playMusic = PlayMusic(music.ref, music.info, playinfo, extra); - return playMusic; -} - -class DisplayMusic { - late MusicW ref; - late MusicInfo info; - DisplayMusic(MusicW musicRef_, {MusicInfo? info_}) { - ref = musicRef_; - if (info_ != null) { - info = info_; - } else { - info = ref.getMusicInfo(); - } - } - - (String, String)? toCacheFileNameAndExtra(Quality quality) { - if (info.defaultQuality == null) { - return null; - } - var extra = ref.getExtraInto(quality: quality); - var cacheFileName = - "${info.name}_${info.artist.join(',')}_${info.source}_${extra.hashCode}.${info.defaultQuality!.format ?? "unknown"}"; - return (cacheFileName, extra); - } - - Future hasCache() async { - if (info.defaultQuality == null) return false; - var result = toCacheFileNameAndExtra(info.defaultQuality!); - if (result == null) { - return false; - } - var cache = await useCacheFile( - file: "", cachePath: musicCachePath, filename: result.$1); - if (cache != null) { - return true; - } else { - return false; - } - } -} - class PlayInfo { late String file; late Quality quality; @@ -122,34 +28,44 @@ class PlayInfo { } // 这个结构代表了待播音乐的信息 -class PlayMusic { +class Music { late MusicW ref; late MusicInfo info; - late MediaItem item; - late PlayInfo playInfo; late String extra; - PlayMusic( - MusicW musicRef_, MusicInfo info_, PlayInfo playinfo_, String extra_) { + late Rx useQuality; + late AudioSource audioSource; + bool empty = true; + DateTime lastUpdate = DateTime(1999); + Music(MusicW musicRef_) { ref = musicRef_; - info = info_; - playInfo = playinfo_; - extra = extra_; - item = MediaItem( - id: info.name + info.source + info.artist.join(","), - title: info.name, - album: info.album, - artUri: () { - if (info.artPic != null) { - return Uri.parse(info.artPic!); - } else { - return null; - } - }(), - displayTitle: info.name, - displaySubtitle: info.artist.join(",")); + info = musicRef_.getMusicInfo(); + extra = musicRef_.getExtraInto(quality: info.defaultQuality!); + audioSource = AudioSource.asset("assets/blank.mp3", tag: toMediaItem()); + useQuality = info.defaultQuality!.obs; } - String toCacheFileName() { - return "${info.name}_${info.artist.join(',')}_${info.source}_${extra.hashCode}.${playInfo.quality.format ?? "unknown"}"; + + bool shouldUpdate() { + try { + return (audioSource as ProgressiveAudioSource) + .uri + .path + .contains("/assets/") || + empty || + DateTime.now().difference(lastUpdate).abs().inSeconds >= 1800; + } catch (_) { + return true; + } + } + + String toCacheFileName({Quality? quality_}) { + var quality = quality_ ?? info.defaultQuality!; + return "${info.name}_${info.artist.join(',')}_${info.source}_${extra.hashCode}_${quality.short}.${quality.format ?? "unknown"}"; + } + + Future hasCache() async { + var cache = await useCacheFile( + file: "", cachePath: musicCachePath, filename: toCacheFileName()); + return cache != null; } MediaItem toMediaItem() { @@ -160,36 +76,92 @@ class PlayMusic { artUri = null; } return MediaItem( - id: playInfo.file, + id: extra.hashCode.toString(), title: info.name, album: info.album, artUri: artUri, artist: info.artist.join(",")); } - AudioSource toAudioSource() { + // 主动获取 或者 LazyLoad时使用 + // 如果获取失败,将返回false + Future updateAudioSource([Quality? quality]) async { + empty = false; + lastUpdate = DateTime.now(); + if (quality != null) extra = ref.getExtraInto(quality: quality); + var playInfo = await getPlayInfo(quality); + if (playInfo == null) return false; + useQuality.value = playInfo.quality; if (playInfo.file.contains("http")) { if ((Platform.isIOS || Platform.isMacOS) && playInfo.quality.short.contains("flac")) { - return ProgressiveAudioSource(Uri.parse(playInfo.file), + audioSource = ProgressiveAudioSource(Uri.parse(playInfo.file), tag: toMediaItem(), options: const ProgressiveAudioSourceOptions( darwinAssetOptions: DarwinAssetOptions(preferPreciseDurationAndTiming: true))); } else { - return AudioSource.uri(Uri.parse(playInfo.file), tag: toMediaItem()); + audioSource = + AudioSource.uri(Uri.parse(playInfo.file), tag: toMediaItem()); } } else { if ((Platform.isIOS || Platform.isMacOS) && playInfo.quality.short.contains("flac")) { - return ProgressiveAudioSource(Uri.file(playInfo.file), + audioSource = ProgressiveAudioSource(Uri.file(playInfo.file), tag: toMediaItem(), options: const ProgressiveAudioSourceOptions( darwinAssetOptions: DarwinAssetOptions(preferPreciseDurationAndTiming: true))); } else { - return AudioSource.file(playInfo.file, tag: toMediaItem()); + audioSource = AudioSource.file(playInfo.file, tag: toMediaItem()); } } + return true; + } + + Future getPlayInfo([Quality? quality]) async { + late Quality finalQuality; + if (quality != null) { + finalQuality = quality; + } else { + if (info.defaultQuality != null) { + finalQuality = info.defaultQuality!; + } else if (info.qualities.isNotEmpty) { + finalQuality = info.qualities[0]; + talker.info("[Display2Music] 音乐无默认音质,选择音质中第一个进行播放:$finalQuality"); + } else { + talker.error("[Display2Music] 音乐没有可供播放的音质"); + return null; + } + } + + // 尝试获取本地缓存 + var cache = await useCacheFile( + file: "", + cachePath: musicCachePath, + filename: toCacheFileName(quality_: quality)); + + // 有本地缓存直接返回 + if (cache != null) { + talker.info("[Display2Music] 使用本地歌曲缓存转化歌曲: ${info.name}"); + return PlayInfo(cache, finalQuality); + } + + // 没有本地缓存,也没有第三方api,直接返回null + if (globalExternApi == null) { + talker.error("[Display2Music] 无第三方音乐源,无法获取播放信息"); + } + + // 有第三方api,使用api进行请求 + var playinfo = await globalExternApi!.getMusicPlayInfo(info.source, extra); + + // 如果第三方api查找不到,直接返回null + if (playinfo == null) { + talker.error("[Display2Music] 第三方音乐源无法获取到playinfo: ${info.name}"); + return null; + } else { + talker.info("[Display2Music] 使用第三方Api请求转化歌曲: ${info.name}"); + return PlayInfo(playinfo.file, playinfo.quality); + } } } diff --git a/lib/types/play_music_queue.dart b/lib/types/play_music_queue.dart index d41add3..bfe8a4e 100644 --- a/lib/types/play_music_queue.dart +++ b/lib/types/play_music_queue.dart @@ -4,18 +4,18 @@ // import 'package:app_rhyme/types/music.dart'; // import 'package:get/get.dart'; -// class PlayMusicQueue extends GetxController { -// final RxList musicList = RxList([]); -// final Rx currentlyPlaying = Rx(null); +// class MusicQueue extends GetxController { +// final RxList musicList = RxList([]); +// final Rx currentlyPlaying = Rx(null); // final Rx currentlyPlayingPlayinfo = Rx(null); -// PlayMusicQueue(); +// MusicQueue(); // // 添加播放新音乐(已存在则调整至最后) -// Future addMusic( -// DisplayMusic music, +// Future addMusic( +// Music music, // ) async { -// // 需要将DisplayMusicTuple扩充成MusicTuple,才具备播放能力 -// PlayMusic? newMusic = await display2PlayMusic(music); +// // 需要将MusicTuple扩充成MusicTuple,才具备播放能力 +// Music? newMusic = await display2Music(music); // if (newMusic == null) { // return null; // } @@ -30,7 +30,7 @@ // } else { // // 如果音乐不存在,添加到列表并设置为当前播放 // try { -// var newMusic = await display2PlayMusic(music); +// var newMusic = await display2Music(music); // musicList.add(newMusic); // currentlyPlaying.value = newMusic; // currentlyPlayingPlayinfo.value = newMusic.playInfo; @@ -43,17 +43,17 @@ // return currentlyPlaying.value; // } -// Future replaceMusic(PlayMusic newPlayMusic) async { +// Future replaceMusic(Music newMusic) async { // try { // // 查找具有相同extra的音乐索引 -// int index = musicList.indexWhere((m) => m.extra == newPlayMusic.extra); +// int index = musicList.indexWhere((m) => m.extra == newMusic.extra); // if (index != -1) { -// // 如果找到,替换旧的PlayMusic -// musicList[index] = newPlayMusic; +// // 如果找到,替换旧的Music +// musicList[index] = newMusic; // // 检查是否正在播放这首音乐 -// if (currentlyPlaying.value?.extra == newPlayMusic.extra) { -// currentlyPlaying.value = newPlayMusic; -// currentlyPlayingPlayinfo.value = newPlayMusic.playInfo; +// if (currentlyPlaying.value?.extra == newMusic.extra) { +// currentlyPlaying.value = newMusic; +// currentlyPlayingPlayinfo.value = newMusic.playInfo; // } // update(); // return currentlyPlaying.value; @@ -67,13 +67,13 @@ // } // } -// Future replaceAllMusics( -// List musics, +// Future replaceAllMusics( +// List musics, // ) async { // musicList.clear(); // update(); // for (var music in musics) { -// musicList.add(await display2PlayMusic(music)); +// musicList.add(await display2Music(music)); // } // var firstMusic = _getIndex(0); // currentlyPlaying.value = firstMusic; @@ -83,7 +83,7 @@ // } // // 这里我们认为跳到同一首歌也是改变了(改变进度从头开始) -// PlayMusic? skipToMusic(int index) { +// Music? skipToMusic(int index) { // var music = _getIndex(index); // if (music != null) { // currentlyPlaying.value = music; @@ -97,7 +97,7 @@ // } // // 播放下一首音乐 -// PlayMusic? skipToNext() { +// Music? skipToNext() { // if (musicList.isNotEmpty) { // int currentIndex = currentlyPlaying.value != null // ? musicList.indexOf(currentlyPlaying.value!) @@ -114,7 +114,7 @@ // } // // 播放上一首音乐 -// PlayMusic? skipToPrevious() { +// Music? skipToPrevious() { // if (musicList.isNotEmpty) { // int currentIndex = currentlyPlaying.value != null // ? musicList.indexOf(currentlyPlaying.value!) @@ -132,7 +132,7 @@ // } // // 删除指定索引的音乐 -// PlayMusic? delIndex(int index) { +// Music? delIndex(int index) { // try { // if (currentlyPlaying.value == musicList[index]) { // currentlyPlaying.value = null; @@ -149,7 +149,7 @@ // return null; // } -// Future _changePlayingMusicQuality( +// Future _changePlayingMusicQuality( // int index, // Quality quality, // ) async { @@ -183,13 +183,13 @@ // if (oldIndex < newIndex) { // newIndex -= 1; // } -// final PlayMusic music = musicList.removeAt(oldIndex); +// final Music music = musicList.removeAt(oldIndex); // musicList.insert(newIndex, music); // update(); // } // // 改变当前正在播放的音乐的音质 -// Future changeCurrentPlayingQuality(Quality quality) async { +// Future changeCurrentPlayingQuality(Quality quality) async { // if (currentlyPlaying.value != null) { // return await _changePlayingMusicQuality( // musicList.indexOf(currentlyPlaying.value!), quality); @@ -199,7 +199,7 @@ // } // // 私有方法,用于获取指定索引的音乐 -// PlayMusic? _getIndex(int index) { +// Music? _getIndex(int index) { // try { // return musicList[index]; // } catch (_) { @@ -207,15 +207,15 @@ // } // } -// List downCast() { -// List rst = []; +// List downCast() { +// List rst = []; // for (var m in musicList) { -// rst.add(DisplayMusic(m.ref)); +// rst.add(Music(m.ref)); // } // return rst; // } // // 公共属性和方法 // int get length => musicList.length; -// RxList get allMusic => musicList; +// RxList get allMusic => musicList; // } diff --git a/lib/util/advanced_music_sdk.dart b/lib/util/advanced_music_sdk.dart index 10f0aa6..e1f7ef1 100644 --- a/lib/util/advanced_music_sdk.dart +++ b/lib/util/advanced_music_sdk.dart @@ -1,9 +1,9 @@ import 'package:app_rhyme/src/rust/api/music_sdk.dart'; import 'package:app_rhyme/types/music.dart'; -Future> getAllMusicFromMusicList( +Future> getAllMusicFromMusicList( String payload, String source) async { - List musics = []; + List musics = []; int currentPage = 1; bool isEmptyPage = false; @@ -19,11 +19,11 @@ Future> getAllMusicFromMusicList( // 等待所有请求完成 List> results = await Future.wait(pageRequests); - List newMusics = []; + List newMusics = []; for (var result in results) { for (var music in result) { bool exist = false; - var newMusic = DisplayMusic(music); + var newMusic = Music(music); for (var existMusic in musics) { if (newMusic.info.name == existMusic.info.name && newMusic.info.artist.join(",") == diff --git a/lib/util/audio_controller.dart b/lib/util/audio_controller.dart index f9792df..7c31343 100644 --- a/lib/util/audio_controller.dart +++ b/lib/util/audio_controller.dart @@ -1,16 +1,15 @@ import 'dart:io'; import 'package:app_rhyme/main.dart'; -import 'package:app_rhyme/page/home.dart'; +import 'package:app_rhyme/src/rust/api/mirror.dart'; import 'package:app_rhyme/types/music.dart'; import 'package:app_rhyme/util/time_parse.dart'; -import 'package:app_rhyme/util/toast.dart'; import 'package:audio_session/audio_session.dart'; import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; import 'package:just_audio/just_audio.dart'; import 'package:just_audio_background/just_audio_background.dart'; -import 'package:toastification/toastification.dart'; +import 'package:synchronized/synchronized.dart'; // Windows 平台的just_audio实现存在bug bool isWindowsFirstPlay = true; @@ -36,10 +35,15 @@ Future initGlobalAudioHandler() async { class AudioHandler extends GetxController { final AudioPlayer _player = AudioPlayer(); - final RxList playMusicList = RxList([]); - final Rx playingMusic = Rx(null); - final ConcatenatingAudioSource playSourceList = + final RxList musicList = RxList([]); + final Rx playingMusic = Rx(null); + final ConcatenatingAudioSource audioSourceList = ConcatenatingAudioSource(children: []); + final audioSourceListLock = Lock(); + + AudioHandler() { + _init(); + } Future _init() async { // 先默认开启所有的循环 @@ -47,106 +51,148 @@ class AudioHandler extends GetxController { // 关闭随机播放 _player.setShuffleModeEnabled(false); // 监听错误事件并用talker来log - _player.playbackEventStream.listen((event) {}, onError: (Object e, StackTrace stackTrace) { talker.error('[PlaybackEventStream Error] $e'); }); + // 将playSourceList交给player作为列表,不过目前是空的 + // 在windows平台无法提供一个空白的列表,会造成崩溃,故见后续特殊处理 if (!Platform.isWindows) { - _player.setAudioSource(playSourceList); + _player.setAudioSource(audioSourceList); } - _player.currentIndexStream.listen((event) { - talker.info("[Music Handler] currentIndexStream updated"); - updateRx(); + // 监听播放变化,当获取到一个index时(如歌曲播放下一首时,这里的index将是下一首的index + _player.currentIndexStream.listen((index) async { + if (index == null || musicList.isEmpty) return; + var shouldUpdate = true; + if (index == 0 && lazyLoadLock.locked) shouldUpdate = false; + // 先尝试LazyLoad这首歌 + await tryLazyLoadMusic(index); + // 如果这首即将播放的音乐并仍没有正常LazyLoad,直接尝试播放下一首 + if (musicList[index].shouldUpdate()) { + if (isPlaying) await pause(); + await seekToNext(); + return; + } + if (shouldUpdate) { + updatePlayingMusic(music: musicList[index]); + } }); } - AudioHandler() { - _init(); + var lazyLoadLock = Lock(); + // 用于lazyLoad audioSourceList中的音乐文件 + // 这个函数运行前后必须确保结束后歌曲仍播放正确的index + // 这个函数只会在歌曲需要更新连接时更新,这取决于歌曲是否是一个空白状态抑或超出30分钟 + Future tryLazyLoadMusic(int index, + {Quality? quality, bool force = false}) async { + if (musicList.isEmpty || index > musicList.length - 1) return; + if (force == false && quality == null && !musicList[index].shouldUpdate()) { + return; + } + try { + // 确保同时只有一个LazyLoad运行 + await lazyLoadLock.synchronized(() async { + var current = _player.currentIndex; + if (!await musicList[index].updateAudioSource(quality)) { + talker.info( + "[Music Handler] LazyLoad Music Failed to updateAudioSource: ${musicList[index].info.name}"); + return; + } + // 先将播放器暂停下来 + if (_player.playing) await pause(); + // 确保音频资源不会被两个并发函数同时使用 + await audioSourceListLock.synchronized(() async { + if (audioSourceListLock.inLock) { + await audioSourceList.clear(); + await audioSourceList + .addAll(musicList.map((e) => e.audioSource).toList()); + } else { + await audioSourceListLock.synchronized(() async { + await audioSourceList.clear(); + await audioSourceList + .addAll(musicList.map((e) => e.audioSource).toList()); + }); + } + // 更新音频资源后恢复原来的播放状态 + await _player.seek(Duration.zero, index: current); + play(); + talker.info( + "[Music Hanlder] LazyLoad Music Succeed: ${musicList[index].info.name}"); + }); + }); + } catch (e) { + talker.error("[Music Handler] In LazyLoadMusic, Unknown Error: $e"); + } } - Future addMusicPlay(DisplayMusic music) async { + // App内的手动执行的函数,可能出现首次播放不触发index流,故选择直接获取播放信息 + Future addMusicPlay(Music music) async { try { - PlayMusic? playMusic; - var index = -1; - if (music.info.defaultQuality != null) { - index = playMusicList.indexWhere((element) => + // 由于是手动添加新的音乐,我们直接获取音乐链接并且添加到系统播放资源即可(直接添加到最后面) + // 添加新的音乐到待播列表(直接添加到最后面) + if (!await music.updateAudioSource()) { + talker.info( + "[Music Handler] In addMusicPlay, Failed to updateAudioSource: ${music.info.name}"); + return; + } + // 先暂停播放 + if (_player.playing) await pause(); + // 删去原来的相同音乐并添加新的音乐到最后 + await audioSourceListLock.synchronized(() async { + var index = musicList.indexWhere((element) => element.extra == music.ref.getExtraInto(quality: music.info.defaultQuality!)); - } - if (index != -1) { - playMusic = playMusicList.removeAt(index); - playSourceList.removeAt(index); - } else { - playMusic = await display2PlayMusic(music); - } - - if (playMusic == null) return; - - if (_player.playing) { - await pause(); - } - - // 添加新的音乐 - playMusicList.add(playMusic); - updateRx(music: playMusic); - await playSourceList.add(playMusic.toAudioSource()); + if (index != -1) { + musicList.removeAt(index); + await audioSourceList.removeAt(index); + } + musicList.add(music); + await audioSourceList.add(music.audioSource); + }); + updatePlayingMusic(music: music); + // windows平台的bug,不能添加空的audioSourceList if (Platform.isWindows && isWindowsFirstPlay) { - await _player.setAudioSource(playSourceList); + await _player.setAudioSource(audioSourceList); isWindowsFirstPlay = false; } // 播放新的音乐 - await seek(Duration.zero, index: playSourceList.length - 1); - - await play(); + await seek(Duration.zero, index: audioSourceList.length - 1); } catch (e) { talker.error("[Music Handler] In addMusicPlay, Error occur: $e"); } } - Future replacePlayingMusic(PlayMusic playMusic) async { + // App内手动触发,切换音质,主动更新播放资源 + Future replacePlayingMusic(Quality quality_) async { try { if (playingMusic.value == null) return; - int index = playMusicList + int index = musicList .indexWhere((element) => element.extra == playingMusic.value!.extra); + if (index != -1) { - // 删除对应位置的音乐 - await removeAt(index); - // 插入新音乐到对应位置 - await _insert(index, playMusic); - // 重新播放这个位置的音乐 - await seek(Duration.zero, index: index); - updateRx(music: playMusic); - await play(); + await tryLazyLoadMusic(index, quality: quality_, force: true); + update(); } } catch (e) { talker.error("[Music Handler] In replacePlayingMusic, error occur: $e"); } } - Future replaceMusic(PlayMusic playMusic) async { + // App内手动触发, 但选择使用index流来LazyLoad + // 此函数用在下载/删除缓存时,对应替换musicList中的歌曲,因此出现不会首次播放的情况 + Future replaceMusic(Music music) async { try { - int index = playMusicList - .indexWhere((element) => element.extra == playMusic.extra); - bool shouldPlay = index == _player.currentIndex; + int index = + musicList.indexWhere((element) => element.extra == music.extra); if (index != -1) { - if (shouldPlay && _player.playing) { - await pause(); - await seek(Duration.zero); - } - // 删除对应位置的音乐 - await removeAt(index); - // 插入新音乐到对应位置 - await _insert(index, playMusic); - if (shouldPlay) { - // 重新播放这个位置的音乐 - await seek(Duration.zero, index: index); - updateRx(); - await play(); + if (index == _player.currentIndex) { + await tryLazyLoadMusic(index, force: true); + } else { + musicList[index].empty = true; } } } catch (e) { @@ -154,144 +200,115 @@ class AudioHandler extends GetxController { } } - bool _isClearReplaceMusicAllRunning = false; - + final Lock _clearReplaceMusicAllock = Lock(); + // App内手动触发,必定出现首次播放不触发index流的情况,故手动更新播放资源 Future clearReplaceMusicAll( - BuildContext context, List musics) async { - // 这个函数功能无法承受并发,必须上锁 - var startTime = DateTime.now(); - if (_isClearReplaceMusicAllRunning) { - toast(context, "Music Player", "上一全部播放还未结束", ToastificationType.error); + BuildContext context, List musics) async { + if (musics.isEmpty) { return; } - - _isClearReplaceMusicAllRunning = true; - int index = -1; - - try { - index = globalFloatWidgetContoller.addMsg("播放全部音乐"); - if (musics.isEmpty) { - return; - } - talker.info( - "[Music Handler] Request to add all musics of length: ${musics.length}"); + await _clearReplaceMusicAllock.synchronized(() async { + // 先暂停 if (_player.playing) { await pause(); } + // 清空已有的列表 await clear(); - var firstMusic = await display2PlayMusic(musics[0]); - if (firstMusic == null) return; - playMusicList.add(firstMusic); - await playSourceList.add(firstMusic.toAudioSource()); - updateRx(music: firstMusic); - + // 对于第一首音乐,主动获取其播放信息(因为无法触发index流) + bool shouldSeekNext = !await musics[0].updateAudioSource(); + musicList.add(musics[0]); + await audioSourceListLock.synchronized(() async { + await audioSourceList.add(musics[0].audioSource); + }); + updatePlayingMusic(music: musics[0]); + // windows bug bypass if (Platform.isWindows && isWindowsFirstPlay) { - await _player.setAudioSource(playSourceList); + await _player.setAudioSource(audioSourceList); isWindowsFirstPlay = false; } - await play(); - - List newPlayMusics = []; - List newAudioSources = []; - List> futures = []; + play(); + // 接下来将剩下的所有的音乐添加进去,但是先不获取链接,使用lazy load + musicList.addAll(musics.sublist(1)); - for (var music in musics.sublist(1)) { - futures.add(display2PlayMusic(music)); - } - - List playMusicsResults = await Future.wait(futures); - for (var playMusic in playMusicsResults) { - if (playMusic == null) continue; - newPlayMusics.add(playMusic); - newAudioSources.add(playMusic.toAudioSource()); - } - - if (newAudioSources.isEmpty || newPlayMusics.isEmpty) return; - - playMusicList.addAll(newPlayMusics); try { - await playSourceList.addAll(newAudioSources); + await audioSourceListLock.synchronized(() async { + await audioSourceList + .addAll(musics.sublist(1).map((e) => e.audioSource).toList()); + }); } catch (e) { talker .error("[Music Handler] In clearReplaceMusicAll, Error occur: $e"); } - - log2List("After add all"); - await play(); - } finally { - if (index != -1) { - globalFloatWidgetContoller.delMsg(index); - } - _isClearReplaceMusicAllRunning = false; - var endTime = DateTime.now(); - talker.log( - "[Music Handler] 播放全部耗时: ${endTime.difference(startTime).inSeconds}s"); - } - } - - Future _insert(int index, PlayMusic music) async { - playMusicList.insert(index, music); - await playSourceList.insert(index, music.toAudioSource()); + if (shouldSeekNext) await seekToNext(); + log2List("In clearReplaceMusicAll, After add all"); + }); } Future clear() async { // talker.info("[Music Handler] Request to clear all musics"); - if (playMusicList.isNotEmpty) { - playMusicList.clear(); + if (musicList.isNotEmpty) { + musicList.clear(); } - if (playSourceList.length > 0) { - await playSourceList.clear(); + if (audioSourceList.length > 0) { + await audioSourceListLock.synchronized(() async { + talker.info("[Music Handler] clear获取锁"); + await audioSourceList.clear(); + talker.info("[Music Handler] clear释放锁"); + }); } - updateRx(); log2List("Afer Clear all musics"); } + final _removeLock = Lock(); Future removeAt(int index) async { - // talker.info("[Music Handler] Request to remove music of index:$index"); - if (_player.playing && - _player.currentIndex != null && - _player.currentIndex! == index) { - await _player.pause(); - } - playMusicList.removeAt(index); - await playSourceList.removeAt(index); - updateRx(); + await _removeLock.synchronized(() async { + if (_player.playing && + _player.currentIndex != null && + _player.currentIndex! == index) { + await _player.pause(); + } + musicList.removeAt(index); + await audioSourceList.removeAt(index); + }); } + final _seekToNextLock = Lock(); Future seekToNext() async { try { - await _player.seekToNext(); - // talker.info("[Music Handler] In seekToNext, Succeed"); + if (_player.nextIndex == null) return; + await _seekToNextLock.synchronized(() async { + await _player.seekToNext(); + }); } catch (e) { talker.error("[Music Handler] In seekToNext, error occur: $e"); } - updateRx(); - await play(); + play(); } + final _seekToPreviousLock = Lock(); Future seekToPrevious() async { try { - await _player.seekToPrevious(); - // talker.info("[Music Handler] In seekToPrevious, Succeed"); + if (_player.previousIndex == null) return; + await _seekToPreviousLock.synchronized(() async { + await _player.seekToPrevious(); + }); } catch (e) { talker.error("[Music Handler] In seekToPrevious, error occur: $e"); } - updateRx(); - await play(); + play(); } Future pause() async { try { await _player.pause(); - // talker.info("[Music Handler] In pause, succeed"); } catch (e) { talker.error("[Music Handler] In pause, error occur: $e"); } } - Future play() async { + void play() async { try { // 直接运行在某些平台会导致完全无理由的中断后续代码执行,甚至没有任何报错或者返回(当然也不是阻塞) Future.microtask(() => _player.play()); @@ -301,28 +318,42 @@ class AudioHandler extends GetxController { } } + final _seekLock = Lock(); Future seek(Duration position, {int? index}) async { try { - if (_player.playing) { - await pause(); - } - await _player.seek(position, index: index); - await play(); - talker.info( - "[Music Handler] In seek, Succeed; Seek to ${formatDuration(position.inSeconds)} of ${index ?? "current"}"); + await _seekLock.synchronized(() async { + if (_player.playing) { + await pause(); + } + String name; + if (index != null) { + name = musicList[index].info.name; + await tryLazyLoadMusic(index); + } else { + name = playingMusic.value?.info.name ?? "No Music"; + } + await _player.seek(position, index: index); + play(); + talker.info( + "[Music Handler] In seek, Succeed; Seek to ${formatDuration(position.inSeconds)} of $name"); + }); } catch (e) { talker.error("[Music Handler] In seek, error occur: $e"); } } - void updateRx({PlayMusic? music}) { - if (music != null) { + void updatePlayingMusic({Music? music}) { + // 再LazyLoad中触发的是不可信的,因为可能是在clear后触发的 + if (lazyLoadLock.locked || + (musicList.length > 1 && _player.nextIndex == null)) return; + if (music != null && !music.shouldUpdate()) { playingMusic.value = music; - } else if (playMusicList.isNotEmpty && + } else if (musicList.isNotEmpty && _player.currentIndex != null && _player.currentIndex! >= 0) { try { - playingMusic.value = playMusicList[_player.currentIndex!]; + if (musicList[_player.currentIndex!].shouldUpdate()) return; + playingMusic.value = musicList[_player.currentIndex!]; } catch (e) { talker.error("[Music Handler] Failed to updateRx,set null"); playingMusic.value = null; @@ -337,19 +368,19 @@ class AudioHandler extends GetxController { void log2List(String prefix) { String playListStr = - playMusicList.map((element) => element.info.name).join(","); + musicList.map((element) => element.info.name).join(","); String sourceListStr = - playSourceList.sequence.map((e) => e.tag.title).join(","); + audioSourceList.sequence.map((e) => e.tag.title).join(","); if (playListStr == sourceListStr) { talker.log( - "[Music Handler] $prefix: PlayList = PlaySourceList, length = ${playMusicList.length}, content = [$playListStr]"); + "[Music Handler] $prefix: PlayList = PlaySourceList, length = ${musicList.length}, content = [$playListStr]"); } else { talker.error( - "[Music Handler] $prefix: PlayList != PlaySourceList\nPlayList = length: ${playMusicList.length}, content = [$playListStr]\nPlaySourceList: length = ${playSourceList.length},content = [$playSourceList]"); + "[Music Handler] $prefix: PlayList != PlaySourceList\nPlayList = length: ${musicList.length}, content = [$playListStr]\nPlaySourceList: length = ${audioSourceList.length},content = [$audioSourceList]"); } } - bool isPlaying() { + bool get isPlaying { return _player.playing; } } diff --git a/lib/util/helper.dart b/lib/util/helper.dart index 31b0f73..d807ce7 100644 --- a/lib/util/helper.dart +++ b/lib/util/helper.dart @@ -52,8 +52,8 @@ Future playingMusicImage() async { String get playingMusicQualityShort { late Quality quality; var playingMusic = globalAudioHandler.playingMusic.value; - if (playingMusic != null) { - quality = playingMusic.playInfo.quality; + if (playingMusic != null && playingMusic.useQuality.value != null) { + quality = playingMusic.useQuality.value!; } else { quality = const Quality(short: "Quality"); } diff --git a/lib/util/pull_down_selection.dart b/lib/util/pull_down_selection.dart index fb6f7e4..a5ec94d 100644 --- a/lib/util/pull_down_selection.dart +++ b/lib/util/pull_down_selection.dart @@ -14,7 +14,7 @@ import 'package:pull_down_button/pull_down_button.dart'; List addToMusicListPullDown( BuildContext context, List musicLists, - Future?> musicsFuture, + Future?> musicsFuture, Rect position) => musicLists .map( diff --git a/pubspec.lock b/pubspec.lock index 6765c49..fc5b0c3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -944,7 +944,7 @@ packages: source: hosted version: "0.3.1" synchronized: - dependency: transitive + dependency: "direct main" description: name: synchronized sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" diff --git a/pubspec.yaml b/pubspec.yaml index 8872fa9..a86a095 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,7 @@ dependencies: pull_down_button: ^0.9.4 glassmorphism_ui: ^0.3.0 toastification: ^2.0.0 + synchronized: ^3.1.0+1 dev_dependencies: flutter_test: