TRTC Web SDKAPI 文档事件错误码类型教程更新日志
En

Tutorial: 屏幕分享标注

屏幕分享标注

功能描述

本文介绍如何在屏幕分享的画面上实现实时标注:分享方(下文称 host)开启屏幕分享后,host 与观看方(下文称 guest)可以在共享画面上一起写画,远端看到的是「屏幕 + 标注」合成后的画面。

角色 能力
host(分享方) 开启屏幕分享,按需开启 / 关闭自己的标注交互
guest(观看方) 订阅 host 的屏幕分享,开启 host 的标注,一起写画
  • 不传 userId 表示本端是 host,传 userId 表示本端是 guest,开启该用户的标注。
  • 屏幕分享标注与 互动白板 互斥,同一时刻只能开启其中一个。
  • 体验地址:RoomKit

核心优势

常规的标注方案通常需要单独接入一套标注 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' });