From b84e0a9f51bcd9635f7034f24efc2f5a91933901 Mon Sep 17 00:00:00 2001 From: BB0813 Date: Thu, 23 Apr 2026 00:23:11 +0800 Subject: [PATCH 1/2] =?UTF-8?q?refactor:=20=E6=8E=A5=E5=85=A5=E5=AE=89?= =?UTF-8?q?=E5=8D=93=E5=8E=9F=E7=94=9FAPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/build.gradle | 30 +++++++++++++--- .../android/playback/PlaybackManager.java | 34 ++++++++++++++++++- android/build.gradle | 4 +-- android/gradle.properties | 12 +++++++ .../gradle/wrapper/gradle-wrapper.properties | 2 +- scripts/build-android-node.ts | 2 +- .../audio-player/AndroidNativeAudioPlayer.ts | 15 +++++++- src/core/player/AudioManager.ts | 14 +++++--- src/core/player/PlayerController.ts | 7 +++- src/plugins/androidNativePlayback.ts | 5 ++- src/stores/music.ts | 4 ++- 11 files changed, 111 insertions(+), 18 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 6d1840855..71538a98f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -6,7 +6,27 @@ if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } +tasks.register('cleanupNodeLibGz') { + doLast { + delete fileTree(dir: 'libs/cdvnodejsmobile/libnode/bin', include: ['**/*.so.gz']) + } +} + android { + packagingOptions { + jniLibs { + pickFirsts += ['lib/x86/libnode.so', 'lib/armeabi-v7a/libnode.so', 'lib/arm64-v8a/libnode.so', 'lib/x86_64/libnode.so'] + excludes += ['**/libnode.so.gz'] + } + } + sourceSets { + main { + jniLibs.srcDirs += ['libs/cdvnodejsmobile/libnode/bin'] + } + } + lint { + abortOnError false + } namespace = "top.imsyy.splayer.android" compileSdk = rootProject.ext.compileSdkVersion signingConfigs { @@ -36,21 +56,23 @@ android { release { minifyEnabled false signingConfig keystorePropertiesFile.exists() ? signingConfigs.release : signingConfigs.debug - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } - // 分架构构建:每个 ABI 生成独立 APK,不再生成 universal 合包 + // 分架构构建:暂时关闭以修复 libnode.so 丢失导致的崩溃问题 splits { abi { - enable true + enable false reset() include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' - universalApk false + universalApk true } } } +preBuild.dependsOn cleanupNodeLibGz + repositories { flatDir{ dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' diff --git a/android/app/src/main/java/top/imsyy/splayer/android/playback/PlaybackManager.java b/android/app/src/main/java/top/imsyy/splayer/android/playback/PlaybackManager.java index 362444788..d7a823eba 100644 --- a/android/app/src/main/java/top/imsyy/splayer/android/playback/PlaybackManager.java +++ b/android/app/src/main/java/top/imsyy/splayer/android/playback/PlaybackManager.java @@ -352,6 +352,18 @@ public synchronized void setRate(float rate) { public synchronized void updateMetadata(TrackMetadata metadata) { currentMetadata = metadata == null ? new TrackMetadata() : metadata; + currentMetadata.url = currentSource; + if (player != null) { + MediaItem currentItem = player.getCurrentMediaItem(); + if (currentItem != null) { + player.replaceMediaItem( + player.getCurrentMediaItemIndex(), + currentItem.buildUpon().setMediaMetadata(buildMediaMetadata()).build()); + } + } + if (mediaSession != null) { + mediaSession.setSessionActivity(buildContentIntent()); + } loadCoverBitmapAsync(currentMetadata.coverUrl); updateMediaSessionButtons(); updateNotification(); @@ -683,6 +695,9 @@ private MediaMetadata buildMediaMetadata() { if (currentMetadata.album != null) { builder.setAlbumTitle(currentMetadata.album); } + if (currentMetadata.durationMs > 0) { + builder.setDurationMs(currentMetadata.durationMs); + } if (currentMetadata.coverUrl != null && !currentMetadata.coverUrl.isEmpty() && !currentMetadata.coverUrl.startsWith("blob:")) { @@ -848,6 +863,15 @@ private void updateNotification() { } private Notification buildNotification() { + long durationMs = getDurationMs(); + long positionMs = getPositionMs(); + String artistText = safeText(currentMetadata.artist, ""); + String progressText = durationMs > 0 ? formatTime(positionMs) + " / " + formatTime(durationMs) : ""; + String contentText = artistText; + if (!progressText.isEmpty()) { + contentText = artistText.isEmpty() ? progressText : artistText + " " + progressText; + } + NotificationCompat.Builder builder = new NotificationCompat.Builder(appContext, PlaybackConstants.CHANNEL_ID) .setSmallIcon(R.mipmap.ic_launcher) @@ -860,9 +884,17 @@ private Notification buildNotification() { .setPriority(NotificationCompat.PRIORITY_LOW) .setOngoing(isEffectivelyPlaying() || isEffectivelyBuffering()) .setContentTitle(safeText(currentMetadata.title, appContext.getString(R.string.app_name))) - .setContentText(safeText(currentMetadata.artist, "")) + .setContentText(contentText) .setLargeIcon(coverBitmap); + if (durationMs > 0) { + builder.setProgress((int) durationMs, (int) Math.min(positionMs, durationMs), false); + builder.setSubText(progressText); + } else { + builder.setProgress(0, 0, false); + builder.setSubText(null); + } + if (!controllerEnabled) { return builder.build(); } diff --git a/android/build.gradle b/android/build.gradle index f8f0e43b6..ac7097db8 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,13 +1,13 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - + repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.13.0' + classpath 'com.android.tools.build:gradle:8.9.1' classpath 'com.google.gms:google-services:4.4.4' // NOTE: Do not place your application dependencies here; they belong diff --git a/android/gradle.properties b/android/gradle.properties index 2e87c52f8..ba85f85b7 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -10,6 +10,7 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx1536m +org.gradle.java.home=C:/Program Files/Android/Android Studio/jbr # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit @@ -20,3 +21,14 @@ org.gradle.jvmargs=-Xmx1536m # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true +android.defaults.buildfeatures.resvalues=true +android.sdk.defaultTargetSdkToCompileSdkIfUnset=false +android.enableAppCompileTimeRClass=false +android.usesSdkInManifest.disallowed=false +android.uniquePackageNames=false +android.dependency.useConstraints=true +android.r8.strictFullModeForKeepRules=false +android.r8.optimizedResourceShrinking=false +android.builtInKotlin=false +android.newDsl=false +android.suppressUnsupportedCompileSdk=36 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 7705927e9..e2847c820 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/scripts/build-android-node.ts b/scripts/build-android-node.ts index 509ddd3d1..6f33f0228 100644 --- a/scripts/build-android-node.ts +++ b/scripts/build-android-node.ts @@ -33,7 +33,7 @@ const transpileJavaScriptTree = async (rootPath: string) => { continue; } - if (!entry.isFile() || !entry.name.endsWith(".js")) continue; + if (!entry.isFile() || !entry.name.endsWith(".js") || entry.name === "README.js") continue; const source = await readFile(entryPath, "utf8"); try { diff --git a/src/core/audio-player/AndroidNativeAudioPlayer.ts b/src/core/audio-player/AndroidNativeAudioPlayer.ts index 4f8d6e428..bf991f9f6 100644 --- a/src/core/audio-player/AndroidNativeAudioPlayer.ts +++ b/src/core/audio-player/AndroidNativeAudioPlayer.ts @@ -31,6 +31,7 @@ export class AndroidNativeAudioPlayer extends BaseAudioPlayer { private pendingSeekSeconds: number | null = null; private pendingSeekDeadline = 0; private pendingSeekObservedAtTarget = false; + private suppressPlaybackEventUntil = 0; /** Seek 锁,确保 seek 期间 currentTime 始终返回目标值 */ private isSeekLocked = false; @@ -72,6 +73,7 @@ export class AndroidNativeAudioPlayer extends BaseAudioPlayer { this.pendingSeekSeconds = null; this.pendingSeekDeadline = 0; this.pendingSeekObservedAtTarget = false; + this.suppressPlaybackEventUntil = 0; void this.releaseListeners(); } @@ -111,6 +113,7 @@ export class AndroidNativeAudioPlayer extends BaseAudioPlayer { */ public override async seek(time: number): Promise { const safeTime = Math.max(0, time); + const wasPaused = this.pausedValue; this.isSeekLocked = true; this.seekTargetSeconds = safeTime; this.currentTimeSecondsValue = safeTime; @@ -124,6 +127,10 @@ export class AndroidNativeAudioPlayer extends BaseAudioPlayer { positionMs: Math.max(0, Math.round(safeTime * 1000)), }); this.applyState(state); + if (wasPaused && !this.pausedValue) { + this.suppressPlaybackEventUntil = Date.now() + 1000; + this.applyState(await AndroidNativePlayback.pause()); + } this.isSeekLocked = false; this.dispatch(AUDIO_EVENTS.SEEKED); } @@ -201,7 +208,10 @@ export class AndroidNativeAudioPlayer extends BaseAudioPlayer { this.dispatch(AUDIO_EVENTS.CAN_PLAY); } - if (previousPaused !== this.pausedValue) { + if ( + previousPaused !== this.pausedValue && + Date.now() >= this.suppressPlaybackEventUntil + ) { this.dispatch(this.pausedValue ? AUDIO_EVENTS.PAUSE : AUDIO_EVENTS.PLAY); } }), @@ -273,6 +283,9 @@ export class AndroidNativeAudioPlayer extends BaseAudioPlayer { if (typeof state.errorCode === "number") { this.errorCode = state.errorCode; } + if (Date.now() >= this.suppressPlaybackEventUntil) { + this.suppressPlaybackEventUntil = 0; + } } private shouldIgnoreRegressiveSeekUpdate(nextTime: number) { diff --git a/src/core/player/AudioManager.ts b/src/core/player/AudioManager.ts index 56cefee89..46e32ad84 100644 --- a/src/core/player/AudioManager.ts +++ b/src/core/player/AudioManager.ts @@ -1,6 +1,7 @@ import { useSettingStore } from "@/stores"; -import { checkIsolationSupport, isElectron } from "@/utils/env"; +import { checkIsolationSupport, isElectron, isCapacitorAndroid } from "@/utils/env"; import { TypedEventTarget } from "@/utils/TypedEventTarget"; +import { AndroidNativeAudioPlayer } from "../audio-player/AndroidNativeAudioPlayer"; import { AudioElementPlayer } from "../audio-player/AudioElementPlayer"; import { AUDIO_EVENTS, type AudioEventMap } from "../audio-player/BaseAudioPlayer"; import { FFmpegAudioPlayer } from "../audio-player/ffmpeg-engine/FFmpegAudioPlayer"; @@ -42,10 +43,11 @@ class AudioManager extends TypedEventTarget implements IPlaybackE constructor(playbackEngine: "web-audio" | "mpv", audioEngine: "element" | "ffmpeg") { super(); - // 根据设置选择引擎 - // Android:暂时回退到 HTMLAudioElement(AudioElementPlayer), - // ExoPlayer 原生引擎的 seek 行为一直调不好,先保证可用。 - if (isElectron && playbackEngine === "mpv") { + // Android 优先使用 AndroidNativeAudioPlayer + if (isCapacitorAndroid) { + this.engine = new AndroidNativeAudioPlayer(); + this.engineType = "android-native"; + } else if (isElectron && playbackEngine === "mpv") { const mpvPlayer = useMpvPlayer(); mpvPlayer.init(); this.engine = mpvPlayer; @@ -170,6 +172,8 @@ class AudioManager extends TypedEventTarget implements IPlaybackE let newEngine: IPlaybackEngine; if (this.engineType === "ffmpeg") { newEngine = new FFmpegAudioPlayer(); + } else if (this.engineType === "android-native") { + newEngine = new AndroidNativeAudioPlayer(); } else { newEngine = new AudioElementPlayer(); } diff --git a/src/core/player/PlayerController.ts b/src/core/player/PlayerController.ts index ba244d385..3dc23350a 100644 --- a/src/core/player/PlayerController.ts +++ b/src/core/player/PlayerController.ts @@ -246,6 +246,9 @@ class PlayerController { } // 同步 Android 悬浮歌词歌曲信息 this.syncFloatingLyricSongInfo(); + if (isCapacitorAndroid) { + void mediaSessionManager.updateMetadata(); + } // 获取歌词 lyricManager.handleLyric(song); } @@ -1820,7 +1823,9 @@ class PlayerController { window.$message.warning("请先授予悬浮窗权限"); try { await AndroidNativePlayback.requestOverlayPermission(); - } catch {} + } catch (e) { + console.error("申请权限失败:", e); + } return; } } diff --git a/src/plugins/androidNativePlayback.ts b/src/plugins/androidNativePlayback.ts index dbfdac6b1..d245ce996 100644 --- a/src/plugins/androidNativePlayback.ts +++ b/src/plugins/androidNativePlayback.ts @@ -54,7 +54,10 @@ export interface AndroidNativeApiContextPayload { cookie: string; } -export interface AndroidNativePlaybackStateEvent extends AndroidNativePlaybackState {} +export interface AndroidNativePlaybackStateEvent { + // 增加必要的成员定义,例如 + state: string; +} export interface AndroidNativeProgressEvent { durationMs: number; diff --git a/src/stores/music.ts b/src/stores/music.ts index 8b3a1ea7c..0e7acef22 100644 --- a/src/stores/music.ts +++ b/src/stores/music.ts @@ -126,7 +126,9 @@ export const useMusicStore = defineStore("music", { try { const player = usePlayerController(); player.syncFloatingLyricData(); - } catch {} + } catch (e) { + console.error("同步悬浮歌词失败:", e); + } }); } }, From 4cb1092234cfc2c488f2b47b6ffe3e0d0e442f65 Mon Sep 17 00:00:00 2001 From: BB0813 Date: Thu, 23 Apr 2026 01:56:17 +0800 Subject: [PATCH 2/2] =?UTF-8?q?build:=20=E4=BF=AE=E6=94=B9=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 71538a98f..f5db1c235 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -43,8 +43,8 @@ android { applicationId "top.imsyy.splayer.android" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 30003 - versionName "3.0.0-rc.2" + versionCode 30004 + versionName "3.0.0-rc.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.