优化录制页性能
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/app/router/app_navigator.dart';
|
||||||
import 'package:recording_tool/core/platform/app_platform_info.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_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/dialog/dialog-record.dart';
|
||||||
import 'package:recording_tool/features/recording/model/model_recording_context.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/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/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_camera_preview.dart';
|
||||||
import 'package:recording_tool/features/recording/widgets/widget_record_footer.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 _immersiveApplied = false;
|
||||||
var _previewReady = false;
|
var _previewReady = false;
|
||||||
var _stoppingByUser = false;
|
var _stoppingByUser = false;
|
||||||
|
var _bootstrapScheduled = false;
|
||||||
|
var _bootstrapStarted = false;
|
||||||
|
var _releasingResources = false;
|
||||||
var _controllerDisposed = false;
|
var _controllerDisposed = false;
|
||||||
|
Animation<double>? _routeAnimation;
|
||||||
|
DeviceHealthSnapshot? _deviceHealthSnapshot;
|
||||||
|
Future<void>? _deviceHealthFuture;
|
||||||
|
final Stopwatch _pageEntryStopwatch = Stopwatch();
|
||||||
String? _mainCameraId;
|
String? _mainCameraId;
|
||||||
String? _ultraWideCameraId;
|
String? _ultraWideCameraId;
|
||||||
double _ultraWideZoomRatio = 1.0;
|
double _ultraWideZoomRatio = 1.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
/// 首帧后初始化录制流程
|
/// 创建推流控制器,业务初始化等待路由动画结束。
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_pageEntryStopwatch.start();
|
||||||
_streamController = ApiVideoLiveStreamController(
|
_streamController = ApiVideoLiveStreamController(
|
||||||
initialAudioConfig: AudioConfig(bitrate: 128000),
|
initialAudioConfig: AudioConfig(bitrate: 128000),
|
||||||
initialVideoConfig: VideoConfig.withDefaultBitrate(
|
initialVideoConfig: VideoConfig.withDefaultBitrate(
|
||||||
@@ -88,20 +98,65 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
|||||||
.setError(error.toString());
|
.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((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted || _releasingResources || _bootstrapStarted) return;
|
||||||
|
_bootstrapStarted = true;
|
||||||
|
_bootstrapScheduled = false;
|
||||||
|
_routeAnimation?.removeStatusListener(_handleRouteAnimationStatus);
|
||||||
|
_routeAnimation = null;
|
||||||
ref
|
ref
|
||||||
.read(recordingViewModelProvider.notifier)
|
.read(recordingViewModelProvider.notifier)
|
||||||
.setRecordingContext(widget.recordingContext);
|
.setRecordingContext(widget.recordingContext);
|
||||||
_bootstrap();
|
unawaited(_bootstrap());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 检查设备健康状态并弹窗提示
|
Future<void> _loadDeviceHealth() async {
|
||||||
Future<void> _checkAndShowDeviceHealthAlerts() async {
|
try {
|
||||||
final snapshot = await AppPlatformInfo.deviceHealth();
|
_deviceHealthSnapshot = await measureRecordingOperation(
|
||||||
if (!mounted) return;
|
'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);
|
final lines = DeviceHealthChecker.warningLines(snapshot);
|
||||||
if (lines.isEmpty) return;
|
if (lines.isEmpty) return;
|
||||||
|
|
||||||
@@ -112,16 +167,40 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 页面启动:健康检查、进入录制模式、准备相机会话
|
/// 路由动画完成后再准备权限和相机,避免阻塞转场。
|
||||||
Future<void> _bootstrap() async {
|
Future<void> _bootstrap() async {
|
||||||
await _checkAndShowDeviceHealthAlerts();
|
await measureRecordingOperation(
|
||||||
|
'enter immersive mode',
|
||||||
|
_enterRecordingMode,
|
||||||
|
);
|
||||||
if (!mounted) return;
|
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();
|
Future<void> _loadPostPreviewTasks() async {
|
||||||
if (!mounted) return;
|
try {
|
||||||
await ref.read(recordingViewModelProvider.notifier).prepareSession();
|
await Future.wait([
|
||||||
if (!mounted) return;
|
measureRecordingOperation(
|
||||||
await _initializeLiveStreamPreview();
|
'camera capabilities',
|
||||||
|
_loadBackCameraCapabilities,
|
||||||
|
),
|
||||||
|
ref.read(recordingViewModelProvider.notifier).loadSystemAdvisories(),
|
||||||
|
_deviceHealthFuture ??= _loadDeviceHealth(),
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('加载录制页辅助状态失败: $error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initializeLiveStreamPreview() async {
|
Future<void> _initializeLiveStreamPreview() async {
|
||||||
@@ -132,7 +211,6 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
|||||||
ref
|
ref
|
||||||
.read(recordingViewModelProvider.notifier)
|
.read(recordingViewModelProvider.notifier)
|
||||||
.setPreviewReady(ready: true);
|
.setPreviewReady(ready: true);
|
||||||
await _loadBackCameraCapabilities();
|
|
||||||
} on PlatformException catch (error) {
|
} on PlatformException catch (error) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ref
|
ref
|
||||||
@@ -154,6 +232,7 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
|||||||
Future<void> _loadBackCameraCapabilities() async {
|
Future<void> _loadBackCameraCapabilities() async {
|
||||||
try {
|
try {
|
||||||
final cameras = await _streamController.getBackCameras();
|
final cameras = await _streamController.getBackCameras();
|
||||||
|
if (!mounted || _releasingResources) return;
|
||||||
if (cameras.isEmpty) return;
|
if (cameras.isEmpty) return;
|
||||||
final main = cameras.first;
|
final main = cameras.first;
|
||||||
final widest = cameras.reduce(
|
final widest = cameras.reduce(
|
||||||
@@ -179,6 +258,7 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
|||||||
maxZoomRatio: 1.0,
|
maxZoomRatio: 1.0,
|
||||||
);
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
if (!mounted || _releasingResources) return;
|
||||||
ref
|
ref
|
||||||
.read(recordingViewModelProvider.notifier)
|
.read(recordingViewModelProvider.notifier)
|
||||||
.updateZoomCapabilities(
|
.updateZoomCapabilities(
|
||||||
@@ -217,9 +297,18 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
|||||||
final ready = ref.read(recordingViewModelProvider).session.isPreviewReady;
|
final ready = ref.read(recordingViewModelProvider).session.isPreviewReady;
|
||||||
if (ready) return true;
|
if (ready) return true;
|
||||||
if (!mounted) return false;
|
if (!mounted) return false;
|
||||||
|
await measureRecordingOperation(
|
||||||
|
'preview initialize after permission',
|
||||||
|
_initializeLiveStreamPreview,
|
||||||
|
);
|
||||||
|
if (!mounted || !_previewReady) {
|
||||||
AppToast.show('相机预览启动失败,请重试');
|
AppToast.show('相机预览启动失败,请重试');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
_deviceHealthFuture ??= _loadDeviceHealth();
|
||||||
|
unawaited(_loadPostPreviewTasks());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (!mounted) return false;
|
if (!mounted) return false;
|
||||||
|
|
||||||
await RecordDialog.showSingle(
|
await RecordDialog.showSingle(
|
||||||
@@ -337,50 +426,49 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 退出沉浸式并释放录制会话。
|
@override
|
||||||
/// 先恢复系统栏,再错开一帧释放编码器,减轻返回动画卡顿。
|
/// 路由反向动画结束、页面销毁后统一释放资源。
|
||||||
Future<void> _exitRecordingMode() async {
|
void dispose() {
|
||||||
await _restoreSystemUiIfNeeded();
|
_releasingResources = true;
|
||||||
if (mounted) {
|
_routeAnimation?.removeStatusListener(_handleRouteAnimationStatus);
|
||||||
await ref.read(recordingViewModelProvider.notifier).teardown();
|
_routeAnimation = null;
|
||||||
|
final viewModel = ref.read(recordingViewModelProvider.notifier);
|
||||||
|
unawaited(_releaseResources(viewModel));
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 让 pop 动画先跑起来,再做 MediaCodec 释放。
|
Future<void> _releaseResources(RecordingViewModel viewModel) async {
|
||||||
await Future<void>.delayed(Duration.zero);
|
await measureRecordingOperation('page resource release', () async {
|
||||||
await _disposeStreamController();
|
if (_immersiveApplied) {
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _restoreSystemUiIfNeeded() async {
|
|
||||||
if (!_immersiveApplied) return;
|
|
||||||
_immersiveApplied = false;
|
_immersiveApplied = false;
|
||||||
|
try {
|
||||||
await SystemChrome.setEnabledSystemUIMode(
|
await SystemChrome.setEnabledSystemUIMode(
|
||||||
SystemUiMode.manual,
|
SystemUiMode.manual,
|
||||||
overlays: SystemUiOverlay.values,
|
overlays: SystemUiOverlay.values,
|
||||||
);
|
);
|
||||||
await RecordingPlatform.setImmersiveMode(enabled: false);
|
await RecordingPlatform.setImmersiveMode(enabled: false);
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('恢复系统栏失败: $error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await viewModel.teardown();
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('清理录制状态失败: $error');
|
||||||
|
}
|
||||||
|
await _disposeStreamController();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
/// 只 dispose 一次;原生 dispose 统一停止推流和预览。
|
||||||
/// 页面销毁时兜底恢复系统 UI 并释放推流控制器
|
|
||||||
void dispose() {
|
|
||||||
if (_immersiveApplied) {
|
|
||||||
_immersiveApplied = false;
|
|
||||||
SystemChrome.setEnabledSystemUIMode(
|
|
||||||
SystemUiMode.manual,
|
|
||||||
overlays: SystemUiOverlay.values,
|
|
||||||
);
|
|
||||||
unawaited(RecordingPlatform.setImmersiveMode(enabled: false));
|
|
||||||
}
|
|
||||||
unawaited(_disposeStreamController());
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 只 dispose 一次;原生 dispose 内部已 stopStream,避免 stop+dispose 双次停流。
|
|
||||||
Future<void> _disposeStreamController() async {
|
Future<void> _disposeStreamController() async {
|
||||||
if (_controllerDisposed) return;
|
if (_controllerDisposed) return;
|
||||||
_controllerDisposed = true;
|
_controllerDisposed = true;
|
||||||
try {
|
try {
|
||||||
await _streamController.dispose();
|
await measureRecordingOperation(
|
||||||
|
'stream controller dispose',
|
||||||
|
_streamController.dispose,
|
||||||
|
);
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
debugPrint('释放推流控制器失败: $error\n$stackTrace');
|
debugPrint('释放推流控制器失败: $error\n$stackTrace');
|
||||||
}
|
}
|
||||||
@@ -390,7 +478,6 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
|||||||
/// 构建录制页 UI
|
/// 构建录制页 UI
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return _RecordingPopScope(
|
return _RecordingPopScope(
|
||||||
onExitRecordingMode: _exitRecordingMode,
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
body: Column(
|
body: Column(
|
||||||
@@ -426,12 +513,8 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RecordingPopScope extends ConsumerWidget {
|
class _RecordingPopScope extends ConsumerWidget {
|
||||||
const _RecordingPopScope({
|
const _RecordingPopScope({required this.child});
|
||||||
required this.onExitRecordingMode,
|
|
||||||
required this.child,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Future<void> Function() onExitRecordingMode;
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -443,11 +526,7 @@ class _RecordingPopScope extends ConsumerWidget {
|
|||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: !isRecording,
|
canPop: !isRecording,
|
||||||
onPopInvokedWithResult: (didPop, result) {
|
onPopInvokedWithResult: (didPop, result) {
|
||||||
if (didPop) {
|
if (didPop) return;
|
||||||
// 不 await,避免把 MediaCodec 释放堵在 Pop 回调上。
|
|
||||||
unawaited(onExitRecordingMode());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isRecording) {
|
if (isRecording) {
|
||||||
AppToast.show('录制中无法返回,请先停止录制');
|
AppToast.show('录制中无法返回,请先停止录制');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
Future<T> measureRecordingOperation<T>(
|
||||||
|
String label,
|
||||||
|
Future<T> Function() operation,
|
||||||
|
) async {
|
||||||
|
if (kReleaseMode) {
|
||||||
|
return operation();
|
||||||
|
}
|
||||||
|
|
||||||
|
final stopwatch = Stopwatch()..start();
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} finally {
|
||||||
|
stopwatch.stop();
|
||||||
|
debugPrint(
|
||||||
|
'[RecordingPerformance] $label: ${stopwatch.elapsedMilliseconds}ms',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void logRecordingPerformance(String label, Duration elapsed) {
|
||||||
|
if (kReleaseMode) return;
|
||||||
|
debugPrint('[RecordingPerformance] $label: ${elapsed.inMilliseconds}ms');
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import 'package:recording_tool/features/recording/model/model_recording.dart';
|
|||||||
import 'package:recording_tool/features/recording/model/model_recording_context.dart';
|
import 'package:recording_tool/features/recording/model/model_recording_context.dart';
|
||||||
import 'package:recording_tool/features/recording/model/model_recording_session.dart';
|
import 'package:recording_tool/features/recording/model/model_recording_session.dart';
|
||||||
import 'package:recording_tool/features/recording/platform/recording_platform.dart';
|
import 'package:recording_tool/features/recording/platform/recording_platform.dart';
|
||||||
|
import 'package:recording_tool/features/recording/utils/recording_performance.dart';
|
||||||
|
|
||||||
/// 录制页状态 Provider。
|
/// 录制页状态 Provider。
|
||||||
final recordingViewModelProvider =
|
final recordingViewModelProvider =
|
||||||
@@ -32,6 +33,8 @@ class RecordingRequiredPermissions {
|
|||||||
class RecordingViewModel extends Notifier<RecordingModel> {
|
class RecordingViewModel extends Notifier<RecordingModel> {
|
||||||
Timer? _elapsedTimer;
|
Timer? _elapsedTimer;
|
||||||
DateTime? _recordingStartedAt;
|
DateTime? _recordingStartedAt;
|
||||||
|
RecordingRequiredPermissions? _cachedRequiredPermissions;
|
||||||
|
var _sessionGeneration = 0;
|
||||||
|
|
||||||
/// 初始化状态并注册销毁回调。
|
/// 初始化状态并注册销毁回调。
|
||||||
@override
|
@override
|
||||||
@@ -52,55 +55,88 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
|||||||
state = state.copyWith(recordingContext: recordingContext);
|
state = state.copyWith(recordingContext: recordingContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 申请权限并检查系统设置。
|
/// 准备相机和麦克风权限;已授权时复用当前会话结果。
|
||||||
Future<void> prepareSession() async {
|
Future<RecordingRequiredPermissions> prepareRequiredPermissions({
|
||||||
|
bool forceRefresh = false,
|
||||||
|
}) async {
|
||||||
|
final generation = _sessionGeneration;
|
||||||
if (!RecordingPlatform.isSupported) {
|
if (!RecordingPlatform.isSupported) {
|
||||||
_updateSession((s) => s.copyWith(errorMessage: '当前设备不支持录制'));
|
_updateSession((s) => s.copyWith(errorMessage: '当前设备不支持录制'));
|
||||||
return;
|
return const RecordingRequiredPermissions(
|
||||||
|
cameraGranted: false,
|
||||||
|
microphoneGranted: false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final permissions = await PermissionService.requestMissing([
|
final cached = _cachedRequiredPermissions;
|
||||||
|
if (!forceRefresh && cached?.allGranted == true) {
|
||||||
|
return cached!;
|
||||||
|
}
|
||||||
|
|
||||||
|
final permissions = await measureRecordingOperation(
|
||||||
|
'required permissions',
|
||||||
|
() => PermissionService.requestMissing([
|
||||||
Permission.camera,
|
Permission.camera,
|
||||||
Permission.microphone,
|
Permission.microphone,
|
||||||
if (Platform.isAndroid) Permission.notification,
|
]),
|
||||||
]);
|
);
|
||||||
|
final result = RecordingRequiredPermissions(
|
||||||
|
cameraGranted: _isPermissionGranted(permissions[Permission.camera]),
|
||||||
|
microphoneGranted: _isPermissionGranted(
|
||||||
|
permissions[Permission.microphone],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (generation != _sessionGeneration) return result;
|
||||||
|
_cachedRequiredPermissions = result;
|
||||||
|
final errorMessage = !result.cameraGranted
|
||||||
|
? '需要相机权限才能录制'
|
||||||
|
: (!result.microphoneGranted ? '需要录音权限才能录制' : null);
|
||||||
|
|
||||||
final cameraGranted = permissions[Permission.camera]?.isGranted ?? false;
|
_updateSession(
|
||||||
if (!cameraGranted) {
|
(s) => s.copyWith(
|
||||||
_updateSession((s) => s.copyWith(errorMessage: '需要相机权限才能录制'));
|
isMicrophoneGranted: result.microphoneGranted,
|
||||||
return;
|
errorMessage: errorMessage,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
final microphoneGranted =
|
/// 加载不影响相机首帧的通知、勿扰和电池状态。
|
||||||
permissions[Permission.microphone]?.isGranted ?? false;
|
Future<void> loadSystemAdvisories() async {
|
||||||
|
final generation = _sessionGeneration;
|
||||||
|
final notificationFuture = Platform.isAndroid
|
||||||
|
? PermissionService.requestMissing([Permission.notification])
|
||||||
|
: Future.value(<Permission, PermissionStatus>{});
|
||||||
|
final results = await measureRecordingOperation(
|
||||||
|
'system advisories',
|
||||||
|
() => Future.wait<dynamic>([
|
||||||
|
notificationFuture,
|
||||||
|
RecordingPlatform.hasNotificationPolicyAccess(),
|
||||||
|
RecordingPlatform.isIgnoringBatteryOptimizations(),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
final notificationPermissions =
|
||||||
|
results[0] as Map<Permission, PermissionStatus>;
|
||||||
final notificationsGranted = Platform.isAndroid
|
final notificationsGranted = Platform.isAndroid
|
||||||
? (permissions[Permission.notification]?.isGranted ?? false)
|
? _isPermissionGranted(notificationPermissions[Permission.notification])
|
||||||
: true;
|
: true;
|
||||||
|
final hasDnd = results[1] as bool;
|
||||||
|
final batteryIgnored = results[2] as bool;
|
||||||
|
if (generation != _sessionGeneration) return;
|
||||||
final warnings = <String>[];
|
final warnings = <String>[];
|
||||||
if (Platform.isAndroid && !notificationsGranted) {
|
if (Platform.isAndroid && !notificationsGranted) {
|
||||||
warnings.add('未授予通知权限,录制时可能看不到前台服务通知,系统更容易结束后台录制');
|
warnings.add('未授予通知权限,录制时可能看不到前台服务通知,系统更容易结束后台录制');
|
||||||
}
|
}
|
||||||
if (!microphoneGranted) {
|
|
||||||
warnings.add('未授予麦克风权限,当前将以静音模式录制');
|
|
||||||
}
|
|
||||||
final hasDnd = await RecordingPlatform.hasNotificationPolicyAccess();
|
|
||||||
final batteryIgnored =
|
|
||||||
await RecordingPlatform.isIgnoringBatteryOptimizations();
|
|
||||||
|
|
||||||
_updateSession(
|
_updateSession(
|
||||||
(s) => s.copyWith(
|
(s) => s.copyWith(
|
||||||
hasDndAccess: hasDnd,
|
hasDndAccess: hasDnd,
|
||||||
isBatteryOptimizedIgnored: batteryIgnored,
|
isBatteryOptimizedIgnored: batteryIgnored,
|
||||||
isMicrophoneGranted: microphoneGranted,
|
|
||||||
notificationsGranted: notificationsGranted,
|
notificationsGranted: notificationsGranted,
|
||||||
permissionWarning: warnings.isEmpty ? null : warnings.join('\n'),
|
permissionWarning: warnings.isEmpty ? null : warnings.join('\n'),
|
||||||
errorMessage: null,
|
|
||||||
clearPermissionWarning: warnings.isEmpty,
|
clearPermissionWarning: warnings.isEmpty,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
_updateSession((s) => s.copyWith(errorMessage: null));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPreviewReady({required bool ready, String? errorMessage}) {
|
void setPreviewReady({required bool ready, String? errorMessage}) {
|
||||||
@@ -112,27 +148,8 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
|||||||
/// 检测并尝试申请相机、麦克风权限,同步更新 session 中的 isMicrophoneGranted。
|
/// 检测并尝试申请相机、麦克风权限,同步更新 session 中的 isMicrophoneGranted。
|
||||||
Future<RecordingRequiredPermissions>
|
Future<RecordingRequiredPermissions>
|
||||||
ensureCameraAndMicrophonePermissions() async {
|
ensureCameraAndMicrophonePermissions() async {
|
||||||
final permissions = await PermissionService.requestMissing([
|
final cached = _cachedRequiredPermissions;
|
||||||
Permission.camera,
|
return prepareRequiredPermissions(forceRefresh: cached?.allGranted != true);
|
||||||
Permission.microphone,
|
|
||||||
]);
|
|
||||||
|
|
||||||
final cameraGranted = _isPermissionGranted(permissions[Permission.camera]);
|
|
||||||
final microphoneGranted = _isPermissionGranted(
|
|
||||||
permissions[Permission.microphone],
|
|
||||||
);
|
|
||||||
|
|
||||||
_updateSession((s) => s.copyWith(isMicrophoneGranted: microphoneGranted));
|
|
||||||
|
|
||||||
if (cameraGranted && !state.session.isPreviewReady) {
|
|
||||||
_updateSession((s) => s.copyWith(errorMessage: null));
|
|
||||||
_updateSession((s) => s.copyWith(isPreviewReady: true));
|
|
||||||
}
|
|
||||||
|
|
||||||
return RecordingRequiredPermissions(
|
|
||||||
cameraGranted: cameraGranted,
|
|
||||||
microphoneGranted: microphoneGranted,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isPermissionGranted(PermissionStatus? status) {
|
bool _isPermissionGranted(PermissionStatus? status) {
|
||||||
@@ -282,11 +299,13 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
|||||||
|
|
||||||
/// 退出录制页时释放勿扰和会话状态(沉浸式由页面统一恢复)。
|
/// 退出录制页时释放勿扰和会话状态(沉浸式由页面统一恢复)。
|
||||||
Future<void> teardown() async {
|
Future<void> teardown() async {
|
||||||
await RecordingPlatform.disableDoNotDisturb();
|
_sessionGeneration++;
|
||||||
|
_cachedRequiredPermissions = null;
|
||||||
_recordingStartedAt = null;
|
_recordingStartedAt = null;
|
||||||
_elapsedTimer?.cancel();
|
_elapsedTimer?.cancel();
|
||||||
_elapsedTimer = null;
|
_elapsedTimer = null;
|
||||||
state = state.copyWith(session: const RecordingSessionState());
|
state = state.copyWith(session: const RecordingSessionState());
|
||||||
|
await RecordingPlatform.disableDoNotDisturb();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provider 销毁时取消状态流订阅。
|
/// Provider 销毁时取消状态流订阅。
|
||||||
|
|||||||
+109
-21
@@ -2,6 +2,8 @@ package video.api.flutter.livestream
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
import android.view.Surface
|
import android.view.Surface
|
||||||
import io.flutter.view.TextureRegistry
|
import io.flutter.view.TextureRegistry
|
||||||
@@ -17,7 +19,15 @@ import io.github.thibaultbee.streampack.utils.frontCameraList
|
|||||||
import io.github.thibaultbee.streampack.utils.isBackCamera
|
import io.github.thibaultbee.streampack.utils.isBackCamera
|
||||||
import io.github.thibaultbee.streampack.utils.isExternalCamera
|
import io.github.thibaultbee.streampack.utils.isExternalCamera
|
||||||
import io.github.thibaultbee.streampack.utils.isFrontCamera
|
import io.github.thibaultbee.streampack.utils.isFrontCamera
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class FlutterLiveStreamView(
|
class FlutterLiveStreamView(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@@ -39,9 +49,21 @@ class FlutterLiveStreamView(
|
|||||||
initialOnConnectionListener = this,
|
initialOnConnectionListener = this,
|
||||||
initialOnErrorListener = this
|
initialOnErrorListener = this
|
||||||
)
|
)
|
||||||
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
private val operationScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
private val operationMutex = Mutex()
|
||||||
|
private val disposeLock = Any()
|
||||||
|
private val disposeCallbacks =
|
||||||
|
mutableListOf<Pair<() -> Unit, (Exception) -> Unit>>()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
private var _isPreviewing = false
|
private var _isPreviewing = false
|
||||||
|
@Volatile
|
||||||
private var _isStreaming = false
|
private var _isStreaming = false
|
||||||
|
@Volatile
|
||||||
|
private var _isDisposing = false
|
||||||
|
@Volatile
|
||||||
|
private var _isDisposed = false
|
||||||
val isStreaming: Boolean
|
val isStreaming: Boolean
|
||||||
get() = _isStreaming
|
get() = _isStreaming
|
||||||
|
|
||||||
@@ -63,7 +85,7 @@ class FlutterLiveStreamView(
|
|||||||
|
|
||||||
val wasPreviewing = _isPreviewing
|
val wasPreviewing = _isPreviewing
|
||||||
if (wasPreviewing) {
|
if (wasPreviewing) {
|
||||||
stopPreview()
|
stopPreviewInternal()
|
||||||
}
|
}
|
||||||
streamer.configure(videoConfig)
|
streamer.configure(videoConfig)
|
||||||
_videoConfig = videoConfig
|
_videoConfig = videoConfig
|
||||||
@@ -174,23 +196,58 @@ class FlutterLiveStreamView(
|
|||||||
setCamera(cameraList.first(), onSuccess, onError)
|
setCamera(cameraList.first(), onSuccess, onError)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dispose() {
|
fun dispose(onSuccess: () -> Unit, onError: (Exception) -> Unit) {
|
||||||
|
var shouldStartDispose = false
|
||||||
|
synchronized(disposeLock) {
|
||||||
|
if (_isDisposed) {
|
||||||
|
mainHandler.post(onSuccess)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
disposeCallbacks.add(onSuccess to onError)
|
||||||
|
if (!_isDisposing) {
|
||||||
|
_isDisposing = true
|
||||||
|
shouldStartDispose = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!shouldStartDispose) return
|
||||||
|
|
||||||
|
operationScope.launch {
|
||||||
|
var failure: Exception? = null
|
||||||
|
operationMutex.withLock {
|
||||||
try {
|
try {
|
||||||
stopStream()
|
stopStreamInternal()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.w("ApiVideoLiveStream", "stopStream during dispose failed", e)
|
android.util.Log.w("ApiVideoLiveStream", "stopStream during dispose failed", e)
|
||||||
_isStreaming = false
|
failure = e
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
streamer.stopPreview()
|
stopPreviewInternal()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.w("ApiVideoLiveStream", "stopPreview during dispose failed", e)
|
android.util.Log.w("ApiVideoLiveStream", "stopPreview during dispose failed", e)
|
||||||
|
failure = failure ?: e
|
||||||
}
|
}
|
||||||
_isPreviewing = false
|
}
|
||||||
|
withContext(Dispatchers.Main.immediate) {
|
||||||
|
try {
|
||||||
flutterTexture.release()
|
flutterTexture.release()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
failure = failure ?: e
|
||||||
|
}
|
||||||
|
val callbacks = synchronized(disposeLock) {
|
||||||
|
_isDisposed = true
|
||||||
|
_isDisposing = false
|
||||||
|
disposeCallbacks.toList().also { disposeCallbacks.clear() }
|
||||||
|
}
|
||||||
|
callbacks.forEach { (success, error) ->
|
||||||
|
failure?.let(error) ?: success()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
operationScope.cancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startStream(url: String) {
|
fun startStream(url: String) {
|
||||||
|
check(!_isDisposing && !_isDisposed) { "Live stream has been disposed" }
|
||||||
runBlocking {
|
runBlocking {
|
||||||
streamer.connect(url)
|
streamer.connect(url)
|
||||||
try {
|
try {
|
||||||
@@ -204,29 +261,31 @@ class FlutterLiveStreamView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopStream() {
|
fun stopStream(onSuccess: () -> Unit, onError: (Exception) -> Unit) {
|
||||||
if (!_isStreaming && !streamer.isConnected) {
|
runBackgroundOperation(onSuccess, onError) {
|
||||||
return
|
stopStreamInternal()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun stopStreamInternal() {
|
||||||
|
if (!_isStreaming && !streamer.isConnected) return
|
||||||
val isConnected = streamer.isConnected
|
val isConnected = streamer.isConnected
|
||||||
|
var failure: Exception? = null
|
||||||
try {
|
try {
|
||||||
runBlocking {
|
|
||||||
streamer.stopStream()
|
streamer.stopStream()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
failure = e
|
||||||
|
}
|
||||||
|
try {
|
||||||
streamer.disconnect()
|
streamer.disconnect()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
failure = failure ?: e
|
||||||
|
}
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
onDisconnected()
|
onDisconnected()
|
||||||
}
|
}
|
||||||
_isStreaming = false
|
_isStreaming = false
|
||||||
}
|
failure?.let { throw it }
|
||||||
} catch (e: Exception) {
|
|
||||||
android.util.Log.w("ApiVideoLiveStream", "stopStream failed", e)
|
|
||||||
_isStreaming = false
|
|
||||||
try {
|
|
||||||
streamer.disconnect()
|
|
||||||
} catch (_: Exception) {
|
|
||||||
// ignore secondary disconnect failures
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startPreview(onSuccess: () -> Unit, onError: (Exception) -> Unit) {
|
fun startPreview(onSuccess: () -> Unit, onError: (Exception) -> Unit) {
|
||||||
@@ -262,11 +321,40 @@ class FlutterLiveStreamView(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopPreview() {
|
fun stopPreview(onSuccess: () -> Unit, onError: (Exception) -> Unit) {
|
||||||
|
runBackgroundOperation(onSuccess, onError) {
|
||||||
|
stopPreviewInternal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopPreviewInternal() {
|
||||||
|
if (!_isPreviewing) return
|
||||||
streamer.stopPreview()
|
streamer.stopPreview()
|
||||||
_isPreviewing = false
|
_isPreviewing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun runBackgroundOperation(
|
||||||
|
onSuccess: () -> Unit,
|
||||||
|
onError: (Exception) -> Unit,
|
||||||
|
operation: suspend () -> Unit,
|
||||||
|
) {
|
||||||
|
if (_isDisposing || _isDisposed) {
|
||||||
|
mainHandler.post(onSuccess)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
operationScope.launch {
|
||||||
|
val failure = try {
|
||||||
|
operationMutex.withLock { operation() }
|
||||||
|
null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main.immediate) {
|
||||||
|
failure?.let(onError) ?: onSuccess()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getSurface(resolution: Size): Surface {
|
private fun getSurface(resolution: Size): Surface {
|
||||||
val surfaceTexture = flutterTexture.surfaceTexture().apply {
|
val surfaceTexture = flutterTexture.surfaceTexture().apply {
|
||||||
setDefaultBufferSize(
|
setDefaultBufferSize(
|
||||||
|
|||||||
+70
-17
@@ -50,28 +50,45 @@ class MethodCallHandlerImpl(
|
|||||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"create" -> {
|
"create" -> {
|
||||||
try {
|
val previousView = flutterView
|
||||||
flutterView?.dispose()
|
if (previousView == null) {
|
||||||
flutterView = FlutterLiveStreamView(
|
createFlutterView(result)
|
||||||
context,
|
} else {
|
||||||
textureRegistry,
|
previousView.dispose(
|
||||||
permissionsManager,
|
onSuccess = {
|
||||||
{ sendConnected() },
|
if (flutterView === previousView) {
|
||||||
{ sendDisconnected() },
|
flutterView = null
|
||||||
{ sendConnectionFailed(it) },
|
}
|
||||||
{ sendError(it) },
|
createFlutterView(result)
|
||||||
{ sendVideoSizeChanged(it) }
|
},
|
||||||
|
onError = {
|
||||||
|
result.error("failed_to_replace_live_stream", it.message, null)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
result.success(mapOf("textureId" to flutterView!!.textureId))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
result.error("failed_to_create_live_stream", e.message, null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"dispose" -> {
|
"dispose" -> {
|
||||||
flutterView?.dispose()
|
val view = flutterView
|
||||||
|
if (view == null) {
|
||||||
|
result.success(null)
|
||||||
|
} else {
|
||||||
|
view.dispose(
|
||||||
|
onSuccess = {
|
||||||
|
if (flutterView === view) {
|
||||||
flutterView = null
|
flutterView = null
|
||||||
}
|
}
|
||||||
|
result.success(null)
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
if (flutterView === view) {
|
||||||
|
flutterView = null
|
||||||
|
}
|
||||||
|
result.error("failed_to_dispose_live_stream", it.message, null)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
"setVideoConfig" -> {
|
"setVideoConfig" -> {
|
||||||
try {
|
try {
|
||||||
@@ -128,8 +145,17 @@ class MethodCallHandlerImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
"stopPreview" -> {
|
"stopPreview" -> {
|
||||||
flutterView?.stopPreview()
|
val view = flutterView
|
||||||
|
if (view == null) {
|
||||||
result.success(null)
|
result.success(null)
|
||||||
|
} else {
|
||||||
|
view.stopPreview(
|
||||||
|
onSuccess = { result.success(null) },
|
||||||
|
onError = {
|
||||||
|
result.error("failed_to_stop_preview", it.message, null)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"startStreaming" -> {
|
"startStreaming" -> {
|
||||||
@@ -163,8 +189,17 @@ class MethodCallHandlerImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
"stopStreaming" -> {
|
"stopStreaming" -> {
|
||||||
flutterView?.stopStream()
|
val view = flutterView
|
||||||
|
if (view == null) {
|
||||||
result.success(null)
|
result.success(null)
|
||||||
|
} else {
|
||||||
|
view.stopStream(
|
||||||
|
onSuccess = { result.success(null) },
|
||||||
|
onError = {
|
||||||
|
result.error("failed_to_stop_stream", it.message, null)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"getIsStreaming" -> result.success(mapOf("isStreaming" to flutterView!!.isStreaming))
|
"getIsStreaming" -> result.success(mapOf("isStreaming" to flutterView!!.isStreaming))
|
||||||
@@ -275,6 +310,24 @@ class MethodCallHandlerImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createFlutterView(result: MethodChannel.Result) {
|
||||||
|
try {
|
||||||
|
flutterView = FlutterLiveStreamView(
|
||||||
|
context,
|
||||||
|
textureRegistry,
|
||||||
|
permissionsManager,
|
||||||
|
{ sendConnected() },
|
||||||
|
{ sendDisconnected() },
|
||||||
|
{ sendConnectionFailed(it) },
|
||||||
|
{ sendError(it) },
|
||||||
|
{ sendVideoSizeChanged(it) },
|
||||||
|
)
|
||||||
|
result.success(mapOf("textureId" to flutterView!!.textureId))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
result.error("failed_to_create_live_stream", e.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun sendConnected() {
|
private fun sendConnected() {
|
||||||
sendEvent("connected")
|
sendEvent("connected")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ class _ApiVideoCameraPreviewState extends State<ApiVideoCameraPreview> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
widget.controller.stopPreview();
|
|
||||||
widget.controller.removeWidgetListener(_widgetListener);
|
widget.controller.removeWidgetListener(_widgetListener);
|
||||||
widget.controller.removeEventsListener(_eventsListener);
|
widget.controller.removeEventsListener(_eventsListener);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ class ApiVideoLiveStreamController {
|
|||||||
int get textureId => _textureId;
|
int get textureId => _textureId;
|
||||||
|
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
|
Future<void>? _initializeFuture;
|
||||||
|
Future<void>? _disposeFuture;
|
||||||
|
|
||||||
/// Gets the current state of the video player.
|
/// Gets the current state of the video player.
|
||||||
bool get isInitialized => _isInitialized;
|
bool get isInitialized => _isInitialized;
|
||||||
@@ -68,7 +70,14 @@ class ApiVideoLiveStreamController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new live stream instance with initial audio and video configurations.
|
/// Creates a new live stream instance with initial audio and video configurations.
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() {
|
||||||
|
if (_disposeFuture != null) {
|
||||||
|
throw StateError('Cannot initialize a disposed controller');
|
||||||
|
}
|
||||||
|
return _initializeFuture ??= _initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initialize() async {
|
||||||
_textureId = await _platform.initialize() ?? kUninitializedTextureId;
|
_textureId = await _platform.initialize() ?? kUninitializedTextureId;
|
||||||
|
|
||||||
_eventSubscription = _platform
|
_eventSubscription = _platform
|
||||||
@@ -91,12 +100,23 @@ class ApiVideoLiveStreamController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Disposes the live stream instance.
|
/// Disposes the live stream instance.
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() {
|
||||||
|
return _disposeFuture ??= _dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _dispose() async {
|
||||||
|
try {
|
||||||
|
await _initializeFuture;
|
||||||
|
} catch (_) {
|
||||||
|
// A partially initialized native view still needs to be disposed.
|
||||||
|
}
|
||||||
await _eventSubscription?.cancel();
|
await _eventSubscription?.cancel();
|
||||||
|
_eventSubscription = null;
|
||||||
_eventsListeners.clear();
|
_eventsListeners.clear();
|
||||||
_widgetListeners.clear();
|
_widgetListeners.clear();
|
||||||
await _platform.dispose();
|
await _platform.dispose();
|
||||||
return;
|
_isInitialized = false;
|
||||||
|
_textureId = kUninitializedTextureId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets new video parameters.
|
/// Sets new video parameters.
|
||||||
|
|||||||
Reference in New Issue
Block a user