对抗疫情,让我们来一场酷酷的在线考试

随着移动互联网的迅速发展,手机已经成为了中青年人身体的一部分,他们从起床到睡觉前的最后一分钟都不会离开手。最近几个月,受疫情影响,在线学习和在线考试再次成为互联网上的热门话题。那问题来了,在线学习和考试如何保证学生的学习和考试的质量呢?慢慢的各大网络机构出了各...
继续阅读 »

随着移动互联网的迅速发展,手机已经成为了中青年人身体的一部分,他们从起床到睡觉前的最后一分钟都不会离开手。最近几个月,受疫情影响,在线学习和在线考试再次成为互联网上的热门话题。

那问题来了,在线学习和考试如何保证学生的学习和考试的质量呢?慢慢的各大网络机构出了各种线上教学线上考试的功能,国家也推出来听课不停学的政策。作为一个 web 端的开发还是很好奇音视频这块功能的。选择了融云做音视频的三方,开启了我的探测路程。

功能构想

(1)学生端做直播 ,1 个学生三个视图( 摄像头采集,屏幕共享,手机端采集)通过融云后台转码合图后推到 CDN ; (2)老师同时订阅并播放多个学生的直播画面,也可查看学生端采集视频的截图或前几分钟录像 ; (3)当老师发现某个学生需要警告或答疑时,老师可以进入单个学生所在房间 ,与学生连麦进行实时通讯。

学生端实现

(1)学生的角色我使用的是“主播”;在多机位拍摄时,需要两个设备,一般是一个 pc,一个移动端;两个端登录应用用一个账号,但是加入融云音视频房间时用两个 uid(此处有些小坑调试了很久才弄明白,pc 端创建并加入直播间,进行音视频推流;之后移动端加入相应的房间,也发送音视频流;(可以关闭麦克风 ,看业务场景需求);调试完毕后 可以正常进行考试。

此处参考的是主播流程文档:https://docs.rongcloud.cn/v3/views/rtc/livevideo/guide/quick/anchor/web.html

(2)学生端在 pc 上通过浏览器登录考试系统;

(3)登录后电脑摄像头采集学生正面实时图像,学生还可以发出屏幕共享 ,手机端登录应用加入相同的考场,摆放在学生身后来捕捉考生整体视图;

(4)学生端应用可以调用融云提供的截图接口随时截图,以便给图像识别模块进行是否作弊的检测;

(5)学生端上传的面部头像,全身图像,屏幕共享图像;会在融云服务器上合成一个流。

教师端实现

(1) 老师的角色是“观众”;订阅多个学生输出的图像整体;观众观看参考的接口文档 :https://docs.rongcloud.cn/v3/views/rtc/livevideo/guide/quick/audience/web.html

(2)当老师发现问题时可以查看指定学生直播流的前几分钟回放,需要开通实时录制,录制在 cdn 处。

(3)当需要警告或答疑时,老师可以进入单个学生所在房间,与学生做实时通讯。加入时可以只发音频。老师警告答疑参考文档:https://docs.rongcloud.cn/v3/views/rtc/livevideo/guide/joinManage/join/web.html

录制留存

(1)可以用融云云端录制;每个学生上传的所有视频流可以合图录制,以满足审查人员在考试后检查学生考试全过程有无作弊等情况。融云云端录制 ,可以上传多家对象存储,可以按需求选择音视频格式参考文档 https://docs.rongcloud.cn/v3/views/rtc/record/intro/guide.html

上图

24954647-204042d8c97b3599.png24954647-1f71b7908bfc0131.png

收起阅读 »

H5 如何实现直播

目前直播大火,在看直播后突然想自己做个直播软件。想了想想自己做下,开始在网上各种找资源。直播嘛,那必然有主播端和观众端。经过一系列的考察后后来找到了融云,使用了他们的 SDK,一共用了无 UI IM 和低延时直播两个 SDK。IM 用了连接和发送消息,选择的是...
继续阅读 »

微信截图_20210415150543.png

目前直播大火,在看直播后突然想自己做个直播软件。想了想想自己做下,开始在网上各种找资源。直播嘛,那必然有主播端和观众端。经过一系列的考察后后来找到了融云,使用了他们的 SDK,一共用了无 UI IM 和低延时直播两个 SDK。

IM 用了连接和发送消息,选择的是融云的聊天室场景。

低延时直播分了主播端和观众端。使用 H5 作为观众端进行观看,使用 web 端作为主播端进行了直播。

功能点介绍

主播端

按照文档的思路,主播端用的 web 界面,使用的 chrome 浏览器。 涉及到的功能点:

  1. 主播分享视频

  2. 查看观众发的消息

  3. 回复消息

观众端

观众端用的 H5 实现,涉及到的功能点:

  1. 观看主播分享的视频

  2. 发消息

  3. 点赞

额,就是为了体验功能做的简单了些,没有弄那些礼物什么的有些麻烦。

实现思路

主播端

  1. 需要先初始化连接 IM,RTC 两个融云的 SDK,此处参考的快速集成 https://docs.rongcloud.cn/v4/views/rtc/livevideo/guide/quick/premise/web.html

  2. 加入到房间中 https://docs.rongcloud.cn/v4/views/rtc/livevideo/guide/anchorManage/room/web.html#newroom

  3. 发布视频资源供观众查看 https://docs.rongcloud.cn/v4/views/rtc/livevideo/guide/anchorManage/resource/web.html#publish

  4. 配置直播 CDN 推流地址 https://docs.rongcloud.cn/v4/views/rtc/livevideo/guide/manager/pushcdn/web.html

  5. 完成

此处仅给了我认为关键的几个接口,如果大家感兴趣可以自己细看下文档。有些高级的接口这边没有测试,不过看介绍可以设置直播视频的样式。感兴趣的小伙伴按照文档自行研究下吧。

观众端

观众端就很简单了,H5 的观众端仅需要订阅 CDN 推流的地址就可以看到主播的视频流,很方便。

总结

整体做下来大概零零散散用了两三天,UI 也没怎么最求在网上找了个大致的样式做了下。不过整体还好最起码基本的功能实现了。也算是自己做的第一个直播的小 Demo,还是很欣喜的。喜欢的大家可以体验下。


收起阅读 »

成为主播的必备神器

前言本人是一个游戏爱好者,经常会在斗鱼或者虎牙看游戏主播的精彩操作,特别是近几年直播的火速崛起,各个平台都打着人人都可以成为主播的口号来获取直播流量,所以我就在想直播门槛这么低的吗!这个疑问直到我看到 融云的低延迟直播 SDK 才打消了。那...
继续阅读 »

前言

本人是一个游戏爱好者,经常会在斗鱼或者虎牙看游戏主播的精彩操作,特别是近几年直播的火速崛起,各个平台都打着人人都可以成为主播的口号来获取直播流量,所以我就在想直播门槛这么低的吗!这个疑问直到我看到 融云的低延迟直播 SDK 才打消了。

微信截图_20210415150703.png

那么这个低延迟直播 SDK 是什么呢?

工欲善其事,必先利其器!那么这个低延迟直播就是成为主播之路的神器。下面是介绍:

低延迟直播 SDK 可以满足 1 个或多个主播与百万观众实时互动,避免延迟带来的距离感,增加用户活跃,支持主播推流,观众拉流,连麦互动等功能,为方便开发者集成降低集成难度,直播场景和会议场景都用 RongRTCLib SDK ,可在 SDK 调用时用参数加以区分集成场景

也就是说通过集成 低延迟直播 SDK 我们可以自己实现一个主播平台,集成的方式也非常简单,快速集成分三个步骤:

1、首先您需要了解集成的前置条件https://docs.rongcloud.cn/v4/views/rtc/livevideo/guide/quick/premise/web.html

2、完成前置条件后,开始实现主播端发布音视频流的操作:https://docs.rongcloud.cn/v4/views/rtc/livevideo/guide/quick/anchor/web.html

3、有了主播后,咋们也不能忘记观众端,观众端就比较简单了,只需要订阅主播端发布成功的音视频资源就可以了:https://docs.rongcloud.cn/v4/views/rtc/livevideo/guide/quick/audience/web.html

完成上面三个步骤后,您就已经初步集成完 低延迟直播 SDK 了,主播和观众就可以互通了,是不是很简单!如果您觉得集成比较麻烦,OK !还有一个 Web 在线集成体验的示例,体验完后还可以直接拿到 demo ,跳转地址:https://docs.rongcloud.cn/v4/views/rtc/livevideo/guide/demo/web.html

如果您还想进阶更高级一点的,需要好好研究一下文档当中的其他功能,比如:

发布文件资源:Web 可以通过拿到本地音频、视频文件资源,进行发布。代码示例:

// 通过 video 标签播放本地音视频资源。
let videoNode = document.querySelector('#target');
let mediaStream = videoNode.captureStream();
let user = {
  id: '请填写用户 ID',
  stream: {    
    tag: 'custom',
    type: StreamType.AUDIO_AND_VIDEO,
    mediaStream: mediaStream
  }
};
stream.publish(user).then(() => {
  console.log('发布成功');
}, error => {
  console.log(error);
});

发布屏幕共享资源:Web 可以通过 stream.get 方法获取屏幕媒体流。 仅支持 Chrome 获取屏幕共享资源。代码示例:

var config = {
  height:480,
  width:640,
  frameRate:15,
  screen: true,
  desktopStreamId: 'desktopStreamId',
}

stream.get(config).then(function ({ mediaStream }) {
  let user = {
    id: '请填写用户 ID',
    stream: {    
      tag: 'screen',
      type: StreamType.AUDIO_AND_VIDEO,
      mediaStream: mediaStream
    }
  }
  stream.publish(user).then(() => {
    console.log('发布成功');
  }, error => {
    console.log(error);
  });
}, error => {
  console.error(error);
});

等等······还有好多扩展功能等您来发现!授人以鱼,不如授人以渔,哈哈哈,我们不仅能自己成为主播,还能创造主播,还等什么呢,赶紧试一下吧


收起阅读 »

实现类似微信视频聊天的功能

前言今年因为疫情的影响,很多线下的项目或者活动都放在了线上,所以集成音视频等相关的话题一直比较火热。 作为一个小码农对微信音视频通话的功能还是比较好奇的,所以空闲的时候就想着自己能不能也实现一个,给朋友一个链接地址然后就可以愉快的视频通话了(小小的满足一下虚荣...
继续阅读 »

前言

今年因为疫情的影响,很多线下的项目或者活动都放在了线上,所以集成音视频等相关的话题一直比较火热。 作为一个小码农对微信音视频通话的功能还是比较好奇的,所以空闲的时候就想着自己能不能也实现一个,给朋友一个链接地址然后就可以愉快的视频通话了(小小的满足一下虚荣心)

微信截图_20210415151315.png

对于集成音视频纯小白来说,前期准备工作的确不好整,因为对这块没什么概念,对后面需要做什么列不出一个清晰的步骤出来(蓝廋香菇~~~),幸运的是,在网上找相关知识时意外发现了一家做音视频 SDK 的(融云https://www.rongcloud.cn/

为什么推荐他们家呢,主要是他们的开发文档比较适用于像我这样的小白,跟着他们的快速文档无脑实现音视频通话。点这个地址,开启你的音视频之旅吧:https://docs.rongcloud.cn/v4/views/rtc/call/noui/quick/web.html

特别意外的是融云还提供了 Web 端在线集成体验示例,体验完后还能拿到 demo ,附上跳转连接:https://docs.rongcloud.cn/v4/views/rtc/call/noui/demo/web.html

音视频通话介绍 音视频通话 SDK 含呼叫流程,可以实现一对一呼叫、多人呼叫等音视频通话场景,音视频通话 SDK 区别于音视频会议 SDK,音视频通话(呼叫)SDK 不需要双方约定房间号,呼叫流程自动处理房间号,适用于类似微信音视频通话等必须含有呼叫流程的应用场景,由两部分组成 信令通知 和 音视频传输

信令通知:基于 IMLib SDK,例如 A 给 B 发起通话,A 优先会通过 IM 给 B 发送一个通话请求,B 同意后再进入音视频房间进行音视频通信 音视频传输:基于 RongRTCLib SDK,例如 A 给 B 发起通话,信令传输完成后,会适用融云提供的音视频能力进行通信

但是在集成的时候,有几点是需要吐槽的:

1、我给其他人发起通话的时候,直接就触发挂断了返回 "SummaryMessage" 类型消息,咋又看不懂在文档找了半天都没发现 SummaryMessage 的详细描述,只知道是挂断电话返回的消息,最后提交了工单才了解挂断的原因。

Web 端挂断原因 HungupMessage 消息中 reason 字段及 SummaryMessage 消息中 status 字段都为挂断原因

状态码说明状态码说明
1己方取消已发出的通话请求11对方取消已发出的通话请求
2己方拒绝收到的通话请求12对方拒绝收到的通话请求
3己方挂断13对方挂断
4己方忙碌14对方忙碌
5己方未接听15对方未接听
6己方不支持当前引擎16对方不支持当前引擎
7己方网络错误17对方网络错误
8其他设备已处理18CallLib 不可用

原来是对方忙碌导致的接听失败!!!

2、前段时间还能正常通话,升级完 chrome 后就报错了(一脸懵逼 ),报错信息如下: Failed to execute 'setLocalDescription' on 'RTCPeerConnection': Failed to parse SessionDescription. m=video 9 UDP/TLS/RTP/SAVPF 98 99 96 97 100 101 127 Invalid value:

提供单询问融云的技术人员,才知道是由于 chrome 86 升级了 webRTC 的规范,SDK 在 3.2.6 版本做了相关适配。如果 SDK 使用的不是 3.2.6 版本。需要升级至 3.2.6 版本


收起阅读 »

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

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


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


收起阅读 »

如何 1 天快速集成自己的“Clubhouse”?

“你有 Clubhouse 邀请码吗?”,这句话在近期似乎盖过了“做核酸检测了吗?”的风头,成为大家之间的问候语。就在月初,一款主打即时交流的音频社交平台——Clubhouse 蓦然走入大众视野,并且快速、广泛地引起关注和讨论。成立不到一年的 Clubhous...
继续阅读 »

“你有 Clubhouse 邀请码吗?”,这句话在近期似乎盖过了“做核酸检测了吗?”的风头,成为大家之间的问候语。

就在月初,一款主打即时交流的音频社交平台——Clubhouse 蓦然走入大众视野,并且快速、广泛地引起关注和讨论。成立不到一年的 Clubhouse 之所以能够出圈,主要得益于埃隆·马斯克的一场线上分享,估计马斯克也没有想到自己竟有如此强大的“带货”能力,直接促成了 2021 年第一个“互联网风口”的到来。

微信图片_20210218094303.pngClubhouse 为什么能够有如此魔力?知乎相关问题“Clubhouse 这款社交软件为什么这么火?”便吸引了 72 万人围观和讨论,大家都很好奇 Clubhouse 究竟是一款什么样的应用?微信图片_20210218094320.jpg

简单来说,Clubhouse 是一款语音社交产品,核心功能主要是:

1.  由主持人创建一个房间,邀请一个或者多个嘉宾一起语音聊天,观众有问题可以举手申请,经过同意后便可发言;

2.  主持人对房间进行管理,包括上麦、下麦、禁麦、封麦、解封等功能。

其实在 Clubhouse 走红前,国内早已出现了各类语音社交应用,且产品已经非常成熟。在此次 Clubhouse 的影响下,语音社交再次爆火,想必有些团队打算“乘热出击”推出自己的“Clubhouse”,那么如何 1 天快速集成自己的“Clubhouse”呢?

02
你的“Clubhouse”该具备哪些功能?

微信图片_20210218094337.jpg

其实要做类似 Clubhouse 的产品技术难度并不大,目前市面上有专业服务商可以提供成熟的解决方案。比如,互联网通信云服务商融云的语音社交解决方案,就能够帮助开发者快速实现类似 Clubhouse 的通信功能。

互动连麦:房主可以发出邀请或观众请求上麦,观众上麦后成为连麦主播,房间内所有用户都可以实时收听房主和连麦主播互动。

麦位管理:房主可以对麦位进行管理,包括上麦、下麦、禁麦、解禁、封麦、解封、拉黑用户等,抢到麦位的用户可以与房主连麦,同时支持直播间所有用户都可以实时看到每个麦位的状态。

用户状态统计:直播间内的房主、连麦主播和观众可以实时看到进入或离开当前直播间的用户情况,让直播间用户状态一目了然。

微信图片_20210218094350.png主播端业务流程微信图片_20210218094409.png

除了这些基本功能,融云还提供更多 Clubhouse 尚不具备的功能。

直播录制:支持平台录制直播间内容,方便监测回溯;

审核功能:对语音消息、文字图片消息、音视频流、平台 UGC 内容可以实时审核,避免违规、打击黑产;

网络状况查询:用户加入直播间可以查看当前网络质量,判断网络状况;

IM消息互动:直播间内,房主、连麦主播和观众可以通过使用文字、图片消息进行实时交流;

扩展功能:支持背景音乐播放,帮助烘托直播间主题气氛,也可以灵活接入第三方插件,实现美颜、变声/美声等功能。

在对 Clubhouse 进行“复刻”的过程中,还需要考虑用户的使用体验。即便是 Clubhouse,在用户规模激增的情况下,也暴露出一些产品技术的不足之处,比如经常出现用户突然退出房间,用户在线状态不准确、语音丢帧/卡顿/延迟等情况,其中除了音频高并发处理能力的问题,还有 IM 信令不够稳定的问题。

03
融云技术优势帮助开发者解决难题

融云通过十余年的技术积累及客户服务实践经验,形成了一套成熟、完善的语音社交解决方案,通过“IM+RTC”双重整合能力,帮助开发者解决各种问题,同时沉淀了自身五大优势:

信令极简高效:基于融云 IM 通讯服务的信令通道,安全、可靠、高效;

海量消息并发:久经考验的海量消息分发模型,从容应对每小时千亿条消息分发场景;

全球互联:基于融云 SD-CAN 全球通信网络,全球用户物理节点就近接入;

实时音视频互动:多人之间的音视频连麦;直播互动流畅,延迟小于 300ms;

在线人数无上限:通过可任意水平扩展的服务架构,实现同时在线人数无上限。

为了更高效便捷地服务开发者,融云免费开放了语音聊天室 Demo ,同时提供丰富的支持文档,让开发者们最快 1 天即可实现语音社交功能,快速上线。


收起阅读 »

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

随着在线教育、电商直播、泛娱乐社交等 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 在当前依然拥有价格上的优势,而两者作为音视频领域的实用技术,无论是适用场景、还是贴近开发的服务都越来越多样化,开发者未来选型之路也将更顺畅。



收起阅读 »

【融云分析】WebRTC如何通过STUN、ICE协议实现P2P连接

WebRTC中两个或多个主机进行P2P连接是通过STUN、TURN、ICE等技术实现的。主机往往都是在NAT之后,且不同的NAT导致外部主机向内网主机发送数据的可见性不同。 内网主机通过STUN协议可以获得NAT分配的外部地址。ICE是主机之间发现P2P传输路...
继续阅读 »

WebRTC中两个或多个主机进行P2P连接是通过STUN、TURN、ICE等技术实现的。主机往往都是在NAT之后,且不同的NAT导致外部主机向内网主机发送数据的可见性不同。 内网主机通过STUN协议可以获得NAT分配的外部地址。ICE是主机之间发现P2P传输路径机制,ICE中使用了STUN协议进行连通检测、传输路径的指定和保活。 本文将对STUN和ICE协议进行分析和解读,希望能为开发者们带来一些启发和帮助

1. NAT类型

网络地址转换, 简称NAT,节省了IPv4地址空间的使用并且隔离了内网和外网。NAT对待UDP的实现方式有4种,分别如下:

1.1完全圆锥型

一个内网地址(iAddr:iPort)被映射到一个外网地址 (eAddr:ePort)。这个内网(iAddr:iPort)地址发送的数据包都会通过这个外网地址(eAddr:ePort)。外部主机可以通过这个外网地址(eAddr:ePort)向这个内网地址(iAddr:iPort)发送数据包

1.png

1.2地址受限锥型

一个内网地址(iAddr:iPort)被映射到一个外网地址 (eAddr:ePort)。这个内网(iAddr:iPort)地址发送的数据包都会通过这个外网地址(eAddr:ePort)。外部主机 (hAddr:any) 只有接收过从内网(iAddr:iPort)发送来的数据包后,才能通过外部地址 (eAddr:ePort)发送数据包给内网地址(iAddr:iPort)。其中外部主机的端口可以是任意的。

2.png

1.3端口受限锥型

一个内网地址(iAddr:iPort)被映射到一个外网地址 (eAddr:ePort)。这个内网(iAddr:iPort)地址发送的数据包都会通过这个外网地址(eAddr:ePort)。外部主机 (hAddr:hPort) 只有接收过从内网(iAddr:iPort)发送来的数据包后,才能通过外部地址 (eAddr:ePort)发送数据包给内网地址(iAddr:iPort)。其中外部主机的端口hPort是受限的。

3.png

1.4对称型

一个内网地址(iAddr:iPort)向外网地址 (sAddr1:sPort1)发送的多次请求时,NAT会分配同一个外网地址(eAddr1:ePort1)。若向不同的外网地址如(sAddr2:sPort2)发送数据包时,NAT分配另外一个外网地址(eAddr2:ePort2)。外网地址 (sAddr1:sPort1)只有接收过从内网(iAddr:iPort)发送来的数据后,才能通过已经在NAT上开辟的(eAddr1:ePort1)发送数据包给内网.

4.png

2. STUN简介

STUN是Session Traversal Utilities for NAT简写,RFC5389规定了具体内容。STUN协议是用来获取内网地址对应在NAT上的外网地址,NAT穿越。 STUN是C/S模式的协议,由客户端发送STUN请求;STUN服务响应,告知由NAT分配给主机的IP地址和端口号。

2.1 STUN消息结构

STUN消息头为20字节,后面紧跟0或多个属性。STUN头部包含一STUN消息类型、magic cookie、事务ID和消息长度。 

5.png

(图)STUN消息结构

每个STUN消息的最高位前2位必须为0。当多个协议复用同一个端口的时候,这个可以用于与其他协议区分STUN数据包。 消息类型确定消息的类别(如请求、成功回应、失败回应、指示indication)。虽然这里有四种消息类型,但可以分为2类事务:请求/响应事务、指示事务。

magic cookie为固定值0x2112A442。

Transaction ID标识同一个事务的请求和响应。当客户端发送多个STUN请求,通过Transaction ID识别对应的STUN响应。

2.2 STUN消息类型

6.png

(图)STUN消息类型

C0和C1位置的bit指明了消息的分类。其余的12位置标示不同的请求类型,比如绑定请求。

2.3 STUN属性

STUN头之后是0或多个属性。每个属性都采用TLV编码,type为16位的类型、lenght为16位的长度、value为属性值。每个STUN属性必须是4字节对齐。

7.png

(图)STUN属性

STUN属性格式

STUN服务器请求和响应都包含消息属性。一些属性不是强制性的,其中一些只能出现在绑定请求中,而其他一些只能出现在绑定响应中。  属性空间被划分为2个范围。强制理解属性STUN代理必须处理,否则STUN代理将无法正常处理该属性的消息;STUN代理不能理解可选理解属性的话,这些属性可以被忽略。

强制理解属性 (0x0000-0x7FFF):

8.png

 可选理解属性 (0x8000-0xFFFF)

9.png

具体说明如下: MAPPED-ADDRESS属性标识了NAT映射后的地址。

XOR-MAPPED-ADDRESS属性与MAPPED-ADDRESS属性一致,映射后的地址要做异或处理。

USERNAME属性用于消息完整性。用户名和密码包含在消息完整性中。

MESSAGE-INTEGRITY属性是STUN消息的HMAC-SHA1值,长度20字节。MESSAGE-INTEGRITY属性可以出现在任何类型的STUN消息中。用作HMAC输入的文本是STUN消息,包括头部,直到且包括MESSAGE-INTEGRITY属性前面的属性。FINGERPRINT属性出现MESSAGE-INTEGRITY后。所以FINGERPRINT属性外,STUN代理忽略其他出现在MESSAGE-INTEGRITY属性后的任何属性。

FINGERPRINT属性可以出现在所有的STUN消息中,该属性用于区分STUN数据包与其他协议的包。属性的值为采用CRC32方式计算STUN消息直到但不包括FINGERPRINT属性的的结果,并与32位的值0x5354554e异或。

ERROR-CODE属性被用于错误响应消息中。它包含一个在300至699范围内的错误响应号。

REALM属性出现在请求中,表示认证时要用长期资格。出现在响应中,表示服务器希望客户端使用长期资格进行认证。

NONCE属性是出现在请求和响应消息中的一段字符串。

UNKNOWN-ATTRIBUTES属性出现在错误代码为420的的错误响应,表示服务器端无法理解的属性。

SOFTWARE属性用于代理发送消息时所使用的软件的描述。

ALTERNATE-SERVER属性表示STUN客户可以尝试的不同的STUN服务器地址。属性格式与MAPPED-ADDRESS相同。

2.4 STUN示例

下面是Wireshark抓取的一对STUN绑定请求和响应。STUN绑定请求,源地址192.168.2.36:47798,目标地址180.76.137.157:30001。

10.png

STUN绑定响应,源地址180.76.137.157:30001,目标地址192.168.2.36:47798

11.png

其中ICE-CONTROLLING、PRIORITY属性是下面提到的ICE中扩充的属性。

3. ICE简介

ICE两端并不知道所处的网络的位置和NAT类型,通过ICE能够动态的发现最优的传输路径。如下图L和R是ICE代理,下面简称L和R。L和R有各自的传输地址,包括主机的网卡地址、NAT上的外网地址、 TURN服务地址。ICE就是要从这些地址中,找到L和R的候选地址对,实现两端高效连通。

12.png

(图)ICE部署图举例

ICE两端可以通过信令服务器交换SDP信息。ICE使用STUN,TURN等协议来建立会话。

3.1收集候选地址

ICE端收集本地地址。通过STUN服务收集NAT外网地址;通过TURN收集中继地址。

所以有四种候选地址:

13.png

如下图: 主机候选X:x 服务器反射候选X1':x1' 中继候选Y:y 这里称主机候选地址是服务器候选地址的BASE。

14.png

(图)ICE端口

3.2连通检测

L收集了所有的候选地址后,按优先级从高到低排序,通过信令服务器发送SDP offer给R。R收到offer后,收集获选地址,并将自己候选地址放入SDP answer发送给L。此时两端都有了对端的和本端的候选地址。然后配对,生成candidate pair。为了确保candidate pair的有效性,两端都要做连通检测。根据candidate pair,从本地candidate发送STUN请求到远端candidate;接收端返回STUN响应给发送端。如下图。

15.png

(图)ICE基本连通检测

两端都按照各自checklist分别进行检查。

当R收到L的检测时,R发送向L的检测被称为Triggered检测。

3.3 Candidates pair排序

将连通性检查成功的candidate pair按优先级排序加入check list。两端定期遍历这个check list, 发送STUN请求给对端称为Ordinary检测。优先级的计算根据以下原则: 每端给自己的candidate一个优先级数值。本端优先级和远端优先级结合,得到candidate pair的优先级优先级。

公式 priority = (2^24)*(type preference) + (2^8)*(local preference) + (2^0)*(256 - component ID)

16.png

再根据candidate的优先级计算candidate pair的优先级。

priority = 2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0)

G:controlling candidate 优先级 D:controlled candidate 优先级

3.4提名Candidates

ICE中有两种角色, controlling角色可以选取最终的candidate pair;controlled角色会等待controlling角色选取的candidate pair。 ICE指定一个ICE代理为controlling角色,其他ICE代理为controlled角色。ICE优先检测优先级高的candidate pair。

Controlling角色有两中提名方案:REGULAR提名:当得到至少一对有效的pair的时候,Controlling角色就会选择其中的一个pair作为候选,此次连通检测发送一个带flag的请求,告诉对端这个就是被选中的pair。

17.png

(图)REGULAR提名

AGGRESSIVE提名:Controlling角色会在每个STUN请求之中添加flag标志,最先成功的那个被选为媒体传输通道。

18.png

(图)AGGRESSIVE提名

3.5 ICE示例

下面是例子中,L和R都是full模式ICE代理,采用aggressive提名,传输媒体为RTP。full模式为双方都要进行连通性检查,都要的走一遍流程;lite模式为,full模式ICE一方进行连通性检查,lite一方只需回应response消息。

19.png

(图)ICE举例

便于理解,采用"主机类型-网络类型-序号"的格式表示传输的地址。地址有两个分量,分别是IP和PORT。L,R,STUN,NAT代表不同的主机类型;PUB代表外网,PRV代表内网; L处在内网中,内网地址是10.0.1.1,R处在外网,外网地址是192.0.2.1。L和R都配置了STUN服务,地址是192.0.2.2,端口是3478。L在NAT后面,NAT外网地址是192.0.2.3。序号表示不同的媒体类型,这里只有RTP所以序号为1。 "S="表示STUN消息的发送地址、"D=" 表示STUN消息的接收地址。 "MA=" 表示STUN绑定响应的中mapped address。"USE-CAND" 表示带有"USE-CANDIDATE" STUN消息。

L收集本地候选地址,并发起STUN绑定请求给STUN服务器,L得到 NAT-PUB-1作为服务器反射候选地址。 L计算候选的优先级,主机候选地址type preference为126;服务器反射候选地址type preference为100。local preference为65535。component ID为1 套用公式priority = (2^24)*(type preference) + (2^8)*(local preference) + (2^0)*(256 - component ID) 得主机候选地址的优先级为2130706431,服务器反射候选地址的优先级为1694498815。 L设置主机候选地址的foundation为1,服务器反射候选地址foundation为2。  L将服务器反射候选地址作为default候选地址。对应的offer sdp为

20.png

替换地址后

21.png

因为L和R都采用的是full-mode,这种情况下ICE协议规定发送offer为controlling端,L为controlling端。 L和R生成candidate pair,两端都有2个candidate pair。L会裁减掉包含服务映射候选地址,保留candidate pair为本端$L_PRIV_1、远端$R_PUB_1

22.png

消息9表示R做连通检测,因为R是controlled角色,所以无需设置USE-CANDIDATE。L处于NAT后面,且没有向R发送过请求,所以此次连通检测会失败。

当L收到answer sdp后,开始连通检测(消息10-13)。L采用的是aggressive提名,所以每个请求都会有USE-CANDIDATE。L使用candidate pair为$L_PRIV_1/$R_PUB_1发起的连通检测成功后,L创建一个新的candidate pair,本端为NAT-PUB-1(消息13中得到) 、远端为R-PUB-1(消息10中得到),加入valid list中。这个连通检测中设置了USE-CANDIDA属性,该candidate pair为选中的候选。L的RTP流在valid list中有选中的candidate pair,所以L进入完成状态。

R收到L的STUN绑定请求(消息11)后,R发起消息11对应的Triggered检测,其candidate pair的本端为R-PUB-1、远端为NAT-PUB-1。检测成功后,R创建本端为R-PUB-1、远端为NAT-PUB-1的candidate pair,加入valid list。因为消息11中包含了USE-CANDIDATE,所以这个candidate pair就被选中为这个RTP流的传输通道。R进入完成状态。

4. 总结

本文介绍了NAT、STUN、ICE等基本概念。STUN部分介绍了STUN的消息结构、消息类型和消息属性ICE协议中STUN消息要遵循STUN协议。 ICE部分介绍了ICE代理之间是如何根据各自的网络地址建立连接的步骤有收集候选地址、连通检测、Candidates pair生成与排序、提名Candidates。 详细内容还需查看ICE协议rfc5245以及webrtc的p2p部分的具体实现。


收起阅读 »

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

本文主要介绍 Flash 播放 AMR 格式 Base64码 音频。在此之前么有接触过 Flash ,接触 AS3 是一头雾水,不过幸好有 TypeScript 和 JavaScript 的基础看起来不是很费劲,现学现卖的就是开了 ”跳坑“ 之旅~~~1、实现...
继续阅读 »

本文主要介绍 Flash 播放 AMR 格式 Base64码 音频。

在此之前么有接触过 Flash ,接触 AS3 是一头雾水,不过幸好有 TypeScript 和 JavaScript 的基础看起来不是很费劲,现学现卖的就是开了 ”跳坑“ 之旅~~~

1、实现思路

起初一点实现思路都木有,不知道该从何做起,只知道用 Flash 播放 AMR ,度娘谷姐的一顿找,结果可想而知,没有糟糕,只有十分糟糕,哈哈。

后来想了想,凡事都得有个思路,不能闷头干,瞬间恍然大悟,为自己浪费的快一天的时间,感到羞愧和害怕…..

① Flash 都能播放哪些音频。

② 在 ActionScript 中AMR 是如何转换成 Flash 可播放的音频的。

③ JS 中如何调用 ActionScript 中方法,如何交互。

④ 如何把 SWF 文件嵌入到 HTML 页面中。

⑤ 如何把 AMR(Audio) 和 Flash 播放AMR 两种方式封装起来。

2、逐一破解

① Flash 都能播放哪些音频:

MP3 格式是 Flash 默认支持的音频格式,WAVE 格式需要转换可以播放,其他格式也是需要转换的,因为先做的 Chrome 下播放声音,对 WAVE 音频多少有些了解,所以决定从 WAVE 音频入手,所以按照上述的套路来 ”屡思路“:

(1) 不管如何转换,肯定要操作字节数组,所以第一步把 AMR 格式的 base64 码 转换为 ByteArray 数组。

(2) 如何把 AMR 的 ByteArray 转换成 WAVE 格式的 ByteArray 数组,毫无疑问 ,肯定需要解码的过程。

按照这两个小步骤逐一做,很快找到了 base64 的转码过程(开始是自己用 AS 实现了 JS 中的的 转码过程,可用但不完美,最终借鉴 github 上大牛的转换过程),但 AMR 转换 WAVE 这个就没有那么容易了,最终确定,AS 解 AMR 比较费劲,需要 用 C 语言来解码,然后用 CrossBirdge 生成可以供 Flash 调用的 SWC 文件。

OK,到这为止,第一步就完美解决了。

② 在 ActionScript 中AMR 是如何转换成 Flash 可播放的音频的:

    上文中有提到,需要用 C 语言 进行对 AMR 解码,下文中会给出 C 语言解码和生成 SWC 的教程(借鉴大牛的)。

③ JS 中如何调用 ActionScript 中方法,如何交互,下文中会贴出完整代码(一定要看注释、注释、注释),此处写出 JS 调用过程。

JS 代码:

function callFlashMethod() {
  // play 是 flash 代码中定义的 ExternalInterface.addCallback("play",this.play); 下文中会有详细介绍。
  thisMovie("嵌入页面上<object>的ID").play("base64str");
}
function thisMovie(movieName) {
    if (navigator.appName.indexOf("Microsoft") != -1) {
        return window[movieName]
    }
    else {
        return document[movieName]
    }
}
document.getElementById("playId").onclick = function(){
    callFlashMethod();
};

④ 如何把 SWF 文件嵌入到 HTML 页面中:

使用 swfobject.js 可以将 swf 文件嵌入到 HTML 页面中,参考资料 :http://www.cnblogs.com/Carpe-Diem/articles/2310831.html

⑤ 如何把 AMR(Audio) 和 Flash 播放AMR 两种方式封装起来:

有了上面的铺垫,轻而易举的就可以封装啦(主要看 isIE 为 true 的情况):

var RongIMLib;
(function (RongIMLib) {
    var RongIMVoice = (function () {
        function RongIMVoice() {
        }
        /**
        * 初始化声音库
        */
        RongIMVoice.init = function () {
            if (this.isIE) {
                var div = document.createElement("div");
                div.setAttribute("id", "flashContent");
                document.body.appendChild(div);
                var script = document.createElement("script");
                script.src = "http://cdn.ronghub.com/swfobject-2.0.0.min.js";
                var header = document.getElementsByTagName("head")[0];
                header.appendChild(script);
                setTimeout(function () {
                    var swfVersionStr = "11.4.0";
                    var flashvars = {};
                    var params = {};
                    params.quality = "high";
                    params.bgcolor = "#ffffff";
                    params.allowScriptAccess = "always";
                    params.allowfullscreen = "true";
                    var attributes = {};
                    attributes.id = "player";
                    attributes.name = "player";
                    attributes.align = "middle";
                    swfobject.embedSWF("http://cdn.ronghub.com/player-2.0.2.swf", "flashContent", "1", "1", swfVersionStr, null, flashvars, params, attributes);
                }, 200);
            }
            else {
                var list = ["http://cdn.ronghub.com/pcmdata-2.0.0.min.js", "http://cdn.ronghub.com/libamr-2.0.1.min.js"];
                for (var i = 0, len = list.length; i < len; i++) {
                    var script = document.createElement("script");
                    script.src = list[i];
                    document.head.appendChild(script);
                }
            }
            this.isInit = true;
        };
        /**
        * 开始播放声音
        * @param data {string} amr 格式的 base64 码
        * @param duration {number} 播放大概时长 用 data.length / 1024
        */
        RongIMVoice.play = function (data, duration) {
            this.checkInit("play");
            var me = this;
            if (me.isIE) {
                me.thisMovie().doAction("init", data);
            }
            else {
                me.palyVoice(data);
                me.onCompleted(duration);
            }
        };
        /**
        * 停止播放声音
        */
        RongIMVoice.stop = function () {
            this.checkInit("stop");
            var me = this;
            if (me.isIE) {
                me.thisMovie().doAction("stop");
            }
            else {
                if (me.element) {
                    me.element.stop();
                }
            }
        };
        /**
        * 播放声音时调用的方法
        */
        RongIMVoice.onprogress = function () {
            this.checkInit("onprogress");
        };
        RongIMVoice.checkInit = function (postion) {
            if (!this.isInit) {
                throw new Error("RongIMVoice not initialized,postion:" + postion);
            }
        };
        RongIMVoice.thisMovie = function () {
            return eval("window['player']");
        };
        RongIMVoice.onCompleted = function (duration) {
            var me = this;
            var count = 0;
            var timer = setInterval(function () {
                count++;
                me.onprogress();
                if (count >= duration) {
                    clearInterval(timer);
                }
            }, 1000);
            if (me.isIE) {
                me.thisMovie().doAction("play");
            }
        };
        RongIMVoice.base64ToBlob = function (base64Data, type) {
            var mimeType;
            if (type) {
                mimeType = { type: type };
            }
            base64Data = base64Data.replace(/^(.*)[,]/, '');
            var sliceSize = 1024;
            var byteCharacters = atob(base64Data);
            var bytesLength = byteCharacters.length;
            var slicesCount = Math.ceil(bytesLength / sliceSize);
            var byteArrays = new Array(slicesCount);
            for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
                var begin = sliceIndex * sliceSize;
                var end = Math.min(begin + sliceSize, bytesLength);
                var bytes = new Array(end - begin);
                for (var offset = begin, i = 0; offset < end; ++i, ++offset) {
                    bytes[i] = byteCharacters[offset].charCodeAt(0);
                }
                byteArrays[sliceIndex] = new Uint8Array(bytes);
            }
            return new Blob(byteArrays, mimeType);
        };
        RongIMVoice.palyVoice = function (base64Data) {
            var reader = new FileReader(), blob = this.base64ToBlob(base64Data, "audio/amr"), me = this;
            reader.onload = function () {
                var samples = new AMR({
                    benchmark: true
                }).decode(reader.result);
                me.element = AMR.util.play(samples);
            };
            reader.readAsBinaryString(blob);
        };
        RongIMVoice.isIE = /Trident/.test(navigator.userAgent);
        RongIMVoice.isInit = false;
        return RongIMVoice;
    })();
    RongIMLib.RongIMVoice = RongIMVoice;
    //兼容AMD CMD
    if ("function" === typeof require && "object" === typeof module && module && module.id && "object" === typeof exports && exports) {
        module.exports = RongIMVoice;
    }
    else if ("function" === typeof define && define.amd) {
        define("RongIMVoice", [], function () {
            return RongIMVoice;
        });
    }
})(RongIMLib || (RongIMLib = {}));

 

截止到这里,Flash 播放 AMR 格式 base64 码 就说完了,主要是介绍下大概思路

以上是融云 SDK 里的源码,不过最近推出了直接使用 Audio 方式播放 AAC 格式的音频文件

原始文章连接:https://www.cnblogs.com/yuhongda0315/p/5224450.html

融云官网:https://www.rongcloud.cn

融云文档:https://docs.rongcloud.cn/v4


收起阅读 »

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

前言最近有个项目需要使用的融云的 CallLib SDK 实现类似微信的视频通话,所以在项目还未正式启动的时候,我已经偷偷的开始进行集成了,以免到时候不熟一顿加班那真的欲哭无泪了,好消息就是我已经使用过融云家的 IMLib SDK 做过即时通讯的功能,所以整个...
继续阅读 »

前言

最近有个项目需要使用的融云的 CallLib SDK 实现类似微信的视频通话,所以在项目还未正式启动的时候,我已经偷偷的开始进行集成了,以免到时候不熟一顿加班那真的欲哭无泪了,好消息就是我已经使用过融云家的 IMLib SDK 做过即时通讯的功能,所以整个注册流程和开发者后台的使用已经比较熟了,当然,即时不熟也没关系,跟着他们的文档一步一步来,也能很快的就上手了。
融云官网:https://www.rongcloud.cn/

下面是集成的时候碰到的一些需要注意的问题:

1、Web 站点必须为 localhost 或 https
2、必须成功连接 IM 后, 才可进行 CallLib 通话
3、新版谷歌浏览器(86版本)会报错,我集成的 RTC 版本是 3.2.3
4、音视频通话接通不了

综上问题,我会逐一解答,关于具体的集成可以直接参考:https://docs.rongcloud.cn/v4/views/rtc/call/noui/quick/web.html

还有具体的 demo 也可以参考一下 https://github.com/rongcloud-snippets/web-call-quickstart

Web 站点必须为 localhost 或 https:
这个是融云的使用音视频通话的前置条件,本来在本地调试的时候好好的,可是发布到线上的时候就不能用了,最后提交工单询问融云的技术人员上线是否还需要配置什么,最后排查一圈发现生产环境使用的站点是 http(欲哭无泪。。。),童鞋们引以为戒啊!!

必须成功连接 IM 后, 才可进行 CallLib 通话:
直接看代码:

// appKey 可在融云开发者后台获取
const im = RongIMLib.init({ appkey: '<your-appkey>' })
// 添加事件监听器
im.watch({
  // 连接状态监听
  status(evt) {
    console.log('连接状态码:', evt.status);
  },
  // 消息监听
  message(evt) {
    console.log('收到新消息:', evt.message);
  }
})
// CallLib 初始化
var config = {
    timeout: 20000,
    RongIMLib: RongIMLib,
    RongRTC: RongRTC
};
rongCallLib = RongCallLib.init(config);
//token 可从开发者后台获取 或 Server API
const token = ''
im.connect({ token }).then(user => {
  console.log('链接成功, 链接用户 id 为: ', user.id);
}).catch(error => {
  console.log('链接失败: ', error.code, error.msg);
});

新版谷歌浏览器会报错:
由于浏览器更新,导致 SDK 需要升级,升级到最新版本的 RTC SDK 下载地址:https://cdn.ronghub.com/RongRTC-3.2.6.min.js
需要注意,如果使用的 SDK 2.X 也需要升级到 2.5.10 以上

音视频通话接通不了:
这种情况我总结分析了一下几种情况,如下:

代码传参错误或者书写错误
这个好像没什么说的,只能怪自己不仔细吧!跟着文档来呗

结束通话没有调用挂断方法 hungup
用户主动发起挂断,在 rongCallLib.commandWatch 监听中收到 HungupMessage 消息,可以调用 rongCallLib.hungup 来挂断通话,不然下次呼叫时会出现报错,提示对方正忙

如何识别挂断原因
HungupMessage 消息中 reason 字段及 SummaryMessage 消息中 status 字段都为挂断原因,详情地址参考:https://docs.rongcloud.cn/rtc/calllib/web/code/

因为对音视频的集成也刚开始,还在学习当中。后续随着继续深入,也会同步音视频相关的集成问题,方便复习记录也希望能帮到需要的童鞋!!!


收起阅读 »