RTC

RTC

【社区精华|持续更新】收录本社区精华内容,手把手教学IM/RTC开发!

IM即时通讯admin 发表了文章 • 8 个评论 • 537 次浏览 • 2020-12-07 14:41 • 来自相关话题

本文收录了GeekOnline社区精华内容,希望帮助社区开发者学习IM+RTC知识,解答疑惑。赠人玫瑰,手有余香,如您有不错的内容需要收录,欢迎在在评论区投稿回复。Android篇融云即时通讯SDK集成 — 通知检查融云 IM SDK 集成 —- 刷新会话界面... ...查看全部

本文收录了GeekOnline社区精华内容,希望帮助社区开发者学习IM+RTC知识,解答疑惑。赠人玫瑰,手有余香,如您有不错的内容需要收录,欢迎在在评论区投稿回复。

微信截图_20201207144054.png

Android篇

融云即时通讯SDK集成 — 通知检查

融云 IM SDK 集成 —- 刷新会话界面和会话列表界面

Android 端如何添加自定义表情

解决融云 SDK 4.0 版本配置 https 导航报 SSLHandshakeException

融云清空历史消息 Android 端

唠一唠融云的消息扩展功能

融云 IMkit 拦截或监听所有发送消息

融云如何把图片消息的图片上传到自己的文件服务器

唠一唠融云 VIVO push 无法跳转的解决方案

融云即时通讯SDK集成 — 定制UI(一) ——会话界面小改动

融云即时通讯SDK集成 — 定制UI(二) ——添加自定义表情库

融云即时通讯SDK集成 — 定制UI(三) ——兼容Android Q

融云如何把图片消息的图片上传到自己的文件服务器

融云即时通讯SDK集成 — 华为推送的点击跳转处理

带你实现女朋友欲罢不能的 App

Flutter 集成融云 sdk

配置融云SDK的自签证书

自定义消息 包含 list 数组

关于融云聊天室KV 值的正确使用

融云 IM SDK 转 AndroidX

融云即时通讯SDK集成 — 国内厂商推送集成踩坑篇(Android平台)

在融云 IMkit 会话界面基础上添加消息已读未读

融云聊天室属性 kv

融云 ConversationListFragment 会话列表添加头部布局

融云即时通讯SDK集成 — FCM推送集成指南(Android平台)

融云集成之避坑指南-Android推送篇

融云IMKit 动态删除或添加plugin 的实现


iOS篇

iOS 基于实时音视频 SDK 实现屏幕共享功能——1

iOS 基于实时音视频 SDK 实现屏幕共享功能——2

iOS 基于实时音视频 SDK 实现屏幕共享功能——3

iOS 基于实时音视频 SDK 实现屏幕共享功能——4

如何隐藏融云输入框语音按钮

给融云的输入框上方加个功能按钮,怎么整?

融云 IM SDK 如何插入消息

集成融云 IMLib 时,如何实现一套类似于 IMKit 的用户信息管理机制

为融云聊天页面的输入框添加 Placeholder

30 分钟集成融云 IM 即时通讯

简单介绍融云 imkit 包含功能

融云的聊天页面在 iOS14 出现崩溃的解决办法

融云聊天页面长按消息后“翻译”功能的实现方法

使用融云 IM 点击最近聊天记录时跳转到 @ 自己的消息

如何设置融云用户信息

自定义融云会话列表 cell 选中背景

融云 IMKit 音频录制参数

融云会话页面刷新不及时问题

融云 Flutter IM SDK 解析

关于融云 SDK 在使用 p8 证书的坎坷~

融云 SDK 如何实现群组操作

如何利用融云 IMLib 来实现一个阅后即焚功能

干货分享——使用融云通讯能力库 IMLib 实现单群聊的阅读回执


Web篇

作为小白接融云 IM SDK 新路体验~

微信小程序集成融云 SDK (即时通讯) 集成必备条件

Web 端使用融云 SDK 集成实现滑动加载历史消息

融云IM SDK web 端集成 — 表情采坑篇

融云 Web SDK 如何实现表情的收发 ?

集成融云小程序 SDK 遇到的问题

使用融云 Web SDK 撤回消息

融云 RTC SDK 集成实现直播,趟坑之旅~~~

融云 Web SDK 删除历史消息

集成融云小程序 SDK 遇到的问题

Web 端集成融云 SDK 如何发送正确图片消息给移动端展示?

使用融云 IM SDK 实现 H5 直播聊天

WebRTC 实现实时音视频技术研究

融云发送语音消息

融云 CallLib 集成遇到的问题

结合融云 WebSDK 了解 WebSocket 基本原理

集成融云 Web 音视频通话踩坑之旅

SDK 兼容 JSON

融云 IM SDK 发送语音消息

集成融云 IM 问题总结

融云 Web SDK 如何实现只有一个设备登入

融云 Web 播放声音 — Flash 篇 (播放 AMR、WAV)

融云 IM 那些事儿

融云 AMR(Aduio) 播放 AMR 格式 Base64 码音频


社区福利

【领取见面礼】限量 100份 GeekOnline加油包!等你来拿

【有奖调研】Geek Online 2020 编程挑战赛参赛调研

【征稿活动】Geek Online 社区第一期投稿激励计划已启动!


GeekOnline编程挑战赛

Geek Online 2020 编程挑战赛官网

重磅!Geek Online 2020 编程挑战赛来了!

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的开发工程师,坐标合肥,待遇从优

和50万优质程序员一起成长——程序员客栈招聘

持续更新....

超大规模会议技术优化策略 轻松实现 500 人线上流畅沟通

WebRTC梅川酷子 发表了文章 • 0 个评论 • 21 次浏览 • 5 天前 • 来自相关话题

受疫情影响,许多公司已经形成线上办公习惯,尤其是在线音视频会议,已经成为一种常态。对于一些大型企业和组织机构来说,分支机构遍布全国各地,员工异地参会人数众多,大规模音视频会议成为刚需。而当前音视频会议主流产品中,单个会议最多支持 500 人入会进行互动。但是 ... ...查看全部
受疫情影响,许多公司已经形成线上办公习惯,尤其是在线音视频会议,已经成为一种常态。对于一些大型企业和组织机构来说,分支机构遍布全国各地,员工异地参会人数众多,大规模音视频会议成为刚需。而当前音视频会议主流产品中,单个会议最多支持 500 人入会进行互动。


但是 500 人同时线上开会,对于资源消耗比较高。而传统的 WebRTC 架构并不擅长超过 200 人以上的会议场景。在面对超大规模会议室、聊天室、直播等各种复杂场景时,对流进行按需合流,可以降低带宽占用和设备压力;对流进行有选择的订阅分发,有助于扩展各种组合场景。针对 App 具体的应用场景,可以配合订阅分发模式,组合使用 SFU 和 MCU 架构。下来我们将详细分析一下大规模会议的资源优化策略。

1.超大规模会议架构对比


WebRTC 多对多网络架构有 P2P、MCU、SFU 三种。各种网络拓扑的优缺点如下:

1.png

SFU 方式灵活,只要降低带宽就可以实现大规模会议的要求。


2.超大规模会议中存在的挑战


在超过 20 人会议场景下,SFU 及 WebRTC 兼容场景仍然无法很好的解决。如果直接选择参会人之间进行音视频互动,音视频数据完全转发对服务器资源的要求是巨大的,如果会议中有大量人员同时接入,服务端上行流量和下行流量陡增,会对服务器造成巨大压力。


这里我们来对比一下 20 人与 200 人同时参加音视频会议时,对服务端造成压力的差距:
20人
各端流量:
20*(1Mbps+32Kbps)=20.64Mbps
服务端上行流量:
20*(1Mbps+32Kbps)=20.64Mbps
服务端下行流量:
20*(20-1)*(1Mbps+32Kbps)=392.16Mbps
200人
各端流量:
200*(1Mbps+32Kbps)=206.4Mbps
服务端上行流量:
200*(1Mbps+32Kbps)=206.4Mbps
服务端下行流量:
200*(200-1)*(1Mbps+32Kbps)=41.07Gbps

从对比结果中可以看出,服务端下行流量直接上升了一个量级。如果采用视频按需订阅,音频选择出音量最大的几路可以大大降低下行流量。比如每个客户端订阅 4 路视频,服务器只需下发 4 路音量最大的音频,服务端下行流量只需要 200*4*(1Mbps+32Kbps)=800+25.6=825.6Mbps,可以极大缓解服务器压力。
 
若要解决上面的问题,建议通过按需订阅与转发、音频流量两个方面来制定策略,在保证效果的前提下,降低服务端的压力。

3.按需订阅与转发以及音频流量优化策略


3.1 按需订阅与转发


按需订阅与转发的方式有:
支持单独订阅某个人的某路视频或某路音频。
接收端仅订阅正在说话的人的视频,音频全部订阅。
融云 SDK 支持发送端视频编码支持大小流。接收端按需订阅大流或小流。大流的清晰度高,码率高;小流的清晰度低,码率低。这样当接收端想观看清晰视频的时候订阅大流;对清晰度要求不高的时候订阅小流。另外,弱网下自动切换大小流,可以保证视频的流畅性。

3.2 音频流量优化策略

针对音频全部订阅有以下几种优化音频流量的方法。

3.2.1 发送端静音时不发送数据
WebRTC 的音频 codec 如果采用 Opus,可以开启 Opus 的 DTX(Discontinuous Transmission)。SDP 对应的设置为 usedtx=1。但测试中发现流量下降不如预期,因为用户的使用环境多少有点背景音。背景音量很容易超出静音阈值。像 Android/iOS 这种定制开发端可以手动调整静音阈值,而 PC 的 Web 端因为是浏览器,则无法调整静音阈值。

3.2.2 调整音频码率
通过设置客户端上音频码率,降低客户端上行的音频码率。当音频路数跟多的时候,限定每一路的音频码率后,总的音频码率会减少很多。SDP 设置方式 b=AS:码率。下面是摘自 RFC3556 的原文:

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).
3.2.3 服务器下发音量 Top N 路

客户端收到音频流,在音频解码后,默认一般仅混流播放音量最大的 3(WebRTC 中的 kMaximumAmountOfMixedAudioSources 值)路声音。所以避免不必要的音频包的转发可以减少服务流量的。步骤如下:
发送端通过 Audio Level 标识音频能量。
音频包进入 SFU 转发队列,先进入计算队列,定期弹出 Top N 的音频包。
只有有效音频包,会进入到下行分发队列。
 
下面介绍音频如何转发音量最大几路的方法实践。

4. 音频 Top N 选择


4.1 客户端处理

客户端会计算出音量大小,并把值记录在 RTP 包中。所以客户端需要开启 audio-level 的 RTP 扩展, 如下: a=extmap:1urn:ietf:params:rtp-hdrext:ssrc-audio-level 开启这个 RTP 扩展后,WebRTC 客户端机会计算 audio 包的音量大小。这个音量大小计算方法 RFC6464 有明确定义。WebRTC 中的计算方法为 modules/audio_processing/rms_level.cc 的 ComputeRms 方法:

// 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);
}
客户端告诉服务器音频包的音量大小。服务器收到音频包后不用做解码,就能知道从客户端上来的音频包的音量值,为后面的服务器音频包下发策略奠定了基础。


4.2 服务器处理

下面用 Publisher 表示发布者的音频流,Subscriber 表示订阅者的音频流。RtpAudioPacket 表示一个音频包。RtpAudioPacket 里有个 mute 属性,标记这个音频包时是否静音。

在没有音频根据音量大小转发的逻辑前,Publisher 和 Subscriber 的处理关系如下。


2.png

Subscriber1、Subscriber2、Subscriber3 订阅 Publisher1、Publisher2、Publisher3。Publisher 发上来的音频包都会转发给各自的订阅者。


音频根据音量大小转发的逻辑如下:
AudioLevelHandler 表示每个 Publisher 的音频处理单元。AudioLevelHandler 里有两个音频包缓冲队列,计算队列 calculate_queue 和发送队列 send_queue。Publisher 的音频包先进入计算队列 calculate_queue 中。有个定时计算任务 AudioLevelCalculator。AudioLevelCalculator 会每隔一个音频打包时间 ptime 进行一次对所有 Publisher 的计算队列里音频包的 audio_level 均值(因为均值表示这个 Publisher 收到的若干个音频包的音量)做排序计算,选出音量值最大的几路。这几路的音频包 RtpAudioPacket 的 mute 被标记为 false,而其他音频包标记为 true。
排序后,这些音频包会从计算队列里移入到发送队列 send_queue 中。
之后音频包从 send_queue 出队,转发给 Subscriber。Subscriber 中的 MuteHandler 有以下两个作用:
a. 根据 RtpAudioPacket 的 mute 属性,mute 为 true 时,这个音频包直接被吞掉,false 表示转发给订阅者。
b. 因为下发给订阅者的音频包 RTP 序号 SeqNum 不是连续的,需要做连续化处理。

下面图中 Subscriber1、Subscriber2、Subscriber3 订阅 Publisher1、Publisher2、Publisher3。假设 Publisher1 收到的当前音量最大,最终只有它的音频包会转发给 Subscriber1、Subscriber2、Subscriber3。

3.png

4.3 级联的考虑


比如下面的图中,Subscriber4 通过级联服务器连接到当前 MediaServer 上。Publisher1、Publisher2、Publisher3 的音频包都会直接转发级联服务器。由级联服务器负责计算 Top N 音频包的计算下发给 Subscriber4。

5.png

下面是这部逻辑的伪代码:
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
        }
    }
}
4.4 音频下发策略优化


现实中人的说话是有停顿的。比如停顿前后人声比较大,如果简单的排序下发音频包,客户端会收到连续的非静音包。经测试,这样的体验并不理想,因此需要加入平滑处理。这里 history 为过去若干次的音频是否进入 Top N。音频包是最大的几路中的,加入 history 队列尾部加入 true,转发表示此次声音大而发。否则,加入 history 队列尾部加入 false。因为本次静音,还需判断过去的静音情况,若 history 中有 true 值,转发可表示过去一小段说过话,所以需要转发。若 history 中全为 false, 不转发则表示本次声音不大,过去一小段声音也不大,所以不转。

4.5 其他相关策略

当会议中的人数相对比较的少的时候,音频包为上面所述的正常转发。而当多个 Publisher 的订阅人数超过某个阈值(比如 50),此时 MediaServer 发的音频码率很大,对应客户端也要收相应的音频流量。这时可以走超大会议音频最大几路转发逻辑。而当会议中多个 Publisher 的订阅人数下降到阈值之下,再回归正常的转发逻辑。
经过选取最大几路流的下发方式,音频流量已经大大降低了。而在此基础上实际设置的选取路数做少许冗余,可以多发一些有音量的音频包,提高接收方体验。
当参会者增加时,相应的 MediaServer 也需要动态调度。通过把参会者音视频流打到多个 MediaServer 上,通过级联的方式解决问题,保证每台 MediaServer 服务器上的 CPU、内存、带宽的正常。

5. 总结


以上是基于超大规模会议技术优化进行的策略方面的探索。其主要思想是视频按需订阅,音频降低不必要的流量。其中涉及客户端音量值的上传、服务器端音量选择、级联、优化体验、减少音频流量等多个方面。研发过程中,超大会议需要多测试,才能暴露其中的问题,从而提高最终的会议体验。


RTC vs RTMP,适合的才是最好的!

WebRTC赵炳东 发表了文章 • 0 个评论 • 143 次浏览 • 2021-01-29 16:17 • 来自相关话题

随着在线教育、电商直播、泛娱乐社交等 App 的普及,实时音视频技术的应用需求也越来越多元化。目前,市场中能够支持音视频通信的主流技术有“RTMP+CDN”和“RTC”两大阵营。选型时,开发者如何根据场景选择更适合自己的通信技术?这就要从两者的技术特点、价格、... ...查看全部

随着在线教育、电商直播、泛娱乐社交等 App 的普及,实时音视频技术的应用需求也越来越多元化。目前,市场中能够支持音视频通信的主流技术有“RTMP+CDN”和“RTC”两大阵营。选型时,开发者如何根据场景选择更适合自己的通信技术?这就要从两者的技术特点、价格、厂商服务综合考虑。1.png

RTMP+CDN 技术特点与适用场景

RTMP (Real Time Messaging Protocol)基于 TCP 的流媒体传输协议,最大的特点是与 CDN 的强绑定,需要借助 CDN 的负载均衡系统将内容推送到接近用户的边缘节点,使用户就近取得所需内容,提高用户访问的响应速度和成功率,解决因分布、带宽、服务器性能带来的访问延迟问题。更多适用于站点加速、点播、短视频等场景。

对于初次通过 CDN 服务来实现音视频通信的开发者来说,技术指标应主要关注延时、卡顿率、下载速度、打开速度、回源率、宽带冗余提升率等几个维度。

有研究表明,在 0.1s 以下的延迟,用户几乎是无感知的;1s 左右的延迟,用户会明显注意到延时的发生,但在该时间内思维依然是连贯的;超过 10s 的延时,用户会失去等待的耐心。在所有关键技术指标中,控制延时是 CDN 最需要提升的。

以直播场景为例,延时主要看 2 个核心指标:首播时间和再缓存时间。首播时间即从打开到看到视频画面的时间,会受域名解析、连接、第一包时间的影响,首播时间控制在 1 秒内算是不错的效果。其次是再缓冲时间,是用户观看视频时的卡顿时间。由于实际服务中视频长度不一,一般会做播放的体验统计,主要监测的是卡顿率。行业内而言,直播首播时间 300ms,卡顿率在 15% 以下算是优质的通信服务。

目前的 CDN,通常有 3-5 秒的延迟,在浏览图片、短视频等内容时用户感知不明显,对于不需要实时强互动的直播,比如体育赛事网络直播、演唱会网络直播、新闻现场直播,延迟是可以接受的,并不会影响用户体验。

2.png

而在线视频会议、在线教育、电商直播、远程医疗会诊这些对互动有非常高要求的场景,RTMP+CDN 的模式与这些场景对于低延时、无卡顿的要求有一定差距。这时,选择 RTC 技术才能更好地满足开发者的需求。

RTC 技术特点与适用场景

说到 RTC(Real Time Communication)实时音视频通信,它最大的特点就是低延时和无卡顿。从功能流程上说,它包含了采集、编码、前后处理、传输、解码、缓冲、渲染等诸多环节,RTC 不是靠“优化”各环节去实现的实时互动,而是依靠推流端实时的传输机制。

3.png

很多实时音视频服务专业厂商使用的就是 WebRTC 标准,这是一种基于浏览器的实时通信的开源解决方案,使用 UDP 私有协议来进行媒体推流,而不需要创建离散的媒体段;并且它是面向无连接的,没有 TCP 连接断开时的挥手确认连接关闭的机制,基于这两点,WebRTC 能够做到毫秒级的低延迟,远远低于基于 RTMP 协议的 CDN 分发的延迟。而且,它直接通过浏览器就可以完成推流和播放,对于开发者接入来说实在太方便。

因此,WebRTC 标准针对有高互动性要求的直播场景尤为适宜。以直播连麦为例,主播端把通信直播流发到观众端,同时也可以把观众端拉上麦,实现主播和观众的互动。使用 WebRTC,内容实时传输,主播和观众可以进行音视频连麦互动,实时沟通,延时一般低至 400ms 以内。

4.png

基于 WebRTC 标准的融云实时音视频服务,拥有超低延迟的优势,同时也支持将 RTC 音视频流合流(MCU)转码为 RTMP,并推流到第三方 CDN 上,保留了标准协议普遍被 CDN 网络支持的好处。目前,融云音视频通话,可做到全球端到端延时小于 400ms,最低延时 66ms;低延时互动直播的直播推流可以做到主播观众间延迟在 300ms 左右,保障端到端之间延迟无感知的实时互动。

CDN vs RTC 选型还需看价格服务综合比

一套实时音视频通信能力的搭建,除了要根据场景选择适合的技术外,还要看价格、服务的综合性价比。通常来说,使用 RTC 技术的成本比 RTMP+CDN 高。因为,从实践来看,UDP 传输比 TCP 传输对资源消耗要多,而且重传、封包、FEC 冗余计算等都会额外增加计算量,在多进程模式下可能还会遇到内存资源的过多消耗,这些都导致开发及使用成本的增加。

开发者选型中,性价比需综合技术特点、适用场景、价格和服务四个方面的全面考量。服务在产品上线前后的开发阶段和运营阶段,都要发挥重要作用。目前,开发者服务做得比较好的厂商比如融云,会与开发者共建开发文档,技术手册短视频化,提供场景化的 Demo,以及在官网搭建开发者专区,帮助开发者更便捷、更快速的理解 SDK。

融云全新升级的实时音视频服务,提出“以一套 SDK 解决所有通信场景”,使用融云 RTC 的开发者,同时可以用融云 IM 作为信令通道,而不用自己重新搭建或选择第三方信令通道,这样可以大大提升开发效率,减少 SDK 文档学习时间。

总体而言,RTC 低延迟直播是未来发展的趋势,而 RTMP 在当前依然拥有价格上的优势,而两者作为音视频领域的实用技术,无论是适用场景、还是贴近开发的服务都越来越多样化,开发者未来选型之路也将更顺畅。



万人群聊的消息分发控速方案

IM即时通讯徐凤年 发表了文章 • 0 个评论 • 114 次浏览 • 2021-01-21 11:04 • 来自相关话题

当前阶段,群聊已经成为主流IM软件的基本功能,不管是亲属群,朋友群亦或是工作群,都是非常常见的场景。随着移动互联网的发展,即时通讯服务被广泛应用到各个行业,客户业务快速发展,传统百人甚至千人上限的群聊已经无法满足很多业务发展需求,所以超大群的业务应运而生。&n... ...查看全部

当前阶段,群聊已经成为主流IM软件的基本功能,不管是亲属群,朋友群亦或是工作群,都是非常常见的场景。随着移动互联网的发展,即时通讯服务被广泛应用到各个行业,客户业务快速发展,传统百人甚至千人上限的群聊已经无法满足很多业务发展需求,所以超大群的业务应运而生。

 

1超大群面临的挑战

我们以一个万人群的模型进行举例:

1、如果群中有人发了消息,那么这条消息需要按照1:9999的比例进行分发投递,如果我们按照常规消息的处理流程,那么消息处理服务压力巨大。

2、消息量大的情况下,服务端向客户端直推消息的处理速度将会成为系统瓶颈,而一旦用户的消息下发队列造成了挤压,会影响到正常的消息分发,也会导致服务缓存使用量激增。

3、在微服务架构中,服务以及存储(DB,缓存)之间的QPS和网络流量也会急剧增高。

4、以群为单位的消息缓存,内存和存储开销较大(消息体的存储被放大了万倍)。

基于这些挑战,我们的服务势必要做一定的优化来应对。

 

2群消息分发模型

整体的群聊服务架构如下图所示:

1.png

   用户在群里发了一条群消息后,消息先到群组服务,然后通过群组服务缓存的群关系,锁定这条消息最终需要分发的目标用户,然后根据一定的策略分发到消息服务上,消息服务再根据用户的在线状态和消息状态来判断这条消息是直推、通知拉取还是转Push,最终投递给目标用户。

 

3超大群消息分发解决方案

3.1分发控速:

第一,首先我们会根据服务器的核数来建立多个群消息分发队列,这些队列我们设置了不同的休眠时间以及不同的消费线程数,这里可以理解为快、中、慢等队列。如下图所示:

2.png

第二,我们根据群成员数量的大小来将所有群映射到相应的队列中,规则是小群映射到快队列中,大群映射到相应的慢队列中。

第三,小群由于人数少,对服务的影响很小,所以服务利用快队列快速的将群消息分发出去,而大群群消息则利用慢队列的相对高延时来起到控速的作用。

3.2 合并分发:

一条群消息发送到IM服务器后,需要从群组服务投递给消息服务,如果每一个群成员都投递一次,并且投递的群消息内容是一致的话,那肯定会造成相应的资源浪费和服务压力。

服务落点计算中我们使用的是一致性哈希,群成员落点相对固定,所以落点一致的群成员我们可以合并成一次请求进行投递,这样就大幅提高了投递效率同时减少了服务的压力。

3.3 超大规模群的处理方案

在实际群聊业务中,还有一种业务场景是超大规模群,这种群的群人数达到了数十万甚至上百万,这种群如果按照上述的分发方案,势必也会造成消息节点的巨大压力。比如我们有一个十万人的群,消息节点五台,消息服务处理消息的上限是一秒钟4000条,那每台消息节点大约会分到2万条群消息,这超出了消息节点的处理能力。

所以为了避免上述问题,我们的超大群(群成员上线超过3000,可以根据服务器数量和服务器配置相应做调整)会用特殊的队列来处理群消息的分发,这个特殊的队列一秒钟往后端消息服务投递的消息数是消息服务处理上限的一半(留相应的能力处理其他消息),如果单台消息服务处理的QPS上限是4000,那群组服务一秒往单台消息服务最多投递2000条。

 

结束语

我们后续也会针对群消息进行引用分发,对于大群里发的消息体比较大的消息,我们给群成员只分发和缓存消息的索引,比如MessageID,等群成员真正拉取群消息时再从将消息组装好给客户端分发下去。这样做会节省分发的流量以及存储的空间。

随着互联网的发展,群组业务的模型和压力也在不停地扩展,后续可能还会遇到更多的挑战,届时我们服务器也会通过更优的处理方式来应对。

 

感兴趣的开发者可以扫码下载融云的 IM 即时通讯 Demo 产品:SealTalk,体验融云的群聊、聊天室等通信能力。

3.png

如何实现跨房间连麦功能

WebRTCadmin 发表了文章 • 0 个评论 • 93 次浏览 • 2020-12-24 18:13 • 来自相关话题

在社交娱乐、直播教育等业务场景中,为了增强趣味性和互动性,经常会设计一些主播PK的互动场景,将不同房间的主播拉入同一个房间内进行游戏互动,同时各主播原有房间的观众还能同时观看到自己关注的主播表演并进行打赏等互动,今天就跟大家分享如何实现跨房间连麦功能。跨房间连... ...查看全部

在社交娱乐、直播教育等业务场景中,为了增强趣味性和互动性,经常会设计一些主播PK的互动场景,将不同房间的主播拉入同一个房间内进行游戏互动,同时各主播原有房间的观众还能同时观看到自己关注的主播表演并进行打赏等互动,今天就跟大家分享如何实现跨房间连麦功能。

跨房间连麦场景:

场景1.两个直播间中的主播PK

场景2.四个直播间中的主播连麦

场景3.超级小班课

场景示例:

场景一

1607420662751-dd29a472-38aa-4df7-8a27-a1494f102234.png

场景二

1607420760291-5c6aebcf-bd9c-44ac-96d2-0091c66a2ab1.jpeg

场景三

1607421010097-e06a9a2c-53e1-4abf-a555-616f9ade3353.png


功能点

  1. 跨房间邀请过程(邀请,取消邀请,应答,应答通知房间内所有人)

  2. 跨房间媒体流合流

  3. 加入多RTC房间

  4. 订阅多房间人员的音视频流 (监听多房间事件)

  5. 支持观众订阅不变的情况能看到新加入的主播

  6. 离开副房间(自动取消订阅媒体流)

功能亮点

  1. 邀请信令

  2. 多房间支持6个房间,再大可以沟通适配

  3. 多房间稳定性

  4. 跨房间自定义布局

体验 Demo

集成 Demo 示例

融云在 GitHub 上提供了跨房间连麦快速集成 Demo quickdemo-pk (opens new window)代码示例,方便开发者参考。

跨房间连麦开发文档文档地址

iOS 基于实时音视频 SDK 实现屏幕共享功能——4

WebRTCadmin 发表了文章 • 0 个评论 • 424 次浏览 • 2020-12-03 18:36 • 来自相关话题

iOS 基于实时音视频 SDK 实现屏幕共享功能——1iOS 基于实时音视频 SDK 实现屏幕共享功能——2iOS 基于实时音视频 SDK 实现屏幕共享功能——3iOS 基于实时音视频 SDK 实现屏幕共享功能——41.7 工具类// // &nbs... ...查看全部

微信截图_20201203183601.png

iOS 基于实时音视频 SDK 实现屏幕共享功能——1

iOS 基于实时音视频 SDK 实现屏幕共享功能——2

iOS 基于实时音视频 SDK 实现屏幕共享功能——3

iOS 基于实时音视频 SDK 实现屏幕共享功能——4

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

WebRTCadmin 发表了文章 • 0 个评论 • 410 次浏览 • 2020-12-03 18:34 • 来自相关话题

iOS 基于实时音视频 SDK 实现屏幕共享功能——1iOS 基于实时音视频 SDK 实现屏幕共享功能——2iOS 基于实时音视频 SDK 实现屏幕共享功能——3iOS 基于实时音视频 SDK 实现屏幕共享功能——41.5 VideotoolBox 硬编码//... ...查看全部

微信截图_20201203183634.png

iOS 基于实时音视频 SDK 实现屏幕共享功能——1

iOS 基于实时音视频 SDK 实现屏幕共享功能——2

iOS 基于实时音视频 SDK 实现屏幕共享功能——3

iOS 基于实时音视频 SDK 实现屏幕共享功能——4

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

WebRTCadmin 发表了文章 • 0 个评论 • 402 次浏览 • 2020-12-03 18:26 • 来自相关话题

iOS 基于实时音视频 SDK 实现屏幕共享功能——1iOS 基于实时音视频 SDK 实现屏幕共享功能——2iOS 基于实时音视频 SDK 实现屏幕共享功能——3iOS 基于实时音视频 SDK 实现屏幕共享功能——4这里的核心思想是拿到屏幕共享的数据之后,先进... ...查看全部

微信截图_20201203182500.png


iOS 基于实时音视频 SDK 实现屏幕共享功能——1

iOS 基于实时音视频 SDK 实现屏幕共享功能——2

iOS 基于实时音视频 SDK 实现屏幕共享功能——3

iOS 基于实时音视频 SDK 实现屏幕共享功能——4



这里的核心思想是拿到屏幕共享的数据之后,先进行压缩,当压缩完成后会通过回调上报给当前类。既而通过 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

WebRTCadmin 发表了文章 • 0 个评论 • 412 次浏览 • 2020-12-03 18:21 • 来自相关话题

iOS 基于实时音视频 SDK 实现屏幕共享功能——1iOS 基于实时音视频 SDK 实现屏幕共享功能——2iOS 基于实时音视频 SDK 实现屏幕共享功能——3iOS 基于实时音视频 SDK 实现屏幕共享功能——4Replaykit 介绍在之前的 iOS 版... ...查看全部

微信截图_20201203181458.png

iOS 基于实时音视频 SDK 实现屏幕共享功能——1

iOS 基于实时音视频 SDK 实现屏幕共享功能——2

iOS 基于实时音视频 SDK 实现屏幕共享功能——3

iOS 基于实时音视频 SDK 实现屏幕共享功能——4

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、Ope... ...查看全部

今天,我们来一起学习一下 WebRTC,相信你已经对这个前端音视频网红儿有所耳闻了。

WebRTC Web Real-Time Communication 网页即时通信

WebRTC 于 2011 年 6 月 1 日开源,并在 Google、Mozilla、Opera 等大佬们的支持下被纳入 W3C 推荐标准,它给浏览器和移动应用提供了即时通信的能力。

WebRTC 优势及应用场景

优势

  • 跨平台(Web、Windows、MacOS、Linux、iOS、Android)
  • 实时传输
  • 音视频引擎
  • 免费、免插件、免安装
  • 主流浏览器支持
  • 强大的打洞能力

应用场景

在线教育、在线医疗、音视频会议、即时通讯工具、直播、共享远程桌面、P2P网络加速、游戏(狼人杀、线上KTV)等。

1.png

(有喜欢玩狼人杀的同学吗?有时间可以一起来一局,给我一轮听发言的时间,给你裸点狼坑,一个坑容错。)

WebRTC 整体架构

拉回来,我们看一看 WebRTC 的整体架构,我用不同的颜色标识出了各层级所代表的含义。

2.png

  • 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 并不是一个真正的协议,它就是一种描述各端“能力”的数据格式。

3.png

上图所示就是 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集成 — 通知检查融云 IM SDK 集成 —- 刷新会话界面... ...查看全部

本文收录了GeekOnline社区精华内容,希望帮助社区开发者学习IM+RTC知识,解答疑惑。赠人玫瑰,手有余香,如您有不错的内容需要收录,欢迎在在评论区投稿回复。

微信截图_20201207144054.png

Android篇

融云即时通讯SDK集成 — 通知检查

融云 IM SDK 集成 —- 刷新会话界面和会话列表界面

Android 端如何添加自定义表情

解决融云 SDK 4.0 版本配置 https 导航报 SSLHandshakeException

融云清空历史消息 Android 端

唠一唠融云的消息扩展功能

融云 IMkit 拦截或监听所有发送消息

融云如何把图片消息的图片上传到自己的文件服务器

唠一唠融云 VIVO push 无法跳转的解决方案

融云即时通讯SDK集成 — 定制UI(一) ——会话界面小改动

融云即时通讯SDK集成 — 定制UI(二) ——添加自定义表情库

融云即时通讯SDK集成 — 定制UI(三) ——兼容Android Q

融云如何把图片消息的图片上传到自己的文件服务器

融云即时通讯SDK集成 — 华为推送的点击跳转处理

带你实现女朋友欲罢不能的 App

Flutter 集成融云 sdk

配置融云SDK的自签证书

自定义消息 包含 list 数组

关于融云聊天室KV 值的正确使用

融云 IM SDK 转 AndroidX

融云即时通讯SDK集成 — 国内厂商推送集成踩坑篇(Android平台)

在融云 IMkit 会话界面基础上添加消息已读未读

融云聊天室属性 kv

融云 ConversationListFragment 会话列表添加头部布局

融云即时通讯SDK集成 — FCM推送集成指南(Android平台)

融云集成之避坑指南-Android推送篇

融云IMKit 动态删除或添加plugin 的实现


iOS篇

iOS 基于实时音视频 SDK 实现屏幕共享功能——1

iOS 基于实时音视频 SDK 实现屏幕共享功能——2

iOS 基于实时音视频 SDK 实现屏幕共享功能——3

iOS 基于实时音视频 SDK 实现屏幕共享功能——4

如何隐藏融云输入框语音按钮

给融云的输入框上方加个功能按钮,怎么整?

融云 IM SDK 如何插入消息

集成融云 IMLib 时,如何实现一套类似于 IMKit 的用户信息管理机制

为融云聊天页面的输入框添加 Placeholder

30 分钟集成融云 IM 即时通讯

简单介绍融云 imkit 包含功能

融云的聊天页面在 iOS14 出现崩溃的解决办法

融云聊天页面长按消息后“翻译”功能的实现方法

使用融云 IM 点击最近聊天记录时跳转到 @ 自己的消息

如何设置融云用户信息

自定义融云会话列表 cell 选中背景

融云 IMKit 音频录制参数

融云会话页面刷新不及时问题

融云 Flutter IM SDK 解析

关于融云 SDK 在使用 p8 证书的坎坷~

融云 SDK 如何实现群组操作

如何利用融云 IMLib 来实现一个阅后即焚功能

干货分享——使用融云通讯能力库 IMLib 实现单群聊的阅读回执


Web篇

作为小白接融云 IM SDK 新路体验~

微信小程序集成融云 SDK (即时通讯) 集成必备条件

Web 端使用融云 SDK 集成实现滑动加载历史消息

融云IM SDK web 端集成 — 表情采坑篇

融云 Web SDK 如何实现表情的收发 ?

集成融云小程序 SDK 遇到的问题

使用融云 Web SDK 撤回消息

融云 RTC SDK 集成实现直播,趟坑之旅~~~

融云 Web SDK 删除历史消息

集成融云小程序 SDK 遇到的问题

Web 端集成融云 SDK 如何发送正确图片消息给移动端展示?

使用融云 IM SDK 实现 H5 直播聊天

WebRTC 实现实时音视频技术研究

融云发送语音消息

融云 CallLib 集成遇到的问题

结合融云 WebSDK 了解 WebSocket 基本原理

集成融云 Web 音视频通话踩坑之旅

SDK 兼容 JSON

融云 IM SDK 发送语音消息

集成融云 IM 问题总结

融云 Web SDK 如何实现只有一个设备登入

融云 Web 播放声音 — Flash 篇 (播放 AMR、WAV)

融云 IM 那些事儿

融云 AMR(Aduio) 播放 AMR 格式 Base64 码音频


社区福利

【领取见面礼】限量 100份 GeekOnline加油包!等你来拿

【有奖调研】Geek Online 2020 编程挑战赛参赛调研

【征稿活动】Geek Online 社区第一期投稿激励计划已启动!


GeekOnline编程挑战赛

Geek Online 2020 编程挑战赛官网

重磅!Geek Online 2020 编程挑战赛来了!

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的开发工程师,坐标合肥,待遇从优

和50万优质程序员一起成长——程序员客栈招聘

持续更新....

【社区精华|持续更新】收录本社区精华内容,手把手教学IM/RTC开发!

IM即时通讯admin 发表了文章 • 8 个评论 • 537 次浏览 • 2020-12-07 14:41 • 来自相关话题

本文收录了GeekOnline社区精华内容,希望帮助社区开发者学习IM+RTC知识,解答疑惑。赠人玫瑰,手有余香,如您有不错的内容需要收录,欢迎在在评论区投稿回复。Android篇融云即时通讯SDK集成 — 通知检查融云 IM SDK 集成 —- 刷新会话界面... ...查看全部

本文收录了GeekOnline社区精华内容,希望帮助社区开发者学习IM+RTC知识,解答疑惑。赠人玫瑰,手有余香,如您有不错的内容需要收录,欢迎在在评论区投稿回复。

微信截图_20201207144054.png

Android篇

融云即时通讯SDK集成 — 通知检查

融云 IM SDK 集成 —- 刷新会话界面和会话列表界面

Android 端如何添加自定义表情

解决融云 SDK 4.0 版本配置 https 导航报 SSLHandshakeException

融云清空历史消息 Android 端

唠一唠融云的消息扩展功能

融云 IMkit 拦截或监听所有发送消息

融云如何把图片消息的图片上传到自己的文件服务器

唠一唠融云 VIVO push 无法跳转的解决方案

融云即时通讯SDK集成 — 定制UI(一) ——会话界面小改动

融云即时通讯SDK集成 — 定制UI(二) ——添加自定义表情库

融云即时通讯SDK集成 — 定制UI(三) ——兼容Android Q

融云如何把图片消息的图片上传到自己的文件服务器

融云即时通讯SDK集成 — 华为推送的点击跳转处理

带你实现女朋友欲罢不能的 App

Flutter 集成融云 sdk

配置融云SDK的自签证书

自定义消息 包含 list 数组

关于融云聊天室KV 值的正确使用

融云 IM SDK 转 AndroidX

融云即时通讯SDK集成 — 国内厂商推送集成踩坑篇(Android平台)

在融云 IMkit 会话界面基础上添加消息已读未读

融云聊天室属性 kv

融云 ConversationListFragment 会话列表添加头部布局

融云即时通讯SDK集成 — FCM推送集成指南(Android平台)

融云集成之避坑指南-Android推送篇

融云IMKit 动态删除或添加plugin 的实现


iOS篇

iOS 基于实时音视频 SDK 实现屏幕共享功能——1

iOS 基于实时音视频 SDK 实现屏幕共享功能——2

iOS 基于实时音视频 SDK 实现屏幕共享功能——3

iOS 基于实时音视频 SDK 实现屏幕共享功能——4

如何隐藏融云输入框语音按钮

给融云的输入框上方加个功能按钮,怎么整?

融云 IM SDK 如何插入消息

集成融云 IMLib 时,如何实现一套类似于 IMKit 的用户信息管理机制

为融云聊天页面的输入框添加 Placeholder

30 分钟集成融云 IM 即时通讯

简单介绍融云 imkit 包含功能

融云的聊天页面在 iOS14 出现崩溃的解决办法

融云聊天页面长按消息后“翻译”功能的实现方法

使用融云 IM 点击最近聊天记录时跳转到 @ 自己的消息

如何设置融云用户信息

自定义融云会话列表 cell 选中背景

融云 IMKit 音频录制参数

融云会话页面刷新不及时问题

融云 Flutter IM SDK 解析

关于融云 SDK 在使用 p8 证书的坎坷~

融云 SDK 如何实现群组操作

如何利用融云 IMLib 来实现一个阅后即焚功能

干货分享——使用融云通讯能力库 IMLib 实现单群聊的阅读回执


Web篇

作为小白接融云 IM SDK 新路体验~

微信小程序集成融云 SDK (即时通讯) 集成必备条件

Web 端使用融云 SDK 集成实现滑动加载历史消息

融云IM SDK web 端集成 — 表情采坑篇

融云 Web SDK 如何实现表情的收发 ?

集成融云小程序 SDK 遇到的问题

使用融云 Web SDK 撤回消息

融云 RTC SDK 集成实现直播,趟坑之旅~~~

融云 Web SDK 删除历史消息

集成融云小程序 SDK 遇到的问题

Web 端集成融云 SDK 如何发送正确图片消息给移动端展示?

使用融云 IM SDK 实现 H5 直播聊天

WebRTC 实现实时音视频技术研究

融云发送语音消息

融云 CallLib 集成遇到的问题

结合融云 WebSDK 了解 WebSocket 基本原理

集成融云 Web 音视频通话踩坑之旅

SDK 兼容 JSON

融云 IM SDK 发送语音消息

集成融云 IM 问题总结

融云 Web SDK 如何实现只有一个设备登入

融云 Web 播放声音 — Flash 篇 (播放 AMR、WAV)

融云 IM 那些事儿

融云 AMR(Aduio) 播放 AMR 格式 Base64 码音频


社区福利

【领取见面礼】限量 100份 GeekOnline加油包!等你来拿

【有奖调研】Geek Online 2020 编程挑战赛参赛调研

【征稿活动】Geek Online 社区第一期投稿激励计划已启动!


GeekOnline编程挑战赛

Geek Online 2020 编程挑战赛官网

重磅!Geek Online 2020 编程挑战赛来了!

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的开发工程师,坐标合肥,待遇从优

和50万优质程序员一起成长——程序员客栈招聘

持续更新....

超大规模会议技术优化策略 轻松实现 500 人线上流畅沟通

WebRTC梅川酷子 发表了文章 • 0 个评论 • 21 次浏览 • 5 天前 • 来自相关话题

受疫情影响,许多公司已经形成线上办公习惯,尤其是在线音视频会议,已经成为一种常态。对于一些大型企业和组织机构来说,分支机构遍布全国各地,员工异地参会人数众多,大规模音视频会议成为刚需。而当前音视频会议主流产品中,单个会议最多支持 500 人入会进行互动。但是 ... ...查看全部
受疫情影响,许多公司已经形成线上办公习惯,尤其是在线音视频会议,已经成为一种常态。对于一些大型企业和组织机构来说,分支机构遍布全国各地,员工异地参会人数众多,大规模音视频会议成为刚需。而当前音视频会议主流产品中,单个会议最多支持 500 人入会进行互动。


但是 500 人同时线上开会,对于资源消耗比较高。而传统的 WebRTC 架构并不擅长超过 200 人以上的会议场景。在面对超大规模会议室、聊天室、直播等各种复杂场景时,对流进行按需合流,可以降低带宽占用和设备压力;对流进行有选择的订阅分发,有助于扩展各种组合场景。针对 App 具体的应用场景,可以配合订阅分发模式,组合使用 SFU 和 MCU 架构。下来我们将详细分析一下大规模会议的资源优化策略。

1.超大规模会议架构对比


WebRTC 多对多网络架构有 P2P、MCU、SFU 三种。各种网络拓扑的优缺点如下:

1.png

SFU 方式灵活,只要降低带宽就可以实现大规模会议的要求。


2.超大规模会议中存在的挑战


在超过 20 人会议场景下,SFU 及 WebRTC 兼容场景仍然无法很好的解决。如果直接选择参会人之间进行音视频互动,音视频数据完全转发对服务器资源的要求是巨大的,如果会议中有大量人员同时接入,服务端上行流量和下行流量陡增,会对服务器造成巨大压力。


这里我们来对比一下 20 人与 200 人同时参加音视频会议时,对服务端造成压力的差距:
20人
各端流量:
20*(1Mbps+32Kbps)=20.64Mbps
服务端上行流量:
20*(1Mbps+32Kbps)=20.64Mbps
服务端下行流量:
20*(20-1)*(1Mbps+32Kbps)=392.16Mbps
200人
各端流量:
200*(1Mbps+32Kbps)=206.4Mbps
服务端上行流量:
200*(1Mbps+32Kbps)=206.4Mbps
服务端下行流量:
200*(200-1)*(1Mbps+32Kbps)=41.07Gbps

从对比结果中可以看出,服务端下行流量直接上升了一个量级。如果采用视频按需订阅,音频选择出音量最大的几路可以大大降低下行流量。比如每个客户端订阅 4 路视频,服务器只需下发 4 路音量最大的音频,服务端下行流量只需要 200*4*(1Mbps+32Kbps)=800+25.6=825.6Mbps,可以极大缓解服务器压力。
 
若要解决上面的问题,建议通过按需订阅与转发、音频流量两个方面来制定策略,在保证效果的前提下,降低服务端的压力。

3.按需订阅与转发以及音频流量优化策略


3.1 按需订阅与转发


按需订阅与转发的方式有:
支持单独订阅某个人的某路视频或某路音频。
接收端仅订阅正在说话的人的视频,音频全部订阅。
融云 SDK 支持发送端视频编码支持大小流。接收端按需订阅大流或小流。大流的清晰度高,码率高;小流的清晰度低,码率低。这样当接收端想观看清晰视频的时候订阅大流;对清晰度要求不高的时候订阅小流。另外,弱网下自动切换大小流,可以保证视频的流畅性。

3.2 音频流量优化策略

针对音频全部订阅有以下几种优化音频流量的方法。

3.2.1 发送端静音时不发送数据
WebRTC 的音频 codec 如果采用 Opus,可以开启 Opus 的 DTX(Discontinuous Transmission)。SDP 对应的设置为 usedtx=1。但测试中发现流量下降不如预期,因为用户的使用环境多少有点背景音。背景音量很容易超出静音阈值。像 Android/iOS 这种定制开发端可以手动调整静音阈值,而 PC 的 Web 端因为是浏览器,则无法调整静音阈值。

3.2.2 调整音频码率
通过设置客户端上音频码率,降低客户端上行的音频码率。当音频路数跟多的时候,限定每一路的音频码率后,总的音频码率会减少很多。SDP 设置方式 b=AS:码率。下面是摘自 RFC3556 的原文:

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).
3.2.3 服务器下发音量 Top N 路

客户端收到音频流,在音频解码后,默认一般仅混流播放音量最大的 3(WebRTC 中的 kMaximumAmountOfMixedAudioSources 值)路声音。所以避免不必要的音频包的转发可以减少服务流量的。步骤如下:
发送端通过 Audio Level 标识音频能量。
音频包进入 SFU 转发队列,先进入计算队列,定期弹出 Top N 的音频包。
只有有效音频包,会进入到下行分发队列。
 
下面介绍音频如何转发音量最大几路的方法实践。

4. 音频 Top N 选择


4.1 客户端处理

客户端会计算出音量大小,并把值记录在 RTP 包中。所以客户端需要开启 audio-level 的 RTP 扩展, 如下: a=extmap:1urn:ietf:params:rtp-hdrext:ssrc-audio-level 开启这个 RTP 扩展后,WebRTC 客户端机会计算 audio 包的音量大小。这个音量大小计算方法 RFC6464 有明确定义。WebRTC 中的计算方法为 modules/audio_processing/rms_level.cc 的 ComputeRms 方法:

// 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);
}
客户端告诉服务器音频包的音量大小。服务器收到音频包后不用做解码,就能知道从客户端上来的音频包的音量值,为后面的服务器音频包下发策略奠定了基础。


4.2 服务器处理

下面用 Publisher 表示发布者的音频流,Subscriber 表示订阅者的音频流。RtpAudioPacket 表示一个音频包。RtpAudioPacket 里有个 mute 属性,标记这个音频包时是否静音。

在没有音频根据音量大小转发的逻辑前,Publisher 和 Subscriber 的处理关系如下。


2.png

Subscriber1、Subscriber2、Subscriber3 订阅 Publisher1、Publisher2、Publisher3。Publisher 发上来的音频包都会转发给各自的订阅者。


音频根据音量大小转发的逻辑如下:
AudioLevelHandler 表示每个 Publisher 的音频处理单元。AudioLevelHandler 里有两个音频包缓冲队列,计算队列 calculate_queue 和发送队列 send_queue。Publisher 的音频包先进入计算队列 calculate_queue 中。有个定时计算任务 AudioLevelCalculator。AudioLevelCalculator 会每隔一个音频打包时间 ptime 进行一次对所有 Publisher 的计算队列里音频包的 audio_level 均值(因为均值表示这个 Publisher 收到的若干个音频包的音量)做排序计算,选出音量值最大的几路。这几路的音频包 RtpAudioPacket 的 mute 被标记为 false,而其他音频包标记为 true。
排序后,这些音频包会从计算队列里移入到发送队列 send_queue 中。
之后音频包从 send_queue 出队,转发给 Subscriber。Subscriber 中的 MuteHandler 有以下两个作用:
a. 根据 RtpAudioPacket 的 mute 属性,mute 为 true 时,这个音频包直接被吞掉,false 表示转发给订阅者。
b. 因为下发给订阅者的音频包 RTP 序号 SeqNum 不是连续的,需要做连续化处理。

下面图中 Subscriber1、Subscriber2、Subscriber3 订阅 Publisher1、Publisher2、Publisher3。假设 Publisher1 收到的当前音量最大,最终只有它的音频包会转发给 Subscriber1、Subscriber2、Subscriber3。

3.png

4.3 级联的考虑


比如下面的图中,Subscriber4 通过级联服务器连接到当前 MediaServer 上。Publisher1、Publisher2、Publisher3 的音频包都会直接转发级联服务器。由级联服务器负责计算 Top N 音频包的计算下发给 Subscriber4。

5.png

下面是这部逻辑的伪代码:
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
        }
    }
}
4.4 音频下发策略优化


现实中人的说话是有停顿的。比如停顿前后人声比较大,如果简单的排序下发音频包,客户端会收到连续的非静音包。经测试,这样的体验并不理想,因此需要加入平滑处理。这里 history 为过去若干次的音频是否进入 Top N。音频包是最大的几路中的,加入 history 队列尾部加入 true,转发表示此次声音大而发。否则,加入 history 队列尾部加入 false。因为本次静音,还需判断过去的静音情况,若 history 中有 true 值,转发可表示过去一小段说过话,所以需要转发。若 history 中全为 false, 不转发则表示本次声音不大,过去一小段声音也不大,所以不转。

4.5 其他相关策略

当会议中的人数相对比较的少的时候,音频包为上面所述的正常转发。而当多个 Publisher 的订阅人数超过某个阈值(比如 50),此时 MediaServer 发的音频码率很大,对应客户端也要收相应的音频流量。这时可以走超大会议音频最大几路转发逻辑。而当会议中多个 Publisher 的订阅人数下降到阈值之下,再回归正常的转发逻辑。
经过选取最大几路流的下发方式,音频流量已经大大降低了。而在此基础上实际设置的选取路数做少许冗余,可以多发一些有音量的音频包,提高接收方体验。
当参会者增加时,相应的 MediaServer 也需要动态调度。通过把参会者音视频流打到多个 MediaServer 上,通过级联的方式解决问题,保证每台 MediaServer 服务器上的 CPU、内存、带宽的正常。

5. 总结


以上是基于超大规模会议技术优化进行的策略方面的探索。其主要思想是视频按需订阅,音频降低不必要的流量。其中涉及客户端音量值的上传、服务器端音量选择、级联、优化体验、减少音频流量等多个方面。研发过程中,超大会议需要多测试,才能暴露其中的问题,从而提高最终的会议体验。


RTC vs RTMP,适合的才是最好的!

WebRTC赵炳东 发表了文章 • 0 个评论 • 143 次浏览 • 2021-01-29 16:17 • 来自相关话题

随着在线教育、电商直播、泛娱乐社交等 App 的普及,实时音视频技术的应用需求也越来越多元化。目前,市场中能够支持音视频通信的主流技术有“RTMP+CDN”和“RTC”两大阵营。选型时,开发者如何根据场景选择更适合自己的通信技术?这就要从两者的技术特点、价格、... ...查看全部

随着在线教育、电商直播、泛娱乐社交等 App 的普及,实时音视频技术的应用需求也越来越多元化。目前,市场中能够支持音视频通信的主流技术有“RTMP+CDN”和“RTC”两大阵营。选型时,开发者如何根据场景选择更适合自己的通信技术?这就要从两者的技术特点、价格、厂商服务综合考虑。1.png

RTMP+CDN 技术特点与适用场景

RTMP (Real Time Messaging Protocol)基于 TCP 的流媒体传输协议,最大的特点是与 CDN 的强绑定,需要借助 CDN 的负载均衡系统将内容推送到接近用户的边缘节点,使用户就近取得所需内容,提高用户访问的响应速度和成功率,解决因分布、带宽、服务器性能带来的访问延迟问题。更多适用于站点加速、点播、短视频等场景。

对于初次通过 CDN 服务来实现音视频通信的开发者来说,技术指标应主要关注延时、卡顿率、下载速度、打开速度、回源率、宽带冗余提升率等几个维度。

有研究表明,在 0.1s 以下的延迟,用户几乎是无感知的;1s 左右的延迟,用户会明显注意到延时的发生,但在该时间内思维依然是连贯的;超过 10s 的延时,用户会失去等待的耐心。在所有关键技术指标中,控制延时是 CDN 最需要提升的。

以直播场景为例,延时主要看 2 个核心指标:首播时间和再缓存时间。首播时间即从打开到看到视频画面的时间,会受域名解析、连接、第一包时间的影响,首播时间控制在 1 秒内算是不错的效果。其次是再缓冲时间,是用户观看视频时的卡顿时间。由于实际服务中视频长度不一,一般会做播放的体验统计,主要监测的是卡顿率。行业内而言,直播首播时间 300ms,卡顿率在 15% 以下算是优质的通信服务。

目前的 CDN,通常有 3-5 秒的延迟,在浏览图片、短视频等内容时用户感知不明显,对于不需要实时强互动的直播,比如体育赛事网络直播、演唱会网络直播、新闻现场直播,延迟是可以接受的,并不会影响用户体验。

2.png

而在线视频会议、在线教育、电商直播、远程医疗会诊这些对互动有非常高要求的场景,RTMP+CDN 的模式与这些场景对于低延时、无卡顿的要求有一定差距。这时,选择 RTC 技术才能更好地满足开发者的需求。

RTC 技术特点与适用场景

说到 RTC(Real Time Communication)实时音视频通信,它最大的特点就是低延时和无卡顿。从功能流程上说,它包含了采集、编码、前后处理、传输、解码、缓冲、渲染等诸多环节,RTC 不是靠“优化”各环节去实现的实时互动,而是依靠推流端实时的传输机制。

3.png

很多实时音视频服务专业厂商使用的就是 WebRTC 标准,这是一种基于浏览器的实时通信的开源解决方案,使用 UDP 私有协议来进行媒体推流,而不需要创建离散的媒体段;并且它是面向无连接的,没有 TCP 连接断开时的挥手确认连接关闭的机制,基于这两点,WebRTC 能够做到毫秒级的低延迟,远远低于基于 RTMP 协议的 CDN 分发的延迟。而且,它直接通过浏览器就可以完成推流和播放,对于开发者接入来说实在太方便。

因此,WebRTC 标准针对有高互动性要求的直播场景尤为适宜。以直播连麦为例,主播端把通信直播流发到观众端,同时也可以把观众端拉上麦,实现主播和观众的互动。使用 WebRTC,内容实时传输,主播和观众可以进行音视频连麦互动,实时沟通,延时一般低至 400ms 以内。

4.png

基于 WebRTC 标准的融云实时音视频服务,拥有超低延迟的优势,同时也支持将 RTC 音视频流合流(MCU)转码为 RTMP,并推流到第三方 CDN 上,保留了标准协议普遍被 CDN 网络支持的好处。目前,融云音视频通话,可做到全球端到端延时小于 400ms,最低延时 66ms;低延时互动直播的直播推流可以做到主播观众间延迟在 300ms 左右,保障端到端之间延迟无感知的实时互动。

CDN vs RTC 选型还需看价格服务综合比

一套实时音视频通信能力的搭建,除了要根据场景选择适合的技术外,还要看价格、服务的综合性价比。通常来说,使用 RTC 技术的成本比 RTMP+CDN 高。因为,从实践来看,UDP 传输比 TCP 传输对资源消耗要多,而且重传、封包、FEC 冗余计算等都会额外增加计算量,在多进程模式下可能还会遇到内存资源的过多消耗,这些都导致开发及使用成本的增加。

开发者选型中,性价比需综合技术特点、适用场景、价格和服务四个方面的全面考量。服务在产品上线前后的开发阶段和运营阶段,都要发挥重要作用。目前,开发者服务做得比较好的厂商比如融云,会与开发者共建开发文档,技术手册短视频化,提供场景化的 Demo,以及在官网搭建开发者专区,帮助开发者更便捷、更快速的理解 SDK。

融云全新升级的实时音视频服务,提出“以一套 SDK 解决所有通信场景”,使用融云 RTC 的开发者,同时可以用融云 IM 作为信令通道,而不用自己重新搭建或选择第三方信令通道,这样可以大大提升开发效率,减少 SDK 文档学习时间。

总体而言,RTC 低延迟直播是未来发展的趋势,而 RTMP 在当前依然拥有价格上的优势,而两者作为音视频领域的实用技术,无论是适用场景、还是贴近开发的服务都越来越多样化,开发者未来选型之路也将更顺畅。



万人群聊的消息分发控速方案

IM即时通讯徐凤年 发表了文章 • 0 个评论 • 114 次浏览 • 2021-01-21 11:04 • 来自相关话题

当前阶段,群聊已经成为主流IM软件的基本功能,不管是亲属群,朋友群亦或是工作群,都是非常常见的场景。随着移动互联网的发展,即时通讯服务被广泛应用到各个行业,客户业务快速发展,传统百人甚至千人上限的群聊已经无法满足很多业务发展需求,所以超大群的业务应运而生。&n... ...查看全部

当前阶段,群聊已经成为主流IM软件的基本功能,不管是亲属群,朋友群亦或是工作群,都是非常常见的场景。随着移动互联网的发展,即时通讯服务被广泛应用到各个行业,客户业务快速发展,传统百人甚至千人上限的群聊已经无法满足很多业务发展需求,所以超大群的业务应运而生。

 

1超大群面临的挑战

我们以一个万人群的模型进行举例:

1、如果群中有人发了消息,那么这条消息需要按照1:9999的比例进行分发投递,如果我们按照常规消息的处理流程,那么消息处理服务压力巨大。

2、消息量大的情况下,服务端向客户端直推消息的处理速度将会成为系统瓶颈,而一旦用户的消息下发队列造成了挤压,会影响到正常的消息分发,也会导致服务缓存使用量激增。

3、在微服务架构中,服务以及存储(DB,缓存)之间的QPS和网络流量也会急剧增高。

4、以群为单位的消息缓存,内存和存储开销较大(消息体的存储被放大了万倍)。

基于这些挑战,我们的服务势必要做一定的优化来应对。

 

2群消息分发模型

整体的群聊服务架构如下图所示:

1.png

   用户在群里发了一条群消息后,消息先到群组服务,然后通过群组服务缓存的群关系,锁定这条消息最终需要分发的目标用户,然后根据一定的策略分发到消息服务上,消息服务再根据用户的在线状态和消息状态来判断这条消息是直推、通知拉取还是转Push,最终投递给目标用户。

 

3超大群消息分发解决方案

3.1分发控速:

第一,首先我们会根据服务器的核数来建立多个群消息分发队列,这些队列我们设置了不同的休眠时间以及不同的消费线程数,这里可以理解为快、中、慢等队列。如下图所示:

2.png

第二,我们根据群成员数量的大小来将所有群映射到相应的队列中,规则是小群映射到快队列中,大群映射到相应的慢队列中。

第三,小群由于人数少,对服务的影响很小,所以服务利用快队列快速的将群消息分发出去,而大群群消息则利用慢队列的相对高延时来起到控速的作用。

3.2 合并分发:

一条群消息发送到IM服务器后,需要从群组服务投递给消息服务,如果每一个群成员都投递一次,并且投递的群消息内容是一致的话,那肯定会造成相应的资源浪费和服务压力。

服务落点计算中我们使用的是一致性哈希,群成员落点相对固定,所以落点一致的群成员我们可以合并成一次请求进行投递,这样就大幅提高了投递效率同时减少了服务的压力。

3.3 超大规模群的处理方案

在实际群聊业务中,还有一种业务场景是超大规模群,这种群的群人数达到了数十万甚至上百万,这种群如果按照上述的分发方案,势必也会造成消息节点的巨大压力。比如我们有一个十万人的群,消息节点五台,消息服务处理消息的上限是一秒钟4000条,那每台消息节点大约会分到2万条群消息,这超出了消息节点的处理能力。

所以为了避免上述问题,我们的超大群(群成员上线超过3000,可以根据服务器数量和服务器配置相应做调整)会用特殊的队列来处理群消息的分发,这个特殊的队列一秒钟往后端消息服务投递的消息数是消息服务处理上限的一半(留相应的能力处理其他消息),如果单台消息服务处理的QPS上限是4000,那群组服务一秒往单台消息服务最多投递2000条。

 

结束语

我们后续也会针对群消息进行引用分发,对于大群里发的消息体比较大的消息,我们给群成员只分发和缓存消息的索引,比如MessageID,等群成员真正拉取群消息时再从将消息组装好给客户端分发下去。这样做会节省分发的流量以及存储的空间。

随着互联网的发展,群组业务的模型和压力也在不停地扩展,后续可能还会遇到更多的挑战,届时我们服务器也会通过更优的处理方式来应对。

 

感兴趣的开发者可以扫码下载融云的 IM 即时通讯 Demo 产品:SealTalk,体验融云的群聊、聊天室等通信能力。

3.png

如何实现跨房间连麦功能

WebRTCadmin 发表了文章 • 0 个评论 • 93 次浏览 • 2020-12-24 18:13 • 来自相关话题

在社交娱乐、直播教育等业务场景中,为了增强趣味性和互动性,经常会设计一些主播PK的互动场景,将不同房间的主播拉入同一个房间内进行游戏互动,同时各主播原有房间的观众还能同时观看到自己关注的主播表演并进行打赏等互动,今天就跟大家分享如何实现跨房间连麦功能。跨房间连... ...查看全部

在社交娱乐、直播教育等业务场景中,为了增强趣味性和互动性,经常会设计一些主播PK的互动场景,将不同房间的主播拉入同一个房间内进行游戏互动,同时各主播原有房间的观众还能同时观看到自己关注的主播表演并进行打赏等互动,今天就跟大家分享如何实现跨房间连麦功能。

跨房间连麦场景:

场景1.两个直播间中的主播PK

场景2.四个直播间中的主播连麦

场景3.超级小班课

场景示例:

场景一

1607420662751-dd29a472-38aa-4df7-8a27-a1494f102234.png

场景二

1607420760291-5c6aebcf-bd9c-44ac-96d2-0091c66a2ab1.jpeg

场景三

1607421010097-e06a9a2c-53e1-4abf-a555-616f9ade3353.png


功能点

  1. 跨房间邀请过程(邀请,取消邀请,应答,应答通知房间内所有人)

  2. 跨房间媒体流合流

  3. 加入多RTC房间

  4. 订阅多房间人员的音视频流 (监听多房间事件)

  5. 支持观众订阅不变的情况能看到新加入的主播

  6. 离开副房间(自动取消订阅媒体流)

功能亮点

  1. 邀请信令

  2. 多房间支持6个房间,再大可以沟通适配

  3. 多房间稳定性

  4. 跨房间自定义布局

体验 Demo

集成 Demo 示例

融云在 GitHub 上提供了跨房间连麦快速集成 Demo quickdemo-pk (opens new window)代码示例,方便开发者参考。

跨房间连麦开发文档文档地址

iOS 基于实时音视频 SDK 实现屏幕共享功能——4

WebRTCadmin 发表了文章 • 0 个评论 • 424 次浏览 • 2020-12-03 18:36 • 来自相关话题

iOS 基于实时音视频 SDK 实现屏幕共享功能——1iOS 基于实时音视频 SDK 实现屏幕共享功能——2iOS 基于实时音视频 SDK 实现屏幕共享功能——3iOS 基于实时音视频 SDK 实现屏幕共享功能——41.7 工具类// // &nbs... ...查看全部

微信截图_20201203183601.png

iOS 基于实时音视频 SDK 实现屏幕共享功能——1

iOS 基于实时音视频 SDK 实现屏幕共享功能——2

iOS 基于实时音视频 SDK 实现屏幕共享功能——3

iOS 基于实时音视频 SDK 实现屏幕共享功能——4

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

WebRTCadmin 发表了文章 • 0 个评论 • 410 次浏览 • 2020-12-03 18:34 • 来自相关话题

iOS 基于实时音视频 SDK 实现屏幕共享功能——1iOS 基于实时音视频 SDK 实现屏幕共享功能——2iOS 基于实时音视频 SDK 实现屏幕共享功能——3iOS 基于实时音视频 SDK 实现屏幕共享功能——41.5 VideotoolBox 硬编码//... ...查看全部

微信截图_20201203183634.png

iOS 基于实时音视频 SDK 实现屏幕共享功能——1

iOS 基于实时音视频 SDK 实现屏幕共享功能——2

iOS 基于实时音视频 SDK 实现屏幕共享功能——3

iOS 基于实时音视频 SDK 实现屏幕共享功能——4

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

WebRTCadmin 发表了文章 • 0 个评论 • 402 次浏览 • 2020-12-03 18:26 • 来自相关话题

iOS 基于实时音视频 SDK 实现屏幕共享功能——1iOS 基于实时音视频 SDK 实现屏幕共享功能——2iOS 基于实时音视频 SDK 实现屏幕共享功能——3iOS 基于实时音视频 SDK 实现屏幕共享功能——4这里的核心思想是拿到屏幕共享的数据之后,先进... ...查看全部

微信截图_20201203182500.png


iOS 基于实时音视频 SDK 实现屏幕共享功能——1

iOS 基于实时音视频 SDK 实现屏幕共享功能——2

iOS 基于实时音视频 SDK 实现屏幕共享功能——3

iOS 基于实时音视频 SDK 实现屏幕共享功能——4



这里的核心思想是拿到屏幕共享的数据之后,先进行压缩,当压缩完成后会通过回调上报给当前类。既而通过 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

WebRTCadmin 发表了文章 • 0 个评论 • 412 次浏览 • 2020-12-03 18:21 • 来自相关话题

iOS 基于实时音视频 SDK 实现屏幕共享功能——1iOS 基于实时音视频 SDK 实现屏幕共享功能——2iOS 基于实时音视频 SDK 实现屏幕共享功能——3iOS 基于实时音视频 SDK 实现屏幕共享功能——4Replaykit 介绍在之前的 iOS 版... ...查看全部

微信截图_20201203181458.png

iOS 基于实时音视频 SDK 实现屏幕共享功能——1

iOS 基于实时音视频 SDK 实现屏幕共享功能——2

iOS 基于实时音视频 SDK 实现屏幕共享功能——3

iOS 基于实时音视频 SDK 实现屏幕共享功能——4

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、Ope... ...查看全部

今天,我们来一起学习一下 WebRTC,相信你已经对这个前端音视频网红儿有所耳闻了。

WebRTC Web Real-Time Communication 网页即时通信

WebRTC 于 2011 年 6 月 1 日开源,并在 Google、Mozilla、Opera 等大佬们的支持下被纳入 W3C 推荐标准,它给浏览器和移动应用提供了即时通信的能力。

WebRTC 优势及应用场景

优势

  • 跨平台(Web、Windows、MacOS、Linux、iOS、Android)
  • 实时传输
  • 音视频引擎
  • 免费、免插件、免安装
  • 主流浏览器支持
  • 强大的打洞能力

应用场景

在线教育、在线医疗、音视频会议、即时通讯工具、直播、共享远程桌面、P2P网络加速、游戏(狼人杀、线上KTV)等。

1.png

(有喜欢玩狼人杀的同学吗?有时间可以一起来一局,给我一轮听发言的时间,给你裸点狼坑,一个坑容错。)

WebRTC 整体架构

拉回来,我们看一看 WebRTC 的整体架构,我用不同的颜色标识出了各层级所代表的含义。

2.png

  • 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 并不是一个真正的协议,它就是一种描述各端“能力”的数据格式。

3.png

上图所示就是 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