Integrate a whiteboard

更新时间:
复制 MD 格式

Quickly integrate the CommsEase Whiteboard SDK and publish a whiteboard stream.

Activate the whiteboard service

You can activate the CommsEase whiteboard service in two ways:

  1. Activate the Interactive Whiteboard service on the CommsEase official website.

  2. If your Alibaba Cloud Real-Time Communication (ARTC) spending is expected to exceed 50,000 CNY per month, you can submit a ticket or send an email to request that Alibaba Cloud purchase the CommsEase Interactive Whiteboard service on your behalf. You pay the whiteboard fees directly to Alibaba Cloud. For more information about how to submit a ticket, see Contact us.

Integrate the whiteboard SDK

Import SDK files

Download and decompress the SDK zip package. Copy the WhiteBoardSDK.js, ToolCollection.js, and pptRenderer.js files from the g2 folder to your project's static files folder (usually the `public` folder) or publish them to your CDN. Then, import these three JS files into your entry HTML file using <script> tags.

Get the Interactive Whiteboard AppKey and AppSecret

  1. On the homepage of the CommsEase console, find your application in the Application Management section and click the Application Name.

  2. In the Application Configuration navigation bar, click the AppKey Management tab.

  3. View and record the AppKey and AppSecret for the application.

For more information about Interactive Whiteboard configurations, see the Interactive Whiteboard Quick Start Guide for Novices.

If Alibaba Cloud purchases the service on your behalf, Alibaba Cloud provides you with the AppKey and AppSecret after the purchase is complete.

Develop the getAuthInfo interface

Add the whiteboard SDK's appkey and appSecret to your server-side configuration parameters. Develop a getAuthInfo interface that performs SHA-1 encryption on data, such as appSecret and curTime, and returns the relevant data to the frontend.

Important

The local example performs encryption on the web page for demonstration purposes only. Do not expose your appSecret in a production environment.

Initialization

Add a DIV element in your HTML code to serve as the whiteboard container.

<div id="whiteboard"></div>

Create a whiteboard SDK instance and join a channel with the `joinRoom` interface.

const appKey = 'Your AppKey'; // You can get this from AppKey Management for your application in the CommsEase console.
const appSecret = 'Your AppSecret';
const nickname = 'Your Nickname';
const uid = 123123;  // A positive integer less than Number.MAX_SAFE_INTEGER. If the same UID logs on from multiple locations, previous sessions will be disconnected. If you need multi-device synchronization, use different UIDs for each device.

/**
* This function returns the authentication information required by the Interactive Whiteboard application.
* The Interactive Whiteboard SDK calls this function when needed. The function passes the auth info to the SDK through a promise.
*
* The following code is for demonstration only. In actual development, do not store the appSecret on the client. This could lead to the appSecret being stolen. In actual development, you can use getAuthInfo to request the auth message from your server and then return the auth info to the SDK in the promise.
*/
function getAuthInfo() {
  const Nonce = 'xxxx';   // A random string of any length less than 128 characters.
  const curTime = Math.round((Date.now() / 1000)); // The current UTC timestamp in seconds since January 1, 1970, 00:00:00.
  const checksum = sha1(appSecret + Nonce + curTime);
  return Promise.resolve({
    nonce: Nonce,
    checksum: checksum,
    curTime: curTime,
  });
}

const sdk = WhiteBoardSDK.getInstance({
  appKey: appKey,
  nickname: nickname,     // Optional
  uid: uid,
  container: document.getElementById('whiteboard'),
  platform: 'web',
  record: false,   // Specifies whether to enable recording.
  getAuthInfo: getAuthInfo,
});

// A string of your choice for the channel. Different clients must join the same channel to communicate.
const channel = '821937123'
sdk.joinRoom({
  channel: channel,
  createRoom: true
})
.then((drawPlugin) => {
  // Allow editing.
  drawPlugin.enableDraw(true)
  // Set the brush color.
  drawPlugin.setColor('rgb(243,0,0)')

  // Initialize the toolbar.
  const toolCollection = ToolCollection.getInstance({
    /**
    * The toolbar container. It should be the same as the whiteboard container.
    *
    * Note that the child elements inside the toolbar are absolutely positioned. Therefore, the container outside the toolbar should have its position set to relative, absolute, or fixed.
    * This ensures that the toolbar is displayed correctly inside the container.
    */
    container: document.getElementById('whiteboard'),
    handler: drawPlugin,
    options: {
      platform: 'web',
    }
  });
  
  // Show the toolbar.
  toolCollection.show();
});

Load documents

The whiteboard SDK supports ppt, pptx, doc, docx, and pdf files. Use the file upload control in the left sidebar to manage your file list.

File list persistence

The whiteboard SDK stores file lists in the browser's LocalStorage. Lists persist when you reopen the page but not across devices or browsers. To enable cross-device persistence, implement server-side interfaces that listen for whiteboard SDK events such as docAdd and docDelete. During initialization, call setDefaultDocList to restore the file list. For more information, see the Whiteboard SDK documentation.

Notes

The whiteboard SDK converts only the whiteboard canvas content into a video stream. Video or audio files added to the canvas cannot be shared through the whiteboard stream.

Publish the whiteboard stream

After the whiteboard SDK joins a channel, call the getStream interface to retrieve the video stream and publish it through the custom input feature.

// The drawPlugin object is returned after the whiteboard SDK instance successfully joins the channel.
const mediaStream = drawPlugin.getStream({
  width: 720,
  frameRate: 15,
});
const videoTrack = mediaStream.getVideoTracks()[0];
if (videoTrack) {
  try {
    await aliRtcEngine.startScreenShare({
      videoTrack, // Pass the custom video track.
    });
    console.log('Whiteboard sharing started successfully.');
  } catch (error) {
    console.log('Failed to share the whiteboard.');
  }
} else {
  console.log('No whiteboard videoTrack available.');
}

Integration example

Prerequisites

This example extends the quick start example in the Quick Start document. Complete that example before proceeding.

Step 1: Create files

In the demo folder, create two new files: whiteboard.html and whiteboard.js. Place the three whiteboard SDK JS files, WhiteBoardSDK.js, ToolCollection.js, and pptRenderer.js, in the same folder. The directory structure is as follows:

- demo
  - quick.html
  - quick.js
  - whiteboard.html
  - whiteboard.js
  - WhiteBoardSDK.js
  - pptRenderer.js
  - ToolCollection.js

Step 2: Edit whiteboard.html

Copy and paste the code below into whiteboard.html and save the file.

Expand to view whiteboard.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>aliyun-rtc-sdk whiteboard</title>
    <link rel="stylesheet" href="https://g.alicdn.com/code/lib/bootstrap/5.3.0/css/bootstrap.min.css" />
    <style>
      .video {
        display: inline-block;
        width: 320px;
        height: 180px;
        margin-right: 8px;
        margin-bottom: 8px;
        background-color: black;
      }
      .whiteboard {
        position: relative;
        width: 100%;
        height: 500px;
      }
    </style>
  </head>
  <body class="container">
    <h1 class="mt-2">aliyun-rtc-sdk Whiteboard Integration</h1>

    <div class="toast-container position-fixed top-0 end-0 p-3">
      <div id="loginToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
        <div class="toast-header">
          Logon Message
          <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
        </div>
        <div class="toast-body" id="loginToastBody"></div>
      </div>

      <div id="onlineToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
        <div class="toast-header">
          User Online
          <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
        </div>
        <div class="toast-body" id="onlineToastBody"></div>
      </div>

      <div id="offlineToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
        <div class="toast-header">
          User Offline
          <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
        </div>
        <div class="toast-body" id="offlineToastBody"></div>
      </div>

      <div id="screenToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
        <div class="toast-header">
          Screen/Whiteboard Message
          <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
        </div>
        <div class="toast-body" id="screenToastBody"></div>
      </div>
    </div>

    <div class="row mt-3">
      <div class="col-8">
        <form id="loginForm">
          <div class="form-group mb-2">
            <label for="channelId" class="form-label">Channel ID</label>
            <input class="form-control form-control-sm" id="channelId" />
          </div>
          <div class="form-group mb-2">
            <label for="userId" class="form-label">User ID</label>
            <input class="form-control form-control-sm" id="userId" />
          </div>
          <div class="mb-2">
            <button id="joinBtn" type="submit" class="btn btn-primary btn-sm">Join Channel</button>
            <button id="leaveBtn" type="button" class="btn btn-secondary btn-sm" disabled>Leave Channel</button>
            <button id="screenBtn" type="button" class="btn btn-secondary btn-sm" disabled>Share Screen</button>
            <button id="boardBtn" type="button" class="btn btn-secondary btn-sm" disabled>Share Whiteboard</button>
          </div>
        </form>
        <div id="whiteboard" class="whiteboard"></div>
      </div>
      <div class="col-4">
        <div>
          <h5>Local Preview</h5>
          <video
            id="localPreviewer"
            muted
            class="video"
          ></video>
        </div>
        <div>
          <h5>Remote Users</h5>
          <div id="remoteVideoContainer"></div>
        </div>
      </div>
    </div>

    <script src="https://g.alicdn.com/code/lib/jquery/3.7.1/jquery.min.js"></script>
    <script src="https://g.alicdn.com/code/lib/bootstrap/5.3.0/js/bootstrap.min.js"></script>
    <script src="https://g.alicdn.com/apsara-media-box/imp-web-rtc/6.11.1/aliyun-rtc-sdk.js"></script>
    <script src="./WhiteBoardSDK.js"></script>
    <script src="./pptRenderer.js"></script>
    <script src="./ToolCollection.js"></script>
    <script src="./whiteboard.js"></script>
  </body>
</html>

Step 3: Edit whiteboard.js

Copy and paste the code below into whiteboard.js, replace the placeholder aliyun-rtc-sdk Application ID and AppKey and the whiteboard SDK appKey and appsecret with your credentials, and save the file.

Expand to view whiteboard.js

function hex(buffer) {
  const hexCodes = [];
  const view = new DataView(buffer);
  for (let i = 0; i < view.byteLength; i += 4) {
    const value = view.getUint32(i);
    const stringValue = value.toString(16);
    const padding = '00000000';
    const paddedValue = (padding + stringValue).slice(-padding.length);
    hexCodes.push(paddedValue);
  }
  return hexCodes.join('');
}
async function generateToken(appId, appKey, channelId, userId, timestamp) {
  const encoder = new TextEncoder();
  const data = encoder.encode(`${appId}${appKey}${channelId}${userId}${timestamp}`);

  const hash = await crypto.subtle.digest('SHA-256', data);
  return hex(hash);
}

function showToast(baseId, message) {
  $(`#${baseId}Body`).text(message);
  const toast = new bootstrap.Toast($(`#${baseId}`));

  toast.show();
}

// Enter your Application ID and AppKey.
const appId = '';
const appKey = '';
AliRtcEngine.setLogLevel(0);
let aliRtcEngine;
const remoteVideoElMap = {};
const remoteVideoContainer = document.querySelector('#remoteVideoContainer');

function removeRemoteVideo(userId, type = 'camera') {
  const vid = `${type}_${userId}`;
  const el = remoteVideoElMap[vid];
  if (el) {
    aliRtcEngine.setRemoteViewConfig(null, userId, type === 'camera' ? 1: 2);
    el.pause();
    remoteVideoContainer.removeChild(el);
    delete remoteVideoElMap[vid];
  }
}

function listenEvents() {
  if (!aliRtcEngine) {
    return;
  }
  // Listen for remote users coming online.
  aliRtcEngine.on('remoteUserOnLineNotify', (userId, elapsed) => {
    console.log(`User ${userId} joined the channel in ${elapsed} seconds`);
    // Handle your business logic here, such as displaying this user's module.
    showToast('onlineToast', `User ${userId} is online`);
  });

  // Listen for remote users going offline.
  aliRtcEngine.on('remoteUserOffLineNotify', (userId, reason) => {
    // reason is the reason code. For details, see the API reference.
    console.log(`User ${userId} left the channel, reason code: ${reason}`);
    // Handle your business logic here, such as destroying this user's module.
    showToast('offlineToast', `User ${userId} is offline`);
    removeRemoteVideo(userId, 'camera');
    removeRemoteVideo(userId, 'screen');
  });

  aliRtcEngine.on('bye', code => {
    // code is the reason code. For details, see the API reference.
    console.log(`bye, code=${code}`);
    // Handle your business logic here, such as exiting the call page.
    showToast('loginToast', `You have left the channel, reason code: ${code}`);
  });

  aliRtcEngine.on('videoSubscribeStateChanged', (userId, oldState, newState, interval, channelId) => {
    // oldState and newState are of type AliRtcSubscribeState, with values 0 (initialized), 1 (unsubscribed), 2 (subscribing), 3 (subscribed).
    // interval is the time between the two states in milliseconds.
    console.log(`Channel ${channelId} remote user ${userId} subscription state changed from ${oldState} to ${newState}`);
    const vid = `camera_${userId}`;
    // Example handling
    if (newState === 3) {
      const video = document.createElement('video');
      video.autoplay = true;
      // video.setAttribute(
      //   'style',
      //   'display: inline-block;width: 320px;height: 180px;background-color: black;margin-right: 8px;margin-bottom: 8px;'
      // );
      video.className = 'video';
      remoteVideoElMap[vid] = video;
      remoteVideoContainer.appendChild(video);
      // The first parameter is an HTMLVideoElement.
      // The second parameter is the remote user ID.
      // The third parameter can be 1 (preview camera stream) or 2 (preview screen sharing stream).
      aliRtcEngine.setRemoteViewConfig(video, userId, 1);
    } else if (newState === 1) {
      removeRemoteVideo(userId, 'camera');
    }
  });

  aliRtcEngine.on('screenShareSubscribeStateChanged', (userId, oldState, newState, interval, channelId) => {
    // oldState and newState are of type AliRtcSubscribeState, with values 0 (initialized), 1 (unsubscribed), 2 (subscribing), 3 (subscribed).
    // interval is the time between the two states in milliseconds.
    console.log(`Channel ${channelId} remote user ${userId} screen stream subscription state changed from ${oldState} to ${newState}`);
    const vid = `screen_${userId}`;
    // Example handling    
    if (newState === 3) {
      const video = document.createElement('video');
      video.autoplay = true;
      video.className = 'video';
      remoteVideoElMap[vid] = video;
      remoteVideoContainer.appendChild(video);
      // The first parameter is an HTMLVideoElement.
      // The second parameter is the remote user ID.
      // The third parameter can be 1 (preview camera stream) or 2 (preview screen sharing stream).
      aliRtcEngine.setRemoteViewConfig(video, userId, 2);
    } else if (newState === 1) {
      removeRemoteVideo(userId, 'screen');
    }
  });
  
  // Screen sharing stream publish state changed.
  aliRtcEngine.on('screenSharePublishStateChanged', (oldState, newState, interval, channelId) => {
    // oldState and newState are of type AliRtcSubscribeState, with values 0 (initialized), 1 (unpublished), 2 (publishing), 3 (published).
    // interval is the time between the two states in milliseconds.
    console.log(`Channel ${channelId} local screen stream publish state changed from ${oldState} to ${newState}`);
    if (oldState === 3 && newState === 1) {
      showToast('screenToast', 'Screen sharing stopped.');
    }
  });
}

$('#loginForm').submit(async e => {
  // Prevent the default form submission action.
  e.preventDefault();
  const channelId = $('#channelId').val();
  const userId = $('#userId').val();
  const timestamp = Math.floor(Date.now() / 1000) + 3600 * 3;

  if (!channelId || !userId) {
    showToast('loginToast', 'Incomplete data.');
    return;
  }

  aliRtcEngine = AliRtcEngine.getInstance();
  listenEvents();

  try {
    const token = await generateToken(appId, appKey, channelId, userId, timestamp);
    // Set the channel profile. Supports strings 'communication' (communication mode) and 'interactive_live' (interactive live streaming mode).
    aliRtcEngine.setChannelProfile('communication');
    // Set the client role. This only takes effect in interactive live streaming mode.
    // Supports strings 'interactive' (interactive role, allows publishing and subscribing to streams) and 'live' (viewer role, only allows subscribing to streams).
    // aliRtcEngine.setClientRole('interactive');
    // Join the channel. Parameters like token and nonce are usually returned by the server.
    await aliRtcEngine.joinChannel(
      {
        channelId,
        userId,
        appId,
        token,
        timestamp,
      },
      userId
    );
    showToast('loginToast', 'Joined channel successfully.');
    $('#joinBtn').prop('disabled', true);
    $('#leaveBtn').prop('disabled', false);
    $('#boardBtn').prop('disabled', false);
    $('#screenBtn').prop('disabled', false);

    // Preview
    aliRtcEngine.setLocalViewConfig('localPreviewer', 1);
  } catch (error) {
    console.log('Failed to join channel', error);
    showToast('loginToast', 'Failed to join channel.');
  }
});

$('#leaveBtn').click(async () => {
  Object.keys(remoteVideoElMap).forEach(vid => {
    const arr = vid.split('_');
    removeRemoteVideo(arr[1], arr[0]);
  });
  // Stop local preview.
  await aliRtcEngine.stopPreview();
  // Leave the channel.
  await aliRtcEngine.leaveChannel();
  // Destroy the instance.
  aliRtcEngine.destroy();
  aliRtcEngine = undefined;
  $('#joinBtn').prop('disabled', false);
  $('#leaveBtn').prop('disabled', true);
  $('#boardBtn').prop('disabled', true);
  $('#screenBtn').prop('disabled', true);
  showToast('loginToast', 'Left the channel.');
});

// Enter your CommsEase whiteboard AppKey and AppSecret here.
// For local development and testing only. Do not expose the AppSecret in a production environment.
const boradAppKey = '';
const boradAppSecret = '';
const boradUid = Date.now(); // CommsEase whiteboard requires a numeric UID.
const boradNickname = boradUid.toString();
const boradChannel = '821937123';

async function sha1(data) {
  // Convert the string to an ArrayBuffer.
  const buffer = new TextEncoder().encode(data);
  
  // Use the Crypto API to calculate the hash value.
  const digest = await crypto.subtle.digest('SHA-1', buffer);
  
  // Convert the ArrayBuffer to a hexadecimal string.
  const hashArray = Array.from(new Uint8Array(digest));
  const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
  
  return hashHex;
}

async function getAuthInfo() {
  const Nonce = 'xxxx';   // A random string of any length less than 128 characters.
  const curTime = Math.round((Date.now() / 1000)); // The current UTC timestamp in seconds since January 1, 1970, 00:00:00.
  const checksum = await sha1(boradAppSecret + Nonce + curTime);
  return {
    nonce: Nonce,
    checksum: checksum,
    curTime: curTime
  };
}

// Create a whiteboard instance.
const boardIns = WhiteBoardSDK.getInstance({
  appKey: boradAppKey,
  nickname: boradNickname,     // Optional
  uid: boradUid,
  container: document.getElementById('whiteboard'),
  platform: 'web',
  record: false,   // Specifies whether to enable recording.
  getAuthInfo: getAuthInfo
});
let drawPluginIns;

// Join the whiteboard channel.
boardIns.joinRoom({
  channel: boradChannel,
  createRoom: true
})
.then((drawPlugin) => {
  drawPluginIns = drawPlugin;
  // Allow editing.
  drawPlugin.enableDraw(true);
  // Set the brush color.
  drawPlugin.setColor('rgb(243,0,0)');

  // Initialize the toolbar.
  const toolCollection = ToolCollection.getInstance({
      container: document.getElementById('whiteboard'),
      handler: drawPlugin,
      options: {
          platform: 'web'
      }
  });
  toolCollection.addOrSetTool({
    position: 'left',
    insertAfterTool: 'pan',
    item: {
      tool: 'uploadCenter',
      hint: 'Upload Document',
      supportPptToH5: true,
      supportDocToPic: true,
      supportUploadMedia: false, // Disable uploading multimedia files.
      supportTransMedia: false, // Disable transcoding multimedia files.
    },
  });
  toolCollection.removeTool({ name: 'image' });
  
  // Show the toolbar.
  toolCollection.show();

  // Listen for document events.
  toolCollection.on('docAdd', (newDocs, allDocs) => {
    console.log('add allDocs->', newDocs, allDocs);
    // You can upload document data to your server in the docAdd event callback.
    // Recommendation: Store data on the server-side based on the whiteboard channel.
  });
  toolCollection.on('docDelete', (newDocs, allDocs) => {
    console.log('delete allDocs->', newDocs, allDocs);
    // You can upload document data to your server in the docDelete event callback.
    // Recommendation: Store data on the server-side based on the whiteboard channel.
  });

  // After initialization, get the file list for this channel from the server and update it in the whiteboard SDK.
  // fetch('/docList', (list) => {
  //   toolCollection.setDefaultDocList(list);
  // });
});

$('#screenBtn').click(async() => {
  if (!aliRtcEngine) {
    showToast('screenToast', 'SDK not ready.');
    return;
  }
  try {
    await aliRtcEngine.startScreenShare();
    showToast('screenToast', 'Screen sharing started successfully.');
  } catch (error) {
    showToast('screenToast', 'Failed to share the screen.');
  }
});

$('#boardBtn').click(async() => {
  if (!aliRtcEngine || !drawPluginIns) {
    showToast('screenToast', 'SDK not ready.');
    return;
  }
  const mediaStream = drawPluginIns.getStream({
    width: 720,
    frameRate: 15,
  });
  const videoTrack = mediaStream.getVideoTracks()[0];
  if (videoTrack) {
    try {
      await aliRtcEngine.startScreenShare({
        videoTrack,
      });
      showToast('screenToast', 'Whiteboard sharing started successfully.');
    } catch (error) {
      showToast('screenToast', 'Failed to share the whiteboard.');
    }
  } else {
    showToast('screenToast', 'No whiteboard videoTrack available.');
  }
});

Step 4: Run the example

  1. In your terminal, navigate to the demo folder and run http-server -p 8080 to start an HTTP service.

  2. Open a new tab in your browser and navigate to localhost:8080/quick.html to act as a regular user. On the page, enter a Channel ID and a User ID, and then click Join Channel.

  3. Open another new tab in your browser and navigate to localhost:8080/whiteboard.html to act as the whiteboard user. On the page, enter the same Channel ID as in the previous step and a different User ID, and then click Join Channel.

  4. The page automatically subscribes to the other user's media stream. In whiteboard.html, click the Share Whiteboard button. The quick.html page then automatically subscribes to the whiteboard content from whiteboard.html.