功能描述
本文介绍如何在通话中开启互动白板:发起方(下文称 host)无需屏幕分享,直接开启一块白板推送给远端;观看方(下文称 guest)开启 host 的白板,一起书写、绘画。白板支持插入 PDF、PPT、图片、视频等内容。
| 角色 | 能力 |
|---|---|
| host(发起方) | 开启白板(占用辅流推送白板画面),插入 PDF / PPT / 图片 / 视频,按需切换只读 |
| guest(观看方) | 订阅 host 的白板画面(辅流),开启 host 的白板,一起书写 |
核心优势
常规的互动白板方案通常需要单独接入一套白板 SDK 与服务:白板内容通过独立的信令通道同步,各端分别渲染,与音视频是两条链路,观看端、录制、回放都要额外适配。TRTC 互动白板在 host 端把白板直接渲染成视频画面,推给远端的就是一路普通的视频流(辅流),因此:
| 对比项 | 常规方案 | TRTC 互动白板 |
|---|---|---|
| 服务开通 | 需要额外开通白板服务,维护独立的房间与鉴权 | 复用 TRTC 房间与音视频通道,无需额外服务 |
| 接入成本 | 集成独立 SDK,自行实现工具栏、课件转码与状态同步 | 注册插件后调用几个接口即可,内置工具栏开箱即用,PDF / PPT 在本地直接加载 |
| 观看端 | 每个观看端都要集成白板 SDK | 不支持白板能力的端(只拉流的观众、小程序、原生端)也能看到白板 |
| 音画同步 | 白板数据与音视频分两路传输,需要按时间戳对齐 | 白板在同一帧里渲染,再与音频一起推流,天然同步 |
| 录制 | 需要专门的白板录制服务,或分别录制音视频与白板数据 | 直接复用 TRTC 云端录制,录制文件就包含完整的白板画面(笔迹、PDF / PPT 翻页、图片、视频) |
| 旁路推流 | 需要额外合成后才能转推 | 直接复用云端混流与转推 CDN,CDN 观众同样看到白板 |
| 回放 | 需要专用播放器还原白板,重新加载课件并与音视频对齐 | 标准音视频文件,任意播放器、网页 <video>、点播服务都能直接播放,不依赖原始课件文件 |
| 显示一致性 | 各端分别渲染,PDF / PPT 版式受字体、浏览器、分辨率影响 | 实时观看、录制、旁路、回放画面完全一致 |
| 服务端成本 | 额外的白板服务、课件转码、录制转码费用 | 无额外服务,沿用现有的录制、混流与点播计费 |
前提条件
-
TRTC Web SDK 版本 >= 5.21.0
-
兼容性:
操作系统 浏览器 host(发起方) guest(参与方) Windows / macOS Chrome 86+、Edge 86+ 及其他 Chromium 86+ 内核浏览器 ✅ ✅ macOS Safari 15.4+ ✅ ✅ Windows / macOS Firefox 117+ ✅ ✅ Android Chrome 86+、微信内嵌浏览器及其他 Chromium 86+ 内核浏览器 ❌ 移动端暂不支持合流 ✅ iOS 15.4+ Safari、微信内嵌浏览器等 ❌ 移动端暂不支持合流 ✅ 只拉流观看、不参与白板互动的用户不受上表限制:白板已渲染进辅流画面,任意支持 TRTC 拉流的环境都能看到,参见 浏览器支持情况。
-
角色为 anchor(audience 角色不能调用白板 API)。
实现流程
部署静态资源
插入 PDF / PPT 依赖一些静态资源文件,为保证浏览器可以正常加载这些文件,你需要完成以下步骤:
- 将
node_modules/trtc-sdk-v5/assets目录发布至 CDN 或者静态资源服务器中(白板资源位于其中的whiteboard目录)。 - 在创建 trtc 实例时传入您的 CDN 地址给
assetsPath参数,例如:TRTC.create({ assetsPath: 'https://xxxx/assets' }),SDK 会按需加载相关资源文件。
- 未配置
assetsPath时,白板的 PDF 按钮与 PPT 入口不会出现,其他功能不受影响。- 如果目录下文件的 Host URL 与网页应用的 Host URL 不一致,则需要开启访问文件域名的 CORS 策略。
- 不能把目录文件放在 HTTP 服务下,因为在 HTTPS 域名下加载 HTTP 资源会被浏览器安全策略禁止。
引入并注册插件
白板依赖 VideoMixer(生成白板画面)与 Whiteboard(白板插件),并且需要开启 enableSEI。
import TRTC from 'trtc-sdk-v5';
import { VideoMixer } from 'trtc-sdk-v5/plugins/video-effect/video-mixer';
import { Whiteboard } from 'trtc-sdk-v5/plugins/whiteboard';
import AudioPlayer from 'trtc-sdk-v5/plugins/audio-player'; // 可选,见下方说明
const trtc = TRTC.create({
plugins: [VideoMixer, Whiteboard, AudioPlayer],
enableSEI: true,
assetsPath: 'https://xxxx/assets'
});
- 未注册
VideoMixer/Whiteboard时调用白板 API 会抛出错误。guest 只需要注册Whiteboard。AudioPlayer可选:用于播放白板中插入视频的声音,未注册时视频无声。
host:开启白板
await trtc.startWhiteboard({
view: 'local-board', // 白板容器,传元素 id 或 HTMLElement
option: {
profile: '1080p', // 默认值:1920×1080 / 15fps / 2000kbps
backgroundColor: '#fcfcfc', // 可选,不传使用主题底色
theme: 'light' // 可选
}
});
- 白板会占用辅流推送白板画面,因此开启白板期间不能开启屏幕分享,反之亦然。
profile同时决定白板画布尺寸与出帧帧率。
guest:开启白板
guest 需要先订阅 host 的白板画面(辅流),再开启白板。能否开启成功取决于对端状态,请使用 try/catch 处理。
await trtc.startRemoteVideo({
userId: 'host_user_id',
streamType: TRTC.TYPE.STREAM_TYPE_SUB,
view: 'remote-board'
});
try {
await trtc.startWhiteboard({ userId: 'host_user_id' });
} catch (error) {
// 对端未开启白板 / 未订阅该用户辅流 / 等待超时等
console.warn(error.message);
}
- 必须先
startRemoteVideo订阅辅流,再调用startWhiteboard({ userId }),顺序颠倒会抛出错误。- host 与 guest 谁先开启都可以,SDK 会等待对端就绪(最长 10s)。
- guest 同一时刻只能开启一个 host 的白板,传入新的
userId即切换 host。
按需更新参数
// host:切换底色与主题
await trtc.updateWhiteboard({ option: { backgroundColor: '#ffffff', theme: 'dark' } });
// host:本端只读,guest 照常可以书写
await trtc.updateWhiteboard({ option: { interactive: false } });
// host:更换白板容器
await trtc.updateWhiteboard({ view: 'small-board' });
// guest:只读模式(只看不写,不关闭白板)
await trtc.updateWhiteboard({ userId: 'host_user_id', option: { interactive: false } });
// guest:更换播放容器,白板随视频画面一起迁移
await trtc.updateRemoteVideo({ userId: 'host_user_id', streamType: TRTC.TYPE.STREAM_TYPE_SUB, view: 'another-view' });
关闭白板
// guest:关闭白板
await trtc.stopWhiteboard({ userId: 'host_user_id' });
// host:关闭白板(停止推流并释放白板)
await trtc.stopWhiteboard();
API 说明
trtc.startWhiteboard(options)
- host:开启白板,并通过辅流推送白板画面。
- guest:开启指定用户的白板。
options
| 参数名 | 类型 | Attributes | Description |
|---|---|---|---|
| userId | string |
guest 必填 | 目标 host userId。不传表示本端为 host |
| view | string | HTMLElement | null |
host 必填 | 白板容器,传元素 id 或 HTMLElement。不需要本地预览时传 null。guest 传入会被忽略 |
| option.profile | string | object |
选填,仅 host | 白板辅流的编码参数,默认 '1080p'(1920×1080 / 15fps / 2000kbps)。可传 '720p' 等字符串,或 { width, height, frameRate, bitrate } |
| option.backgroundColor | string |
选填,仅 host | 白板底色(CSS 颜色),同时是远端看到的底色。默认使用主题底色 |
| option.theme | string |
选填 | 内置工具栏配色:'light'(默认)、'dark',只影响本端 |
Example:
// host
await trtc.startWhiteboard({ view: 'local-board', option: { profile: '720p' } });
// guest
await trtc.startWhiteboard({ userId: 'host_user_id' });
trtc.updateWhiteboard(options)
增量更新白板参数,只修改传入的字段。
options
| 参数名 | 类型 | Attributes | Description |
|---|---|---|---|
| userId | string |
guest 必填 | 目标 host userId。不传表示本端为 host |
| view | string | HTMLElement | null |
选填,仅 host | 更换白板容器,null 表示不预览。传入与当前相同的容器时,若容器尺寸发生变化,白板会按新尺寸重新布局 |
| option.backgroundColor | string |
选填,仅 host | 白板底色(CSS 颜色) |
| option.theme | string |
选填 | 内置工具栏配色:'light'、'dark' |
| option.interactive | boolean |
选填 | 本端能否书写。白板开启时为 true,不传保持当前值。false 时隐藏工具栏、页码条等交互 UI |
trtc.stopWhiteboard(options)
- host:关闭白板,停止推流并释放白板。
- guest:关闭白板。
options
| 参数名 | 类型 | Attributes | Description |
|---|---|---|---|
| userId | string |
guest 必填 | 目标 host userId。不传表示本端为 host |
事件
白板状态变化通过 TRTC.EVENT.WHITEBOARD_STATE_CHANGED 通知,回调参数为 { userId, type, payload }:
| 字段 | 说明 |
|---|---|
| userId | host 端为空字符串;guest 端为白板所属的 host userId |
| type | 'available':白板可用;'unavailable':白板不可用;'error':白板运行异常 |
| payload | 事件详情,见下文 |
请只处理以上三种
type,其他取值请忽略。
available:白板可用
收到后可以显示 / 点亮白板相关的入口。payload.reason 取值:
| reason | 含义 |
|---|---|
'started' |
白板会话建立:host 白板开启成功;guest 与 host 握手成功 |
'resume' |
仅 guest:白板从暂时不可用(网络中断、同步繁忙)中恢复 |
trtc.on(TRTC.EVENT.WHITEBOARD_STATE_CHANGED, ({ userId, type, payload }) => {
if (type !== 'available') return;
// 显示 / 点亮白板入口
showWhiteboardEntry(userId);
if (payload.reason === 'resume') {
hideBanner(); // 收起「连接中断」等提示
}
});
unavailable:白板不可用
收到后应收起 / 置灰白板相关的入口。是否需要提示用户、是否需要重新调用接口,取决于 payload.reason:
| reason | 角色 | 含义 | 处理建议 |
|---|---|---|---|
'end' |
host / guest | 白板会话结束:host 调用了 stopWhiteboard() |
收起白板入口;guest 可提示「主讲人已关闭白板」。guest 无需关闭白板,host 再次开启时会重新收到 available |
'local-stop' |
guest | 本端已关闭该 host 的白板:调用了 stopWhiteboard({ userId }),或切换到了其他 host(userId 为原来的 host) |
收起该 host 的白板入口即可,不要提示「已结束」:会话本身可能仍在进行 |
'silence' |
guest | 等待 host 响应超时,通常是 host 尚未开启白板 | 置灰入口,可提示「等待主讲人开启白板」。插件会继续等待,host 开启后收到 available,无需重新调用 |
'host-lost' |
guest | 与 host 之间的连接中断(本端或 host 断网、host 无响应)。会话未结束,已有笔迹保留 | 暂停书写,提示「连接中断,正在重连」。连接恢复后收到 available(reason 为 'resume');长时间未恢复会收到 'end' |
'queue-full' / 'ack-timeout' |
guest | 笔迹同步繁忙或确认超时,暂时不能书写 | 提示「同步中,请稍候」,无需处理,恢复后收到 available(reason 为 'resume') |
- host 端只会收到
'end'。- 同一个会话可能先后收到多次
unavailable(例如先'host-lost'再'end'),请以最近一次为准。
trtc.on(TRTC.EVENT.WHITEBOARD_STATE_CHANGED, ({ userId, type, payload }) => {
if (type !== 'unavailable') return;
disableWhiteboardEntry(userId);
switch (payload.reason) {
case 'end':
showToast('主讲人已关闭白板');
break;
case 'local-stop':
// 本端主动关闭白板 / 切换 host,不提示
break;
case 'silence':
showBanner('等待主讲人开启白板');
break;
case 'host-lost':
showBanner('连接中断,正在重连');
break;
case 'queue-full':
case 'ack-timeout':
showBanner('同步中,请稍候');
break;
}
});
error:白板运行异常
运行过程中发生的异步异常,payload 有两种形式:
| 形式 | 含义 | 处理建议 |
|---|---|---|
Error 对象 |
白板运行失败 | 上报日志,按需提示用户 |
带 reason 字段的对象 |
白板中的 PDF / PPT / 图片 / 视频加载失败。白板仍可正常使用,只是该内容没有显示 | 按 reason 提示用户,例如「文件已损坏」「网络异常,请重试」 |
内容加载失败时 payload 的字段:
| 字段 | 说明 |
|---|---|
| reason | 失败原因:'invalid-url' 链接不合法;'file-too-large' 文件过大;'corrupt' 文件损坏或无法解码;'unsupported' 格式不支持,或未配置 assetsPath;'network' 资源获取失败(请求失败、跨域被拒、断网);'unknown' 其他原因 |
| contentType | 内容类型:'pdf'、'ppt'、'image'、'video' |
| message | 原始错误信息(英文),用于排查问题 |
接口调用失败(如
startWhiteboard被 reject)通过try/catch处理,见下文「错误处理」。
const CONTENT_ERROR_TEXT = {
'invalid-url': '文件链接不合法',
'file-too-large': '文件过大',
corrupt: '文件已损坏',
unsupported: '不支持该文件格式',
network: '网络异常,请重试',
unknown: '文件加载失败'
};
trtc.on(TRTC.EVENT.WHITEBOARD_STATE_CHANGED, ({ userId, type, payload }) => {
if (type !== 'error') return;
if (payload instanceof Error) {
console.error('[whiteboard] error', payload.message);
reportError(payload);
return;
}
// PDF / PPT / 图片 / 视频加载失败
showToast(CONTENT_ERROR_TEXT[payload.reason] || CONTENT_ERROR_TEXT.unknown);
console.warn('[whiteboard] content load failed', payload.contentType, payload.message);
});
错误处理
白板相关的错误 code 为 TRTC.ERROR_CODE.INVALID_OPERATION(5100),通过 extraCode 区分:
| extraCode | 含义 |
|---|---|
| 5110 | 白板异常,具体原因见 error.message,例如插件未注册、屏幕分享正在运行、远端辅流未订阅、对端未开启白板、等待对端超时 |
| 5107 | audience 角色不能调用 |
| 5108 | 未开启 enableSEI |
try {
await trtc.startWhiteboard({ userId: 'host_user_id' });
} catch (error) {
if (error.code === TRTC.ERROR_CODE.INVALID_OPERATION && error.extraCode === 5110) {
console.warn(error.message);
}
}
常见问题
1. 白板工具栏中没有 PDF / PPT 入口?
请检查是否已按上文「部署静态资源」完成部署,并在 TRTC.create 中传入 assetsPath。
2. 开启白板时报错「屏幕共享正在运行」?
白板与屏幕分享互斥,请先调用 stopScreenShare() 再开启白板。
白板与屏幕分享共用同一条辅流。前一个
startScreenShare()/startWhiteboard()还没有返回(例如选屏弹窗尚未关闭)时就调用另一个,报的是OPERATION_ABORT(already start),而不是INVALID_OPERATION(5110)。请等前一个调用返回(成功或失败)后再切换,或在调用期间禁用切换入口。
3. 本地预览中 PDF / 图片位置偏移,远端画面正常?
请不要用后代选择器给容器内的 canvas / video 设置尺寸(尤其带 !important),这会命中插件内部的画布。推荐只约束容器自身这一层:
#local-board > .trtc-wb-video-layer video {
width: 100%;
height: 100%;
}
4. 容器尺寸变化后需要调用 API 吗?
不需要。容器自身改变尺寸,或父容器尺寸变化传递到容器(百分比 / flex 布局),白板及其中的 PDF / PPT 会自动重新布局。更换容器时请使用 updateWhiteboard({ view })(host)或 updateRemoteVideo({ view })(guest)。
不建议给容器或其祖先元素设置
transform: scale(),否则窗口、PDF、文字等内容会与白板错位。需要调整显示大小时,请直接修改容器的width/height。