Flutter WebRTC 入门实战:5 步实现本地视频预览,搞懂 getUserMedia、RTCVideoRenderer、RTCVideoView

这篇文章先不讲 Socket、Offer、Answer、Candidate,也不讨论复杂的音视频架构。
我们先做一件最重要的事:

确认 flutter_webrtc 已经可以在 Flutter 项目里采集并渲染本地视频。

对初学者来说,这是最值得先完成的一步。因为只有本地采集和本地渲染先跑通,后面的信令、房间、通话流程才有意义。

一、先看最重要的结论

如果你只是想先知道“本地预览到底怎么跑起来”,可以先记住这一条链:

  1. 创建 RTCVideoRenderer
  2. 调用 initialize()
  3. 调用 navigator.mediaDevices.getUserMedia(...)
  4. 得到本地 MediaStream
  5. 执行 renderer.srcObject = stream
  6. 用 RTCVideoView(renderer) 把画面显示出来

也就是说,最核心的顺序其实就是:

先初始化渲染器 -> 再获取本地媒体流 -> 再把流绑定给渲染器 -> 最后交给 RTCVideoView 显示

这一篇文章就围绕这几个 RTC 相关类、调用参数和调用步骤来讲,不展开页面架构本身。

二、flutter_webrtc 是什么

flutter_webrtc 是一个 Flutter 插件,用来在 Flutter 中接入 WebRTC 能力。根据它的官方 Pub 页面描述,它是一个基于 Google WebRTC 的 Flutter 插件,支持 iOS、Android、Desktop 和 Web。

官方资料:

简单理解,它帮我们在 Flutter 里封装了这些核心能力:

  • 采集摄像头和麦克风
  • 创建本地媒体流 MediaStream
  • 创建 RTCPeerConnection
  • 交换 Offer / Answer / ICE Candidate
  • 渲染本地和远端视频

如果没有这个插件,我们就需要分别去处理 iOS、Android、Web 的原生 WebRTC 接入细节,成本会高很多。

三、它主要是做什么的

flutter_webrtc 最常见的用途有 4 类:

  1. 音视频通话
  2. 直播连麦
  3. 屏幕共享
  4. DataChannel 实时数据通信

对于一个聊天项目来说,最典型的使用方式通常是:

  • Flutter 页面负责 UI 展示
  • flutter_webrtc 负责媒体采集、连接建立和视频渲染
  • Socket 负责传递信令消息

不过在真正进入“通话”之前,我们完全可以先只做本地预览。

四、为什么第一步先做“本地预览”

可以,而且非常建议先这样做。

原因很简单:

  • 可以先验证摄像头权限有没有问题
  • 可以先验证 RTCVideoRenderer 和 RTCVideoView 是否工作正常
  • 可以先验证真机环境是否已经满足 flutter_webrtc 的运行要求
  • 出问题时,排查范围会小很多

当前项目里虽然用了一个预览页面承载这条链路,但真正值得初学者关注的,不是页面结构,而是下面这些 RTC 相关对象和调用。

五、这一篇里最值得先认识的 RTC 类

1. RTCVideoRenderer

它是视频渲染器。

你可以把它理解成:

  • Dart 层持有的一个视频输出对象
  • 后面会和原生视频帧渲染链路连接起来

它本身不负责采集视频,它负责:

  • 接收某条媒体流
  • 把这条流交给底层渲染链路
  • 最后让 RTCVideoView 有内容可显示

使用前通常要先初始化:

final RTCVideoRenderer renderer = RTCVideoRenderer();
await renderer.initialize();

2. MediaStream

它表示一条媒体流。

一条 MediaStream 里通常包含:

  • 音频轨
  • 视频轨

本地预览场景下,它通常来自:

  • 摄像头
  • 麦克风

后面我们会通过 getUserMedia(...) 去拿到它。

3. MediaStreamTrack

如果说 MediaStream 是“一整条流”,那 MediaStreamTrack 就是这条流里面的具体轨道。

常见的就是两种:

  • 音频轨 audio track
  • 视频轨 video track

后面开关麦克风、开关摄像头,本质上操作的都是 track.enabled。

4. RTCVideoView

它是 Flutter Widget,用来把 RTCVideoRenderer 里的画面显示到页面上。

例如:

RTCVideoView(
  renderer,
  mirror: true,
  objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
)

所以它的职责很明确:

  • RTCVideoRenderer 负责持有视频输出
  • RTCVideoView 负责把这个输出显示到 Flutter 页面

5. RTCPeerConnection

这是 WebRTC 建立点对点连接的核心对象。

不过第一篇文章里可以先不展开它,因为本地预览不需要建立 RTCPeerConnection。
第一篇的目标是先把自己的视频显示出来。

六、最关键的调用:getUserMedia(...)

根据 Flutter WebRTC Community Docs,getUserMedia(...) 的作用是:

向设备申请本地媒体采集,并返回一个 MediaStream。

官方文档:https://flutter-webrtc.org/docs/flutter-webrtc/api-docs/get-user-media/

当前项目中的调用是:

final MediaStream stream = await navigator.mediaDevices.getUserMedia(<String, dynamic>{
  'audio': true,
  'video': <String, dynamic>{
    'facingMode': 'user',
    'width': <String, dynamic>{'ideal': 1280},
    'height': <String, dynamic>{'ideal': 720},
  },
});

这段代码的作用就是:

  • 打开麦克风
  • 打开摄像头
  • 返回一条本地音视频流 MediaStream

参数逐项解释

1. 'audio': true

表示:

需要音频输入

也就是要打开麦克风。

2. 'video': {...}

表示:

需要视频输入

也就是要打开摄像头。

3. 'facingMode': 'user'

表示:

优先使用前置摄像头

移动端常见值有两个:

  • 'user':前置摄像头
  • 'environment':后置摄像头

所以本地预览场景里,默认用 'user' 很合理。

4. 'width': {'ideal': 1280}

表示:

期望视频宽度尽量接近 1280

这里用的是 ideal,意思不是“必须是 1280”,而是“尽量满足”。

5. 'height': {'ideal': 720}

表示:

期望视频高度尽量接近 720

所以这一组参数的目标,基本可以理解成:

  • 希望拿到接近 1280 x 720 的本地视频流
  • 也就是接近 720p

为什么这里更推荐 ideal

因为入门场景下,兼容性往往比“必须固定分辨率”更重要。

如果设备不支持某个强约束值,直接失败会让初学者更难排查。
而 ideal 的语义更温和:

  • 能满足最好
  • 不能完全满足,也尽量给你接近的结果

七、拿到 MediaStream 之后,下一步要做什么

拿到本地流以后,还不能立刻显示。

你还需要做这一步:

renderer.srcObject = stream;

这一步的作用是:

把刚拿到的本地媒体流交给渲染器。

如果没有这一步:

  • 你虽然成功拿到了 MediaStream
  • 但 RTCVideoRenderer 还不知道该显示哪条流

所以它的本质是:

把“采集结果”交给“显示管线”。

八、最后是谁把视频显示到页面上的

真正把视频显示出来的是:

RTCVideoView(
  renderer,
  mirror: true,
  objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
)

这里几个参数值得初学者先记住:

1. renderer

表示:

当前要显示哪个 RTCVideoRenderer 里的画面

2. mirror: true

表示:

镜像显示

本地预览场景里通常会打开镜像,这样看起来更符合自拍习惯。

3. objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover

表示:

画面尽量铺满当前区域

它更像 Flutter 里图片或视频的 cover 效果。

九、控制摄像头和麦克风,本质上是在控制什么

很多初学者看到“静音”“关摄像头”时,会以为又重新调用了一次采集接口。
其实不是。

当前项目里的做法是:

  • 麦克风开关:遍历 stream.getAudioTracks()
  • 摄像头开关:遍历 stream.getVideoTracks()
  • 然后修改 track.enabled

也就是说:

  • getUserMedia(...) 是“获取流”
  • track.enabled = false 是“关闭已有轨道输出”

这是两个不同层面的动作。

麦克风开关

for (final MediaStreamTrack track in stream.getAudioTracks()) {
  track.enabled = nextEnabled;
}

作用:

  • 开启或关闭音频轨

摄像头开关

for (final MediaStreamTrack track in stream.getVideoTracks()) {
  track.enabled = nextEnabled;
}

作用:

  • 开启或关闭视频轨

十、切换前后摄像头是怎么做的

当前项目里使用的是 flutter_webrtc 提供的辅助方法:

await Helper.switchCamera(stream.getVideoTracks().first, null, stream);

这句话的作用是:

  • 取出当前视频轨
  • 切换它背后的摄像头设备

所以这里不是重新从零写一套摄像头切换逻辑,而是直接使用插件能力。

十一、为什么还要关心资源释放

本地预览不是“显示一下图片”那么简单,它背后持有的是:

  • 摄像头资源
  • 麦克风资源
  • 本地媒体流对象
  • 视频渲染器对象

所以用完以后要做清理。

当前项目里主要有两类清理动作:

1. 释放本地流

关键动作包括:

  • renderer.srcObject = null
  • track.stop()
  • stream.dispose()

这一步的目的是:

  • 停止当前采集
  • 释放摄像头和麦克风占用

2. 释放渲染器

await renderer.dispose();

这一步的目的是:

  • 释放渲染器相关资源

如果不做这些清理,后续页面切换、重复打开预览、甚至通话流程都可能出现问题。

十二、把整个调用顺序再串一遍

这一篇文章最核心的调用步骤,总结起来就是:

第一步:创建渲染器

final RTCVideoRenderer renderer = RTCVideoRenderer();

作用:

  • 创建一个本地视频输出对象

第二步:初始化渲染器

await renderer.initialize();

作用:

  • 为后续视频显示做好准备

第三步:申请本地媒体流

final MediaStream stream = await navigator.mediaDevices.getUserMedia({
  'audio': true,
  'video': {
    'facingMode': 'user',
    'width': {'ideal': 1280},
    'height': {'ideal': 720},
  },
});

作用:

  • 打开麦克风
  • 打开摄像头
  • 返回本地 MediaStream

第四步:把流绑定给渲染器

renderer.srcObject = stream;

作用:

  • 告诉渲染器要显示这条本地流

第五步:把渲染器交给 RTCVideoView

RTCVideoView(
  renderer,
  mirror: true,
  objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
)

作用:

  • 在 Flutter 页面上真正把本地视频显示出来

十三、flutter_webrtc 的优点

结合官方文档和实际项目经验,flutter_webrtc 的优点主要有这些。

1. 跨平台能力比较完整

从 Pub 页面可以看到,它支持:

  • iOS
  • Android
  • Web
  • macOS
  • Windows
  • Linux

这对 Flutter 项目非常友好。

2. 核心能力覆盖比较全

官方列出的能力包括:

  • Audio / Video
  • Data Channel
  • Screen Capture
  • Unified Plan
  • Simulcast

所以不只是简单视频预览,后续做实时音视频、直播连麦、屏幕共享,也有继续扩展的空间。

3. 和 Flutter 页面结合自然

Flutter 侧直接通过 RTCVideoView 展示视频,页面层写法相对统一。
对 Flutter 开发者来说,上手门槛比自己封装原生插件低很多。

4. 社区资料相对集中

至少入门阶段,下面几类资料是比较容易找到的:

  • Pub 文档
  • 官方社区文档
  • 示例项目
  • 常见问题讨论

十四、flutter_webrtc 的局限和注意点

初学者也要知道,它并不是“装上就万事大吉”的插件。

1. 需要理解一些 WebRTC 基础概念

即便插件已经帮你封装了很多能力,你仍然需要逐步理解:

  • MediaStream
  • MediaStreamTrack
  • RTCPeerConnection
  • Offer / Answer
  • ICE Candidate
  • STUN / TURN

否则后面做通话时很容易只会“照着抄代码”,但不理解为什么这样写。

2. 真机调试非常重要

尤其是摄像头、麦克风、权限、前后摄切换这类能力,模拟器环境通常不够完整。
所以真正做视频能力时,建议尽量用真机验证。

3. 平台差异不能完全忽略

虽然 flutter_webrtc 对外暴露的是统一接口,但底层依然涉及:

  • iOS 原生 WebRTC
  • Android 原生 WebRTC
  • 各平台权限配置
  • 渲染实现差异

所以复杂场景下,还是要具备一点平台层认知。

4. 一旦进入通话实战,信令设计会变复杂

视频通话真正难的地方,很多时候不在“把画面显示出来”,而在:

  • 如何设计信令
  • 如何处理状态同步
  • 如何管理 roomId / sessionId
  • 如何处理拒接、忙线、超时、挂断

这些内容更适合放到第二篇文章单独讲。

十五、小结

这篇文章最重要的结论有 4 个:

  1. flutter_webrtc 是 Flutter 接入 WebRTC 的核心插件
  2. 对初学者来说,第一步最应该先做“本地预览”
  3. 本地预览最核心的 RTC 类是:RTCVideoRenderer、MediaStream、MediaStreamTrack、RTCVideoView
  4. 当前这条预览链路本质上就是:初始化渲染器 -> 获取本地流 -> 绑定渲染器 -> 页面显示

如果你正在跟着当前项目一起学,建议先验证下面这几件事:

  • 能否正常申请摄像头和麦克风权限
  • 能否看到自己的本地画面
  • 能否切换前后摄像头
  • 能否控制麦克风和摄像头开关

这些都跑通以后,再进入第二篇去讲:

  • 聊天场景下如何通过 Socket 承载 WebRTC 信令
  • callInvite / callAccept / webrtcOffer / webrtcAnswer / webrtcIceCandidate 是如何协作的

参考资料

作者: 911hzh
邮箱: 911hzh@gmail.com
需要demo:请私信

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐