添加 network_info_plus 依赖以支持局域网 IP 获取;在事件信息和录制页面中增强局域网 IP 查询功能,更新相关模型以包含 scheduleId 字段;重构录制页面以改善用户体验,修复推流控制器释放逻辑。
This commit is contained in:
@@ -10,6 +10,9 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
|
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="28" />
|
android:maxSdkVersion="28" />
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:network_info_plus/network_info_plus.dart';
|
||||||
|
|
||||||
|
/// 局域网 NAS IP 探测(结果仅存内存静态字段)。
|
||||||
|
class UtilSearchNasIp {
|
||||||
|
UtilSearchNasIp._();
|
||||||
|
|
||||||
|
static const String _defaultPreUrl = 'http://192.168.1.';
|
||||||
|
static const String _probePath =
|
||||||
|
':5666/sac/rpcproxy/v1/new-user-guide/status';
|
||||||
|
static const Duration _timeout = Duration(seconds: 1);
|
||||||
|
|
||||||
|
/// 命中的 NAS IPv4(仅内存,进程内有效)。
|
||||||
|
static String? nasIp;
|
||||||
|
|
||||||
|
/// 并发扫同网段 1~255,超时 1s;首个有 HTTP 响应者写入 [nasIp] 并返回。
|
||||||
|
static Future<String?> discover() async {
|
||||||
|
if (nasIp != null && nasIp!.isNotEmpty) {
|
||||||
|
return nasIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
final preUrl = await _resolvePreUrl();
|
||||||
|
final dio = Dio(
|
||||||
|
BaseOptions(
|
||||||
|
connectTimeout: _timeout,
|
||||||
|
receiveTimeout: _timeout,
|
||||||
|
sendTimeout: _timeout,
|
||||||
|
validateStatus: (_) => true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final cancelToken = CancelToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Future.wait([
|
||||||
|
for (var host = 1; host <= 255; host++)
|
||||||
|
_probeHost(
|
||||||
|
dio: dio,
|
||||||
|
cancelToken: cancelToken,
|
||||||
|
preUrl: preUrl,
|
||||||
|
host: host,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
return nasIp;
|
||||||
|
} finally {
|
||||||
|
dio.close(force: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 由本机 Wi‑Fi IPv4 推导 `http://a.b.c.`,失败则用默认网段。
|
||||||
|
static Future<String> _resolvePreUrl() async {
|
||||||
|
try {
|
||||||
|
final wifiIp = await NetworkInfo().getWifiIP();
|
||||||
|
if (wifiIp == null || wifiIp.isEmpty) return _defaultPreUrl;
|
||||||
|
final parts = wifiIp.split('.');
|
||||||
|
if (parts.length != 4) return _defaultPreUrl;
|
||||||
|
return 'http://${parts[0]}.${parts[1]}.${parts[2]}.';
|
||||||
|
} catch (_) {
|
||||||
|
return _defaultPreUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 探测单个主机;任意 HTTP 响应视为命中。
|
||||||
|
static Future<void> _probeHost({
|
||||||
|
required Dio dio,
|
||||||
|
required CancelToken cancelToken,
|
||||||
|
required String preUrl,
|
||||||
|
required int host,
|
||||||
|
}) async {
|
||||||
|
if (cancelToken.isCancelled || nasIp != null) return;
|
||||||
|
|
||||||
|
final target = '$preUrl$host$_probePath';
|
||||||
|
try {
|
||||||
|
await dio.get<dynamic>(target, cancelToken: cancelToken);
|
||||||
|
_onHit(preUrl, host, cancelToken);
|
||||||
|
} on DioException catch (error) {
|
||||||
|
if (error.type == DioExceptionType.cancel) return;
|
||||||
|
// 带 response 的 DioException 也表示已连通
|
||||||
|
if (error.response != null) {
|
||||||
|
_onHit(preUrl, host, cancelToken);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// 超时 / 连接失败:未命中
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录命中:写 [nasIp]、打印并取消其余请求。
|
||||||
|
static void _onHit(String preUrl, int host, CancelToken cancelToken) {
|
||||||
|
if (nasIp != null) return;
|
||||||
|
final ip = '${_hostPrefix(preUrl)}$host';
|
||||||
|
nasIp = ip;
|
||||||
|
debugPrint('NAS IP: $ip');
|
||||||
|
if (!cancelToken.isCancelled) {
|
||||||
|
cancelToken.cancel('nas-found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `http://192.168.1.` → `192.168.1.`
|
||||||
|
static String _hostPrefix(String preUrl) {
|
||||||
|
return preUrl.replaceFirst(RegExp(r'^https?://'), '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 调用案例
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// 1) Auth 页静默探测(不阻塞登录):
|
||||||
|
//
|
||||||
|
// import 'dart:async';
|
||||||
|
// import 'package:recording_tool/core/utils/util_getNasIp.dart';
|
||||||
|
//
|
||||||
|
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
// unawaited(UtilGetNasIp.discover());
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// 2) 业务侧读取已命中 IP:
|
||||||
|
//
|
||||||
|
// final ip = UtilGetNasIp.nasIp;
|
||||||
|
// if (ip != null) {
|
||||||
|
// final base = 'http://$ip:5666';
|
||||||
|
// // 使用 base 访问 NAS...
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 3) 主动等待探测结果(少用,会阻塞当前异步流程):
|
||||||
|
//
|
||||||
|
// final ip = await UtilGetNasIp.discover();
|
||||||
|
// debugPrint('result: $ip');
|
||||||
|
//
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:recording_tool/app/router/app_navigator.dart';
|
import 'package:recording_tool/app/router/app_navigator.dart';
|
||||||
import 'package:recording_tool/core/cache/app_storage.dart';
|
import 'package:recording_tool/core/cache/app_storage.dart';
|
||||||
import 'package:recording_tool/core/cache/storage_keys.dart';
|
import 'package:recording_tool/core/cache/storage_keys.dart';
|
||||||
|
import 'package:recording_tool/core/utils/util_search_nasIp.dart';
|
||||||
import 'package:recording_tool/features/auth/view_model_auth/view_model_auth.dart';
|
import 'package:recording_tool/features/auth/view_model_auth/view_model_auth.dart';
|
||||||
import 'package:recording_tool/features/scan_qrcode/pages/page_scan_qrcode.dart';
|
import 'package:recording_tool/features/scan_qrcode/pages/page_scan_qrcode.dart';
|
||||||
import 'package:recording_tool/shared/widgets/widgets.dart';
|
import 'package:recording_tool/shared/widgets/widgets.dart';
|
||||||
@@ -25,6 +28,9 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
|
|||||||
_controller?.text = '555';
|
_controller?.text = '555';
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
// 静默探测 NAS,不阻塞登录 / 自动跳转
|
||||||
|
unawaited(UtilSearchNasIp.discover());
|
||||||
|
|
||||||
final token = AppStorage.getString(StorageKeys.authToken);
|
final token = AppStorage.getString(StorageKeys.authToken);
|
||||||
if (token?.isNotEmpty ?? false) {
|
if (token?.isNotEmpty ?? false) {
|
||||||
AppNavigator.push(const ScanQrCodePage());
|
AppNavigator.push(const ScanQrCodePage());
|
||||||
|
|||||||
@@ -16,14 +16,21 @@ class StreamKeyReq {
|
|||||||
required this.eventId,
|
required this.eventId,
|
||||||
required this.itemId,
|
required this.itemId,
|
||||||
required this.userId,
|
required this.userId,
|
||||||
|
required this.scheduleId,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String eventId;
|
final String eventId;
|
||||||
final String itemId;
|
final String itemId;
|
||||||
final String userId;
|
final String userId;
|
||||||
|
final String scheduleId;
|
||||||
|
|
||||||
Map<String, dynamic> toFormDataMap() {
|
Map<String, dynamic> toFormDataMap() {
|
||||||
return {'eventId': eventId, 'itemId': itemId, 'userId': userId};
|
return {
|
||||||
|
'eventId': eventId,
|
||||||
|
'itemId': itemId,
|
||||||
|
'userId': userId,
|
||||||
|
'scheduleId': scheduleId,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,8 +143,7 @@ class EventRegistrationItem {
|
|||||||
final String playerName;
|
final String playerName;
|
||||||
final String playerPhone;
|
final String playerPhone;
|
||||||
|
|
||||||
String get scheduleTime =>
|
String get scheduleTime => _formatScheduleTime(matchStartTime, matchEndTime);
|
||||||
_formatScheduleTime(matchStartTime, matchEndTime);
|
|
||||||
|
|
||||||
String? get statusLabel => completed ? '已完成' : null;
|
String? get statusLabel => completed ? '已完成' : null;
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:recording_tool/app/router/app_navigator.dart';
|
import 'package:recording_tool/app/router/app_navigator.dart';
|
||||||
|
import 'package:recording_tool/core/utils/util_search_nasIp.dart';
|
||||||
import 'package:recording_tool/features/events/model/model_event_info.dart';
|
import 'package:recording_tool/features/events/model/model_event_info.dart';
|
||||||
import 'package:recording_tool/features/events/state/state_event_info.dart';
|
import 'package:recording_tool/features/events/state/state_event_info.dart';
|
||||||
import 'package:recording_tool/features/events/view_model/view_model_event_info.dart';
|
import 'package:recording_tool/features/events/view_model/view_model_event_info.dart';
|
||||||
import 'package:recording_tool/shared/widgets/app_webview.dart';
|
import 'package:recording_tool/features/recording/model/model_recording_context.dart';
|
||||||
|
import 'package:recording_tool/features/recording/pages/page_record.dart';
|
||||||
import 'package:recording_tool/shared/widgets/widgets.dart';
|
import 'package:recording_tool/shared/widgets/widgets.dart';
|
||||||
|
|
||||||
class EventInfoPage extends ConsumerStatefulWidget {
|
class EventInfoPage extends ConsumerStatefulWidget {
|
||||||
@@ -24,16 +26,33 @@ class _EventInfoPageState extends ConsumerState<EventInfoPage> {
|
|||||||
|
|
||||||
Future<void> onItemTap(EventRegistrationItem item) async {
|
Future<void> onItemTap(EventRegistrationItem item) async {
|
||||||
// debugPrint('item tapped: ${item.itemName}');
|
// debugPrint('item tapped: ${item.itemName}');
|
||||||
// await ref.read(eventInfoProvider.notifier).requestStreamKey(item);
|
final streamKey = await ref
|
||||||
|
.read(eventInfoProvider.notifier)
|
||||||
|
.requestStreamKey(item);
|
||||||
|
if (streamKey == null || streamKey.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
// final profile = ref.read(eventInfoProvider).profile;
|
|
||||||
|
|
||||||
/// 个人赛
|
/// 获取局域网 IP
|
||||||
|
// final ip =
|
||||||
|
// await ref.read(eventInfoProvider.notifier).getLocalIP() ?? 'sheling';
|
||||||
|
final ip = UtilSearchNasIp.nasIp ?? 'sheling';
|
||||||
|
// final ip = '192.168.1.245';
|
||||||
|
// final streamUrl = 'rtmp://$ip:19090/$streamKey';
|
||||||
|
final streamUrl = 'rtmp://$ip:19090/测试团队赛录分缓存/空中足球赛/初中组/林培伦vs周白芷';
|
||||||
AppNavigator.push(
|
AppNavigator.push(
|
||||||
WebviewPage(
|
RecordingPage(
|
||||||
url: 'https://drone.apptest.sportsx.cc/#/pages/h5/score-entry',
|
recordingContext: RecordingContext(
|
||||||
eventRegistrationItem: item,
|
eventTitle: item.eventName,
|
||||||
playerId: widget.playerId,
|
matchName: item.itemName,
|
||||||
|
group: item.groupName,
|
||||||
|
venue: item.matchPlace,
|
||||||
|
time: item.scheduleTime,
|
||||||
|
playerName: item.playerName,
|
||||||
|
playerPhone: item.playerPhone,
|
||||||
|
),
|
||||||
|
streamUrl: streamUrl,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_riverpod/legacy.dart';
|
import 'package:flutter_riverpod/legacy.dart';
|
||||||
import 'package:recording_tool/core/network/api_exception.dart';
|
import 'package:network_info_plus/network_info_plus.dart';
|
||||||
import 'package:recording_tool/features/events/model/model_event_info.dart';
|
import 'package:recording_tool/features/events/model/model_event_info.dart';
|
||||||
import 'package:recording_tool/features/events/server/server_events.dart';
|
import 'package:recording_tool/features/events/server/server_events.dart';
|
||||||
import 'package:recording_tool/features/events/state/state_event_info.dart';
|
import 'package:recording_tool/features/events/state/state_event_info.dart';
|
||||||
@@ -20,10 +20,6 @@ class EventInfoViewModel extends StateNotifier<EventInfoState> {
|
|||||||
status: '',
|
status: '',
|
||||||
);
|
);
|
||||||
|
|
||||||
static const _fallbackEventId = '';
|
|
||||||
static const _fallbackItemId = '';
|
|
||||||
static const _fallbackStreamUserId = '';
|
|
||||||
|
|
||||||
final Ref _ref;
|
final Ref _ref;
|
||||||
|
|
||||||
Future<bool> loadRegistrationList({
|
Future<bool> loadRegistrationList({
|
||||||
@@ -50,28 +46,43 @@ class EventInfoViewModel extends StateNotifier<EventInfoState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> requestStreamKey(EventRegistrationItem item) async {
|
/// 获取局域网 IP
|
||||||
|
|
||||||
|
Future<String?> getLocalIP() async {
|
||||||
|
final info = NetworkInfo();
|
||||||
|
try {
|
||||||
|
// 获取当前连接 Wi-Fi 分配的局域网 IPv4 地址(例如 192.168.1.105)
|
||||||
|
String? wifiIP = await info.getWifiIP();
|
||||||
|
return wifiIP;
|
||||||
|
} catch (e) {
|
||||||
|
print("获取局域网 IP 失败: $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> requestStreamKey(EventRegistrationItem item) async {
|
||||||
state = state.copyWith(isRequestingStreamKey: true);
|
state = state.copyWith(isRequestingStreamKey: true);
|
||||||
final req = StreamKeyReq(
|
final req = StreamKeyReq(
|
||||||
eventId: item.eventId.isNotEmpty ? item.eventId : _fallbackEventId,
|
eventId: item.eventId,
|
||||||
itemId: item.itemId.isNotEmpty ? item.itemId : _fallbackItemId,
|
itemId: item.itemId,
|
||||||
userId: item.userId.isNotEmpty ? item.userId : _fallbackStreamUserId,
|
userId: item.userId,
|
||||||
|
scheduleId: item.scheduleId,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await _ref
|
final response = await _ref
|
||||||
.read(eventsServerProvider)
|
.read(eventsServerProvider)
|
||||||
.fetchStreamKey(req);
|
.fetchStreamKey(req);
|
||||||
debugPrint('推流 Key 接口响应: $response');
|
debugPrint('推流 Key 接口响应: ${response.rawData['streamKey']}');
|
||||||
state = state.copyWith(isRequestingStreamKey: false);
|
state = state.copyWith(isRequestingStreamKey: false);
|
||||||
} on ApiException catch (error) {
|
|
||||||
debugPrint('推流 Key 接口请求失败: ${error.message}');
|
return response.rawData['streamKey'];
|
||||||
state = state.copyWith(isRequestingStreamKey: false);
|
// return response['streamKey'];
|
||||||
AppToast.show(error.message);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugPrint('推流 Key 接口请求失败: $error');
|
debugPrint('推流 Key 接口请求失败: $error');
|
||||||
state = state.copyWith(isRequestingStreamKey: false);
|
state = state.copyWith(isRequestingStreamKey: false);
|
||||||
AppToast.show('推流 Key 获取失败');
|
AppToast.show('获取选手信息失败');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:apivideo_live_stream/apivideo_live_stream.dart';
|
import 'package:apivideo_live_stream/apivideo_live_stream.dart';
|
||||||
@@ -20,7 +21,6 @@ import 'package:recording_tool/features/recording/widgets/widget_recording_hud.d
|
|||||||
import 'package:recording_tool/features/recording/widgets/widget_recording_loading_overlay.dart';
|
import 'package:recording_tool/features/recording/widgets/widget_recording_loading_overlay.dart';
|
||||||
import 'package:recording_tool/features/recording/widgets/widget_recording_saved_dialog.dart';
|
import 'package:recording_tool/features/recording/widgets/widget_recording_saved_dialog.dart';
|
||||||
import 'package:recording_tool/features/recording/widgets/widget_recording_touch_lock_overlay.dart';
|
import 'package:recording_tool/features/recording/widgets/widget_recording_touch_lock_overlay.dart';
|
||||||
import 'package:recording_tool/features/scan_qrcode/pages/page_scan_qrcode.dart';
|
|
||||||
import 'package:recording_tool/features/scan_qrcode/utils/rtmp_stream_target.dart';
|
import 'package:recording_tool/features/scan_qrcode/utils/rtmp_stream_target.dart';
|
||||||
import 'package:recording_tool/shared/widgets/widgets.dart';
|
import 'package:recording_tool/shared/widgets/widgets.dart';
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
|||||||
_streamController = ApiVideoLiveStreamController(
|
_streamController = ApiVideoLiveStreamController(
|
||||||
initialAudioConfig: AudioConfig(bitrate: 128000),
|
initialAudioConfig: AudioConfig(bitrate: 128000),
|
||||||
initialVideoConfig: VideoConfig.withDefaultBitrate(
|
initialVideoConfig: VideoConfig.withDefaultBitrate(
|
||||||
resolution: Resolution.RESOLUTION_1080,
|
resolution: Resolution.RESOLUTION_720,
|
||||||
fps: 30,
|
fps: 30,
|
||||||
),
|
),
|
||||||
onConnectionSuccess: () => {debugPrint('推流成功')},
|
onConnectionSuccess: () => {debugPrint('推流成功')},
|
||||||
@@ -314,7 +314,7 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
|||||||
|
|
||||||
/// 返回扫码页,准备新一轮录制。
|
/// 返回扫码页,准备新一轮录制。
|
||||||
void _recordNewRound() {
|
void _recordNewRound() {
|
||||||
AppNavigator.pushAndRemoveUntil(const ScanQrCodePage());
|
AppNavigator.pop(context: context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 推流结束后按需弹出完成对话框
|
/// 推流结束后按需弹出完成对话框
|
||||||
@@ -337,38 +337,53 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 退出沉浸式并释放录制会话
|
/// 退出沉浸式并释放录制会话。
|
||||||
|
/// 先恢复系统栏,再错开一帧释放编码器,减轻返回动画卡顿。
|
||||||
Future<void> _exitRecordingMode() async {
|
Future<void> _exitRecordingMode() async {
|
||||||
if (!_immersiveApplied) return;
|
await _restoreSystemUiIfNeeded();
|
||||||
|
if (mounted) {
|
||||||
await ref.read(recordingViewModelProvider.notifier).teardown();
|
await ref.read(recordingViewModelProvider.notifier).teardown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 让 pop 动画先跑起来,再做 MediaCodec 释放。
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
await _disposeStreamController();
|
await _disposeStreamController();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _restoreSystemUiIfNeeded() async {
|
||||||
|
if (!_immersiveApplied) return;
|
||||||
|
_immersiveApplied = false;
|
||||||
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);
|
||||||
_immersiveApplied = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
/// 页面销毁时恢复系统 UI
|
/// 页面销毁时兜底恢复系统 UI 并释放推流控制器
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (_immersiveApplied) {
|
if (_immersiveApplied) {
|
||||||
|
_immersiveApplied = false;
|
||||||
SystemChrome.setEnabledSystemUIMode(
|
SystemChrome.setEnabledSystemUIMode(
|
||||||
SystemUiMode.manual,
|
SystemUiMode.manual,
|
||||||
overlays: SystemUiOverlay.values,
|
overlays: SystemUiOverlay.values,
|
||||||
);
|
);
|
||||||
RecordingPlatform.setImmersiveMode(enabled: false);
|
unawaited(RecordingPlatform.setImmersiveMode(enabled: false));
|
||||||
}
|
}
|
||||||
_disposeStreamController();
|
unawaited(_disposeStreamController());
|
||||||
super.dispose();
|
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;
|
||||||
await _streamController.stop();
|
try {
|
||||||
await _streamController.dispose();
|
await _streamController.dispose();
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
debugPrint('释放推流控制器失败: $error\n$stackTrace');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -427,9 +442,10 @@ class _RecordingPopScope extends ConsumerWidget {
|
|||||||
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: !isRecording,
|
canPop: !isRecording,
|
||||||
onPopInvokedWithResult: (didPop, result) async {
|
onPopInvokedWithResult: (didPop, result) {
|
||||||
if (didPop) {
|
if (didPop) {
|
||||||
await onExitRecordingMode();
|
// 不 await,避免把 MediaCodec 释放堵在 Pop 回调上。
|
||||||
|
unawaited(onExitRecordingMode());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isRecording) {
|
if (isRecording) {
|
||||||
|
|||||||
@@ -280,9 +280,8 @@ class RecordingViewModel extends Notifier<RecordingModel> {
|
|||||||
_updateSession((s) => s.copyWith(isBatteryOptimizedIgnored: ignored));
|
_updateSession((s) => s.copyWith(isBatteryOptimizedIgnored: ignored));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 退出录制页时释放相机、勿扰和状态订阅。
|
/// 退出录制页时释放勿扰和会话状态(沉浸式由页面统一恢复)。
|
||||||
Future<void> teardown() async {
|
Future<void> teardown() async {
|
||||||
await RecordingPlatform.setImmersiveMode(enabled: false);
|
|
||||||
await RecordingPlatform.disableDoNotDisturb();
|
await RecordingPlatform.disableDoNotDisturb();
|
||||||
_recordingStartedAt = null;
|
_recordingStartedAt = null;
|
||||||
_elapsedTimer?.cancel();
|
_elapsedTimer?.cancel();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:recording_tool/app/router/app_navigator.dart';
|
||||||
import 'package:recording_tool/features/recording/widgets/record_content_transition.dart';
|
import 'package:recording_tool/features/recording/widgets/record_content_transition.dart';
|
||||||
import 'package:recording_tool/gen/assets.gen.dart';
|
import 'package:recording_tool/gen/assets.gen.dart';
|
||||||
|
|
||||||
@@ -59,6 +60,14 @@ class RecordHeaderWidget extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// 返回上一页
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => AppNavigator.pop(context: context),
|
||||||
|
icon: Icon(Icons.arrow_back, size: 32.r),
|
||||||
|
color: Colors.white,
|
||||||
|
padding: EdgeInsets.only(left: 16.w),
|
||||||
|
constraints: BoxConstraints(minWidth: 56.w, minHeight: 52.h),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -1,2 +1,2 @@
|
|||||||
#Mon Jul 13 17:53:35 CST 2026
|
#Fri Jul 17 16:25:39 CST 2026
|
||||||
gradle.version=8.2
|
gradle.version=8.2
|
||||||
|
|||||||
+23
@@ -175,8 +175,18 @@ class FlutterLiveStreamView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun dispose() {
|
fun dispose() {
|
||||||
|
try {
|
||||||
stopStream()
|
stopStream()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.w("ApiVideoLiveStream", "stopStream during dispose failed", e)
|
||||||
|
_isStreaming = false
|
||||||
|
}
|
||||||
|
try {
|
||||||
streamer.stopPreview()
|
streamer.stopPreview()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.w("ApiVideoLiveStream", "stopPreview during dispose failed", e)
|
||||||
|
}
|
||||||
|
_isPreviewing = false
|
||||||
flutterTexture.release()
|
flutterTexture.release()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +205,11 @@ class FlutterLiveStreamView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun stopStream() {
|
fun stopStream() {
|
||||||
|
if (!_isStreaming && !streamer.isConnected) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val isConnected = streamer.isConnected
|
val isConnected = streamer.isConnected
|
||||||
|
try {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
streamer.stopStream()
|
streamer.stopStream()
|
||||||
streamer.disconnect()
|
streamer.disconnect()
|
||||||
@@ -204,6 +218,15 @@ class FlutterLiveStreamView(
|
|||||||
}
|
}
|
||||||
_isStreaming = false
|
_isStreaming = false
|
||||||
}
|
}
|
||||||
|
} 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) {
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ dependencies:
|
|||||||
apivideo_live_stream:
|
apivideo_live_stream:
|
||||||
path: plugins/apivideo_live_stream
|
path: plugins/apivideo_live_stream
|
||||||
jwt_decoder: ^2.0.1
|
jwt_decoder: ^2.0.1
|
||||||
|
network_info_plus: ^8.2.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user