You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 13:39:37