后台播放能力已调通,介入部分细节优化
This commit is contained in:
+114
-49
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
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 'services/audio_service.dart';
|
||||
@@ -9,6 +9,7 @@ import 'services/playback_service.dart';
|
||||
import 'services/app_lifecycle_service.dart';
|
||||
import 'services/webdav_service.dart';
|
||||
import 'services/audio_player_handler.dart';
|
||||
import 'services/app_lifecycle_manager.dart';
|
||||
import 'pages/home_page.dart';
|
||||
import 'pages/player_page.dart';
|
||||
import 'pages/playlist_page.dart';
|
||||
@@ -23,6 +24,9 @@ bool _appInitialized = false;
|
||||
// ⭐ 全局缓存 Handler
|
||||
AudioPlayerHandler? _audioHandler;
|
||||
|
||||
// ⭐ 全局缓存 LifecycleManager
|
||||
final AppLifecycleManager lifecycleManager = AppLifecycleManager();
|
||||
|
||||
void main() async {
|
||||
// ⭐ P0:防止重复初始化
|
||||
if (_appInitialized) {
|
||||
@@ -53,6 +57,10 @@ void main() async {
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => AudioService()),
|
||||
ChangeNotifierProvider(create: (_) => AppLifecycleService()),
|
||||
// ⭐ 生命周期管理器
|
||||
ChangeNotifierProvider<AppLifecycleManager>(
|
||||
create: (_) => lifecycleManager,
|
||||
),
|
||||
Provider<AudioPlayerHandler>(create: (_) => _audioHandler!),
|
||||
],
|
||||
child: const QTPlayerApp(),
|
||||
@@ -121,8 +129,14 @@ class QTPlayerApp extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _QTPlayerAppState extends State<QTPlayerApp> {
|
||||
// ⭐ build 计数器(诊断用)
|
||||
static int _buildCount = 0;
|
||||
|
||||
bool _isInitializing = false;
|
||||
|
||||
// ⭐ 保存生命周期监听器引用
|
||||
void Function(AppLifecycleStatus)? _lifecycleListener;
|
||||
|
||||
// ============================================================
|
||||
// 生命周期
|
||||
// ============================================================
|
||||
@@ -134,92 +148,143 @@ class _QTPlayerAppState extends State<QTPlayerApp> {
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
debugPrint('⏱️ T2 首帧回调: ${stopwatch.elapsedMilliseconds}ms');
|
||||
|
||||
// ⭐ 1. 初始化生命周期管理器
|
||||
lifecycleManager.init();
|
||||
|
||||
// ⭐ 2. 绑定生命周期到播放器
|
||||
_bindLifecycleToPlayback();
|
||||
|
||||
// ⭐ 3. 后台初始化
|
||||
_startBackgroundInitialization();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// ⭐ 移除生命周期监听器
|
||||
if (_lifecycleListener != null) {
|
||||
lifecycleManager.removeListenerFromManager(_lifecycleListener!);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 后台初始化(不阻塞首帧)
|
||||
// 生命周期 → 播放器绑定
|
||||
// ============================================================
|
||||
void _bindLifecycleToPlayback() {
|
||||
if (_lifecycleListener != null) {
|
||||
lifecycleManager.removeListenerFromManager(_lifecycleListener!);
|
||||
}
|
||||
|
||||
_lifecycleListener = (status) {
|
||||
final audioService = context.read<AudioService>();
|
||||
|
||||
switch (status) {
|
||||
case AppLifecycleStatus.paused:
|
||||
case AppLifecycleStatus.inactive:
|
||||
debugPrint('🎵 App 进入后台,播放继续');
|
||||
break;
|
||||
|
||||
case AppLifecycleStatus.resumed:
|
||||
debugPrint('🎵 App 回到前台');
|
||||
break;
|
||||
|
||||
case AppLifecycleStatus.backgroundIdle:
|
||||
debugPrint('🎵 长后台唤醒,检查播放状态');
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
lifecycleManager.addListenerToManager(_lifecycleListener!);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 后台初始化(精细时间切片)
|
||||
// ============================================================
|
||||
Future<void> _startBackgroundInitialization() async {
|
||||
if (_isInitializing) return;
|
||||
_isInitializing = true;
|
||||
|
||||
final tStart = DateTime.now();
|
||||
debugPrint('⏱️ T3 后台初始化开始');
|
||||
|
||||
// 1. PlaybackService(轻量级)
|
||||
// ----- 1. PlaybackService -----
|
||||
final t1 = DateTime.now();
|
||||
try {
|
||||
PlaybackService().init();
|
||||
debugPrint('⏱️ T3.1 PlaybackService 初始化完成');
|
||||
debugPrint('⏱️ T3.1 PlaybackService 完成: ${_elapsedMs(t1)}ms');
|
||||
} catch (e) {
|
||||
debugPrint('❌ PlaybackService 初始化失败: $e');
|
||||
debugPrint('❌ PlaybackService 失败: $e');
|
||||
}
|
||||
|
||||
// 2. ⭐ AudioService 暂时禁用(等 MainActivity 改为 AudioServiceActivity 后启用)
|
||||
// await _initAudioService();
|
||||
|
||||
// 3. WebDAV
|
||||
await _initWebDAV();
|
||||
|
||||
// 4. 通知权限
|
||||
await _requestPermissions();
|
||||
|
||||
debugPrint('⏱️ T4 全部初始化完成');
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// 子初始化方法
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
||||
// ⭐ 暂时禁用,等 MainActivity 改为 AudioServiceActivity 后启用
|
||||
// Future<void> _initAudioService() async {
|
||||
// try {
|
||||
// await audio_service.AudioService.init(
|
||||
// builder: () => _audioHandler!,
|
||||
// config: const audio_service.AudioServiceConfig(
|
||||
// androidNotificationChannelId: 'com.example.qt_player.music',
|
||||
// androidNotificationChannelName: '清听音乐播放',
|
||||
// androidNotificationOngoing: true,
|
||||
// androidStopForegroundOnPause: true,
|
||||
// ),
|
||||
// );
|
||||
// debugPrint('✅ AudioService 初始化完成');
|
||||
// } catch (e) {
|
||||
// debugPrint('❌ AudioService 初始化失败: $e');
|
||||
// }
|
||||
// }
|
||||
|
||||
Future<void> _initWebDAV() async {
|
||||
// ----- 2. WebDAV(只加载凭证) -----
|
||||
final t2 = DateTime.now();
|
||||
try {
|
||||
final hasCred = await WebDAVService.instance.loadCredentials();
|
||||
debugPrint('✅ WebDAV 加载完成, 已连接: $hasCred');
|
||||
debugPrint('⏱️ T3.2 WebDAV 凭证加载完成: ${_elapsedMs(t2)}ms, 有凭证: $hasCred');
|
||||
} catch (e) {
|
||||
debugPrint('❌ WebDAV 加载失败: $e');
|
||||
}
|
||||
debugPrint('❌ WebDAV 凭证加载失败: $e');
|
||||
}
|
||||
|
||||
Future<void> _requestPermissions() async {
|
||||
// ----- 3. ⭐ AudioService(后台播放核心) -----
|
||||
await _initAudioService();
|
||||
|
||||
// ----- 4. 通知权限 -----
|
||||
final t3 = DateTime.now();
|
||||
try {
|
||||
if (await Permission.notification.isDenied) {
|
||||
final status = await Permission.notification.request();
|
||||
debugPrint('📢 通知权限状态: $status');
|
||||
await Permission.notification.request();
|
||||
debugPrint('⏱️ T3.3 通知权限完成: ${_elapsedMs(t3)}ms');
|
||||
} else {
|
||||
debugPrint('⏱️ T3.3 通知权限已授予: ${_elapsedMs(t3)}ms');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ 权限申请失败: $e');
|
||||
}
|
||||
|
||||
final tEnd = DateTime.now();
|
||||
debugPrint('⏱️ T4 全部初始化完成: ${tEnd.difference(tStart).inMilliseconds}ms');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// ⭐ AudioService 初始化(后台播放核心)
|
||||
// ════════════════════════════════════════════════════════════
|
||||
Future<void> _initAudioService() async {
|
||||
try {
|
||||
await audio_service.AudioService.init(
|
||||
builder: () => _audioHandler!,
|
||||
config: const audio_service.AudioServiceConfig(
|
||||
androidNotificationChannelId: 'com.example.qt_player.music',
|
||||
androidNotificationChannelName: '清听音乐播放',
|
||||
androidNotificationOngoing: true,
|
||||
androidStopForegroundOnPause: true,
|
||||
),
|
||||
);
|
||||
debugPrint('✅ AudioService 初始化完成');
|
||||
} catch (e) {
|
||||
debugPrint('❌ AudioService 初始化失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// 辅助方法
|
||||
// ════════════════════════════════════════════════════════════
|
||||
String _elapsedMs(DateTime start) {
|
||||
return '${DateTime.now().difference(start).inMilliseconds}';
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// Build
|
||||
// ============================================================
|
||||
// ════════════════════════════════════════════════════════════
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint('⏱️ T1 build: ${stopwatch.elapsedMilliseconds}ms');
|
||||
_buildCount++;
|
||||
debugPrint(
|
||||
'🎨 [QTPlayerApp] build #$_buildCount ${stopwatch.elapsedMilliseconds}ms');
|
||||
|
||||
// 监听歌曲变化,更新通知栏
|
||||
final audioService = context.watch<AudioService>();
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
// lib/services/app_lifecycle_manager.dart
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
/// App 生命周期状态(扩展了 Flutter 原生状态)
|
||||
enum AppLifecycleStatus {
|
||||
/// 应用正在启动(初始化阶段)
|
||||
launching,
|
||||
|
||||
/// 应用在前台,用户可交互
|
||||
resumed,
|
||||
|
||||
/// 应用在前台但不可交互(如:来电、系统弹窗)
|
||||
inactive,
|
||||
|
||||
/// 应用在后台
|
||||
paused,
|
||||
|
||||
/// 应用即将被销毁
|
||||
detached,
|
||||
|
||||
/// 应用已进入后台且播放停止(自定义,用于播放策略)
|
||||
backgroundIdle,
|
||||
}
|
||||
|
||||
/// App 生命周期管理器
|
||||
///
|
||||
/// 职责:
|
||||
/// 1. 统一监听 Flutter 的 AppLifecycleState
|
||||
/// 2. 管理播放器在前后台的行为
|
||||
/// 3. 管理渲染调度器的运行状态
|
||||
/// 4. 管理 WebDAV 的缓存策略
|
||||
class AppLifecycleManager extends ChangeNotifier {
|
||||
static final AppLifecycleManager _instance = AppLifecycleManager._internal();
|
||||
factory AppLifecycleManager() => _instance;
|
||||
AppLifecycleManager._internal();
|
||||
|
||||
// ---- 当前状态 ----
|
||||
AppLifecycleStatus _status = AppLifecycleStatus.launching;
|
||||
AppLifecycleStatus get status => _status;
|
||||
|
||||
// ---- 上次进入后台的时间 ----
|
||||
DateTime? _backgroundedAt;
|
||||
DateTime? get backgroundedAt => _backgroundedAt;
|
||||
|
||||
// ---- 是否在后台 ----
|
||||
bool get isInBackground =>
|
||||
_status == AppLifecycleStatus.paused ||
|
||||
_status == AppLifecycleStatus.inactive ||
|
||||
_status == AppLifecycleStatus.detached;
|
||||
|
||||
bool get isInForeground => _status == AppLifecycleStatus.resumed;
|
||||
|
||||
// ---- 后台时长 ----
|
||||
Duration get backgroundDuration {
|
||||
if (_backgroundedAt == null) return Duration.zero;
|
||||
return DateTime.now().difference(_backgroundedAt!);
|
||||
}
|
||||
|
||||
// ---- 是否处于"长后台"(超过 5 分钟) ----
|
||||
bool get isLongBackground => backgroundDuration > const Duration(minutes: 5);
|
||||
|
||||
// ---- 监听器 ----
|
||||
final List<void Function(AppLifecycleStatus)> _listeners = [];
|
||||
|
||||
// ---- Flutter 原生生命周期订阅 ----
|
||||
late final WidgetsBindingObserver _observer;
|
||||
bool _isObserving = false;
|
||||
|
||||
// ---- 初始化 ----
|
||||
void init() {
|
||||
if (_isObserving) return;
|
||||
_isObserving = true;
|
||||
|
||||
_observer = _AppLifecycleObserver(this);
|
||||
WidgetsBinding.instance.addObserver(_observer);
|
||||
|
||||
// 获取初始状态
|
||||
_updateStatus(AppLifecycleStatus.resumed);
|
||||
|
||||
debugPrint('📱 AppLifecycleManager 已初始化');
|
||||
}
|
||||
|
||||
// ---- 状态更新 ----
|
||||
void _handleNativeState(AppLifecycleState state) {
|
||||
final newStatus = _convertNativeState(state);
|
||||
|
||||
// 状态变化时更新
|
||||
if (newStatus != _status) {
|
||||
_updateStatus(newStatus);
|
||||
}
|
||||
|
||||
// 记录进入后台的时间
|
||||
if (newStatus == AppLifecycleStatus.paused ||
|
||||
newStatus == AppLifecycleStatus.inactive) {
|
||||
if (_backgroundedAt == null) {
|
||||
_backgroundedAt = DateTime.now();
|
||||
debugPrint('📱 App 进入后台: ${_backgroundedAt}');
|
||||
}
|
||||
}
|
||||
|
||||
// 回到前台时,如果是长后台,触发通知
|
||||
if (newStatus == AppLifecycleStatus.resumed) {
|
||||
if (_backgroundedAt != null) {
|
||||
final duration = backgroundDuration;
|
||||
debugPrint('📱 App 回到前台,后台时长: ${duration.inMinutes}分钟');
|
||||
_backgroundedAt = null;
|
||||
|
||||
// 如果是长后台,通知监听器做重连等操作
|
||||
if (duration > const Duration(minutes: 5)) {
|
||||
_notifyListeners(AppLifecycleStatus.backgroundIdle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _updateStatus(AppLifecycleStatus newStatus) {
|
||||
final oldStatus = _status;
|
||||
_status = newStatus;
|
||||
debugPrint('📱 生命周期状态变化: $oldStatus → $newStatus');
|
||||
notifyListeners();
|
||||
_notifyListeners(newStatus);
|
||||
}
|
||||
|
||||
AppLifecycleStatus _convertNativeState(AppLifecycleState state) {
|
||||
switch (state) {
|
||||
case AppLifecycleState.resumed:
|
||||
return AppLifecycleStatus.resumed;
|
||||
case AppLifecycleState.inactive:
|
||||
return AppLifecycleStatus.inactive;
|
||||
case AppLifecycleState.paused:
|
||||
return AppLifecycleStatus.paused;
|
||||
case AppLifecycleState.detached:
|
||||
return AppLifecycleStatus.detached;
|
||||
default:
|
||||
return AppLifecycleStatus.paused;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 自定义监听器管理 ----
|
||||
void addListenerToManager(void Function(AppLifecycleStatus) listener) {
|
||||
_listeners.add(listener);
|
||||
}
|
||||
|
||||
void removeListenerFromManager(void Function(AppLifecycleStatus) listener) {
|
||||
_listeners.remove(listener);
|
||||
}
|
||||
|
||||
void _notifyListeners(AppLifecycleStatus status) {
|
||||
for (final listener in _listeners) {
|
||||
try {
|
||||
listener(status);
|
||||
} catch (e) {
|
||||
debugPrint('❌ 生命周期监听器执行失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 检查是否需要重新连接 WebDAV ----
|
||||
bool get shouldRecheckWebDAV {
|
||||
if (!isInForeground) return false;
|
||||
if (_backgroundedAt == null) return false;
|
||||
return isLongBackground;
|
||||
}
|
||||
|
||||
// ---- 释放 ----
|
||||
void dispose() {
|
||||
if (_isObserving) {
|
||||
WidgetsBinding.instance.removeObserver(_observer);
|
||||
_isObserving = false;
|
||||
}
|
||||
_listeners.clear();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// 内部观察者(绑定 WidgetsBinding)
|
||||
class _AppLifecycleObserver extends WidgetsBindingObserver {
|
||||
final AppLifecycleManager _manager;
|
||||
|
||||
_AppLifecycleObserver(this._manager);
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
_manager._handleNativeState(state);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user