Feature Description
This article describes how to use the interactive whiteboard during a call. The initiator (the host) opens a whiteboard and publishes it to remote users without screen sharing. Viewers (the guests) open the host's whiteboard and write and draw together. The whiteboard supports inserting PDF, PPT, images, videos and more.
| Role | Capability |
|---|---|
| host (initiator) | Starts the whiteboard (publishes it via the sub stream), inserts PDF / PPT / images / videos, switches to read-only as needed |
| guest (viewer) | Subscribes to the host's whiteboard (sub stream), opens the host's whiteboard and writes together |
- Omitting
userIdmeans the local user is the host. PassinguserIdmeans the local user is a guest opening that user's whiteboard. - The interactive whiteboard and Screen Share Annotation are mutually exclusive. Only one of them can run at a time.
- Demo RoomKit
Core Advantages
Conventional interactive whiteboard solutions usually require a separate whiteboard SDK and service: whiteboard content is synchronized over an independent signaling channel and rendered separately on each endpoint, separate from the audio/video path, so viewing endpoints, recording and playback all need extra adaptation. TRTC Interactive Whiteboard renders the whiteboard directly into a video picture on the host side, and what is published to remote users is an ordinary video stream (the sub stream). As a result:
| Item | Conventional solution | TRTC Interactive Whiteboard |
|---|---|---|
| Service activation | Requires an extra whiteboard service, with separate rooms and authentication to maintain | Reuses the TRTC room and audio/video channel, no extra service needed |
| Integration cost | Integrate a separate SDK and implement the toolbar, courseware transcoding and state synchronization yourself | Register the plugins and call a few APIs; the built-in toolbar works out of the box, and PDF / PPT files are loaded locally |
| Viewing endpoints | Every viewing endpoint must integrate the whiteboard SDK | Endpoints without whiteboard support (pull-only viewers, mini programs, native apps) also see the whiteboard |
| Audio/video sync | Whiteboard data and audio/video are transmitted separately and must be aligned by timestamp | The whiteboard is rendered into the same frame and published together with the audio, naturally in sync |
| Recording | Requires a dedicated whiteboard recording service, or recording audio/video and whiteboard data separately | Reuses TRTC Cloud Recording directly; the recording contains the complete whiteboard picture (strokes, PDF / PPT page turns, images, videos) |
| Relayed push | Requires extra compositing before relaying | Reuses cloud mixing and CDN relay directly; CDN viewers also see the whiteboard |
| Playback | Requires a dedicated player to restore the whiteboard, reload courseware and align it with audio/video | Standard audio/video files that play in any player, an HTML <video> element or a VOD service, without depending on the original courseware files |
| Display consistency | Each endpoint renders separately; PDF / PPT layout is affected by fonts, browser and resolution | Live view, recording, relayed stream and playback look exactly the same |
| Server cost | Extra fees for the whiteboard service, courseware transcoding and recording transcoding | No extra service; existing recording, mixing and VOD billing applies |
Prerequisites
-
TRTC Web SDK version >= 5.21.0
-
Compatibility:
OS Browser Host (initiator) Guest (participant) Windows / macOS Chrome 86+, Edge 86+ and other Chromium 86+ based browsers ✅ ✅ macOS Safari 15.4+ ✅ ✅ Windows / macOS Firefox 117+ ✅ ✅ Android Chrome 86+, WeChat built-in browser and other Chromium 86+ based browsers ❌ Mixing is not supported on mobile yet ✅ iOS 15.4+ Safari, WeChat built-in browser, etc. ❌ Mixing is not supported on mobile yet ✅ Users who only pull streams and do not take part in the whiteboard are not limited by the table above: the whiteboard is rendered into the sub stream picture, so it is visible in any environment that supports TRTC stream pulling. See Browsers Supported.
-
The role must be anchor (the audience role cannot call whiteboard APIs).
Implementation
Deploy Static Assets
Inserting PDF / PPT depends on some static asset files. To make sure the browser can load them, complete the following steps:
- Publish the
node_modules/trtc-sdk-v5/assetsdirectory to your CDN or static asset server (the whiteboard assets are in itswhiteboardsubdirectory). - Pass your CDN address to the
assetsPathparameter when creating the trtc instance, e.g.TRTC.create({ assetsPath: 'https://xxxx/assets' }). The SDK loads the assets on demand.
- Without
assetsPath, the PDF button and PPT entry do not appear in the whiteboard. Other features are not affected.- If the host URL of the assets differs from that of your web app, enable CORS on the asset domain.
- Do not serve the assets over HTTP, because loading HTTP resources from an HTTPS page is blocked by the browser.
Import and Register Plugins
The whiteboard depends on VideoMixer (renders the whiteboard picture) and Whiteboard (the whiteboard plugin), and requires 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'; // optional, see below
const trtc = TRTC.create({
plugins: [VideoMixer, Whiteboard, AudioPlayer],
enableSEI: true,
assetsPath: 'https://xxxx/assets'
});
- Calling whiteboard APIs without
VideoMixer/Whiteboardregistered throws an error. Guests only needWhiteboard.AudioPlayeris optional. It plays the audio of videos inserted into the whiteboard. Without it, those videos are silent.
Host: Start the Whiteboard
await trtc.startWhiteboard({
view: 'local-board', // whiteboard container: element id or HTMLElement
option: {
profile: '1080p', // default: 1920×1080 / 15fps / 2000kbps
backgroundColor: '#fcfcfc', // optional; the theme background is used if omitted
theme: 'light' // optional
}
});
- The whiteboard occupies the sub stream, so screen sharing cannot be started while the whiteboard is running, and vice versa.
profiledetermines both the whiteboard canvas size and its frame rate.
Guest: Open the Whiteboard
A guest must subscribe to the host's whiteboard (sub stream) first, then open the whiteboard. Whether it succeeds depends on the remote side, so wrap it in 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) {
// The remote user hasn't started the whiteboard / the sub stream isn't subscribed / timed out, etc.
console.warn(error.message);
}
- You must subscribe to the sub stream with
startRemoteVideobefore callingstartWhiteboard({ userId }). The reverse order throws an error.- Either the host or the guest can start first. The SDK waits for the remote side to be ready (up to 10s).
- A guest can open the whiteboard for only one host at a time. Passing a new
userIdswitches the target host.
Update Parameters as Needed
// Host: change the background color and theme
await trtc.updateWhiteboard({ option: { backgroundColor: '#ffffff', theme: 'dark' } });
// Host: read-only locally; guests can still write
await trtc.updateWhiteboard({ option: { interactive: false } });
// Host: switch the whiteboard container
await trtc.updateWhiteboard({ view: 'small-board' });
// Guest: open in read-only mode (watch without writing, keep the whiteboard open)
await trtc.updateWhiteboard({ userId: 'host_user_id', option: { interactive: false } });
// Guest: switch the playback container; the whiteboard moves with the video
await trtc.updateRemoteVideo({ userId: 'host_user_id', streamType: TRTC.TYPE.STREAM_TYPE_SUB, view: 'another-view' });
Stop the Whiteboard
// Guest: close the whiteboard
await trtc.stopWhiteboard({ userId: 'host_user_id' });
// Host: stop the whiteboard (stops publishing and releases the whiteboard)
await trtc.stopWhiteboard();
API Description
trtc.startWhiteboard(options)
- Host: starts the whiteboard and publishes it via the sub stream.
- Guest: opens the specified user's whiteboard.
options
| Name | Type | Attributes | Description |
|---|---|---|---|
| userId | string |
Required for guests | The host userId whose whiteboard to open. Omit it for the host |
| view | string | HTMLElement | null |
Required for the host | Whiteboard container: element id or HTMLElement. Pass null if no local preview is needed. Ignored for guests |
| option.profile | string | object |
Optional, host only | Encoding parameters of the whiteboard sub stream. Default '1080p' (1920×1080 / 15fps / 2000kbps). Accepts strings such as '720p', or { width, height, frameRate, bitrate } |
| option.backgroundColor | string |
Optional, host only | Whiteboard background (CSS color), which is also what remote users see. Defaults to the theme background |
| option.theme | string |
Optional | Built-in toolbar theme: 'light' (default) or 'dark'. Affects the local side only |
Example:
// host
await trtc.startWhiteboard({ view: 'local-board', option: { profile: '720p' } });
// guest
await trtc.startWhiteboard({ userId: 'host_user_id' });
trtc.updateWhiteboard(options)
Updates whiteboard parameters incrementally. Only the fields passed in are changed.
options
| Name | Type | Attributes | Description |
|---|---|---|---|
| userId | string |
Required for guests | The host userId whose whiteboard to open. Omit it for the host |
| view | string | HTMLElement | null |
Optional, host only | Switches the whiteboard container; null means no preview. If the same container is passed and its size has changed, the whiteboard re-lays out to the new size |
| option.backgroundColor | string |
Optional, host only | Whiteboard background (CSS color) |
| option.theme | string |
Optional | Built-in toolbar theme: 'light' or 'dark' |
| option.interactive | boolean |
Optional | Whether the local user can write. It is true when the whiteboard starts and keeps its current value if omitted. When false, interactive UI such as the toolbar and page bar is hidden |
trtc.stopWhiteboard(options)
- Host: stops the whiteboard, stops publishing and releases it.
- Guest: closes the whiteboard.
options
| Name | Type | Attributes | Description |
|---|---|---|---|
| userId | string |
Required for guests | The host userId whose whiteboard to open. Omit it for the host |
Events
Whiteboard state changes are notified via TRTC.EVENT.WHITEBOARD_STATE_CHANGED. The callback receives { userId, type, payload }:
| Field | Description |
|---|---|
| userId | An empty string on the host side; the host userId whose whiteboard is open on the guest side |
| type | 'available': the whiteboard is available; 'unavailable': the whiteboard is unavailable; 'error': a whiteboard runtime error |
| payload | Event details, see below |
Handle only the three
typevalues above and ignore any others.
available: Whiteboard Is Available
Show / enable your whiteboard entry when you receive it. Values of payload.reason:
| reason | Meaning |
|---|---|
'started' |
The whiteboard session is established: the whiteboard started successfully on the host; the guest completed the handshake with the host |
'resume' |
Guest only: the whiteboard recovered from being temporarily unavailable (network interruption, busy syncing) |
trtc.on(TRTC.EVENT.WHITEBOARD_STATE_CHANGED, ({ userId, type, payload }) => {
if (type !== 'available') return;
// Show / enable the whiteboard entry
showWhiteboardEntry(userId);
if (payload.reason === 'resume') {
hideBanner(); // dismiss notices such as "Connection lost"
}
});
unavailable: Whiteboard Is Unavailable
Hide / disable your whiteboard entry when you receive it. Whether to notify the user or call an API again depends on payload.reason:
| reason | Role | Meaning | Recommended handling |
|---|---|---|---|
'end' |
host / guest | The whiteboard session ended: the host called stopWhiteboard() |
Hide the whiteboard entry; a guest can show "The presenter closed the whiteboard". The guest does not need to close the whiteboard, and receives available again when the host restarts |
'local-stop' |
guest | The local user closed this host's whiteboard: stopWhiteboard({ userId }) was called, or the whiteboard was opened for another host (userId is the previous host) |
Just hide that host's whiteboard entry. Do not say "ended": the session may still be running |
'silence' |
guest | Timed out waiting for the host, usually because the host has not started the whiteboard yet | Disable the entry and optionally show "Waiting for the presenter to open the whiteboard". The plugin keeps waiting and you receive available once the host starts; no need to call again |
'host-lost' |
guest | The connection to the host is lost (local or host network down, host unresponsive). The session has not ended and existing strokes are kept | Pause writing and show "Connection lost, reconnecting". You receive available (reason is 'resume') when the connection recovers; if it does not recover for a long time, you receive 'end' |
'queue-full' / 'ack-timeout' |
guest | Stroke syncing is busy or acknowledgements timed out; writing is temporarily unavailable | Show "Syncing, please wait". No action needed; you receive available (reason is 'resume') after recovery |
- The host only receives
'end'.- A session may receive
unavailableseveral times (e.g.'host-lost'and then'end'). Use the most recent one.
trtc.on(TRTC.EVENT.WHITEBOARD_STATE_CHANGED, ({ userId, type, payload }) => {
if (type !== 'unavailable') return;
disableWhiteboardEntry(userId);
switch (payload.reason) {
case 'end':
showToast('The presenter closed the whiteboard');
break;
case 'local-stop':
// The local user closed the whiteboard / switched hosts; no notice needed
break;
case 'silence':
showBanner('Waiting for the presenter to open the whiteboard');
break;
case 'host-lost':
showBanner('Connection lost, reconnecting');
break;
case 'queue-full':
case 'ack-timeout':
showBanner('Syncing, please wait');
break;
}
});
error: Whiteboard Runtime Error
An asynchronous error during runtime. payload takes one of two forms:
| Form | Meaning | Recommended handling |
|---|---|---|
An Error object |
The whiteboard failed at runtime | Report it to your logs and notify the user if needed |
An object with a reason field |
A PDF / PPT / image / video in the whiteboard failed to load. The whiteboard still works; only that content is not shown | Notify the user according to reason, e.g. "The file is corrupted" or "Network error, please retry" |
Fields of payload when content fails to load:
| Field | Description |
|---|---|
| reason | Failure reason: 'invalid-url' invalid link; 'file-too-large' file too large; 'corrupt' file corrupted or cannot be decoded; 'unsupported' format not supported, or assetsPath not configured; 'network' failed to fetch the resource (request failed, blocked by CORS, offline); 'unknown' other reasons |
| contentType | Content type: 'pdf', 'ppt', 'image', 'video' |
| message | The original error message (English) for troubleshooting |
Failed API calls (e.g.
startWhiteboardrejected) are handled withtry/catch; see "Error Handling" below.
const CONTENT_ERROR_TEXT = {
'invalid-url': 'Invalid file link',
'file-too-large': 'The file is too large',
corrupt: 'The file is corrupted',
unsupported: 'Unsupported file format',
network: 'Network error, please retry',
unknown: 'Failed to load the file'
};
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;
}
// A PDF / PPT / image / video failed to load
showToast(CONTENT_ERROR_TEXT[payload.reason] || CONTENT_ERROR_TEXT.unknown);
console.warn('[whiteboard] content load failed', payload.contentType, payload.message);
});
Error Handling
The error code for the whiteboard is TRTC.ERROR_CODE.INVALID_OPERATION (5100), distinguished by extraCode:
| extraCode | Meaning |
|---|---|
| 5110 | Whiteboard error. See error.message for the reason, e.g. plugins not registered, screen sharing is running, sub stream not subscribed, the remote user hasn't started the whiteboard, timed out waiting for the remote user |
| 5107 | Cannot be called in the audience role |
| 5108 | enableSEI is not enabled |
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);
}
}
FAQ
1. There is no PDF / PPT entry in the whiteboard toolbar?
Check that you have deployed the static assets as described in "Deploy Static Assets" above and passed assetsPath to TRTC.create.
2. Starting the whiteboard fails because screen sharing is running?
The whiteboard and screen sharing are mutually exclusive. Call stopScreenShare() before starting the whiteboard.
The whiteboard and screen sharing share the same sub stream. If you call one while the previous
startScreenShare()/startWhiteboard()has not returned yet (for example, the screen picker is still open), the call fails withOPERATION_ABORT(already start) instead ofINVALID_OPERATION(5110). Wait for the previous call to return (whether it succeeds or fails) before switching, or disable the switch entry while it is in progress.
3. PDF / images are misaligned in the local preview but correct for remote users?
Do not size canvas / video inside the container with descendant selectors (especially with !important), because they also match the plugin's internal canvases. Constrain only the container's own layer:
#local-board > .trtc-wb-video-layer video {
width: 100%;
height: 100%;
}
4. Do I need to call an API after the container size changes?
No. When the container itself is resized, or a parent's size change propagates to the container (percentage / flex layout), the whiteboard and the PDF / PPT in it re-lay out automatically. To switch containers, use updateWhiteboard({ view }) (host) or updateRemoteVideo({ view }) (guest).
Applying
transform: scale()to the container or its ancestors is not recommended, as windows, PDF, text and other content will be misaligned with the whiteboard. To change the display size, change the container'swidth/heightdirectly.