Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -23,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.
Expand All @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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:")) {
Expand Down Expand Up @@ -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)
Expand All @@ -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();
}
Expand Down
4 changes: 2 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -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
Expand Down
12 changes: 12 additions & 0 deletions android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
2 changes: 1 addition & 1 deletion android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-android-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
15 changes: 14 additions & 1 deletion src/core/audio-player/AndroidNativeAudioPlayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -72,6 +73,7 @@ export class AndroidNativeAudioPlayer extends BaseAudioPlayer {
this.pendingSeekSeconds = null;
this.pendingSeekDeadline = 0;
this.pendingSeekObservedAtTarget = false;
this.suppressPlaybackEventUntil = 0;
void this.releaseListeners();
}

Expand Down Expand Up @@ -111,6 +113,7 @@ export class AndroidNativeAudioPlayer extends BaseAudioPlayer {
*/
public override async seek(time: number): Promise<void> {
const safeTime = Math.max(0, time);
const wasPaused = this.pausedValue;
this.isSeekLocked = true;
this.seekTargetSeconds = safeTime;
this.currentTimeSecondsValue = safeTime;
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
}),
Expand Down Expand Up @@ -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) {
Expand Down
14 changes: 9 additions & 5 deletions src/core/player/AudioManager.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -42,10 +43,11 @@ class AudioManager extends TypedEventTarget<AudioEventMap> 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;
Expand Down Expand Up @@ -170,6 +172,8 @@ class AudioManager extends TypedEventTarget<AudioEventMap> 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();
}
Expand Down
7 changes: 6 additions & 1 deletion src/core/player/PlayerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ class PlayerController {
}
// 同步 Android 悬浮歌词歌曲信息
this.syncFloatingLyricSongInfo();
if (isCapacitorAndroid) {
void mediaSessionManager.updateMetadata();
}
// 获取歌词
lyricManager.handleLyric(song);
}
Expand Down Expand Up @@ -1820,7 +1823,9 @@ class PlayerController {
window.$message.warning("请先授予悬浮窗权限");
try {
await AndroidNativePlayback.requestOverlayPermission();
} catch {}
} catch (e) {
console.error("申请权限失败:", e);
}
return;
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/plugins/androidNativePlayback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ export interface AndroidNativeApiContextPayload {
cookie: string;
}

export interface AndroidNativePlaybackStateEvent extends AndroidNativePlaybackState {}
export interface AndroidNativePlaybackStateEvent {
// 增加必要的成员定义,例如
state: string;
}

export interface AndroidNativeProgressEvent {
durationMs: number;
Expand Down
4 changes: 3 additions & 1 deletion src/stores/music.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ export const useMusicStore = defineStore("music", {
try {
const player = usePlayerController();
player.syncFloatingLyricData();
} catch {}
} catch (e) {
console.error("同步悬浮歌词失败:", e);
}
});
}
},
Expand Down