
RTC
【社区精华|持续更新】收录本社区精华内容,手把手教学IM/RTC开发!
IM即时通讯 • admin 发表了文章 • 8 个评论 • 537 次浏览 • 2020-12-07 14:41

本文收录了GeekOnline社区精华内容,希望帮助社区开发者学习IM+RTC知识,解答疑惑。赠人玫瑰,手有余香,如您有不错的内容需要收录,欢迎在在评论区投稿回复。
Android篇
解决融云 SDK 4.0 版本配置 https 导航报 SSLHandshakeException
融云即时通讯SDK集成 — 定制UI(一) ——会话界面小改动
融云即时通讯SDK集成 — 定制UI(二) ——添加自定义表情库
融云即时通讯SDK集成 — 定制UI(三) ——兼容Android Q
融云即时通讯SDK集成 — 国内厂商推送集成踩坑篇(Android平台)
融云 ConversationListFragment 会话列表添加头部布局
融云即时通讯SDK集成 — FCM推送集成指南(Android平台)
iOS篇
集成融云 IMLib 时,如何实现一套类似于 IMKit 的用户信息管理机制
干货分享——使用融云通讯能力库 IMLib 实现单群聊的阅读回执
Web篇
作为小白接融云 IM SDK 新路体验~
微信小程序集成融云 SDK (即时通讯) 集成必备条件
Web 端使用融云 SDK 集成实现滑动加载历史消息
融云IM SDK web 端集成 — 表情采坑篇
融云 Web SDK 如何实现表情的收发 ?
集成融云小程序 SDK 遇到的问题
使用融云 Web SDK 撤回消息
Web 端集成融云 SDK 如何发送正确图片消息给移动端展示?
融云 Web 播放声音 — Flash 篇 (播放 AMR、WAV)
融云 AMR(Aduio) 播放 AMR 格式 Base64 码音频
社区福利
【领取见面礼】限量 100份 GeekOnline加油包!等你来拿
【有奖调研】Geek Online 2020 编程挑战赛参赛调研
【征稿活动】Geek Online 社区第一期投稿激励计划已启动!
GeekOnline编程挑战赛
Geek Online 2020 编程挑战赛 GitHub 仓库
2 个月激烈角逐,15 支队伍突围决赛路演!Geek Online 2020 编程挑战赛完美收官!
一张图回顾 Geek Online 2020 编程挑战赛精彩瞬间!
“这些项目不是什么赚大钱的项目,但是它们足够有趣。”丨关于 Geek Online 2020 编程挑战赛,选手们如是说
融云 CTO 杨攀: Geek Online 2020 编程挑战赛 让开发者站上 C 位
【参赛攻略】你想了解的Geek Online 2020 编程挑战赛常见问题这里都有!
【融云集成常见问题整理】Geek Online 2020 编程挑战赛选手提问整理
求职招聘
持续更新....
超大规模会议技术优化策略 轻松实现 500 人线上流畅沟通
WebRTC • 梅川酷子 发表了文章 • 0 个评论 • 21 次浏览 • 5 天前

1.超大规模会议架构对比
2.超大规模会议中存在的挑战 在超过 20 人会议场景下,SFU 及 WebRTC 兼容场景仍然无法很好的解决。如果直接选择参会人之间进行音视频互动,音视频数据完全转发对服务器资源的要求是巨大的,如果会议中有大量人员同时接入,服务端上行流量和下行流量陡增,会对服务器造成巨大压力。 3.按需订阅与转发以及音频流量优化策略 3.1 按需订阅与转发
The Session Description Protocol includes an optional bandwidth
attribute with the following syntax:
b=<modifier>:<bandwidth-value>
where <modifier> is a single alphanumeric word giving the meaning of
the bandwidth figure, and where the default units for <bandwidth-
value> are kilobits per second. This attribute specifies the
proposed bandwidth to be used by the session or media.
A typical use is with the modifier "AS" (for Application Specific
Maximum) which may be used to specify the total bandwidth for a
single media stream from one site (source).
4. 音频 Top N 选择
// Calculates the normalized RMS value from a mean square value. The input
// should be the sum of squared samples divided by the number of samples. The
// value will be normalized to full range before computing the RMS, wich is
// returned as a negated dBfs. That is, 0 is full amplitude while 127 is very
// faint.
int ComputeRms(float mean_square) {
if (mean_square <= kMinLevel * kMaxSquaredLevel) {
// Very faint; simply return the minimum value.
return RmsLevel::kMinLevelDb;
}
// Normalize by the max level.
const float mean_square_norm = mean_square / kMaxSquaredLevel;
RTC_DCHECK_GT(mean_square_norm, kMinLevel);
// 20log_10(x^0.5) = 10log_10(x)
const float rms = 10.f * log10(mean_square_norm);
RTC_DCHECK_LE(rms, 0.f);
RTC_DCHECK_GT(rms, -RmsLevel::kMinLevelDb);
// Return the negated value.
return static_cast<int>(-rms + 0.5f);
}
void Publisher::Process(RtpAudioPacket packet, AudioLevelHandler handler) {
handler.calculate_queue.enqueue(packet)
RtpAudioPacket packetSend = handler.send_queue.dequeue();
for (对当前Publisher的所有Subscriber subscriber) {
if (subscriber是级联服务器) {
转发packet
} else {
转发packetSend
}
}
}
5. 总结
RTC vs RTMP,适合的才是最好的!
WebRTC • 赵炳东 发表了文章 • 0 个评论 • 143 次浏览 • 2021-01-29 16:17

随着在线教育、电商直播、泛娱乐社交等 App 的普及,实时音视频技术的应用需求也越来越多元化。目前,市场中能够支持音视频通信的主流技术有“RTMP+CDN”和“RTC”两大阵营。选型时,开发者如何根据场景选择更适合自己的通信技术?这就要从两者的技术特点、价格、厂商服务综合考虑。
万人群聊的消息分发控速方案
IM即时通讯 • 徐凤年 发表了文章 • 0 个评论 • 114 次浏览 • 2021-01-21 11:04

当前阶段,群聊已经成为主流IM软件的基本功能,不管是亲属群,朋友群亦或是工作群,都是非常常见的场景。随着移动互联网的发展,即时通讯服务被广泛应用到各个行业,客户业务快速发展,传统百人甚至千人上限的群聊已经无法满足很多业务发展需求,所以超大群的业务应运而生。
1超大群面临的挑战
我们以一个万人群的模型进行举例:
1、如果群中有人发了消息,那么这条消息需要按照1:9999的比例进行分发投递,如果我们按照常规消息的处理流程,那么消息处理服务压力巨大。
2、消息量大的情况下,服务端向客户端直推消息的处理速度将会成为系统瓶颈,而一旦用户的消息下发队列造成了挤压,会影响到正常的消息分发,也会导致服务缓存使用量激增。
3、在微服务架构中,服务以及存储(DB,缓存)之间的QPS和网络流量也会急剧增高。
4、以群为单位的消息缓存,内存和存储开销较大(消息体的存储被放大了万倍)。
基于这些挑战,我们的服务势必要做一定的优化来应对。
2群消息分发模型
整体的群聊服务架构如下图所示:
用户在群里发了一条群消息后,消息先到群组服务,然后通过群组服务缓存的群关系,锁定这条消息最终需要分发的目标用户,然后根据一定的策略分发到消息服务上,消息服务再根据用户的在线状态和消息状态来判断这条消息是直推、通知拉取还是转Push,最终投递给目标用户。
3超大群消息分发解决方案
3.1分发控速:
第一,首先我们会根据服务器的核数来建立多个群消息分发队列,这些队列我们设置了不同的休眠时间以及不同的消费线程数,这里可以理解为快、中、慢等队列。如下图所示:
第二,我们根据群成员数量的大小来将所有群映射到相应的队列中,规则是小群映射到快队列中,大群映射到相应的慢队列中。
第三,小群由于人数少,对服务的影响很小,所以服务利用快队列快速的将群消息分发出去,而大群群消息则利用慢队列的相对高延时来起到控速的作用。
3.2 合并分发:
一条群消息发送到IM服务器后,需要从群组服务投递给消息服务,如果每一个群成员都投递一次,并且投递的群消息内容是一致的话,那肯定会造成相应的资源浪费和服务压力。
服务落点计算中我们使用的是一致性哈希,群成员落点相对固定,所以落点一致的群成员我们可以合并成一次请求进行投递,这样就大幅提高了投递效率同时减少了服务的压力。
3.3 超大规模群的处理方案
在实际群聊业务中,还有一种业务场景是超大规模群,这种群的群人数达到了数十万甚至上百万,这种群如果按照上述的分发方案,势必也会造成消息节点的巨大压力。比如我们有一个十万人的群,消息节点五台,消息服务处理消息的上限是一秒钟4000条,那每台消息节点大约会分到2万条群消息,这超出了消息节点的处理能力。
所以为了避免上述问题,我们的超大群(群成员上线超过3000,可以根据服务器数量和服务器配置相应做调整)会用特殊的队列来处理群消息的分发,这个特殊的队列一秒钟往后端消息服务投递的消息数是消息服务处理上限的一半(留相应的能力处理其他消息),如果单台消息服务处理的QPS上限是4000,那群组服务一秒往单台消息服务最多投递2000条。
结束语
我们后续也会针对群消息进行引用分发,对于大群里发的消息体比较大的消息,我们给群成员只分发和缓存消息的索引,比如MessageID,等群成员真正拉取群消息时再从将消息组装好给客户端分发下去。这样做会节省分发的流量以及存储的空间。
随着互联网的发展,群组业务的模型和压力也在不停地扩展,后续可能还会遇到更多的挑战,届时我们服务器也会通过更优的处理方式来应对。
感兴趣的开发者可以扫码下载融云的 IM 即时通讯 Demo 产品:SealTalk,体验融云的群聊、聊天室等通信能力。
如何实现跨房间连麦功能
WebRTC • admin 发表了文章 • 0 个评论 • 93 次浏览 • 2020-12-24 18:13

在社交娱乐、直播教育等业务场景中,为了增强趣味性和互动性,经常会设计一些主播PK的互动场景,将不同房间的主播拉入同一个房间内进行游戏互动,同时各主播原有房间的观众还能同时观看到自己关注的主播表演并进行打赏等互动,今天就跟大家分享如何实现跨房间连麦功能。
跨房间连麦场景:
场景1.两个直播间中的主播PK
场景2.四个直播间中的主播连麦
场景3.超级小班课
场景示例:
场景一
场景二
场景三
功能点
跨房间邀请过程(邀请,取消邀请,应答,应答通知房间内所有人)
跨房间媒体流合流
加入多RTC房间
订阅多房间人员的音视频流 (监听多房间事件)
支持观众订阅不变的情况能看到新加入的主播
离开副房间(自动取消订阅媒体流)
功能亮点
邀请信令
多房间支持6个房间,再大可以沟通适配
多房间稳定性
跨房间自定义布局
体验 Demo
集成 Demo 示例
融云在 GitHub 上提供了跨房间连麦快速集成 Demo quickdemo-pk 代码示例,方便开发者参考。
跨房间连麦开发文档文档地址
iOS 基于实时音视频 SDK 实现屏幕共享功能——4
WebRTC • admin 发表了文章 • 0 个评论 • 424 次浏览 • 2020-12-03 18:36
1.7 工具类
//
// RongRTCBufferUtil.m
// SealRTC
//
// Created by Sun on 2020/5/8.
// Copyright © 2020 RongCloud. All rights reserved.
//
#import "RongRTCBufferUtil.h"
// 下面的这些方法,一定要记得release,有的没有在方法里面release,但是在外面release了,要不然会内存泄漏
@implementation RongRTCBufferUtil
+ (UIImage *)imageFromBuffer:(CMSampleBufferRef)buffer {
CVPixelBufferRef pixelBuffer = (CVPixelBufferRef)CMSampleBufferGetImageBuffer(buffer);
CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
CIContext *temporaryContext = [CIContext contextWithOptions:nil];
CGImageRef videoImage = [temporaryContext createCGImage:ciImage fromRect:CGRectMake(0, 0, CVPixelBufferGetWidth(pixelBuffer), CVPixelBufferGetHeight(pixelBuffer))];
UIImage *image = [UIImage imageWithCGImage:videoImage];
CGImageRelease(videoImage);
return image;
}
+ (UIImage *)compressImage:(UIImage *)image newWidth:(CGFloat)newImageWidth {
if (!image) return nil;
float imageWidth = image.size.width;
float imageHeight = image.size.height;
float width = newImageWidth;
float height = image.size.height/(image.size.width/width);
float widthScale = imageWidth /width;
float heightScale = imageHeight /height;
UIGraphicsBeginImageContext(CGSizeMake(width, height));
if (widthScale > heightScale) {
[image drawInRect:CGRectMake(0, 0, imageWidth /heightScale , height)];
}
else {
[image drawInRect:CGRectMake(0, 0, width , imageHeight /widthScale)];
}
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
+ (CVPixelBufferRef)CVPixelBufferRefFromUiImage:(UIImage *)img {
CGSize size = img.size;
CGImageRef image = [img CGImage];
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
[NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey, nil];
CVPixelBufferRef pxbuffer = NULL;
CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, size.width, size.height, kCVPixelFormatType_32ARGB, (__bridge CFDictionaryRef) options, &pxbuffer);
NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
CVPixelBufferLockBaseAddress(pxbuffer, 0);
void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
NSParameterAssert(pxdata != NULL);
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(pxdata, size.width, size.height, 8, 4*size.width, rgbColorSpace, kCGImageAlphaPremultipliedFirst);
NSParameterAssert(context);
CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image), CGImageGetHeight(image)), image);
CGColorSpaceRelease(rgbColorSpace);
CGContextRelease(context);
CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
return pxbuffer;
}
+ (CMSampleBufferRef)sampleBufferFromPixbuffer:(CVPixelBufferRef)pixbuffer time:(CMTime)time {
CMSampleBufferRef sampleBuffer = NULL;
//获取视频信息
CMVideoFormatDescriptionRef videoInfo = NULL;
OSStatus result = CMVideoFormatDescriptionCreateForImageBuffer(NULL, pixbuffer, &videoInfo);
CMTime currentTime = time;
// CMSampleTimingInfo timing = {currentTime, currentTime, kCMTimeInvalid};
CMSampleTimingInfo timing = {currentTime, currentTime, kCMTimeInvalid};
result = CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault,pixbuffer, true, NULL, NULL, videoInfo, &timing, &sampleBuffer);
CFRelease(videoInfo);
return sampleBuffer;
}
+ (size_t)getCMTimeSize {
size_t size = sizeof(CMTime);
return size;
}
@end
此工具类中实现是由 CPU 处理,当进行 CMSampleBufferRef 转 UIImage、UIImage 转 CVPixelBufferRef、 CVPixelBufferRef 转 CMSampleBufferRef 以及裁剪图片时,这里需要注意将使用后的对象及时释放,否则会出现内存大量泄漏。
2. 视频发送
2.1 准备阶段
使用融云的 RongRTCLib 的前提需要一个 AppKey,请在官网(https://www.rongcloud.cn/)获取,通过 AppKey 取得 token 之后进行 IM 连接,在连接成功后加入 RTC 房间,这是屏幕共享发送的准备阶段。
- (void)broadcastStartedWithSetupInf(NSDictionary<NSString *,NSObject *> *)setupInfo {
// User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
// 请填写您的 AppKey
self.appKey = @"";
// 请填写用户的 Token
self.token = @"";
// 请指定房间号
self.roomId = @"123456";
[[RCIMClient sharedRCIMClient] initWithAppKey:self.appKey];
[[RCIMClient sharedRCIMClient] setLogLevel:RC_Log_Level_Verbose];
// 连接 IM
[[RCIMClient sharedRCIMClient] connectWithToken:self.token
dbOpened:^(RCDBErrorCode code) {
NSLog(@"dbOpened: %zd", code);
} success:^(NSString *userId) {
NSLog(@"connectWithToken success userId: %@", userId);
// 加入房间
[[RCRTCEngine sharedInstance] joinRoom:self.roomId
completion:^(RCRTCRoom * _Nullable room, RCRTCCode code) {
self.room = room;
self.room.delegate = self;
[self publishScreenStream];
}];
} error:^(RCConnectErrorCode errorCode) {
NSLog(@"ERROR status: %zd", errorCode);
}];
}
如上是连接 IM 和加入 RTC 房间的全过程,其中还包含调用发布自定义视频 [self publishScreenStream]; 此方法在加入房间成功后才可以进行。
- (void)publishScreenStream {
RongRTCStreamParams *param = [[RongRTCStreamParams alloc] init];
param.videoSizePreset = RongRTCVideoSizePreset1280x720;
self.videoOutputStream = [[RongRTCAVOutputStream alloc] initWithParameters:param tag:@"RongRTCScreenVideo"];
[self.room publishAVStream:self.videoOutputStream extra:@"" completion:^(BOOL isSuccess, RongRTCCode desc) {
if (isSuccess) {
NSLog(@"发布自定义流成功");
}
}];
}
自定义一个 RongRTCAVOutputStream 流即可,使用此流发送屏幕共享数据。
2.2 开始发送屏幕共享数据
上面我们已经连接了融云的 IM 和加入了 RTC 房间,并且自定义了一个发送屏幕共享的自定义流,接下来,如何将此流发布出去呢?
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
switch (sampleBufferType) {
case RPSampleBufferTypeVide
// Handle video sample buffer
[self.videoOutputStream write:sampleBuffer error:nil];
break;
case RPSampleBufferTypeAudioApp:
// Handle audio sample buffer for app audio
break;
case RPSampleBufferTypeAudioMic:
// Handle audio sample buffer for mic audio
break;
default:
break;
}
}
但我们接收到了苹果上报的数据之后,调用 RongRTCAVOutputStream 中的 write 方法,将 sampleBuffer 发送给远端,至此,屏幕共享数据就发送出去啦。
[self.videoOutputStream write:sampleBuffer error:nil];
融云的核心代码就是通过上面的连接 IM,加入房间,发布自定义流,然后通过自定义流的 write 方法将 sampleBuffer 发送出去。
不管是通过 ReplayKit 取得屏幕视频,还是使用 Socket 在进程间传输,都是为最终的 write 服务。
总结
Extension 内存是有限制的,最大 50M,所以在 Extension 里面处理数据需要格外注意内存释放;
如果 VideotoolBox 在后台解码一直失败,只需把 VideotoolBox 重启一下即可,此步骤在上面的代码中有体现;
如果不需要将 Extension 的数据传到主 App,只需在 Extension 里直接将流通过 RongRTCLib 发布出去即可,缺点是 Extension 中发布自定义流的用户与主 App 中的用户不是同一个,这也是上面通过 Socket 将数据传递给主 App 要解决的问题;
如果主 App 需要拿到屏幕共享的数据处理,使用 Socket 将流先发给主 App,然后在主 App 里面通过 RongRTCLib 将流发出去。
最后附上 Demo
iOS 基于实时音视频 SDK 实现屏幕共享功能——3
WebRTC • admin 发表了文章 • 0 个评论 • 410 次浏览 • 2020-12-03 18:34

1.5 VideotoolBox 硬编码
//
// RongRTCVideoEncoder.m
// SealRTC
//
// Created by Sun on 2020/5/13.
// Copyright © 2020 RongCloud. All rights reserved.
//
#import "RongRTCVideoEncoder.h"
#import "helpers.h"
@interface RongRTCVideoEncoder() {
VTCompressionSessionRef _compressionSession;
int _frameTime;
}
/**
settings
*/
@property (nonatomic, strong) RongRTCVideoEncoderSettings *settings;
/**
callback queue
*/
@property (nonatomic , strong ) dispatch_queue_t callbackQueue;
- (void)sendSpsAndPPSWithSampleBuffer:(CMSampleBufferRef)sampleBuffer;
- (void)sendNaluData:(CMSampleBufferRef)sampleBuffer;
@end
void compressionOutputCallback(void *encoder,
void *params,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CMSampleBufferRef sampleBuffer) {
RongRTCVideoEncoder *videoEncoder = (__bridge RongRTCVideoEncoder *)encoder;
if (status != noErr) {
return;
}
if (infoFlags & kVTEncodeInfo_FrameDropped) {
return;
}
BOOL isKeyFrame = NO;
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, 0);
if (attachments != nullptr && CFArrayGetCount(attachments)) {
CFDictionaryRef attachment = static_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(attachments, 0)) ;
isKeyFrame = !CFDictionaryContainsKey(attachment, kCMSampleAttachmentKey_NotSync);
}
CMBlockBufferRef block_buffer = CMSampleBufferGetDataBuffer(sampleBuffer);
CMBlockBufferRef contiguous_buffer = nullptr;
if (!CMBlockBufferIsRangeContiguous(block_buffer, 0, 0)) {
status = CMBlockBufferCreateContiguous(nullptr, block_buffer, nullptr, nullptr, 0, 0, 0, &contiguous_buffer);
if (status != noErr) {
return;
}
} else {
contiguous_buffer = block_buffer;
CFRetain(contiguous_buffer);
block_buffer = nullptr;
}
size_t block_buffer_size = CMBlockBufferGetDataLength(contiguous_buffer);
if (isKeyFrame) {
[videoEncoder sendSpsAndPPSWithSampleBuffer:sampleBuffer];
}
if (contiguous_buffer) {
CFRelease(contiguous_buffer);
}
[videoEncoder sendNaluData:sampleBuffer];
}
@implementation RongRTCVideoEncoder
@synthesize settings = _settings;
@synthesize callbackQueue = _callbackQueue;
- (BOOL)configWithSettings:(RongRTCVideoEncoderSettings *)settings onQueue:(nonnull dispatch_queue_t)queue {
self.settings = settings;
if (queue) {
_callbackQueue = queue;
} else {
_callbackQueue = dispatch_get_main_queue();
}
if ([self resetCompressionSession:settings]) {
_frameTime = 0;
return YES;
} else {
return NO;
}
}
- (BOOL)resetCompressionSession:(RongRTCVideoEncoderSettings *)settings {
[self destroyCompressionSession];
OSStatus status = VTCompressionSessionCreate(nullptr, settings.width, settings.height, kCMVideoCodecType_H264, nullptr, nullptr, nullptr, compressionOutputCallback, (__bridge void * _Nullable)(self), &_compressionSession);
if (status != noErr) {
return NO;
}
[self configureCompressionSession:settings];
return YES;
}
- (void)configureCompressionSession:(RongRTCVideoEncoderSettings *)settings {
if (_compressionSession) {
SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_RealTime, true);
SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_AllowFrameReordering, false);
SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, 10);
uint32_t targetBps = settings.startBitrate * 1000;
SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_AverageBitRate, targetBps);
SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, settings.maxFramerate);
int bitRate = settings.width * settings.height * 3 * 4 * 4;
SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_AverageBitRate, bitRate);
int bitRateLimit = settings.width * settings.height * 3 * 4;
SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_DataRateLimits, bitRateLimit);
}
}
- (void)encode:(CMSampleBufferRef)sampleBuffer {
CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
CMTime pts = CMTimeMake(self->_frameTime++, 1000);
VTEncodeInfoFlags flags;
OSStatus res = VTCompressionSessionEncodeFrame(self->_compressionSession,
imageBuffer,
pts,
kCMTimeInvalid,
NULL, NULL, &flags);
if (res != noErr) {
NSLog(@"encode frame error:%d", (int)res);
VTCompressionSessionInvalidate(self->_compressionSession);
CFRelease(self->_compressionSession);
self->_compressionSession = NULL;
return;
}
}
- (void)sendSpsAndPPSWithSampleBuffer:(CMSampleBufferRef)sampleBuffer {
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
const uint8_t *sps ;
const uint8_t *pps;
size_t spsSize ,ppsSize , spsCount,ppsCount;
OSStatus spsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sps, &spsSize, &spsCount, NULL);
OSStatus ppsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pps, &ppsSize, &ppsCount, NULL);
if (spsStatus == noErr && ppsStatus == noErr) {
const char bytes[] = "\x00\x00\x00\x01";
size_t length = (sizeof bytes) - 1;
NSMutableData *spsData = [NSMutableData dataWithCapacity:4+ spsSize];
NSMutableData *ppsData = [NSMutableData dataWithCapacity:4 + ppsSize];
[spsData appendBytes:bytes length:length];
[spsData appendBytes:sps length:spsSize];
[ppsData appendBytes:bytes length:length];
[ppsData appendBytes:pps length:ppsSize];
if (self && self.callbackQueue) {
dispatch_async(self.callbackQueue, ^{
if (self.delegate && [self.delegate respondsToSelector:@selector(spsData:ppsData:)]) {
[self.delegate spsData:spsData ppsData:ppsData];
}
});
}
} else {
NSLog(@"sps status:%@, pps status:%@", @(spsStatus), @(ppsStatus));
}
}
- (void)sendNaluData:(CMSampleBufferRef)sampleBuffer {
size_t totalLength = 0;
size_t lengthAtOffset=0;
char *dataPointer;
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
OSStatus status1 = CMBlockBufferGetDataPointer(blockBuffer, 0, &lengthAtOffset, &totalLength, &dataPointer);
if (status1 != noErr) {
NSLog(@"video encoder error, status = %d", (int)status1);
return;
}
static const int h264HeaderLength = 4;
size_t bufferOffset = 0;
while (bufferOffset < totalLength - h264HeaderLength) {
uint32_t naluLength = 0;
memcpy(&naluLength, dataPointer + bufferOffset, h264HeaderLength);
naluLength = CFSwapInt32BigToHost(naluLength);
const char bytes[] = "\x00\x00\x00\x01";
NSMutableData *naluData = [NSMutableData dataWithCapacity:4 + naluLength];
[naluData appendBytes:bytes length:4];
[naluData appendBytes:dataPointer + bufferOffset + h264HeaderLength length:naluLength];
dispatch_async(self.callbackQueue, ^{
if (self.delegate && [self.delegate respondsToSelector:@selector(naluData:)]) {
[self.delegate naluData:naluData];
}
});
bufferOffset += naluLength + h264HeaderLength;
}
}
- (void)destroyCompressionSession {
if (_compressionSession) {
VTCompressionSessionInvalidate(_compressionSession);
CFRelease(_compressionSession);
_compressionSession = nullptr;
}
}
- (void)dealloc {
if (_compressionSession) {
VTCompressionSessionCompleteFrames(_compressionSession, kCMTimeInvalid);
VTCompressionSessionInvalidate(_compressionSession);
CFRelease(_compressionSession);
_compressionSession = NULL;
}
}
@end
1.6 VideotoolBox 解码
//
// RongRTCVideoDecoder.m
// SealRTC
//
// Created by Sun on 2020/5/14.
// Copyright © 2020 RongCloud. All rights reserved.
//
#import "RongRTCVideoDecoder.h"
#import <UIKit/UIKit.h>
#import "helpers.h"
@interface RongRTCVideoDecoder() {
uint8_t *_sps;
NSUInteger _spsSize;
uint8_t *_pps;
NSUInteger _ppsSize;
CMVideoFormatDescriptionRef _videoFormatDescription;
VTDecompressionSessionRef _decompressionSession;
}
/**
settings
*/
@property (nonatomic, strong) RongRTCVideoEncoderSettings *settings;
/**
callback queue
*/
@property (nonatomic, strong) dispatch_queue_t callbackQueue;
@end
void DecoderOutputCallback(void * CM_NULLABLE decompressionOutputRefCon,
void * CM_NULLABLE sourceFrameRefCon,
OSStatus status,
VTDecodeInfoFlags infoFlags,
CM_NULLABLE CVImageBufferRef imageBuffer,
CMTime presentationTimeStamp,
CMTime presentationDuration ) {
if (status != noErr) {
NSLog(@" decoder callback error :%@", @(status));
return;
}
CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;
*outputPixelBuffer = CVPixelBufferRetain(imageBuffer);
RongRTCVideoDecoder *decoder = (__bridge RongRTCVideoDecoder *)(decompressionOutputRefCon);
dispatch_async(decoder.callbackQueue, ^{
[decoder.delegate didGetDecodeBuffer:imageBuffer];
CVPixelBufferRelease(imageBuffer);
});
}
@implementation RongRTCVideoDecoder
@synthesize settings = _settings;
@synthesize callbackQueue = _callbackQueue;
- (BOOL)configWithSettings:(RongRTCVideoEncoderSettings *)settings onQueue:(dispatch_queue_t)queue {
self.settings = settings;
if (queue) {
_callbackQueue = queue;
} else {
_callbackQueue = dispatch_get_main_queue();
}
return YES;
}
- (BOOL)createVT {
if (_decompressionSession) {
return YES;
}
const uint8_t * const parameterSetPointers[2] = {_sps, _pps};
const size_t parameterSetSizes[2] = {_spsSize, _ppsSize};
int naluHeaderLen = 4;
OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, parameterSetPointers, parameterSetSizes, naluHeaderLen, &_videoFormatDescription );
if (status != noErr) {
NSLog(@"CMVideoFormatDescriptionCreateFromH264ParameterSets error:%@", @(status));
return false;
}
NSDictionary *destinationImageBufferAttributes =
@{
(id)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange],
(id)kCVPixelBufferWidthKey: [NSNumber numberWithInteger:self.settings.width],
(id)kCVPixelBufferHeightKey: [NSNumber numberWithInteger:self.settings.height],
(id)kCVPixelBufferOpenGLCompatibilityKey: [NSNumber numberWithBool:true]
};
VTDecompressionOutputCallbackRecord CallBack;
CallBack.decompressionOutputCallback = DecoderOutputCallback;
CallBack.decompressionOutputRefCon = (__bridge void * _Nullable)(self);
status = VTDecompressionSessionCreate(kCFAllocatorDefault, _videoFormatDescription, NULL, (__bridge CFDictionaryRef _Nullable)(destinationImageBufferAttributes), &CallBack, &_decompressionSession);
if (status != noErr) {
NSLog(@"VTDecompressionSessionCreate error:%@", @(status));
return false;
}
status = VTSessionSetProperty(_decompressionSession, kVTDecompressionPropertyKey_RealTime,kCFBooleanTrue);
return YES;
}
- (CVPixelBufferRef)decode:(uint8_t *)frame withSize:(uint32_t)frameSize {
CVPixelBufferRef outputPixelBuffer = NULL;
CMBlockBufferRef blockBuffer = NULL;
CMBlockBufferFlags flag0 = 0;
OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, frame, frameSize, kCFAllocatorNull, NULL, 0, frameSize, flag0, &blockBuffer);
if (status != kCMBlockBufferNoErr) {
NSLog(@"VCMBlockBufferCreateWithMemoryBlock code=%d", (int)status);
CFRelease(blockBuffer);
return outputPixelBuffer;
}
CMSampleBufferRef sampleBuffer = NULL;
const size_t sampleSizeArray[] = {frameSize};
status = CMSampleBufferCreateReady(kCFAllocatorDefault, blockBuffer, _videoFormatDescription, 1, 0, NULL, 1, sampleSizeArray, &sampleBuffer);
if (status != noErr || !sampleBuffer) {
NSLog(@"CMSampleBufferCreateReady failed status=%d", (int)status);
CFRelease(blockBuffer);
return outputPixelBuffer;
}
VTDecodeFrameFlags flag1 = kVTDecodeFrame_1xRealTimePlayback;
VTDecodeInfoFlags infoFlag = kVTDecodeInfo_Asynchronous;
status = VTDecompressionSessionDecodeFrame(_decompressionSession, sampleBuffer, flag1, &outputPixelBuffer, &infoFlag);
if (status == kVTInvalidSessionErr) {
NSLog(@"decode frame error with session err status =%d", (int)status);
[self resetVT];
} else {
if (status != noErr) {
NSLog(@"decode frame error with status =%d", (int)status);
}
}
CFRelease(sampleBuffer);
CFRelease(blockBuffer);
return outputPixelBuffer;
}
- (void)resetVT {
[self destorySession];
[self createVT];
}
- (void)decode:(NSData *)data {
uint8_t *frame = (uint8_t*)[data bytes];
uint32_t length = data.length;
uint32_t nalSize = (uint32_t)(length - 4);
uint32_t *pNalSize = (uint32_t *)frame;
*pNalSize = CFSwapInt32HostToBig(nalSize);
int type = (frame[4] & 0x1F);
CVPixelBufferRef pixelBuffer = NULL;
switch (type) {
case 0x05:
if ([self createVT]) {
pixelBuffer= [self decode:frame withSize:length];
}
break;
case 0x07:
self->_spsSize = length - 4;
self->_sps = (uint8_t *)malloc(self->_spsSize);
memcpy(self->_sps, &frame[4], self->_spsSize);
break;
case 0x08:
self->_ppsSize = length - 4;
self->_pps = (uint8_t *)malloc(self->_ppsSize);
memcpy(self->_pps, &frame[4], self->_ppsSize);
break;
default:
if ([self createVT]) {
pixelBuffer = [self decode:frame withSize:length];
}
break;
}
}
- (void)dealloc {
[self destorySession];
}
- (void)destorySession {
if (_decompressionSession) {
VTDecompressionSessionInvalidate(_decompressionSession);
CFRelease(_decompressionSession);
_decompressionSession = NULL;
}
}
@end
iOS 基于实时音视频 SDK 实现屏幕共享功能——2
WebRTC • admin 发表了文章 • 0 个评论 • 402 次浏览 • 2020-12-03 18:26

这里的核心思想是拿到屏幕共享的数据之后,先进行压缩,当压缩完成后会通过回调上报给当前类。既而通过 clientSend
方法,发给主 App。发给主 App 的数据中自定义了一个头部,头部添加了一个前缀和一个每次发送字节的长度,当接收端收到数据包后解析即可。
- (void)clientSend:(NSData *)data {
//data length
NSUInteger dataLength = data.length;
// data header struct
DataHeader dataH;
memset((void *)&dataH, 0, sizeof(dataH));
// pre
PreHeader preH;
memset((void *)&preH, 0, sizeof(preH));
preH.pre[0] = '&';
preH.dataLength = dataLength;
dataH.preH = preH;
// buffer
int headerlength = sizeof(dataH);
int totalLength = dataLength + headerlength;
// srcbuffer
Byte *src = (Byte *)[data bytes];
// send buffer
char *buffer = (char *)malloc(totalLength * sizeof(char));
memcpy(buffer, &dataH, headerlength);
memcpy(buffer + headerlength, src, dataLength);
// to send
[self sendBytes:buffer length:totalLength];
free(buffer);
}
1.4 接收屏幕共享数据
//
// RongRTCServerSocket.m
// SealRTC
//
// Created by Sun on 2020/5/7.
// Copyright © 2020 RongCloud. All rights reserved.
//
#import "RongRTCServerSocket.h"
#import <arpa/inet.h>
#import <netdb.h>
#import <sys/types.h>
#import <sys/socket.h>
#import <ifaddrs.h>
#import <UIKit/UIKit.h>
#import "RongRTCThread.h"
#import "RongRTCSocketHeader.h"
#import "RongRTCVideoDecoder.h"
@interface RongRTCServerSocket() <RongRTCCodecProtocol>
{
pthread_mutex_t lock;
int _frameTime;
CMTime _lastPresentationTime;
Float64 _currentMediaTime;
Float64 _currentVideoTime;
dispatch_queue_t _frameQueue;
}
@property (nonatomic, assign) int acceptSocket;
/**
data length
*/
@property (nonatomic, assign) NSUInteger dataLength;
/**
timeData
*/
@property (nonatomic, strong) NSData *timeData;
/**
decoder queue
*/
@property (nonatomic, strong) dispatch_queue_t decoderQueue;
/**
decoder
*/
@property (nonatomic, strong) RongRTCVideoDecoder *decoder;
@end
@implementation RongRTCServerSocket
- (BOOL)createServerSocket {
if ([self createSocket] == -1) {
return NO;
}
[self setReceiveBuffer];
[self setReceiveTimeout];
BOOL isB = [self bind];
BOOL isL = [self listen];
if (isB && isL) {
_decoderQueue = dispatch_queue_create("cn.rongcloud.decoderQueue", NULL);
_frameTime = 0;
[self createDecoder];
[self receive];
return YES;
} else {
return NO;
}
}
- (void)createDecoder {
self.decoder = [[RongRTCVideoDecoder alloc] init];
self.decoder.delegate = self;
RongRTCVideoEncoderSettings *settings = [[RongRTCVideoEncoderSettings alloc] init];
settings.width = 720;
settings.height = 1280;
settings.startBitrate = 300;
settings.maxFramerate = 30;
settings.minBitrate = 1000;
[self.decoder configWithSettings:settings onQueue:_decoderQueue];
}
- (void)receiveData {
struct sockaddr_in rest;
socklen_t rest_size = sizeof(struct sockaddr_in);
self.acceptSocket = accept(self.socket, (struct sockaddr *) &rest, &rest_size);
while (self.acceptSocket != -1) {
DataHeader dataH;
memset(&dataH, 0, sizeof(dataH));
if (![self receiveData:(char *)&dataH length:sizeof(dataH)]) {
continue;
}
PreHeader preH = dataH.preH;
char pre = preH.pre[0];
if (pre == '&') {
// rongcloud socket
NSUInteger dataLenght = preH.dataLength;
char *buff = (char *)malloc(sizeof(char) * dataLenght);
if ([self receiveData:(char *)buff length:dataLenght]) {
NSData *data = [NSData dataWithBytes:buff length:dataLenght];
[self.decoder decode:data];
free(buff);
}
} else {
NSLog(@"pre is not &");
return;
}
}
}
- (BOOL)receiveData:(char *)data length:(NSUInteger)length {
LOCK(lock);
int receiveLength = 0;
while (receiveLength < length) {
ssize_t res = recv(self.acceptSocket, data, length - receiveLength, 0);
if (res == -1 || res == 0) {
UNLOCK(lock);
NSLog(@"receive data error");
break;
}
receiveLength += res;
data += res;
}
UNLOCK(lock);
return YES;
}
- (void)didGetDecodeBuffer:(CVPixelBufferRef)pixelBuffer {
_frameTime += 1000;
CMTime pts = CMTimeMake(_frameTime, 1000);
CMSampleBufferRef sampleBuffer = [RongRTCBufferUtil sampleBufferFromPixbuffer:pixelBuffer time:pts];
// Check to see if there is a problem with the decoded data. If the image appears, you are right.
UIImage *image = [RongRTCBufferUtil imageFromBuffer:sampleBuffer];
[self.delegate didProcessSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
}
- (void)close {
int res = close(self.acceptSocket);
self.acceptSocket = -1;
NSLog(@"shut down server: %d", res);
[super close];
}
- (void)dealloc {
NSLog(@"dealoc server socket");
}
@end
主 App 通过 Socket
会持续收到数据包,再将数据包进行解码,将解码后的数据通过代理 didGetDecodeBuffer
代理方法回调给 App 层。App 层就可以通过融云 RongRTCLib
的发送自定义流方法将视频数据发送到对端。
iOS 基于实时音视频 SDK 实现屏幕共享功能——1
WebRTC • admin 发表了文章 • 0 个评论 • 412 次浏览 • 2020-12-03 18:21

Replaykit 介绍
在之前的 iOS 版本中,iOS 开发者只能拿到编码后的数据,拿不到原始的 PCM 和 YUV,到 iOS 10 之后,开发者可以拿到原始数据,但是只能录制 App 内的内容,如果切到后台,将停止录制,直到 iOS 11,苹果对屏幕共享进行了升级并开放了权限,既可以拿到原始数据,又可以录制整个系统,以下我们重点来说 iOS 11 之后的屏幕共享功能。
系统屏幕共享
- (void)initMode_1 {
self.systemBroadcastPickerView = [[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(0, 64, ScreenWidth, 80)];
self.systemBroadcastPickerView.preferredExtension = @"cn.rongcloud.replaytest.Recoder";
self.systemBroadcastPickerView.backgroundColor = [UIColor colorWithRed:53.0/255.0 green:129.0/255.0 blue:242.0/255.0 alpha:1.0];
self.systemBroadcastPickerView.showsMicrophoneButton = NO;
[self.view addSubview:self.systemBroadcastPickerView];
}
在 iOS 11 创建一个 Extension
之后,调用上面的代码就可以开启屏幕共享了,然后系统会为我们生成一个 SampleHandler
的类,在这个方法中,苹果会根据 RPSampleBufferType
上报不同类型的数据。
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType
那怎么通过融云的 RongRTCLib
将屏幕共享数据发送出去呢?
1. 基于 Socket 的逼格玩法
1.1. Replaykit 框架启动和创建 Socket
//
// ViewController.m
// Socket_Replykit
//
// Created by Sun on 2020/5/19.
// Copyright © 2020 RongCloud. All rights reserved.
//
#import "ViewController.h"
#import <ReplayKit/ReplayKit.h>
#import "RongRTCServerSocket.h"
@interface ViewController ()<RongRTCServerSocketProtocol>
@property (nonatomic, strong) RPSystemBroadcastPickerView *systemBroadcastPickerView;
/**
server socket
*/
@property(nonatomic , strong)RongRTCServerSocket *serverSocket;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// Do any additional setup after loading the view.
[self.serverSocket createServerSocket];
self.systemBroadcastPickerView = [[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(0, 64, [UIScreen mainScreen].bounds.size.width, 80)];
self.systemBroadcastPickerView.preferredExtension = @"cn.rongcloud.sealrtc.RongRTCRP";
self.systemBroadcastPickerView.backgroundColor = [UIColor colorWithRed:53.0/255.0 green:129.0/255.0 blue:242.0/255.0 alpha:1.0];
self.systemBroadcastPickerView.showsMicrophoneButton = NO;
[self.view addSubview:self.systemBroadcastPickerView];
}
- (RongRTCServerSocket *)serverSocket {
if (!_serverSocket) {
RongRTCServerSocket *socket = [[RongRTCServerSocket alloc] init];
socket.delegate = self;
_serverSocket = socket;
}
return _serverSocket;
}
- (void)didProcessSampleBuffer:(CMSampleBufferRef)sampleBuffer {
// 这里拿到了最终的数据,比如最后可以使用融云的音视频SDK RTCLib 进行传输就可以了
}
@end
其中,包括了创建 Server Socket
的步骤,我们把主 App 当做 Server
,然后屏幕共享 Extension
当做 Client
,通过 Socket
向我们的主 APP 发送数据。
在 Extension
里面,我们拿到 ReplayKit
框架上报的屏幕视频数据后:
//
// SampleHandler.m
// SocketReply
//
// Created by Sun on 2020/5/19.
// Copyright © 2020 RongCloud. All rights reserved.
//
#import "SampleHandler.h"
#import "RongRTCClientSocket.h"
@interface SampleHandler()
/**
Client Socket
*/
@property (nonatomic, strong) RongRTCClientSocket *clientSocket;
@end
@implementation SampleHandler
- (void)broadcastStartedWithSetupInf(NSDictionary<NSString *,NSObject *> *)setupInfo {
// User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
self.clientSocket = [[RongRTCClientSocket alloc] init];
[self.clientSocket createCliectSocket];
}
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
switch (sampleBufferType) {
case RPSampleBufferTypeVide
// Handle video sample buffer
[self sendData:sampleBuffer];
break;
case RPSampleBufferTypeAudioApp:
// Handle audio sample buffer for app audio
break;
case RPSampleBufferTypeAudioMic:
// Handle audio sample buffer for mic audio
break;
default:
break;
}
}
- (void)sendData:(CMSampleBufferRef)sampleBuffer {
[self.clientSocket encodeBuffer:sampleBuffer];
}
@end
可见 ,这里我们创建了一个 Client Socket
,然后拿到屏幕共享的视频 sampleBuffer
之后,通过 Socket
发给我们的主 App,这就是屏幕共享的流程。
1.2 Local Socket 的使用
//
// RongRTCSocket.m
// SealRTC
//
// Created by Sun on 2020/5/7.
// Copyright © 2020 RongCloud. All rights reserved.
//
#import "RongRTCSocket.h"
#import <arpa/inet.h>
#import <netdb.h>
#import <sys/types.h>
#import <sys/socket.h>
#import <ifaddrs.h>
#import "RongRTCThread.h"
@interface RongRTCSocket()
/**
receive thread
*/
@property (nonatomic, strong) RongRTCThread *receiveThread;
@end
@implementation RongRTCSocket
- (int)createSocket {
int socket = socket(AF_INET, SOCK_STREAM, 0);
self.socket = socket;
if (self.socket == -1) {
close(self.socket);
NSLog(@"socket error : %d", self.socket);
}
self.receiveThread = [[RongRTCThread alloc] init];
[self.receiveThread run];
return socket;
}
- (void)setSendBuffer {
int optVal = 1024 * 1024 * 2;
int optLen = sizeof(int);
int res = setsockopt(self.socket, SOL_SOCKET, SO_SNDBUF, (char *)&optVal,optLen);
NSLog(@"set send buffer:%d", res);
}
- (void)setReceiveBuffer {
int optVal = 1024 * 1024 * 2;
int optLen = sizeof(int);
int res = setsockopt(self.socket, SOL_SOCKET, SO_RCVBUF, (char*)&optVal,optLen );
NSLog(@"set send buffer:%d",res);
}
- (void)setSendingTimeout {
struct timeval timeout = {10,0};
int res = setsockopt(self.socket, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(int));
NSLog(@"set send timeout:%d", res);
}
- (void)setReceiveTimeout {
struct timeval timeout = {10, 0};
int res = setsockopt(self.socket, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(int));
NSLog(@"set send timeout:%d", res);
}
- (BOOL)connect {
NSString *serverHost = [self ip];
struct hostent *server = gethostbyname([serverHost UTF8String]);
if (server == NULL) {
close(self.socket);
NSLog(@"get host error");
return NO;
}
struct in_addr *remoteAddr = (struct in_addr *)server->h_addr_list[0];
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr = *remoteAddr;
addr.sin_port = htons(CONNECTPORT);
int res = connect(self.socket, (struct sockaddr *) &addr, sizeof(addr));
if (res == -1) {
close(self.socket);
NSLog(@"connect error");
return NO;
}
NSLog(@"socket connect to server success");
return YES;
}
- (BOOL)bind {
struct sockaddr_in client;
client.sin_family = AF_INET;
NSString *ipStr = [self ip];
if (ipStr.length <= 0) {
return NO;
}
const char *ip = [ipStr cStringUsingEncoding:NSASCIIStringEncoding];
client.sin_addr.s_addr = inet_addr(ip);
client.sin_port = htons(CONNECTPORT);
int bd = bind(self.socket, (struct sockaddr *) &client, sizeof(client));
if (bd == -1) {
close(self.socket);
NSLog(@"bind error: %d", bd);
return NO;
}
return YES;
}
- (BOOL)listen {
int ls = listen(self.socket, 128);
if (ls == -1) {
close(self.socket);
NSLog(@"listen error: %d", ls);
return NO;
}
return YES;
}
- (void)receive {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self receiveData];
});
}
- (NSString *)ip {
NSString *ip = nil;
struct ifaddrs *addrs = NULL;
struct ifaddrs *tmpAddrs = NULL;
BOOL res = getifaddrs(&addrs);
if (res == 0) {
tmpAddrs = addrs;
while (tmpAddrs != NULL) {
if (tmpAddrs->ifa_addr->sa_family == AF_INET) {
// Check if interface is en0 which is the wifi connection on the iPhone
NSLog(@"%@", [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)tmpAddrs->ifa_addr)->sin_addr)]);
if ([[NSString stringWithUTF8String:tmpAddrs->ifa_name] isEqualToString:@"en0"]) {
// Get NSString from C String
ip = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)tmpAddrs->ifa_addr)->sin_addr)];
}
}
tmpAddrs = tmpAddrs->ifa_next;
}
}
// Free memory
freeifaddrs(addrs);
NSLog(@"%@",ip);
return ip;
}
- (void)close {
int res = close(self.socket);
NSLog(@"shut down: %d", res);
}
- (void)receiveData {
}
- (void)dealloc {
[self.receiveThread stop];
}
@end
首先创建了一个 Socket
的父类,然后用 Server Socket
和 Client Socket
分别继承类来实现链接、绑定等操作。可以看到有些数据可以设置,有些则不用,这里不是核心,核心是怎样收发数据。
1.3 发送屏幕共享数据
//
// RongRTCClientSocket.m
// SealRTC
//
// Created by Sun on 2020/5/7.
// Copyright © 2020 RongCloud. All rights reserved.
//
#import "RongRTCClientSocket.h"
#import <arpa/inet.h>
#import <netdb.h>
#import <sys/types.h>
#import <sys/socket.h>
#import <ifaddrs.h>
#import "RongRTCThread.h"
#import "RongRTCSocketHeader.h"
#import "RongRTCVideoEncoder.h"
@interface RongRTCClientSocket() <RongRTCCodecProtocol> {
pthread_mutex_t lock;
}
/**
video encoder
*/
@property (nonatomic, strong) RongRTCVideoEncoder *encoder;
/**
encode queue
*/
@property (nonatomic, strong) dispatch_queue_t encodeQueue;
@end
@implementation RongRTCClientSocket
- (BOOL)createClientSocket {
if ([self createSocket] == -1) {
return NO;
}
BOOL isC = [self connect];
[self setSendBuffer];
[self setSendingTimeout];
if (isC) {
_encodeQueue = dispatch_queue_create("cn.rongcloud.encodequeue", NULL);
[self createVideoEncoder];
return YES;
} else {
return NO;
}
}
- (void)createVideoEncoder {
self.encoder = [[RongRTCVideoEncoder alloc] init];
self.encoder.delegate = self;
RongRTCVideoEncoderSettings *settings = [[RongRTCVideoEncoderSettings alloc] init];
settings.width = 720;
settings.height = 1280;
settings.startBitrate = 300;
settings.maxFramerate = 30;
settings.minBitrate = 1000;
[self.encoder configWithSettings:settings onQueue:_encodeQueue];
}
- (void)clientSend:(NSData *)data {
//data length
NSUInteger dataLength = data.length;
// data header struct
DataHeader dataH;
memset((void *)&dataH, 0, sizeof(dataH));
// pre
PreHeader preH;
memset((void *)&preH, 0, sizeof(preH));
preH.pre[0] = '&';
preH.dataLength = dataLength;
dataH.preH = preH;
// buffer
int headerlength = sizeof(dataH);
int totalLength = dataLength + headerlength;
// srcbuffer
Byte *src = (Byte *)[data bytes];
// send buffer
char *buffer = (char *)malloc(totalLength * sizeof(char));
memcpy(buffer, &dataH, headerlength);
memcpy(buffer + headerlength, src, dataLength);
// tosend
[self sendBytes:buffer length:totalLength];
free(buffer);
}
- (void)encodeBuffer:(CMSampleBufferRef)sampleBuffer {
[self.encoder encode:sampleBuffer];
}
- (void)sendBytes:(char *)bytes length:(int)length {
LOCK(self->lock);
int hasSendLength = 0;
while (hasSendLength < length) {
// connect socket success
if (self.socket > 0) {
// send
int sendRes = send(self.socket, bytes, length - hasSendLength, 0);
if (sendRes == -1 || sendRes == 0) {
UNLOCK(self->lock);
NSLog(@"send buffer error");
[self close];
break;
}
hasSendLength += sendRes;
bytes += sendRes;
} else {
NSLog(@"client socket connect error");
UNLOCK(self->lock);
}
}
UNLOCK(self->lock);
}
- (void)spsData:(NSData *)sps ppsData:(NSData *)pps {
[self clientSend:sps];
[self clientSend:pps];
}
- (void)naluData:(NSData *)naluData {
[self clientSend:naluData];
}
- (void)deallo c{
NSLog(@"dealoc cliect socket");
}
@end
前端音视频之WebRTC初探
WebRTC • 大兴 发表了文章 • 0 个评论 • 138 次浏览 • 2020-10-22 17:35

今天,我们来一起学习一下 WebRTC,相信你已经对这个前端音视频网红儿有所耳闻了。
WebRTC Web Real-Time Communication 网页即时通信
WebRTC 于 2011 年 6 月 1 日开源,并在 Google、Mozilla、Opera 等大佬们的支持下被纳入 W3C 推荐标准,它给浏览器和移动应用提供了即时通信的能力。
WebRTC 优势及应用场景
优势
跨平台(Web、Windows、MacOS、Linux、iOS、Android) 实时传输 音视频引擎 免费、免插件、免安装 主流浏览器支持 强大的打洞能力
应用场景
在线教育、在线医疗、音视频会议、即时通讯工具、直播、共享远程桌面、P2P网络加速、游戏(狼人杀、线上KTV)等。
(有喜欢玩狼人杀的同学吗?有时间可以一起来一局,给我一轮听发言的时间,给你裸点狼坑,一个坑容错。)
WebRTC 整体架构
拉回来,我们看一看 WebRTC 的整体架构,我用不同的颜色标识出了各层级所代表的含义。
Web 应用 Web API WebRTC C++ API Session Management 信令管理 Transport 传输层 Voice Engine 音频引擎 Video Engine 视频处理引擎
我们再来看下核心的模块:
Voice Engine 音频引擎
VoIP 软件开发商 Global IP Solutions 提供的 GIPS 引擎可以说是世界上最好的语音引擎,谷歌大佬一举将其收购并开源,也就是 WebRTC 中的 音频引擎。
iSAC:WebRTC 音频引擎的默认编解码器,针对 VoIP 和音频流的宽带和超宽带音频编解码器。 iLBC:VoIP 音频流的窄带语音编解码器。 NetEQ For Voice:针对音频软件实现的语音信号处理元件。NetEQ 算法是自适应抖动控制算法以及语音包丢失隐藏算法,能够有效的处理网络抖动和语音包丢失时对语音质量产生的影响。 Acoustic Echo Canceler:AEC,回声消除器。 Noise Reduction:NR,噪声抑制。
Video Engine 视频处理引擎
VPx 系列视频编解码器是 Google 大佬收购 ON2 公司后开源的。
VP8:视频图像编解码器,WebRTC 视频引擎默认的编解码器。 Video Jitter Buffer:视频抖动缓冲器模块。 Image Enhancements:图像质量增强模块。
WebRTC 通信原理
媒体协商
媒体协商也就是让双方可以找到共同支持的媒体能力,比如双方都支持的编解码器,这样才能实现彼此之间的音视频通信。
SDP Session Description Protocal
媒体协商所交换的数据就是 SDP,说是协议,其实 SDP 并不是一个真正的协议,它就是一种描述各端“能力”的数据格式。
上图所示就是 SDP 的一部分,详细内容请参考:SDP: Session Description Protocol
https://tools.ietf.org/html/rfc4566
或者参考卡神的这篇文章:WebRTC:会话描述协议SDP
https://zhuanlan.zhihu.com/p/75492311
网络协商
ICE Interactive Connectivity Establishment 互动式连接建立
想要建立连接,我们要需要拿到双方 IP 和端口的信息,在当下复杂的网络环境下,ICE 统一了各种 NAT 穿越技术(STUN、TURN),可以让客户端成功地穿透远程用户与网络之间可能存在的各类防火墙。
STUN、TURN
STUN:简单 UDP 穿透 NAT,可以使位于 NAT(或多重 NAT) 后的客户端找出自己的公网 IP 地址,以及查出自己位于哪种类型的 NAT 及 NAT 所绑定的 Internet 端口。
我们知道,NAT 主要有以下四个种类:
完全锥型 NAT IP 限制锥型 端口限制锥型 对称型
前三种都可以使用 STUN 穿透,而面对第四种类型,也是大型公司网络中经常采用的对称型 NAT ,这时的路由器只会接受之前连线过的节点所建立的连线。
那么想要处理这种网络情况,我们就需要使用 TURN (中继穿透 NAT) 技术。
TURN 是 STUN 的一个扩展,其主要添加了中继功能。在 STUN 服务器的基础上,再添加几台 TURN 服务器,如果 STUN 分配公网 IP 失败,则可以通过 TURN 服务器请求公网 IP 地址作为中继地址,将媒体数据通过 TURN 服务器进行中转。
信令服务器 Signal Server
拿到了双方的媒体信息(SDP)和网络信息(Candidate)后,我们还需要一台信令服务器作为中间商来转发交换它们。
信令服务器还可以实现一些 IM 功能,比如房间管理,用户进入、退出等。
小结
本文我们了解了 WebRTC 优势及应用场景、WebRTC 的整体架构及主要模块构成以及 WebRTC 的通信原理。这些基础知识和概念是需要我们牢记的,大家要记牢~
参考
《从 0 打造音视频直播系统》 李超 《WebRTC 音视频开发 React+Flutter+Go 实战》 亢少军 https://webrtc.github.io/webrtc-org/architecture/ https://developer.mozilla.org/zh-CN/docs/Web/API/WebRTC_API https://www.w3.org/TR/webrtc/
本文转自公众号“前端食堂”,作者霍语佳
【社区精华|持续更新】收录本社区精华内容,手把手教学IM/RTC开发!
IM即时通讯 • admin 发表了文章 • 8 个评论 • 537 次浏览 • 2020-12-07 14:41

本文收录了GeekOnline社区精华内容,希望帮助社区开发者学习IM+RTC知识,解答疑惑。赠人玫瑰,手有余香,如您有不错的内容需要收录,欢迎在在评论区投稿回复。
Android篇
解决融云 SDK 4.0 版本配置 https 导航报 SSLHandshakeException
融云即时通讯SDK集成 — 定制UI(一) ——会话界面小改动
融云即时通讯SDK集成 — 定制UI(二) ——添加自定义表情库
融云即时通讯SDK集成 — 定制UI(三) ——兼容Android Q
融云即时通讯SDK集成 — 国内厂商推送集成踩坑篇(Android平台)
融云 ConversationListFragment 会话列表添加头部布局
融云即时通讯SDK集成 — FCM推送集成指南(Android平台)
iOS篇
集成融云 IMLib 时,如何实现一套类似于 IMKit 的用户信息管理机制
干货分享——使用融云通讯能力库 IMLib 实现单群聊的阅读回执
Web篇
作为小白接融云 IM SDK 新路体验~
微信小程序集成融云 SDK (即时通讯) 集成必备条件
Web 端使用融云 SDK 集成实现滑动加载历史消息
融云IM SDK web 端集成 — 表情采坑篇
融云 Web SDK 如何实现表情的收发 ?
集成融云小程序 SDK 遇到的问题
使用融云 Web SDK 撤回消息
Web 端集成融云 SDK 如何发送正确图片消息给移动端展示?
融云 Web 播放声音 — Flash 篇 (播放 AMR、WAV)
融云 AMR(Aduio) 播放 AMR 格式 Base64 码音频
社区福利
【领取见面礼】限量 100份 GeekOnline加油包!等你来拿
【有奖调研】Geek Online 2020 编程挑战赛参赛调研
【征稿活动】Geek Online 社区第一期投稿激励计划已启动!
GeekOnline编程挑战赛
Geek Online 2020 编程挑战赛 GitHub 仓库
2 个月激烈角逐,15 支队伍突围决赛路演!Geek Online 2020 编程挑战赛完美收官!
一张图回顾 Geek Online 2020 编程挑战赛精彩瞬间!
“这些项目不是什么赚大钱的项目,但是它们足够有趣。”丨关于 Geek Online 2020 编程挑战赛,选手们如是说
融云 CTO 杨攀: Geek Online 2020 编程挑战赛 让开发者站上 C 位
【参赛攻略】你想了解的Geek Online 2020 编程挑战赛常见问题这里都有!
【融云集成常见问题整理】Geek Online 2020 编程挑战赛选手提问整理
求职招聘
持续更新....
【社区精华|持续更新】收录本社区精华内容,手把手教学IM/RTC开发!
IM即时通讯 • admin 发表了文章 • 8 个评论 • 537 次浏览 • 2020-12-07 14:41

本文收录了GeekOnline社区精华内容,希望帮助社区开发者学习IM+RTC知识,解答疑惑。赠人玫瑰,手有余香,如您有不错的内容需要收录,欢迎在在评论区投稿回复。
Android篇
解决融云 SDK 4.0 版本配置 https 导航报 SSLHandshakeException
融云即时通讯SDK集成 — 定制UI(一) ——会话界面小改动
融云即时通讯SDK集成 — 定制UI(二) ——添加自定义表情库
融云即时通讯SDK集成 — 定制UI(三) ——兼容Android Q
融云即时通讯SDK集成 — 国内厂商推送集成踩坑篇(Android平台)
融云 ConversationListFragment 会话列表添加头部布局
融云即时通讯SDK集成 — FCM推送集成指南(Android平台)
iOS篇
集成融云 IMLib 时,如何实现一套类似于 IMKit 的用户信息管理机制
干货分享——使用融云通讯能力库 IMLib 实现单群聊的阅读回执
Web篇
作为小白接融云 IM SDK 新路体验~
微信小程序集成融云 SDK (即时通讯) 集成必备条件
Web 端使用融云 SDK 集成实现滑动加载历史消息
融云IM SDK web 端集成 — 表情采坑篇
融云 Web SDK 如何实现表情的收发 ?
集成融云小程序 SDK 遇到的问题
使用融云 Web SDK 撤回消息
Web 端集成融云 SDK 如何发送正确图片消息给移动端展示?
融云 Web 播放声音 — Flash 篇 (播放 AMR、WAV)
融云 AMR(Aduio) 播放 AMR 格式 Base64 码音频
社区福利
【领取见面礼】限量 100份 GeekOnline加油包!等你来拿
【有奖调研】Geek Online 2020 编程挑战赛参赛调研
【征稿活动】Geek Online 社区第一期投稿激励计划已启动!
GeekOnline编程挑战赛
Geek Online 2020 编程挑战赛 GitHub 仓库
2 个月激烈角逐,15 支队伍突围决赛路演!Geek Online 2020 编程挑战赛完美收官!
一张图回顾 Geek Online 2020 编程挑战赛精彩瞬间!
“这些项目不是什么赚大钱的项目,但是它们足够有趣。”丨关于 Geek Online 2020 编程挑战赛,选手们如是说
融云 CTO 杨攀: Geek Online 2020 编程挑战赛 让开发者站上 C 位
【参赛攻略】你想了解的Geek Online 2020 编程挑战赛常见问题这里都有!
【融云集成常见问题整理】Geek Online 2020 编程挑战赛选手提问整理
求职招聘
持续更新....
超大规模会议技术优化策略 轻松实现 500 人线上流畅沟通
WebRTC • 梅川酷子 发表了文章 • 0 个评论 • 21 次浏览 • 5 天前

1.超大规模会议架构对比
2.超大规模会议中存在的挑战 在超过 20 人会议场景下,SFU 及 WebRTC 兼容场景仍然无法很好的解决。如果直接选择参会人之间进行音视频互动,音视频数据完全转发对服务器资源的要求是巨大的,如果会议中有大量人员同时接入,服务端上行流量和下行流量陡增,会对服务器造成巨大压力。 3.按需订阅与转发以及音频流量优化策略 3.1 按需订阅与转发
The Session Description Protocol includes an optional bandwidth
attribute with the following syntax:
b=<modifier>:<bandwidth-value>
where <modifier> is a single alphanumeric word giving the meaning of
the bandwidth figure, and where the default units for <bandwidth-
value> are kilobits per second. This attribute specifies the
proposed bandwidth to be used by the session or media.
A typical use is with the modifier "AS" (for Application Specific
Maximum) which may be used to specify the total bandwidth for a
single media stream from one site (source).
4. 音频 Top N 选择
// Calculates the normalized RMS value from a mean square value. The input
// should be the sum of squared samples divided by the number of samples. The
// value will be normalized to full range before computing the RMS, wich is
// returned as a negated dBfs. That is, 0 is full amplitude while 127 is very
// faint.
int ComputeRms(float mean_square) {
if (mean_square <= kMinLevel * kMaxSquaredLevel) {
// Very faint; simply return the minimum value.
return RmsLevel::kMinLevelDb;
}
// Normalize by the max level.
const float mean_square_norm = mean_square / kMaxSquaredLevel;
RTC_DCHECK_GT(mean_square_norm, kMinLevel);
// 20log_10(x^0.5) = 10log_10(x)
const float rms = 10.f * log10(mean_square_norm);
RTC_DCHECK_LE(rms, 0.f);
RTC_DCHECK_GT(rms, -RmsLevel::kMinLevelDb);
// Return the negated value.
return static_cast<int>(-rms + 0.5f);
}
void Publisher::Process(RtpAudioPacket packet, AudioLevelHandler handler) {
handler.calculate_queue.enqueue(packet)
RtpAudioPacket packetSend = handler.send_queue.dequeue();
for (对当前Publisher的所有Subscriber subscriber) {
if (subscriber是级联服务器) {
转发packet
} else {
转发packetSend
}
}
}
5. 总结
RTC vs RTMP,适合的才是最好的!
WebRTC • 赵炳东 发表了文章 • 0 个评论 • 143 次浏览 • 2021-01-29 16:17

随着在线教育、电商直播、泛娱乐社交等 App 的普及,实时音视频技术的应用需求也越来越多元化。目前,市场中能够支持音视频通信的主流技术有“RTMP+CDN”和“RTC”两大阵营。选型时,开发者如何根据场景选择更适合自己的通信技术?这就要从两者的技术特点、价格、厂商服务综合考虑。
万人群聊的消息分发控速方案
IM即时通讯 • 徐凤年 发表了文章 • 0 个评论 • 114 次浏览 • 2021-01-21 11:04

当前阶段,群聊已经成为主流IM软件的基本功能,不管是亲属群,朋友群亦或是工作群,都是非常常见的场景。随着移动互联网的发展,即时通讯服务被广泛应用到各个行业,客户业务快速发展,传统百人甚至千人上限的群聊已经无法满足很多业务发展需求,所以超大群的业务应运而生。
1超大群面临的挑战
我们以一个万人群的模型进行举例:
1、如果群中有人发了消息,那么这条消息需要按照1:9999的比例进行分发投递,如果我们按照常规消息的处理流程,那么消息处理服务压力巨大。
2、消息量大的情况下,服务端向客户端直推消息的处理速度将会成为系统瓶颈,而一旦用户的消息下发队列造成了挤压,会影响到正常的消息分发,也会导致服务缓存使用量激增。
3、在微服务架构中,服务以及存储(DB,缓存)之间的QPS和网络流量也会急剧增高。
4、以群为单位的消息缓存,内存和存储开销较大(消息体的存储被放大了万倍)。
基于这些挑战,我们的服务势必要做一定的优化来应对。
2群消息分发模型
整体的群聊服务架构如下图所示:
用户在群里发了一条群消息后,消息先到群组服务,然后通过群组服务缓存的群关系,锁定这条消息最终需要分发的目标用户,然后根据一定的策略分发到消息服务上,消息服务再根据用户的在线状态和消息状态来判断这条消息是直推、通知拉取还是转Push,最终投递给目标用户。
3超大群消息分发解决方案
3.1分发控速:
第一,首先我们会根据服务器的核数来建立多个群消息分发队列,这些队列我们设置了不同的休眠时间以及不同的消费线程数,这里可以理解为快、中、慢等队列。如下图所示:
第二,我们根据群成员数量的大小来将所有群映射到相应的队列中,规则是小群映射到快队列中,大群映射到相应的慢队列中。
第三,小群由于人数少,对服务的影响很小,所以服务利用快队列快速的将群消息分发出去,而大群群消息则利用慢队列的相对高延时来起到控速的作用。
3.2 合并分发:
一条群消息发送到IM服务器后,需要从群组服务投递给消息服务,如果每一个群成员都投递一次,并且投递的群消息内容是一致的话,那肯定会造成相应的资源浪费和服务压力。
服务落点计算中我们使用的是一致性哈希,群成员落点相对固定,所以落点一致的群成员我们可以合并成一次请求进行投递,这样就大幅提高了投递效率同时减少了服务的压力。
3.3 超大规模群的处理方案
在实际群聊业务中,还有一种业务场景是超大规模群,这种群的群人数达到了数十万甚至上百万,这种群如果按照上述的分发方案,势必也会造成消息节点的巨大压力。比如我们有一个十万人的群,消息节点五台,消息服务处理消息的上限是一秒钟4000条,那每台消息节点大约会分到2万条群消息,这超出了消息节点的处理能力。
所以为了避免上述问题,我们的超大群(群成员上线超过3000,可以根据服务器数量和服务器配置相应做调整)会用特殊的队列来处理群消息的分发,这个特殊的队列一秒钟往后端消息服务投递的消息数是消息服务处理上限的一半(留相应的能力处理其他消息),如果单台消息服务处理的QPS上限是4000,那群组服务一秒往单台消息服务最多投递2000条。
结束语
我们后续也会针对群消息进行引用分发,对于大群里发的消息体比较大的消息,我们给群成员只分发和缓存消息的索引,比如MessageID,等群成员真正拉取群消息时再从将消息组装好给客户端分发下去。这样做会节省分发的流量以及存储的空间。
随着互联网的发展,群组业务的模型和压力也在不停地扩展,后续可能还会遇到更多的挑战,届时我们服务器也会通过更优的处理方式来应对。
感兴趣的开发者可以扫码下载融云的 IM 即时通讯 Demo 产品:SealTalk,体验融云的群聊、聊天室等通信能力。
如何实现跨房间连麦功能
WebRTC • admin 发表了文章 • 0 个评论 • 93 次浏览 • 2020-12-24 18:13

在社交娱乐、直播教育等业务场景中,为了增强趣味性和互动性,经常会设计一些主播PK的互动场景,将不同房间的主播拉入同一个房间内进行游戏互动,同时各主播原有房间的观众还能同时观看到自己关注的主播表演并进行打赏等互动,今天就跟大家分享如何实现跨房间连麦功能。
跨房间连麦场景:
场景1.两个直播间中的主播PK
场景2.四个直播间中的主播连麦
场景3.超级小班课
场景示例:
场景一
场景二
场景三
功能点
跨房间邀请过程(邀请,取消邀请,应答,应答通知房间内所有人)
跨房间媒体流合流
加入多RTC房间
订阅多房间人员的音视频流 (监听多房间事件)
支持观众订阅不变的情况能看到新加入的主播
离开副房间(自动取消订阅媒体流)
功能亮点
邀请信令
多房间支持6个房间,再大可以沟通适配
多房间稳定性
跨房间自定义布局
体验 Demo
集成 Demo 示例
融云在 GitHub 上提供了跨房间连麦快速集成 Demo quickdemo-pk 代码示例,方便开发者参考。
跨房间连麦开发文档文档地址
iOS 基于实时音视频 SDK 实现屏幕共享功能——4
WebRTC • admin 发表了文章 • 0 个评论 • 424 次浏览 • 2020-12-03 18:36
1.7 工具类
//
// RongRTCBufferUtil.m
// SealRTC
//
// Created by Sun on 2020/5/8.
// Copyright © 2020 RongCloud. All rights reserved.
//
#import "RongRTCBufferUtil.h"
// 下面的这些方法,一定要记得release,有的没有在方法里面release,但是在外面release了,要不然会内存泄漏
@implementation RongRTCBufferUtil
+ (UIImage *)imageFromBuffer:(CMSampleBufferRef)buffer {
CVPixelBufferRef pixelBuffer = (CVPixelBufferRef)CMSampleBufferGetImageBuffer(buffer);
CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
CIContext *temporaryContext = [CIContext contextWithOptions:nil];
CGImageRef videoImage = [temporaryContext createCGImage:ciImage fromRect:CGRectMake(0, 0, CVPixelBufferGetWidth(pixelBuffer), CVPixelBufferGetHeight(pixelBuffer))];
UIImage *image = [UIImage imageWithCGImage:videoImage];
CGImageRelease(videoImage);
return image;
}
+ (UIImage *)compressImage:(UIImage *)image newWidth:(CGFloat)newImageWidth {
if (!image) return nil;
float imageWidth = image.size.width;
float imageHeight = image.size.height;
float width = newImageWidth;
float height = image.size.height/(image.size.width/width);
float widthScale = imageWidth /width;
float heightScale = imageHeight /height;
UIGraphicsBeginImageContext(CGSizeMake(width, height));
if (widthScale > heightScale) {
[image drawInRect:CGRectMake(0, 0, imageWidth /heightScale , height)];
}
else {
[image drawInRect:CGRectMake(0, 0, width , imageHeight /widthScale)];
}
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
+ (CVPixelBufferRef)CVPixelBufferRefFromUiImage:(UIImage *)img {
CGSize size = img.size;
CGImageRef image = [img CGImage];
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
[NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey, nil];
CVPixelBufferRef pxbuffer = NULL;
CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, size.width, size.height, kCVPixelFormatType_32ARGB, (__bridge CFDictionaryRef) options, &pxbuffer);
NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
CVPixelBufferLockBaseAddress(pxbuffer, 0);
void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
NSParameterAssert(pxdata != NULL);
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(pxdata, size.width, size.height, 8, 4*size.width, rgbColorSpace, kCGImageAlphaPremultipliedFirst);
NSParameterAssert(context);
CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image), CGImageGetHeight(image)), image);
CGColorSpaceRelease(rgbColorSpace);
CGContextRelease(context);
CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
return pxbuffer;
}
+ (CMSampleBufferRef)sampleBufferFromPixbuffer:(CVPixelBufferRef)pixbuffer time:(CMTime)time {
CMSampleBufferRef sampleBuffer = NULL;
//获取视频信息
CMVideoFormatDescriptionRef videoInfo = NULL;
OSStatus result = CMVideoFormatDescriptionCreateForImageBuffer(NULL, pixbuffer, &videoInfo);
CMTime currentTime = time;
// CMSampleTimingInfo timing = {currentTime, currentTime, kCMTimeInvalid};
CMSampleTimingInfo timing = {currentTime, currentTime, kCMTimeInvalid};
result = CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault,pixbuffer, true, NULL, NULL, videoInfo, &timing, &sampleBuffer);
CFRelease(videoInfo);
return sampleBuffer;
}
+ (size_t)getCMTimeSize {
size_t size = sizeof(CMTime);
return size;
}
@end
此工具类中实现是由 CPU 处理,当进行 CMSampleBufferRef 转 UIImage、UIImage 转 CVPixelBufferRef、 CVPixelBufferRef 转 CMSampleBufferRef 以及裁剪图片时,这里需要注意将使用后的对象及时释放,否则会出现内存大量泄漏。
2. 视频发送
2.1 准备阶段
使用融云的 RongRTCLib 的前提需要一个 AppKey,请在官网(https://www.rongcloud.cn/)获取,通过 AppKey 取得 token 之后进行 IM 连接,在连接成功后加入 RTC 房间,这是屏幕共享发送的准备阶段。
- (void)broadcastStartedWithSetupInf(NSDictionary<NSString *,NSObject *> *)setupInfo {
// User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
// 请填写您的 AppKey
self.appKey = @"";
// 请填写用户的 Token
self.token = @"";
// 请指定房间号
self.roomId = @"123456";
[[RCIMClient sharedRCIMClient] initWithAppKey:self.appKey];
[[RCIMClient sharedRCIMClient] setLogLevel:RC_Log_Level_Verbose];
// 连接 IM
[[RCIMClient sharedRCIMClient] connectWithToken:self.token
dbOpened:^(RCDBErrorCode code) {
NSLog(@"dbOpened: %zd", code);
} success:^(NSString *userId) {
NSLog(@"connectWithToken success userId: %@", userId);
// 加入房间
[[RCRTCEngine sharedInstance] joinRoom:self.roomId
completion:^(RCRTCRoom * _Nullable room, RCRTCCode code) {
self.room = room;
self.room.delegate = self;
[self publishScreenStream];
}];
} error:^(RCConnectErrorCode errorCode) {
NSLog(@"ERROR status: %zd", errorCode);
}];
}
如上是连接 IM 和加入 RTC 房间的全过程,其中还包含调用发布自定义视频 [self publishScreenStream]; 此方法在加入房间成功后才可以进行。
- (void)publishScreenStream {
RongRTCStreamParams *param = [[RongRTCStreamParams alloc] init];
param.videoSizePreset = RongRTCVideoSizePreset1280x720;
self.videoOutputStream = [[RongRTCAVOutputStream alloc] initWithParameters:param tag:@"RongRTCScreenVideo"];
[self.room publishAVStream:self.videoOutputStream extra:@"" completion:^(BOOL isSuccess, RongRTCCode desc) {
if (isSuccess) {
NSLog(@"发布自定义流成功");
}
}];
}
自定义一个 RongRTCAVOutputStream 流即可,使用此流发送屏幕共享数据。
2.2 开始发送屏幕共享数据
上面我们已经连接了融云的 IM 和加入了 RTC 房间,并且自定义了一个发送屏幕共享的自定义流,接下来,如何将此流发布出去呢?
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
switch (sampleBufferType) {
case RPSampleBufferTypeVide
// Handle video sample buffer
[self.videoOutputStream write:sampleBuffer error:nil];
break;
case RPSampleBufferTypeAudioApp:
// Handle audio sample buffer for app audio
break;
case RPSampleBufferTypeAudioMic:
// Handle audio sample buffer for mic audio
break;
default:
break;
}
}
但我们接收到了苹果上报的数据之后,调用 RongRTCAVOutputStream 中的 write 方法,将 sampleBuffer 发送给远端,至此,屏幕共享数据就发送出去啦。
[self.videoOutputStream write:sampleBuffer error:nil];
融云的核心代码就是通过上面的连接 IM,加入房间,发布自定义流,然后通过自定义流的 write 方法将 sampleBuffer 发送出去。
不管是通过 ReplayKit 取得屏幕视频,还是使用 Socket 在进程间传输,都是为最终的 write 服务。
总结
Extension 内存是有限制的,最大 50M,所以在 Extension 里面处理数据需要格外注意内存释放;
如果 VideotoolBox 在后台解码一直失败,只需把 VideotoolBox 重启一下即可,此步骤在上面的代码中有体现;
如果不需要将 Extension 的数据传到主 App,只需在 Extension 里直接将流通过 RongRTCLib 发布出去即可,缺点是 Extension 中发布自定义流的用户与主 App 中的用户不是同一个,这也是上面通过 Socket 将数据传递给主 App 要解决的问题;
如果主 App 需要拿到屏幕共享的数据处理,使用 Socket 将流先发给主 App,然后在主 App 里面通过 RongRTCLib 将流发出去。
最后附上 Demo
iOS 基于实时音视频 SDK 实现屏幕共享功能——3
WebRTC • admin 发表了文章 • 0 个评论 • 410 次浏览 • 2020-12-03 18:34

1.5 VideotoolBox 硬编码
//
// RongRTCVideoEncoder.m
// SealRTC
//
// Created by Sun on 2020/5/13.
// Copyright © 2020 RongCloud. All rights reserved.
//
#import "RongRTCVideoEncoder.h"
#import "helpers.h"
@interface RongRTCVideoEncoder() {
VTCompressionSessionRef _compressionSession;
int _frameTime;
}
/**
settings
*/
@property (nonatomic, strong) RongRTCVideoEncoderSettings *settings;
/**
callback queue
*/
@property (nonatomic , strong ) dispatch_queue_t callbackQueue;
- (void)sendSpsAndPPSWithSampleBuffer:(CMSampleBufferRef)sampleBuffer;
- (void)sendNaluData:(CMSampleBufferRef)sampleBuffer;
@end
void compressionOutputCallback(void *encoder,
void *params,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CMSampleBufferRef sampleBuffer) {
RongRTCVideoEncoder *videoEncoder = (__bridge RongRTCVideoEncoder *)encoder;
if (status != noErr) {
return;
}
if (infoFlags & kVTEncodeInfo_FrameDropped) {
return;
}
BOOL isKeyFrame = NO;
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, 0);
if (attachments != nullptr && CFArrayGetCount(attachments)) {
CFDictionaryRef attachment = static_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(attachments, 0)) ;
isKeyFrame = !CFDictionaryContainsKey(attachment, kCMSampleAttachmentKey_NotSync);
}
CMBlockBufferRef block_buffer = CMSampleBufferGetDataBuffer(sampleBuffer);
CMBlockBufferRef contiguous_buffer = nullptr;
if (!CMBlockBufferIsRangeContiguous(block_buffer, 0, 0)) {
status = CMBlockBufferCreateContiguous(nullptr, block_buffer, nullptr, nullptr, 0, 0, 0, &contiguous_buffer);
if (status != noErr) {
return;
}
} else {
contiguous_buffer = block_buffer;
CFRetain(contiguous_buffer);
block_buffer = nullptr;
}
size_t block_buffer_size = CMBlockBufferGetDataLength(contiguous_buffer);
if (isKeyFrame) {
[videoEncoder sendSpsAndPPSWithSampleBuffer:sampleBuffer];
}
if (contiguous_buffer) {
CFRelease(contiguous_buffer);
}
[videoEncoder sendNaluData:sampleBuffer];
}
@implementation RongRTCVideoEncoder
@synthesize settings = _settings;
@synthesize callbackQueue = _callbackQueue;
- (BOOL)configWithSettings:(RongRTCVideoEncoderSettings *)settings onQueue:(nonnull dispatch_queue_t)queue {
self.settings = settings;
if (queue) {
_callbackQueue = queue;
} else {
_callbackQueue = dispatch_get_main_queue();
}
if ([self resetCompressionSession:settings]) {
_frameTime = 0;
return YES;
} else {
return NO;
}
}
- (BOOL)resetCompressionSession:(RongRTCVideoEncoderSettings *)settings {
[self destroyCompressionSession];
OSStatus status = VTCompressionSessionCreate(nullptr, settings.width, settings.height, kCMVideoCodecType_H264, nullptr, nullptr, nullptr, compressionOutputCallback, (__bridge void * _Nullable)(self), &_compressionSession);
if (status != noErr) {
return NO;
}
[self configureCompressionSession:settings];
return YES;
}
- (void)configureCompressionSession:(RongRTCVideoEncoderSettings *)settings {
if (_compressionSession) {
SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_RealTime, true);
SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_AllowFrameReordering, false);
SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, 10);
uint32_t targetBps = settings.startBitrate * 1000;
SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_AverageBitRate, targetBps);
SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, settings.maxFramerate);
int bitRate = settings.width * settings.height * 3 * 4 * 4;
SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_AverageBitRate, bitRate);
int bitRateLimit = settings.width * settings.height * 3 * 4;
SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_DataRateLimits, bitRateLimit);
}
}
- (void)encode:(CMSampleBufferRef)sampleBuffer {
CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
CMTime pts = CMTimeMake(self->_frameTime++, 1000);
VTEncodeInfoFlags flags;
OSStatus res = VTCompressionSessionEncodeFrame(self->_compressionSession,
imageBuffer,
pts,
kCMTimeInvalid,
NULL, NULL, &flags);
if (res != noErr) {
NSLog(@"encode frame error:%d", (int)res);
VTCompressionSessionInvalidate(self->_compressionSession);
CFRelease(self->_compressionSession);
self->_compressionSession = NULL;
return;
}
}
- (void)sendSpsAndPPSWithSampleBuffer:(CMSampleBufferRef)sampleBuffer {
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
const uint8_t *sps ;
const uint8_t *pps;
size_t spsSize ,ppsSize , spsCount,ppsCount;
OSStatus spsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sps, &spsSize, &spsCount, NULL);
OSStatus ppsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pps, &ppsSize, &ppsCount, NULL);
if (spsStatus == noErr && ppsStatus == noErr) {
const char bytes[] = "\x00\x00\x00\x01";
size_t length = (sizeof bytes) - 1;
NSMutableData *spsData = [NSMutableData dataWithCapacity:4+ spsSize];
NSMutableData *ppsData = [NSMutableData dataWithCapacity:4 + ppsSize];
[spsData appendBytes:bytes length:length];
[spsData appendBytes:sps length:spsSize];
[ppsData appendBytes:bytes length:length];
[ppsData appendBytes:pps length:ppsSize];
if (self && self.callbackQueue) {
dispatch_async(self.callbackQueue, ^{
if (self.delegate && [self.delegate respondsToSelector:@selector(spsData:ppsData:)]) {
[self.delegate spsData:spsData ppsData:ppsData];
}
});
}
} else {
NSLog(@"sps status:%@, pps status:%@", @(spsStatus), @(ppsStatus));
}
}
- (void)sendNaluData:(CMSampleBufferRef)sampleBuffer {
size_t totalLength = 0;
size_t lengthAtOffset=0;
char *dataPointer;
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
OSStatus status1 = CMBlockBufferGetDataPointer(blockBuffer, 0, &lengthAtOffset, &totalLength, &dataPointer);
if (status1 != noErr) {
NSLog(@"video encoder error, status = %d", (int)status1);
return;
}
static const int h264HeaderLength = 4;
size_t bufferOffset = 0;
while (bufferOffset < totalLength - h264HeaderLength) {
uint32_t naluLength = 0;
memcpy(&naluLength, dataPointer + bufferOffset, h264HeaderLength);
naluLength = CFSwapInt32BigToHost(naluLength);
const char bytes[] = "\x00\x00\x00\x01";
NSMutableData *naluData = [NSMutableData dataWithCapacity:4 + naluLength];
[naluData appendBytes:bytes length:4];
[naluData appendBytes:dataPointer + bufferOffset + h264HeaderLength length:naluLength];
dispatch_async(self.callbackQueue, ^{
if (self.delegate && [self.delegate respondsToSelector:@selector(naluData:)]) {
[self.delegate naluData:naluData];
}
});
bufferOffset += naluLength + h264HeaderLength;
}
}
- (void)destroyCompressionSession {
if (_compressionSession) {
VTCompressionSessionInvalidate(_compressionSession);
CFRelease(_compressionSession);
_compressionSession = nullptr;
}
}
- (void)dealloc {
if (_compressionSession) {
VTCompressionSessionCompleteFrames(_compressionSession, kCMTimeInvalid);
VTCompressionSessionInvalidate(_compressionSession);
CFRelease(_compressionSession);
_compressionSession = NULL;
}
}
@end
1.6 VideotoolBox 解码
//
// RongRTCVideoDecoder.m
// SealRTC
//
// Created by Sun on 2020/5/14.
// Copyright © 2020 RongCloud. All rights reserved.
//
#import "RongRTCVideoDecoder.h"
#import <UIKit/UIKit.h>
#import "helpers.h"
@interface RongRTCVideoDecoder() {
uint8_t *_sps;
NSUInteger _spsSize;
uint8_t *_pps;
NSUInteger _ppsSize;
CMVideoFormatDescriptionRef _videoFormatDescription;
VTDecompressionSessionRef _decompressionSession;
}
/**
settings
*/
@property (nonatomic, strong) RongRTCVideoEncoderSettings *settings;
/**
callback queue
*/
@property (nonatomic, strong) dispatch_queue_t callbackQueue;
@end
void DecoderOutputCallback(void * CM_NULLABLE decompressionOutputRefCon,
void * CM_NULLABLE sourceFrameRefCon,
OSStatus status,
VTDecodeInfoFlags infoFlags,
CM_NULLABLE CVImageBufferRef imageBuffer,
CMTime presentationTimeStamp,
CMTime presentationDuration ) {
if (status != noErr) {
NSLog(@" decoder callback error :%@", @(status));
return;
}
CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;
*outputPixelBuffer = CVPixelBufferRetain(imageBuffer);
RongRTCVideoDecoder *decoder = (__bridge RongRTCVideoDecoder *)(decompressionOutputRefCon);
dispatch_async(decoder.callbackQueue, ^{
[decoder.delegate didGetDecodeBuffer:imageBuffer];
CVPixelBufferRelease(imageBuffer);
});
}
@implementation RongRTCVideoDecoder
@synthesize settings = _settings;
@synthesize callbackQueue = _callbackQueue;
- (BOOL)configWithSettings:(RongRTCVideoEncoderSettings *)settings onQueue:(dispatch_queue_t)queue {
self.settings = settings;
if (queue) {
_callbackQueue = queue;
} else {
_callbackQueue = dispatch_get_main_queue();
}
return YES;
}
- (BOOL)createVT {
if (_decompressionSession) {
return YES;
}
const uint8_t * const parameterSetPointers[2] = {_sps, _pps};
const size_t parameterSetSizes[2] = {_spsSize, _ppsSize};
int naluHeaderLen = 4;
OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, parameterSetPointers, parameterSetSizes, naluHeaderLen, &_videoFormatDescription );
if (status != noErr) {
NSLog(@"CMVideoFormatDescriptionCreateFromH264ParameterSets error:%@", @(status));
return false;
}
NSDictionary *destinationImageBufferAttributes =
@{
(id)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange],
(id)kCVPixelBufferWidthKey: [NSNumber numberWithInteger:self.settings.width],
(id)kCVPixelBufferHeightKey: [NSNumber numberWithInteger:self.settings.height],
(id)kCVPixelBufferOpenGLCompatibilityKey: [NSNumber numberWithBool:true]
};
VTDecompressionOutputCallbackRecord CallBack;
CallBack.decompressionOutputCallback = DecoderOutputCallback;
CallBack.decompressionOutputRefCon = (__bridge void * _Nullable)(self);
status = VTDecompressionSessionCreate(kCFAllocatorDefault, _videoFormatDescription, NULL, (__bridge CFDictionaryRef _Nullable)(destinationImageBufferAttributes), &CallBack, &_decompressionSession);
if (status != noErr) {
NSLog(@"VTDecompressionSessionCreate error:%@", @(status));
return false;
}
status = VTSessionSetProperty(_decompressionSession, kVTDecompressionPropertyKey_RealTime,kCFBooleanTrue);
return YES;
}
- (CVPixelBufferRef)decode:(uint8_t *)frame withSize:(uint32_t)frameSize {
CVPixelBufferRef outputPixelBuffer = NULL;
CMBlockBufferRef blockBuffer = NULL;
CMBlockBufferFlags flag0 = 0;
OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, frame, frameSize, kCFAllocatorNull, NULL, 0, frameSize, flag0, &blockBuffer);
if (status != kCMBlockBufferNoErr) {
NSLog(@"VCMBlockBufferCreateWithMemoryBlock code=%d", (int)status);
CFRelease(blockBuffer);
return outputPixelBuffer;
}
CMSampleBufferRef sampleBuffer = NULL;
const size_t sampleSizeArray[] = {frameSize};
status = CMSampleBufferCreateReady(kCFAllocatorDefault, blockBuffer, _videoFormatDescription, 1, 0, NULL, 1, sampleSizeArray, &sampleBuffer);
if (status != noErr || !sampleBuffer) {
NSLog(@"CMSampleBufferCreateReady failed status=%d", (int)status);
CFRelease(blockBuffer);
return outputPixelBuffer;
}
VTDecodeFrameFlags flag1 = kVTDecodeFrame_1xRealTimePlayback;
VTDecodeInfoFlags infoFlag = kVTDecodeInfo_Asynchronous;
status = VTDecompressionSessionDecodeFrame(_decompressionSession, sampleBuffer, flag1, &outputPixelBuffer, &infoFlag);
if (status == kVTInvalidSessionErr) {
NSLog(@"decode frame error with session err status =%d", (int)status);
[self resetVT];
} else {
if (status != noErr) {
NSLog(@"decode frame error with status =%d", (int)status);
}
}
CFRelease(sampleBuffer);
CFRelease(blockBuffer);
return outputPixelBuffer;
}
- (void)resetVT {
[self destorySession];
[self createVT];
}
- (void)decode:(NSData *)data {
uint8_t *frame = (uint8_t*)[data bytes];
uint32_t length = data.length;
uint32_t nalSize = (uint32_t)(length - 4);
uint32_t *pNalSize = (uint32_t *)frame;
*pNalSize = CFSwapInt32HostToBig(nalSize);
int type = (frame[4] & 0x1F);
CVPixelBufferRef pixelBuffer = NULL;
switch (type) {
case 0x05:
if ([self createVT]) {
pixelBuffer= [self decode:frame withSize:length];
}
break;
case 0x07:
self->_spsSize = length - 4;
self->_sps = (uint8_t *)malloc(self->_spsSize);
memcpy(self->_sps, &frame[4], self->_spsSize);
break;
case 0x08:
self->_ppsSize = length - 4;
self->_pps = (uint8_t *)malloc(self->_ppsSize);
memcpy(self->_pps, &frame[4], self->_ppsSize);
break;
default:
if ([self createVT]) {
pixelBuffer = [self decode:frame withSize:length];
}
break;
}
}
- (void)dealloc {
[self destorySession];
}
- (void)destorySession {
if (_decompressionSession) {
VTDecompressionSessionInvalidate(_decompressionSession);
CFRelease(_decompressionSession);
_decompressionSession = NULL;
}
}
@end
iOS 基于实时音视频 SDK 实现屏幕共享功能——2
WebRTC • admin 发表了文章 • 0 个评论 • 402 次浏览 • 2020-12-03 18:26

这里的核心思想是拿到屏幕共享的数据之后,先进行压缩,当压缩完成后会通过回调上报给当前类。既而通过 clientSend
方法,发给主 App。发给主 App 的数据中自定义了一个头部,头部添加了一个前缀和一个每次发送字节的长度,当接收端收到数据包后解析即可。
- (void)clientSend:(NSData *)data {
//data length
NSUInteger dataLength = data.length;
// data header struct
DataHeader dataH;
memset((void *)&dataH, 0, sizeof(dataH));
// pre
PreHeader preH;
memset((void *)&preH, 0, sizeof(preH));
preH.pre[0] = '&';
preH.dataLength = dataLength;
dataH.preH = preH;
// buffer
int headerlength = sizeof(dataH);
int totalLength = dataLength + headerlength;
// srcbuffer
Byte *src = (Byte *)[data bytes];
// send buffer
char *buffer = (char *)malloc(totalLength * sizeof(char));
memcpy(buffer, &dataH, headerlength);
memcpy(buffer + headerlength, src, dataLength);
// to send
[self sendBytes:buffer length:totalLength];
free(buffer);
}
1.4 接收屏幕共享数据
//
// RongRTCServerSocket.m
// SealRTC
//
// Created by Sun on 2020/5/7.
// Copyright © 2020 RongCloud. All rights reserved.
//
#import "RongRTCServerSocket.h"
#import <arpa/inet.h>
#import <netdb.h>
#import <sys/types.h>
#import <sys/socket.h>
#import <ifaddrs.h>
#import <UIKit/UIKit.h>
#import "RongRTCThread.h"
#import "RongRTCSocketHeader.h"
#import "RongRTCVideoDecoder.h"
@interface RongRTCServerSocket() <RongRTCCodecProtocol>
{
pthread_mutex_t lock;
int _frameTime;
CMTime _lastPresentationTime;
Float64 _currentMediaTime;
Float64 _currentVideoTime;
dispatch_queue_t _frameQueue;
}
@property (nonatomic, assign) int acceptSocket;
/**
data length
*/
@property (nonatomic, assign) NSUInteger dataLength;
/**
timeData
*/
@property (nonatomic, strong) NSData *timeData;
/**
decoder queue
*/
@property (nonatomic, strong) dispatch_queue_t decoderQueue;
/**
decoder
*/
@property (nonatomic, strong) RongRTCVideoDecoder *decoder;
@end
@implementation RongRTCServerSocket
- (BOOL)createServerSocket {
if ([self createSocket] == -1) {
return NO;
}
[self setReceiveBuffer];
[self setReceiveTimeout];
BOOL isB = [self bind];
BOOL isL = [self listen];
if (isB && isL) {
_decoderQueue = dispatch_queue_create("cn.rongcloud.decoderQueue", NULL);
_frameTime = 0;
[self createDecoder];
[self receive];
return YES;
} else {
return NO;
}
}
- (void)createDecoder {
self.decoder = [[RongRTCVideoDecoder alloc] init];
self.decoder.delegate = self;
RongRTCVideoEncoderSettings *settings = [[RongRTCVideoEncoderSettings alloc] init];
settings.width = 720;
settings.height = 1280;
settings.startBitrate = 300;
settings.maxFramerate = 30;
settings.minBitrate = 1000;
[self.decoder configWithSettings:settings onQueue:_decoderQueue];
}
- (void)receiveData {
struct sockaddr_in rest;
socklen_t rest_size = sizeof(struct sockaddr_in);
self.acceptSocket = accept(self.socket, (struct sockaddr *) &rest, &rest_size);
while (self.acceptSocket != -1) {
DataHeader dataH;
memset(&dataH, 0, sizeof(dataH));
if (![self receiveData:(char *)&dataH length:sizeof(dataH)]) {
continue;
}
PreHeader preH = dataH.preH;
char pre = preH.pre[0];
if (pre == '&') {
// rongcloud socket
NSUInteger dataLenght = preH.dataLength;
char *buff = (char *)malloc(sizeof(char) * dataLenght);
if ([self receiveData:(char *)buff length:dataLenght]) {
NSData *data = [NSData dataWithBytes:buff length:dataLenght];
[self.decoder decode:data];
free(buff);
}
} else {
NSLog(@"pre is not &");
return;
}
}
}
- (BOOL)receiveData:(char *)data length:(NSUInteger)length {
LOCK(lock);
int receiveLength = 0;
while (receiveLength < length) {
ssize_t res = recv(self.acceptSocket, data, length - receiveLength, 0);
if (res == -1 || res == 0) {
UNLOCK(lock);
NSLog(@"receive data error");
break;
}
receiveLength += res;
data += res;
}
UNLOCK(lock);
return YES;
}
- (void)didGetDecodeBuffer:(CVPixelBufferRef)pixelBuffer {
_frameTime += 1000;
CMTime pts = CMTimeMake(_frameTime, 1000);
CMSampleBufferRef sampleBuffer = [RongRTCBufferUtil sampleBufferFromPixbuffer:pixelBuffer time:pts];
// Check to see if there is a problem with the decoded data. If the image appears, you are right.
UIImage *image = [RongRTCBufferUtil imageFromBuffer:sampleBuffer];
[self.delegate didProcessSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
}
- (void)close {
int res = close(self.acceptSocket);
self.acceptSocket = -1;
NSLog(@"shut down server: %d", res);
[super close];
}
- (void)dealloc {
NSLog(@"dealoc server socket");
}
@end
主 App 通过 Socket
会持续收到数据包,再将数据包进行解码,将解码后的数据通过代理 didGetDecodeBuffer
代理方法回调给 App 层。App 层就可以通过融云 RongRTCLib
的发送自定义流方法将视频数据发送到对端。
iOS 基于实时音视频 SDK 实现屏幕共享功能——1
WebRTC • admin 发表了文章 • 0 个评论 • 412 次浏览 • 2020-12-03 18:21

Replaykit 介绍
在之前的 iOS 版本中,iOS 开发者只能拿到编码后的数据,拿不到原始的 PCM 和 YUV,到 iOS 10 之后,开发者可以拿到原始数据,但是只能录制 App 内的内容,如果切到后台,将停止录制,直到 iOS 11,苹果对屏幕共享进行了升级并开放了权限,既可以拿到原始数据,又可以录制整个系统,以下我们重点来说 iOS 11 之后的屏幕共享功能。
系统屏幕共享
- (void)initMode_1 {
self.systemBroadcastPickerView = [[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(0, 64, ScreenWidth, 80)];
self.systemBroadcastPickerView.preferredExtension = @"cn.rongcloud.replaytest.Recoder";
self.systemBroadcastPickerView.backgroundColor = [UIColor colorWithRed:53.0/255.0 green:129.0/255.0 blue:242.0/255.0 alpha:1.0];
self.systemBroadcastPickerView.showsMicrophoneButton = NO;
[self.view addSubview:self.systemBroadcastPickerView];
}
在 iOS 11 创建一个 Extension
之后,调用上面的代码就可以开启屏幕共享了,然后系统会为我们生成一个 SampleHandler
的类,在这个方法中,苹果会根据 RPSampleBufferType
上报不同类型的数据。
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType
那怎么通过融云的 RongRTCLib
将屏幕共享数据发送出去呢?
1. 基于 Socket 的逼格玩法
1.1. Replaykit 框架启动和创建 Socket
//
// ViewController.m
// Socket_Replykit
//
// Created by Sun on 2020/5/19.
// Copyright © 2020 RongCloud. All rights reserved.
//
#import "ViewController.h"
#import <ReplayKit/ReplayKit.h>
#import "RongRTCServerSocket.h"
@interface ViewController ()<RongRTCServerSocketProtocol>
@property (nonatomic, strong) RPSystemBroadcastPickerView *systemBroadcastPickerView;
/**
server socket
*/
@property(nonatomic , strong)RongRTCServerSocket *serverSocket;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// Do any additional setup after loading the view.
[self.serverSocket createServerSocket];
self.systemBroadcastPickerView = [[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(0, 64, [UIScreen mainScreen].bounds.size.width, 80)];
self.systemBroadcastPickerView.preferredExtension = @"cn.rongcloud.sealrtc.RongRTCRP";
self.systemBroadcastPickerView.backgroundColor = [UIColor colorWithRed:53.0/255.0 green:129.0/255.0 blue:242.0/255.0 alpha:1.0];
self.systemBroadcastPickerView.showsMicrophoneButton = NO;
[self.view addSubview:self.systemBroadcastPickerView];
}
- (RongRTCServerSocket *)serverSocket {
if (!_serverSocket) {
RongRTCServerSocket *socket = [[RongRTCServerSocket alloc] init];
socket.delegate = self;
_serverSocket = socket;
}
return _serverSocket;
}
- (void)didProcessSampleBuffer:(CMSampleBufferRef)sampleBuffer {
// 这里拿到了最终的数据,比如最后可以使用融云的音视频SDK RTCLib 进行传输就可以了
}
@end
其中,包括了创建 Server Socket
的步骤,我们把主 App 当做 Server
,然后屏幕共享 Extension
当做 Client
,通过 Socket
向我们的主 APP 发送数据。
在 Extension
里面,我们拿到 ReplayKit
框架上报的屏幕视频数据后:
//
// SampleHandler.m
// SocketReply
//
// Created by Sun on 2020/5/19.
// Copyright © 2020 RongCloud. All rights reserved.
//
#import "SampleHandler.h"
#import "RongRTCClientSocket.h"
@interface SampleHandler()
/**
Client Socket
*/
@property (nonatomic, strong) RongRTCClientSocket *clientSocket;
@end
@implementation SampleHandler
- (void)broadcastStartedWithSetupInf(NSDictionary<NSString *,NSObject *> *)setupInfo {
// User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
self.clientSocket = [[RongRTCClientSocket alloc] init];
[self.clientSocket createCliectSocket];
}
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
switch (sampleBufferType) {
case RPSampleBufferTypeVide
// Handle video sample buffer
[self sendData:sampleBuffer];
break;
case RPSampleBufferTypeAudioApp:
// Handle audio sample buffer for app audio
break;
case RPSampleBufferTypeAudioMic:
// Handle audio sample buffer for mic audio
break;
default:
break;
}
}
- (void)sendData:(CMSampleBufferRef)sampleBuffer {
[self.clientSocket encodeBuffer:sampleBuffer];
}
@end
可见 ,这里我们创建了一个 Client Socket
,然后拿到屏幕共享的视频 sampleBuffer
之后,通过 Socket
发给我们的主 App,这就是屏幕共享的流程。
1.2 Local Socket 的使用
//
// RongRTCSocket.m
// SealRTC
//
// Created by Sun on 2020/5/7.
// Copyright © 2020 RongCloud. All rights reserved.
//
#import "RongRTCSocket.h"
#import <arpa/inet.h>
#import <netdb.h>
#import <sys/types.h>
#import <sys/socket.h>
#import <ifaddrs.h>
#import "RongRTCThread.h"
@interface RongRTCSocket()
/**
receive thread
*/
@property (nonatomic, strong) RongRTCThread *receiveThread;
@end
@implementation RongRTCSocket
- (int)createSocket {
int socket = socket(AF_INET, SOCK_STREAM, 0);
self.socket = socket;
if (self.socket == -1) {
close(self.socket);
NSLog(@"socket error : %d", self.socket);
}
self.receiveThread = [[RongRTCThread alloc] init];
[self.receiveThread run];
return socket;
}
- (void)setSendBuffer {
int optVal = 1024 * 1024 * 2;
int optLen = sizeof(int);
int res = setsockopt(self.socket, SOL_SOCKET, SO_SNDBUF, (char *)&optVal,optLen);
NSLog(@"set send buffer:%d", res);
}
- (void)setReceiveBuffer {
int optVal = 1024 * 1024 * 2;
int optLen = sizeof(int);
int res = setsockopt(self.socket, SOL_SOCKET, SO_RCVBUF, (char*)&optVal,optLen );
NSLog(@"set send buffer:%d",res);
}
- (void)setSendingTimeout {
struct timeval timeout = {10,0};
int res = setsockopt(self.socket, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(int));
NSLog(@"set send timeout:%d", res);
}
- (void)setReceiveTimeout {
struct timeval timeout = {10, 0};
int res = setsockopt(self.socket, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(int));
NSLog(@"set send timeout:%d", res);
}
- (BOOL)connect {
NSString *serverHost = [self ip];
struct hostent *server = gethostbyname([serverHost UTF8String]);
if (server == NULL) {
close(self.socket);
NSLog(@"get host error");
return NO;
}
struct in_addr *remoteAddr = (struct in_addr *)server->h_addr_list[0];
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr = *remoteAddr;
addr.sin_port = htons(CONNECTPORT);
int res = connect(self.socket, (struct sockaddr *) &addr, sizeof(addr));
if (res == -1) {
close(self.socket);
NSLog(@"connect error");
return NO;
}
NSLog(@"socket connect to server success");
return YES;
}
- (BOOL)bind {
struct sockaddr_in client;
client.sin_family = AF_INET;
NSString *ipStr = [self ip];
if (ipStr.length <= 0) {
return NO;
}
const char *ip = [ipStr cStringUsingEncoding:NSASCIIStringEncoding];
client.sin_addr.s_addr = inet_addr(ip);
client.sin_port = htons(CONNECTPORT);
int bd = bind(self.socket, (struct sockaddr *) &client, sizeof(client));
if (bd == -1) {
close(self.socket);
NSLog(@"bind error: %d", bd);
return NO;
}
return YES;
}
- (BOOL)listen {
int ls = listen(self.socket, 128);
if (ls == -1) {
close(self.socket);
NSLog(@"listen error: %d", ls);
return NO;
}
return YES;
}
- (void)receive {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self receiveData];
});
}
- (NSString *)ip {
NSString *ip = nil;
struct ifaddrs *addrs = NULL;
struct ifaddrs *tmpAddrs = NULL;
BOOL res = getifaddrs(&addrs);
if (res == 0) {
tmpAddrs = addrs;
while (tmpAddrs != NULL) {
if (tmpAddrs->ifa_addr->sa_family == AF_INET) {
// Check if interface is en0 which is the wifi connection on the iPhone
NSLog(@"%@", [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)tmpAddrs->ifa_addr)->sin_addr)]);
if ([[NSString stringWithUTF8String:tmpAddrs->ifa_name] isEqualToString:@"en0"]) {
// Get NSString from C String
ip = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)tmpAddrs->ifa_addr)->sin_addr)];
}
}
tmpAddrs = tmpAddrs->ifa_next;
}
}
// Free memory
freeifaddrs(addrs);
NSLog(@"%@",ip);
return ip;
}
- (void)close {
int res = close(self.socket);
NSLog(@"shut down: %d", res);
}
- (void)receiveData {
}
- (void)dealloc {
[self.receiveThread stop];
}
@end
首先创建了一个 Socket
的父类,然后用 Server Socket
和 Client Socket
分别继承类来实现链接、绑定等操作。可以看到有些数据可以设置,有些则不用,这里不是核心,核心是怎样收发数据。
1.3 发送屏幕共享数据
//
// RongRTCClientSocket.m
// SealRTC
//
// Created by Sun on 2020/5/7.
// Copyright © 2020 RongCloud. All rights reserved.
//
#import "RongRTCClientSocket.h"
#import <arpa/inet.h>
#import <netdb.h>
#import <sys/types.h>
#import <sys/socket.h>
#import <ifaddrs.h>
#import "RongRTCThread.h"
#import "RongRTCSocketHeader.h"
#import "RongRTCVideoEncoder.h"
@interface RongRTCClientSocket() <RongRTCCodecProtocol> {
pthread_mutex_t lock;
}
/**
video encoder
*/
@property (nonatomic, strong) RongRTCVideoEncoder *encoder;
/**
encode queue
*/
@property (nonatomic, strong) dispatch_queue_t encodeQueue;
@end
@implementation RongRTCClientSocket
- (BOOL)createClientSocket {
if ([self createSocket] == -1) {
return NO;
}
BOOL isC = [self connect];
[self setSendBuffer];
[self setSendingTimeout];
if (isC) {
_encodeQueue = dispatch_queue_create("cn.rongcloud.encodequeue", NULL);
[self createVideoEncoder];
return YES;
} else {
return NO;
}
}
- (void)createVideoEncoder {
self.encoder = [[RongRTCVideoEncoder alloc] init];
self.encoder.delegate = self;
RongRTCVideoEncoderSettings *settings = [[RongRTCVideoEncoderSettings alloc] init];
settings.width = 720;
settings.height = 1280;
settings.startBitrate = 300;
settings.maxFramerate = 30;
settings.minBitrate = 1000;
[self.encoder configWithSettings:settings onQueue:_encodeQueue];
}
- (void)clientSend:(NSData *)data {
//data length
NSUInteger dataLength = data.length;
// data header struct
DataHeader dataH;
memset((void *)&dataH, 0, sizeof(dataH));
// pre
PreHeader preH;
memset((void *)&preH, 0, sizeof(preH));
preH.pre[0] = '&';
preH.dataLength = dataLength;
dataH.preH = preH;
// buffer
int headerlength = sizeof(dataH);
int totalLength = dataLength + headerlength;
// srcbuffer
Byte *src = (Byte *)[data bytes];
// send buffer
char *buffer = (char *)malloc(totalLength * sizeof(char));
memcpy(buffer, &dataH, headerlength);
memcpy(buffer + headerlength, src, dataLength);
// tosend
[self sendBytes:buffer length:totalLength];
free(buffer);
}
- (void)encodeBuffer:(CMSampleBufferRef)sampleBuffer {
[self.encoder encode:sampleBuffer];
}
- (void)sendBytes:(char *)bytes length:(int)length {
LOCK(self->lock);
int hasSendLength = 0;
while (hasSendLength < length) {
// connect socket success
if (self.socket > 0) {
// send
int sendRes = send(self.socket, bytes, length - hasSendLength, 0);
if (sendRes == -1 || sendRes == 0) {
UNLOCK(self->lock);
NSLog(@"send buffer error");
[self close];
break;
}
hasSendLength += sendRes;
bytes += sendRes;
} else {
NSLog(@"client socket connect error");
UNLOCK(self->lock);
}
}
UNLOCK(self->lock);
}
- (void)spsData:(NSData *)sps ppsData:(NSData *)pps {
[self clientSend:sps];
[self clientSend:pps];
}
- (void)naluData:(NSData *)naluData {
[self clientSend:naluData];
}
- (void)deallo c{
NSLog(@"dealoc cliect socket");
}
@end
前端音视频之WebRTC初探
WebRTC • 大兴 发表了文章 • 0 个评论 • 138 次浏览 • 2020-10-22 17:35

今天,我们来一起学习一下 WebRTC,相信你已经对这个前端音视频网红儿有所耳闻了。
WebRTC Web Real-Time Communication 网页即时通信
WebRTC 于 2011 年 6 月 1 日开源,并在 Google、Mozilla、Opera 等大佬们的支持下被纳入 W3C 推荐标准,它给浏览器和移动应用提供了即时通信的能力。
WebRTC 优势及应用场景
优势
跨平台(Web、Windows、MacOS、Linux、iOS、Android) 实时传输 音视频引擎 免费、免插件、免安装 主流浏览器支持 强大的打洞能力
应用场景
在线教育、在线医疗、音视频会议、即时通讯工具、直播、共享远程桌面、P2P网络加速、游戏(狼人杀、线上KTV)等。
(有喜欢玩狼人杀的同学吗?有时间可以一起来一局,给我一轮听发言的时间,给你裸点狼坑,一个坑容错。)
WebRTC 整体架构
拉回来,我们看一看 WebRTC 的整体架构,我用不同的颜色标识出了各层级所代表的含义。
Web 应用 Web API WebRTC C++ API Session Management 信令管理 Transport 传输层 Voice Engine 音频引擎 Video Engine 视频处理引擎
我们再来看下核心的模块:
Voice Engine 音频引擎
VoIP 软件开发商 Global IP Solutions 提供的 GIPS 引擎可以说是世界上最好的语音引擎,谷歌大佬一举将其收购并开源,也就是 WebRTC 中的 音频引擎。
iSAC:WebRTC 音频引擎的默认编解码器,针对 VoIP 和音频流的宽带和超宽带音频编解码器。 iLBC:VoIP 音频流的窄带语音编解码器。 NetEQ For Voice:针对音频软件实现的语音信号处理元件。NetEQ 算法是自适应抖动控制算法以及语音包丢失隐藏算法,能够有效的处理网络抖动和语音包丢失时对语音质量产生的影响。 Acoustic Echo Canceler:AEC,回声消除器。 Noise Reduction:NR,噪声抑制。
Video Engine 视频处理引擎
VPx 系列视频编解码器是 Google 大佬收购 ON2 公司后开源的。
VP8:视频图像编解码器,WebRTC 视频引擎默认的编解码器。 Video Jitter Buffer:视频抖动缓冲器模块。 Image Enhancements:图像质量增强模块。
WebRTC 通信原理
媒体协商
媒体协商也就是让双方可以找到共同支持的媒体能力,比如双方都支持的编解码器,这样才能实现彼此之间的音视频通信。
SDP Session Description Protocal
媒体协商所交换的数据就是 SDP,说是协议,其实 SDP 并不是一个真正的协议,它就是一种描述各端“能力”的数据格式。
上图所示就是 SDP 的一部分,详细内容请参考:SDP: Session Description Protocol
https://tools.ietf.org/html/rfc4566
或者参考卡神的这篇文章:WebRTC:会话描述协议SDP
https://zhuanlan.zhihu.com/p/75492311
网络协商
ICE Interactive Connectivity Establishment 互动式连接建立
想要建立连接,我们要需要拿到双方 IP 和端口的信息,在当下复杂的网络环境下,ICE 统一了各种 NAT 穿越技术(STUN、TURN),可以让客户端成功地穿透远程用户与网络之间可能存在的各类防火墙。
STUN、TURN
STUN:简单 UDP 穿透 NAT,可以使位于 NAT(或多重 NAT) 后的客户端找出自己的公网 IP 地址,以及查出自己位于哪种类型的 NAT 及 NAT 所绑定的 Internet 端口。
我们知道,NAT 主要有以下四个种类:
完全锥型 NAT IP 限制锥型 端口限制锥型 对称型
前三种都可以使用 STUN 穿透,而面对第四种类型,也是大型公司网络中经常采用的对称型 NAT ,这时的路由器只会接受之前连线过的节点所建立的连线。
那么想要处理这种网络情况,我们就需要使用 TURN (中继穿透 NAT) 技术。
TURN 是 STUN 的一个扩展,其主要添加了中继功能。在 STUN 服务器的基础上,再添加几台 TURN 服务器,如果 STUN 分配公网 IP 失败,则可以通过 TURN 服务器请求公网 IP 地址作为中继地址,将媒体数据通过 TURN 服务器进行中转。
信令服务器 Signal Server
拿到了双方的媒体信息(SDP)和网络信息(Candidate)后,我们还需要一台信令服务器作为中间商来转发交换它们。
信令服务器还可以实现一些 IM 功能,比如房间管理,用户进入、退出等。
小结
本文我们了解了 WebRTC 优势及应用场景、WebRTC 的整体架构及主要模块构成以及 WebRTC 的通信原理。这些基础知识和概念是需要我们牢记的,大家要记牢~
参考
《从 0 打造音视频直播系统》 李超 《WebRTC 音视频开发 React+Flutter+Go 实战》 亢少军 https://webrtc.github.io/webrtc-org/architecture/ https://developer.mozilla.org/zh-CN/docs/Web/API/WebRTC_API https://www.w3.org/TR/webrtc/
本文转自公众号“前端食堂”,作者霍语佳
RTC