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