解决查看录像页 UI
This commit is contained in:
@@ -0,0 +1,142 @@
|
|||||||
|
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 startedAt = DateTime.now();
|
||||||
|
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,
|
||||||
|
startedAt: startedAt,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
if (nasIp == null) {
|
||||||
|
final elapsedMs = DateTime.now().difference(startedAt).inMilliseconds;
|
||||||
|
debugPrint('NAS 探测未命中,总耗时: ${elapsedMs}ms');
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
required DateTime startedAt,
|
||||||
|
}) async {
|
||||||
|
if (cancelToken.isCancelled || nasIp != null) return;
|
||||||
|
|
||||||
|
final target = '$preUrl$host$_probePath';
|
||||||
|
try {
|
||||||
|
await dio.get<dynamic>(target, cancelToken: cancelToken);
|
||||||
|
_onHit(preUrl, host, cancelToken, startedAt);
|
||||||
|
} on DioException catch (error) {
|
||||||
|
if (error.type == DioExceptionType.cancel) return;
|
||||||
|
// 带 response 的 DioException 也表示已连通
|
||||||
|
if (error.response != null) {
|
||||||
|
_onHit(preUrl, host, cancelToken, startedAt);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// 超时 / 连接失败:未命中
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录命中:写 [nasIp]、打印 IP 与总耗时,并取消其余请求。
|
||||||
|
static void _onHit(
|
||||||
|
String preUrl,
|
||||||
|
int host,
|
||||||
|
CancelToken cancelToken,
|
||||||
|
DateTime startedAt,
|
||||||
|
) {
|
||||||
|
if (nasIp != null) return;
|
||||||
|
final ip = '${_hostPrefix(preUrl)}$host';
|
||||||
|
nasIp = ip;
|
||||||
|
final elapsedMs = DateTime.now().difference(startedAt).inMilliseconds;
|
||||||
|
debugPrint('NAS IP: $ip,探测总耗时: ${elapsedMs}ms');
|
||||||
|
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_search_nasIp.dart';
|
||||||
|
//
|
||||||
|
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
// unawaited(UtilSearchNasIp.discover());
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// 2) 业务侧读取已命中 IP:
|
||||||
|
//
|
||||||
|
// final ip = UtilSearchNasIp.nasIp;
|
||||||
|
// if (ip != null) {
|
||||||
|
// final base = 'http://$ip:5666';
|
||||||
|
// // 使用 base 访问 NAS...
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 3) 主动等待探测结果(少用,会阻塞当前异步流程):
|
||||||
|
//
|
||||||
|
// final ip = await UtilSearchNasIp.discover();
|
||||||
|
// debugPrint('result: $ip');
|
||||||
|
//
|
||||||
@@ -22,15 +22,20 @@ class GetRecordListResModel {
|
|||||||
|
|
||||||
GetRecordListResModel({this.path, this.items});
|
GetRecordListResModel({this.path, this.items});
|
||||||
|
|
||||||
factory GetRecordListResModel.fromJson(Map<String, dynamic> json) =>
|
factory GetRecordListResModel.fromJson(Map<String, dynamic> json) {
|
||||||
GetRecordListResModel(
|
final rawItems = json['items'];
|
||||||
path: json['path'],
|
return GetRecordListResModel(
|
||||||
items: json['items'] == null
|
path: json['path']?.toString(),
|
||||||
? []
|
items: rawItems is! List
|
||||||
: List<RecordListItem>.from(
|
? []
|
||||||
json['items']!.map((x) => RecordListItem.fromJson(x)),
|
: rawItems
|
||||||
),
|
.whereType<Map>()
|
||||||
);
|
.map(
|
||||||
|
(x) => RecordListItem.fromJson(Map<String, dynamic>.from(x)),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'path': path,
|
'path': path,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = TextEditingController(text: '999779');
|
_controller = TextEditingController(text: '905758');
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
final token = AppStorage.getString(StorageKeys.authToken);
|
final token = AppStorage.getString(StorageKeys.authToken);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:recording_tool/app/config/api_common.dart';
|
import 'package:recording_tool/app/config/api_common.dart';
|
||||||
import 'package:recording_tool/core/network/providers/dio_providers.dart';
|
import 'package:recording_tool/core/network/providers/dio_providers.dart';
|
||||||
@@ -28,17 +29,30 @@ class AuthServer {
|
|||||||
|
|
||||||
/// 获取赛事列表
|
/// 获取赛事列表
|
||||||
/// [path] 赛事目录
|
/// [path] 赛事目录
|
||||||
static Future<GetRecordListResModel> getRecordList(
|
static Future<GetRecordListResModel?> getRecordList(
|
||||||
Ref ref,
|
Ref ref,
|
||||||
String path,
|
String path,
|
||||||
) async {
|
) async {
|
||||||
final apiClient = ref.read(apiClientProvider);
|
try {
|
||||||
final data = await apiClient.get<GetRecordListResModel>(
|
final apiClient = ref.read(apiClientProvider);
|
||||||
'http://sheling.local:9001/${AuthApi.getRecordList.path}',
|
// NAS /api/files 直接返回 {path, items},不是业务网关的 {code,message,data} 包装。
|
||||||
queryParameters: {'path': path},
|
final data = await apiClient.get<GetRecordListResModel>(
|
||||||
parser: (json) =>
|
'http://sheling.local:9001/${AuthApi.getRecordList.path}',
|
||||||
GetRecordListResModel.fromJson(json as Map<String, dynamic>),
|
queryParameters: {'path': path},
|
||||||
);
|
wrapResponse: false,
|
||||||
return data;
|
parser: (json) {
|
||||||
|
if (json is! Map) {
|
||||||
|
throw const FormatException('录像列表响应格式错误');
|
||||||
|
}
|
||||||
|
return GetRecordListResModel.fromJson(
|
||||||
|
Map<String, dynamic>.from(json),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('getRecordList failed: $error');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ class AuthViewModel extends StateNotifier<AuthState> {
|
|||||||
Future<bool> getRecordList(String eventName) async {
|
Future<bool> getRecordList(String eventName) async {
|
||||||
if (eventName.isEmpty) return false;
|
if (eventName.isEmpty) return false;
|
||||||
final data = await AuthServer.getRecordList(_ref, eventName);
|
final data = await AuthServer.getRecordList(_ref, eventName);
|
||||||
if (data.items == null || data.items!.isEmpty) return false;
|
if (data == null) return false;
|
||||||
|
if (data.items == null) return false;
|
||||||
state = state.copyWith(recordList: data.items);
|
state = state.copyWith(recordList: data.items);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,26 +11,12 @@ import 'package:recording_tool/features/competition_teams/pages/page_competition
|
|||||||
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/pages/page_event_info.dart';
|
import 'package:recording_tool/features/events/pages/page_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/features/recording/model/model_recording_context.dart';
|
|
||||||
import 'package:recording_tool/shared/widgets/app_qr_scanner_dialog.dart';
|
import 'package:recording_tool/shared/widgets/app_qr_scanner_dialog.dart';
|
||||||
import 'package:recording_tool/shared/widgets/app_toast.dart';
|
import 'package:recording_tool/shared/widgets/app_toast.dart';
|
||||||
|
|
||||||
class ScanQrCodePage extends ConsumerStatefulWidget {
|
class ScanQrCodePage extends ConsumerStatefulWidget {
|
||||||
const ScanQrCodePage({super.key});
|
const ScanQrCodePage({super.key});
|
||||||
|
|
||||||
static const mockRtmpUrl =
|
|
||||||
'rtmp://192.168.1.245:19090/蔡依婷vs夏志豪_空中格斗赛_高中组/蔡依婷vs夏志豪_空中格斗赛_高中组';
|
|
||||||
|
|
||||||
static const mockRecordingContext = RecordingContext(
|
|
||||||
eventTitle: '全国青少年无人机大赛',
|
|
||||||
matchName: '空中格斗赛',
|
|
||||||
group: '高中组',
|
|
||||||
venue: '场地 1',
|
|
||||||
time: '7月1日 12:00-15:00',
|
|
||||||
playerName: '蔡依婷vs夏志豪',
|
|
||||||
playerPhone: '',
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<ScanQrCodePage> createState() => _ScanQrCodePageState();
|
ConsumerState<ScanQrCodePage> createState() => _ScanQrCodePageState();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: ^7.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user