七牛WebRTN实时音视频应用开发实践

七牛WebRTN实时音视频应用开发实践

这篇文章将使用 Web SDK 详细地介绍一个可用的连麦应用搭建流程,并针对一些常见的问题和需求给出一套可用的解决方案。

目标

我们的目标是一个可以被用于产品的网页连麦应用,为了不让过程显得太复杂,我们将这个连麦的场景定为一对一连麦,也就是两人的在线视频通话。这篇教程将给这个一对一连麦应用实现如下功能:

  • 基本的连麦功能
  • 基本后端服务(使用 NodeJS)
  • 纯音频连麦
  • 自动订阅/发布
  • 大小窗切换
  • 绘制声波图

准备

如果您打算跟着这篇教程一步一步搭建自己的连麦应用,请首先确认完成了下方的开发准备:

  • 拥有基本的 Javascript 开发经验,理解 Promise/async/await 等异步方案
  • 一台拥有摄像头和麦克风的设备(usb 摄像头/麦克风也可以)并安装了最新的 Chrome 浏览器
  • 完成 接入流程 ,拥有一个创建好的 app(建议将 app 的房间最大人数设置为 2 人)

开发流程概述

在正式进入开发流程之前,让我们先梳理一下接下来开发流程的大概结构。之后的篇幅会比较长,您可以根据整个流程结构选择阅读想要了解的细节,或者是对整个开发过程有个初步的认识。

整个开发过程分成 3 个部分,后端开发连麦基本功能开发应用功能完善

后端开发将使用 NodeJS 配合七牛的 NodeJS SDK 来搭建一个简单的后台服务,负责计算 roomToken 提供给前端。

连麦基本功能开发会使用 Web SDK 完成一个基本的一对一通话功能。这里我们不会使用任何的 Web 开发框架,所有的代码都会是框架无关代码。

应用功能完善会一步一步完善我们目标中制定的所有功能,最终达成我们的目标。

好了,让我们正式进入开发流程吧。

说明

这里将使用 NodeJS 来开发我们实时音视频应用需要的后端服务,如果您不熟悉后端开发或者 NodeJS,可以先从这里 下载并安装 NodeJS 到您的机器。下面的代码不会很复杂,一个基本的后端服务是我们开发前端应用的基础,希望您可以按照流程完成后端开发的步骤。

首先简单介绍一下 roomToken,roomToken 是一个包含了一次连麦所需要的主要信息的 token,这些信息包括 七牛的账户标识、连麦的应用id(appId)、连麦的房间号(roomName)、连麦的用户名(userId)、连麦用户的权限(是否可以踢人)等等。这个 token 通过自己七牛账户的私钥进行加密,因为涉及到私钥加密,所以计算 roomToken 的工作不能放在客户端(前端), 所以这里我们需要搭建一个后台为我们计算 roomToken。

顺便也需要一个后台为我们的前端代码提供静态服务,所以这里我们的后端就是实现 2 个功能:

  • 提供计算 roomToken 的接口
  • 一个 http 静态服务

Express 静态服务

准备两个同级的文件夹 app 和 server,前者会放置我们的前端代码,后者放置我们的后端代码。我们首先起一个 Express 服务来为 app 文件夹提供 http 静态服务。
让我们首先进入 server 文件夹并打开命令行窗口(确保命令行目前在 server 目录下)

npm init # 初始化 npm,一直回车即可
npm install express --save

让我们在 server 目录下创建文件 index.js 写入如下代码

// index.js
const express = require('express');
const path = require('path');
const app = express();

// 在 app 文件夹开启静态服务
app.use(express.static('../app'));

app.listen(8888, () => {
  console.log('Demo server listening on port 8888');
});

在刚刚的命令行里运行 node index.js 开启服务。看到 Demo server listening on port 8888 说明服务开启。
您可以尝试在 app 文件夹下创建一个 index.html 文件,一切正常的话访问 http://localhost:8888 就能看到您刚刚创建的 index.html

RoomToken 接口

计算 roomToken 是个复杂的过程,不过通过七牛的 NodeJS SDK 我们可以很快完成这个步骤(如果您想详细了解 roomToken 的计算过程,参见这里)。同上文所说,一个 roomToken 包含了一次连麦的主要信息,所以在计算 roomToken 之前我们需要前端给我们提供部分信息。这里主要包括 3 个信息:连麦的房间号、连麦的用户名、连麦用户的权限,其他信息比如 七牛账户信息、连麦 app 信息等都是在后端提前配置好固定下来的,不需要前端动态提供。

本篇教程出于简单考虑,默认给予所有用户 admin 的权限,即所有用户都有踢人的权限,方便我们后文演示功能。所以梳理下来,这个 roomToken 我们需要前端为我们提供 2 个信息,即 连麦房间号连麦用户名

继续在刚刚的命令行下输入以下命令,安装七牛的 NodeJS SDK

npm install qiniu --save # 安装七牛 NodeJS SDK

修改我们刚刚创建的 index.js,在文件末尾加入如下代码

// index.js
const qiniu = require('qiniu');

const QINIU_AK = '<填写您七牛账户的 AK>';
const QINIU_SK = '<填写您七牛账户的 SK>';
const QINIU_RTN_APPID = '<填写您的连麦 APPID>'; 
const QINIU_CREDENTIALS = new qiniu.Credentials(QINIU_AK, QINIU_SK);

app.get('/roomtoken/user/:userid/room/:roomname', (req, res) => {
  const userId = req.params.userid;
  const roomName = req.params.roomname;

  const roomToken = qiniu.room.getRoomToken({
    appId: QINIU_RTN_APPID,
    roomName: roomName,
    userId: userId,
    expireAt: Date.now() + (1000 * 60 * 60 * 3), // token 的过期时间默认为当前时间之后 3 小时
    permission: 'admin', // 默认所有的用户权限都是 admin,都可以踢人
  }, QINIU_CREDENTIALS);

  res.send(roomToken);
});

其中 AK/SK 在控制台界面 管理面板-密钥管理 里可以查看,连麦 APPID 为实时音视频云中您创建的连麦应用 ID。

一切顺利的话,再次启动后台,访问 http://localhost:8888/roomtoken/user/testuser/room/testroom 就能看到 roomToken的返回。
现在我们可以指定用户名和房间名构造一个请求来获取相应的 roomToken 了。

至此,我们完成了后台的开发工作。下面的代码我们暂定您的后台一直处于运行状态。

交互流程

让我们把工作区切到之前创建的 app 文件夹,现在轮到前端部分,我们的目标是搭建一个连麦应用。首先,我们先梳理这个应用的整个交互流程。整个应用分为 2 个页面,主页(负责输入用户名,采集参数等等信息),房间页面(加入连麦房间后显示的页面,主要为自己和远端的视频画面)。用户首先进入主页输入用户名和房间号,点击进入房间后我们通过这个信息通过后台获取 roomToken,获取成功后加入连麦房间加入第二个页面,开始连麦。

好的,让我们开始逐步完成这个流程

获取 Web SDK 并准备主页

这里我们不打算使用任何 web 开发框架,使用最传统的方式开发这个应用,所以我们通过直接引用 js 来引入 Web SDK。关于引入方式的细节,参照 引入方式。从 Github 上获取到 Web SDK 的最新代码,将其放置在 app/libs 文件夹下。

创建 app/js 文件夹并新建一个空白的 index.js,我们将会在这里编写我们主页的 js 代码。

app 文件夹下创建如下的 index.html


<html>
	<head>
        <meta charset="utf-8" />
		<title>Indextitle>
	head>
	<body>
    <h1>Qiniu Web SDK Demo 1v1ver.h1>
    <p>请输入以下信息加入房间p>

    <form id="rtcroom">
      <input id="userid" type="text" placeholder="请输入用户名" required />
      <input id="roomname" type="text" placeholder="请输入房间号" required />
    form>

    <button form="rtcroom">加入房间botton>

    <script src="./js/index.js">script>
	body>
html>

可以看到,主页就是一个简单的 form 表单,用户输入用户名和房间号,点击加入房间进入我们的下一个步骤。

加入房间

从加入连麦房间开始,就进入到连麦 SDK 负责的步骤了,我们使用这个 API 来 加入房间 。这个方法需要 roomToken 作为加入房间的参数,所以加入房间就分为了 2 个步骤:获取 roomToken、调用 SDK 加入房间。

回到我们之前的设计,在加入房间之后需要进入房间页面进行连麦的逻辑。所以这里涉及到一个页面跳转,如果我们在页面跳转之前就调用连麦 SDK 的 joinRoomWithToken 方法,页面跳转后下一个页面同步上一个页面的 SDK 状态就会比较复杂。所以这里我们让第一个页面能在获取到 roomToken 的时候就跳转页面,将 roomToken 带给下一个页面,然后在房间页面调用 SDK 避免复杂的状态同步。

// js/index.js
document.getElementById('rtcroom').onsubmit = joinRoom;

function joinRoom(e) {
  e.preventDefault();
  const userId = document.getElementById('userid').value;
  const roomName = document.getElementById('roomname').value;
  // 获取 roomToken
  fetch(`/roomtoken/user/${userId}/room/${roomName}`)
    .then(res => res.text())
    .then(roomToken => {
      // 跳转到 room.html
      window.location = "/room.html?token=" + roomToken;
    }).catch(e => {
      console.log('get roomToken error!', e);
    })
}

之后,让我们创建 room.html ,在下一个页面完成加入房间的逻辑,除了 html 文件以外,再创建 css/room.css文件来编写样式,以及 js/room.js 来放置我们 room 页面的 js 代码。


<html>
  <head>
    <meta charset="utf-8" />
    <title>Room Pagetitle>
    <link rel="stylesheet" href="./css/room.css">link>
    <script src="./libs/pili-rtc-web.js">script>
  head>
  <body>
    <div id="localplayer" class="mini-player">div>
    <div id="remoteplayer" class="fullscreen-player">div>

    <script src="./js/room.js">script>
  body>
html>

这里 localplayerremoteplayer 分别用来放置自己和远端的视频流。

/* css/room.css */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.qnrtc-stream-player {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.fullscreen-player {
  width: 100vw;
  height: 100vh;
  position: absolute;
  top: 0;
  left: 0;
  background: #000;
}
.mini-player {
  width: 300px;
  height: 200px;
  position: absolute;
  bottom: 10px;
  right: 10px;
  background: #000;
  z-index: 10;
}

为 room 页面添加一些基本的 css,其中 .qnrtc-stream-player 这个类是 SDK 在播放音视频流自动生成的 video/audio 标签元素。详细情况可以见 stream.play

好了,下面让我们在 room 页面调用 SDK 完成加入房间吧,在 js 文件夹下创建 room.js

// js/room.js

// 注意这里外层套了一个 async iife
// 为了使下面的代码里能够运行 await 方法
// 关于 async/await 的介绍可以看这里 https://www.jianshu.com/p/8d73e187b9e1
(async () => {
  // 先通过地址的 query 拿到上一个页面传过来的 roomToken
  const tokenMatch = window.location.search.match(/\?token\=(.*)$/);
  const roomToken = tokenMatch[1];
  // 初始化 SDK
  const myRTC = new QNRTC.QNRTCSession();
  try {
    // 调用 SDK 加入房间
    const users = await myRTC.joinRoomWithToken(roomToken);
    console.log('joinRoom success! 当前房间用户:', users);
  } catch (e) {
    console.log('error!', e);
  }
})();

好啦,现在访问 http://localhost:8888 输入一对合法的用户名和房间号(只能有数字、字母和下划线且不少于 3 个字符),就会先获取 roomToken 然后跳转到 room 页面完成加入房间。您可以在 room 页面打开浏览器控制台,看到如下输出就代表加入房间成功了。

七牛WebRTN实时音视频应用开发实践_第1张图片

您可以尝试新开一个 tab 页用另一个用户名访问同一个房间,观察房间里 log 输出的变化。

预览并发布自己的视频流

当用户加入房间之后,就可以将自己本地的视频流发布到房间里了,这就是我们通常所说的上麦(发布自己的流)。当然了,在发布自己的流之前,我们需要先采集自己的本地媒体流,所以整个过程就是 加入房间–采集本地媒体流—发布媒体流。让我们在 room.js 中加入如下代码

// js/room.js
...
...
const users = await myRTC.joinRoomWithToken(roomToken);
console.log('joinRoom success!', users);
// 采集本地媒体流,视频和音频都采集
const localStream = await QNRTC.deviceManager.getLocalStream({
    video: { enabled: true, width: 640, height: 480, bitrate: 600 },
    audio: { enabled: true, bitrate: 32 },
});
// 获取我们 room.html 中准备用来显示本地媒体流的元素
const localPlayer = document.getElementById('localplayer');
// 调用媒体流的 play 方法,在我们指定的元素上播放媒体流,其中第二个参数代表 静音播放
localStream.play(localPlayer, true);

// 发布刚刚采集到的媒体流到房间
await myRTC.publish(localStream);
console.log('publish success!');
...
...

这里涉及到 3 个 SDK 的 API,getLocalStream 用来采集本地的媒体流,play 用来指定一个页面元素播放媒体流, publish 用来将媒体流发布到房间。点击这 3 个方法的链接查看详细的 API 说明。

好啦,重新访问 http://localhost:8888 加入房间,一切顺利的话就能在右下角看到自己的视频流。打开控制台,看到 publish success! 就代表发布也成功了,说明我们采集到的视频流已经顺利地发布到房间中了。

自动订阅其他用户

对于一对一连麦来说,这是一个基本功能,即自动订阅房间里另一个用户来获取他发布的媒体流。但是订阅这个操作不像发布一样进入房间就可以调用,订阅操作成功必须满足 2 个条件:

  • 获取订阅目标的用户名 userId
  • 订阅目标必须已经发布了自己的媒体流

为了能让您更好地感受到这个 2 个条件何时被满足,建议您先感受一下两人依次加入房间的时候浏览器控制台的 log 输出的变化。浏览器打开 2 个 tab 页,都访问 http://localhost:8888,在第一个 tab 页以 user1 为用户名 roomtest 为房间号加入房间,加入房间后打开浏览器的控制台观察输出,在joinRoom success! 那一行我们可以看到当我们加入房间之后当前房间里的用户,此时只有user1 一人,也就是他自己。 这时我们切到第二个 tab 页,以 user2 为用户名 roomtest 为房间号也加入这个房间,观察第二个 tab 页的控制台输出,我们可以看到此时输出的当前房间里的用户就已经是 2 个人了。再切回第一个 tab 页观察控制台输出,我们收到了 2 个事件,分别是 user-join 代表有新用户加入了房间(user2),和 user-publish 代表房间内有其他用户发布了自己的媒体流。

自己感受过一次之后,我们就知道,当一个用户加入房间之后,他可以立刻获得这个房间内已有用户的信息,其中 published 字段代表这个用户是否已经发布了媒体流,此时如果有除自己以外的用户已经发布的话,就满足了订阅条件可以发起订阅了。之后我们再通过事件监听获取之后发生的用户加入/用户发布事件,当满足订阅条件时发起订阅,这就是我们实现自动订阅功能实现的基础。

讲的可能有点繁琐,让我们直接来看代码怎么写吧,订阅过程可能在加入房间后或者事件监听回调中发生,所以我们把这个过程抽出来作为一个函数复用。在 room.js 的一开始加入如下代码

// js/room.js

// 订阅用户的函数,myRTC 代表之前初始化 SDK 后拿到的示例
// user 代表加入房间返回或者事件返回的单个用户对象
function subscribeUser(myRTC, user) {
  // 如果用户没有发布就直接返回
  if (!user.published) {
    return;
  }
  // 注意这里订阅使用了 Promise 的写法而没有用 async/await
  // 因为在我们 Demo 中并没有依赖订阅这个操作的后续操作
  // 即没有操作必须等到订阅操作结束之后再运行
  myRTC.subscribe(user.userId).then(remoteStream => {
    // 我们在 room 页面上准备用来显示远端媒体流的元素
    const remotePlayer = document.getElementById('remoteplayer');
    // 在我们准备的元素上播放远端媒体流
    remoteStream.play(remotePlayer);
  }).catch(e => {
    console.log('subscribe error!', e);
  });
}

(async () => {
  const tokenMatch = window.location.search.match(/\?token\=(.*)$/);
  const roomToken = tokenMatch[1];
...
...

好了,准备好了订阅函数之后让我们看准时机发起订阅把,继续在 room.js 中加入如下代码

// js/room.js
...
...
const users = await myRTC.joinRoomWithToken(roomToken);
console.log('joinRoom success! 当前房间用户:', users);
// 监听房间里的用户发布事件,一旦有用户发布,就订阅他
myRTC.on('user-publish', user => {
    subscribeUser(myRTC, user);
});
// 判断房间当前的用户是否有可以订阅的
for (let i = 0; i < users.length; i += 1) {
    const user = users[i];
    // 如果当前房间的用户不是自己并且已经发布
    // 那就订阅他
    if (user.published && user.userId !== myRTC.userId) {
        subscribeUser(myRTC, user);
    }
}
...
...

好啦,现在再重复之前 2 个 tab 页的操作,就能在页面上同时看到本地和远端了。这里我们使用了 SDK 的这 2 个功能,subscribe 用来订阅其他用户发布的媒体流,事件监听 用来通过事件回调同步房间各种状态的变化,事件列表见此。想要了解详细说明点击文中的链接。

自动退出房间

通过上面的步骤我们已经完成了一个连麦应用打大部分功能,这里我们做一个小优化。假设您现在正在使用这个应用进行 2 人连麦,此时关闭其中一人的浏览器窗口,我们在另一个人的页面发现远端的画面立刻卡住了,之后黑屏。此时打开控制台观察 log,发现 SDK 在不断尝试重新订阅,过了很久才会收到 user-unpublishuser-leave 这 2 个事件。这是因为我们在关闭浏览器之前没有立刻发出 “我马上要离开房间了” 这个信息给到房间其他人,其他端发现 P2P 连接断开后认为远端可能发生了网络波动在不断重试。

所以当我们在关闭浏览器页面之前,需要调用 SDK 的 leaveRoom 方法来离开房间。如何在浏览器页面被关闭之前完成这个操作呢,onbeforeunload 事件 的设计就是为了满足这个需求。

在我们的 room.js 中加入如下代码

// js/room.js
...
...

const users = await myRTC.joinRoomWithToken(roomToken);
console.log('joinRoom success! 当前房间用户:', users);

// 加入房间成功后注册事件,当页面被关闭就离开房间
window.onbeforeunload = () => {
    myRTC.leaveRoom();
};

...
...

现在再重复我们刚刚关闭页面的操作,可以从远端的 log 中看出我们很快收到了用户取消发布和离开房间的消息。

至此,我们完成了一个基本可用的一对一连麦应用,下一步,我们将逐渐完善这个应用的功能来达到我们的目标

纯音频连麦(更换采集参数)

在某些场景比如在线通话中,并不需要视频的参与,这里我们就推荐使用纯音频连麦。注意这里的纯音频是一个采集上的概念,也就是在采集端只采集麦克风不采集摄像头。而不是同时采集摄像头和麦克风,只在发送的时候将视频 mute 掉(纯音频的错误用法)。

所以纯音频连麦就是一个更换采集参数的过程,参见我们 getLocalStream 的说明, 只要不传入 video 字段或者将 video 字段的 enabled 设置为 false 就能不采集摄像头,下面让我们用代码实现吧。

体验上应该让用户先在主页 (index.html) 选择是否开启纯音频连麦,加入房间后再在 room 页面配置相应的采集参数。所以在这里我们需要将主页的用户选择带入 room 页面,使用和之前 roomToken 一样的方法。


<form id="rtcroom">
    <input id="userid" type="text" placeholder="请输入用户名" required />
    <input id="roomname" type="text" placeholder="请输入房间号" required />
    
    <select id="record_option">
        <option value="audioonly">纯音频连麦option>
        <option value="normal" selected>正常连麦option>
    select>
form>
// js/index.js

// 修改 joinRoom 函数,加入采集选项的参数,并通过地址 query 传入 room 页面
function joinRoom(e) {
  e.preventDefault();

  const userId = document.getElementById('userid').value;
  const roomName = document.getElementById('roomname').value;
  // 获取用户采集选项的选择
  const recordOption = document.getElementById('record_option').value;

  fetch(`/roomtoken/user/${userId}/room/${roomName}`)
    .then(res => res.text())
    .then(roomToken => {
      // 将采集选项的结果传入下一个页面
      window.location = `/room.html?token=${roomToken}&option=${recordOption}`;
    }).catch(e => {
      console.log('get roomToken error!', e);
    })
}

好了,下一步就是在 room.js 中读取这个采集参数来判断使用什么方式来采集媒体流了。修改 room.js 的如下行

// js/room.js
...
...
// 这里获取 roomToken 的正则和之前有修改,因为加入了 option 字段后 query 的格式变了
const tokenMatch = window.location.search.match(/\?token\=(.*)\&/);
const roomToken = tokenMatch[1];
// 获取 option 字段的值,这里多说一句建议在正式的开发中建议使用 query-parser 这种成熟的 query 解析库
const recordOptionMatch = window.location.search.match(/\&option\=(.*)$/);
const recordOption = recordOptionMatch[1];

const myRTC = new QNRTC.QNRTCSession();
try {
    const users = await myRTC.joinRoomWithToken(roomToken);
    console.log('joinRoom success! 当前房间用户:', users);
	// 根据 option 是否为 audioonly 来选择是否开启视频采集
    const enableVideo = recordOption !== "audioonly";
    const localStream = await QNRTC.deviceManager.getLocalStream({
        video: { enabled: enableVideo, width: 640, height: 480, bitrate: 600 },
        audio: { enabled: true, bitrate: 32 },
    });
...
...

好了,现在访问 http://localhost:8888 选择纯音频连麦再尝试 2 人加入房间,界面就是一片黑色只能听到远端的视频。这就是纯音频连麦了。

绘制声波图(获取音频回调)

在纯音频连麦的过程中,我们经常有这种需求,展示当前是谁在发言,比如当某人说话时就在他的麦克风图标上做高亮处理。为了实现这种需求,我们就需要实时地去获取一个媒体流中正在播放的音频数据。在我们的场景中,这种设计可能显得有些多余,但我们仅仅是为了演示这个功能,所以就来绘制一个实时的声波图吧。

在绘制之前,我想先介绍一下我们 SDK 提供的和音频回调相关的 API,它们分别是:

  • getCurrentTimeDomainData 获取当前音频的时域数据
  • getCurrentFrequencyData 获取当前音频的频域数据
  • onAudioBuffer 获取音频 PCM 数据

这 3 个方法的详细说明可以参见 stream 对象。这里我们主要讨论这 3 种音频采集方法的区别,以及在什么情况下使用哪种方法。

我们知道,音频数据是根据采样率的大小在一个数组里按时间顺序填充的采样数据,播放音频时,也会时序地将这个数组中的数据按批次取出并送入声卡中,声卡中正在处理的那一批音频数据,就是我们在那一刻听到的声音。所以这里的第一个方法 getCurrentTimeDomainData,就是实时地去获取当前正在处理的音频数据(实际上这并不是声卡当前正在处理的数据,只是尽量精确的一个离当前播放 buffer 最近的范围为 2048 长度的音频)。不过这里要注意,我们不能通过不断地调用这个方法来收集音频的原始数据,这个方法仅用于一些实时的音频分析和处理(比如我们绘制声波图),如果想要收集音频的原始数据,使用我们的第三个方法 onAudioBuffer,这个函数的返回不能保证实时性,但是会根据播放的进度不断地将之前用于播放的音频数据回调回来。好了,这样我们还剩下最后一个方法没有介绍,其实很简单,第二个方法 getCurrentFrequencyData 就是将当前的时序音频数据做了一次 STFT 变换得到的频域数据,一般可用用来绘制频谱图,或者用来判断某个频段是否有声音(是否有人说话)。

介绍完了我们 SDK 提供的方法,回到我们的场景,这里需要用到的其实就是获取当前的时域数据(getCurrentTimeDomainData) 这个方法。下面看代码吧,首先在 room.html 里创建 2 个 canvas 对象用于绘制我们的声波图。


...
<div id="localplayer" class="mini-player">
    <canvas width="300" height="200" id="localwave">canvas>
div>
<div id="remoteplayer" class="fullscreen-player">
    <canvas width="640" height="480" id="remotewave">canvas>
div>
...

在 room.css 中添加相应的 css,注意这里我们固定了 canvas 的宽高,因为 canvas 跟随窗口动态宽高太过复杂,这里就不赘述了。

/* css/room.css */
canvas {
  position: absolute;
  top: 0;
  left: 0;
}

有了 canvas 之后,就可以开始绘制了,绘制的过程设计到本地音频的绘制和远端音频的绘制,所以我们还是先将绘制操作抽成一个公共的函数,在 room.js 一开始加入如下代码。

// js/room.js
// 绘制声波图,stream 为需要实时绘制的媒体流对象
// ctx 为 canvas 的 context 对象,用来区分画在哪个 canvas 上
function drawAudioWave(stream, ctx) {
  // 如果没有流或者流被释放了(远端取消发布等情况)就直接返回
  if (!stream || stream.isDestoryed) {
    return;
  }
  // 获取当前实时的时域数据
  const timeData = stream.getCurrentTimeDomainData();
  // 以下为 canvas 相关的绘制代码
  const width = ctx.canvas.width;
  const height = ctx.canvas.height;
  ctx.fillStyle = "#000";
  ctx.strokeStyle = "#fff";
  ctx.lineWidth = 2;
  ctx.fillRect(0, 0, width, height);
  ctx.beginPath();
  for (let i = 0; i < width; i += 1) {
    const dataIndex = Math.round(i * (timeData.length / width));
    const data = Math.round(timeData[dataIndex] * (height / 255.0));
    if (i === 0) {
      ctx.moveTo(i, data);
    } else {
      ctx.lineTo(i, data);
    }
  }
  ctx.stroke();
  // 调用 requestAnimationFrame 逐帧更新
  requestAnimationFrame(() => drawAudioWave(stream, ctx));
}

有了绘制函数,接下来就在相应的地方调用它吧

// js/room.js
function subscribeUser(myRTC, user) {
  if (!user.published) {
    return;
  }

  myRTC.subscribe(user.userId).then(remoteStream => {
    const remotePlayer = document.getElementById('remoteplayer');
    remoteStream.play(remotePlayer);
    // 修改订阅函数,如果订阅的目标流没有开启视频(纯音频)
    // 就绘制声波图
    if (!remoteStream.enableVideo) {
      const ctx = document.getElementById('remotewave').getContext('2d');
      drawAudioWave(remoteStream, ctx);
    }
  }).catch(e => {
    console.log('subscribe error!', e);
  });
}
...
...

const localPlayer = document.getElementById('localplayer');
localStream.play(localPlayer, true);
await myRTC.publish(localStream);
// 如果本地发布的流没有开启视频采集
// 就绘制本地的声波图
if (!enableVideo) {
    const ctx = document.getElementById('localwave').getContext('2d');
    drawAudioWave(localStream, ctx);
}

...
...

好了,现在再使用纯音频连麦进入的话,就可以看到自己和远端的声波图啦。这里是用了最简单的方法绘制的声波图,仅仅展示 API 用,使用频域和一些算法搭配绘制会有更好的效果,这里就不赘述了。

大小窗切换

严格来说大小窗切换并不算 SDK 需要负责实现的功能,但这个需求在一对一连麦场景中经常用到,这里就介绍其中一种实现方案。

我们所谓"大""小"窗中的大小概念不过是 css 中的一些属性控制的,所以最简单的大小窗切换方案就是通过改变元素的 css 来实现。这里我们使用的方法更为简单,通过直接交换 2 个视频容器元素的 class 值来达到交换 2 者 css 属性的效果。再通过 transition 来为 css 切换的过程加上动画,一个低成本的大小窗切换就实现了。

回到我们的 room.html 上,我们注意到之前我们分别给 2 个容器元素设定了 2 个 class:mini-playerfullscreen-player, 我们现在只需要交换这 2 个 css 就行了。


...
<div id="remoteplayer" class="fullscreen-player">
    <canvas width="640" height="480" id="remotewave">canvas>
div>

<button class="btn screen-switch" onclick="switchScreen()">大小窗切换button>
<script src="./js/room.js">script>
...

下面在 room.js 中完成 switchScreen , 在开头添加如下代码

// js/room.js
function switchScreen() {
  const localPlayer = document.getElementById("localplayer");
  const remotePlayer = document.getElementById("remoteplayer");

  // 交换 2 个元素的 class
  if (localPlayer.className === "mini-player") {
    localPlayer.className = "fullscreen-player";
    remotePlayer.className = "mini-player";
  } else {
    localPlayer.className = "mini-player";
    remotePlayer.className = "fullscreen-player";
  }
}

最后,为我们添加的 button 加上 css,并给 css 切换加上动画过渡

/* css/room.css */
.fullscreen-player,.mini-player {
  transition: all ease 0.6s;
}

.btn {
  outline: none;
  border: none;
  position: absolute;
  z-index: 9;
  padding: 5px;
}

.screen-switch {
  bottom: 230px;
  right: 30px;
}

body {
  overflow: hidden;
  background: #000;
}

进入页面后,点击页面上的大小窗切换按钮就能看到动态的切换效果了。

你可能感兴趣的:(Qiniu)