Commit 21360be7 authored by 张宏's avatar 张宏

接入p2p完成,支持webrtc和p2p来回切换

parent 9895a233
This diff is collapsed.
org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk-22.0.1.jdk/Contents/Home
org.gradle.jvmargs=-Xmx4G org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
/// 视频传输模式
///
/// 涉及页面:首页监护舱 - 监控画面
enum VideoStreamMode {
/// vsdk P2P + AppPlayer 连接模式
p2p,
/// flutter_webrtc HTTP 信令模式
webrtc,
}
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:vsdk/app_player.dart';
import 'package:vsdk/camera_device/camera_device.dart';
import 'package:vsdk/camera_device/commands/video_command.dart';
/// P2P 视频服务连接状态
enum P2pServiceState {
/// 未连接
disconnected,
/// 正在连接(含 P2P 穿透 + 设备登录 + 视频流启动)
connecting,
/// 已连接,视频流播放中
connected,
/// 连接失败
failed,
}
/// P2P 视频服务层
///
/// 职责:
/// - 封装 [CameraDevice] 的 P2P 连接生命周期
/// - 管理 [AppPlayerController] 实例(视频解码 + Texture 渲染)
/// - 向上层暴露连接状态流和播放控制器
///
/// 涉及页面:首页监护舱 - 监控画面
class P2pVideoService {
/// P2P 摄像头设备实例
CameraDevice? _cameraDevice;
/// 视频播放控制器(供 [AppPlayerView] 使用)
/// stop() 只停止播放不 dispose,保持 Texture widget 存活
AppPlayerController? _playerController;
/// 连接状态广播流
final StreamController<P2pServiceState> _stateController =
StreamController<P2pServiceState>.broadcast();
/// 连接状态流(供 UI 层订阅)
Stream<P2pServiceState> get stateStream => _stateController.stream;
P2pServiceState _currentState = P2pServiceState.disconnected;
/// 当前连接状态
P2pServiceState get currentState => _currentState;
/// 获取视频播放控制器
AppPlayerController? get playerController => _playerController;
// ==================== 公共接口 ====================
/// 初始化 P2P 连接并启动视频流
///
/// [deviceId] 设备序列号(与 WebRTC 使用相同的 deviceNo)
/// [username] 设备登录用户名,默认 'admin'
/// [password] 设备登录密码
/// [model] 设备型号,默认 'wifi'
Future<void> connect({
required String deviceId,
String username = 'admin',
required String password,
String model = 'wifi',
}) async {
_updateState(P2pServiceState.connecting);
try {
// 1. 清理上一次 CameraDevice 残留
await _cleanupCamera();
// 2. 创建 CameraDevice 并监听连接状态
_cameraDevice = CameraDevice(deviceId, 'camera', username, password, model);
_cameraDevice!.addListener<CameraConnectChanged>(_onConnectChanged);
// 3. 发起 P2P 连接(内部完成:创建客户端 → P2P 穿透 → 登录)
final result = await _cameraDevice!.connect();
debugPrint('[P2pVideoService] P2P connect 结果: $result');
if (result != CameraConnectState.connected) {
debugPrint('[P2pVideoService] 连接失败: $result');
_updateState(P2pServiceState.failed);
return;
}
// 4. 复用现有播放器或创建新播放器
final bool needCreate = _playerController == null;
if (needCreate) {
_playerController = AppPlayerController();
final created = await _playerController!.create(
audioRate: 8000,
hardware: 1,
);
if (!created) {
debugPrint('[P2pVideoService] 播放器创建失败');
_updateState(P2pServiceState.failed);
return;
}
}
// 5. 获取 clientPtr 并绑定视频源
final clientPtr = await _cameraDevice!.getClientPtr();
if (clientPtr == 0) {
debugPrint('[P2pVideoService] clientPtr 无效: $clientPtr');
_updateState(P2pServiceState.failed);
return;
}
final sourceSet = await _playerController!.setVideoSource(
LiveVideoSource(clientPtr),
);
if (!sourceSet) {
debugPrint('[P2pVideoService] 视频源绑定失败');
_updateState(P2pServiceState.failed);
return;
}
// 6. 开启直播流
final streamStarted = await _cameraDevice!.startStream(
resolution: VideoResolution.general,
);
if (!streamStarted) {
debugPrint('[P2pVideoService] 直播流启动失败');
_updateState(P2pServiceState.failed);
return;
}
// 7. 启动播放
final started = await _playerController!.start();
if (!started) {
debugPrint('[P2pVideoService] 播放器启动失败');
_updateState(P2pServiceState.failed);
return;
}
_updateState(P2pServiceState.connected);
debugPrint('[P2pVideoService] P2P 视频连接建立完成');
} catch (e) {
debugPrint('[P2pVideoService] 连接异常: $e');
_updateState(P2pServiceState.failed);
}
}
/// 停止当前连接(不关闭 StreamController,不销毁播放器)
///
/// 用于模式切换场景:停掉 P2P 连接资源(CameraDevice、直播流),
/// 但保持 AppPlayerController 存活(Texture widget 不会访问已销毁纹理)
/// 且保持 StreamController 存活。
Future<void> stop() async {
debugPrint('[P2pVideoService] 停止连接...');
await _cleanupCamera();
// 播放器保持存活 —— stop() 后 connect() 可复用
_updateState(P2pServiceState.disconnected);
}
/// 释放所有资源(含播放器 + StreamController)
///
/// 调用后此实例不可再复用 [connect]。
Future<void> dispose() async {
debugPrint('[P2pVideoService] 释放资源...');
await _cleanupCamera();
await _cleanupPlayer();
await _stateController.close();
}
// ==================== 内部实现 ====================
/// P2P 连接状态变化回调
void _onConnectChanged(CameraDevice device, CameraConnectState state) {
debugPrint('[P2pVideoService] CameraConnectState 变化: $state');
if (state == CameraConnectState.disconnect ||
state == CameraConnectState.offline) {
if (_currentState == P2pServiceState.connected) {
_updateState(P2pServiceState.failed);
}
}
}
/// 清理 CameraDevice(停流 + 移除监听 + 销毁设备)
Future<void> _cleanupCamera() async {
if (_cameraDevice != null) {
try {
await _cameraDevice!.stopStream();
} catch (_) {}
try {
_cameraDevice!.removeListener<CameraConnectChanged>(_onConnectChanged);
} catch (_) {}
try {
await _cameraDevice!.deviceDestroy();
} catch (_) {}
_cameraDevice = null;
}
}
/// 清理播放器(仅在最终 dispose 时调用)
Future<void> _cleanupPlayer() async {
if (_playerController != null) {
try {
await _playerController!.dispose();
} catch (_) {}
_playerController = null;
}
}
/// 更新连接状态并广播
void _updateState(P2pServiceState state) {
if (_currentState == state) return;
_currentState = state;
if (!_stateController.isClosed) {
_stateController.add(state);
}
}
}
...@@ -37,8 +37,14 @@ class WebrtcService { ...@@ -37,8 +37,14 @@ class WebrtcService {
final WebrtcRepository _repository; final WebrtcRepository _repository;
/// 视频渲染器(供 RTCVideoView 使用) /// 视频渲染器(供 RTCVideoView 使用)
/// 生命周期:[renderer] 在首次 connect 时 initialize,
/// stop() 只清空 srcObject 不 dispose,保持 PlatformView 存活;
/// 仅在 [dispose] 最终销毁。
final RTCVideoRenderer renderer = RTCVideoRenderer(); final RTCVideoRenderer renderer = RTCVideoRenderer();
/// 渲染器是否已完成首次初始化
bool _rendererInitialized = false;
/// P2P 连接实例 /// P2P 连接实例
RTCPeerConnection? _peerConnection; RTCPeerConnection? _peerConnection;
...@@ -123,11 +129,13 @@ class WebrtcService { ...@@ -123,11 +129,13 @@ class WebrtcService {
} }
} }
/// 释放所有资源 /// 停止当前连接(不关闭 StreamController,不销毁 Renderer)
/// ///
/// 必须在使用完毕后调用,否则会导致内存泄漏和摄像头资源占用。 /// 用于模式切换场景:停掉 WebRTC 连接资源(PeerConnection、DataChannel、
Future<void> dispose() async { /// 媒体轨道),但保持 StreamController 存活(后续 connect 可继续广播状态)
debugPrint('[WebrtcService] 释放资源...'); /// 且保持 Renderer 存活(RTCVideoView 的 PlatformView 不会访问已销毁纹理)。
Future<void> stop() async {
debugPrint('[WebrtcService] 停止连接...');
_heartbeatTimer?.cancel(); _heartbeatTimer?.cancel();
_heartbeatTimer = null; _heartbeatTimer = null;
...@@ -135,18 +143,27 @@ class WebrtcService { ...@@ -135,18 +143,27 @@ class WebrtcService {
await _dataChannel?.close(); await _dataChannel?.close();
_dataChannel = null; _dataChannel = null;
// 停止所有音视频轨道 // 停止所有音视频轨道,并清空 srcObject 使 RTCVideoView 显示黑屏
renderer.srcObject?.getAudioTracks().forEach((t) => t.stop()); renderer.srcObject?.getAudioTracks().forEach((t) => t.stop());
renderer.srcObject?.getVideoTracks().forEach((t) => t.stop()); renderer.srcObject?.getVideoTracks().forEach((t) => t.stop());
renderer.srcObject = null;
await _peerConnection?.close(); await _peerConnection?.close();
await _peerConnection?.dispose(); await _peerConnection?.dispose();
_peerConnection = null; _peerConnection = null;
await renderer.dispose(); _updateState(WebrtcConnectionState.disconnected);
}
/// 释放所有资源(含 Renderer + StreamController)
///
/// 调用后此实例不可再复用 [connect]。
Future<void> dispose() async {
debugPrint('[WebrtcService] 释放资源...');
await stop();
await renderer.dispose();
_rendererInitialized = false;
await _stateController.close(); await _stateController.close();
_updateState(WebrtcConnectionState.disconnected);
} }
// ==================== 内部实现 ==================== // ==================== 内部实现 ====================
...@@ -163,8 +180,11 @@ class WebrtcService { ...@@ -163,8 +180,11 @@ class WebrtcService {
// 和后续 onConnectionState 迟到触发重连 // 和后续 onConnectionState 迟到触发重连
await _cleanupConnection(); await _cleanupConnection();
// 初始化视频渲染器 // 仅首次需要初始化渲染器;stop() 后 renderer 保持存活无需重新初始化
await renderer.initialize(); if (!_rendererInitialized) {
await renderer.initialize();
_rendererInitialized = true;
}
// 创建 P2P 连接 // 创建 P2P 连接
_peerConnection = await createPeerConnection(_iceServers); _peerConnection = await createPeerConnection(_iceServers);
...@@ -238,9 +258,6 @@ class WebrtcService { ...@@ -238,9 +258,6 @@ class WebrtcService {
); );
// ========== 竞态防护 ========== // ========== 竞态防护 ==========
// 在 HTTP 请求期间可能触发了重连(_onReconnect → _initWebrtc),
// 此时 _peerConnection 已经是新连接,当前调用必须放弃,
// 否则会在新连接上错误地 setRemoteDescription → native SIGSEGV
if (currentGen != _connectionGen) { if (currentGen != _connectionGen) {
debugPrint( debugPrint(
'[WebrtcService] 代际不匹配 (current=$currentGen, latest=$_connectionGen),' '[WebrtcService] 代际不匹配 (current=$currentGen, latest=$_connectionGen),'
......
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:laki_icu_app/enums/video_stream_mode_enum.dart';
import 'package:laki_icu_app/services/monitoring_service.dart'; import 'package:laki_icu_app/services/monitoring_service.dart';
import 'package:laki_icu_app/services/p2p_video_service.dart';
import 'package:laki_icu_app/services/webrtc_service.dart'; import 'package:laki_icu_app/services/webrtc_service.dart';
import 'package:laki_icu_app/utils/constants.dart';
import 'home_index_state.dart'; import 'home_index_state.dart';
...@@ -15,14 +17,22 @@ const _mockDevicePassword = 'MOCK_WIFI_PWD'; ...@@ -15,14 +17,22 @@ const _mockDevicePassword = 'MOCK_WIFI_PWD';
class HomeIndexCubit extends Cubit<HomeIndexState> { class HomeIndexCubit extends Cubit<HomeIndexState> {
final MonitoringService _monitoringService; final MonitoringService _monitoringService;
final WebrtcService _webrtcService; final WebrtcService _webrtcService;
final P2pVideoService _p2pVideoService;
StreamSubscription<WebrtcConnectionState>? _webrtcStateSub; StreamSubscription<WebrtcConnectionState>? _webrtcStateSub;
StreamSubscription<P2pServiceState>? _p2pStateSub;
/// 连接代际计数器 —— 每次切换模式时 +1,
/// 防止旧连接的异步结果污染当前模式的状态。
int _switchGen = 0;
HomeIndexCubit() HomeIndexCubit()
: _monitoringService = MonitoringService(), : _monitoringService = MonitoringService(),
_webrtcService = WebrtcService(), _webrtcService = WebrtcService(),
_p2pVideoService = P2pVideoService(),
super(const HomeIndexState()) { super(const HomeIndexState()) {
_listenWebrtcState(); _listenWebrtcState();
_listenP2pState();
loadData(); loadData();
} }
...@@ -34,6 +44,16 @@ class HomeIndexCubit extends Cubit<HomeIndexState> { ...@@ -34,6 +44,16 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
}); });
} }
/// 监听 P2P 视频服务状态变化并同步到 State
void _listenP2pState() {
_p2pStateSub = _p2pVideoService.stateStream.listen((state) {
if (isClosed) return;
emit(this.state.copyWith(
isP2pConnected: state == P2pServiceState.connected,
));
});
}
Future<void> loadData() async { Future<void> loadData() async {
emit(state.copyWith(isLoading: true, error: null)); emit(state.copyWith(isLoading: true, error: null));
try { try {
...@@ -56,8 +76,8 @@ class HomeIndexCubit extends Cubit<HomeIndexState> { ...@@ -56,8 +76,8 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
error: null, error: null,
)); ));
// 数据加载完成后初始化 WebRTC 视频连接 // 数据加载完成后,按当前模式初始化视频连接
_initWebrtc(); _initVideoByMode();
} catch (e) { } catch (e) {
if (isClosed) return; if (isClosed) return;
emit(state.copyWith( emit(state.copyWith(
...@@ -68,13 +88,17 @@ class HomeIndexCubit extends Cubit<HomeIndexState> { ...@@ -68,13 +88,17 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
} }
} }
/// 初始化 WebRTC 视频连接 /// 依据当前 [VideoStreamMode] 启动对应的视频连接
Future<void> _initWebrtc() async { void _initVideoByMode() {
// if (Constants.useMockData) { if (state.videoStreamMode == VideoStreamMode.p2p) {
// // Mock 模式:跳过真实连接,保持断开状态 _initP2p();
// return; } else {
// } _initWebrtc();
}
}
/// 初始化 WebRTC 视频连接(已有逻辑保持不变)
Future<void> _initWebrtc() async {
await _webrtcService.connect( await _webrtcService.connect(
// deviceNo: _mockDeviceNo, // deviceNo: _mockDeviceNo,
// password: _mockDevicePassword, // password: _mockDevicePassword,
...@@ -83,11 +107,74 @@ class HomeIndexCubit extends Cubit<HomeIndexState> { ...@@ -83,11 +107,74 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
); );
} }
/// 手动重试视频连接 /// 手动重试 WebRTC
Future<void> retryWebrtc() async { Future<void> retryWebrtc() async {
await _initWebrtc(); await _initWebrtc();
} }
/// 初始化 P2P 视频连接
Future<void> _initP2p() async {
await _p2pVideoService.connect(
deviceId: 'VE10085871QOXG',
username: 'admin',
password: '143548',
);
}
// ==================== 模式切换 ====================
/// 切换视频传输模式
///
/// 先停止当前模式 → 更新状态 → 启动新模式。
/// 使用 [_switchGen] 代际计数器防止旧连接的异步结果污染当前状态。
Future<void> switchStreamMode(VideoStreamMode mode) async {
if (state.isSwitchingMode) return;
if (state.videoStreamMode == mode) return;
final currentGen = ++_switchGen;
debugPrint(
'[HomeIndexCubit] 切换模式: ${state.videoStreamMode}$mode (gen=$currentGen)');
// 1. 标记切换中
emit(state.copyWith(isSwitchingMode: true));
// 2. 停止当前模式(用 stop 停连接,保留 StreamController 可复用)
if (state.videoStreamMode == VideoStreamMode.p2p) {
await _p2pVideoService.stop();
} else {
await _webrtcService.stop();
}
// 3. 竞态检查:如果在清理期间又触发了一次切换则放弃本次
if (currentGen != _switchGen || isClosed) return;
// 4. 切换到新模式
emit(state.copyWith(
videoStreamMode: mode,
isSwitchingMode: false,
isP2pConnected: false,
videoConnectionState: WebrtcConnectionState.disconnected,
));
// 5. 启动新模式视频连接
if (mode == VideoStreamMode.p2p) {
await _initP2p();
} else {
await _initWebrtc();
}
}
/// 手动重试当前模式的视频连接
Future<void> retryCurrentMode() async {
if (state.videoStreamMode == VideoStreamMode.p2p) {
await _p2pVideoService.stop();
await _initP2p();
} else {
await _webrtcService.stop();
await _initWebrtc();
}
}
Future<void> refreshData() async { Future<void> refreshData() async {
try { try {
final metrics = await _monitoringService.getMetrics(); final metrics = await _monitoringService.getMetrics();
...@@ -116,13 +203,18 @@ class HomeIndexCubit extends Cubit<HomeIndexState> { ...@@ -116,13 +203,18 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
} }
} }
/// 获取渲染器供 UI 层使用 /// 获取 WebRTC 渲染器供 UI 层使用
WebrtcService get webrtcService => _webrtcService; WebrtcService get webrtcService => _webrtcService;
/// 获取 P2P 视频播放控制器供 UI 层使用
P2pVideoService get p2pVideoService => _p2pVideoService;
@override @override
Future<void> close() { Future<void> close() {
_webrtcStateSub?.cancel(); _webrtcStateSub?.cancel();
_p2pStateSub?.cancel();
_webrtcService.dispose(); _webrtcService.dispose();
_p2pVideoService.dispose();
return super.close(); return super.close();
} }
} }
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:laki_icu_app/enums/video_stream_mode_enum.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart'; import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
import 'package:laki_icu_app/services/webrtc_service.dart'; import 'package:laki_icu_app/services/webrtc_service.dart';
...@@ -21,6 +22,15 @@ class HomeIndexState extends Equatable { ...@@ -21,6 +22,15 @@ class HomeIndexState extends Equatable {
/// WebRTC 视频连接状态 /// WebRTC 视频连接状态
final WebrtcConnectionState videoConnectionState; final WebrtcConnectionState videoConnectionState;
/// 当前视频传输模式
final VideoStreamMode videoStreamMode;
/// 是否正在切换传输模式(用于 UI loading 状态)
final bool isSwitchingMode;
/// P2P 模式是否已连接
final bool isP2pConnected;
const HomeIndexState({ const HomeIndexState({
this.status = HomeIndexStatus.initial, this.status = HomeIndexStatus.initial,
this.isLoading = false, this.isLoading = false,
...@@ -30,6 +40,9 @@ class HomeIndexState extends Equatable { ...@@ -30,6 +40,9 @@ class HomeIndexState extends Equatable {
this.alerts = const [], this.alerts = const [],
this.menuItems = const [], this.menuItems = const [],
this.videoConnectionState = WebrtcConnectionState.disconnected, this.videoConnectionState = WebrtcConnectionState.disconnected,
this.videoStreamMode = VideoStreamMode.webrtc,
this.isSwitchingMode = false,
this.isP2pConnected = false,
}); });
HomeIndexState copyWith({ HomeIndexState copyWith({
...@@ -41,6 +54,9 @@ class HomeIndexState extends Equatable { ...@@ -41,6 +54,9 @@ class HomeIndexState extends Equatable {
List<AlertInfoBO>? alerts, List<AlertInfoBO>? alerts,
List<MonitoringMenuItemBO>? menuItems, List<MonitoringMenuItemBO>? menuItems,
WebrtcConnectionState? videoConnectionState, WebrtcConnectionState? videoConnectionState,
VideoStreamMode? videoStreamMode,
bool? isSwitchingMode,
bool? isP2pConnected,
}) { }) {
return HomeIndexState( return HomeIndexState(
status: status ?? this.status, status: status ?? this.status,
...@@ -52,6 +68,9 @@ class HomeIndexState extends Equatable { ...@@ -52,6 +68,9 @@ class HomeIndexState extends Equatable {
menuItems: menuItems ?? this.menuItems, menuItems: menuItems ?? this.menuItems,
videoConnectionState: videoConnectionState:
videoConnectionState ?? this.videoConnectionState, videoConnectionState ?? this.videoConnectionState,
videoStreamMode: videoStreamMode ?? this.videoStreamMode,
isSwitchingMode: isSwitchingMode ?? this.isSwitchingMode,
isP2pConnected: isP2pConnected ?? this.isP2pConnected,
); );
} }
...@@ -65,5 +84,8 @@ class HomeIndexState extends Equatable { ...@@ -65,5 +84,8 @@ class HomeIndexState extends Equatable {
alerts, alerts,
menuItems, menuItems,
videoConnectionState, videoConnectionState,
videoStreamMode,
isSwitchingMode,
isP2pConnected,
]; ];
} }
...@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; ...@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:laki_icu_app/enums/video_stream_mode_enum.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart'; import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
import 'cubit/home_index_cubit.dart'; import 'cubit/home_index_cubit.dart';
...@@ -10,7 +11,7 @@ import 'widgets/alert_info_card.dart'; ...@@ -10,7 +11,7 @@ import 'widgets/alert_info_card.dart';
import 'widgets/metric_card.dart'; import 'widgets/metric_card.dart';
import 'widgets/patient_info_card.dart'; import 'widgets/patient_info_card.dart';
import 'widgets/sidebar_menu_item.dart'; import 'widgets/sidebar_menu_item.dart';
import 'widgets/webrtc_video_widget.dart'; import 'widgets/video_player_widget.dart';
@RoutePage() @RoutePage()
class HomeIndexView extends StatelessWidget { class HomeIndexView extends StatelessWidget {
...@@ -175,10 +176,17 @@ class _HomeIndexContentState extends State<HomeIndexContent> { ...@@ -175,10 +176,17 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
// 监控画面模块(2/3宽度、100%高度) // 监控画面模块(2/3宽度、100%高度)
Expanded( Expanded(
flex: 2, flex: 2,
child: WebrtcVideoWidget( child: VideoPlayerWidget(
renderer: _homeIndexCubit.webrtcService.renderer, streamMode: state.videoStreamMode,
connectionState: state.videoConnectionState, isSwitchingMode: state.isSwitchingMode,
onRetry: () => _homeIndexCubit.retryWebrtc(), webrtcRenderer:
_homeIndexCubit.webrtcService.renderer,
webrtcConnectionState: state.videoConnectionState,
p2pController:
_homeIndexCubit.p2pVideoService.playerController,
isP2pConnected: state.isP2pConnected,
onRetry: () => _homeIndexCubit.retryCurrentMode(),
onToggleMode: _onToggleStreamMode,
), ),
), ),
SizedBox(width: 24.w), SizedBox(width: 24.w),
...@@ -237,6 +245,16 @@ class _HomeIndexContentState extends State<HomeIndexContent> { ...@@ -237,6 +245,16 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
); );
} }
/// 模式切换回调
void _onToggleStreamMode() {
final cubit = _homeIndexCubit;
final currentMode = cubit.state.videoStreamMode;
final nextMode = currentMode == VideoStreamMode.p2p
? VideoStreamMode.webrtc
: VideoStreamMode.p2p;
cubit.switchStreamMode(nextMode);
}
Widget _buildEmptyCard(String text) { Widget _buildEmptyCard(String text) {
return Container( return Container(
padding: EdgeInsets.all(24.w), padding: EdgeInsets.all(24.w),
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:vsdk/app_player.dart';
/// WebRTC 视频全屏播放页面 import '../../../../enums/video_stream_mode_enum.dart';
/// 通用视频全屏播放页面(兼容 WebRTC + P2P)
///
/// 全屏展示摄像头实时画面,点击任意位置退出全屏。
/// ///
/// 全屏展示远程摄像头实时画面,点击任意位置退出全屏。 /// 涉及页面:首页监护舱 - 监控画面
class WebrtcFullscreenPage extends StatelessWidget { class VideoFullscreenPage extends StatelessWidget {
final RTCVideoRenderer renderer; /// 当前视频传输模式
final VideoStreamMode mode;
/// WebRTC 模式下的视频渲染器
final RTCVideoRenderer? webrtcRenderer;
/// P2P 模式下的播放控制器
final AppPlayerController? p2pController;
const WebrtcFullscreenPage({super.key, required this.renderer}); const VideoFullscreenPage({
super.key,
required this.mode,
this.webrtcRenderer,
this.p2pController,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -18,38 +35,52 @@ class WebrtcFullscreenPage extends StatelessWidget { ...@@ -18,38 +35,52 @@ class WebrtcFullscreenPage extends StatelessWidget {
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
// 全屏视频画面 // 根据模式渲染不同的视频画面
Center( _buildVideoContent(),
child: RTCVideoView(
renderer,
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
),
),
// 顶部提示栏 // 顶部提示栏
Positioned( _buildTopBar(),
top: 0, ],
left: 0, ),
right: 0, ),
child: Container( );
padding: const EdgeInsets.fromLTRB(20, 48, 20, 12), }
decoration: const BoxDecoration(
gradient: LinearGradient( /// 按模式渲染视频内容
begin: Alignment.topCenter, Widget _buildVideoContent() {
end: Alignment.bottomCenter, if (mode == VideoStreamMode.p2p && p2pController != null) {
colors: [Colors.black54, Colors.transparent], return AppPlayerView(controller: p2pController!);
), }
), if (mode == VideoStreamMode.webrtc && webrtcRenderer != null) {
child: const Row( return RTCVideoView(
children: [ webrtcRenderer!,
Icon(Icons.fullscreen_exit, color: Colors.white70, size: 20), objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
SizedBox(width: 8), );
Text( }
'点击任意位置退出全屏', return const SizedBox.shrink();
style: TextStyle(color: Colors.white70, fontSize: 14), }
),
], /// 顶部提示栏
), Widget _buildTopBar() {
), return Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.fromLTRB(20, 48, 20, 12),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.black54, Colors.transparent],
),
),
child: const Row(
children: [
Icon(Icons.fullscreen_exit, color: Colors.white70, size: 20),
SizedBox(width: 8),
Text(
'点击任意位置退出全屏',
style: TextStyle(color: Colors.white70, fontSize: 14),
), ),
], ],
), ),
......
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:vsdk/app_player.dart';
import '../../../../enums/video_stream_mode_enum.dart';
import '../../../../services/webrtc_service.dart';
import 'video_fullscreen_page.dart';
import 'video_stream_switch.dart';
/// 通用实时视频播放组件(兼容 WebRTC + P2P 双模式)
///
/// 职责:
/// - WebRTC 模式:通过 [RTCVideoView] 渲染远程摄像头实时画面
/// - P2P 模式:通过 [AppPlayerView] 渲染 Texture 视频画面
/// - 依据当前模式的连接状态显示对应的状态指示器
/// - 右下角提供模式切换按钮(P2P/RTC)和全屏按钮
///
/// 涉及页面:首页监护舱 - 监控画面
class VideoPlayerWidget extends StatelessWidget {
/// 当前视频传输模式
final VideoStreamMode streamMode;
/// 是否正在切换模式
final bool isSwitchingMode;
/// WebRTC 视频渲染器(WebRTC 模式时使用)
final RTCVideoRenderer? webrtcRenderer;
/// WebRTC 连接状态
final WebrtcConnectionState webrtcConnectionState;
/// P2P 视频播放控制器(P2P 模式时使用)
final AppPlayerController? p2pController;
/// P2P 模式是否已连接
final bool isP2pConnected;
/// 连接失败时点击重试回调
final VoidCallback? onRetry;
/// 模式切换回调
final VoidCallback? onToggleMode;
const VideoPlayerWidget({
super.key,
required this.streamMode,
this.isSwitchingMode = false,
this.webrtcRenderer,
required this.webrtcConnectionState,
this.p2pController,
this.isP2pConnected = false,
this.onRetry,
this.onToggleMode,
});
/// 当前模式视频是否正在显示
bool get _isVideoShowing {
if (streamMode == VideoStreamMode.webrtc) {
return webrtcConnectionState == WebrtcConnectionState.connected;
}
return isP2pConnected && p2pController != null;
}
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(20.r),
child: Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xCC63A0FF), Color(0xCC0024C4)],
),
border: Border.all(color: Colors.white24, width: 1),
),
child: Stack(
fit: StackFit.expand,
children: [
// 视频画面层
_buildVideoLayer(),
// 状态指示层
_buildStatusOverlay(),
// 模式切换按钮(右下角倒数第二个位置)
if (onToggleMode != null && !isSwitchingMode)
_buildToggleModeButton(),
// 全屏按钮(右下角最后一个位置)
if (_isVideoShowing) _buildFullscreenButton(context),
],
),
),
);
}
// ==================== 视频画面层 ====================
/// 视频画面
Widget _buildVideoLayer() {
if (streamMode == VideoStreamMode.webrtc) {
if (webrtcConnectionState == WebrtcConnectionState.connected &&
webrtcRenderer != null) {
return RTCVideoView(
webrtcRenderer!,
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
);
}
return _buildPlaceholder();
}
// P2P 模式
if (isP2pConnected && p2pController != null) {
return AppPlayerView(controller: p2pController!);
}
return _buildPlaceholder();
}
// ==================== 状态遮罩层 ====================
/// 状态遮罩层
Widget _buildStatusOverlay() {
if (isSwitchingMode) {
return _buildSwitchingOverlay();
}
if (streamMode == VideoStreamMode.webrtc) {
switch (webrtcConnectionState) {
case WebrtcConnectionState.connecting:
return _buildLoadingOverlay('正在连接摄像头...');
case WebrtcConnectionState.reconnecting:
return _buildLoadingOverlay('正在重新连接...');
case WebrtcConnectionState.failed:
return _buildErrorOverlay();
default:
return const SizedBox.shrink();
}
}
// P2P 模式:未连接显示加载
if (!isP2pConnected) {
return _buildLoadingOverlay('正在 P2P 连接...');
}
return const SizedBox.shrink();
}
/// 模式切换中遮罩
Widget _buildSwitchingOverlay() {
return Container(
color: Colors.black26,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(color: Colors.white70),
SizedBox(height: 16.h),
Text(
'正在切换传输模式...',
style: TextStyle(fontSize: 18.sp, color: Colors.white70),
),
],
),
),
);
}
/// 加载中遮罩
Widget _buildLoadingOverlay(String message) {
return Container(
color: Colors.black26,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(color: Colors.white70),
SizedBox(height: 16.h),
Text(
message,
style: TextStyle(fontSize: 18.sp, color: Colors.white70),
),
],
),
),
);
}
/// 错误遮罩
Widget _buildErrorOverlay() {
return Container(
color: Colors.black38,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline, size: 48.w, color: Colors.white54),
SizedBox(height: 16.h),
Text(
'视频连接失败',
style: TextStyle(
fontSize: 20.sp,
color: Colors.white70,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 24.h),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: const Text('重新连接'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3A87FF),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.r),
),
),
),
],
),
),
);
}
// ==================== 叠加按钮 ====================
/// 全屏按钮(右下角)
Widget _buildFullscreenButton(BuildContext context) {
return Positioned(
right: 12.w,
bottom: 12.h,
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => VideoFullscreenPage(
mode: streamMode,
webrtcRenderer: webrtcRenderer,
p2pController: p2pController,
),
),
);
},
child: Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: Colors.black38,
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(
Icons.fullscreen,
color: Colors.white,
size: 22.w,
),
),
),
);
}
/// 模式切换按钮(全屏按钮左侧 8px 间距)
Widget _buildToggleModeButton() {
return Positioned(
right: 12.w + 40.w + 8.w,
bottom: 12.h,
child: VideoStreamSwitch(
currentMode: streamMode,
onToggle: onToggleMode!,
),
);
}
/// 未连接时的占位
Widget _buildPlaceholder() {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.videocam_off, size: 48.w, color: Colors.white38),
SizedBox(height: 16.h),
Text(
'暂无视频画面',
style: TextStyle(
fontSize: 20.sp,
color: Colors.white54,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../../enums/video_stream_mode_enum.dart';
/// P2P/WebRTC 模式切换控件
///
/// 显示当前传输模式(P2P / RTC),点击切换至另一种模式。
/// 位置:视频容器右下角,全屏按钮左侧。
///
/// 涉及页面:首页监护舱 - 监控画面
class VideoStreamSwitch extends StatelessWidget {
/// 当前视频传输模式
final VideoStreamMode currentMode;
/// 点击切换回调
final VoidCallback onToggle;
const VideoStreamSwitch({
super.key,
required this.currentMode,
required this.onToggle,
});
@override
Widget build(BuildContext context) {
final isP2p = currentMode == VideoStreamMode.p2p;
return GestureDetector(
onTap: onToggle,
child: Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: isP2p
? const Color(0xFF3A87FF).withValues(alpha: 0.6)
: Colors.black38,
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: isP2p ? Colors.white54 : Colors.white24,
width: 1,
),
),
child: Center(
child: Text(
isP2p ? 'P2P' : 'RTC',
style: TextStyle(
fontSize: 10.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import '../../../../services/webrtc_service.dart';
import 'webrtc_fullscreen_page.dart';
/// WebRTC 实时视频播放组件
///
/// 职责:
/// - 通过 [RTCVideoView] 渲染远程摄像头实时画面
/// - 依据 [connectionState] 显示对应的连接状态指示器
/// - 连接失败或未连接时显示重试占位
///
/// 涉及页面:首页监护舱 - 监控画面
class WebrtcVideoWidget extends StatelessWidget {
/// 视频渲染器(来自 [WebrtcService])
final RTCVideoRenderer renderer;
/// 当前连接状态
final WebrtcConnectionState connectionState;
/// 连接失败时点击重试回调
final VoidCallback? onRetry;
const WebrtcVideoWidget({
super.key,
required this.renderer,
required this.connectionState,
this.onRetry,
});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(20.r),
child: Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xCC63A0FF), Color(0xCC0024C4)],
),
border: Border.all(color: Colors.white24, width: 1),
),
child: Stack(
fit: StackFit.expand,
children: [
// 视频画面层
_buildVideoLayer(),
// 状态指示层
_buildStatusOverlay(),
// 全屏按钮(仅连接状态下显示)
if (connectionState == WebrtcConnectionState.connected)
_buildFullscreenButton(context),
],
),
),
);
}
/// 视频画面
Widget _buildVideoLayer() {
if (connectionState == WebrtcConnectionState.connected) {
return RTCVideoView(
renderer,
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
);
}
// 非连接状态显示占位
return _buildPlaceholder();
}
/// 状态遮罩层
Widget _buildStatusOverlay() {
switch (connectionState) {
case WebrtcConnectionState.connecting:
case WebrtcConnectionState.reconnecting:
return _buildLoadingOverlay();
case WebrtcConnectionState.failed:
return _buildErrorOverlay();
default:
return const SizedBox.shrink();
}
}
/// 加载中遮罩
Widget _buildLoadingOverlay() {
final isReconnecting =
connectionState == WebrtcConnectionState.reconnecting;
return Container(
color: Colors.black26,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(color: Colors.white70),
SizedBox(height: 16.h),
Text(
isReconnecting ? '正在重新连接...' : '正在连接摄像头...',
style: TextStyle(
fontSize: 18.sp,
color: Colors.white70,
),
),
],
),
),
);
}
/// 错误遮罩
Widget _buildErrorOverlay() {
return Container(
color: Colors.black38,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline, size: 48.w, color: Colors.white54),
SizedBox(height: 16.h),
Text(
'视频连接失败',
style: TextStyle(
fontSize: 20.sp,
color: Colors.white70,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 24.h),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: const Text('重新连接'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3A87FF),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.r),
),
),
),
],
),
),
);
}
/// 全屏按钮(右下角)
Widget _buildFullscreenButton(BuildContext context) {
return Positioned(
right: 12.w,
bottom: 12.h,
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => WebrtcFullscreenPage(renderer: renderer),
),
);
},
child: Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: Colors.black38,
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(
Icons.fullscreen,
color: Colors.white,
size: 22.w,
),
),
),
);
}
/// 未连接时的占位
Widget _buildPlaceholder() {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.videocam_off,
size: 48.w,
color: Colors.white38,
),
SizedBox(height: 16.h),
Text(
'暂无视频画面',
style: TextStyle(
fontSize: 20.sp,
color: Colors.white54,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
}
...@@ -6,6 +6,7 @@ import FlutterMacOS ...@@ -6,6 +6,7 @@ import FlutterMacOS
import Foundation import Foundation
import device_info_plus import device_info_plus
import flutter_image_compress_macos
import flutter_secure_storage_macos import flutter_secure_storage_macos
import flutter_webrtc import flutter_webrtc
import share_plus import share_plus
...@@ -14,6 +15,7 @@ import url_launcher_macos ...@@ -14,6 +15,7 @@ import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
......
## 0.0.1
低功耗:
AccessKey : 9QDU2DZ3gulyXnze
SecretKey :2LZaRSu4ND5f7wJ5
offline, 离线
deepSleep,深度休眠
sleep,深度休眠
online,在线
poweroff,关机
microPower, //微功耗模式
lowPowerOff, //低电关机
autoWakeup 唤醒设备接口
VELX0000223ENYV 888888
云储存
urls 播放视频地址
APP_BIND_DEV_UNKNOWN_ERR = 0, // 0 未知异常,当出现此错误信息请联系后台
APP_BIND_DEV_SUCCESS = 1, // 1 添加成功
APP_BIND_DEV_REPEAT = 2, // 2 重复添加
APP_BIND_DEV_PARAM_ERR = 3, // 3 参数缺失,调试期间请联系后台
APP_BIND_DEV_TOKEN_ERR = 4, // 4 设备token异常
APP_BIND_DEV_TOKEN_EXPIRE_TIME = 5, // 5 设备token超时
APP_BIND_DEV_AUTHKEY_ERR = 6, // 6 用户authkey异常
APP_BIND_DEV_AUTHKEY_EXPIRE_TIME = 7, // 7 用户authkey过期
CONNECT_NETWORK_FAIL = 1000, // 1000 连接网络失败
DEV_VUID_NULL = 1001, // VUID为空
GET_UID_BY_VUID_FAIL = 1002, // 获取UID服务器失败
AUTH_SERVER_FAIL = 1003, // 授权失败
REGISTER_SERVER_FAIL = 1004, // 注册服务器失败
DEV_UNKNOWN_ERROR = 1005, // 设备未知异常
DEV_UNKNOWN_ERROR = 1007, // 服务器受限
DEV_SIM_UNKNOWN_ERROR = 1008, // 非法卡
DEV_REPORT_FIRST_STAGE_TIMEOUT = 2001, // 设备上报第一步超时
DEV_REPORT_SECOND_STAGE_TIMEOUT = 2002, // 设备上报第二步超时
DEV_REPORT_THIRD_STAGE_TIMEOUT = 2003, // 设备上报第三步超时
更新内容
#1.0.1 (20231215)
更新支持二目,三目
var supportMutilSensorStream = RxInt(0);
/// 1 2 是双, 3是三目
直播 tf 和云存储 同步
2目效果 :img2.png
3目效果 :img3.png
20250926
支持16K 库的更新
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
group 'com.example.vsdk'
version '1.0'
buildscript {
ext.kotlin_version = '1.5.31'
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
rootProject.allprojects {
repositories {
google()
jcenter()
flatDir {
dirs project(':vsdk').file('libs')
}
}
}
apply plugin: 'com.android.library'
android {
namespace 'com.example.vsdk'
compileSdkVersion 31
defaultConfig {
minSdkVersion 24
ndk {
// 设置支持的SO库架构
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
consumerProguardFiles 'consumer-rules.pro'
}
lintOptions {
disable 'InvalidPackage'
}
packagingOptions {
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
pickFirst 'lib/arm64-v8a/libc++_shared.so'
exclude 'META-INF/com.android.tools/proguard/coroutines.pro'
jniLibs {
useLegacyPackaging true
}
//
}
configurations.all {
resolutionStrategy {
force 'androidx.core:core:1.17.0'
}
}
}
dependencies {
implementation(name: 'vp_log-5.0.0', ext: 'aar')
implementation(name: 'app_p2p_api-5.1.0', ext: 'aar')
implementation(name: 'app_player-5.1.0', ext: 'aar')
// implementation(name: 'app_p2p_api-release', ext: 'aar')
// implementation(name: 'app_player-release', ext: 'aar')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
}
-dontwarn android.support.**
-keep class android.support.** { *; }
-dontwarn android.support.v4.**
-keep class android.support.v4.** { *; }
-ignorewarnings
-dontskipnonpubliclibraryclassmembers
\ No newline at end of file
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
rootProject.name = 'vsdk'
include ':vp_log'
include ':vp_p2p'
include ':vp_play'
\ No newline at end of file
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.vsdk">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
</manifest>
package com.example.vsdk;
import android.content.Context;
import androidx.annotation.NonNull;
import com.veepai.app_player.AppPlayerPlugin;
import com.vstarcam.app_p2p_api.AppP2PApiPlugin;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
/*import com.tencent.bugly.crashreport.CrashReport;*/
/** VsdkPlugin */
public class VsdkPlugin implements FlutterPlugin, MethodCallHandler {
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
/* CrashReport.initCrashReport(mContext, "weChatAppId", false);
CrashReport.putUserData(mContext, "BuglyVersion", CrashReport.getBuglyVersion(mContext));*/
AppP2PApiPlugin plugin = new AppP2PApiPlugin();
plugin.onAttachedToEngine(flutterPluginBinding);
AppPlayerPlugin playerPlugin = new AppPlayerPlugin();
playerPlugin.onAttachedToEngine(flutterPluginBinding);
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
}
}
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
/Flutter/Generated.xcconfig
/Flutter/flutter_export_environment.sh
\ No newline at end of file
#import <Flutter/Flutter.h>
@interface AppP2PApiPlugin : NSObject<FlutterPlugin>
@end
#import <Flutter/Flutter.h>
@interface AppPlayerPlugin : NSObject<FlutterPlugin>
@end
#import <Flutter/Flutter.h>
@interface VsdkPlugin : NSObject<FlutterPlugin>
@end
#import "VsdkPlugin.h"
#import "AppP2PApiPlugin.h"
#import "AppPlayerPlugin.h"
@implementation VsdkPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
[AppP2PApiPlugin registerWithRegistrar:registrar];
[AppPlayerPlugin registerWithRegistrar:registrar];
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
}
@end
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint vsdk.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'vsdk'
s.version = '0.0.1'
s.summary = 'A new Flutter project.'
s.description = <<-DESC
A new Flutter project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'emzail@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.public_header_files = 'Classes/VsdkPlugin.h'
s.dependency 'Flutter'
s.platform = :ios, '8.0'
s.vendored_libraries = 'SDK/*.a'
s.libraries = 'z', 'c++','iconv','bz2','m'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
end
import 'dart:ffi';
import 'dart:isolate';
import 'app_player.dart';
class VpDartExecuteArgsT extends Struct {
external Pointer<NativeFunction<Void Function(Pointer<Void>)>> function;
external Pointer<Void> args;
external Pointer<Void> syncLock;
}
typedef ProgressListener = void Function(dynamic data);
typedef GPSListener = void Function(dynamic data);
typedef VideoHeadListener = void Function(dynamic data);
typedef DrawListener = void Function(dynamic data);
class AppDart {
static final AppDart _instance = AppDart._internal();
factory AppDart() => _instance;
int Function(Pointer<NativeFunction<Void Function(Pointer<Void>)>>,
Pointer<VpDartExecuteArgsT>)? _vp_dart_execute;
int Function(Pointer<Void>, int)? _vp_dart_init;
void Function(
Pointer<
NativeFunction<
Void Function(Uint64, Uint32, Uint32, Uint32, Uint32, Uint32,
Uint32, Uint32, Uint64, Uint64)>>)? _app_player_listener;
void Function(
Pointer<
NativeFunction<
Void Function(
Uint64, Uint32, Uint32, Float, Float, Float, Float)>>)?
_app_player_gps_listener;
void Function(
Pointer<
NativeFunction<Void Function(Uint64, Uint32, Uint32, Uint32)>>)?
_app_player_head_listener;
void Function(
Pointer<
NativeFunction<
Void Function(Uint64, Uint32, Uint32, Uint32, Float, Float, Float,
Float)>>)? _app_player_draw_listener;
static void _playerCallback(
int textureId,
int total_duration,
int play_duration,
int cache_progress,
int loading_status,
int velocity,
int focal,
int version,
int timestamp,
int mainFrameCount) {
final listeners = _listenersByTexture[textureId];
if (listeners == null) return;
final data = [
textureId,
total_duration,
play_duration,
cache_progress,
loading_status,
velocity,
focal,
version,
timestamp,
mainFrameCount,
];
for (final listener in listeners) {
listener(data);
}
}
static void _playerGPSCallback(
int textureId,
int fix_status,
int satellites_inview,
double latitude,
double logitude,
double speed,
double coursel) {
final gpsListeners = _gpsListenersByTexture[textureId];
if (gpsListeners == null) return;
final gpsData = [
textureId,
fix_status,
satellites_inview,
latitude,
logitude,
speed,
coursel,
];
for (final listener in gpsListeners) {
listener(gpsData);
}
}
static void _playerDrawCallback(
int textureId,
int width,
int height,
int draw_type,
double percent_x1,
double percent_y1,
double percent_x2,
double percent_y2) {
final drawListeners = _drawListenersByTexture[textureId];
if (drawListeners == null) return;
final drawData = [
textureId,
width,
height,
draw_type,
percent_x1,
percent_y1,
percent_x2,
percent_y2,
];
for (final listener in drawListeners) {
listener(drawData);
}
}
static void _playerVideoHeadCallback(
int textureId, int resolution, int channel, int type) {
final headListeners = _headListenersByTexture[textureId];
if (headListeners == null) return;
final headData = [textureId, resolution, channel, type];
for (final listener in headListeners) {
listener(headData);
}
}
final ReceivePort receivePort = ReceivePort("vp_dart_port");
void _receiveOnData(message) {
if (message is int && _vp_dart_execute != null) {
final args = Pointer<VpDartExecuteArgsT>.fromAddress(message);
_vp_dart_execute!(args.ref.function, args);
}
}
AppDart._internal() {
_vp_dart_execute = AppPlayerController.playerLib
.lookup<
NativeFunction<
Int32 Function(
Pointer<NativeFunction<Void Function(Pointer<Void>)>>,
Pointer<VpDartExecuteArgsT>)>>("vp_dart_execute")
.asFunction();
_vp_dart_init = AppPlayerController.playerLib
.lookup<NativeFunction<Int32 Function(Pointer<Void>, Int64)>>(
"vp_dart_init")
.asFunction();
_app_player_listener = AppPlayerController.playerLib
.lookup<
NativeFunction<
Void Function(
Pointer<
NativeFunction<
Void Function(
Uint64,
Uint32,
Uint32,
Uint32,
Uint32,
Uint32,
Uint32,
Uint32,
Uint64,
Uint64)>>)>>("app_player_listener")
.asFunction();
_app_player_gps_listener = AppPlayerController.playerLib
.lookup<
NativeFunction<
Void Function(
Pointer<
NativeFunction<
Void Function(Uint64, Uint32, Uint32, Float, Float,
Float, Float)>>)>>("app_player_gps_listener")
.asFunction();
_app_player_head_listener = AppPlayerController.playerLib
.lookup<
NativeFunction<
Void Function(
Pointer<
NativeFunction<
Void Function(Uint64, Uint32, Uint32,
Uint32)>>)>>("app_player_head_listener")
.asFunction();
_app_player_draw_listener = AppPlayerController.playerLib
.lookup<
NativeFunction<
Void Function(
Pointer<
NativeFunction<
Void Function(
Uint64,
Uint32,
Uint32,
Uint32,
Float,
Float,
Float,
Float)>>)>>("app_player_draw_listener")
.asFunction();
receivePort.listen(_receiveOnData, onDone: () {
print("vp_dart_port done");
}, onError: (error) {
print("vp_dart_port error:$error");
});
print(
"data:${NativeApi.initializeApiDLData} nativePort:${receivePort.sendPort.nativePort}");
_vp_dart_init!(
NativeApi.initializeApiDLData, receivePort.sendPort.nativePort);
_app_player_listener!(Pointer.fromFunction(_playerCallback));
_app_player_gps_listener!(Pointer.fromFunction(_playerGPSCallback));
_app_player_head_listener!(Pointer.fromFunction(_playerVideoHeadCallback));
_app_player_draw_listener!(Pointer.fromFunction(_playerDrawCallback));
}
static final Map<int, List<ProgressListener>> _listenersByTexture = {};
void addListener(int textureId, ProgressListener listener) {
_listenersByTexture.putIfAbsent(textureId, () => []).add(listener);
}
void rmvListener(int textureId, ProgressListener listener) {
_listenersByTexture[textureId]?.remove(listener);
if (_listenersByTexture[textureId]?.isEmpty ?? false) {
_listenersByTexture.remove(textureId);
}
}
static final Map<int, List<GPSListener>> _gpsListenersByTexture = {};
void addGPSListener(int textureId, GPSListener listener) {
_gpsListenersByTexture.putIfAbsent(textureId, () => []).add(listener);
}
void rmvGPSListener(int textureId, GPSListener listener) {
_gpsListenersByTexture[textureId]?.remove(listener);
if (_gpsListenersByTexture[textureId]?.isEmpty ?? false) {
_gpsListenersByTexture.remove(textureId);
}
}
static final Map<int, List<VideoHeadListener>> _headListenersByTexture = {};
void addVideoHeadListener(int textureId, VideoHeadListener listener) {
_headListenersByTexture.putIfAbsent(textureId, () => []).add(listener);
}
void rmvVideoHeadListener(int textureId, VideoHeadListener listener) {
_headListenersByTexture[textureId]?.remove(listener);
if (_headListenersByTexture[textureId]?.isEmpty ?? false) {
_headListenersByTexture.remove(textureId);
}
}
static final Map<int, List<DrawListener>> _drawListenersByTexture = {};
void addDrawListener(int textureId, DrawListener listener) {
_drawListenersByTexture.putIfAbsent(textureId, () => []).add(listener);
}
void rmvDrawListener(int textureId, DrawListener listener) {
_drawListenersByTexture[textureId]?.remove(listener);
if (_drawListenersByTexture[textureId]?.isEmpty ?? false) {
_drawListenersByTexture.remove(textureId);
}
}
}
/*export 'package:app_devices/support_device/support_mode.dart';
export 'package:app_devices/support_device/support_device_manager.dart';
export 'package:app_devices/device_config.dart';
export 'package:app_devices/device_icons.dart';
export 'package:app_devices/camera_device/camera_device.dart';
export 'package:app_devices/dalie_device/dalie_camera_device.dart';*/
This diff is collapsed.
This diff is collapsed.
import 'dart:async';
import 'dart:io';
import 'package:equatable/equatable.dart';
import 'package:path_provider/path_provider.dart';
abstract class ListenEvent extends Equatable {
@override
bool get stringify => true;
@override
List<Object?> get props => [];
}
class AddListenEvent<T> extends ListenEvent {
final T listener;
AddListenEvent(this.listener);
@override
List<Object?> get props => [listener];
}
class RemoveListenEvent<T> extends ListenEvent {
final T listener;
RemoveListenEvent(this.listener);
@override
List<Object?> get props => [listener];
}
class ActionListenEvent extends ListenEvent {
final Function callback;
final Type type;
ActionListenEvent(this.callback, this.type);
@override
List<Object> get props => [callback, type];
}
///基础设备
abstract class BasisDevice {
late StreamController<ListenEvent> _controller;
BasisDevice(this.id, this.name, this.model) {
_controller = StreamController<ListenEvent>();
_controller.stream.asyncMap(_funcListen).listen((event) {});
}
_funcListen(ListenEvent event) {
if (event is AddListenEvent) {
_listeners.add(event.listener);
} else if (event is RemoveListenEvent) {
_listeners.remove(event.listener);
} else if (event is ActionListenEvent) {
for (var func in _listeners) {
if (func.runtimeType == event.type) {
Function.apply(event.callback, [func]);
}
}
}
return event;
}
List _listeners = [];
void addListener<T>(T listener) {
if (!_controller.isClosed) _controller.add(AddListenEvent<T>(listener));
}
void removeListener<T>(T listener) {
if (!_controller.isClosed) _controller.add(RemoveListenEvent<T>(listener));
}
void notifyListeners<T>(void Function(T listener) callback) {
if (!_controller.isClosed) _controller.add(ActionListenEvent(callback, T));
// _listeners.forEach((func) {
// if (func is T) {
// callback(func);
// }
// });
}
void cleanListener() {
_controller.close();
_listeners.clear();
}
///设备名称
final String name;
///设备ID
final String id;
/// 设备型号
final String model;
Future<Directory> getDeviceDirectory() async {
// return null;
Directory dir = await getApplicationDocumentsDirectory();
dir = Directory("${dir.path}/$id");
if (!dir.existsSync()) {
dir.createSync(recursive: true);
}
return dir;
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import 'dart:typed_data';
import 'package:vsdk/p2p_device/p2p_command.dart';
import 'package:vsdk/p2p_device/p2p_connect.dart';
import '../../app_p2p_api.dart';
class LoginResult {
bool? isSuccess;
int? cmd;
String? result;
// ignore: non_constant_identifier_names
String? current_users;
// ignore: non_constant_identifier_names
String? max_support_users;
LoginResult.form(CommandResult commandResult) {
isSuccess = false;
if (commandResult != null && commandResult.isSuccess == true) {
isSuccess = true;
cmd = commandResult.cmd;
try {
Map data = commandResult.getMap();
result = data["result"];
current_users = data["current_users"];
max_support_users = data["max_support_users"];
} catch (Exception) {}
}
}
@override
String toString() {
return "cmd:$cmd success:$isSuccess {result:$result,current_users:$current_users,max_support_users:$max_support_users}";
}
}
mixin LoginCommand on P2PCommand {
/// 登录指令
Future<LoginResult?> login(String username, String password,
{int timeout = 5}) async {
if (p2pConnectState != ClientConnectState.CONNECT_STATUS_ONLINE) {
return null;
}
int clientPtr = await getClientPtr();
bool ret = await AppP2PApi().clientLogin(clientPtr, username, password);
if (ret == true) {
CommandResult result = await waitCommandResult((int cmd, Uint8List data) {
return cmd == 24736 || cmd == 24577;
}, timeout);
if (result.isSuccess) {
return LoginResult.form(result);
}
}
return null;
}
}
import 'dart:typed_data';
import 'package:vsdk/camera_device/commands/camera_command.dart';
import 'package:vsdk/p2p_device/p2p_command.dart';
class TimeParam {
String? now;
String? tz;
// ignore: non_constant_identifier_names
String? ntp_enable;
// ignore: non_constant_identifier_names
String? ntp_svr;
TimeParam.from(Map data) {
now = data["now"];
tz = data["tz"];
ntp_enable = data["ntp_enable"];
ntp_svr = data["ntp_svr"];
}
}
class NetworkParam {
String? dhcpen;
String? ip;
String? mask;
String? gateway;
String? dns1;
String? dns2;
String? port;
NetworkParam.from(Map data) {
dhcpen = data["dhcpen"];
ip = data["ip"];
mask = data["mask"];
gateway = data["gateway"];
dns1 = data["dns1"];
dns2 = data["dns2"];
port = data["port"];
}
}
class WiFiParam {
// ignore: non_constant_identifier_names
String? wifi_enable;
// ignore: non_constant_identifier_names
String? wifi_ssid;
// ignore: non_constant_identifier_names
String? wifi_mode;
// ignore: non_constant_identifier_names
String? wifi_encrypt;
// ignore: non_constant_identifier_names
String? wifi_authtype;
// ignore: non_constant_identifier_names
String? wifi_channel;
WiFiParam.from(Map data) {
wifi_enable = data["wifi_enable"];
wifi_ssid = data["wifi_ssid"];
wifi_mode = data["wifi_mode"];
wifi_encrypt = data["wifi_encrypt"];
wifi_authtype = data["wifi_authtype"];
wifi_channel = data["wifi_channel"];
}
}
class AlarmMotionParam {
// ignore: non_constant_identifier_names
String? alarm_motion_armed;
// ignore: non_constant_identifier_names
String? alarm_motion_sensitivity;
String? cloudVideoDuration;
AlarmMotionParam.from(Map data) {
alarm_motion_armed = data["alarm_motion_armed"];
alarm_motion_sensitivity = data["alarm_motion_sensitivity"];
if (data.containsKey("CloudVideoDuration")) {
cloudVideoDuration = data["CloudVideoDuration"];
} else {
cloudVideoDuration = "-1";
}
}
}
class ParamResult {
bool? isSuccess;
int? cmd;
String? result;
TimeParam? timeParam;
NetworkParam? networkParam;
WiFiParam? wifiParam;
AlarmMotionParam? alarmMotionParam;
Map? sourceData;
ParamResult.form(CommandResult commandResult) {
isSuccess = false;
if (commandResult != null && commandResult.isSuccess == true) {
isSuccess = true;
cmd = commandResult.cmd;
try {
Map data = commandResult.getMap();
sourceData = data;
result = data["result"];
timeParam = TimeParam.from(data);
networkParam = NetworkParam.from(data);
wifiParam = WiFiParam.from(data);
alarmMotionParam = AlarmMotionParam.from(data);
} catch (Exception) {}
}
}
@override
String toString() {
return "$sourceData";
}
}
mixin ParamsCommand on CameraCommand {
ParamResult? paramResult;
/// 获取设备参数
/// @param [cache] 是否使用缓存,默认为true 使用缓存
Future<ParamResult?> getParams({int timeout = 5, bool cache = true}) async {
if (cache) {
return paramResult;
}
bool ret = await writeCgi("get_params.cgi?", timeout: timeout);
if (ret) {
CommandResult result = await waitCommandResult((int cmd, Uint8List data) {
return cmd == 24578;
}, timeout);
if (result.isSuccess) {
paramResult = ParamResult.form(result);
return paramResult;
}
}
return null;
}
Future<bool> left({int timeout = 5}) async {
print("left ${left} decoder_control.cgi?command=4&onestep=1&");
bool ret = await writeCgi("decoder_control.cgi?command=4&onestep=1&",
timeout: timeout);
print("left ${left} ");
if (ret) {
CommandResult result = await waitCommandResult((int cmd, Uint8List data) {
return cmd == 24601;
}, timeout);
return result.isSuccess;
}
return false;
}
/// 获取截图快照
Future<bool> getSnapshot(String name, {int timeout = 5}) async {
int time = DateTime.now().millisecondsSinceEpoch;
bool ret = await writeCgi("snapshot.cgi?sensor=${name}&", timeout: timeout);
if (ret) {
CommandResult result = await waitCommandResult((int cmd, Uint8List data) {
return cmd == 24597;
}, timeout);
print(
"getSnapshot result.isSuccess ${result.isSuccess} result.data${result.data} ${result?.data?.length}");
if (result.isSuccess && result.data != null && result.data!.length > 0) {
///return await _saveSnapshotFile(result.data, '${time}${"_"}{$name}');
return true;
}
}
return false;
}
}
This diff is collapsed.
import 'dart:typed_data';
import 'package:vsdk/p2p_device/p2p_command.dart';
class RecordResult {
RecordResult();
bool isSuccess = false;
int cmd = 0;
late String result;
// ignore: non_constant_identifier_names
late String enc_size; //跟resolution一致
// ignore: non_constant_identifier_names
late String enc_framerate; //主码流帧率
// ignore: non_constant_identifier_names
late String enc_keyframe; //主码流关键帧
// ignore: non_constant_identifier_names
late String enc_quant; //主码流画质
// ignore: non_constant_identifier_names
late String enc_bitrate; //主码流码率
// ignore: non_constant_identifier_names
late String enc_ratemode; //主码流码流模式
// ignore: non_constant_identifier_names
late String sub_enc_size; //跟resolutionsub一致
// ignore: non_constant_identifier_names
late String sub_enc_framerate; //次码流帧率
// ignore: non_constant_identifier_names
late String sub_enc_keyframe; //次码流关键帧
// ignore: non_constant_identifier_names
late String sub_enc_quant; //次码流画质
// ignore: non_constant_identifier_names
late String sub_enc_bitrate; //次码流码率
// ignore: non_constant_identifier_names
late String sub_enc_ratemode; //次码流码流模式
// ignore: non_constant_identifier_names
late String sub_sub_enc_size; //跟resolutionsubsub一致
// ignore: non_constant_identifier_names
late String sub_sub_enc_framerate; //次次码流帧率
// ignore: non_constant_identifier_names
late String sub_sub_enc_keyframe; //次次码流关键帧
// ignore: non_constant_identifier_names
late String sub_sub_enc_quant; //次次码流画质
// ignore: non_constant_identifier_names
late String sub_sub_enc_bitrate; //次次码流码率
// ignore: non_constant_identifier_names
late String sub_sub_enc_ratemode; //次次码流码流模式
// ignore: non_constant_identifier_names
late String record_audio; //表示录制音频0:不录制音频1:录制音频
// ignore: non_constant_identifier_names
late String record_cover_enable; //表示录像覆盖0->表示不允许覆盖1->表示允许覆盖
// ignore: non_constant_identifier_names
late String record_timer; //表示录像时长
// ignore: non_constant_identifier_names
late String record_size; //保留
// ignore: non_constant_identifier_names
late String record_time_enable;
// ignore: non_constant_identifier_names
late String tf_enable; //TF卡挂载状态
// ignore: non_constant_identifier_names
late String record_chnl; //录像通道选择0:主码流录像1:次码流录像2:次次码流录像
// ignore: non_constant_identifier_names
late String sdtotal; //TF卡总容量
// ignore: non_constant_identifier_names
late String sdfree; //TF卡剩余容量
// ignore: non_constant_identifier_names
String record_sd_status = ""; //TF卡状态
Map sourceData = {};
RecordResult.form(CommandResult? commandResult) {
isSuccess = false;
if (commandResult != null && commandResult.isSuccess == true) {
isSuccess = true;
cmd = commandResult.cmd;
try {
Map data = commandResult.getMap();
sourceData = data;
result = data["result"];
enc_size = data["enc_size"];
enc_framerate = data["enc_framerate"];
enc_keyframe = data["enc_keyframe"];
enc_quant = data["enc_quant"];
enc_bitrate = data["enc_bitrate"];
enc_ratemode = data["enc_ratemode"];
sub_enc_size = data["sub_enc_size"];
sub_enc_framerate = data["sub_enc_framerate"];
sub_enc_keyframe = data["sub_enc_keyframe"];
sub_enc_quant = data["sub_enc_quant"];
sub_enc_bitrate = data["sub_enc_bitrate"];
sub_enc_ratemode = data["sub_enc_ratemode"];
sub_sub_enc_size = data["sub_sub_enc_size"];
sub_sub_enc_framerate = data["sub_sub_enc_framerate"];
sub_sub_enc_keyframe = data["sub_sub_enc_keyframe"];
sub_sub_enc_quant = data["sub_sub_enc_quant"];
record_sd_status = data["record_sd_status"];
sub_sub_enc_bitrate = data["sub_sub_enc_bitrate"];
sub_sub_enc_ratemode = data["sub_sub_enc_ratemode"];
record_audio = data["record_audio"];
record_cover_enable = data["record_cover_enable"];
record_timer = data["record_timer"];
record_size = data["record_size"];
record_time_enable = data["record_time_enable"];
tf_enable = data["tf_enable"];
record_chnl = data["record_chnl"];
sdtotal = data["sdtotal"];
sdfree = data["sdfree"];
record_sd_status = data["record_sd_status"];
} catch (Exception) {}
}
}
@override
String toString() {
return "$sourceData";
}
}
mixin RecordCommand on P2PCommand {
RecordResult recordResult = RecordResult();
Future<bool> getRecordParam({int timeout = 5}) async {
bool ret = await writeCgi("get_record.cgi?", timeout: timeout);
if (ret) {
CommandResult result = await waitCommandResult((int cmd, Uint8List data) {
return cmd == 24582;
}, timeout);
if (result.isSuccess) {
recordResult = RecordResult.form(result);
if (recordResult.result == "0") {
return true;
}
}
}
return false;
}
Future<bool> changedRecordVoice(bool enable, {int timeout = 5}) async {
//record_cover_enable=1&record_size=0&&record_timer=15 这个字段去掉
bool ret = await writeCgi(
"set_recordsch.cgi?record_audio=${enable ? 1 : 0}&",
timeout: timeout);
if (ret) {
CommandResult result = await waitCommandResult((int cmd, Uint8List data) {
return cmd == 24617;
}, timeout);
if (result.isSuccess) {
Map data = result.getMap();
if (data["result"] == "0") {
recordResult.record_audio = enable ? "1" : "0";
return true;
}
}
}
return false;
}
/// 格式化SD 卡
Future<bool> formatSD({int timeout = 5}) async {
bool ret = await writeCgi("set_formatsd.cgi?", timeout: timeout);
if (ret) {
CommandResult result = await waitCommandResult((int cmd, Uint8List data) {
return cmd == 24616;
}, timeout);
if (result.isSuccess) {
if (result.getMap()["result"] == "0") {
recordResult.record_sd_status = "4";
return true;
}
}
}
return false;
}
/// 实时录像
Future<bool> setRecordParams(int enable, {int timeout = 5}) async {
String strCMD;
if (enable == 1) {
strCMD = "set_recordsch.cgi?record_cover=1&"
"record_timer=${recordResult.record_timer}&"
"time_schedule_enable=$enable&"
"schedule_sun_0=-1&"
"schedule_sun_1=-1&"
"schedule_sun_2=-1&"
"schedule_mon_0=-1&"
"schedule_mon_1=-1&"
"schedule_mon_2=-1&"
"schedule_tue_0=-1&"
"schedule_tue_1=-1&"
"schedule_tue_2=-1&"
"schedule_wed_0=-1&"
"schedule_wed_1=-1&"
"schedule_wed_2=-1&"
"schedule_thu_0=-1&"
"schedule_thu_1=-1&"
"schedule_thu_2=-1&"
"schedule_fri_0=-1&"
"schedule_fri_1=-1&"
"schedule_fri_2=-1&"
"schedule_sat_0=-1&"
"schedule_sat_1=-1&"
"schedule_sat_2=-1&"
"record_audio=${recordResult.record_audio}&";
} else {
strCMD = "set_recordsch.cgi?record_cover=1&"
"record_timer=${recordResult.record_timer}&"
"time_schedule_enable=$enable&"
"schedule_sun_0=0&"
"schedule_sun_1=0&"
"schedule_sun_2=0&"
"schedule_mon_0=0&"
"schedule_mon_1=0&"
"schedule_mon_2=0&"
"schedule_tue_0=0&"
"schedule_tue_1=0&"
"schedule_tue_2=0&"
"schedule_wed_0=0&"
"schedule_wed_1=0&"
"schedule_wed_2=0&"
"schedule_thu_0=0&"
"schedule_thu_1=0&"
"schedule_thu_2=0&"
"schedule_fri_0=0&"
"schedule_fri_1=0&"
"schedule_fri_2=0&"
"schedule_sat_0=0&"
"schedule_sat_1=0&"
"schedule_sat_2=0&"
"record_audio=${recordResult.record_audio}&";
}
bool ret = await writeCgi(strCMD, timeout: timeout);
if (ret) {
CommandResult result = await waitCommandResult((int cmd, Uint8List data) {
return cmd == 24617;
}, timeout);
if (result.isSuccess) {
Map data = result.getMap();
if (data["result"] == "0") {
return true;
}
}
}
return false;
}
}
This diff is collapsed.
This diff is collapsed.
import 'camera_command.dart';
mixin VoiceCommand on CameraCommand {
Future<bool> _startSoundCgi(bool bSupportg711a, {int count = 0}) async {
final ret = bSupportg711a
? await writeCgi("audiostream.cgi?streamid=7&")
: await writeCgi("audiostream.cgi?streamid=1&adpcm_ver=1&");
if (!ret) return false;
final value = await waitCommandResult((cmd, data) {
return cmd == 24625;
}, 3);
if (!value.isSuccess && count < 3) {
return _startSoundCgi(bSupportg711a, count: count + 1);
}
return value.isSuccess;
}
Future<bool> _stopSoundCgi({int count = 0}) async {
final ret = await writeCgi("audiostream.cgi?streamid=16&");
if (!ret) return false;
final value = await waitCommandResult((cmd, data) {
return cmd == 24625;
}, 3);
if (!value.isSuccess && count < 3) {
return _stopSoundCgi(count: count + 1);
}
return value.isSuccess;
}
bool supportG711() {
return statusResult!.support_audio_g711a != null &&
statusResult!.support_audio_g711a != "0";
}
bool supportbothWay() {
return statusResult!.EchoCancellationVer != null &&
statusResult!.EchoCancellationVer != "0";
}
Future<bool> startSoundStream() async {
return await _startSoundCgi(supportG711());
}
Future<bool> stopSoundStream() async {
return await _stopSoundCgi();
}
@override
Future<void> deviceDestroy() async {
super.deviceDestroy();
}
}
import 'dart:async';
import 'dart:convert';
import 'package:vsdk/p2p_device/p2p_command.dart';
import 'package:vsdk/p2p_device/p2p_device.dart';
import '../../device_wakeup_server.dart';
import 'camera_command.dart';
typedef WakeupStateChanged = void Function(
P2PBasisDevice device, DeviceWakeupState? wakeupState);
mixin WakeupCommand on CameraCommand {
DeviceWakeupState? _wakeupState;
DeviceWakeupState? get wakeupState => _wakeupState;
set wakeupState(DeviceWakeupState? value) {
if (value != _wakeupState) {
_wakeupState = value;
notifyListeners<WakeupStateChanged>((WakeupStateChanged func) {
func(this, _wakeupState);
});
}
}
void _wakeupListener(String did, DeviceWakeupState state) {
if (state != _wakeupState) {
wakeupState = state;
}
}
Timer? keepAliveTimer;
Future<bool> _keepAliveCgi(int time) async {
bool ret = await writeCgi(
"trans_cmd_string.cgi?cmd=2131&command=1&DevActiveTime=$time&",
timeout: (time / 2).floor());
if (ret == true) {
CommandResult result = await waitCommandResult((cmd, data) {
return cmd == 24785 && utf8.decode(data).contains("cmd=2131;");
}, (time / 2).floor());
Map map = result.getMap();
if (map != null && map.containsKey("DevActiveTime")) {
return true;
} else {
return false;
}
}
return ret;
}
void keepAlive({int time = 10}) async {
keepAliveTimer?.cancel();
keepAliveTimer =
Timer.periodic(Duration(seconds: (time / 2).floor()), (timer) {
_keepAliveCgi(time);
});
}
void cancelKeepAlive() {
keepAliveTimer?.cancel();
}
void addListener<T>(T listener) async {
if (listener != null &&
listener is WakeupStateChanged &&
_addListener == false) {
DeviceWakeUpServer().removeListener(id, _wakeupListener);
DeviceWakeUpServer().addListener(id, _wakeupListener);
}
super.addListener(listener);
}
bool _addListener = false;
void requestWakeup() {
DeviceWakeUpServer().wakeup(id);
Timer(Duration(seconds: 3), () {
DeviceWakeUpServer().wakeup(id);
});
}
void requestWakeupOnce() {
DeviceWakeUpServer().wakeup(id);
}
void requestWakeupStatus() {
var bl = DeviceWakeUpServer().checkNotNode(id);
if (bl) return;
DeviceWakeUpServer().getStatus(id);
}
@override
Future<void> deviceDestroy() async {
cancelKeepAlive();
DeviceWakeUpServer().removeListener(id, _wakeupListener);
DeviceWakeUpServer().removeAutoWakeup(id);
super.deviceDestroy();
}
}
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'app_player.dart';
enum VideoState {
/// 默认状态
none,
/// 准备中
starting,
/// 正在播放
playing,
/// 暂停
pause,
/// 停止
stop,
/// 出现错误
error,
}
enum AudioState {
/// 默认状态
none,
/// 正在播放
play,
/// 停止
stop,
/// 出现错误
error,
}
class CameraPlayer {
int clientPtr = 0;
/// 图形绘制纹理
Rx<int> texture = Rx<int>(0);
/// 播放控制器
late AppPlayerController controller;
/// 视频状态
Rx<VideoState> videoState = Rx<VideoState>(VideoState.none);
/// 音频状态
Rx<AudioState> audioState = Rx<AudioState>(AudioState.none);
/// 视频时长
/// 秒为单位
var duration = 0.obs;
/// 播放进度
/// 秒为单位
var progress = 0.obs;
}
This diff is collapsed.
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:fast_gbk/fast_gbk.dart';
import '../app_p2p_api.dart';
import 'p2p_connect.dart';
import 'p2p_device.dart';
typedef CommandFilterFunc = bool Function(int cmd, Uint8List data);
typedef CommandCallbackFunc = void Function(int cmd, CommandResult result);
class CommandCondition {
final CommandFilterFunc filter;
Completer<CommandResult> completer;
CommandCondition(this.filter, this.completer);
}
class UTF8Check {
int _getUtf8charByteNum(int ch) {
int byteNum = 0;
if (ch >= 0xFC && ch < 0xFE) {
byteNum = 6;
} else if (ch >= 0xF8) {
byteNum = 5;
} else if (ch >= 0xF0) {
byteNum = 4;
} else if (ch >= 0xE0) {
byteNum = 3;
} else if (ch >= 0xC0) {
byteNum = 2;
} else if (0 == (ch & 0x80)) {
byteNum = 1;
}
return byteNum;
}
bool isUtf8Format(List<int> buffer) {
if (buffer.isEmpty) return false;
int byteNum = 0;
int ch = 0;
int off = 0;
do {
ch = buffer[off];
if (byteNum == 0) {
if (0 == (byteNum = _getUtf8charByteNum(ch))) return false;
} else {
if ((ch & 0xC0) != 0x80) return false;
}
byteNum--;
off++;
} while (off < buffer.length);
if (byteNum > 0) return false;
return true;
}
}
class CommandResult {
final bool isSuccess;
final int cmd;
final Uint8List? data;
static UTF8Check utf8check = UTF8Check();
CommandResult(this.isSuccess, this.cmd, this.data);
String getString() {
if (data == null) {
return "";
}
try {
return utf8.decode(data!);
} catch (e) {
return gbk.decode(data!);
}
}
String getString2(List<int> buffer) {
if (utf8check.isUtf8Format(buffer)) {
try {
return utf8.decode(buffer, allowMalformed: true);
} catch (e) {
return String.fromCharCodes(buffer);
}
} else {
try {
return gbk.decode(buffer, allowMalformed: true);
} catch (e) {
return String.fromCharCodes(buffer);
}
}
}
List<int> _replaceAll(Map<int, int> replace) {
var newData = data?.toList() ?? [];
List<int> removeList = [];
for (int i = 0; i < newData.length; i++) {
var item = newData[i];
if (replace.containsKey(item)) {
var value = replace[item];
if (value == 0) {
removeList.add(i);
} else {
newData[i] = value!;
}
}
}
var removeLen = 0;
for (var element in removeList) {
newData.removeAt(element - removeLen);
removeLen += 1;
}
return newData;
}
List<List<int>> _split(List<int> data, int sp) {
List<List<int>> splitBuffer = [];
int index = 0;
int start = 0;
while (index >= 0) {
index = data.indexOf(sp, start);
if (index > 0) {
var spBuffer = data.getRange(start, index).toList();
splitBuffer.add(spBuffer);
start = index + 1;
}
}
return splitBuffer;
}
Map getMap() {
if (data == null || data!.isEmpty) return {};
var newData = _replaceAll({'\r'.codeUnitAt(0): 0, '\n'.codeUnitAt(0): 0});
var lines = _split(newData, ';'.codeUnitAt(0));
Map maps = {};
for (var buffer in lines) {
var line = getString2(buffer);
if (line.contains("=")) {
List<String> keyValue = line.split("=");
if (keyValue.length >= 2) {
if (keyValue[0].trim().replaceAll("var ", "") == 'AiCfg') {
var value = keyValue[1].trim().replaceFirst('"', '');
int lastIndex = value.lastIndexOf('"');
value = value.substring(0, lastIndex);
maps.putIfAbsent(
keyValue[0].trim().replaceAll("var ", ""), () => value.trim());
} else {
maps.putIfAbsent(keyValue[0].trim().replaceAll("var ", ""),
() => keyValue[1].trim().replaceAll('"', ""));
}
}
}
}
return maps;
}
}
mixin P2PCommand on P2PBasisDevice, P2PConnect {
final List<CommandCondition> _commandFilters = [];
final Map<int, CommandCallbackFunc> _callbackMaps = {};
void _onCommandListener(int cmd, Uint8List data) {
List<CommandCondition> _remove = [];
for (var item in _commandFilters) {
if (item.filter(cmd, data)) {
_remove.add(item);
if (!item.completer.isCompleted) {
item.completer.complete(CommandResult(true, cmd, data));
}
break;
} else {}
}
if (_remove.isEmpty) {
if (_callbackMaps.containsKey(cmd)) {
_callbackMaps[cmd]!(cmd, CommandResult(true, cmd, data));
}
}
for (var item in _remove) {
_commandFilters.remove(item);
}
}
void addCallback(int cmd, CommandCallbackFunc func) {
_callbackMaps[cmd] = func;
}
void removeCallback(int cmd) {
_callbackMaps.remove(cmd);
}
///数据发送,用于发送原始数据
Future<bool> write(
ClientChannelType channel, Uint8List buffer, int timeout) async {
if (p2pConnectState != ClientConnectState.CONNECT_STATUS_ONLINE) {
return false;
}
int clientPtr = await getClientPtr();
int ret =
await AppP2PApi().clientWrite(clientPtr, channel, buffer, timeout);
return ret == buffer.length;
}
/// 发送CGI指令
/// 底层封装CGI数据格式
Future<bool> writeCgi(String cgi,
{int timeout = 5, bool needLogin = true}) async {
if (p2pConnectState != ClientConnectState.CONNECT_STATUS_ONLINE) {
return false;
}
int clientPtr = await getClientPtr();
bool bl =
await AppP2PApi().clientWriteCgi(clientPtr, cgi, timeout: timeout);
if (bl != true) {
bl = await AppP2PApi().clientWriteCgi(clientPtr, cgi, timeout: timeout);
}
return bl;
}
///等待指令返回
///@param [filterFunc] 指令筛选条件
///@param [timeout] 等待时间 秒为单位
///
///@return [CommandResult]
Future<CommandResult> waitCommandResult(
CommandFilterFunc filterFunc, int timeout) async {
Completer<CommandResult> completer = Completer();
CommandCondition condition = CommandCondition(filterFunc, completer);
Future.delayed(Duration(seconds: timeout), () {
_commandFilters.remove(condition);
if (!completer.isCompleted) {
completer.complete(CommandResult(false, 0, null));
}
});
_commandFilters.add(condition);
return completer.future;
}
/// 设置指令监听
Future<void> setCommandListener() async {
int clientPtr = await getClientPtr();
AppP2PApi().setCommandListener(clientPtr, _onCommandListener);
}
void _clearPendingCommands() {
for (final item in List<CommandCondition>.of(_commandFilters)) {
if (!item.completer.isCompleted) {
item.completer.complete(CommandResult(false, 0, null));
}
}
_commandFilters.clear();
_callbackMaps.clear();
}
/// 移除指令监听
Future<void> removeCommandListener() async {
_clearPendingCommands();
final clientPtr = await getClientPtr();
if (clientPtr != 0) {
AppP2PApi().removeCommandListener(clientPtr);
}
}
}
import 'dart:async';
import '../app_p2p_api.dart';
import 'p2p_device.dart';
typedef void P2PConnectStateChanged(
P2PBasisDevice sender, ClientConnectState state);
/// P2P连接功能
mixin P2PConnect on P2PBasisDevice {
/// 连接状态
ClientConnectState _p2pConnectState = ClientConnectState.CONNECT_STATUS_MAX;
ClientConnectState get p2pConnectState => _p2pConnectState;
set p2pConnectState(ClientConnectState value) {
if (value != _p2pConnectState) {
_p2pConnectState = value;
notifyListeners<P2PConnectStateChanged>((P2PConnectStateChanged func) {
func(this, _p2pConnectState);
});
}
}
/// 连接状态回调
/// 但读取数据失败时,触发回调[ClientConnectState.CONNECT_STATUS_DISCONNECT]
void _onConnectListener(ClientConnectState state) async {
p2pConnectState = state;
if (state == ClientConnectState.CONNECT_STATUS_DISCONNECT) {
int clientPtr = await getClientPtr();
await AppP2PApi().clientDisconnect(clientPtr);
if (_disconnectCompleter != null) {
_disconnectCompleter!.complete(true);
_disconnectCompleter = null;
}
}
}
Completer<bool>? _connectCompleter;
/// 等待设备连接中断
Future<bool> _waitConnected() async {
if (_connectCompleter != null) {
bool bl = await _connectCompleter!.future
.timeout(Duration(seconds: 10), onTimeout: () => false);
_connectCompleter = null;
return bl;
}
return false;
}
int _connectByTimeTimeout(int clientPtr) {
return -3;
}
Future<int> connectByTime(
{required int clientPtr,
required bool lanScan,
required int reConnectCount,
required int connectType}) async {
String? serverParam = await getServiceParam();
int time = await AppP2PApi()
.clientCheckTimeout(clientPtr, lanScan, serverParam,
connectType: connectType)
.timeout(Duration(seconds: 15),
onTimeout: () => _connectByTimeTimeout(clientPtr));
return time;
}
ClientConnectState _beginConnectTimeout(int clientPtr) {
return ClientConnectState.CONNECT_STATUS_CONNECT_TIMEOUT;
}
Future<ClientConnectState> _beginConnect(int clientPtr, String serverParam,
bool lanScan, int reConnectCount, int connectType) async {
_connectCompleter = Completer();
p2pConnectState = ClientConnectState.CONNECT_STATUS_CONNECTING;
_isDisconnect = false;
ClientConnectState state = ClientConnectState.CONNECT_STATUS_DISCONNECT;
do {
state = await AppP2PApi()
.clientConnect(clientPtr, lanScan, serverParam,
connectType: connectType)
.onError((error, stackTrace) =>
ClientConnectState.CONNECT_STATUS_DISCONNECT)
.timeout(Duration(seconds: 20),
onTimeout: () => _beginConnectTimeout(clientPtr));
reConnectCount -= 1;
} while (state == ClientConnectState.CONNECT_STATUS_CONNECT_TIMEOUT &&
reConnectCount > 0 &&
!_isDisconnect);
if (_isDisconnect) {
state = ClientConnectState.CONNECT_STATUS_DISCONNECT;
}
if (_connectCompleter!.isCompleted == false)
_connectCompleter!.complete(true);
return state;
}
/// 进行连接
/// @param [lanScan] 是否进行局域网搜索
/// @param [recount] 重连次数
///
/// @return true 调用成功
/// false 调用失败
Future<ClientConnectState> p2pConnect(
{bool lanScan = true,
int reConnectCount = 2,
required int connectType}) async {
if (p2pConnectState == ClientConnectState.CONNECT_STATUS_CONNECTING) {
return p2pConnectState;
}
p2pConnectState = ClientConnectState.CONNECT_STATUS_CONNECTING;
int clientPtr = await getClientPtr();
if (clientPtr == null || clientPtr == 0) {
p2pConnectState = ClientConnectState.CONNECT_STATUS_INVALID_CLIENT;
return p2pConnectState;
}
String? serverParam = await getServiceParam();
if (serverParam == null) {
p2pConnectState = ClientConnectState.CONNECT_STATUS_INVALID_CLIENT;
return p2pConnectState;
}
ClientConnectState state = await _beginConnect(
clientPtr, serverParam, lanScan, reConnectCount, connectType);
if (state == ClientConnectState.CONNECT_STATUS_ONLINE) {
_disconnectCompleter = Completer();
AppP2PApi().setConnectListener(clientPtr, _onConnectListener);
} else {}
p2pConnectState = state;
return p2pConnectState;
}
bool _isDisconnect = false;
Completer<bool>? _disconnectCompleter;
/// 等待连接断开
/// 如果设备已经连接处于ONLINE 状态,需要等待设备回调DISCONNECT
/// 才能确定设备已经完全断开了
Future<bool> _waitDisconnected() async {
if (_disconnectCompleter != null) {
bool bl = await _disconnectCompleter!.future
.timeout(Duration(seconds: 10), onTimeout: () async {
return false;
});
_disconnectCompleter = null;
return bl;
}
return true;
}
/// 断开连接
/// 如果设备正处于连接状态,需要等待设备退出连接后再调用
/// `bool ret = await AppP2PApi().clientDisconnect(clientPtr);`
/// 调用`clientDisconnect`如果返回true,则说明设备曾经连接成功,
/// 需要等待设备回调disconnect才能确保设备已经完全断开连接
/// 需要调用`await _waitDisconnected()`来确认设备已经完全断开
///
/// @return true 连接已断开
/// false 断开失败
///
Future<bool> disconnect() async {
int clientPtr = await getClientPtr();
AppP2PApi().clientConnectBreak(clientPtr);
bool ret = await _waitConnected();
ret = await AppP2PApi().clientDisconnect(clientPtr);
if (ret) {
ret = await _waitDisconnected();
AppP2PApi().removeConnectListener(clientPtr);
}
return ret;
}
}
This diff is collapsed.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:vsdk/vp_log_code.dart';
enum VPLogLevel {
INFO,
DEBUG,
WARN,
ERROR,
FILE,
MAX,
}
class VPLogStackTraceInfo {
final StackTrace trace;
String fileName = "Unknown";
String lineNumber = "0";
String columnNumber = "0";
String funName = "Unknown";
int stackTraceNum = 0;
VPLogStackTraceInfo(this.trace, int traceNum) {
stackTraceNum = traceNum;
}
void parseTrace() {
if (trace == null) return;
var stackString = trace.toString();
var strList = stackString.split("\n");
if (stackTraceNum >= strList.length) return;
var traceString = strList[stackTraceNum];
var funList = traceString.split("(");
funName = funList.first;
funName = funName.replaceAll("#$stackTraceNum", "").trim();
funList = funName.split(".");
funName = funList.last;
var fileInfo = traceString.split("/").last;
var listOfInfo = fileInfo.split(":");
if (listOfInfo.length < 1) return;
fileName = "${listOfInfo[0]}";
if (listOfInfo.length < 2) return;
lineNumber = listOfInfo[1];
if (listOfInfo.length < 3) return;
columnNumber = listOfInfo[2].replaceFirst(")", "");
}
}
typedef LogFileFullListener = Future<void> Function(File file);
class VPLog {
static const EventChannel _logEvent = const EventChannel('vp_log/event');
static const MethodChannel _logMethod = const MethodChannel('vp_log/method');
static VPLogLevel _logLevel = VPLogLevel.INFO;
static int _maxFileSize = 15;
static int _logFileSize = 0;
static File? _logFile;
static IOSink? _logFileIO;
static LogFileFullListener? _logListener;
static String _levelString(VPLogLevel level) {
switch (level) {
case VPLogLevel.FILE:
return " FILE";
case VPLogLevel.ERROR:
return "ERROR";
case VPLogLevel.WARN:
return " WARN";
case VPLogLevel.DEBUG:
return "DEBUG";
case VPLogLevel.INFO:
return " INFO";
case VPLogLevel.MAX:
break;
}
return " NONE";
}
static void _writeFile(String log) async {
if (_logLevel == VPLogLevel.MAX) return;
if (_logFile == null || _logFileIO == null) return;
try {
List<int> listLogs = utf8.encode(log);
var encodeStr = logEncode(Uint8List.fromList(listLogs));
_logFileIO?.writeln(encodeStr);
_logFileSize += encodeStr.length;
if (_logFileSize > _maxFileSize) {
await _logFileIO?.flush();
await _logFileIO?.close();
if (_logListener != null) {
await _logListener!(_logFile!);
}
_logFile?.deleteSync();
_logFile?.createSync();
_logFileIO = _logFile?.openWrite(mode: FileMode.write);
_logFileSize = 0;
}
} catch (ex) {}
}
static String _fourDigits(int n) {
int absN = n.abs();
if (absN >= 1000) return "$n";
if (absN >= 100) return "0$absN";
if (absN >= 10) return "00$absN";
return "000$absN";
}
static String _sixDigits(int n) {
int absN = n.abs();
if (absN >= 100000) return "$n";
if (absN >= 10000) return "0$absN";
if (absN >= 1000) return "00$absN";
if (absN >= 100) return "000$absN";
if (absN >= 10) return "000$absN";
return "00000$absN";
}
static String _twoDigits(int n) {
if (n >= 10) return "$n";
return "0$n";
}
static String _dateString(DateTime date) {
String y = _fourDigits(date.year);
String m = _twoDigits(date.month);
String d = _twoDigits(date.day);
String h = _twoDigits(date.hour);
String min = _twoDigits(date.minute);
String sec = _twoDigits(date.second);
String ms = _sixDigits(date.millisecond * 1000 + date.microsecond);
return "$y-$m-$d $h:$min:$sec.$ms";
}
static void _log(Object msg,
{String tag = "APP",
bool writeFile = false,
VPLogLevel level = VPLogLevel.INFO,
VPLogStackTraceInfo? stack}) {
if (_logLevel == VPLogLevel.MAX) return;
if (msg == null) msg = "null";
if (level.index < _logLevel.index && writeFile == false) return;
if (stack == null) stack = VPLogStackTraceInfo(StackTrace.current, 0);
stack.parseTrace();
var date = DateTime.now();
String data =
'${_dateString(date)} [${_levelString(level)}] [${stack?.fileName}:${stack?.lineNumber}] ${stack?.funName} :[$tag] $msg';
if (level.index >= _logLevel.index) print(data);
if (writeFile) _writeFile(data);
}
static void _nativeLog(dynamic data) {
_writeFile(data);
}
static void printLog(String msg) {
_log(msg,
level: VPLogLevel.INFO,
stack: VPLogStackTraceInfo(StackTrace.current, 5));
}
static void printError(Object error, StackTrace stackTrace) {
List<String> str = [];
if (error != null) str.addAll(error.toString().split("\n"));
if (stackTrace != null) str.addAll(stackTrace.toString().split("\n"));
for (var o in str) {
_log(o,
tag: "ERR",
writeFile: true,
level: VPLogLevel.ERROR,
stack: VPLogStackTraceInfo(StackTrace.current, 1));
}
}
/// 初始化日志记录器
static init(
{VPLogLevel logLevel = VPLogLevel.INFO,
int logFileSize = 30,
String? logFilePath}) {
_maxFileSize = logFileSize * 1024 * 1024;
_logLevel = logLevel;
if (_logFileIO == null) {
try {
if (logFilePath != null)
_logFile = File(logFilePath);
else
_logFile = File("${Directory.systemTemp.path}/logs.log");
if (!_logFile!.existsSync()) {
_logFile?.createSync();
}
_logFileSize = _logFile!.lengthSync();
_logFileIO = _logFile?.openWrite(mode: FileMode.append);
} catch (ex) {}
}
_logEvent.receiveBroadcastStream().listen(_nativeLog);
_logMethod.invokeMethod("vp_log_set_level", [_logLevel.index]);
}
static void warn(Object object,
{String tag = "APP", bool writeFile = false}) {
_log(object,
level: VPLogLevel.WARN,
writeFile: writeFile,
tag: tag,
stack: VPLogStackTraceInfo(StackTrace.current, 1));
}
static void error(Object object,
{String tag = "APP", bool writeFile = false}) {
_log(object,
level: VPLogLevel.ERROR,
writeFile: writeFile,
tag: tag,
stack: VPLogStackTraceInfo(StackTrace.current, 1));
}
static void file(Object object, {String tag = "APP"}) {
_log(object,
level: VPLogLevel.FILE,
writeFile: true,
tag: tag,
stack: VPLogStackTraceInfo(StackTrace.current, 1));
}
static File? getLogFile() {
return _logFile;
}
static void setLogFileFullListener(LogFileFullListener listener) {
_logListener = listener;
}
}
import 'dart:typed_data';
const String _log_chars =
"ZWXYMPTERQUONCHBJILGFSDVKAhiopxyzamtbeqnwrsjklucdvfg089/174+5236";
bool _isBase64(String char) {
return _log_chars.contains(char);
}
String logEncode(Uint8List input) {
if (input == null || input.isEmpty) {
return "";
}
int length = input.length;
int i = 0, j = 0, s = 0;
var charArray3 = []..length = 3;
var charArray4 = []..length = 4;
var b64str = StringBuffer();
while (length-- > 0) {
charArray3[i] = input[s];
s += 1;
i += 1;
if (i == 3) {
charArray4[0] = (charArray3[0] & 0xfc) >> 2;
charArray4[1] =
((charArray3[0] & 0x03) << 4) + ((charArray3[1] & 0xf0) >> 4);
charArray4[2] =
((charArray3[1] & 0x0f) << 2) + ((charArray3[2] & 0xc0) >> 6);
charArray4[3] = charArray3[2] & 0x3f;
for (i = 0; i < 4; i++)
b64str.writeCharCode(_log_chars.codeUnitAt(charArray4[i]));
i = 0;
}
}
if (i != 0) {
for (j = i; j < 3; j++) charArray3[j] = 0;
charArray4[0] = (charArray3[0] & 0xfc) >> 2;
charArray4[1] =
((charArray3[0] & 0x03) << 4) + ((charArray3[1] & 0xf0) >> 4);
charArray4[2] =
((charArray3[1] & 0x0f) << 2) + ((charArray3[2] & 0xc0) >> 6);
charArray4[3] = charArray3[2] & 0x3f;
for (j = 0; j < i + 1; j++)
b64str.writeCharCode(_log_chars.codeUnitAt(charArray4[j]));
while (i++ < 3) b64str.writeCharCode('='.codeUnitAt(0));
}
return b64str.toString();
}
List<int>? logDecode(String input) {
if (input == null || input.isEmpty) {
return null;
}
int length = input.length;
int i = 0;
int j = 0;
int idx = 0;
var charArray4 = []..length = 4;
var charArray3 = []..length = 3;
var output = <int>[];
while (length-- > 0 && input[idx] != '=') {
if (!_isBase64(input[idx])) {
idx++;
continue;
}
charArray4[i++] = input[idx++];
if (i == 4) {
for (i = 0; i < 4; i++) {
charArray4[i] = _log_chars.indexOf(charArray4[i]);
}
charArray3[0] = (charArray4[0] << 2) + ((charArray4[1] & 0x30) >> 4);
charArray3[1] =
((charArray4[1] & 0xf) << 4) + ((charArray4[2] & 0x3c) >> 2);
charArray3[2] = ((charArray4[2] & 0x3) << 6) + charArray4[3];
for (i = 0; (i < 3); i++) output.add(charArray3[i]);
i = 0;
}
}
if (i != 0) {
for (j = i; j < 4; j++) charArray4[j] = '\0';
for (j = 0; j < 4; j++) {
charArray4[j] = _log_chars.indexOf(charArray4[j]);
}
charArray3[0] = (charArray4[0] << 2) + ((charArray4[1] & 0x30) >> 4);
charArray3[1] =
((charArray4[1] & 0xf) << 4) + ((charArray4[2] & 0x3c) >> 2);
charArray3[2] = ((charArray4[2] & 0x3) << 6) + charArray4[3];
for (j = 0; (j < i - 1); j++) output.add(charArray3[j]);
}
return output;
}
name: vsdk
description: A new Flutter project.
version: 1.0.1
author:
homepage:
environment:
sdk: ">=2.17.0 <4.0.0"
flutter: ">=1.20.0"
dependencies:
flutter:
sdk: flutter
fast_gbk: ^1.0.0
equatable: ^2.0.0
dio: ^5.4.0
crypto: ^3.0.0
get: ^4.6.0
flutter_easyloading: ^3.0.5
path_provider: ^2.0.0
flutter_image_compress: ^2.4.0
shared_preferences: ^2.0.0
# 日志
logger: ^2.5.0
dev_dependencies:
flutter_test:
sdk: flutter
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' and Android 'package' identifiers should not ordinarily
# be modified. They are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.example.vsdk
pluginClass: VsdkPlugin
ios:
pluginClass: VsdkPlugin
# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.
# To add custom fonts to your plugin package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages
...@@ -54,6 +54,8 @@ dependencies: ...@@ -54,6 +54,8 @@ dependencies:
path_provider: ^2.1.2 path_provider: ^2.1.2
flutter_webrtc: flutter_webrtc:
path: ./plugins/flutter_webrtc path: ./plugins/flutter_webrtc
vsdk:
path: ./plugins/vsdk
# flutter_usbcamera: ^0.0.1 # flutter_usbcamera: ^0.0.1
# fl_chart: ^0.71.0 # fl_chart: ^0.71.0
# fl_chart: ^1.1.0 # fl_chart: ^1.1.0
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment