功能描述
本文介绍如何在屏幕分享的画面上实现实时标注:分享方(下文称 host)开启屏幕分享后,host 与观看方(下文称 guest)可以在共享画面上一起写画,远端看到的是「屏幕 + 标注」合成后的画面。
| 角色 | 能力 |
|---|---|
| host(分享方) | 开启屏幕分享,按需开启 / 关闭自己的标注交互 |
| guest(观看方) | 订阅 host 的屏幕分享,开启 host 的标注,一起写画 |
核心优势
常规的标注方案通常需要单独接入一套标注 SDK 与服务:笔迹通过独立的信令通道同步,与音视频是两条链路,观看端、录制、回放都要额外适配。TRTC 屏幕分享标注在 host 端把标注直接合成进屏幕分享画面,推给远端的就是一路普通的视频流(辅流),因此:
| 对比项 | 常规方案 | TRTC 屏幕分享标注 |
|---|---|---|
| 服务开通 | 需要额外开通标注 / 白板服务,维护独立的房间与鉴权 | 复用 TRTC 房间与音视频通道,无需额外服务 |
| 接入成本 | 集成独立 SDK,自行实现工具栏与状态同步 | 注册插件即可,开启屏幕分享时自动带上标注,内置工具栏开箱即用 |
| 观看端 | 每个观看端都要集成标注 SDK | 不支持标注能力的端(只拉流的观众、小程序、原生端)也能看到标注 |
| 音画同步 | 笔迹与音视频分两路传输,需要按时间戳对齐 | 标注与屏幕在同一帧里合成,再与音频一起推流,天然同步 |
| 录制 | 需要专门的标注录制服务,或分别录制音视频与笔迹数据 | 直接复用 TRTC 云端录制,录制文件就包含完整的「屏幕 + 标注」画面 |
| 旁路推流 | 需要额外合成后才能转推 | 直接复用云端混流与转推 CDN,CDN 观众同样看到标注 |
| 回放 | 需要专用播放器还原笔迹,并与音视频对齐 | 标准音视频文件,任意播放器、网页 <video>、点播服务都能直接播放 |
| 显示一致性 | 各端分别渲染,受设备、浏览器、分辨率影响 | 实时观看、录制、旁路、回放画面完全一致 |
| 服务端成本 | 额外的标注服务、录制转码费用 | 无额外服务,沿用现有的录制、混流与点播计费 |
前提条件
-
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)。
实现流程
引入并注册插件
标注依赖 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
});
- 未注册
VideoMixer/Whiteboard时,startScreenShare按普通屏幕分享处理,不受任何影响;显式调用标注 API 会抛出错误。AudioPlayer可选:用于播放窗口内视频的声音,未注册时窗口视频无声,不影响标注。
host:开启屏幕分享
注册了插件并开启 enableSEI 后,startScreenShare 会自动开启标注,推给远端的是「屏幕 + 标注」的合成画面。此时 host 本端为只读:不显示工具栏、不能写画,guest 开启标注后可以写画。
await trtc.startScreenShare({
view: 'local-screen',
option: {
profile: '1080p_2', // 建议显式设置
fillMode: 'contain' // 推荐 contain / cover
}
});
host:开启 / 关闭本端标注
// 开启本端标注:显示工具栏,可以写画
await trtc.startAnnotation();
// 关闭本端标注:隐藏工具栏,不能写画。标注仍在合成画面里,guest 照常可以写画
await trtc.stopAnnotation();
stopAnnotation()只关闭 host 自己的交互,不会结束标注会话。结束标注请调用stopScreenShare()。
guest:开启标注
guest 需要先订阅 host 的屏幕分享(辅流),再开启标注。能否开启成功取决于对端状态,请使用 try/catch 处理。
await trtc.startRemoteVideo({
userId: 'host_user_id',
streamType: TRTC.TYPE.STREAM_TYPE_SUB,
view: 'remote-screen'
});
try {
await trtc.startAnnotation({ userId: 'host_user_id' });
} catch (error) {
// 对端未开启屏幕分享 / 未订阅该用户辅流 / 等待超时等
console.warn(error.message);
}
- 必须先
startRemoteVideo订阅辅流,再调用startAnnotation({ userId }),顺序颠倒会抛出错误。- host 与 guest 谁先开启都可以,SDK 会等待对端就绪(最长 10s)。
- guest 同一时刻只能开启一个 host 的标注,传入新的
userId即切换 host。
按需更新参数
// host:切换主题
await trtc.updateAnnotation({ option: { theme: 'dark' } });
// guest:只读模式(只看不画,不关闭标注)
await trtc.updateAnnotation({ userId: 'host_user_id', option: { interactive: false } });
// host:更换预览容器,标注随视频画面一起迁移
await trtc.updateScreenShare({ view: 'another-view' });
// guest:更换播放容器,标注随视频画面一起迁移
await trtc.updateRemoteVideo({ userId: 'host_user_id', streamType: TRTC.TYPE.STREAM_TYPE_SUB, view: 'another-view' });
结束标注
// guest:关闭标注
await trtc.stopAnnotation({ userId: 'host_user_id' });
// host:停止屏幕分享,标注随之结束
await trtc.stopScreenShare();
退房与切房
调用 exitRoom() 会结束标注会话,但屏幕分享按 SDK 既有行为保留:本地预览会播回业务的 view,重新进房后自动重推的是原始屏幕画面(不带标注)。switchRoom() 回退到「退房 + 进房」时同样如此。
因此,退房后仍要继续标注,需要在重新进房后由 host 再次调用 startAnnotation()、guest 再次调用 startAnnotation({ userId });此前跟随该 host 的 guest 也需要重新发起跟随。
退房时屏幕采集仍在继续,不需要重新调用
startScreenShare();只有stopScreenShare()才会真正停掉它。
API 说明
trtc.startAnnotation(options)
- host:开启本端标注交互,显示工具栏(重复调用无副作用)。
- guest:开启指定用户的标注。
options
| 参数名 | 类型 | Attributes | Description |
|---|---|---|---|
| userId | string |
guest 必填 | 目标 host userId。不传表示本端为 host |
| option.theme | string |
选填 | 内置工具栏配色:'light'(默认)、'dark',只影响本端 |
Example:
// host
await trtc.startAnnotation({ option: { theme: 'dark' } });
// guest
await trtc.startAnnotation({ userId: 'host_user_id' });
trtc.updateAnnotation(options)
增量更新标注参数,只修改传入的字段。
options
| 参数名 | 类型 | Attributes | Description |
|---|---|---|---|
| userId | string |
guest 必填 | 目标 host userId。不传表示本端为 host |
| option.theme | string |
选填 | 内置工具栏配色:'light'、'dark' |
| option.interactive | boolean |
选填 | 本端能否写画。host 传 false / true 等同于 stopAnnotation() / startAnnotation();guest 传 false 为只读模式 |
trtc.stopAnnotation(options)
- host:关闭本端标注交互,隐藏工具栏。标注仍在合成画面中,guest 照常可以写画。
- guest:关闭标注。
options
| 参数名 | 类型 | Attributes | Description |
|---|---|---|---|
| userId | string |
guest 必填 | 目标 host userId。不传表示本端为 host |
事件
标注状态变化通过 TRTC.EVENT.ANNOTATION_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.ANNOTATION_STATE_CHANGED, ({ userId, type, payload }) => {
if (type !== 'available') return;
// 显示 / 点亮标注入口
showAnnotationEntry(userId);
if (payload.reason === 'resume') {
hideBanner(); // 收起「连接中断」等提示
}
});
unavailable:标注不可用
收到后应收起 / 置灰标注相关的入口。是否需要提示用户、是否需要重新调用接口,取决于 payload.reason:
| reason | 角色 | 含义 | 处理建议 |
|---|---|---|---|
'end' |
host / guest | 标注会话结束:host 调用了 stopScreenShare() |
收起标注入口;guest 可提示「主讲人已结束标注」。guest 无需关闭标注,host 再次开启时会重新收到 available |
'local-stop' |
guest | 本端已关闭该 host 的标注:调用了 stopAnnotation({ 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.ANNOTATION_STATE_CHANGED, ({ userId, type, payload }) => {
if (type !== 'unavailable') return;
disableAnnotationEntry(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 对象(message 为失败原因)。host 调用 startScreenShare 时自动开启标注失败也会发这个事件,包括缺插件、VideoMixer 启动失败、白板插件启动失败;此时屏幕分享仍正常推流,只是没有标注,需要标注时可提示用户重新开启屏幕分享。
接口调用失败(如
startAnnotation被 reject)通过try/catch处理,见下文「错误处理」。
trtc.on(TRTC.EVENT.ANNOTATION_STATE_CHANGED, ({ userId, type, payload }) => {
if (type !== 'error') return;
console.error('[annotation] error', payload.message);
reportError(payload);
});
错误处理
标注相关的错误 code 为 TRTC.ERROR_CODE.INVALID_OPERATION(5100),通过 extraCode 区分:
| extraCode | 含义 |
|---|---|
| 5110 | 标注异常,具体原因见 error.message,例如插件未注册、未开启屏幕分享、远端辅流未订阅、对端未开启标注、等待对端超时 |
| 5107 | audience 角色不能调用 |
| 5108 | 未开启 enableSEI |
try {
await trtc.startAnnotation({ userId: 'host_user_id' });
} catch (error) {
if (error.code === TRTC.ERROR_CODE.INVALID_OPERATION && error.extraCode === 5110) {
console.warn(error.message);
}
}
常见问题
1. host 开启屏幕分享后无法写画?
startScreenShare 自动开启的标注在 host 本端为只读,需要调用 trtc.startAnnotation() 开启本端交互。
2. 开启屏幕分享后远端看不到标注内容?
请检查 TRTC.create 是否开启了 enableSEI: true。未开启时屏幕分享照常推流,但标注内容被隐藏。
3. 本地预览中标注位置偏移,远端画面正常?
请不要用后代选择器给容器内的 canvas / video 设置尺寸(尤其带 !important),这会命中插件内部的画布。推荐只约束容器自身这一层:
#local-screen > .trtc-wb-video-layer video {
width: 100%;
height: 100%;
/* 不写 object-fit,由 fillMode 决定 */
}
4. 容器尺寸变化后需要调用 API 吗?
不需要。容器自身改变尺寸,或父容器尺寸变化传递到容器(百分比 / flex 布局),标注会自动重新布局。更换容器时请使用 updateScreenShare({ view })(host)或 updateRemoteVideo({ view })(guest)。
不建议给容器或其祖先元素设置
transform: scale(),否则窗口、文字等内容会与标注错位。需要调整显示大小时,请直接修改容器的width/height。
5. 分享过程中被分享窗口改变了尺寸?
合成画面保持开始分享时的尺寸和比例,屏幕画面按 fillMode 放入其中:contain 留边、cover 裁切。已有笔迹相对画面位置不变,但可能与窗口内容错开,可由 host 点击工具栏的清屏按钮清除。如需重新铺满,请停止分享后重新开始。
6. 在白板 / 屏幕分享之间切换时报 OPERATION_ABORT(already start),而不是 5110?
屏幕分享与 互动白板 共用同一条辅流。前一个 startScreenShare() / startWhiteboard() 还没有返回(例如选屏弹窗尚未关闭)时就调用另一个,会报 OPERATION_ABORT;前一个已启动完成后再调用,才会报 INVALID_OPERATION(5110)。请等前一个调用返回(成功或失败)后再切换,或在调用期间禁用切换入口:
await trtc.startScreenShare(); // 等待返回后再切换
await trtc.stopScreenShare();
await trtc.startWhiteboard({ view: 'local-board' });