From 07809f34f7af0fb1222c370c84823650f3dead23 Mon Sep 17 00:00:00 2001 From: Dev Date: Sat, 11 Jul 2026 02:09:35 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=A4=B1=E8=B4=A5=20C?= =?UTF-8?q?L=20=E7=BB=AD=E6=92=AD=E5=B4=A9=E6=BA=83=20+=20=E9=9A=8F?= =?UTF-8?q?=E6=9C=BA=E6=92=AD=E6=94=BE=E5=85=A8=E9=83=A8=20+=20=E7=BB=AD?= =?UTF-8?q?=E6=92=AD=E8=BF=9B=E5=BA=A6=E5=91=A8=E6=9C=9F=E6=8C=81=E4=B9=85?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PlayerService: 增加 prepared 状态机,未准备时走 playAt 准备而非直接 start(),彻底修复 restoreQueue 恢复后点播放的 IllegalStateException 崩溃 (PR#1 只修了队列被覆盖,未修此崩溃,续播依旧不可用) - 续播位置在 ACTION_PLAY 与 ACTION_PLAY_PAUSE 两条路径均正确应用 - 播放中每约 15s 持久化进度,避免系统强杀导致续播位置丢失 - 首曲 onPrepared 时若可视化未初始化则补建(与均衡器兜底对称) - LibraryFragment: 顶栏「随机」按钮一键打乱全部歌曲播放(借鉴 Auxio) - 同步更新 README --- README.md | 2 + .../puremusicplayer/player/PlayerService.kt | 138 ++++++++++++------ .../com/puremusicplayer/ui/LibraryFragment.kt | 16 ++ app/src/main/res/layout/fragment_library.xml | 17 +++ app/src/main/res/values/strings.xml | 1 + 5 files changed, 130 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index b541410..f756881 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ - **本地曲库扫描**:通过 `MediaStore` 读取设备音乐,按 歌曲 / 专辑 / 艺术家 分组浏览 - **曲库排序**:歌曲与收藏列表可按 标题 / 艺术家 / 专辑(升序)或 时长(降序)排序,选择记忆于 `SharedPreferences`(借鉴 Music Player GO / Auxio) +- **随机播放全部**:曲库顶栏「随机」一键打乱全部歌曲并开始播放(借鉴 Auxio 的播放/随机入口),不改动当前播放模式记忆 +- **续播更可靠**:播放中每约 15 秒持久化一次进度,即便被系统强杀也能恢复到近似位置;应用重启后从迷你播放器点播放可正确续播到上次位置(修复此前对未准备播放器调用 `start()` 的崩溃) - **播放控制**:播放 / 暂停 / 上一首 / 下一首 / 拖动进度;顺序、随机、单曲循环、列表循环 - **后台播放**:媒体播放前台服务,锁屏与控制中心可控,媒体通知常驻 - **歌词**:优先匹配同目录同名 `.lrc`,其次读取文件内嵌歌词——FLAC / OGG(Vorbis Comment `LYRICS`/`UNSYNCEDLYRICS`)、MP3(ID3v2 `USLT`)、M4A / MP4(`©lyr` 原子)、WAV(RIFF LIST/INFO 的 `lyr`/`ILT` 块);当前行高亮 + 平滑滚动;无时间戳的纯文本歌词逐行兜底展示。解析为纯 Kotlin 字节解析,零额外依赖 diff --git a/app/src/main/java/com/puremusicplayer/player/PlayerService.kt b/app/src/main/java/com/puremusicplayer/player/PlayerService.kt index ac929c9..96f3496 100644 --- a/app/src/main/java/com/puremusicplayer/player/PlayerService.kt +++ b/app/src/main/java/com/puremusicplayer/player/PlayerService.kt @@ -48,6 +48,11 @@ class PlayerService : android.app.Service() { private var progressTick = 0 /** 本次播放需要续播到的位置(毫秒);0 表示从头播放 */ private var resumePosition = 0 + /** MediaPlayer 是否已准备就绪(prepareAsync 完成)。 + * 未准备时不可直接 start()/seekTo(),否则抛 IllegalStateException。 + * 这是“失败 CL”续播崩溃的根因:服务刚创建、歌曲已由 restoreQueue 恢复, + * 但 MediaPlayer 还没 prepare,此时点播放会直接 start() 而崩溃。 */ + private var prepared = false private var foregroundStarted = false @@ -76,6 +81,8 @@ class PlayerService : android.app.Service() { mediaPlayer = MediaPlayer().apply { setWakeMode(applicationContext, PowerManager.PARTIAL_WAKE_LOCK) setOnPreparedListener { mp -> + // 标记已准备就绪:此后才可安全地 start() / seekTo() + prepared = true // 续播:回到上次保存的播放位置(由 restoreQueue 经 pendingResumePosition 传入) val savedPos = resumePosition resumePosition = 0 @@ -86,13 +93,15 @@ class PlayerService : android.app.Service() { mp.start() PlayerManager.isPlaying.value = true PlayerManager.duration.value = mp.duration - // 若服务创建时音频会话尚未就绪导致均衡器未初始化,此时会话已有效,补初始化 + // 若服务创建时音频会话尚未就绪导致均衡器/可视化未初始化,此时会话已有效,补初始化 if (Prefs.equalizerEnabled && !EqualizerHelper.isInitialized()) { EqualizerHelper.init(mp.audioSessionId) EqualizerHelper.setEnabled(true) val presets = EqualizerHelper.Preset.values() EqualizerHelper.applyPreset(presets[Prefs.equalizerPreset.coerceIn(0, presets.size - 1)]) } + // 与均衡器兜底对称:会话此刻一定有效,若可视化尚未建立则补建,保证首曲可视化可用 + if (visualizer == null) ensureVisualizer(mp.audioSessionId) applyPlaybackSpeed(mp) updatePlaybackState() updateNotification() @@ -117,19 +126,17 @@ class PlayerService : android.app.Service() { if (PlayerManager.currentIndex < 0 && PlayerManager.playlist.isNotEmpty()) { PlayerManager.currentIndex = 0 } - // 仅当本次播放的歌曲与「恢复出的歌曲」一致时,才续播到上次位置 - val song = PlayerManager.playlist.getOrNull(PlayerManager.currentIndex) - resumePosition = if (song != null && song.favKey() == PlayerManager.resumeSongKey) { - PlayerManager.pendingResumePosition - } else 0 - PlayerManager.resumeSongKey = null - PlayerManager.pendingResumePosition = 0 + // 显式播放当前选择:始终(重新)准备并播放,续播位置一并应用 + prepareResumePosition() playAt(PlayerManager.currentIndex, true) } PlayerControls.ACTION_PAUSE -> pause() - PlayerControls.ACTION_PLAY_PAUSE -> togglePlayPause() - PlayerControls.ACTION_NEXT -> playAt(step(true), true) - PlayerControls.ACTION_PREV -> playAt(step(false), true) + PlayerControls.ACTION_PLAY_PAUSE -> { + if (mediaPlayer.isPlaying) pause() + else { prepareResumePosition(); playOrResume() } + } + PlayerControls.ACTION_NEXT -> { resumePosition = 0; playAt(step(true), true) } + PlayerControls.ACTION_PREV -> { resumePosition = 0; playAt(step(false), true) } PlayerControls.ACTION_SEEK -> seekTo(intent.getIntExtra(PlayerControls.EXTRA_POSITION, 0)) PlayerControls.ACTION_SET_MODE -> { @@ -181,6 +188,7 @@ class PlayerService : android.app.Service() { if (index !in list.indices) return PlayerManager.currentIndex = index PlayerManager.saveQueue(this) + prepared = false // 即将重新准备,标记为未就绪 val song = list[index] try { mediaPlayer.reset() @@ -198,32 +206,57 @@ class PlayerService : android.app.Service() { } private fun resume() { - if (PlayerManager.current() == null && PlayerManager.playlist.isNotEmpty()) { - playAt(0, true); return - } - if (!mediaPlayer.isPlaying) { - mediaPlayer.start() - PlayerManager.isPlaying.value = true - updatePlaybackState() - updateNotification() - } + prepareResumePosition() + playOrResume() } private fun pause() { - if (mediaPlayer.isPlaying) { - mediaPlayer.pause() - PlayerManager.isPlaying.value = false - PlayerManager.saveQueue(this) + if (!prepared || !mediaPlayer.isPlaying) return + mediaPlayer.pause() + PlayerManager.isPlaying.value = false + PlayerManager.saveQueue(this) + updatePlaybackState() + updateNotification() + } + + /** + * 计算续播位置:仅当「恢复出的歌曲」与当前曲目一致时, + * 才把待续播位置写入服务级 resumePosition;随后清空 PlayerManager 的待续播信息, + * 避免误续播到其它歌曲。与 PR#1 修复的「续播位置被清零」对称。 + */ + private fun prepareResumePosition() { + val song = PlayerManager.playlist.getOrNull(PlayerManager.currentIndex) + resumePosition = if (song != null && song.favKey() == PlayerManager.resumeSongKey) { + PlayerManager.pendingResumePosition + } else 0 + PlayerManager.resumeSongKey = null + PlayerManager.pendingResumePosition = 0 + } + + /** + * 播放或恢复当前曲目: + * - 已准备就绪且处于暂停态 → 直接 start(瞬时恢复,含暂停位置) + * - 未准备就绪(如刚创建服务、续播场景,MediaPlayer 尚未 prepare)→ 走 playAt 准备 + * 这避免了「失败 CL」中对未准备播放器调用 start() 的崩溃。 + */ + private fun playOrResume() { + // 无有效曲目时回退到列表首曲 + if (PlayerManager.currentIndex !in PlayerManager.playlist.indices) { + if (PlayerManager.playlist.isNotEmpty()) PlayerManager.currentIndex = 0 + else return + } + if (prepared && ::mediaPlayer.isInitialized && !mediaPlayer.isPlaying) { + mediaPlayer.start() + PlayerManager.isPlaying.value = true updatePlaybackState() updateNotification() + } else if (!prepared) { + playAt(PlayerManager.currentIndex, true) } } - private fun togglePlayPause() { - if (mediaPlayer.isPlaying) pause() else resume() - } - private fun seekTo(posMs: Int) { + if (!prepared) return if (posMs in 0..mediaPlayer.duration) { mediaPlayer.seekTo(posMs) PlayerManager.progress.value = posMs @@ -248,6 +281,7 @@ class PlayerService : android.app.Service() { PlayerManager.sleepRemaining.value = 0 pause() } else { + resumePosition = 0 // 下一首从头播放,避免误带续播位置 playAt(step(true), true) } } @@ -418,26 +452,36 @@ class PlayerService : android.app.Service() { val idx = Prefs.equalizerPreset.coerceIn(0, presets.size - 1) EqualizerHelper.applyPreset(presets[idx]) } + ensureVisualizer(sessionId) + } catch (_: Exception) { + visualizer = null + } + } + + /** 为指定有效音频会话创建可视化(若尚未创建);会话无效或已创建则跳过 */ + private fun ensureVisualizer(sessionId: Int) { + if (sessionId <= 0 || visualizer != null) return + try { visualizer = Visualizer(sessionId).apply { captureSize = Visualizer.getCaptureSizeRange()[1] setDataCaptureListener( object : Visualizer.OnDataCaptureListener { - override fun onWaveFormDataCapture( - v: Visualizer?, waveform: ByteArray?, s: Int - ) { - waveform?.let { PlayerManager.dispatchWave(it) } - } - - override fun onFftDataCapture( - v: Visualizer?, fft: ByteArray?, s: Int - ) { - fft?.let { PlayerManager.dispatchFft(it) } - } - }, - Visualizer.getMaxCaptureRate(), - true, - true - ) + override fun onWaveFormDataCapture( + v: Visualizer?, waveform: ByteArray?, s: Int + ) { + waveform?.let { PlayerManager.dispatchWave(it) } + } + + override fun onFftDataCapture( + v: Visualizer?, fft: ByteArray?, s: Int + ) { + fft?.let { PlayerManager.dispatchFft(it) } + } + }, + Visualizer.getMaxCaptureRate(), + true, + true + ) enabled = true } } catch (_: Exception) { @@ -603,12 +647,18 @@ class PlayerService : android.app.Service() { // ---------- 进度循环 ---------- private var progressRunnable: Runnable? = null + private var saveTick = 0 // 进度持久化计数:每约 15s 写一次,保证续播位置不过期 private fun startProgressLoop() { progressRunnable = object : Runnable { override fun run() { if (::mediaPlayer.isInitialized && mediaPlayer.isPlaying) { PlayerManager.progress.value = mediaPlayer.currentPosition if (progressTick++ % 2 == 0) updatePlaybackState() + // 周期持久化播放进度(约 15s),即便被系统强杀也能恢复到近似位置 + if (saveTick++ >= 30) { + saveTick = 0 + PlayerManager.saveQueue(this@PlayerService) + } } handler.postDelayed(this, 500) } diff --git a/app/src/main/java/com/puremusicplayer/ui/LibraryFragment.kt b/app/src/main/java/com/puremusicplayer/ui/LibraryFragment.kt index fd021d7..5d8fb52 100644 --- a/app/src/main/java/com/puremusicplayer/ui/LibraryFragment.kt +++ b/app/src/main/java/com/puremusicplayer/ui/LibraryFragment.kt @@ -86,6 +86,9 @@ class LibraryFragment : Fragment() { sortMode = Prefs.sortModeOrdinal binding.sortButton.setOnClickListener { showSortDialog() } + // 借鉴 Auxio:顶栏“随机”按钮,一键打乱全部歌曲并开始播放 + binding.shuffleButton.setOnClickListener { shuffleAll() } + // 收藏集合变化时刷新(迷你栏/播放页也可能修改收藏) PlayerManager.favorites.observe(viewLifecycleOwner) { set -> favoritesList = allSongs.filter { set.contains(it.favKey()) } @@ -301,6 +304,19 @@ class LibraryFragment : Fragment() { (activity as? MainActivity)?.switchToNowPlaying() } + /** 借鉴 Auxio:打乱全部歌曲(保留当前目录/筛选范围)并开始播放 */ + private fun shuffleAll() { + if (allSongs.isEmpty()) return + val shuffled = allSongs.shuffled() + PlayerManager.playlist.clear() + PlayerManager.playlist.addAll(shuffled) + PlayerManager.currentIndex = 0 + PlayerManager.playMode = PlayMode.REPEAT_ALL + Prefs.playModeOrdinal = PlayMode.REPEAT_ALL.ordinal + PlayerControls.play(requireContext()) + (activity as? MainActivity)?.switchToNowPlaying() + } + private fun playGroup(songs: List) { // 单曲或少于 5 首直接播放 if (songs.size <= 5) { diff --git a/app/src/main/res/layout/fragment_library.xml b/app/src/main/res/layout/fragment_library.xml index 039f80a..4bd585d 100644 --- a/app/src/main/res/layout/fragment_library.xml +++ b/app/src/main/res/layout/fragment_library.xml @@ -61,6 +61,23 @@ android:focusable="true" android:background="?attr/selectableItemBackground" android:contentDescription="@string/sort_by" /> + + 排序 排序方式 + 随机 标题 艺术家 专辑