Flutter WebRTC iOS 原理解析:从 getUserMedia 到 Texture,讲清视频采集、纹理渲染与远端通话链路

Flutter WebRTC iOS 原理解析:从 getUserMedia 到 Texture,讲清视频采集、纹理渲染与远端通话链路

适合读者:

已经会用 flutter_webrtc 做本地预览或简单视频通话,但还想继续往下搞明白:

  • Flutter 侧到底做了什么
  • iOS 原生侧到底创建了什么
  • Texture / FlutterTexture 到底怎么配合
  • 本地和远端视频为什么都能最终显示在 Flutter 页面上

先给结论

如果你只想先抓住主线,那就记住下面这条链路:

  1. Flutter 页面里先创建 RTCVideoRenderer
  2. RTCVideoRenderer.initialize() 通过插件调用 iOS 原生,创建一个原生渲染器并注册 textureId
  3. Flutter 页面里的 RTCVideoView 最终会用 Texture(textureId: ...) 把这个原生纹理挂到界面上
  4. 当你调用 getUserMedia() 时,iOS 原生侧会通过 WebRTC iOS SDK 采集摄像头和麦克风
  5. 本地视频帧到达 iOS 原生渲染器后,会被转换成 CVPixelBuffer
  6. iOS 通过 textureFrameAvailable(textureId) 通知 Flutter 引擎"这张纹理有新帧了"
  7. Flutter 引擎在光栅线程读取 copyPixelBuffer() 返回的像素缓冲区,然后把内容画到 Texture 对应的位置
  8. 远端视频也是同样的渲染链路,只是视频帧来源从"本地摄像头"变成了"远端 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 渲染链路

这张图里最重要的是两句话:

  1. Flutter 侧负责声明"哪里显示视频"
  2. 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.dart
  • lib/pages/ChatVideoPage/RTCVideoCallSessionStore.dart

从调用顺序上看,Flutter 侧做了 3 件事:

  1. 创建渲染器:RTCVideoRenderer()
  2. 初始化渲染器:initialize()
  3. 获取媒体流并绑定: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.m
  • FlutterRTCMediaStream.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 侧本地视频采集链路是:

  1. 创建 RTCVideoSource
  2. 创建 RTCCameraVideoCapturer
  3. 选择摄像头、分辨率、帧率
  4. startCaptureWithDevice:format:fps:...
  5. 采集结果进入 RTCVideoSource
  6. 再基于 RTCVideoSource 创建 RTCVideoTrack
  7. 最后把 RTCVideoTrack 放进 MediaStream

3. 为什么这说明它真的走到了 iOS 原生摄像头

因为源码里明确使用了:

  • AVCaptureDevice
  • AVCaptureDeviceFormat
  • AVMediaTypeVideo
  • RTCCameraVideoCapturer

也就是说,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_32BGRA
  • kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
  • kCVPixelFormatType_420YpCbCr8BiPlanarFullRange

所以插件 iOS 侧必须把 WebRTC 的视频帧整理成 Flutter 引擎能消费的 CVPixelBuffer

FlutterRTCVideoRenderer 里就是这么做的:

1. 先把帧转成 I420,并处理旋转

objc 复制代码
id i420Buffer = [self correctRotation:[frame.buffer toI420]
                         withRotation:frame.rotation];

2. 再把 I420 转成目标像素格式

源码里用了 RTCYUVHelper 做转换:

  • I420ToNV12
  • I420ToARGB
  • I420ToBGRA

也就是说,底层数据不是直接"拿来就画",而是做了颜色格式转换。

3. 最后写入 CVPixelBuffer

objc 复制代码
[self copyI420ToCVPixelBuffer:_pixelBufferRef withFrame:frame];

4. 然后通知 Flutter 有新帧

objc 复制代码
[_registry textureFrameAvailable:_textureId];

这一句极其关键。

它的含义是:

"Flutter,这个 textureId 对应的纹理现在有新画面了,你可以来取帧了。"

而 Flutter 官方 FlutterTextureRegistry 文档也明确写了:

textureFrameAvailable: 会触发引擎在 raster thread 上调用 copyPixelBuffer

所以完整流程是:

  1. renderFrame(frame) 收到 WebRTC 视频帧
  2. 转成 CVPixelBuffer
  3. textureFrameAvailable(textureId)
  4. Flutter 引擎回调 copyPixelBuffer()
  5. Texture Widget 对应的区域显示出新画面

八、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 更高效

如果每一帧都这样走:

  1. iOS 原生拿到视频帧
  2. 转成某种图片对象
  3. 通过平台通道传给 Dart
  4. Dart 再构造成 Flutter Image
  5. Flutter 再重新布局/重绘

那代价会非常高:

  • 拷贝次数多
  • Dart 与原生跨层通信太频繁
  • 高频视频帧下性能压力很大

Texture 的优势是:

  • 只传一个稳定的 textureId
  • 像素数据留在原生 / 引擎这一侧处理
  • Flutter Widget 树不需要每帧重新参与

所以视频渲染几乎都会优先走这类纹理机制,而不是"每帧转图片"。


十、Flutter 和 iOS 在 RTC 里到底是怎么通信的

这部分也很容易被混淆。

flutter_webrtc 里,Flutter 和 iOS 原生之间主要有两类通信:

1. MethodChannel:Flutter 主动调用原生

典型调用有:

  • createVideoRenderer
  • videoRendererSetSrcObject
  • getUserMedia
  • createPeerConnection
  • createOffer
  • createAnswer
  • setLocalDescription
  • setRemoteDescription
  • addCandidate

也就是说,Dart 层负责发起"动作请求",原生侧真正执行。

2. EventChannel:原生主动回调 Flutter

当前链路里最关键的两类事件:

渲染器事件

事件通道名类似:

text 复制代码
FlutterWebRTC/Texture{textureId}

主要上报:

  • didTextureChangeVideoSize
  • didTextureChangeRotation
  • didFirstFrameRendered

PeerConnection 事件

插件原生侧会给每个 RTCPeerConnection 建立事件通道,并把远端轨道等事件抛回 Flutter。

例如在 iOS 原生侧的 didAddReceiver 回调里,会往 Flutter 抛出:

objc 复制代码
@"event" : @"onTrack"

这就对应了 Flutter 侧 peerConnection.onTrack 回调。


十一、远端视频又是如何显示出来的

讲完本地采集和本地渲染,再看远端就容易多了。

远端视频链路,本质上分成两层:

第一层:先把 RTC 连接建起来

在当前项目的 RTCVideoCallSessionStore 里,核心步骤是:

  1. createPeerConnection(...)
  2. 本地流 addTrack(...)
  3. 主叫 createOffer()
  4. setLocalDescription(offer)
  5. 被叫 setRemoteDescription(offer)
  6. 被叫 createAnswer()
  7. 被叫 setLocalDescription(answer)
  8. 主叫 setRemoteDescription(answer)
  9. 双方持续交换 candidate
  10. 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

后面的渲染链路和本地其实是一样的:

  1. remoteRenderer 已经持有自己的 textureId
  2. remoteRenderer.srcObject = remoteStream
  3. iOS 原生拿到远端 RTCVideoTrack
  4. FlutterRTCVideoRenderer 收到远端 RTCVideoFrame
  5. 转成 CVPixelBuffer
  6. 通知 textureFrameAvailable(textureId)
  7. Flutter Texture(textureId: ...) 更新画面

也就是说:

  • 本地和远端显示,Flutter 侧用法几乎一样
  • 真正不同的是视频帧来源不同

十二、远端 RTC 通话真正需要哪些关键参数

如果从"建立 RTC 连接"角度看,最关键的参数主要有两组。

1. SDP 协商参数

type

  • offer
  • answer

sdp

  • 里面描述双方的媒体协商信息
  • 比如音视频能力、编解码、媒体方向等

2. ICE 联通参数

candidate

  • 一条候选网络路径信息

sdpMid

  • candidate 属于哪条媒体描述

sdpMLineIndex

  • candidate 对应 SDP 里的哪段媒体信息

3. 业务层额外参数

在当前项目里,为了把一次通话串起来,还额外使用了:

  • roomId
  • sessionId
  • from
  • to

这些不是 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 句话:

  1. Flutter 页面上看到的不是"原生 View 截图",而是 Texture(textureId) 对应的一块原生纹理内容
  2. iOS 原生侧的 FlutterRTCVideoRenderer 负责把 WebRTC 的 RTCVideoFrame 转成 CVPixelBuffer
  3. Flutter 通过 MethodChannel + EventChannel 与 iOS 原生协作,既能发起采集/建连,也能接收纹理和轨道事件
  4. 远端视频之所以也能显示,是因为远端 RTCVideoTrack 最终同样被绑定到了一个 RTCVideoRenderer,后续继续走 Texture 渲染链路

说得再直白一点:

Flutter 负责"声明界面上哪里显示视频",iOS 原生负责"把真实视频帧准备成 Flutter 可消费的纹理内容"。

这就是 flutter_webrtc 在 iOS 上实现视频显示的核心原理。


参考资料

Flutter 官方

flutter_webrtc 官方 / 源码

WebRTC iOS 采集补充

作者 : 911hzh
邮箱 : 911hzh@gmail.com
需要demo:支持一下,点个赞,点个关注,请私信

相关推荐
xmdy58666 小时前
Flutter+开源鸿蒙实战|智联邻里Day1 项目搭建+环境适配+架构规划(十五五民生创新版)
flutter·开源·harmonyos
软泡芙6 小时前
【iOS】 开发入门指南
ios
maaath6 小时前
【maaath】Flutter for OpenHarmony 音乐播放器应用实战开发
flutter·华为·harmonyos
水中加点糖7 小时前
ios中使用DockKit和CoreML实现自定义目标的自动跟随(一)
目标检测·ios·目标跟踪·硬件控制·dockkit
maaath7 小时前
【maaath】 Flutter for OpenHarmony 实战:图片壁纸应用开发指南
flutter·华为·harmonyos
maaath7 小时前
【maaath】Flutter for OpenHarmony:跨平台天气应用开发指南
flutter·华为·harmonyos
maaath7 小时前
【maaath】Flutter for OpenHarmony 宠物社区应用实战开发
flutter·华为·harmonyos
2501_915909067 小时前
iOS应用签名的三种方法全解析:从官方到第三方工具
android·ios·小程序·https·uni-app·iphone·webview
shao9185167 小时前
第12章Streaming(下):视频应用(1)——项目八:基于WebRTC+YOLO的实时目标检测
yolo·目标检测·webrtc·gradio·视频流·yolov10·流式传输