七牛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

<!DOCTYPE html>
<html>
	<head>
        <meta charset="utf-8" />
		<title>Index</title>
	</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 代碼。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Room Page</title>
    <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 頁面打開瀏覽器控制檯,看到如下輸出就代表加入房間成功了。

您可以嘗試新開一個 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 一樣的方法。

<!-- index.html -->
<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 對象用於繪製我們的聲波圖。

<!-- room.html -->
...
<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 就行了。

<!-- room.html -->
...
<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;
}

進入頁面後,點擊頁面上的大小窗切換按鈕就能看到動態的切換效果了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章