iOS上使用WebRTC推拉流的案例

一、库集成

首先,确保在你的 Podfile 中添加依赖:

pod 'GoogleWebRTC'

然后执行 pod install 安装库。

二、代码示例

2.1、权限配置:在 Info.plist 中添加摄像头、麦克风权限

<!-- 需要在 Info.plist 中添加以下权限 -->
<key>NSCameraUsageDescription</key>
<string>需要访问摄像头进行视频通话</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要访问麦克风进行语音通话</string>
<key>NSAppTransportSecurity</key>
<dict><key>NSAllowsArbitraryLoads</key><true/>
</dict>

2.2、代码
核心类oc:WebRTCManager.m

#import "WebRTCManager.h"
#import <AVFoundation/AVFoundation.h>@interface WebRTCManager ()
@property (nonatomic, strong) RTCPeerConnectionFactory *factory;
@property (nonatomic, strong) RTCVideoTrack *localVideoTrack;
@property (nonatomic, strong) RTCAudioTrack *localAudioTrack;
@property (nonatomic, strong) RTCVideoRendererAdapter *localRenderer;
@end@implementation WebRTCManager- (instancetype)initWithDelegate:(id<WebRTCManagerDelegate>)delegate {self = [super init];if (self) {_delegate = delegate;[self setupPeerConnectionFactory];[self setupPeerConnection];}return self;
}// 初始化 PeerConnection 工厂
- (void)setupPeerConnectionFactory {RTCInitializeSSL();_factory = [[RTCPeerConnectionFactory alloc] init];
}// 配置并创建 PeerConnection
- (void)setupPeerConnection {RTCConfiguration *config = [[RTCConfiguration alloc] init];config.iceServers = @[[[RTCIceServer alloc] initWithURLStrings:@[@"stun:stun.l.google.com:19302"]]];RTCOfferAnswerOptions *offerAnswerOptions = [[RTCOfferAnswerOptions alloc] init];offerAnswerOptions.offerToReceiveAudio = YES;offerAnswerOptions.offerToReceiveVideo = YES;RTCMediaConstraints *constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:@[] optionalConstraints:@[]];_peerConnection = [_factory peerConnectionWithConfiguration:config constraints:constraints delegate:self];
}// 初始化本地媒体流
- (void)setupLocalStreamWithVideoView:(UIView *)videoView {// 请求音视频权限[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {dispatch_async(dispatch_get_main_queue(), ^{if (granted) {[self createLocalMediaStream];[self setupLocalVideoRender:videoView];} else {NSLog(@"需要摄像头权限才能使用视频功能");}});}];
}// 创建本地媒体流
- (void)createLocalMediaStream {_localStream = [_factory mediaStreamWithStreamId:@"localStream"];// 创建音频轨道RTCAudioSource *audioSource = [_factory audioSourceWithConstraints:[self defaultMediaConstraints]];_localAudioTrack = [_factory audioTrackWithSource:audioSource trackId:@"audio0"];[_localStream addAudioTrack:_localAudioTrack];// 创建视频轨道RTCVideoSource *videoSource = [_factory videoSourceWithConstraints:[self videoConstraints]];_localVideoTrack = [_factory videoTrackWithSource:videoSource trackId:@"video0"];[_localStream addVideoTrack:_localVideoTrack];// 将本地流添加到 PeerConnection[_peerConnection addStream:_localStream];
}// 设置本地视频渲染
- (void)setupLocalVideoRender:(UIView *)videoView {RTCMTLVideoView *rendererView = [[RTCMTLVideoView alloc] init];rendererView.frame = videoView.bounds;rendererView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;[videoView addSubview:rendererView];_localRenderer = [[RTCVideoRendererAdapter alloc] initWithRenderer:rendererView];[_localVideoTrack addRenderer:_localRenderer];
}// 创建 Offer
- (void)createOffer {RTCOfferAnswerConstraints *constraints = [[RTCOfferAnswerConstraints alloc] init];constraints.mandatoryConstraints = @[[[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"],[[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"]];[_peerConnection createOfferWithConstraints:constraints completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {if (error) {NSLog(@"创建 Offer 失败: %@", error.localizedDescription);return;}[self.peerConnection setLocalDescriptionWithSessionDescription:sdp completionHandler:^(NSError * _Nullable error) {if (error) {NSLog(@"设置本地描述失败: %@", error.localizedDescription);return;}// 这里应该将 sdp 发送给信令服务器NSString *offerString = [self sessionDescriptionToString:sdp];NSLog(@"生成 Offer: %@", offerString);// [self.signalingClient sendOffer:offerString];}];}];
}// 处理远程 Offer
- (void)handleRemoteOffer:(NSString *)offer {RTCSessionDescription *remoteSDP = [self stringToSessionDescription:offer type:RTCSdpTypeOffer];[_peerConnection setRemoteDescriptionWithSessionDescription:remoteSDP completionHandler:^(NSError * _Nullable error) {if (error) {NSLog(@"设置远程 Offer 失败: %@", error.localizedDescription);return;}// 创建 Answer[self createAnswer];}];
}// 创建 Answer
- (void)createAnswer {RTCOfferAnswerConstraints *constraints = [[RTCOfferAnswerConstraints alloc] init];[_peerConnection createAnswerWithConstraints:constraints completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {if (error) {NSLog(@"创建 Answer 失败: %@", error.localizedDescription);return;}[self.peerConnection setLocalDescriptionWithSessionDescription:sdp completionHandler:^(NSError * _Nullable error) {if (error) {NSLog(@"设置本地 Answer 失败: %@", error.localizedDescription);return;}// 将 Answer 发送给信令服务器NSString *answerString = [self sessionDescriptionToString:sdp];NSLog(@"生成 Answer: %@", answerString);// [self.signalingClient sendAnswer:answerString];}];}];
}// 处理远程 Answer
- (void)handleRemoteAnswer:(NSString *)answer {RTCSessionDescription *remoteSDP = [self stringToSessionDescription:answer type:RTCSdpTypeAnswer];[_peerConnection setRemoteDescriptionWithSessionDescription:remoteSDP completionHandler:^(NSError * _Nullable error) {if (error) {NSLog(@"设置远程 Answer 失败: %@", error.localizedDescription);}}];
}// 处理远程 ICE 候选者
- (void)handleRemoteICECandidate:(NSString *)candidate {NSDictionary *candidateDict = [NSJSONSerialization JSONObjectWithData:[candidate dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];if (!candidateDict) return;RTCIceCandidate *iceCandidate = [[RTCIceCandidate alloc] initWithSdpMLineIndex:[candidateDict[@"sdpMLineIndex"] integerValue]sdpMid:candidateDict[@"sdpMid"]sdp:candidateDict[@"sdp"]];[_peerConnection addIceCandidate:iceCandidate completionHandler:^(NSError * _Nullable error) {if (error) {NSLog(@"添加远程 ICE 候选者失败: %@", error.localizedDescription);}}];
}// 断开连接
- (void)disconnect {[_peerConnection close];_peerConnection = nil;[_localStream removeAllAudioTracks];[_localStream removeAllVideoTracks];_localStream = nil;
}#pragma mark - RTCPeerConnectionDelegate// 收到远程媒体流
- (void)peerConnection:(RTCPeerConnection *)peerConnection didAddStream:(RTCMediaStream *)stream {NSLog(@"收到远程媒体流");if ([self.delegate respondsToSelector:@selector(didReceiveRemoteStream:)] && stream) {[self.delegate didReceiveRemoteStream:stream];}
}// 生成 ICE 候选者
- (void)peerConnection:(RTCPeerConnection *)peerConnection didGenerateIceCandidate:(RTCIceCandidate *)candidate {NSDictionary *candidateDict = @{@"sdpMLineIndex": @(candidate.sdpMLineIndex),@"sdpMid": candidate.sdpMid ?: @"",@"sdp": candidate.sdp};NSData *jsonData = [NSJSONSerialization dataWithJSONObject:candidateDict options:0 error:nil];NSString *candidateString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];// 将 ICE 候选者发送给信令服务器NSLog(@"生成 ICE 候选者: %@", candidateString);// [self.signalingClient sendICECandidate:candidateString];
}#pragma mark - 工具方法// 媒体约束配置
- (RTCMediaConstraints *)defaultMediaConstraints {return [[RTCMediaConstraints alloc] initWithMandatoryConstraints:@[] optionalConstraints:@[]];
}// 视频约束配置
- (RTCMediaConstraints *)videoConstraints {return [[RTCMediaConstraints alloc] initWithMandatoryConstraints:@[[[RTCPair alloc] initWithKey:@"maxWidth" value:@"1280"],[[RTCPair alloc] initWithKey:@"maxHeight" value:@"720"],[[RTCPair alloc] initWithKey:@"maxFrameRate" value:@"30"]] optionalConstraints:@[]];
}// 将 SessionDescription 转换为字符串
- (NSString *)sessionDescriptionToString:(RTCSessionDescription *)sdp {NSDictionary *dict = @{@"type": [self sdpTypeToString:sdp.type],@"sdp": sdp.sdp};NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil];return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}// 将字符串转换为 SessionDescription
- (RTCSessionDescription *)stringToSessionDescription:(NSString *)string type:(RTCSdpType)type {NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];return [[RTCSessionDescription alloc] initWithType:type sdp:dict[@"sdp"]];
}// SDP 类型转换
- (NSString *)sdpTypeToString:(RTCSdpType)type {switch (type) {case RTCSdpTypeOffer: return @"offer";case RTCSdpTypeAnswer: return @"answer";case RTCSdpTypePrAnswer: return @"pranswer";case RTCSdpTypeRollback: return @"rollback";default: return @"";}
}@end

调用OC:ViewController.m

#import "ViewController.h"
#import <WebRTC/WebRTC.h>@interface ViewController ()
@property (nonatomic, strong) WebRTCManager *rtcManager;
@property (nonatomic, strong) UIView *localVideoView;
@property (nonatomic, strong) UIView *remoteVideoView;
@property (nonatomic, strong) UIButton *connectButton;
@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];self.view.backgroundColor = UIColor.whiteColor;[self setupUI];[self setupWebRTC];
}- (void)setupUI {// 本地视频视图_localVideoView = [[UIView alloc] init];_localVideoView.backgroundColor = UIColor.lightGrayColor;_localVideoView.translatesAutoresizingMaskIntoConstraints = NO;[self.view addSubview:_localVideoView];// 远程视频视图_remoteVideoView = [[UIView alloc] init];_remoteVideoView.backgroundColor = UIColor.darkGrayColor;_remoteVideoView.translatesAutoresizingMaskIntoConstraints = NO;[self.view addSubview:_remoteVideoView];// 连接按钮_connectButton = [UIButton buttonWithType:UIButtonTypeSystem];[_connectButton setTitle:@"建立连接" forState:UIControlStateNormal];[_connectButton addTarget:self action:@selector(connectButtonTapped) forControlEvents:UIControlEventTouchUpInside];_connectButton.translatesAutoresizingMaskIntoConstraints = NO;[self.view addSubview:_connectButton];// 布局[NSLayoutConstraint activateConstraints:@[// 本地视频(右上角小窗口)[_localVideoView topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:20],[_localVideoView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20],[_localVideoView.widthAnchor constraintEqualToConstant:120],[_localVideoView.heightAnchor constraintEqualToConstant:180],// 远程视频(全屏)[_remoteVideoView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],[_remoteVideoView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],[_remoteVideoView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],[_remoteVideoView.bottomAnchor constraintEqualToAnchor:self.connectButton.topAnchor constant:-20],// 连接按钮[_connectButton.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor constant:-20],[_connectButton.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],[_connectButton.widthAnchor constraintEqualToConstant:120],[_connectButton.heightAnchor constraintEqualToConstant:44]]];
}- (void)setupWebRTC {_rtcManager = [[WebRTCManager alloc] initWithDelegate:self];[_rtcManager setupLocalStreamWithVideoView:self.localVideoView];
}- (void)connectButtonTapped {[_rtcManager createOffer];
}#pragma mark - WebRTCManagerDelegate// 处理收到的远程流
- (void)didReceiveRemoteStream:(RTCMediaStream *)stream {dispatch_async(dispatch_get_main_queue(), ^{// 渲染远程视频if (stream.videoTracks.count > 0) {RTCVideoTrack *remoteVideoTrack = stream.videoTracks[0];RTCMTLVideoView *rendererView = [[RTCMTLVideoView alloc] init];rendererView.frame = self.remoteVideoView.bounds;rendererView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;[self.remoteVideoView addSubview:rendererView];[remoteVideoTrack addRenderer:[[RTCVideoRendererAdapter alloc] initWithRenderer:rendererView]];}});
}- (void)dealloc {[_rtcManager disconnect];
}@end

代码说明
这个示例实现了 iOS 平台上基于 WebRTC 的基本音视频推拉流功能,主要包含以下部分:
WebRTCManager:核心管理类,负责:

  • 初始化 WebRTC 相关组件
  • 处理本地音视频流的捕获和预览
  • 管理 PeerConnection 连接
  • 处理 SDP 交换和 ICE 候选者

ViewController:界面控制器,负责:

  • 创建本地和远程视频的预览视图
  • 处理用户交互(如建立连接)
  • 渲染远程视频流

三、使用说明

这个示例缺少信令服务器的实现,你需要自己搭建一个信令服务器
在实际使用中,需要将代码中注释掉的信令发送部分替换为实际的网络请求
代码中的 STUN 服务器使用了 Google 的公共服务器,生产环境中建议使用自己的 STUN/TURN 服务器

四、扩展建议

  • 添加错误处理和重连机制
  • 实现多人通话功能
  • 添加视频质量控制
  • 实现屏幕共享功能
  • 添加音频 /video 开关控制

这个示例仅仅提供了基础的推拉流框架,你可以根据实际需求进行扩展和优化。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/916224.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/916224.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

API: return response as HTML table

想要把response table变成HTML的table&#xff0c;即想达到下面这种的话<table boarder"1" style"width:100%; boarder-collapse: collapse; text-align:left"><tr><th>Customer</th><th>Date</th><th>Debit Am…

OneNote 当前无法同步笔记。将继续尝试。 (错误代码: 0xE00009C8 bb0ur)问题解决

之前因为同步错误&#xff0c;导致OneNote一个笔记本内容全部消失&#xff0c;笔记本内容如下图同步状态和错误如下&#xff1a;提醒错误为&#xff1a;OneNote 当前无法同步笔记。将继续尝试。 (错误代码: 0xE00009C8 bb0ur)当时心态有点崩&#xff0c;也是查了好些资料&#…

OneCode3.0 Gallery 组件前后端映射机制:从注解配置到前端渲染的完整链路

一、注解体系与前端组件的映射基础 ​ OneCode Gallery 组件实现了 Java 注解与前端 UI 组件的深度绑定&#xff0c;通过GalleryAnnotation、GalleryItemAnnotation和GalleryViewAnnotation三个核心注解&#xff0c;构建了从后端配置到前端渲染的完整链路。这种映射机制的核心价…

规则分配脚本

需求&#xff1a; 1.根据用户编写的要报规则,去mysql库里SysManage_Rule表获取已经启用的规则作为条件&#xff08;例如[{“field”: “关键词”, “logic”: “AND”, “value”: “阿尔法”, “operator”: “”&#xff0c;, “assign_user”: “user222”}]&#xff09;条…

SEO实战派白杨SEO:SEO中说的框计算、知心搜索(知识图谱)是什么?有什么用处?

SEO里框计算是什么&#xff1f;有什么用处&#xff1f;SEO里框计划算是百度2010年提出的&#xff0c;指当用户搜索某些关键词查询时&#xff0c;搜索引擎在结果页直接展示答案的技术&#xff08;如天气、汇率等&#xff09;&#xff0c;用户无需点击网站即可获取信息&#xff0…

软件工程:软件需求

简介本篇博客记录了我在软件工程学习过程中关于软件需求与面向对象基础知识的学习体会和要点总结。博客共分为三个关卡内容&#xff1a;第1关围绕“软件需求”的定义、分类及分析过程展开&#xff0c;让我清晰地理解了功能性需求、非功能性需求与约束条件的区别&#xff1b;第2…

MES系统是什么,有哪些特性?

MES系统是一套面向制造企业车间执行层的生产信息化管理系统。它能够为操作人员和管理人员提供计划的执行、跟踪以及所有资源&#xff08;包括人、设备、物料、客户需求等&#xff09;的当前状态。通过MES系统可以对从订单下达到产品完成的整个生产过程进行优化管理。当工厂发生…

Vue2下

六&#xff1a;vue-router &#xff08;重要&#xff09; &#xff08;一&#xff09;. 对路由的理解 1.什么是路由 路由&#xff08;Router&#xff09; 是管理页面跳转和 URL 与视图映射关系的机制&#xff0c;核心作用是&#xff1a;根据不同的 URL 路径&#xff0c;展示对…

在 Windows 上安装设置 MongoDB及常见问题

介绍 MongoDB 是一个开源的 NoSQL 数据库系统&#xff0c;它以一种灵活的类似 JSON 的格式&#xff08;称为 BSON&#xff08;二进制 JSON&#xff09;&#xff09;存储数据。它使用动态模式&#xff0c;这意味着与关系型数据库不同&#xff0c;MongoDB 不需要在向数据库添加数…

Effective C++ 条款01:视 C++ 为一个语言联邦

Effective C 条款01&#xff1a;视 C 为一个语言联邦核心思想&#xff1a;C 是由多个子语言组成的联邦&#xff0c;每个子语言有自己的编程范式。理解这些子语言及其规则切换&#xff0c;是写出高效 C 代码的关键。 四个子语言及其规则&#xff1a; C 语言 基础&#xff1a;过程…

云效CI/CD教程(PHP项目)

参考文档 参考云效的官方文档https://help.aliyun.com/zh/yunxiao/ 一、新建代码库 这是第一步&#xff0c;和码云的差不多 二、配SSH密钥 这个和码云&#xff0c;github上类似&#xff0c;都需要&#xff0c;云效的SSH密钥证书不是采用 RSA算法&#xff0c;而是采用了ED2…

单片机是怎么控制的

单片机作为电子系统的控制核心&#xff0c;通过接收外部信号、执行预设程序、驱动外部设备的方式实现控制功能&#xff0c;其控制过程涉及信号输入、数据处理和指令输出三个关键环节&#xff0c;每个环节的协同配合决定了整体控制效果。 信号输入&#xff1a;获取外部信息 单片…

deepseek本地部署,轻松实现编程自由

小伙伴们&#xff0c;大家好&#xff0c;今天我们来实现deepseek本地部署&#xff0c;轻松实现编程自由&#xff01;安装ollama 安装ollama 首先我们安装ollama 打开ollama官网&#xff0c;下载安装符合自己系统的版本。 找到要安装的模型deepseek-r1开始-运行 输入cmd出现…

基础NLP | 常用工具

编辑器 PycharmVSCodeSpyderPython 自带 ideVim 机器学习相关python框架 Pytorch 学术界宠儿&#xff0c;调试方便&#xff0c;目前的主流Tensorflow 大名鼎鼎&#xff0c;工程配套完善Keras 高级封装&#xff0c;简单好用&#xff0c;现已和Tensorflow合体Gensim 训练词向…

Unity3D + VR头显 × RTSP|RTMP播放器:构建沉浸式远程诊疗系统的技术实践

一、背景&#xff1a;远程医疗迈入“沉浸式协同”的新阶段 过去&#xff0c;远程医疗主要依赖视频会议系统&#xff0c;实现基础的远程问诊、会诊或术中指导。虽然初步解决了地域限制问题&#xff0c;但其单视角、平面化、缺乏沉浸感与交互性的特征&#xff0c;已无法满足临床…

海云安斩获“智能金融创新应用“标杆案例 彰显AI安全左移技术创新实力

近日&#xff0c;由中国人民银行广东省分行、广东省金融管理局、广东省政务服务和数据管理局指导&#xff0c;广东省金融科技协会主办的“智能金融 创新应用”优秀案例名单最终揭晓&#xff0c;海云安开发者安全助手系统项目凭借其创新的"AI安全左移"技术架构&#x…

Fluent许可与网络安全策略

在流体动力学模拟领域&#xff0c;Fluent软件因其卓越的性能和广泛的应用而备受用户青睐。然而&#xff0c;随着网络安全威胁的不断增加&#xff0c;确保Fluent许可的安全性和合规性变得尤为重要。本文将探讨Fluent许可与网络安全策略的关系&#xff0c;为您提供一套有效的安全…

如何借助AI工具?打赢通信设备制造的高风险之战?(案例分享)

你是否曾在项目管理中遇到过那种让人心跳加速的瞬间&#xff0c;当一项风险突然暴露出来时&#xff0c;全队似乎都屏住了呼吸&#xff1f;今天&#xff0c;我就来分享一个我亲历的项目案例&#xff0c;讲述我们如何借助具体的AI工具&#xff0c;实现从数据到决策的华丽转变&…

Web服务器(Tomcat、项目部署)

1. 简介 1.1 什么是Web服务器 Web服务器是一个应用程序&#xff08;软件&#xff09;&#xff0c;对HTTP协议的操作进行封装&#xff0c;使得程序员不必直接对协议进行操作&#xff0c;让Web开发更加便捷。主要功能是"提供网上信息浏览服务"。 Web服务器是安装在服…

list 介绍 及 底层

list的相关文档&#xff1a;list - C Reference 一、list的介绍及使用 list中的接口比较多&#xff0c;此处类似&#xff0c;只需要掌握如何正确的使用&#xff0c;然后再去深入研究背后的原理&#xff0c;已达到可扩展的能力。以下为list中一些常见的重要接口。我们库里的list…