基于Flutter Bloc模式,利用设备返回数据实现WebRTC视频流获取
Flutter基于Bloc架构获取智能设备视频流方案
从你提供的设备返回数据来看,这是一套WebRTC协议所需的配置信息(STUN/TURN服务器、认证标识),结合Bloc架构,可按以下步骤实现视频流获取:
1. 核心依赖选择
使用flutter_webrtc处理WebRTC连接,搭配flutter_bloc实现状态管理,在pubspec.yaml中添加:
dependencies: flutter_webrtc: ^0.9.46 flutter_bloc: ^8.1.3
2. Bloc层设计与实现
2.1 定义事件
abstract class VideoStreamEvent {} class FetchDeviceVideoStream extends VideoStreamEvent { final Map<String, dynamic> deviceConfig; FetchDeviceVideoStream(this.deviceConfig); } class DisposeVideoStream extends VideoStreamEvent {}
2.2 定义状态
abstract class VideoStreamState {} class VideoStreamInitial extends VideoStreamState {} class VideoStreamLoading extends VideoStreamState {} class VideoStreamLoaded extends VideoStreamState { final MediaStream stream; final RTCPeerConnection peerConnection; VideoStreamLoaded(this.stream, this.peerConnection); } class VideoStreamError extends VideoStreamState { final String message; VideoStreamError(this.message); }
2.3 Bloc核心逻辑
class VideoStreamBloc extends Bloc<VideoStreamEvent, VideoStreamState> { RTCPeerConnection? _peerConnection; MediaStream? _remoteStream; VideoStreamBloc() : super(VideoStreamInitial()) { on<FetchDeviceVideoStream>(_onFetchDeviceVideoStream); on<DisposeVideoStream>(_onDisposeVideoStream); } Future<void> _onFetchDeviceVideoStream( FetchDeviceVideoStream event, Emitter<VideoStreamState> emit, ) async { emit(VideoStreamLoading()); try { // 转换设备返回的ICE服务器为WebRTC标准格式 final iceServers = event.deviceConfig['icesServers'] .map<RTCIceServer>((server) { final urls = server['urls'] as String; // 仅处理标准stun/turn类型服务器,nat类型需根据设备文档确认适配逻辑 if (urls.startsWith('stun:') || urls.startsWith('turn:')) { return RTCIceServer( urls: urls, username: server['username'], credential: server['credential'], ); } return RTCIceServer(urls: urls); }) .toList(); // 初始化RTCPeerConnection final configuration = <String, dynamic>{'iceServers': iceServers}; _peerConnection = await createPeerConnection(configuration); // 监听远端媒体流 _peerConnection!.onTrack = (event) { if (event.streams.isNotEmpty) { _remoteStream = event.streams[0]; emit(VideoStreamLoaded(_remoteStream!, _peerConnection!)); } }; // 监听ICE候选并发送给设备 _peerConnection!.onIceCandidate = (candidate) { if (candidate != null) { _sendIceCandidateToDevice(candidate.toMap(), event.deviceConfig); } }; // 向设备请求SDP Offer(需对接设备实际API) final remoteOffer = await _fetchRemoteOffer( event.deviceConfig['moto_id'], event.deviceConfig['auth'], ); await _peerConnection!.setRemoteDescription(remoteOffer); // 创建本地SDP Answer并发送给设备 final answer = await _peerConnection!.createAnswer(); await _peerConnection!.setLocalDescription(answer); _sendLocalAnswerToDevice(answer.toMap(), event.deviceConfig); } catch (e) { emit(VideoStreamError('视频流获取失败: ${e.toString()}')); } } // 向设备发送ICE候选(替换为实际设备通信逻辑) Future<void> _sendIceCandidateToDevice( Map<String, dynamic> candidate, Map<String, dynamic> deviceConfig, ) async { // 实现HTTP/WebSocket请求,将候选信息发送给设备 } // 从设备获取SDP Offer(替换为实际设备API调用) Future<RTCSessionDescription> _fetchRemoteOffer( String motoId, String auth, ) async { final offerString = await _callDeviceApi(motoId, auth); return RTCSessionDescription(offerString, 'offer'); } // 向设备发送本地SDP Answer(替换为实际设备通信逻辑) Future<void> _sendLocalAnswerToDevice( Map<String, dynamic> answer, Map<String, dynamic> deviceConfig, ) async { // 实现HTTP/WebSocket请求,将Answer发送给设备 } // 设备API调用封装(替换为实际接口请求) Future<String> _callDeviceApi(String motoId, String auth) async { // 传入moto_id和auth,调用设备接口获取SDP Offer return ''; } Future<void> _onDisposeVideoStream( DisposeVideoStream event, Emitter<VideoStreamState> emit, ) async { await _remoteStream?.dispose(); await _peerConnection?.close(); emit(VideoStreamInitial()); } @override Future<void> close() { _remoteStream?.dispose(); _peerConnection?.close(); return super.close(); } }
3. UI层实现
通过BlocConsumer监听状态变化,渲染视频流:
class VideoStreamPage extends StatelessWidget { final Map<String, dynamic> deviceConfig; const VideoStreamPage({super.key, required this.deviceConfig}); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => VideoStreamBloc() ..add(FetchDeviceVideoStream(deviceConfig)), child: Scaffold( appBar: AppBar(title: const Text('设备视频流')), body: BlocConsumer<VideoStreamBloc, VideoStreamState>( listener: (context, state) { if (state is VideoStreamError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(state.message)), ); } }, builder: (context, state) { if (state is VideoStreamLoading) { return const Center(child: CircularProgressIndicator()); } else if (state is VideoStreamLoaded) { return RTCVideoView( state.stream, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, ); } else { return const Center(child: Text('加载中...')); } }, ), floatingActionButton: FloatingActionButton( onPressed: () { context.read<VideoStreamBloc>().add(DisposeVideoStream()); }, child: const Icon(Icons.stop), ), ), ); } }
4. 关键注意事项
- 权限配置:Android需在
AndroidManifest.xml添加网络权限;iOS需在Info.plist中配置网络相关权限,若涉及音视频采集需额外添加相机/麦克风权限描述。 - 设备协议对接:
_sendIceCandidateToDevice、_fetchRemoteOffer等方法需严格按照设备提供的通信协议(HTTP/WebSocket)实现,这是连接成功的核心。 - 资源释放:必须在页面销毁或停止流时触发
DisposeVideoStream事件,释放MediaStream和RTCPeerConnection资源,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Boutghat Oualid
相关产品推荐
相关产品推荐

