10 Commits
Author SHA1 Message Date
lxh2875931338 40d4af322b 开始进入修复阶段 2026-09-02 21:42:42 +08:00
lxh2875931338 1aba004e43 定住这个commit,切换至test分支进行部分破坏性变更:涉及到app内多实例异常问题修复 2026-09-02 21:17:59 +08:00
lxh2875931338 cdf77b308d 开始给原先适配期的状态擦屁股:现在是修复播放页与播放列表页的显示 2026-09-02 20:44:46 +08:00
lxh2875931338 200067a3b2 回归GPL主线的第一版本,后续LGPL仍会继续测试,但是将不会在主分支加入测试 2026-09-02 20:23:06 +08:00
lxh2875931338 974dee2995 revert 99917bac87
revert Core Freeze:核心链路功能已经完成设计,后续进入beta功能开发模式,接下来会在UI部分微调后,进入首个Release版本(1.1.0-Release)
2026-09-02 10:15:54 +08:00
lxh2875931338 b86a5d2b46 准备进入新的测试阶段,先做一个snapshot(此版本下不可用) 2026-08-31 20:51:09 +08:00
lxh2875931338 eb46c53f9a 不考虑上架问题,确认此次更新 2026-08-31 00:05:13 +08:00
lxh2875931338 53f60d393f Merge remote-tracking branch 'origin/test' 2026-08-30 23:57:44 +08:00
lxh2875931338 08ea8a4508 test通道确认功能完整
libmpv.so(LGPL版本)
SHA256 = E56776A1BA03892C9A037A534A7114BA54F6EE202083A706CF878924BAA9D7AA
2026-08-30 23:57:24 +08:00
lxh2875931338 21a1ad1d05 确定这次重构的版本号 2026-08-30 21:55:51 +08:00
24 changed files with 408 additions and 263 deletions
+4
View File
@@ -7,6 +7,10 @@
# The following line activates a set of recommended lints for Flutter apps, # The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices. # packages, and plugins designed to encourage good coding practices.
analyzer:
exclude:
- build/**
- android/**
include: package:flutter_lints/flutter.yaml include: package:flutter_lints/flutter.yaml
linter: linter:
+11 -6
View File
@@ -20,25 +20,31 @@ android {
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName versionName = flutter.versionName
// 仅打包 arm64-v8a(与你的 jniLibs 目录匹配) // 仅打包 arm64-v8a
ndk { ndk {
abiFilters.addAll(listOf("arm64-v8a")) abiFilters.addAll(listOf("arm64-v8a"))
} }
} }
// 显式指定 jniLibs 源目录(确保 .so 文件被包含) // 显式指定 jniLibs 源目录
sourceSets { sourceSets {
getByName("main") { getByName("main") {
jniLibs.srcDirs("src/main/jniLibs") jniLibs.srcDirs("src/main/jniLibs")
} }
} }
// ⭐ 关键:打包配置,确保 .so 不被压缩,且处理重复 tasks.withType<Copy> {
if (name.startsWith("merge") && name.endsWith("JniLibFolders")) {
from("src/main/jniLibs")
into("$buildDir/intermediates/merged_jni_libs/debug/out")
}
}
// 打包配置
packagingOptions { packagingOptions {
jniLibs { jniLibs {
useLegacyPackaging = true // 兼容 Android 7.0+ useLegacyPackaging = true
} }
pickFirsts += "**/*.so" // 如有重复则选第一个 pickFirsts += "**/*.so"
} }
buildTypes { buildTypes {
@@ -47,7 +53,6 @@ android {
release { release {
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
// ⭐ 建议开启混淆时保留某些 native 方法(media_kit 会自动处理)
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
+45
View File
@@ -0,0 +1,45 @@
# ============================================================
# media_kit 核心
# ============================================================
-keep class com.media_kit.** { *; }
-keep class com.alexmercerind.** { *; }
-keep class org.mozilla.** { *; }
# 保留所有 native 方法
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留所有 JNI 相关
-keep class * implements java.lang.reflect.Method { *; }
-keep class * extends java.lang.reflect.Method { *; }
# ============================================================
# audio_service 插件
# ============================================================
-keep class com.ryanheise.audioservice.** { *; }
# ============================================================
# Flutter 核心
# ============================================================
-keep class io.flutter.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.embedding.** { *; }
# ============================================================
# 忽略 Google Play Core 缺失类(如果不想添加依赖)
# ============================================================
-dontwarn com.google.android.play.core.**
-keep class com.google.android.play.core.** { *; }
# ============================================================
# 保留 Serializable 支持
# ============================================================
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
+1
View File
@@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<application <application
android:name="${applicationName}" android:name="${applicationName}"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,17 +4,66 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.ryanheise.audioservice.AudioServiceActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import java.io.File import java.io.File
import android.content.Context
import android.media.AudioManager
class MainActivity : AudioServiceActivity() { class MainActivity : FlutterActivity() {
private val CHANNEL = "com.lxh.qingting_player/file_provider" private val CHANNEL = "com.lxh.qingting_player/file_provider"
override fun onCreate(savedInstanceState: Bundle?) {
// ⭐ 强制加载 libmediakitandroidhelper.so,触发 JNI_OnLoad,缓存 JavaVM
try {
System.loadLibrary("mediakitandroidhelper")
Log.d("MainActivity", "✅ libmediakitandroidhelper loaded")
} catch (e: UnsatisfiedLinkError) {
Log.e("MainActivity", "❌ libmediakitandroidhelper load failed", e)
}
// ⭐ 可选:预加载 libmpv.so(让系统 linker 也可见,增强兼容性)
try {
System.loadLibrary("mpv")
Log.d("MainActivity", "✅ libmpv loaded")
} catch (e: UnsatisfiedLinkError) {
Log.e("MainActivity", "❌ libmpv load failed", e)
}
super.onCreate(savedInstanceState)
requestAudioFocus()
setHighRefreshRate()
}
private fun requestAudioFocus() {
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
audioManager.requestAudioFocus(
AudioManager.OnAudioFocusChangeListener { focusChange ->
Log.d("MainActivity", "音频焦点变化: $focusChange")
},
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN
)
} else {
@Suppress("DEPRECATION")
audioManager.requestAudioFocus(
null,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN
)
}
Log.d("MainActivity", "✅ 已请求音频焦点")
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
// FileProvider MethodChannel for sharing artwork via content:// URI
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result -> .setMethodCallHandler { call, result ->
if (call.method == "getContentUri") { if (call.method == "getContentUri") {
@@ -46,20 +95,6 @@ class MainActivity : AudioServiceActivity() {
} }
} }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 🔥 测试 libmpv.so 能否被系统加载
try {
System.loadLibrary("mpv")
Log.e("MPV_TEST", "🔥 libmpv LOAD SUCCESS")
} catch (e: Throwable) {
Log.e("MPV_TEST", "🔥 libmpv LOAD FAILED", e)
}
setHighRefreshRate()
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
setHighRefreshRate() setHighRefreshRate()
@@ -77,17 +112,4 @@ class MainActivity : AudioServiceActivity() {
Log.e("QTPlayer", "设置高刷失败: ${e.message}") Log.e("QTPlayer", "设置高刷失败: ${e.message}")
} }
} }
private fun clearRefreshRate() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val params = window.attributes
params.preferredRefreshRate = 0f
window.attributes = params
Log.d("QTPlayer", "恢复默认刷新率")
}
} catch (e: Exception) {
Log.e("QTPlayer", "恢复默认刷新率失败: ${e.message}")
}
}
} }
+1 -1
View File
@@ -20,7 +20,7 @@ pluginManagement {
plugins { plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "9.0.1" apply false id("com.android.application") version "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "1.9.0" apply false id("org.jetbrains.kotlin.android") version "2.3.20" apply false
} }
include(":app") include(":app")
-32
View File
@@ -1,32 +0,0 @@
// lib/audio/player_controller.dart
import '../services/playback_service.dart'; // ⭐ 修正路径
class PlayerController {
static final PlayerController _instance = PlayerController._internal();
factory PlayerController() => _instance;
PlayerController._internal();
final PlaybackService _playback = PlaybackService();
// ---- 状态流(只读) ----
Stream<bool> get playingStream => _playback.player.stream.playing;
Stream<Duration> get positionStream => _playback.player.stream.position;
Stream<Duration> get durationStream => _playback.player.stream.duration;
Stream<void> get completedStream => _playback.player.stream.completed;
// ---- 当前状态快照 ----
bool get isPlaying => _playback.player.state.playing;
Duration get position => _playback.player.state.position;
Duration get duration => _playback.player.state.duration;
// ---- 控制命令 ----
Future<void> play() => _playback.resume();
Future<void> pause() => _playback.pause();
Future<void> stop() => _playback.stop();
Future<void> seek(Duration position) => _playback.seek(position);
// ---- 加载歌曲 ----
void load(String url, {Map<String, String>? headers}) {
_playback.play(url, headers: headers);
}
}
+69 -57
View File
@@ -5,6 +5,7 @@ import 'package:media_kit/media_kit.dart';
import 'package:audio_service/audio_service.dart' as audio_service; import 'package:audio_service/audio_service.dart' as audio_service;
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:flutter/services.dart';
import 'services/audio_service.dart'; import 'services/audio_service.dart';
import 'services/playback_service.dart'; import 'services/playback_service.dart';
import 'services/app_lifecycle_service.dart'; import 'services/app_lifecycle_service.dart';
@@ -15,8 +16,8 @@ import 'pages/home_page.dart';
import 'pages/player_page.dart'; import 'pages/player_page.dart';
import 'pages/playlist_page.dart'; import 'pages/playlist_page.dart';
import 'widgets/global_mini_player.dart'; import 'widgets/global_mini_player.dart';
import 'utils/navigation.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
final stopwatch = Stopwatch(); final stopwatch = Stopwatch();
bool _appInitialized = false; bool _appInitialized = false;
@@ -53,6 +54,8 @@ void main() async {
try { try {
_audioHandler = AudioPlayerHandler(); _audioHandler = AudioPlayerHandler();
debugPrint('⏱️ T0.62 AudioPlayerHandler created'); debugPrint('⏱️ T0.62 AudioPlayerHandler created');
// ⭐ 将 Handler 注入到 AudioService
AudioService().setHandler(_audioHandler!);
} catch (e, st) { } catch (e, st) {
debugPrint('❌ AudioPlayerHandler failed: $e'); debugPrint('❌ AudioPlayerHandler failed: $e');
debugPrint('$st'); debugPrint('$st');
@@ -363,65 +366,74 @@ class _QTPlayerAppState extends State<QTPlayerApp> {
debugPrint( debugPrint(
'🎨 [QTPlayerApp] build #$_buildCount ${stopwatch.elapsedMilliseconds}ms'); '🎨 [QTPlayerApp] build #$_buildCount ${stopwatch.elapsedMilliseconds}ms');
return MaterialApp( // ⭐ 使用 AnnotatedRegion 包裹 MaterialApp,统一控制系统栏样式
title: '清听', return AnnotatedRegion<SystemUiOverlayStyle>(
theme: ThemeData( value: SystemUiOverlayStyle(
brightness: Brightness.dark, statusBarColor: Colors.transparent,
primaryColor: const Color(0xFF7C9A9E), statusBarIconBrightness: Brightness.light,
colorScheme: const ColorScheme.dark( systemNavigationBarColor: const Color(0xFF1A1F1E),
primary: Color(0xFF7C9A9E), systemNavigationBarIconBrightness: Brightness.light,
secondary: Color(0xFFB8D4D0),
surface: Color(0xFF1A1F1E),
onSurface: Colors.white,
),
useMaterial3: true,
), ),
navigatorKey: navigatorKey, child: MaterialApp(
navigatorObservers: [MiniPlayerNavigatorObserver()], title: '清听',
onGenerateRoute: _onGenerateRoute, theme: ThemeData(
builder: (context, child) { brightness: Brightness.dark,
return Stack( primaryColor: const Color(0xFF7C9A9E),
fit: StackFit.expand, colorScheme: const ColorScheme.dark(
children: [ primary: Color(0xFF7C9A9E),
if (child != null) child, secondary: Color(0xFFB8D4D0),
AnimatedBuilder( surface: Color(0xFF1A1F1E),
animation: RouteManager(), onSurface: Colors.white,
builder: (context, _) { ),
final routeManager = RouteManager(); useMaterial3: true,
final shouldShow = routeManager.currentRoute != '/player' && ),
routeManager.currentRoute != '/playlist'; navigatorKey: navigatorKey,
navigatorObservers: [MiniPlayerNavigatorObserver()],
onGenerateRoute: _onGenerateRoute,
builder: (context, child) {
return Stack(
fit: StackFit.expand,
children: [
if (child != null) child,
AnimatedBuilder(
animation: RouteManager(),
builder: (context, _) {
final routeManager = RouteManager();
final shouldShow = routeManager.currentRoute != '/player' &&
routeManager.currentRoute != '/playlist';
return AnimatedSwitcher( return AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeOut, switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn, switchOutCurve: Curves.easeIn,
transitionBuilder: (child, animation) { transitionBuilder: (child, animation) {
return FadeTransition( return FadeTransition(
opacity: animation, opacity: animation,
child: SlideTransition( child: SlideTransition(
position: Tween<Offset>( position: Tween<Offset>(
begin: const Offset(0, 0.1), begin: const Offset(0, 0.1),
end: Offset.zero, end: Offset.zero,
).animate(animation), ).animate(animation),
child: child, child: child,
),
);
},
child: shouldShow
? const Align(
key: ValueKey('mini_player_visible'),
alignment: Alignment.bottomCenter,
child: GlobalMiniPlayer(),
)
: const SizedBox.shrink(
key: ValueKey('mini_player_hidden'),
), ),
); );
}, },
), child: shouldShow
], ? const Align(
); key: ValueKey('mini_player_visible'),
}, alignment: Alignment.bottomCenter,
child: GlobalMiniPlayer(),
)
: const SizedBox.shrink(
key: ValueKey('mini_player_hidden'),
),
);
},
),
],
);
},
),
); );
} }
} }
+138 -93
View File
@@ -4,49 +4,62 @@ import 'dart:typed_data';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:audio_service/audio_service.dart' as audio_service; import 'package:audio_service/audio_service.dart' as audio_service;
import '../audio/player_controller.dart';
import '../audio/playback_state_manager.dart';
import '../services/audio_service.dart';
import '../utils/file_provider_utils.dart';
import 'dart:convert';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'dart:convert';
import 'playback_service.dart';
import 'audio_service.dart';
import '../utils/file_provider_utils.dart';
/// 音频服务处理器:连接 media_kit 播放引擎与 Android 系统媒体会话
/// 职责:发布播放状态到系统通知栏,接收系统媒体按钮事件
/// 它不拥有 Player 实例,所有播放控制均委托给 PlaybackService
class AudioPlayerHandler extends audio_service.BaseAudioHandler { class AudioPlayerHandler extends audio_service.BaseAudioHandler {
final PlayerController _player = PlayerController(); final PlaybackService _playback = PlaybackService();
final PlaybackStateManager _state = PlaybackStateManager();
// 当前媒体信息
String? _currentId; String? _currentId;
String? _currentTitle; String? _currentTitle;
String? _currentArtist; String? _currentArtist;
Duration _currentPosition = Duration.zero; Duration _currentPosition = Duration.zero;
DateTime _lastPublishTime = DateTime.now(); DateTime _lastPublishTime = DateTime.now();
static const Duration _publishInterval = Duration(milliseconds: 500); static const Duration _publishInterval = Duration(milliseconds: 500);
// ⭐ 缓存 artwork 文件路径,避免重复写入
String? _currentArtworkPath; String? _currentArtworkPath;
bool _isPublishing = false;
AudioPlayerHandler() { AudioPlayerHandler() {
_player.playingStream.listen((playing) { _bindToPlayer();
_state.updatePlaying(playing); }
_publishState();
// ═══════════════════════════════════════════════════════════════
// 绑定播放器状态流
// ═══════════════════════════════════════════════════════════════
void _bindToPlayer() {
final player = _playback.player;
player.stream.playing.listen((playing) {
_publishPlaybackState(
playing: playing,
position: _currentPosition,
duration: player.state.duration,
);
}); });
_player.positionStream.listen((position) { player.stream.position.listen((position) {
_currentPosition = position; _currentPosition = position;
_state.updatePosition(position);
final now = DateTime.now(); final now = DateTime.now();
if (now.difference(_lastPublishTime) >= _publishInterval) { if (now.difference(_lastPublishTime) >= _publishInterval) {
_lastPublishTime = now; _lastPublishTime = now;
// ⭐ 只保留 _publishStateOnly() _publishPlaybackState(
_publishStateOnly(); playing: player.state.playing,
position: position,
duration: player.state.duration,
onlyPosition: true,
);
} }
}); });
_player.durationStream.listen((duration) { player.stream.duration.listen((duration) {
debugPrint('🎯 [durationStream] duration=$duration');
_state.updateDuration(duration);
if (_currentId != null && _currentTitle != null) { if (_currentId != null && _currentTitle != null) {
_updateMediaItem( _updateMediaItem(
id: _currentId!, id: _currentId!,
@@ -55,45 +68,93 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
duration: duration, duration: duration,
); );
} }
_publishState(); _publishPlaybackState(
playing: player.state.playing,
position: _currentPosition,
duration: duration,
);
}); });
} }
// ---- 发布状态 ---- // ═══════════════════════════════════════════════════════════════
void _publishState() { // 外部同步接口(由 AudioService 调用)
final state = _state.playbackState; // ═══════════════════════════════════════════════════════════════
playbackState.add(audio_service.PlaybackState( void syncState(Song? song) {
controls: state.controls, if (song == null) {
processingState: state.processingState, _currentId = null;
playing: state.playing, _currentTitle = null;
androidCompactActionIndices: state.androidCompactActionIndices, _currentArtist = null;
updatePosition: _currentPosition, return;
updateTime: DateTime.now(), }
systemActions: const {
audio_service.MediaAction.seek, _updateMediaItem(
}, id: song.id,
)); title: song.title,
artist: song.artist,
artwork: song.artwork,
);
final player = _playback.player;
_publishPlaybackState(
playing: player.state.playing,
position: _currentPosition,
duration: player.state.duration,
);
} }
void _publishStateOnly() { // ═══════════════════════════════════════════════════════════════
final current = playbackState.value; // 状态发布
// ═══════════════════════════════════════════════════════════════
void _publishPlaybackState({
required bool playing,
required Duration position,
required Duration duration,
bool onlyPosition = false,
}) {
if (_isPublishing) return;
_isPublishing = true;
playbackState.add(audio_service.PlaybackState( try {
controls: current.controls.isNotEmpty final controls = _buildControls(playing);
? current.controls final state = audio_service.PlaybackState(
: _state.playbackState.controls, controls: controls,
processingState: _state.playbackState.processingState, processingState: duration.inMilliseconds > 0
playing: current.playing, ? audio_service.AudioProcessingState.ready
androidCompactActionIndices: current.androidCompactActionIndices, : audio_service.AudioProcessingState.idle,
updatePosition: _currentPosition, playing: playing,
updateTime: DateTime.now(), updatePosition: position,
systemActions: const { updateTime: DateTime.now(),
audio_service.MediaAction.seek, systemActions: const {audio_service.MediaAction.seek},
}, );
)); playbackState.add(state);
} finally {
_isPublishing = false;
}
} }
// ---- 更新媒体信息 ---- List<audio_service.MediaControl> _buildControls(bool playing) {
return [
const audio_service.MediaControl(
androidIcon: 'drawable/ic_previous',
label: '上一曲',
action: audio_service.MediaAction.skipToPrevious,
),
audio_service.MediaControl(
androidIcon: playing ? 'drawable/ic_pause' : 'drawable/ic_play',
label: playing ? '暂停' : '播放',
action: audio_service.MediaAction.playPause,
),
const audio_service.MediaControl(
androidIcon: 'drawable/ic_next',
label: '下一曲',
action: audio_service.MediaAction.skipToNext,
),
];
}
// ═══════════════════════════════════════════════════════════════
// 媒体信息更新
// ═══════════════════════════════════════════════════════════════
void _updateMediaItem({ void _updateMediaItem({
required String id, required String id,
required String title, required String title,
@@ -101,21 +162,15 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
Duration? duration, Duration? duration,
Uint8List? artwork, Uint8List? artwork,
}) { }) {
debugPrint(
'📢 [handler] _updateMediaItem: artwork is ${artwork != null ? 'not null (${artwork.length} bytes)' : 'null'}');
_currentId = id; _currentId = id;
_currentTitle = title; _currentTitle = title;
_currentArtist = artist; _currentArtist = artist;
final position = _player.position;
debugPrint('📢 [handler] updateMediaItem: $title - $artist'); final position = _playback.player.state.position;
// ⭐ 异步处理 artwork(不阻塞主流程)
_handleArtwork(id, artwork).then((artUri) { _handleArtwork(id, artwork).then((artUri) {
// 如果 artUri 变化,重新推送 MediaItem
final current = mediaItem.value; final current = mediaItem.value;
if (current != null && current.artUri != artUri) { if (current != null && current.artUri != artUri) {
debugPrint('📢 [handler] updating artUri: $artUri');
mediaItem.add(audio_service.MediaItem( mediaItem.add(audio_service.MediaItem(
id: current.id, id: current.id,
title: current.title, title: current.title,
@@ -127,38 +182,28 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
} }
}); });
// 先推送不带封面图的 MediaItem(让 UI 尽快显示)
mediaItem.add(audio_service.MediaItem( mediaItem.add(audio_service.MediaItem(
id: id, id: id,
title: title, title: title,
artist: artist, artist: artist,
duration: duration ?? _player.duration, duration: duration ?? _playback.player.state.duration,
extras: {'position': position.inMilliseconds}, extras: {'position': position.inMilliseconds},
)); ));
} }
/// 处理封面图:保存到本地并生成 content URI
Future<Uri?> _handleArtwork(String id, Uint8List? artwork) async { Future<Uri?> _handleArtwork(String id, Uint8List? artwork) async {
if (artwork == null || artwork.isEmpty) { if (artwork == null || artwork.isEmpty) return null;
_currentArtworkPath = null;
return null;
}
try { try {
final dir = await getApplicationDocumentsDirectory(); final dir = await getApplicationDocumentsDirectory();
final artworkDir = Directory('${dir.path}/artworks'); final artworkDir = Directory('${dir.path}/artworks');
if (!await artworkDir.exists()) { if (!await artworkDir.exists()) {
await artworkDir.create(recursive: true); await artworkDir.create(recursive: true);
} }
// 使用 md5 生成安全的文件名
final bytes = utf8.encode(id); final bytes = utf8.encode(id);
final digest = md5.convert(bytes); final digest = md5.convert(bytes);
final fileName = '$digest.jpg'; final fileName = '$digest.jpg';
final path = '${artworkDir.path}/$fileName'; final path = '${artworkDir.path}/$fileName';
final file = File(path); final file = File(path);
// 检查文件是否已存在
if (await file.exists()) { if (await file.exists()) {
final existingBytes = await file.readAsBytes(); final existingBytes = await file.readAsBytes();
if (existingBytes.length == artwork.length && if (existingBytes.length == artwork.length &&
@@ -167,12 +212,8 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
return await FileProviderUtils.getContentUri(file); return await FileProviderUtils.getContentUri(file);
} }
} }
// 写入新文件
await file.writeAsBytes(artwork); await file.writeAsBytes(artwork);
_currentArtworkPath = path; _currentArtworkPath = path;
debugPrint('📢 [handler] artwork saved: $path');
return await FileProviderUtils.getContentUri(file); return await FileProviderUtils.getContentUri(file);
} catch (e) { } catch (e) {
debugPrint('⚠️ [handler] artwork handling failed: $e'); debugPrint('⚠️ [handler] artwork handling failed: $e');
@@ -180,50 +221,54 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
} }
} }
// ---- 外部接口 ----
void updateNotification({ void updateNotification({
required String id, required String id,
required String title, required String title,
required String artist, required String artist,
Uint8List? artwork, Uint8List? artwork,
}) { }) {
_updateMediaItem( _updateMediaItem(id: id, title: title, artist: artist, artwork: artwork);
id: id, final player = _playback.player;
title: title, _publishPlaybackState(
artist: artist, playing: player.state.playing,
artwork: artwork, position: _currentPosition,
duration: player.state.duration,
); );
_publishState();
} }
// ---- 控制命令 ---- // ═══════════════════════════════════════════════════════════════
// audio_service 控制命令(全部委托给 PlaybackService
// ═══════════════════════════════════════════════════════════════
@override @override
Future<void> play() async { Future<void> play() async {
debugPrint('▶️ [handler] play() called'); debugPrint('▶️ [handler] play() called');
_player.play(); await _playback.resume();
_publishState();
} }
@override @override
Future<void> pause() async { Future<void> pause() async {
debugPrint('⏸️ [handler] pause() called'); debugPrint('⏸️ [handler] pause() called');
_player.pause(); await _playback.pause();
_publishState();
} }
@override @override
Future<void> stop() async { Future<void> stop() async {
debugPrint('⏹️ [handler] stop() called'); debugPrint('⏹️ [handler] stop() called');
_player.stop(); await _playback.stop();
_publishState();
} }
@override @override
Future<void> seek(Duration position) async { Future<void> seek(Duration position) async {
debugPrint('⏩ [handler] seek() called: $position'); debugPrint('⏩ [handler] seek() called: $position');
await _player.seek(position); await _playback.seek(position);
_currentPosition = position; _currentPosition = position;
_publishStateOnly(); final player = _playback.player;
_publishPlaybackState(
playing: player.state.playing,
position: position,
duration: player.state.duration,
onlyPosition: true,
);
} }
@override @override
@@ -251,14 +296,14 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
await skipToPrevious(); await skipToPrevious();
break; break;
case audio_service.MediaButton.media: case audio_service.MediaButton.media:
if (_player.isPlaying) { if (_playback.player.state.playing) {
await pause(); await pause();
} else { } else {
await play(); await play();
} }
break; break;
default: default:
if (_player.isPlaying) { if (_playback.player.state.playing) {
await pause(); await pause();
} else { } else {
await play(); await play();
+20 -22
View File
@@ -13,6 +13,7 @@ import '../utils/artwork_helper.dart';
import '../repositories/playlist_repository.dart'; import '../repositories/playlist_repository.dart';
import '../models/playlist.dart'; import '../models/playlist.dart';
import 'webdav_service.dart'; import 'webdav_service.dart';
import 'audio_player_handler.dart';
enum PlayMode { enum PlayMode {
sequential, sequential,
@@ -62,6 +63,9 @@ class AudioService extends ChangeNotifier {
// ---- 当前播放的歌单 ID ---- // ---- 当前播放的歌单 ID ----
String? _currentPlaylistId; String? _currentPlaylistId;
// ⭐ 插入位置:在现有成员变量之后,方法之前
AudioPlayerHandler? _handler; // ⭐ 添加这一行
// ---- 高频进度 ---- // ---- 高频进度 ----
final ValueNotifier<Duration> positionNotifier = ValueNotifier(Duration.zero); final ValueNotifier<Duration> positionNotifier = ValueNotifier(Duration.zero);
final ValueNotifier<Duration> durationNotifier = ValueNotifier(Duration.zero); final ValueNotifier<Duration> durationNotifier = ValueNotifier(Duration.zero);
@@ -115,6 +119,11 @@ class AudioService extends ChangeNotifier {
_onSongChanged = callback; _onSongChanged = callback;
} }
void setHandler(AudioPlayerHandler handler) {
// ⭐ 添加这个方法
_handler = handler;
}
void togglePlayMode() { void togglePlayMode() {
switch (_playMode) { switch (_playMode) {
case PlayMode.sequential: case PlayMode.sequential:
@@ -264,6 +273,8 @@ class AudioService extends ChangeNotifier {
final song = _queue[_currentIndex]; final song = _queue[_currentIndex];
_currentSong = song; _currentSong = song;
_handler?.syncState(song);
_startListening(); _startListening();
notifyListeners(); notifyListeners();
@@ -547,6 +558,8 @@ class AudioService extends ChangeNotifier {
} }
void stopPlay() { void stopPlay() {
// 停止底层播放器
PlaybackService().stop(); // 新增
_currentSong = null; _currentSong = null;
_isPlaying = false; _isPlaying = false;
positionNotifier.value = Duration.zero; positionNotifier.value = Duration.zero;
@@ -554,7 +567,6 @@ class AudioService extends ChangeNotifier {
bufferedNotifier.value = Duration.zero; bufferedNotifier.value = Duration.zero;
_stopListening(); _stopListening();
notifyListeners(); notifyListeners();
// 停止时也保存一次
savePlaybackState(); savePlaybackState();
} }
@@ -779,6 +791,7 @@ class AudioService extends ChangeNotifier {
debugPrint('💾 [AudioService] playback state saved'); debugPrint('💾 [AudioService] playback state saved');
} }
/// 恢复播放状态
/// 恢复播放状态 /// 恢复播放状态
Future<bool> restorePlaybackState() async { Future<bool> restorePlaybackState() async {
final state = await _db.getPlaybackState(); final state = await _db.getPlaybackState();
@@ -802,9 +815,7 @@ class AudioService extends ChangeNotifier {
_queue = queue; _queue = queue;
_currentIndex = state['current_index'] as int; _currentIndex = state['current_index'] as int;
if (_currentIndex < 0 || _currentIndex >= _queue.length) { if (_currentIndex >= _queue.length) _currentIndex = 0;
_currentIndex = 0;
}
final modeStr = state['play_mode'] as String? ?? 'sequential'; final modeStr = state['play_mode'] as String? ?? 'sequential';
_playMode = modeStr == 'sequential' _playMode = modeStr == 'sequential'
@@ -814,20 +825,6 @@ class AudioService extends ChangeNotifier {
: PlayMode.shuffle; : PlayMode.shuffle;
_currentPlaylistId = state['current_playlist_id'] as String?; _currentPlaylistId = state['current_playlist_id'] as String?;
// ⭐⭐⭐ 关键修复:重建 _shuffledIndices 和 _shuffledIndex ⭐⭐⭐
if (_playMode == PlayMode.shuffle) {
_shuffledIndices = List.generate(_queue.length, (i) => i);
_shuffledIndices.shuffle();
_shuffledIndex = _shuffledIndices.indexOf(_currentIndex);
if (_shuffledIndex == -1) {
_shuffledIndex = 0;
_currentIndex = _shuffledIndices[0];
}
} else {
_shuffledIndices = List.generate(_queue.length, (i) => i);
_shuffledIndex = _currentIndex;
}
// 保存待恢复的进度 // 保存待恢复的进度
final savedPos = state['position_ms'] as int? ?? 0; final savedPos = state['position_ms'] as int? ?? 0;
_pendingSeekPosition = Duration(milliseconds: savedPos); _pendingSeekPosition = Duration(milliseconds: savedPos);
@@ -836,11 +833,12 @@ class AudioService extends ChangeNotifier {
_currentSong = song; _currentSong = song;
_onSongChanged?.call(song); _onSongChanged?.call(song);
debugPrint('♻️ [AudioService] playback state restored: ' _handler?.syncState(song); // ⭐ 添加这一行
'queue=${_queue.length}, index=$_currentIndex, '
'song=${song.title}, mode=$_playMode');
// ⭐ 加载音频但不自动播放 debugPrint(
'♻️ [AudioService] playback state restored: ${song.title} - ${song.artist}');
// ⭐ 关键:加载音频但不自动播放
await _loadRestoredPlayback(); await _loadRestoredPlayback();
return true; return true;
+3
View File
@@ -0,0 +1,3 @@
import 'package:flutter/material.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
+5 -11
View File
@@ -2,7 +2,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../services/audio_service.dart'; import '../services/audio_service.dart';
import '../main.dart'; import '../utils/navigation.dart'; // ⭐ 导入 navigatorKey
class GlobalMiniPlayer extends StatelessWidget { class GlobalMiniPlayer extends StatelessWidget {
const GlobalMiniPlayer({super.key}); const GlobalMiniPlayer({super.key});
@@ -13,7 +13,6 @@ class GlobalMiniPlayer extends StatelessWidget {
final isPlaying = context.select<AudioService, bool>((s) => s.isPlaying); final isPlaying = context.select<AudioService, bool>((s) => s.isPlaying);
final bottomPadding = MediaQuery.of(context).padding.bottom; final bottomPadding = MediaQuery.of(context).padding.bottom;
// 如果没有歌曲,不显示
if (song == null) { if (song == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@@ -21,7 +20,6 @@ class GlobalMiniPlayer extends StatelessWidget {
return Stack( return Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
// 背景条
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
@@ -31,7 +29,6 @@ class GlobalMiniPlayer extends StatelessWidget {
color: const Color(0xFF1A1F1E), color: const Color(0xFF1A1F1E),
), ),
), ),
// 主内容
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
@@ -44,21 +41,20 @@ class GlobalMiniPlayer extends StatelessWidget {
height: 60, height: 60,
color: const Color(0xFF1A1F1E), color: const Color(0xFF1A1F1E),
child: ClipRRect( child: ClipRRect(
// ⭐ 修复涟漪圆角问题
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
onTap: () { onTap: () {
// ⭐ 使用 navigatorKey 直接跳转
navigatorKey.currentState?.pushNamed('/player'); navigatorKey.currentState?.pushNamed('/player');
}, },
highlightColor: Colors.white.withOpacity(0.05), highlightColor: Colors.white.withValues(alpha: 0.05),
splashColor: Colors.white.withOpacity(0.1), splashColor: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: Row( child: Row(
children: [ children: [
const SizedBox(width: 12), const SizedBox(width: 12),
// 封面图
Container( Container(
width: 44, width: 44,
height: 44, height: 44,
@@ -81,7 +77,6 @@ class GlobalMiniPlayer extends StatelessWidget {
: null, : null,
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
// 标题 + 艺术家
Expanded( Expanded(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -111,7 +106,6 @@ class GlobalMiniPlayer extends StatelessWidget {
], ],
), ),
), ),
// 播放/暂停按钮
IconButton( IconButton(
icon: Icon( icon: Icon(
isPlaying ? Icons.pause : Icons.play_arrow, isPlaying ? Icons.pause : Icons.play_arrow,
@@ -122,7 +116,6 @@ class GlobalMiniPlayer extends StatelessWidget {
context.read<AudioService>().togglePlay(); context.read<AudioService>().togglePlay();
}, },
), ),
// 播放列表按钮
IconButton( IconButton(
icon: const Icon( icon: const Icon(
Icons.playlist_play_outlined, Icons.playlist_play_outlined,
@@ -130,6 +123,7 @@ class GlobalMiniPlayer extends StatelessWidget {
size: 24, size: 24,
), ),
onPressed: () { onPressed: () {
// ⭐ 使用 navigatorKey 直接跳转
navigatorKey.currentState?.pushNamed('/playlist'); navigatorKey.currentState?.pushNamed('/playlist');
}, },
), ),
+58 -10
View File
@@ -284,10 +284,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: js name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.7.2" version: "0.6.7"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@@ -332,10 +332,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.12.19" version: "0.12.20"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
@@ -352,14 +352,62 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.2.6" version: "1.2.6"
media_kit_libs_android_audio:
dependency: transitive
description:
name: media_kit_libs_android_audio
sha256: "8f8f9759e537e12d66f08bc4d5279eb1bb21a0ccc519ff3442c68a9f3b6dd68b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.8"
media_kit_libs_audio:
dependency: "direct main"
description:
name: media_kit_libs_audio
sha256: "81bf506c234e81e3ec536ba72f8f700a928543c14c345220210cae0411636316"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.7"
media_kit_libs_ios_audio:
dependency: transitive
description:
name: media_kit_libs_ios_audio
sha256: "78ccf04e27d6b4ba00a355578ccb39b772f00d48269a6ac3db076edf2d51934f"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.4"
media_kit_libs_linux:
dependency: transitive
description:
name: media_kit_libs_linux
sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.1"
media_kit_libs_macos_audio:
dependency: transitive
description:
name: media_kit_libs_macos_audio
sha256: "3be21844df98f286de32808592835073cdef2c1a10078bac135da790badca950"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.4"
media_kit_libs_windows_audio:
dependency: transitive
description:
name: media_kit_libs_windows_audio
sha256: c2fd558cc87b9d89a801141fcdffe02e338a3b21a41a18fbd63d5b221a1b8e53
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.9"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.18.0" version: "1.19.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -721,10 +769,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.7.11" version: "0.7.12"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -761,10 +809,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vector_math name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.2.0" version: "2.4.2"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
+2 -2
View File
@@ -30,8 +30,8 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
media_kit: ^1.2.6 media_kit: 1.2.6
#media_kit_libs_audio: ^1.0.7 media_kit_libs_audio: ^1.0.7
shared_preferences: 2.2.2 shared_preferences: 2.2.2
provider: ^6.1.2 provider: ^6.1.2
dio: ^5.4.0 # HTTP 客户端 dio: ^5.4.0 # HTTP 客户端