添加 network_info_plus 依赖以支持局域网 IP 获取;在事件信息和录制页面中增强局域网 IP 查询功能,更新相关模型以包含 scheduleId 字段;重构录制页面以改善用户体验,修复推流控制器释放逻辑。

This commit is contained in:
2026-07-17 17:31:04 +08:00
parent a4333d3bd1
commit 25a07cbd3b
16 changed files with 273 additions and 51 deletions
+6
View File
@@ -1,9 +1,12 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.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/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/scan_qrcode/pages/page_scan_qrcode.dart';
import 'package:recording_tool/shared/widgets/widgets.dart';
@@ -25,6 +28,9 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
_controller?.text = '555';
WidgetsBinding.instance.addPostFrameCallback((_) async {
// 静默探测 NAS,不阻塞登录 / 自动跳转
unawaited(UtilSearchNasIp.discover());
final token = AppStorage.getString(StorageKeys.authToken);
if (token?.isNotEmpty ?? false) {
AppNavigator.push(const ScanQrCodePage());
@@ -16,14 +16,21 @@ class StreamKeyReq {
required this.eventId,
required this.itemId,
required this.userId,
required this.scheduleId,
});
final String eventId;
final String itemId;
final String userId;
final String scheduleId;
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 playerPhone;
String get scheduleTime =>
_formatScheduleTime(matchStartTime, matchEndTime);
String get scheduleTime => _formatScheduleTime(matchStartTime, matchEndTime);
String? get statusLabel => completed ? '已完成' : null;
+27 -8
View File
@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.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/state/state_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';
class EventInfoPage extends ConsumerStatefulWidget {
@@ -24,16 +26,33 @@ class _EventInfoPageState extends ConsumerState<EventInfoPage> {
Future<void> onItemTap(EventRegistrationItem item) async {
// 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;
// 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(
WebviewPage(
url: 'https://drone.apptest.sportsx.cc/#/pages/h5/score-entry',
eventRegistrationItem: item,
playerId: widget.playerId,
RecordingPage(
recordingContext: RecordingContext(
eventTitle: item.eventName,
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_riverpod/flutter_riverpod.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/server/server_events.dart';
import 'package:recording_tool/features/events/state/state_event_info.dart';
@@ -20,10 +20,6 @@ class EventInfoViewModel extends StateNotifier<EventInfoState> {
status: '',
);
static const _fallbackEventId = '';
static const _fallbackItemId = '';
static const _fallbackStreamUserId = '';
final Ref _ref;
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);
final req = StreamKeyReq(
eventId: item.eventId.isNotEmpty ? item.eventId : _fallbackEventId,
itemId: item.itemId.isNotEmpty ? item.itemId : _fallbackItemId,
userId: item.userId.isNotEmpty ? item.userId : _fallbackStreamUserId,
eventId: item.eventId,
itemId: item.itemId,
userId: item.userId,
scheduleId: item.scheduleId,
);
try {
final response = await _ref
.read(eventsServerProvider)
.fetchStreamKey(req);
debugPrint('推流 Key 接口响应: $response');
debugPrint('推流 Key 接口响应: ${response.rawData['streamKey']}');
state = state.copyWith(isRequestingStreamKey: false);
} on ApiException catch (error) {
debugPrint('推流 Key 接口请求失败: ${error.message}');
state = state.copyWith(isRequestingStreamKey: false);
AppToast.show(error.message);
return response.rawData['streamKey'];
// return response['streamKey'];
} catch (error) {
debugPrint('推流 Key 接口请求失败: $error');
state = state.copyWith(isRequestingStreamKey: false);
AppToast.show('推流 Key 获取失败');
AppToast.show('获取选手信息失败');
return null;
}
}
}
+30 -14
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
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_saved_dialog.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/shared/widgets/widgets.dart';
@@ -57,7 +57,7 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
_streamController = ApiVideoLiveStreamController(
initialAudioConfig: AudioConfig(bitrate: 128000),
initialVideoConfig: VideoConfig.withDefaultBitrate(
resolution: Resolution.RESOLUTION_1080,
resolution: Resolution.RESOLUTION_720,
fps: 30,
),
onConnectionSuccess: () => {debugPrint('推流成功')},
@@ -314,7 +314,7 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
/// 返回扫码页,准备新一轮录制。
void _recordNewRound() {
AppNavigator.pushAndRemoveUntil(const ScanQrCodePage());
AppNavigator.pop(context: context);
}
/// 推流结束后按需弹出完成对话框
@@ -337,38 +337,53 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
);
}
/// 退出沉浸式并释放录制会话
/// 退出沉浸式并释放录制会话
/// 先恢复系统栏,再错开一帧释放编码器,减轻返回动画卡顿。
Future<void> _exitRecordingMode() async {
if (!_immersiveApplied) return;
await ref.read(recordingViewModelProvider.notifier).teardown();
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);
_immersiveApplied = false;
}
@override
/// 页面销毁时恢复系统 UI
/// 页面销毁时兜底恢复系统 UI 并释放推流控制器
void dispose() {
if (_immersiveApplied) {
_immersiveApplied = false;
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values,
);
RecordingPlatform.setImmersiveMode(enabled: false);
unawaited(RecordingPlatform.setImmersiveMode(enabled: false));
}
_disposeStreamController();
unawaited(_disposeStreamController());
super.dispose();
}
/// 只 dispose 一次;原生 dispose 内部已 stopStream,避免 stop+dispose 双次停流。
Future<void> _disposeStreamController() async {
if (_controllerDisposed) return;
_controllerDisposed = true;
await _streamController.stop();
await _streamController.dispose();
try {
await _streamController.dispose();
} catch (error, stackTrace) {
debugPrint('释放推流控制器失败: $error\n$stackTrace');
}
}
@override
@@ -427,9 +442,10 @@ class _RecordingPopScope extends ConsumerWidget {
return PopScope(
canPop: !isRecording,
onPopInvokedWithResult: (didPop, result) async {
onPopInvokedWithResult: (didPop, result) {
if (didPop) {
await onExitRecordingMode();
// 不 await,避免把 MediaCodec 释放堵在 Pop 回调上。
unawaited(onExitRecordingMode());
return;
}
if (isRecording) {
@@ -280,9 +280,8 @@ class RecordingViewModel extends Notifier<RecordingModel> {
_updateSession((s) => s.copyWith(isBatteryOptimizedIgnored: ignored));
}
/// 退出录制页时释放相机、勿扰和状态订阅
/// 退出录制页时释放勿扰和会话状态(沉浸式由页面统一恢复)
Future<void> teardown() async {
await RecordingPlatform.setImmersiveMode(enabled: false);
await RecordingPlatform.disableDoNotDisturb();
_recordingStartedAt = null;
_elapsedTimer?.cancel();
@@ -1,5 +1,6 @@
import 'package:flutter/material.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/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),
),
],
),
),