vue 接入腾讯实时音视频 trtc-js-sdk 的技术难点与解决方案

403 阅读9分钟

技术难点

1.低延迟,如果要满足比较流畅地进行实时互动,那么单向的端到端的迟延大概要在 400 毫秒以下才能保证流畅沟通;

2.流畅性,你也很难想象在视频过程中频繁卡顿会有良好的互动;

3.回声消除,回声的产生是扬声器播放的声音经过环境反射被麦克风重新采集并传输给对方,这样对方就会一直听到自己的回声,整个互动过程会非常难受;

4.国内外互通,随着现在国内同质化产品越来越多,国内的竞争也异常激烈,很多厂商纷纷选择出海,这时就需要做好海内外的互通;

5.海量并发,当然这不仅仅指实时音视频了,基本对于任何一款互联网产品而言都是必须要考虑的难点。

解决思路和实践经验

1、低延迟

首先,如果实时音视频要保证低延迟,那么前端和后端的整个链条一定要做到极致的,比如前端的一些编码算法、流控,甚至丢帧、追帧策略等等都要做到足够好。另外,不同的业务场景下,编码器的选择也会有所区别,从而会带来不同的编码延迟,因此不同的业务场景能达到的延迟程度也是不一样的。

其次,就是对推拉流网络的选择,通常的方案是让需要实时互动的用户通过核心语音视频网络——像 BGP 这样的优质节点来做语音视频传输,而对于一些特定场景来说,比如互动游戏会直播给一些围观用户看,那么这里就需要做转码、转协议、甚至混流,再通过内容分发网络去分发。像 内容分发网络本身天然就有做就近接入,但对于接入核心语音视频网络就需要有智能的调度策略来完成就近接入,以及跨运营商、跨区域的接入,比如可以采用上次登录 IP、常用 IP 和区域调度,甚至可以测速再去连接,当然网络调度的策略也需要根据业务群的分布仔细规划,甚至采用多个策略配置权重的方式。

2、流畅性

要实现流畅性也会有很多的技术难点和策略,我主要会介绍其中几种。

第一、可以做动态伸缩的 JitterBuffer,在网络较差或者网络抖动比较剧烈的情况下,可以适当增大 JitterBuffer,从而降低一点点延迟来对抗抖动。

第二、快播和慢播技术,在网络较差的环境,可以在用户无感知的条件下稍微降低播放速度,来应对短暂网络抖动引起的立即卡顿,当网络恢复可以加快速度追回来,但这种方式并非适合所有场景,比如对于节奏要求非常准确的唱歌场景,当播放速度稍微放慢就可以被感知。

第三、码率自适应,也就是以比较合适的码率做动态传输,为了保证流畅度甚至可以调整帧率和分辨率。语音视频引擎会根据当前网络测速的结果和应用所期望的码率,动态地调整码率、帧率和分辨率,最终达到流畅观看的用户体验。

第四、分层编码、传输控制,在推流端做一些分层的编码,这样在拉流端可以动态根据侦测到的网络带宽情况来拉取不同的数据去做渲染。分层编码允许拉流端取选择不同层次的视频编码数据,网络情况好的时候,就拉取较多层次的数据;网络情况差的情况下,就拉取基础层次的数据。

第五、动态调度,当在推拉流端监测当前推拉流质量比较差,而且即使通过降低码率、帧率和分辨率等策略已经无法保证质量,这时就可以选择放弃这条链路,直接重新做选入、建立连接,当然在这个过程中可能会出现短暂的停顿。

3、回声消除

首先介绍下回声消除的原理:对端发送的信号会先给到回声消除的模块,作为将来消除的参考信号,再把信号给到扬声器播放,扬声器播放后由于周围环境反射形成回声,与真实的音频输入一同被麦克风采集,这时采集到的输入信号是带有回声的,回声消除模块会根据前面的参考信号生成滤波抵消掉回声消后再发送出去。

原理听起来会比较简单,但在实际过程中却蕴藏着很多的难点,比如回声消除模块接收的参考信号与最终被环境反射后的回声本身就是存在差异的,此外设备也会极大的影响回声消除,尤其是国内的安卓机型特别多,比如国内某手机厂商,从麦克风采集音频数据到提交中间有将近一百毫秒的延迟,这时回声消除算法如何适应这么长回声延迟的手机就很关键;再比如很多用户在直播中都会用外置声卡,甚至是模拟器,这无形中也会带来回声的延迟。除了设备,场地同样存在很大的相关性,对于普通会议室,设置 40 米的回声延迟可能已经足够了,但一些大会场这种回声延迟能达到将近上百米,这也是一种挑战。

关于回声消除,其实谷歌开源的 WebRTC 提供了回声消除模块,但本应用的设计本身是为了在 PC 端实时音视频互动的场景,在移动端的适应性上就会差一些,尤其体现在安卓的一些低端机上。而相对来说,苹果因为整体硬件、软件全是自己实现的,麦克风、扬声器也都有声学模型设计,因此回声消除的效果会比安卓好很多。即构科技的音视频引擎都是采用自研,在真机和模拟器等 1000 多的机型上测试过,都可以做到很好的回声消除。

4、 国内外互通

前面提到很多产品都会选择出海,包括主打国内市场的产品也会有一些海外用户,因此流媒体数据和控制信令就要做好跨国的互通,这就需要考虑在全球合理布置一些中继节点。

这张图就是一个典型的中继续传,北京用户和迪拜用户之间要做视频沟通,根据就近接入原则他们会分别连接当地的节点,而这两个节点间如果互拉,效果会非常差,这时就需要布置适合的中继节点,比如香港、新加坡、日本等等,数据路径的选择是需要根据业务侧决定的,也就是说在物理链路路由之上还要再有一条业务的路由表,需要根据用户场景制定,包括用户分布、用户访问频率、高频段峰值等等,可能每次的路由都会有所不同。

5、海量并发

海量并发是所有互联网产品都会遇到的问题,这里就不再展开,主要要考虑负载均衡,如何平滑扩容,对于无法覆盖的地方要做代理调度,甚至需要考虑容灾、接入层的设计等等。

实时语音视频的技术门槛相对比较高,如果依靠自己研发,可能即使会投入很多开发成本也无法与匹配市场快速发展的节奏。我们可以先看一下腾讯的实时音视频(TRTC)。

整体流程

根据业务逻辑在 service 里签名传到前台,前台根据 userId,和签名,房间号(房间号后台管理,每进入房间默认创建房间号,返回前端)进入房间,然后根据前台操作开启视频直播,此房间号后台记录后广播到用户展示的房间列表,其他用户通过点击房间号,进入各个房间,根据操作开启关闭摄像头通讯。

集成方式

下载

npm i trtc-js-sdk 

引入

import TRTC from "trtc-js-sdk";

腾讯云文档上的看到这样配置

const client = TRTC.createClient({
mode: 'videoCall', //实时音视频通话模式,设置为‘videoCall’。
sdkAppId, //您从腾讯云申请的 sdkAppId
userId, //用户ID不唯一的随机数,可自己写
userSig //用户签名 
});

看一下源码和 demo.

发现 userSigsdkAppId 是从genTestUserSig这个方法来的,其他userId,roomId自己随意写

SDKAppId,需要替换为您自己账号下的SDKAppId,需在腾讯云申请,

SECRETKEY 也一样需要替换为您自己账号下的SECRETKEY

具体申请及 demo 请参考官方文档:点我查看

我们来看一下如何使用,代码做了必要的注释。

需要注意的是一定要给以上 div 添加宽高,否则视频无法显示

<template>
  <div class="center-page">
    <div v-html="remoteStream"
         :class="remoteStream?'distant-stream':''">
    </div>
    <div id='local_stream'
        class="local-stream">
    </div>
  </div>
</template>




<script>
//前端测试要导入demo里lib-generate-test-usersig.min.js,不然签名无法成功,后面要从后端签名后返回值。
import LibGenerateTestUserSig from '@/assets/js/lib/lib-generate-test-usersig.min.js'

//导入sdk
import TRTC from "trtc-js-sdk";
export default {
  data () {
    return {
      userId: 'user_' + parseInt(Math.random() * 100000000),//用户id --可更改
      roomId: 888888,//房间号--加入相同房间才能聊
      client: '',//客户端服务
      remoteStream: '',//远方播放流
      localStream: '',//本地流
    }
  },

 mounted () {
    //测试用,所以直接创建了,其他需求可自行更改
    this.createClient(this.userId)
 },

 methods: {
    //创建链接
    createClient (userId) {
      //获取签名
      const config = this.genTestUserSig(userId)
      const sdkAppId = config.sdkAppId
      const userSig = config.userSig
      this.client = TRTC.createClient({
        mode: 'videoCall',
        sdkAppId,
        userId,
        userSig
      });
      //注册远程监听,要放在加入房间前--这里用了发布订阅模式
      this.subscribeStream(this.client)
      //初始化后才能加入房间
      this.joinRoom(this.client, this.roomId)
    },
    //加入房间
    joinRoom (client, roomId) {
      client.join({ roomId })
        .catch(error => {
          console.error('进房失败 ' + error);
        })
        .then(() => {
          console.log('进房成功');
          //创建本地流
          this.createStream(this.userId)
          //播放远端流
          this.playStream(this.client)
      });
    },
    
    //创建本地音视频流
    createStream (userId) {
      const localStream = TRTC.createStream({ userId, audio: true, video: true });
      this.localStream =localStream 
     
      localStream
        .initialize()
        .catch(error => {
          console.error('初始化本地流失败 ' + error);
        })
        .then(() => {
          console.log('初始化本地流成功');
          // 创建好后才能播放 本地流播放 local_stream 是div的id
          localStream.play('local_stream');
          //创建好后才能发布
          this.publishStream(localStream, this.client)
        });
    },

    //发布本地音视频流
    publishStream (localStream, client) {
      client
        .publish(localStream)
        .catch(error => {
          console.error('本地流发布失败 ' + error);
        })
        .then(() => {
          console.log('本地流发布成功');
        });
     },
     
    //订阅远端流--加入房间之前
    subscribeStream (client) {
      client.on('stream-added', event => {
        const remoteStream = event.stream;
        console.log('远端流增加: ' + remoteStream.getId());
        //订阅远端流
        client.subscribe(remoteStream);
      });
    },

    //播放远端流
    playStream (client) {
      client.on('stream-subscribed', event => {
        const remoteStream = event.stream;
        console.log('远端流订阅成功:' + remoteStream.getId());
        // 创建远端流标签,因为id是动态的,所以动态创建,用了v-html
        
        this.remoteStream = `<view id="${'remote_stream-' + remoteStream.getId()}"  ></view>`;
        
        //做了dom操作 需要使用$nextTick(),否则找不到创建的标签无法进行播放
        this.$nextTick(() => {
            //播放
          remoteStream.play('remote_stream-' + remoteStream.getId());
        })
      });
    },

    //退出音视频
    leaveRoom (client) {
      client
        .leave()
        .then(() => {
            console.log('退房成功')
          // 停止本地流,关闭本地流内部的音视频播放器
          this.localStream.stop();
          // 关闭本地流,释放摄像头和麦克风访问权限
          this.localStream.close();
          this.localStream = null;
          this.client = null
          // 退房成功,可再次调用client.join重新进房开启新的通话。
        })
        .catch(error => {
          console.error('退房失败 ' + error);
          // 错误不可恢复,需要刷新页面。
        });
    },


    //获取用户签名--前端测试用
    genTestUserSig (userID) {
      /**
       * 腾讯云 SDKAppId,需要替换为您自己账号下的 SDKAppId。
       *
       * 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/rav ) 创建应用,即可看到 SDKAppId,
       * 它是腾讯云用于区分客户的唯一标识。
       */
      const SDKAPPID = '自己在腾讯云申请的SDKAppId';
      /**
       * 签名过期时间,建议不要设置的过短
       * <p>
       * 时间单位:秒
       * 默认时间:7 x 24 x 60 x 60 = 604800 = 7 天
       */
      const EXPIRETIME = 604800;
      /**
       * 计算签名用的加密密钥,获取步骤如下:
       *
       * step1. 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/rav ),如果还没有应用就创建一个,
       * step2. 单击“应用配置”进入基础配置页面,并进一步找到“帐号体系集成”部分。
       * step3. 点击“查看密钥”按钮,就可以看到计算 UserSig 使用的加密的密钥了,请将其拷贝并复制到如下的变量中
       *
       * 注意:该方案仅适用于调试Demo,正式上线前请将 UserSig 计算代码和密钥迁移到您的后台服务器上,以避免加密密钥泄露导致的流量盗用。
       * 文档:https://cloud.tencent.com/document/product/647/17275#Server
       */
      const SECRETKEY =
        "自己在腾讯云申请的SECRETKEY";

      // a soft reminder to guide developer to configure sdkAppId/secretKey
      if (SDKAPPID === "" || SECRETKEY === "") {
        alert(
          "请先配置好您的账号信息: SDKAPPID 及 SECRETKEY " +
          "\r\n\r\nPlease configure your SDKAPPID/SECRETKEY in js/debug/GenerateTestUserSig.js"
        );
      }
      const generator = new LibGenerateTestUserSig(SDKAPPID, SECRETKEY, EXPIRETIME);
      const userSig = generator.genTestUserSig(userID);
      return {
        sdkAppId: SDKAPPID,
        userSig: userSig
      };
    }
  }
}
</script>




<style lang="scss" scoped>
//本地流
.local-stream {
  width: 500px;
  height: 500px;
}
//远端流
.distant-stream {
  width: 200px;
  height: 200px;
}
</style>

要查看腾讯云的申请的SDKAPPID是否可用,服务器端口也要注意打开,特别注意线上环境要在 https 环境下运行,详情可参考官网,以下有记录地址。

总结

ios 11 版本可以在 Safari 浏览器中才能打开,微信浏览器不支持,可切换为外部的 Safari 浏览器中打开,经测试,h5 端 IOS 微信内置浏览器不支持调用,Android 浏览器允许打开摄像头情况下调用正常。如果报 Relay server timeout observed 错误,可以查看两端通信的 userID 是否是一样的,相同无法推送远方流给双方,和自己打电话给自己无法打通一个道理,可自行在 userID 后面加上随机数,防止两端 id 重复。如果报 navigator.mediaDevices is undefined 错误,其中主要原因是浏览器的安全限制,通过 MediaDevices.getUserMedia() 获取用户多媒体权限时,需要注意其只工作于以下三种环境:

1 .localhost 域, 不要用 ip 地址访问,比如 127.0.0.1 。

2. 启了 https 的域,http 的不能用。

3. 使用 file:/// 协议打开的本地文件,线上环境一定要 https 协议!

这样就完成了简单的接入工作,每个开发的业务工作可能不一样,具体的可以参考官方的API文档