优化录制页性能
This commit is contained in:
@@ -9,9 +9,11 @@ import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:recording_tool/app/router/app_navigator.dart';
|
||||
import 'package:recording_tool/core/platform/app_platform_info.dart';
|
||||
import 'package:recording_tool/core/platform/device_health_checker.dart';
|
||||
import 'package:recording_tool/core/platform/device_health_snapshot.dart';
|
||||
import 'package:recording_tool/features/recording/dialog/dialog-record.dart';
|
||||
import 'package:recording_tool/features/recording/model/model_recording_context.dart';
|
||||
import 'package:recording_tool/features/recording/platform/recording_platform.dart';
|
||||
import 'package:recording_tool/features/recording/utils/recording_performance.dart';
|
||||
import 'package:recording_tool/features/recording/view-model/view_model_recording.dart';
|
||||
import 'package:recording_tool/features/recording/widgets/widget_camera_preview.dart';
|
||||
import 'package:recording_tool/features/recording/widgets/widget_record_footer.dart';
|
||||
@@ -45,15 +47,23 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
||||
var _immersiveApplied = false;
|
||||
var _previewReady = false;
|
||||
var _stoppingByUser = false;
|
||||
var _bootstrapScheduled = false;
|
||||
var _bootstrapStarted = false;
|
||||
var _releasingResources = false;
|
||||
var _controllerDisposed = false;
|
||||
Animation<double>? _routeAnimation;
|
||||
DeviceHealthSnapshot? _deviceHealthSnapshot;
|
||||
Future<void>? _deviceHealthFuture;
|
||||
final Stopwatch _pageEntryStopwatch = Stopwatch();
|
||||
String? _mainCameraId;
|
||||
String? _ultraWideCameraId;
|
||||
double _ultraWideZoomRatio = 1.0;
|
||||
|
||||
@override
|
||||
/// 首帧后初始化录制流程
|
||||
/// 创建推流控制器,业务初始化等待路由动画结束。
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageEntryStopwatch.start();
|
||||
_streamController = ApiVideoLiveStreamController(
|
||||
initialAudioConfig: AudioConfig(bitrate: 128000),
|
||||
initialVideoConfig: VideoConfig.withDefaultBitrate(
|
||||
@@ -88,20 +98,65 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
||||
.setError(error.toString());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final animation = ModalRoute.of(context)?.animation;
|
||||
if (identical(animation, _routeAnimation)) return;
|
||||
_routeAnimation?.removeStatusListener(_handleRouteAnimationStatus);
|
||||
_routeAnimation = animation;
|
||||
if (animation == null || animation.status == AnimationStatus.completed) {
|
||||
_scheduleBootstrap();
|
||||
} else {
|
||||
animation.addStatusListener(_handleRouteAnimationStatus);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleRouteAnimationStatus(AnimationStatus status) {
|
||||
if (status != AnimationStatus.completed) return;
|
||||
logRecordingPerformance(
|
||||
'route transition completed',
|
||||
_pageEntryStopwatch.elapsed,
|
||||
);
|
||||
_scheduleBootstrap();
|
||||
}
|
||||
|
||||
void _scheduleBootstrap() {
|
||||
if (_bootstrapScheduled || _bootstrapStarted || _releasingResources) return;
|
||||
_bootstrapScheduled = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || _releasingResources || _bootstrapStarted) return;
|
||||
_bootstrapStarted = true;
|
||||
_bootstrapScheduled = false;
|
||||
_routeAnimation?.removeStatusListener(_handleRouteAnimationStatus);
|
||||
_routeAnimation = null;
|
||||
ref
|
||||
.read(recordingViewModelProvider.notifier)
|
||||
.setRecordingContext(widget.recordingContext);
|
||||
_bootstrap();
|
||||
unawaited(_bootstrap());
|
||||
});
|
||||
}
|
||||
|
||||
/// 检查设备健康状态并弹窗提示
|
||||
Future<void> _checkAndShowDeviceHealthAlerts() async {
|
||||
final snapshot = await AppPlatformInfo.deviceHealth();
|
||||
if (!mounted) return;
|
||||
Future<void> _loadDeviceHealth() async {
|
||||
try {
|
||||
_deviceHealthSnapshot = await measureRecordingOperation(
|
||||
'device health',
|
||||
AppPlatformInfo.deviceHealth,
|
||||
);
|
||||
} catch (error) {
|
||||
debugPrint('读取设备健康状态失败: $error');
|
||||
}
|
||||
}
|
||||
|
||||
/// 使用当前页面缓存的健康状态,在开始录制前按需提示。
|
||||
Future<void> _checkAndShowDeviceHealthAlerts() async {
|
||||
_deviceHealthFuture ??= _loadDeviceHealth();
|
||||
await _deviceHealthFuture;
|
||||
if (!mounted) return;
|
||||
final snapshot = _deviceHealthSnapshot;
|
||||
if (snapshot == null) return;
|
||||
final lines = DeviceHealthChecker.warningLines(snapshot);
|
||||
if (lines.isEmpty) return;
|
||||
|
||||
@@ -112,16 +167,40 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 页面启动:健康检查、进入录制模式、准备相机会话
|
||||
/// 路由动画完成后再准备权限和相机,避免阻塞转场。
|
||||
Future<void> _bootstrap() async {
|
||||
await _checkAndShowDeviceHealthAlerts();
|
||||
await measureRecordingOperation(
|
||||
'enter immersive mode',
|
||||
_enterRecordingMode,
|
||||
);
|
||||
if (!mounted) return;
|
||||
final permissions = await ref
|
||||
.read(recordingViewModelProvider.notifier)
|
||||
.prepareRequiredPermissions();
|
||||
if (!mounted) return;
|
||||
if (!permissions.allGranted) return;
|
||||
await measureRecordingOperation(
|
||||
'preview initialize',
|
||||
_initializeLiveStreamPreview,
|
||||
);
|
||||
if (!mounted || !_previewReady) return;
|
||||
_deviceHealthFuture ??= _loadDeviceHealth();
|
||||
unawaited(_loadPostPreviewTasks());
|
||||
}
|
||||
|
||||
await _enterRecordingMode();
|
||||
if (!mounted) return;
|
||||
await ref.read(recordingViewModelProvider.notifier).prepareSession();
|
||||
if (!mounted) return;
|
||||
await _initializeLiveStreamPreview();
|
||||
Future<void> _loadPostPreviewTasks() async {
|
||||
try {
|
||||
await Future.wait([
|
||||
measureRecordingOperation(
|
||||
'camera capabilities',
|
||||
_loadBackCameraCapabilities,
|
||||
),
|
||||
ref.read(recordingViewModelProvider.notifier).loadSystemAdvisories(),
|
||||
_deviceHealthFuture ??= _loadDeviceHealth(),
|
||||
]);
|
||||
} catch (error) {
|
||||
debugPrint('加载录制页辅助状态失败: $error');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initializeLiveStreamPreview() async {
|
||||
@@ -132,7 +211,6 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
||||
ref
|
||||
.read(recordingViewModelProvider.notifier)
|
||||
.setPreviewReady(ready: true);
|
||||
await _loadBackCameraCapabilities();
|
||||
} on PlatformException catch (error) {
|
||||
if (!mounted) return;
|
||||
ref
|
||||
@@ -154,6 +232,7 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
||||
Future<void> _loadBackCameraCapabilities() async {
|
||||
try {
|
||||
final cameras = await _streamController.getBackCameras();
|
||||
if (!mounted || _releasingResources) return;
|
||||
if (cameras.isEmpty) return;
|
||||
final main = cameras.first;
|
||||
final widest = cameras.reduce(
|
||||
@@ -179,6 +258,7 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
||||
maxZoomRatio: 1.0,
|
||||
);
|
||||
} catch (_) {
|
||||
if (!mounted || _releasingResources) return;
|
||||
ref
|
||||
.read(recordingViewModelProvider.notifier)
|
||||
.updateZoomCapabilities(
|
||||
@@ -217,8 +297,17 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
||||
final ready = ref.read(recordingViewModelProvider).session.isPreviewReady;
|
||||
if (ready) return true;
|
||||
if (!mounted) return false;
|
||||
AppToast.show('相机预览启动失败,请重试');
|
||||
return false;
|
||||
await measureRecordingOperation(
|
||||
'preview initialize after permission',
|
||||
_initializeLiveStreamPreview,
|
||||
);
|
||||
if (!mounted || !_previewReady) {
|
||||
AppToast.show('相机预览启动失败,请重试');
|
||||
return false;
|
||||
}
|
||||
_deviceHealthFuture ??= _loadDeviceHealth();
|
||||
unawaited(_loadPostPreviewTasks());
|
||||
return true;
|
||||
}
|
||||
if (!mounted) return false;
|
||||
|
||||
@@ -337,50 +426,49 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 退出沉浸式并释放录制会话。
|
||||
/// 先恢复系统栏,再错开一帧释放编码器,减轻返回动画卡顿。
|
||||
Future<void> _exitRecordingMode() async {
|
||||
await _restoreSystemUiIfNeeded();
|
||||
if (mounted) {
|
||||
await ref.read(recordingViewModelProvider.notifier).teardown();
|
||||
}
|
||||
|
||||
// 让 pop 动画先跑起来,再做 MediaCodec 释放。
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
await _disposeStreamController();
|
||||
}
|
||||
|
||||
Future<void> _restoreSystemUiIfNeeded() async {
|
||||
if (!_immersiveApplied) return;
|
||||
_immersiveApplied = false;
|
||||
await SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
await RecordingPlatform.setImmersiveMode(enabled: false);
|
||||
}
|
||||
|
||||
@override
|
||||
/// 页面销毁时兜底恢复系统 UI 并释放推流控制器
|
||||
/// 路由反向动画结束、页面销毁后统一释放资源。
|
||||
void dispose() {
|
||||
if (_immersiveApplied) {
|
||||
_immersiveApplied = false;
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
unawaited(RecordingPlatform.setImmersiveMode(enabled: false));
|
||||
}
|
||||
unawaited(_disposeStreamController());
|
||||
_releasingResources = true;
|
||||
_routeAnimation?.removeStatusListener(_handleRouteAnimationStatus);
|
||||
_routeAnimation = null;
|
||||
final viewModel = ref.read(recordingViewModelProvider.notifier);
|
||||
unawaited(_releaseResources(viewModel));
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 只 dispose 一次;原生 dispose 内部已 stopStream,避免 stop+dispose 双次停流。
|
||||
Future<void> _releaseResources(RecordingViewModel viewModel) async {
|
||||
await measureRecordingOperation('page resource release', () async {
|
||||
if (_immersiveApplied) {
|
||||
_immersiveApplied = false;
|
||||
try {
|
||||
await SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
await RecordingPlatform.setImmersiveMode(enabled: false);
|
||||
} catch (error) {
|
||||
debugPrint('恢复系统栏失败: $error');
|
||||
}
|
||||
}
|
||||
try {
|
||||
await viewModel.teardown();
|
||||
} catch (error) {
|
||||
debugPrint('清理录制状态失败: $error');
|
||||
}
|
||||
await _disposeStreamController();
|
||||
});
|
||||
}
|
||||
|
||||
/// 只 dispose 一次;原生 dispose 统一停止推流和预览。
|
||||
Future<void> _disposeStreamController() async {
|
||||
if (_controllerDisposed) return;
|
||||
_controllerDisposed = true;
|
||||
try {
|
||||
await _streamController.dispose();
|
||||
await measureRecordingOperation(
|
||||
'stream controller dispose',
|
||||
_streamController.dispose,
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
debugPrint('释放推流控制器失败: $error\n$stackTrace');
|
||||
}
|
||||
@@ -390,7 +478,6 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
||||
/// 构建录制页 UI
|
||||
Widget build(BuildContext context) {
|
||||
return _RecordingPopScope(
|
||||
onExitRecordingMode: _exitRecordingMode,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Column(
|
||||
@@ -426,12 +513,8 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
||||
}
|
||||
|
||||
class _RecordingPopScope extends ConsumerWidget {
|
||||
const _RecordingPopScope({
|
||||
required this.onExitRecordingMode,
|
||||
required this.child,
|
||||
});
|
||||
const _RecordingPopScope({required this.child});
|
||||
|
||||
final Future<void> Function() onExitRecordingMode;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
@@ -443,11 +526,7 @@ class _RecordingPopScope extends ConsumerWidget {
|
||||
return PopScope(
|
||||
canPop: !isRecording,
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
if (didPop) {
|
||||
// 不 await,避免把 MediaCodec 释放堵在 Pop 回调上。
|
||||
unawaited(onExitRecordingMode());
|
||||
return;
|
||||
}
|
||||
if (didPop) return;
|
||||
if (isRecording) {
|
||||
AppToast.show('录制中无法返回,请先停止录制');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user