Flutter WebRTC iOS 原理解析:从 getUserMedia 到 Texture,讲清视频采集、纹理渲染与远端通话链路
适合读者:
已经会用
flutter_webrtc做本地预览或简单视频通话,但还想继续往下搞明白:
- Flutter 侧到底做了什么
- iOS 原生侧到底创建了什么
Texture/FlutterTexture到底怎么配合- 本地和远端视频为什么都能最终显示在 Flutter 页面上
先给结论
如果你只想先抓住主线,那就记住下面这条链路:
- Flutter 页面里先创建
RTCVideoRenderer RTCVideoRenderer.initialize()通过插件调用 iOS 原生,创建一个原生渲染器并注册textureId- Flutter 页面里的
RTCVideoView最终会用Texture(textureId: ...)把这个原生纹理挂到界面上 - 当你调用
getUserMedia()时,iOS 原生侧会通过 WebRTC iOS SDK 采集摄像头和麦克风 - 本地视频帧到达 iOS 原生渲染器后,会被转换成
CVPixelBuffer - iOS 通过
textureFrameAvailable(textureId)通知 Flutter 引擎"这张纹理有新帧了" - Flutter 引擎在光栅线程读取
copyPixelBuffer()返回的像素缓冲区,然后把内容画到Texture对应的位置 - 远端视频也是同样的渲染链路,只是视频帧来源从"本地摄像头"变成了"远端
RTCVideoTrack"
所以这篇文章真正要讲清的,不是某一个单点 API,而是这一整条链路。
先看纵向流程图
先不要急着看细节,先把整条链路从上到下看一遍:
Flutter 页面创建 `RTCVideoRenderer`
调用 `renderer.initialize()`
Dart 侧通过 MethodChannel 调原生 `createVideoRenderer`
iOS 创建 `FlutterRTCVideoRenderer`
iOS 调 `registerTexture(self)` 注册纹理
原生返回 `textureId` 给 Flutter
Flutter `RTCVideoView` 内部使用 `Texture(textureId)` 挂载纹理
Flutter 调 `getUserMedia()` 请求本地音视频
iOS 原生创建 `RTCAudioSource` / `RTCAudioTrack`
iOS 原生创建 `RTCVideoSource` / `RTCCameraVideoCapturer` / `RTCVideoTrack`
本地 `MediaStream` 返回给 Flutter
Flutter 执行 `renderer.srcObject = stream`
原生把对应 `RTCVideoTrack` 绑定给 `FlutterRTCVideoRenderer`
iOS 原生收到 `RTCVideoFrame`
把视频帧转成 I420 / 再转成 `CVPixelBuffer`
iOS 调 `textureFrameAvailable(textureId)` 通知 Flutter 引擎
Flutter 引擎回调 `copyPixelBuffer()` 取出最新帧
Flutter `Texture` 区域显示本地视频
如果进入远端通话:Flutter 创建 `RTCPeerConnection`
通过 `createOffer / createAnswer / setLocalDescription / setRemoteDescription / addCandidate` 建立 RTC 连接
远端视频轨到达,触发 `onTrack`
Flutter 执行 `remoteRenderer.srcObject = remoteStream`
远端视频继续走同一套 Texture 渲染链路
这张图里最重要的是两句话:
- Flutter 侧负责声明"哪里显示视频"
- iOS 原生侧负责准备"真正要显示的视频帧内容"
一、先从当前 Flutter 项目是怎么写开始
在当前项目里,本地预览入口很简单:
dart
final RTCVideoRenderer localRenderer = RTCVideoRenderer();
await localRenderer.initialize();
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},
},
});
localRenderer.srcObject = stream;
这段逻辑在当前项目里对应:
lib/pages/ChatVideoPage/RTCVideoPreviewBloc.dartlib/pages/ChatVideoPage/RTCVideoCallSessionStore.dart
从调用顺序上看,Flutter 侧做了 3 件事:
- 创建渲染器:
RTCVideoRenderer() - 初始化渲染器:
initialize() - 获取媒体流并绑定:
getUserMedia()+renderer.srcObject = stream
页面显示时再通过 RTCVideoView(renderer) 把画面渲染出来。
这一步看起来很简单,但底层已经开始涉及:
- Flutter Widget 树
- MethodChannel
- EventChannel
- iOS 原生 WebRTC SDK
- Flutter Texture 机制
二、Flutter 侧第一步:RTCVideoRenderer.initialize() 到底做了什么
flutter_webrtc Dart 侧原生实现里,RTCVideoRenderer.initialize() 的关键逻辑是:
dart
final response = await WebRTC.invokeMethod('createVideoRenderer', {});
_textureId = response['textureId'];
_eventSubscription = EventChannel('FlutterWebRTC/Texture$textureId')
.receiveBroadcastStream()
.listen(eventListener, onError: errorListener);
这一段来自 flutter_webrtc 的 Dart 源码:
lib/src/native/rtc_video_renderer_impl.dart
这里非常关键,说明 Flutter 侧初始化渲染器时,实际上做了两件事:
1. 通过插件方法调用原生:createVideoRenderer
也就是说,Flutter 不会自己"生产视频画面",而是向 iOS 要一个原生渲染器。
2. 拿回一个 textureId
这个 textureId 才是后面 Texture Widget 真正依赖的东西。
3. 建立一个 EventChannel
这个事件通道主要用来接收原生推回来的几个状态:
- 视频尺寸变化
- 视频旋转变化
- 第一帧已经渲染
所以从这一步开始,Flutter 和 iOS 原生已经建立了两条通信链路:
- MethodChannel:Flutter 主动调原生
- EventChannel:原生主动通知 Flutter
三、iOS 原生第二步:createVideoRenderer 如何创建 Texture
flutter_webrtc iOS 原生侧,在收到 createVideoRenderer 后,会创建一个 FlutterRTCVideoRenderer:
objc
FlutterRTCVideoRenderer* render = [self createWithTextureRegistry:_textures
messenger:_messenger];
self.renders[@(render.textureId)] = render;
result(@{@"textureId" : @(render.textureId)});
这段逻辑来自:
common/darwin/Classes/FlutterWebRTCPlugin.m
而真正的关键在 FlutterRTCVideoRenderer 的初始化方法里:
objc
- (instancetype)initWithTextureRegistry:(id)registry
messenger:(NSObject *)messenger {
self = [super init];
if (self) {
_registry = registry;
_textureId = [registry registerTexture:self];
_eventChannel = [FlutterEventChannel
eventChannelWithName:[NSString stringWithFormat:@"FlutterWebRTC/Texture%lld", _textureId]
binaryMessenger:messenger];
[_eventChannel setStreamHandler:self];
}
return self;
}
这段逻辑来自:
common/darwin/Classes/FlutterRTCVideoRenderer.m
看到这里,整个 Texture 机制就开始清晰了:
iOS 原生侧创建的并不是 UIView
而是一个实现了 FlutterTexture 协议的对象:FlutterRTCVideoRenderer。
这个对象会向 Flutter 注册自己
注册接口来自 Flutter 官方 iOS Embedder 文档里的 FlutterTextureRegistry:
registerTexture:textureFrameAvailable:unregisterTexture:
官方文档说明:
registerTexture:会返回一个int64_t的纹理 ID- 这个 ID 就是 Flutter 侧后续
Texture(textureId: ...)要用的值
参考资料:
四、Flutter 侧第三步:RTCVideoView 如何把纹理挂到界面上
flutter_webrtc Dart 侧的 RTCVideoView,最终会渲染成 Flutter 的 Texture Widget:
dart
child: videoRenderer.renderVideo
? Texture(
textureId: videoRenderer.textureId!,
filterQuality: filterQuality,
)
: placeholderBuilder?.call(context) ?? Container(),
这段逻辑来自:
lib/src/native/rtc_video_view_impl.dart
这一步要点非常重要:
Flutter 并没有拿到"图片对象"
Flutter 拿到的是一个 textureId。
Texture Widget 只负责把原生纹理挂在某个区域
Flutter 官方 Texture 文档明确说明:
Texture是把后端纹理映射到 Flutter 视图中的一个矩形区域- 它通过
textureId来引用后端纹理 - 它的更新通常由后端驱动,不需要每一帧都执行 Dart 代码
这也正是实时音视频场景为什么适合走 Texture:
- 不需要把每一帧转成 Dart
Image - 不需要每一帧都回到 Dart 层重绘
- 更适合高频视频帧更新
五、本地视频在 iOS 侧到底是怎么采集出来的
接下来进入更底层的部分。
当 Flutter 调用:
dart
final MediaStream stream = await navigator.mediaDevices.getUserMedia(...);
插件 iOS 侧会进入:
FlutterWebRTCPlugin.mFlutterRTCMediaStream.m
在 FlutterWebRTCPlugin.m 里,getUserMedia 最终会转给原生媒体流处理逻辑:
objc
} else if ([@"getUserMedia" isEqualToString:call.method]) {
NSDictionary* argsMap = call.arguments;
NSDictionary* constraints = argsMap[@"constraints"];
[self getUserMedia:constraints result:result];
}
1. 音频采集
在 FlutterRTCMediaStream.m 里,音频轨创建逻辑大致是:
objc
RTCAudioSource *audioSource = [self.peerConnectionFactory audioSourceWithConstraints:rtcConstraints];
RTCAudioTrack* audioTrack = [self.peerConnectionFactory audioTrackWithSource:audioSource trackId:trackId];
[mediaStream addAudioTrack:audioTrack];
[self ensureAudioSession];
这说明音频采集链路的核心是:
- 先创建
RTCAudioSource - 再创建
RTCAudioTrack - 再把它加进
RTCMediaStream
这里的采集和音频会话管理,本质上依赖的是:
- WebRTC iOS SDK 的音频模块
- iOS 音频会话能力(如
AVAudioSession)
2. 视频采集
视频轨创建逻辑更直观:
objc
RTCVideoSource* videoSource = [self.peerConnectionFactory videoSource];
VideoProcessingAdapter *videoProcessingAdapter =
[[VideoProcessingAdapter alloc] initWithRTCVideoSource:videoSource];
self.videoCapturer = [[RTCCameraVideoCapturer alloc] initWithDelegate:videoProcessingAdapter];
AVCaptureDeviceFormat* selectedFormat = [self selectFormatForDevice:videoDevice
targetWidth:targetWidth
targetHeight:targetHeight];
[self.videoCapturer startCaptureWithDevice:videoDevice
format:selectedFormat
fps:selectedFps
completionHandler:^(NSError* error) {
}];
RTCVideoTrack* videoTrack = [self.peerConnectionFactory videoTrackWithSource:videoSource
trackId:trackUUID];
[mediaStream addVideoTrack:videoTrack];
这说明 iOS 侧本地视频采集链路是:
- 创建
RTCVideoSource - 创建
RTCCameraVideoCapturer - 选择摄像头、分辨率、帧率
- 调
startCaptureWithDevice:format:fps:... - 采集结果进入
RTCVideoSource - 再基于
RTCVideoSource创建RTCVideoTrack - 最后把
RTCVideoTrack放进MediaStream
3. 为什么这说明它真的走到了 iOS 原生摄像头
因为源码里明确使用了:
AVCaptureDeviceAVCaptureDeviceFormatAVMediaTypeVideoRTCCameraVideoCapturer
也就是说,Flutter 只是发起了"我要摄像头"的请求,真正干活的是 iOS 原生层和 WebRTC iOS SDK。
六、本地视频采集出来以后,iOS 是如何把视频帧变成 Texture 内容的
这是第三篇最核心的一段。
当你把 MediaStream 绑定给 RTCVideoRenderer.srcObject 后,插件会在原生侧把对应的 RTCVideoTrack 交给 FlutterRTCVideoRenderer:
objc
- (void)rendererSetSrcObject:(FlutterRTCVideoRenderer*)renderer stream:(RTCVideoTrack*)videoTrack {
renderer.videoTrack = videoTrack;
}
然后在 setVideoTrack: 里,关键动作是:
objc
if (videoTrack) {
[videoTrack addRenderer:self];
}
这意味着:
FlutterRTCVideoRenderer 本身就是一个原生视频帧接收者
一旦它被挂到 RTCVideoTrack 上,WebRTC 原生视频帧就会开始回调给它。
接下来最关键的方法是:
objc
- (void)renderFrame:(RTCVideoFrame*)frame
也就是说,iOS 原生拿到的并不是 Flutter Image,而是 WebRTC 视频帧 RTCVideoFrame。
七、RTCVideoFrame 到了 iOS 原生后,为什么还要转成 CVPixelBuffer
Flutter 官方 iOS FlutterTexture 协议要求实现:
objc
- (CVPixelBufferRef _Nullable)copyPixelBuffer
官方文档明确写了:copyPixelBuffer 要返回 CVPixelBufferRef,像素格式通常是:
kCVPixelFormatType_32BGRAkCVPixelFormatType_420YpCbCr8BiPlanarVideoRangekCVPixelFormatType_420YpCbCr8BiPlanarFullRange
所以插件 iOS 侧必须把 WebRTC 的视频帧整理成 Flutter 引擎能消费的 CVPixelBuffer。
FlutterRTCVideoRenderer 里就是这么做的:
1. 先把帧转成 I420,并处理旋转
objc
id i420Buffer = [self correctRotation:[frame.buffer toI420]
withRotation:frame.rotation];
2. 再把 I420 转成目标像素格式
源码里用了 RTCYUVHelper 做转换:
I420ToNV12I420ToARGBI420ToBGRA
也就是说,底层数据不是直接"拿来就画",而是做了颜色格式转换。
3. 最后写入 CVPixelBuffer
objc
[self copyI420ToCVPixelBuffer:_pixelBufferRef withFrame:frame];
4. 然后通知 Flutter 有新帧
objc
[_registry textureFrameAvailable:_textureId];
这一句极其关键。
它的含义是:
"Flutter,这个
textureId对应的纹理现在有新画面了,你可以来取帧了。"
而 Flutter 官方 FlutterTextureRegistry 文档也明确写了:
textureFrameAvailable: 会触发引擎在 raster thread 上调用 copyPixelBuffer。
所以完整流程是:
renderFrame(frame)收到 WebRTC 视频帧- 转成
CVPixelBuffer - 调
textureFrameAvailable(textureId) - Flutter 引擎回调
copyPixelBuffer() TextureWidget 对应的区域显示出新画面
八、copyPixelBuffer() 到底起什么作用
插件实现大致是:
objc
- (CVPixelBufferRef)copyPixelBuffer {
CVPixelBufferRef buffer = nil;
os_unfair_lock_lock(&_lock);
if (_pixelBufferRef != nil && _frameAvailable) {
buffer = CVBufferRetain(_pixelBufferRef);
_frameAvailable = false;
}
os_unfair_lock_unlock(&_lock);
return buffer;
}
这一步你可以把它理解成:
copyPixelBuffer() 是 Flutter 引擎"取帧"的入口
不是 iOS 主动把像素硬塞给 Flutter,而是:
- iOS 先准备好
CVPixelBuffer - 再通知 Flutter "有新帧"
- Flutter 引擎需要时,再来调用
copyPixelBuffer()取走这一帧
这也是为什么 FlutterTexture 协议的职责,不是"画图",而是"提供像素缓冲区"。
九、为什么不直接转成 Flutter Image
这是很多初学者都会问的一个问题。
答案很简单:
因为实时音视频场景下,Texture + CVPixelBuffer 更高效
如果每一帧都这样走:
- iOS 原生拿到视频帧
- 转成某种图片对象
- 通过平台通道传给 Dart
- Dart 再构造成 Flutter
Image - Flutter 再重新布局/重绘
那代价会非常高:
- 拷贝次数多
- Dart 与原生跨层通信太频繁
- 高频视频帧下性能压力很大
而 Texture 的优势是:
- 只传一个稳定的
textureId - 像素数据留在原生 / 引擎这一侧处理
- Flutter Widget 树不需要每帧重新参与
所以视频渲染几乎都会优先走这类纹理机制,而不是"每帧转图片"。
十、Flutter 和 iOS 在 RTC 里到底是怎么通信的
这部分也很容易被混淆。
在 flutter_webrtc 里,Flutter 和 iOS 原生之间主要有两类通信:
1. MethodChannel:Flutter 主动调用原生
典型调用有:
createVideoRenderervideoRendererSetSrcObjectgetUserMediacreatePeerConnectioncreateOffercreateAnswersetLocalDescriptionsetRemoteDescriptionaddCandidate
也就是说,Dart 层负责发起"动作请求",原生侧真正执行。
2. EventChannel:原生主动回调 Flutter
当前链路里最关键的两类事件:
渲染器事件
事件通道名类似:
text
FlutterWebRTC/Texture{textureId}
主要上报:
didTextureChangeVideoSizedidTextureChangeRotationdidFirstFrameRendered
PeerConnection 事件
插件原生侧会给每个 RTCPeerConnection 建立事件通道,并把远端轨道等事件抛回 Flutter。
例如在 iOS 原生侧的 didAddReceiver 回调里,会往 Flutter 抛出:
objc
@"event" : @"onTrack"
这就对应了 Flutter 侧 peerConnection.onTrack 回调。
十一、远端视频又是如何显示出来的
讲完本地采集和本地渲染,再看远端就容易多了。
远端视频链路,本质上分成两层:
第一层:先把 RTC 连接建起来
在当前项目的 RTCVideoCallSessionStore 里,核心步骤是:
createPeerConnection(...)- 本地流
addTrack(...) - 主叫
createOffer() setLocalDescription(offer)- 被叫
setRemoteDescription(offer) - 被叫
createAnswer() - 被叫
setLocalDescription(answer) - 主叫
setRemoteDescription(answer) - 双方持续交换
candidate addCandidate(...)
当前项目代码里就能看到这条链路:
dart
final RTCPeerConnection peerConnection = await createPeerConnection(<String, dynamic>{
'iceServers': <Map<String, dynamic>>[
<String, dynamic>{'urls': 'stun:stun.l.google.com:19302'},
],
});
dart
final RTCSessionDescription offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
dart
await _peerConnection!.setRemoteDescription(
RTCSessionDescription(envelope.sdp!, 'offer'),
);
final RTCSessionDescription answer = await _peerConnection!.createAnswer();
await _peerConnection!.setLocalDescription(answer);
dart
await _peerConnection!.addCandidate(
RTCIceCandidate(
envelope.candidate,
envelope.sdpMid,
envelope.sdpMLineIndex,
),
);
第二层:远端轨道到达后,把远端流交给 remoteRenderer
在当前项目里:
dart
peerConnection.onTrack = (RTCTrackEvent event) {
if (event.streams.isEmpty) {
return;
}
remoteRenderer.srcObject = event.streams.first;
};
这一句的意义是:
远端流一旦到了,就把它绑定给另一个
RTCVideoRenderer。
后面的渲染链路和本地其实是一样的:
remoteRenderer已经持有自己的textureIdremoteRenderer.srcObject = remoteStream- iOS 原生拿到远端
RTCVideoTrack FlutterRTCVideoRenderer收到远端RTCVideoFrame- 转成
CVPixelBuffer - 通知
textureFrameAvailable(textureId) - Flutter
Texture(textureId: ...)更新画面
也就是说:
- 本地和远端显示,Flutter 侧用法几乎一样
- 真正不同的是视频帧来源不同
十二、远端 RTC 通话真正需要哪些关键参数
如果从"建立 RTC 连接"角度看,最关键的参数主要有两组。
1. SDP 协商参数
type
offeranswer
sdp
- 里面描述双方的媒体协商信息
- 比如音视频能力、编解码、媒体方向等
2. ICE 联通参数
candidate
- 一条候选网络路径信息
sdpMid
- candidate 属于哪条媒体描述
sdpMLineIndex
- candidate 对应 SDP 里的哪段媒体信息
3. 业务层额外参数
在当前项目里,为了把一次通话串起来,还额外使用了:
roomIdsessionIdfromto
这些不是 WebRTC 协议本身强制要求的参数,而是你们这个项目的业务信令上下文。
所以要区分清楚:
sdp / candidate / sdpMid / sdpMLineIndex:偏 RTC 协议层roomId / sessionId / from / to:偏业务信令层
十三、把整条链路按顺序再串一次
为了方便初学者,这里把整条链路重新串一次。
1. 本地预览链路
text
Flutter 创建 RTCVideoRenderer
-> initialize()
-> 原生 createVideoRenderer
-> iOS registerTexture(self)
-> 返回 textureId
-> Flutter RTCVideoView 内部使用 Texture(textureId)
-> Flutter 调 getUserMedia()
-> iOS 用 RTCCameraVideoCapturer + RTCAudioSource/Track 采集
-> localRenderer.srcObject = stream
-> 原生将 RTCVideoTrack 绑定到 FlutterRTCVideoRenderer
-> renderFrame(RTCVideoFrame)
-> 转 CVPixelBuffer
-> textureFrameAvailable(textureId)
-> Flutter 引擎调用 copyPixelBuffer()
-> Texture 显示本地视频
2. 远端通话链路
text
Flutter createPeerConnection()
-> addTrack(localTracks)
-> createOffer / createAnswer
-> setLocalDescription / setRemoteDescription
-> addCandidate
-> iOS 原生 PeerConnection 收到远端 track
-> 原生通过 EventChannel 抛 onTrack
-> Flutter peerConnection.onTrack 回调
-> remoteRenderer.srcObject = remoteStream
-> 后续渲染链路与本地 Texture 渲染完全一致
十四、这篇文章最容易混淆的 5 个点
1. RTCVideoView 不是原生采集器
它只是 Flutter 侧的显示组件,内部核心是 Texture。
2. RTCVideoRenderer 不是"直接画图"的 Widget
它更像是 Flutter 和原生纹理之间的桥接控制器。
3. iOS 原生侧创建的不是普通 UIView
这条纹理链路里,关键对象是实现了 FlutterTexture 协议的 FlutterRTCVideoRenderer。
4. iOS 拿到的是 RTCVideoFrame,不是 Flutter 图片
然后再把帧转换成 CVPixelBuffer 给 Flutter 引擎。
5. 本地视频和远端视频最终都会走 Texture
区别只是:
- 本地视频来自摄像头采集
- 远端视频来自远端
RTCVideoTrack
十五、最后总结
如果你已经看到这里,那请你最后记住这 4 句话:
- Flutter 页面上看到的不是"原生 View 截图",而是
Texture(textureId)对应的一块原生纹理内容 - iOS 原生侧的
FlutterRTCVideoRenderer负责把 WebRTC 的RTCVideoFrame转成CVPixelBuffer - Flutter 通过
MethodChannel + EventChannel与 iOS 原生协作,既能发起采集/建连,也能接收纹理和轨道事件 - 远端视频之所以也能显示,是因为远端
RTCVideoTrack最终同样被绑定到了一个RTCVideoRenderer,后续继续走 Texture 渲染链路
说得再直白一点:
Flutter 负责"声明界面上哪里显示视频",iOS 原生负责"把真实视频帧准备成 Flutter 可消费的纹理内容"。
这就是 flutter_webrtc 在 iOS 上实现视频显示的核心原理。
参考资料
Flutter 官方
- Flutter
TextureWidget:https://api.flutter.dev/flutter/widgets/Texture-class.html - Flutter iOS
FlutterTexture协议:https://api.flutter.dev/ios-embedder/protocol_flutter_texture-p.html - Flutter iOS
FlutterTextureRegistry协议:https://api.flutter.dev/ios-embedder/protocol_flutter_texture_registry-p.html
flutter_webrtc 官方 / 源码
flutter_webrtcPub 页面:https://pub.dev/packages/flutter_webrtcflutter_webrtc仓库:https://github.com/flutter-webrtc/flutter-webrtc- iOS 渲染器实现:https://github.com/flutter-webrtc/flutter-webrtc/blob/main/common/darwin/Classes/FlutterRTCVideoRenderer.m
- iOS 媒体流实现:https://github.com/flutter-webrtc/flutter-webrtc/blob/main/common/darwin/Classes/FlutterRTCMediaStream.m
- iOS 插件入口:https://github.com/flutter-webrtc/flutter-webrtc/blob/main/common/darwin/Classes/FlutterWebRTCPlugin.m
- Dart 侧
RTCVideoRenderer:https://github.com/flutter-webrtc/flutter-webrtc/blob/main/lib/src/native/rtc_video_renderer_impl.dart - Dart 侧
RTCVideoView:https://github.com/flutter-webrtc/flutter-webrtc/blob/main/lib/src/native/rtc_video_view_impl.dart
WebRTC iOS 采集补充
- WebRTC iOS
RTCCameraVideoCapturer源码参考:https://chromium.googlesource.com/external/webrtc/+/branch-heads/62/webrtc/sdk/objc/Framework/Classes/PeerConnection/RTCCameraVideoCapturer.m
作者 : 911hzh
邮箱 : 911hzh@gmail.com
需要demo:支持一下,点个赞,点个关注,请私信