diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index e58e1d2e..14303516 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -88,5 +88,13 @@ 需要使用蓝牙连接设备 NSBluetoothPeripheralUsageDescription 需要使用蓝牙连接设备 + + NSLocalNetworkUsageDescription + 需要访问本地网络以发现并连接局域网摄像头(ONVIF / RTSP)。 + NSBonjourServices + + _wsdiscovery._udp + _http._tcp + diff --git a/lib/features/pv_control/presentation/widgets/pv_local_rtsp_video.dart b/lib/features/pv_control/presentation/widgets/pv_local_rtsp_video.dart index 49416bd5..a7a87cb2 100644 --- a/lib/features/pv_control/presentation/widgets/pv_local_rtsp_video.dart +++ b/lib/features/pv_control/presentation/widgets/pv_local_rtsp_video.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:video_player/video_player.dart'; +import 'package:media_kit_video/media_kit_video.dart'; import 'package:maibu_satabot_v2/features/v2/local_camera/presentation/cubit/local_camera_cubit.dart'; import 'package:maibu_satabot_v2/features/v2/local_camera/presentation/cubit/local_camera_state.dart'; @@ -22,23 +23,44 @@ class PvLocalRtspVideo extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - final controller = context.read().videoController; + final cubit = context.read(); Widget layer; - if (controller != null && controller.value.isInitialized) { - // 部分摄像头 SDP 不带分辨率,size 为 0×0 时 aspectRatio 会崩成 - // 纯黑(与本地视频页同款兜底):回退 16:9。 - final size = controller.value.size; - final ratio = size.width > 0 && size.height > 0 - ? size.width / size.height - : 16 / 9; - layer = Center( - child: AspectRatio( - aspectRatio: ratio, - child: VideoPlayer(controller), - ), - ); + if (cubit.usesMediaKit) { + // iOS:media_kit Video + final vc = cubit.videoControllerIOS; + if (vc != null && cubit.iosVideoReady) { + final size = cubit.iosVideoSize; + final ratio = size.width > 0 && size.height > 0 + ? size.width / size.height + : 16 / 9; + layer = Center( + child: AspectRatio( + aspectRatio: ratio, + child: Video(controller: vc), + ), + ); + } else { + layer = const ColoredBox(color: Colors.black); + } } else { - layer = const ColoredBox(color: Colors.black); + // Android:video_player / ExoPlayer + final controller = cubit.videoController; + if (controller != null && controller.value.isInitialized) { + // 部分摄像头 SDP 不带分辨率,size 为 0×0 时 aspectRatio 会崩成 + // 纯黑(与本地视频页同款兵底):回退 16:9。 + final size = controller.value.size; + final ratio = size.width > 0 && size.height > 0 + ? size.width / size.height + : 16 / 9; + layer = Center( + child: AspectRatio( + aspectRatio: ratio, + child: VideoPlayer(controller), + ), + ); + } else { + layer = const ColoredBox(color: Colors.black); + } } return Stack( fit: StackFit.expand, diff --git a/lib/features/v2/local_camera/presentation/cubit/local_camera_cubit.dart b/lib/features/v2/local_camera/presentation/cubit/local_camera_cubit.dart index 411e719c..1f8810bb 100644 --- a/lib/features/v2/local_camera/presentation/cubit/local_camera_cubit.dart +++ b/lib/features/v2/local_camera/presentation/cubit/local_camera_cubit.dart @@ -7,6 +7,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:video_player/video_player.dart'; +import 'package:media_kit/media_kit.dart'; +import 'package:media_kit_video/media_kit_video.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../data/onvif_client.dart'; @@ -53,6 +55,31 @@ class LocalCameraCubit extends Cubit /// 供页面渲染视频用;未就绪时为 null。 VideoPlayerController? get videoController => _videoController; + // ===== iOS 专用:media_kit 播放器(Android 恒为 null)===== + // iOS 的 video_player 后端是 AVPlayer,不支持 rtsp://;故 iOS 单独用 media_kit(libmpv) + // 拉流,Android 保留 video_player(ExoPlayer) 低延迟链路,两条路按平台互不干扰。 + static final bool _usesMediaKit = !kIsWeb && Platform.isIOS; + bool get usesMediaKit => _usesMediaKit; + + Player? _mkPlayer; + VideoController? _mkVideoController; + bool _mkReady = false; + StreamSubscription? _mkErrorSub; + StreamSubscription? _mkBufferingSub; + + /// iOS:供页面渲染 media_kit `Video` 用;未就绪时为 null。 + VideoController? get videoControllerIOS => _mkVideoController; + bool get iosVideoReady => _mkReady && _mkVideoController != null; + + /// iOS:当前视频尺寸(拿不到时 0×0,渲染侧回退 16:9)。 + Size get iosVideoSize { + final Player? p = _mkPlayer; + if (p == null) return Size.zero; + final int w = p.state.width ?? 0; + final int h = p.state.height ?? 0; + return Size(w.toDouble(), h.toDouble()); + } + static const String _prefsKeyLastList = 'local_camera_last_list'; // ===== 提示语常量(本功能为以太网「一对一」直连,不依赖 WiFi/移动数据)===== @@ -117,12 +144,15 @@ class LocalCameraCubit extends Cubit final ok = await _isEthernetConnected(); if (!ok) { final detail = await _describeNetworks(); + final header = Platform.isIOS + ? '未检测到局域网连接。请让 iPhone/iPad 与摄像头连入同一 WiFi 后重试' + : msgNoNetworkEntry; emit( state.copyWith( phase: LocalCameraPhase.noNetwork, networkStep: ChainStatus.failed, discoveryStep: ChainStatus.idle, - message: '$msgNoNetworkEntry\n\n当前网卡:$detail', + message: '$header\n\n当前网卡:$detail', clearSelected: true, clearRtsp: true, showReconnect: false, @@ -314,6 +344,8 @@ class LocalCameraCubit extends Cubit /// 打开单个候选 URL,[timeout] 内完成 initialize() 且首帧就绪即视为成功。 /// 失败/超时则 dispose 后返回 false。传输方式(UDP/TCP)与缓冲水位在此刻由原生侧读调参配置决定。 Future _tryPlay(String url, {required Duration timeout}) async { + // iOS:AVPlayer 不支持 RTSP,改走 media_kit(libmpv)。 + if (_usesMediaKit) return _tryPlayMediaKit(url, timeout: timeout); VideoPlayerController? c; try { // 调参必须在 initialize 之前下发:LoadControl 与 RtspMediaSource.Factory 都是 @@ -381,6 +413,10 @@ class LocalCameraCubit extends Cubit _stopLagWatchdog(); _stallTimer?.cancel(); _stallTimer = null; + if (_usesMediaKit) { + await _disposeMediaKit(); + return; + } final c = _videoController; _videoController = null; if (c != null) { @@ -396,6 +432,139 @@ class LocalCameraCubit extends Cubit } } + // ===== iOS:media_kit(libmpv) 拉流实现 ===== + Future _tryPlayMediaKit(String url, {required Duration timeout}) async { + try { + // 每次拉流重建 Player,避免旧会话残留(对齐 Android 的 dispose+新建)。 + await _disposeMediaKit(); + final player = Player( + configuration: PlayerConfiguration( + // 减小解耦缓存,压低直播延迟(对应 Android 侧紧水位策略)。 + bufferSize: 1 * 1024 * 1024, + // 关键:media_kit 默认协议白名单不含 rtsp/sdp,必须显式放行(并保留其余默认值), + // 否则 RTSP 直接被判「协议不允许」放不出画面。 + protocolWhitelist: const [ + 'udp', 'rtp', 'tcp', 'tls', 'data', 'file', + 'http', 'https', 'crypto', 'rtsp', 'sdp', + ], + ), + ); + final vc = VideoController(player); + _mkPlayer = player; + _mkVideoController = vc; + _mkReady = false; + + // 低延迟 + 抗花屏参数下发(对齐项目已验证的 media_kit RTSP 调优): + // 必须在 open(loadfile) 之前、播放器空闲时下发;mpv 原生命令只暴露在 NativePlayer 上, + // 每条 try/catch 静默忽略,避免不支持的属性破坏已能播放的路径。 + final Object? native = player.platform; + Future tryCmd(List args) async { + if (native is! NativePlayer) return; + try { + await native.command(args); + } catch (e) { + debugPrint('🎬 [iOS拉流] 忽略命令 $args:${e.runtimeType}'); + } + } + + // RTSP 走 TCP 交错(rtsp_transport 属 lavf 解封装选项,必须经 demuxer-lavf-o): + // 局域网/图传 UDP 丢包会花屏,TCP 重传补全画面。注意同时保留 media_kit 默认白名单。 + await tryCmd([ + 'set_property', + 'demuxer-lavf-o', + 'rtsp_transport=tcp,seg_max_retry=5,strict=experimental,' + 'allowed_extensions=ALL,' + 'protocol_whitelist=[udp,rtp,tcp,tls,data,file,http,https,crypto,rtsp,sdp]', + ]); + // 关缓存 + 紧读前缓冲,压低直播延迟。 + await tryCmd(['set_property', 'cache', 'no']); + await tryCmd(['set_property', 'demuxer-readahead-secs', '0.3']); + + // 断流判定:error 非空 或 缓冲卡死超 12s → 触发自动重连(与 Android 语义一致)。 + _mkErrorSub = player.stream.error.listen((e) { + if (e.isNotEmpty && + state.phase == LocalCameraPhase.playing && + !_userStopped) { + debugPrint('🎬 [iOS拉流] 运行错误:$e'); + _handleDisconnected(); + } + }); + _mkBufferingSub = player.stream.buffering.listen((b) { + if (state.phase != LocalCameraPhase.playing || _userStopped) return; + _stallTimer?.cancel(); + if (b) { + _stallTimer = Timer(const Duration(seconds: 12), _handleDisconnected); + } else { + _stallTimer = null; + } + }); + + // 首帧就绪判据:拿到宽度(>0)即认为流已连上并可渲染。 + final completer = Completer(); + late StreamSubscription wSub; + wSub = player.stream.width.listen((w) { + if ((w ?? 0) > 0 && !completer.isCompleted) completer.complete(true); + }); + unawaited( + player.open(Media(url), play: true).catchError((Object e) { + debugPrint('🎬 [iOS拉流] open 抛错:$e'); + if (!completer.isCompleted) completer.complete(false); + }), + ); + + final bool ok = + await completer.future.timeout(timeout, onTimeout: () => false); + await wSub.cancel(); + if (!ok) { + await _disposeMediaKit(); + return false; + } + _mkReady = true; + debugPrint('🎬 [iOS拉流] 成功:$url'); + return true; + } catch (e) { + debugPrint('🎬 [iOS拉流] 异常:$e'); + await _disposeMediaKit(); + return false; + } + } + + Future _disposeMediaKit() async { + await _mkErrorSub?.cancel(); + _mkErrorSub = null; + await _mkBufferingSub?.cancel(); + _mkBufferingSub = null; + _stallTimer?.cancel(); + _stallTimer = null; + final p = _mkPlayer; + _mkPlayer = null; + _mkVideoController = null; + _mkReady = false; + if (p != null) { + try { + await p.dispose(); + } catch (e) { + debugPrint('🧹 [iOS释放] player.dispose 失败(RTSP 会话可能未 TEARDOWN):$e'); + } + } + } + + /// iOS:局域网连通判据——走同一 WiFi(或以太网),不要求 USB 以太网卡存在。 + Future _isLanConnectedForIOS() async { + try { + final results = await Connectivity().checkConnectivity(); + final bool lan = results.contains(ConnectivityResult.wifi) || + results.contains(ConnectivityResult.ethernet); + debugPrint( + '🌐 [网络检查-iOS] ${lan ? "已连接 WiFi/以太网 LAN ✔" : "未见 LAN"}:$results', + ); + return lan; + } catch (e) { + debugPrint('🌐 [网络检查-iOS] connectivity 失败:$e(保守放行,交给后续探测)'); + return true; + } + } + void _cancelWatchdog() { _openWatchdog?.cancel(); _openWatchdog = null; @@ -416,6 +585,10 @@ class LocalCameraCubit extends Cubit @override void didChangeAppLifecycleState(AppLifecycleState state) { // 参数必须叫 state(重写方法不得改名),所以下面访问 Cubit 自己的状态时必须带 this.。 + if (_usesMediaKit) { + _handleLifecycleMediaKit(state); + return; + } final VideoPlayerController? c = _videoController; if (state == AppLifecycleState.paused || state == AppLifecycleState.hidden) { @@ -447,8 +620,36 @@ class LocalCameraCubit extends Cubit // 据此重拉流只会把正常观看打断。 } + /// iOS(media_kit) 的生命周期处理:退后台只 pause(media_kit 无 video_player 的 seekTo(0) + /// 棘轮问题),回前台直接重拉流以回到实时边缘。 + void _handleLifecycleMediaKit(AppLifecycleState lifecycle) { + final p = _mkPlayer; + if (lifecycle == AppLifecycleState.paused || + lifecycle == AppLifecycleState.hidden) { + if (p != null && + state.phase == LocalCameraPhase.playing && + p.state.playing) { + _resumeOnForeground = true; + unawaited(p.pause()); + _stallTimer?.cancel(); + _stallTimer = null; + debugPrint('🧊 [生命周期-iOS] 退后台:已暂停(回前台重拉流)'); + } + return; + } + if (lifecycle == AppLifecycleState.resumed && _resumeOnForeground) { + _resumeOnForeground = false; + if (_userStopped || isClosed) return; + if (state.phase == LocalCameraPhase.connecting) return; + debugPrint('🔥 [生命周期-iOS] 回前台:重拉流以回到实时边缘'); + unawaited(reapplyAndReplay()); + } + } + // ===== 延迟水位观测 ===== void _startLagWatchdog() { + // 该探针依赖 video_player 的 buffered/position 语义;iOS(media_kit) 无对应读数,跳过。 + if (_usesMediaKit) return; _lagTimer?.cancel(); _prevPosMs = -1; _prevBufMs = -1; @@ -536,7 +737,9 @@ class LocalCameraCubit extends Cubit state.copyWith( phase: LocalCameraPhase.disconnected, playStep: ChainStatus.failed, - message: msgDisconnected, + message: _usesMediaKit + ? '局域网连接已断开,请检查 WiFi 与摄像头是否在线' + : msgDisconnected, showReconnect: true, ), ); @@ -690,6 +893,11 @@ class LocalCameraCubit extends Cubit /// 1) `connectivity_plus` 直接报 `ConnectivityResult.ethernet`; /// 2) 本机网卡里存在“名字像以太网 或 非 WiFi/移动”的 IPv4 接口。 Future _isEthernetConnected() async { + // iOS 无「USB-C 转以太网直连摄像头」这条链路,本功能改走同一 WiFi 局域网, + // 判据放宽为「连上 WiFi/以太网 LAN」,不再要求以太网卡存在。 + if (Platform.isIOS) { + return _isLanConnectedForIOS(); + } // 信号 1 try { final results = await Connectivity().checkConnectivity(); diff --git a/lib/features/v2/local_camera/presentation/pages/local_camera_page.dart b/lib/features/v2/local_camera/presentation/pages/local_camera_page.dart index c4d26f34..7fb95262 100644 --- a/lib/features/v2/local_camera/presentation/pages/local_camera_page.dart +++ b/lib/features/v2/local_camera/presentation/pages/local_camera_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:video_player/video_player.dart'; +import 'package:media_kit_video/media_kit_video.dart'; import 'package:maibu_satabot_v2/features/pv_control/presentation/pages/pv_control_page.dart'; @@ -184,8 +185,22 @@ class _LocalCameraViewState extends State<_LocalCameraView> { ); } - // ===== 视频渲染(video_player / ExoPlayer)===== + // ===== 视频渲染(Android=video_player/ExoPlayer;iOS=media_kit)===== Widget _buildPlayer(LocalCameraCubit cubit) { + // iOS 分支:media_kit 的 Video 组件 + if (cubit.usesMediaKit) { + final vc = cubit.videoControllerIOS; + if (vc == null || !cubit.iosVideoReady) { + return const ColoredBox(color: Colors.black); + } + final Size size = cubit.iosVideoSize; + final double ratio = + size.width > 0 && size.height > 0 ? size.width / size.height : 16 / 9; + return Center( + child: AspectRatio(aspectRatio: ratio, child: Video(controller: vc)), + ); + } + // Android 分支:video_player / ExoPlayer final controller = cubit.videoController; // 控制器尚未创建 / 尚未初始化完成:先置黑底,由上层“连接中”遮罩提示 if (controller == null || !controller.value.isInitialized) { diff --git a/lib/main.dart b/lib/main.dart index e30c56fc..058a5eb3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_patcher/flutter_patcher.dart'; +import 'package:media_kit/media_kit.dart'; import 'package:go_router/go_router.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart'; @@ -41,6 +42,12 @@ void main() async { () async { WidgetsFlutterBinding.ensureInitialized(); + // iOS 本地摄像头 RTSP 走 media_kit(libmpv):AVPlayer 不支持 rtsp://。 + // 仅 iOS 初始化;Android 不引 libmpv、也不调用,避免影响启动与包体。 + if (Platform.isIOS) { + MediaKit.ensureInitialized(); + } + await FlutterPatcher.init(); await init(); final logger = sl(); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 2a71aa42..2aceb4b5 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -9,9 +9,11 @@ #include #include #include +#include #include #include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = @@ -23,6 +25,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) isar_community_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin"); isar_flutter_libs_plugin_register_with_registrar(isar_community_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) media_kit_video_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin"); + media_kit_video_plugin_register_with_registrar(media_kit_video_registrar); g_autoptr(FlPluginRegistrar) open_file_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin"); open_file_linux_plugin_register_with_registrar(open_file_linux_registrar); @@ -32,4 +37,7 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) volume_controller_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "VolumeControllerPlugin"); + volume_controller_plugin_register_with_registrar(volume_controller_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index abab2303..672f0709 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -6,9 +6,11 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux flutter_webrtc isar_community_flutter_libs + media_kit_video open_file_linux sentry_flutter url_launcher_linux + volume_controller ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 3752d622..75f336c4 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -14,6 +14,7 @@ import flutter_webrtc import geolocator_apple import iris_method_channel import isar_community_flutter_libs +import media_kit_video import mobile_scanner import open_file_mac import package_info_plus @@ -21,6 +22,8 @@ import sentry_flutter import shared_preferences_foundation import url_launcher_macos import video_player_avfoundation +import volume_controller +import wakelock_plus import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { @@ -33,6 +36,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) IrisMethodChannelPlugin.register(with: registry.registrar(forPlugin: "IrisMethodChannelPlugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) + MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) @@ -40,5 +44,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) + VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin")) + WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index b71249cf..b442e14b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -948,6 +948,30 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.11.1" + media_kit: + dependency: "direct main" + description: + name: media_kit + sha256: ae9e79597500c7ad6083a3c7b7b7544ddabfceacce7ae5c9709b0ec16a5d6643 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.6" + media_kit_libs_ios_video: + dependency: "direct main" + description: + name: media_kit_libs_ios_video + sha256: b5382994eb37a4564c368386c154ad70ba0cc78dacdd3fb0cd9f30db6d837991 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.4" + media_kit_video: + dependency: "direct main" + description: + name: media_kit_video + sha256: "813858c3fe84eb46679eb698695f60665e2bfbef757766fac4d2e683f926e15a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" meta: dependency: transitive description: @@ -1300,6 +1324,30 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.6.0" + safe_local_storage: + dependency: transitive + description: + name: safe_local_storage + sha256: "494b982d5edb71030650ea463d939670e91b232b588323dc75229d2c5f23e7b7" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.6" + screen_brightness_android: + dependency: transitive + description: + name: screen_brightness_android + sha256: "2008ad8e9527cc968f7a4de1ec58b476d495b3c612a149dbd6550c4f046da147" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.6" + screen_brightness_platform_interface: + dependency: transitive + description: + name: screen_brightness_platform_interface + sha256: "2de60c0ba569b898950029cc1f7e9dd72bda44a22beb5054aac331cb6fce2ff2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" scroll_to_index: dependency: transitive description: @@ -1513,6 +1561,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.3.1" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + uri_parser: + dependency: transitive + description: + name: uri_parser + sha256: "051c62e5f693de98ca9f130ee707f8916e2266945565926be3ff20659f7853ce" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" url_launcher: dependency: transitive description: @@ -1703,6 +1767,30 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.60.6" + volume_controller: + dependency: transitive + description: + name: volume_controller + sha256: bed7c57b1da19d60a58e2ed598b6824754cd9af1b728b26fb966a8745c72629c + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.5.0" + wakelock_plus: + dependency: transitive + description: + name: wakelock_plus + sha256: ddf3db70eaa10c37558ff817519b85d527dbd21034fd5d8e1c2e85f31588f1c1 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.2" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: b13f99e992e7ae6a152e16c5559d3c07ff445b13330192662494e614ca3e7d7b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.1" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9043f3f7..c7a04062 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -152,6 +152,13 @@ dependencies: crypto: ^3.0.3 connectivity_plus: ^6.0.3 + # iOS 专用 RTSP 播放引擎:AVPlayer 不支持 rtsp://,仅 iOS 用 media_kit(libmpv)。 + # Android 仍走 video_player(ExoPlayer) 保持低延迟,不使用这两个播放器包。 + # 只引 iOS libs(media_kit_libs_ios_video),避免把 libmpv 打进 Android APK。 + media_kit: ^1.1.10+1 + media_kit_video: ^1.2.5 + media_kit_libs_ios_video: ^1.1.4 + # 本地覆盖 video_player_android:注入零缓冲 LoadControl (bufferForPlaybackMs=0) 实现直播低延迟 dependency_overrides: video_player_android: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 71a40e41..35d95788 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -14,9 +14,11 @@ #include #include #include +#include #include #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { AgoraRtcEnginePluginRegisterWithRegistrar( @@ -35,10 +37,14 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("IrisMethodChannelPluginCApi")); IsarFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); + MediaKitVideoPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); SentryFlutterPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("SentryFlutterPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + VolumeControllerPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("VolumeControllerPluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 09e2268e..180e6fe1 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -11,9 +11,11 @@ list(APPEND FLUTTER_PLUGIN_LIST geolocator_windows iris_method_channel isar_community_flutter_libs + media_kit_video permission_handler_windows sentry_flutter url_launcher_windows + volume_controller ) list(APPEND FLUTTER_FFI_PLUGIN_LIST