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

objectivec 复制代码
#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

objectivec 复制代码
#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 开关控制

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

相关推荐
赛博仓鼠3 小时前
MiniMax H3 实测:8GB显存跑2K视频+原生音频,开源视频生成的新卷王
开源·aigc·音视频
byte轻骑兵4 小时前
【LE Audio】PBP精讲[4]: 公共广播通告的设计逻辑与数据交互流程
人工智能·音视频·le audio·低功耗蓝牙音频
hz567894 小时前
手术室视频示教系统解决方案:让临床教学突破空间限制
安全·音视频·实时音视频·信息与通信·智能硬件
可乐鸡翅yeah_6 小时前
混合内容 Mixed‑Content 安全策略,HLS HTTPS 页面加载 HTTP 资源排错实战
运维·ffmpeg·音视频·媒体·m3u8
揽秀亭长8 小时前
音轨分离处理方案及效果测试分析
人工智能·音视频
美狐美颜SDK开放平台8 小时前
直播APP如何兼顾画质与低延迟?视频美颜SDK性能优化开发思路
人工智能·深度学习·音视频·美颜sdk·第三方美颜sdk
终端安全笔记9 小时前
iOS 27 给了租赁一个新工具,但它只认受监督的设备
android·网络·安全·ios
大熊猫侯佩9 小时前
独立 App 首发完成 iPhone Duo 展开态适配
ios·swiftui·swift
山顶夕景10 小时前
【VLM】Qwen3.8-Omni-Flash长音视频理解Agentic模型
音视频·vlm·视频理解·agentic·全模态
光影少年10 小时前
setImmediate 和 setTimeout(0) 的区别
android·前端·react.js·ios·前端框架