解决查看录像页 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});
|
||||
|
||||
factory GetRecordListResModel.fromJson(Map<String, dynamic> json) =>
|
||||
GetRecordListResModel(
|
||||
path: json['path'],
|
||||
items: json['items'] == null
|
||||
? []
|
||||
: List<RecordListItem>.from(
|
||||
json['items']!.map((x) => RecordListItem.fromJson(x)),
|
||||
),
|
||||
);
|
||||
factory GetRecordListResModel.fromJson(Map<String, dynamic> json) {
|
||||
final rawItems = json['items'];
|
||||
return GetRecordListResModel(
|
||||
path: json['path']?.toString(),
|
||||
items: rawItems is! List
|
||||
? []
|
||||
: rawItems
|
||||
.whereType<Map>()
|
||||
.map(
|
||||
(x) => RecordListItem.fromJson(Map<String, dynamic>.from(x)),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'path': path,
|
||||
|
||||
@@ -26,7 +26,7 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: '999779');
|
||||
_controller = TextEditingController(text: '905758');
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final token = AppStorage.getString(StorageKeys.authToken);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:recording_tool/app/config/api_common.dart';
|
||||
import 'package:recording_tool/core/network/providers/dio_providers.dart';
|
||||
@@ -28,17 +29,30 @@ class AuthServer {
|
||||
|
||||
/// 获取赛事列表
|
||||
/// [path] 赛事目录
|
||||
static Future<GetRecordListResModel> getRecordList(
|
||||
static Future<GetRecordListResModel?> getRecordList(
|
||||
Ref ref,
|
||||
String path,
|
||||
) async {
|
||||
final apiClient = ref.read(apiClientProvider);
|
||||
final data = await apiClient.get<GetRecordListResModel>(
|
||||
'http://sheling.local:9001/${AuthApi.getRecordList.path}',
|
||||
queryParameters: {'path': path},
|
||||
parser: (json) =>
|
||||
GetRecordListResModel.fromJson(json as Map<String, dynamic>),
|
||||
);
|
||||
return data;
|
||||
try {
|
||||
final apiClient = ref.read(apiClientProvider);
|
||||
// NAS /api/files 直接返回 {path, items},不是业务网关的 {code,message,data} 包装。
|
||||
final data = await apiClient.get<GetRecordListResModel>(
|
||||
'http://sheling.local:9001/${AuthApi.getRecordList.path}',
|
||||
queryParameters: {'path': path},
|
||||
wrapResponse: false,
|
||||
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 {
|
||||
if (eventName.isEmpty) return false;
|
||||
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);
|
||||
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/pages/page_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_toast.dart';
|
||||
|
||||
class ScanQrCodePage extends ConsumerStatefulWidget {
|
||||
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
|
||||
ConsumerState<ScanQrCodePage> createState() => _ScanQrCodePageState();
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ dependencies:
|
||||
apivideo_live_stream:
|
||||
path: plugins/apivideo_live_stream
|
||||
jwt_decoder: ^2.0.1
|
||||
network_info_plus: ^7.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user