diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index efaaff5..2fc131e 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -10,6 +10,9 @@
+
+
+
@@ -57,4 +60,4 @@
-
+
\ No newline at end of file
diff --git a/lib/core/utils/util_search_nasIp.dart b/lib/core/utils/util_search_nasIp.dart
new file mode 100644
index 0000000..adf2e2b
--- /dev/null
+++ b/lib/core/utils/util_search_nasIp.dart
@@ -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 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 _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 _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(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');
+//
diff --git a/lib/features/auth/pages/page_auth.dart b/lib/features/auth/pages/page_auth.dart
index fd36026..4b967da 100644
--- a/lib/features/auth/pages/page_auth.dart
+++ b/lib/features/auth/pages/page_auth.dart
@@ -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 {
_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());
diff --git a/lib/features/events/model/model_event_info.dart b/lib/features/events/model/model_event_info.dart
index f5f59ce..73d8bc9 100644
--- a/lib/features/events/model/model_event_info.dart
+++ b/lib/features/events/model/model_event_info.dart
@@ -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 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;
diff --git a/lib/features/events/pages/page_event_info.dart b/lib/features/events/pages/page_event_info.dart
index 620b093..198a36c 100644
--- a/lib/features/events/pages/page_event_info.dart
+++ b/lib/features/events/pages/page_event_info.dart
@@ -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 {
Future 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,
),
);
}
diff --git a/lib/features/events/view_model/view_model_event_info.dart b/lib/features/events/view_model/view_model_event_info.dart
index 997a5cf..e40df22 100644
--- a/lib/features/events/view_model/view_model_event_info.dart
+++ b/lib/features/events/view_model/view_model_event_info.dart
@@ -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 {
status: '',
);
- static const _fallbackEventId = '';
- static const _fallbackItemId = '';
- static const _fallbackStreamUserId = '';
-
final Ref _ref;
Future loadRegistrationList({
@@ -50,28 +46,43 @@ class EventInfoViewModel extends StateNotifier {
}
}
- Future requestStreamKey(EventRegistrationItem item) async {
+ /// 获取局域网 IP
+
+ Future 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 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;
}
}
}
diff --git a/lib/features/recording/pages/page_record.dart b/lib/features/recording/pages/page_record.dart
index 8573553..1b7a772 100644
--- a/lib/features/recording/pages/page_record.dart
+++ b/lib/features/recording/pages/page_record.dart
@@ -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 {
_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 {
/// 返回扫码页,准备新一轮录制。
void _recordNewRound() {
- AppNavigator.pushAndRemoveUntil(const ScanQrCodePage());
+ AppNavigator.pop(context: context);
}
/// 推流结束后按需弹出完成对话框
@@ -337,38 +337,53 @@ class _RecordingPageState extends ConsumerState {
);
}
- /// 退出沉浸式并释放录制会话
+ /// 退出沉浸式并释放录制会话。
+ /// 先恢复系统栏,再错开一帧释放编码器,减轻返回动画卡顿。
Future _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.delayed(Duration.zero);
await _disposeStreamController();
+ }
+
+ Future _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 _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) {
diff --git a/lib/features/recording/view-model/view_model_recording.dart b/lib/features/recording/view-model/view_model_recording.dart
index 6ddfde5..cf687f3 100644
--- a/lib/features/recording/view-model/view_model_recording.dart
+++ b/lib/features/recording/view-model/view_model_recording.dart
@@ -280,9 +280,8 @@ class RecordingViewModel extends Notifier {
_updateSession((s) => s.copyWith(isBatteryOptimizedIgnored: ignored));
}
- /// 退出录制页时释放相机、勿扰和状态订阅。
+ /// 退出录制页时释放勿扰和会话状态(沉浸式由页面统一恢复)。
Future teardown() async {
- await RecordingPlatform.setImmersiveMode(enabled: false);
await RecordingPlatform.disableDoNotDisturb();
_recordingStartedAt = null;
_elapsedTimer?.cancel();
diff --git a/lib/features/recording/widgets/widget_record_header.dart b/lib/features/recording/widgets/widget_record_header.dart
index b39a4b0..bf9ba63 100644
--- a/lib/features/recording/widgets/widget_record_header.dart
+++ b/lib/features/recording/widgets/widget_record_header.dart
@@ -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),
+ ),
],
),
),
diff --git a/plugins/apivideo_live_stream/android/.gradle/8.13/fileChanges/last-build.bin b/plugins/apivideo_live_stream/android/.gradle/8.13/fileChanges/last-build.bin
new file mode 100644
index 0000000..f76dd23
Binary files /dev/null and b/plugins/apivideo_live_stream/android/.gradle/8.13/fileChanges/last-build.bin differ
diff --git a/plugins/apivideo_live_stream/android/.gradle/8.13/fileHashes/fileHashes.lock b/plugins/apivideo_live_stream/android/.gradle/8.13/fileHashes/fileHashes.lock
new file mode 100644
index 0000000..4c650b9
Binary files /dev/null and b/plugins/apivideo_live_stream/android/.gradle/8.13/fileHashes/fileHashes.lock differ
diff --git a/plugins/apivideo_live_stream/android/.gradle/8.13/gc.properties b/plugins/apivideo_live_stream/android/.gradle/8.13/gc.properties
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/apivideo_live_stream/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/plugins/apivideo_live_stream/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock
index 1c5cf3e..1c545ff 100644
Binary files a/plugins/apivideo_live_stream/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/plugins/apivideo_live_stream/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ
diff --git a/plugins/apivideo_live_stream/android/.gradle/buildOutputCleanup/cache.properties b/plugins/apivideo_live_stream/android/.gradle/buildOutputCleanup/cache.properties
index 7a41652..c53ef16 100644
--- a/plugins/apivideo_live_stream/android/.gradle/buildOutputCleanup/cache.properties
+++ b/plugins/apivideo_live_stream/android/.gradle/buildOutputCleanup/cache.properties
@@ -1,2 +1,2 @@
-#Mon Jul 13 17:53:35 CST 2026
+#Fri Jul 17 16:25:39 CST 2026
gradle.version=8.2
diff --git a/plugins/apivideo_live_stream/android/src/main/kotlin/video/api/flutter/livestream/FlutterLiveStreamView.kt b/plugins/apivideo_live_stream/android/src/main/kotlin/video/api/flutter/livestream/FlutterLiveStreamView.kt
index e93407c..c399bfc 100644
--- a/plugins/apivideo_live_stream/android/src/main/kotlin/video/api/flutter/livestream/FlutterLiveStreamView.kt
+++ b/plugins/apivideo_live_stream/android/src/main/kotlin/video/api/flutter/livestream/FlutterLiveStreamView.kt
@@ -175,8 +175,18 @@ class FlutterLiveStreamView(
}
fun dispose() {
- stopStream()
- streamer.stopPreview()
+ try {
+ stopStream()
+ } catch (e: Exception) {
+ android.util.Log.w("ApiVideoLiveStream", "stopStream during dispose failed", e)
+ _isStreaming = false
+ }
+ try {
+ streamer.stopPreview()
+ } catch (e: Exception) {
+ android.util.Log.w("ApiVideoLiveStream", "stopPreview during dispose failed", e)
+ }
+ _isPreviewing = false
flutterTexture.release()
}
@@ -195,14 +205,27 @@ class FlutterLiveStreamView(
}
fun stopStream() {
+ if (!_isStreaming && !streamer.isConnected) {
+ return
+ }
val isConnected = streamer.isConnected
- runBlocking {
- streamer.stopStream()
- streamer.disconnect()
- if (isConnected) {
- onDisconnected()
+ try {
+ runBlocking {
+ streamer.stopStream()
+ streamer.disconnect()
+ if (isConnected) {
+ onDisconnected()
+ }
+ _isStreaming = false
}
+ } catch (e: Exception) {
+ android.util.Log.w("ApiVideoLiveStream", "stopStream failed", e)
_isStreaming = false
+ try {
+ streamer.disconnect()
+ } catch (_: Exception) {
+ // ignore secondary disconnect failures
+ }
}
}
diff --git a/pubspec.yaml b/pubspec.yaml
index fa5f8b9..f950688 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -54,6 +54,7 @@ dependencies:
apivideo_live_stream:
path: plugins/apivideo_live_stream
jwt_decoder: ^2.0.1
+ network_info_plus: ^8.2.1
dev_dependencies:
flutter_test: