diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 674766d7ca..ab2d4a1de5 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,5 +1,4 @@
@raviteja83
@KaustubhKumar05
-@amar-1995
@hdz-666
@ygit
\ No newline at end of file
diff --git a/README.md b/README.md
index 07fd77dc2a..05ee429921 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,30 @@
# Web SDKs
+[![Lint, Test and Build](https://github.com/100mslive/web-sdks/actions/workflows/lint-test-build.yml/badge.svg)](https://github.com/100mslive/web-sdks/actions/workflows/lint-test-build.yml)
+[![Activity](https://img.shields.io/github/commit-activity/m/100mslive/web-sdks.svg)](https://www.100ms.live/docs/javascript/v2/release-notes/release-notes)
+[![License](https://img.shields.io/npm/l/@100mslive/hms-video-store)](https://www.100ms.live/)
+[![Documentation](https://img.shields.io/badge/Read-Documentation-blue)](https://www.100ms.live/docs/javascript/v2/quickstart/javascript-quickstart)
+[![Register](https://img.shields.io/badge/Contact-Know%20More-blue)](https://dashboard.100ms.live/register)
+
This monorepo contains all the packages required to integrate 100ms on the web.
## What is included?
The packages folder contains all the SDK's of 100ms. Here is a brief overview of them:
+
| Directory | Package | Description | Link |
|--|--|--|--|
| `hms-video-store` | `@100mslive/hms-video-store` | This package contains the core SDK and the reactive store parts. | [README](./packages/hms-video-store) |
-| `react-icons` | `@100mslive/react-icons` | This contains all the icons used in the 100ms prebuilt. | [README](./packages/react-icons) |
| `react-sdk` | `@100mslive/react-sdk` | This contains the base React Hooks and some commonly used functionalities as React Hooks. | [README](./packages/react-sdk) |
| `roomkit-react` | `@100mslive/roomkit-react`| This contains the React components used in the Prebuilt and the Prebuilt component itself. | [README](./packages/roomkit-react) |
-| `roomkit-web` | `@100mslive/roomkit-web` | This is a web component port of the `HMSPrebuilt` component from the `roomkit-react`. If you are not using React,this can be used as a web component. | [README](./packages/roomkit-web)|
+| `roomkit-web` | `@100mslive/roomkit-web` | This is a web component port of the `HMSPrebuilt` component from the `roomkit-react`. If you are not using React, this can be used as a web component. | [README](./packages/roomkit-web)|
+| `hls-player` | `@100mslive/hls-player` | This is a HLS player offered by 100ms that can be used to play live video streams. | [README](./packages/hls-player) |
+| `hms-whiteboard` | `@100mslive/hms-whiteboard` | This contains APIs for integrating Whiteboard collaboration into your conferencing sessions. | [README](./packages/hms-whiteboard) |
+| `hms-virtual-background` | `@100mslive/hms-virtual-background` | This contains the Virtual Background plugin provided by 100ms. | [README](./packages/hms-virtual-background) |
+| `react-icons` | `@100mslive/react-icons` | This contains all the icons used in the 100ms prebuilt. | [README](./packages/react-icons) |
For full documentation, visit [100ms.live/docs](https://www.100ms.live/docs)
-
-
## How to integrate?
The 100ms SDK gives you everything you need to build scalable, high-quality live video and audio experiences.
@@ -26,19 +34,17 @@ The 100ms SDK gives you everything you need to build scalable, high-quality live
1. ## Custom UI
- 100ms SDKs are powerful and highly extensible to build and support all custom experiences and UI.
- **Related packages include:** `@100mslive/react-sdk`, `@100mslive/hms-video-store` and `@100mslive/react-icons`.
- - Get started with integrating the SDK using the [How to Guide](https://www.100ms.live/docs/javascript/v2/how-to-guides/install-the-sdk/integration).
+ - Get started with integrating the SDK using the [How to Guide](https://www.100ms.live/docs/javascript/v2/how-to-guides/install-the-sdk/integration).
> Navigate to `react-sdk` for the base React Hooks and some commonly used functionalities by clicking [here](./packages/react-sdk).
2. ## 100ms Prebuilt
- 100ms Prebuilt is a high-level abstraction with no-code customization that enables you to embed video conferencing and/or live streaming UI—with a few lines of code.
- **Related packages include:** `roomkit-react` and `roomkit-web`.
- - Get started with 100ms Prebuilt using the [Prebuilt Quickstart for Web](https://www.100ms.live/docs/javascript/v2/quickstart/prebuilt-quickstart).
+ - Get started with 100ms Prebuilt using the [Prebuilt Quickstart for Web](https://www.100ms.live/docs/javascript/v2/quickstart/prebuilt-quickstart).
> Navigate to `roomkit-react` for the React components used in Prebuilt and the Prebuilt component itself by clicking [here](./packages/roomkit-react).
-
-
![Banner](prebuilt-banner.png)
### 100ms Prebuilt Cross Platform Support
@@ -51,9 +57,6 @@ The 100ms SDK gives you everything you need to build scalable, high-quality live
| Flutter | [100ms-flutter](https://github.com/100mslive/100ms-flutter/tree/main/packages/hms_room_kit) | [Link](https://www.100ms.live/docs/flutter/v2/quickstart/prebuilt) | [hms_room_kit/example](https://github.com/100mslive/100ms-flutter/tree/main/packages/hms_room_kit/example) |
| React Native | [100ms-react-native](https://github.com/100mslive/100ms-react-native/tree/main/packages/react-native-room-kit) | [Link](https://www.100ms.live/docs/react-native/v2/quickstart/prebuilt) | [react-native-room-kit/example](https://github.com/100mslive/100ms-react-native/tree/main/packages/react-native-room-kit/example) |
-
-
-
## Setup
### Local Setup
@@ -62,7 +65,7 @@ The 100ms SDK gives you everything you need to build scalable, high-quality live
if you are using a different version in other projects, use [nvm](https://github.com/nvm-sh/nvm?tab=readme-ov-file#installing-and-updating) to manage node versions.
-```
+```bash
git clone https://github.com/100mslive/web-sdks.git
cd web-sdks
yarn install
@@ -71,7 +74,7 @@ yarn build
### Running Sample Prebuilt
-```
+```bash
cd examples/prebuilt-react-integration
yarn dev
```
@@ -86,9 +89,7 @@ Run `yarn start` by navigating to the package you are making changes to, the cha
For example, if you are making changes in roomkit-react(prebuilt), run `yarn start` in that package. The sample app should auto reload.
-> Note: Make sure `yarn build` is run atleast once before using `yarn start`
-
-
+> Note: Make sure `yarn build` is run atleast once before using `yarn start`.
### Deploying Your Changes
@@ -104,17 +105,14 @@ For reference:
![Project Settings](./project-settings.png)
-
Once the app has been deployed, you can append the room code at the end of the deployment URL to preview your changes
-
-
### Maintaining A Forked Version
-Tthe following command will build the roomkit-react package and generate a .tgz file:
+The following command will build the roomkit-react package and generate a .tgz file:
-```
+```bash
yarn && yarn build;
cd packages/roomkit-react;
yarn pack
@@ -142,8 +140,6 @@ import { HMSPrebuilt } from '@100mslive/roomkit-react';
We welcome external contributors or anyone excited to help improve 100ms SDKs. If you'd like to get involved, check out our [contribution guide](./DEVELOPER.MD), and get started exploring the codebase.
-
-
## Community & Support
- [GitHub Issues](https://github.com/100mslive/web-sdks/issues) - Submit any bugs or errors you encounter using the Web SDKs.
diff --git a/examples/prebuilt-react-integration/package.json b/examples/prebuilt-react-integration/package.json
index 8c838ca36b..643cb76447 100644
--- a/examples/prebuilt-react-integration/package.json
+++ b/examples/prebuilt-react-integration/package.json
@@ -10,7 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
- "@100mslive/roomkit-react": "0.3.24",
+ "@100mslive/roomkit-react": "0.3.25",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
diff --git a/package.json b/package.json
index 52db340ecb..652b546ff7 100644
--- a/package.json
+++ b/package.json
@@ -3,8 +3,8 @@
"private": true,
"devDependencies": {
"@babel/core": "^7.8.0",
- "@commitlint/cli": "^15.0.0",
- "@commitlint/config-conventional": "^15.0.0",
+ "@commitlint/cli": "^19.6.0",
+ "@commitlint/config-conventional": "^19.6.0",
"@size-limit/file": "^5.0.3",
"@types/jest": "^27.0.3",
"@types/node": "^16.11.17",
diff --git a/packages/hls-player/README.md b/packages/hls-player/README.md
index 0dda1052f8..0f8e8f1dd5 100644
--- a/packages/hls-player/README.md
+++ b/packages/hls-player/README.md
@@ -1,17 +1,348 @@
-`@100mslive/hls-player` is currently a wrapper on hls.js with easy to use interface and few add-ons for [100ms's interactive live streaming feature](https://www.100ms.live/docs/javascript/v2/how--to-guides/record-and-live-stream/hls/hls).
+# 100ms HLS Player
-Sample usage:
+[![Lint, Test and Build](https://github.com/100mslive/web-sdks/actions/workflows/lint-test-build.yml/badge.svg)](https://github.com/100mslive/web-sdks/actions/workflows/lint-test-build.yml)
+[![Bundle Size](https://badgen.net/bundlephobia/minzip/@100mslive/hls-player)](https://bundlephobia.com/result?p=@100mslive/hls-player)
+[![License](https://img.shields.io/npm/l/@100mslive/hls-player)](https://www.100ms.live/)
+![Tree shaking](https://badgen.net/bundlephobia/tree-shaking/@100mslive/hls-player)
+The `HMSHLSPlayer` is an HLS player offered by 100ms that can be used to play HLS streams. The player takes a URL and video element to play the stream.
+
+## How to integrate HLS Player SDK
+
+You can use Node package manager or yarn to add HMSHLSPlayer sdk to your project.
+Use [@100mslive/hls-player](https://www.npmjs.com/package/@100mslive/hls-player) as the package source.
+
+```bash
+npm i @100mslive/hls-player
+```
+
+## HMSHLSPlayer methods
+
+Below shows all the methods exposed from player
+
+```javascript
+interface IHMSHLSPlayer {
+ /**
+ * @returns get html video element
+ */
+ getVideoElement(): HTMLVideoElement;
+
+ /**
+ * set video volumne
+ * @param { volume } - in range [0,100]
+ */
+ setVolume(volume: number): void;
+ /**
+ *
+ * @returns returns HMSHLSLayer which represents current
+ * quality.
+ */
+ getLayer(): HMSHLSLayer | null;
+ /**
+ *
+ * @param { HMSHLSLayer } layer - layer we want to set the stream to.
+ * set { height: auto } to set the layer to auto
+ */
+ setLayer(layer: HMSHLSLayer): void;
+ /**
+ * move the video to Live
+ */
+ seekToLivePosition(): Promise;
+ /**
+ * play stream
+ * call this when autoplay error is received
+ */
+ play(): Promise;
+ /**
+ * pause stream
+ */
+ pause(): void;
+
+ /**
+ * It will update the video element current time
+ * @param seekValue Pass currentTime in second
+ */
+ seekTo(seekValue: number): void;
+}
+```
+
+### How to use Player's HLS Stream
+
+You create an instance of HMSHLSPlayer like below:
+
+```javascript
+import { HMSHLSPlayer } from '@100mslive/hls-player';
+
+// hls url should be provided which player will run.
+// second parameter is optional, if you had video element then pass to player else we will create one.
+const hlsPlayer = new HMSHLSPlayer(hlsURL, videoEl)
+
+// if video element is not present, we will create a new video element which can be attached to your ui.
+const videoEl = hlsPlayer.getVideoElement();
+```
+
+### How to pause and resume the playback
+
+You call play/pause on the hlsPlayer instance like below:
+
+```javascript
+// return Promise
+hmsPlayer.play()
+
+hmsPlayer.pause()
+```
+
+### How to seek forward or backward
+
+You use `seekTo` methods on the hlsPlayer to seek to any position in video, below is given example:
+
+```javascript
+// seekValue Pass currentTime in second
+hmsPlayer.seekTo(5)
+```
+
+### How to seek to live position
+
+You use `seekToLivePosition` methods on the hlsPlayer instance to go to the live poition like below:
+
+```javascript
+hmsPlayer.seekToLivePosition()
+```
+
+### How to change volume of HLS playback
+
+Use volume property to change the volume of HLS player. The volume level is between 0-100. Volume is set to 100 by default.
+
+```javascript
+hlsPlayer.setVolume(50);
```
-import {
- HLSPlaybackState,
-} from "@100mslive/hls-player";
-// hlsUrl is the url in which the hls stream is ongoing
-// videoElement is the video element where you want to play the stream
-const player = new HMSHLSPlayer(hlsUrl, videoElement);
-player.play()
+### Set video quality level to hls player
+```javascript
+/**
+*
+* @returns returns HMSHLSLayer which represents current
+* quality.
+*/
+hlsPlayer.getLayer(): HMSHLSLayer | null;
+/**
+*
+* @param { HMSHLSLayer } layer - layer we want to set the stream to.
+* set { height: auto } to set the layer to auto
+*/
+hlsPlayer.setLayer(layer: HMSHLSLayer): void;
+
+// quality interface
+interface HMSHLSLayer {
+ readonly bitrate: number;
+ readonly height?: number;
+ readonly id?: number;
+ readonly width?: number;
+ url: string;
+ resolution?: string;
+}
```
-More details to be added soon.
+## Events exposed from HMSHLSPlayer
+
+We are exposing events from our hls player.
+
+```javascript
+enum HMSHLSPlayerEvents {
+ TIMED_METADATA_LOADED = 'timed-metadata',
+ SEEK_POS_BEHIND_LIVE_EDGE = 'seek-pos-behind-live-edge',
+
+ CURRENT_TIME = 'current-time',
+ AUTOPLAY_BLOCKED = 'autoplay-blocked',
+
+ MANIFEST_LOADED = 'manifest-loaded',
+ LAYER_UPDATED = 'layer-updated',
+
+ ERROR = 'error',
+ PLAYBACK_STATE = 'playback-state',
+ STATS = 'stats',
+}
+```
+
+### Playback state
+
+```javascript
+enum HLSPlaybackState {
+ playing,
+ paused,
+}
+interface HMSHLSPlaybackState {
+ state: HLSPlaybackState;
+}
+hlsPlayer.on(HMSHLSPlayerEvents.PLAYBACK_STATE, (event: HMSHLSPlayerEvents, data: HMSHLSPlaybackState): void => {});
+```
+
+### HLS Stats
+
+```javascript
+interface HlsPlayerStats {
+ /** Estimated bandwidth in bits/sec. Could be used to show connection speed. */
+ bandwidthEstimate?: number;
+ /** The bitrate of the current level that is playing. Given in bits/sec */
+ bitrate?: number;
+ /** the amount of video available in forward buffer. Given in ms */
+ bufferedDuration?: number;
+ /** how far is the current playback from live edge.*/
+ distanceFromLive?: number;
+ /** total Frames dropped since started watching the stream. */
+ droppedFrames?: number;
+ /** the m3u8 url of the current level that is being played */
+ url?: string;
+ /** the resolution of the level of the video that is being played */
+ videoSize?: {
+ height: number;
+ width: number;
+ };
+}
+
+hlsPlayer.on(HMSHLSPlayerEvents.STATS, (event: HMSHLSPlayerEvents, data: HlsPlayerStats): void => {});
+```
+
+### Manifest loaded data
+
+Hls player will provide a manifest which will provide a data like different quality layer once url is loaded.
+
+```javascript
+interface HMSHLSManifestLoaded {
+ layers: HMSHLSLayer[];
+}
+hlsPlayer.on(HMSHLSPlayerEvents.MANIFEST_LOADED, (event: HMSHLSPlayerEvents, data: HMSHLSManifestLoaded): void => {});
+```
+
+### Quality changed data
+
+```javascript
+interface HMSHLSLayerUpdated {
+ layer: HMSHLSLayer;
+}
+hlsPlayer.on(HMSHLSPlayerEvents.LAYER_UPDATED, (event: HMSHLSPlayerEvents, data: HMSHLSLayerUpdated): void => {});
+```
+
+### Live Event
+
+Player will let you know if player is plaaying video live or not
+
+```javascript
+interface HMSHLSStreamLive {
+ isLive: boolean;
+}
+hlsPlayer.on(HMSHLSPlayerEvents.SEEK_POS_BEHIND_LIVE_EDGE, (event: HMSHLSPlayerEvents, data: HMSHLSStreamLive): void => {});
+```
+
+### HLS timed-metadata
+
+HLS player will parse and send the timed-metadata.
+
+```javascript
+interface HMSHLSCue {
+ id?: string;
+ payload: string;
+ duration: number;
+ startDate: Date;
+ endDate?: Date;
+}
+hlsPlayer.on(HMSHLSPlayerEvents.TIMED_METADATA_LOADED, (event: HMSHLSPlayerEvents, data: HMSHLSCue): void => {});
+```
+
+### Error handling
+
+```javascript
+interface HMSHLSException {
+ name: string,
+ message: string,
+ description: string,
+ isTerminal: boolean, // decide if player error will automatically restart(if false)
+}
+hlsPlayer.on(HMSHLSPlayerEvents.ERROR, (event: HMSHLSPlayerEvents, data: HMSHLSException): void => {});
+hlsPlayer.on(HMSHLSPlayerEvents.AUTOPLAY_BLOCKED, (event: HMSHLSPlayerEvents, data: HMSHLSException): void => {});
+```
+
+### Video current time
+
+```javascript
+hlsPlayer.on(HMSHLSPlayerEvents.CURRENT_TIME, (event: HMSHLSPlayerEvents, data: number): void => {});
+```
+
+### Example for events usage
+
+Below are the simple example of how to use hls player's event
+
+```javascript
+const isPlaying = false;
+const playbackEventHandler = data => isPlaying = data.state === HLSPlaybackState.paused;
+hlsPlayer.on(HMSHLSPlayerEvents.PLAYBACK_STATE, playbackEventHandler);
+```
+
+## HLS Player example
+
+Below is a simple example in which hls-player will be used in your app.
+
+```javascript
+// Vanilla JavaScript Example
+import { HLSPlaybackState, HMSHLSPlayer, HMSHLSPlayerEvents } from "@100mslive/hls-player";
+
+const videoEl; // reference for video element
+const hlsUrl; // reference to hls url
+
+// variable to handle ui and take some actions
+let isLive = true, isPaused = false, isAutoBlockedPaused = false;
+
+const handleError = data => console.error("[HLSView] error in hls", data);
+const handleNoLongerLive = ({ isLive }) => isLive = isLive;
+
+const playbackEventHandler = data => isPaused = (data.state === HLSPlaybackState.paused);
+
+const handleAutoplayBlock = data => isAutoBlockedPaused = !!data;
+
+const hlsPlayer = new HMSHLSPlayer(hlsUrl, videoEl);
+
+hlsPlayer.on(HMSHLSPlayerEvents.SEEK_POS_BEHIND_LIVE_EDGE, handleNoLongerLive);
+hlsPlayer.on(HMSHLSPlayerEvents.ERROR, handleError);
+hlsPlayer.on(HMSHLSPlayerEvents.PLAYBACK_STATE, playbackEventHandler);
+hlsPlayer.on(HMSHLSPlayerEvents.AUTOPLAY_BLOCKED, handleAutoplayBlock);
+```
+
+```jsx
+// React Example
+import { HLSPlaybackState, HMSHLSPlayer, HMSHLSPlayerEvents } from "@100mslive/hls-player";
+import { useEffect, useState } from 'react';
+
+const videoEl; // reference for video element
+const hlsUrl; // reference to hls url
+
+// variable to handle ui and take some actions
+const [isVideoLive, setIsVideoLive] = useState(true);
+const [isHlsAutoplayBlocked, setIsHlsAutoplayBlocked] = useState(false);
+const [isPaused, setIsPaused] = useState(false);
+
+useEffect(() => {
+ const handleError = data => console.error("[HLSView] error in hls", data);
+ const handleNoLongerLive = ({ isLive }) => {
+ setIsVideoLive(isLive);
+ };
+
+ const playbackEventHandler = data =>
+ setIsPaused(data.state === HLSPlaybackState.paused);
+
+ const handleAutoplayBlock = data => setIsHlsAutoplayBlocked(!!data);
+ const hlsPlayer = new HMSHLSPlayer(hlsUrl, videoEl);
+
+ hlsPlayer.on(HMSHLSPlayerEvents.SEEK_POS_BEHIND_LIVE_EDGE, handleNoLongerLive);
+ hlsPlayer.on(HMSHLSPlayerEvents.ERROR, handleError);
+ hlsPlayer.on(HMSHLSPlayerEvents.PLAYBACK_STATE, playbackEventHandler);
+ hlsPlayer.on(HMSHLSPlayerEvents.AUTOPLAY_BLOCKED, handleAutoplayBlock);
+ return () => {
+ hlsPlayer.off(HMSHLSPlayerEvents.SEEK_POS_BEHIND_LIVE_EDGE, handleNoLongerLive);
+ hlsPlayer.off(HMSHLSPlayerEvents.ERROR, handleError);
+ hlsPlayer.off(HMSHLSPlayerEvents.PLAYBACK_STATE, playbackEventHandler);
+ hlsPlayer.off(HMSHLSPlayerEvents.AUTOPLAY_BLOCKED, handleAutoplayBlock);
+ }
+}, []);
+
+```
diff --git a/packages/hls-player/package.json b/packages/hls-player/package.json
index af5c74f56e..b0e2fff8e9 100644
--- a/packages/hls-player/package.json
+++ b/packages/hls-player/package.json
@@ -1,6 +1,6 @@
{
"name": "@100mslive/hls-player",
- "version": "0.3.24",
+ "version": "0.3.25",
"description": "HLS client library which uses HTML5 Video element and Media Source Extension for playback",
"main": "dist/index.cjs.js",
"module": "dist/index.js",
@@ -36,8 +36,16 @@
"author": "100ms",
"license": "MIT",
"dependencies": {
- "@100mslive/hls-stats": "0.4.24",
+ "@100mslive/hls-stats": "0.4.25",
"eventemitter2": "^6.4.9",
"hls.js": "1.4.12"
- }
+ },
+ "keywords": [
+ "hls",
+ "video",
+ "player",
+ "webrtc",
+ "conferencing",
+ "100ms"
+ ]
}
diff --git a/packages/hls-stats/README.md b/packages/hls-stats/README.md
index d422d6ecd0..1af6ec5ed8 100644
--- a/packages/hls-stats/README.md
+++ b/packages/hls-stats/README.md
@@ -1,5 +1,11 @@
# @100mslive/hms-stats
+[![Lint, Test and Build](https://github.com/100mslive/web-sdks/actions/workflows/lint-test-build.yml/badge.svg)](https://github.com/100mslive/web-sdks/actions/workflows/lint-test-build.yml)
+[![Bundle Size](https://badgen.net/bundlephobia/minzip/@100mslive/hls-stats)](https://bundlephobia.com/result?p=@100mslive/hls-stats)
+[![License](https://img.shields.io/npm/l/@100mslive/hls-stats)](https://www.100ms.live/)
+![Tree shaking](https://badgen.net/bundlephobia/tree-shaking/@100mslive/hls-stats)
+
+
A simple library for HLS stats for Hls.js.
## Installation
diff --git a/packages/hls-stats/package.json b/packages/hls-stats/package.json
index fc3522ae93..6b7f077190 100644
--- a/packages/hls-stats/package.json
+++ b/packages/hls-stats/package.json
@@ -1,6 +1,6 @@
{
"name": "@100mslive/hls-stats",
- "version": "0.4.24",
+ "version": "0.4.25",
"description": "A simple library that provides stats for your hls stream",
"main": "dist/index.cjs.js",
"module": "dist/index.js",
diff --git a/packages/hms-video-store/README.md b/packages/hms-video-store/README.md
index 8c37e598cd..1d2f8b5f8e 100644
--- a/packages/hms-video-store/README.md
+++ b/packages/hms-video-store/README.md
@@ -1,8 +1,9 @@
# 100ms Reactive Store
[![NPM](https://badgen.net/npm/v/@100mslive/hms-video-store?color=green)](https://www.npmjs.com/package/@100mslive/hms-video-store)
-![Test](https://github.com/100mslive/hms-video-store/actions/workflows/main.yaml/badge.svg)
+[![Lint, Test and Build](https://github.com/100mslive/web-sdks/actions/workflows/lint-test-build.yml/badge.svg)](https://github.com/100mslive/web-sdks/actions/workflows/lint-test-build.yml)
[![Bundle Size](https://badgen.net/bundlephobia/minzip/@100mslive/hms-video-store)](https://bundlephobia.com/result?p=@100mslive/hms-video-store)
+[![License](https://img.shields.io/npm/l/@100mslive/hms-video-store)](https://www.100ms.live/)
![Tree shaking](https://badgen.net/bundlephobia/tree-shaking/@100mslive/hms-video-store)
![Architecture](images/architecture.png)
diff --git a/packages/hms-video-store/package.json b/packages/hms-video-store/package.json
index dcfed593f6..d16cebb840 100644
--- a/packages/hms-video-store/package.json
+++ b/packages/hms-video-store/package.json
@@ -1,5 +1,5 @@
{
- "version": "0.12.24",
+ "version": "0.12.25",
"license": "MIT",
"repository": {
"type": "git",
diff --git a/packages/hms-video-store/src/IHMSActions.ts b/packages/hms-video-store/src/IHMSActions.ts
index 01cc905201..bcfe6dbdbc 100644
--- a/packages/hms-video-store/src/IHMSActions.ts
+++ b/packages/hms-video-store/src/IHMSActions.ts
@@ -40,6 +40,7 @@ import {
IHMSSessionStoreActions,
} from './schema';
import { HMSRoleChangeRequest } from './selectors';
+import { HMSStats } from './webrtc-stats';
/**
* The below interface defines our SDK API Surface for taking room related actions.
@@ -587,4 +588,10 @@ export interface IHMSActions = [
- // @ts-ignore
- { googEchoCancellation: { exact: true } },
- // @ts-ignore
- { googExperimentalEchoCancellation: { exact: true } },
- // @ts-ignore
+ ...standardMediaConstraints,
{ autoGainControl: { exact: true } },
// @ts-ignore
{ noiseSuppression: { exact: true } },
- // @ts-ignore
- { googHighpassFilter: { exact: true } },
- // @ts-ignore
- { googAudioMirroring: { exact: true } },
];
-
volume(volume: number) {
if (!(0.0 <= volume && volume <= 1.0)) {
throw Error('volume can only be in range [0.0, 1.0]');
diff --git a/packages/hms-video-store/src/media/settings/constants.ts b/packages/hms-video-store/src/media/settings/constants.ts
new file mode 100644
index 0000000000..e1f0e8f4d2
--- /dev/null
+++ b/packages/hms-video-store/src/media/settings/constants.ts
@@ -0,0 +1,8 @@
+export const standardMediaConstraints = [
+ { echoCancellation: { exact: true } },
+ { highpassFilter: { exact: true } },
+ { audioMirroring: { exact: true } },
+ // These options can vary depending on the audio plugin
+ // { autoGainControl: { exact: true } },
+ // { noiseSuppression: { exact: true } },
+];
diff --git a/packages/hms-video-store/src/plugins/audio/HMSAudioPluginsManager.ts b/packages/hms-video-store/src/plugins/audio/HMSAudioPluginsManager.ts
index 75100bb9a5..fa84d76ed4 100644
--- a/packages/hms-video-store/src/plugins/audio/HMSAudioPluginsManager.ts
+++ b/packages/hms-video-store/src/plugins/audio/HMSAudioPluginsManager.ts
@@ -4,6 +4,8 @@ import AnalyticsEventFactory from '../../analytics/AnalyticsEventFactory';
import { ErrorFactory } from '../../error/ErrorFactory';
import { HMSAction } from '../../error/HMSAction';
import { EventBus } from '../../events/EventBus';
+import { HMSAudioTrackSettingsBuilder } from '../../media/settings';
+import { standardMediaConstraints } from '../../media/settings/constants';
import { HMSLocalAudioTrack } from '../../media/tracks';
import Room from '../../sdk/models/HMSRoom';
import HMSLogger from '../../utils/logger';
@@ -74,7 +76,7 @@ export class HMSAudioPluginsManager {
}
switch (plugin.getName()) {
- case 'HMSKrispPlugin':
+ case 'HMSKrispPlugin': {
if (!this.room?.isNoiseCancellationEnabled) {
const errorMessage = 'Krisp Noise Cancellation is not enabled for this room';
if (this.pluginsMap.size === 0) {
@@ -85,7 +87,23 @@ export class HMSAudioPluginsManager {
}
}
this.eventBus.analytics.publish(AnalyticsEventFactory.krispStart());
+ const { settings } = this.hmsTrack;
+ const newAudioTrackSettings = new HMSAudioTrackSettingsBuilder()
+ .codec(settings.codec)
+ .maxBitrate(settings.maxBitrate)
+ .deviceId(settings.deviceId!)
+ .advanced([
+ ...standardMediaConstraints,
+ // @ts-ignore
+ { autoGainControl: { exact: false } },
+ // @ts-ignore
+ { noiseSuppression: { exact: false } },
+ ])
+ .audioMode(settings.audioMode)
+ .build();
+ await this.hmsTrack.setSettings(newAudioTrackSettings);
break;
+ }
default:
}
@@ -162,9 +180,25 @@ export class HMSAudioPluginsManager {
async removePlugin(plugin: HMSAudioPlugin) {
switch (plugin.getName()) {
- case 'HMSKrispPlugin':
+ case 'HMSKrispPlugin': {
this.eventBus.analytics.publish(AnalyticsEventFactory.krispStop());
+ const { settings } = this.hmsTrack;
+ const newAudioTrackSettings = new HMSAudioTrackSettingsBuilder()
+ .codec(settings.codec)
+ .maxBitrate(settings.maxBitrate)
+ .deviceId(settings.deviceId!)
+ .advanced([
+ ...standardMediaConstraints,
+ // @ts-ignore
+ { autoGainControl: { exact: true } },
+ // @ts-ignore
+ { noiseSuppression: { exact: true } },
+ ])
+ .audioMode(settings.audioMode)
+ .build();
+ await this.hmsTrack.setSettings(newAudioTrackSettings);
break;
+ }
default:
break;
}
diff --git a/packages/hms-video-store/src/reactive-store/HMSSDKActions.ts b/packages/hms-video-store/src/reactive-store/HMSSDKActions.ts
index a6f77818a4..9b880b70f7 100644
--- a/packages/hms-video-store/src/reactive-store/HMSSDKActions.ts
+++ b/packages/hms-video-store/src/reactive-store/HMSSDKActions.ts
@@ -92,6 +92,7 @@ import {
selectVideoTrackByID,
} from '../selectors';
import { FindPeerByNameRequestParams } from '../signal/interfaces';
+import { HMSStats } from '../webrtc-stats';
/**
* This class implements the IHMSActions interface for 100ms SDK. It connects with SDK
@@ -1671,4 +1672,24 @@ export class HMSSDKActions> = (fn, name) => {
return this.store.namedSetState(fn, name);
};
+
+ /**
+ * @internal
+ * This will be used by beam to check if the recording should continue, it will pass __hms.stats
+ * It will poll at a fixed interval and start an exit timer if the method fails twice consecutively
+ * The exit timer is stopped if the method returns true before that
+ * @param hmsStats
+ */
+ hasActiveElements(hmsStats: HMSStats): boolean {
+ const isWhiteboardPresent = Object.keys(this.store.getState().whiteboards).length > 0;
+ const isQuizOrPollPresent = Object.keys(this.store.getState().polls).length > 0;
+ const peerCount = Object.keys(this.store.getState().peers).length > 0;
+ const remoteTracks = hmsStats.getState().remoteTrackStats;
+ return (
+ peerCount &&
+ (isWhiteboardPresent ||
+ isQuizOrPollPresent ||
+ Object.values(remoteTracks).some(track => track && typeof track.bitrate === 'number' && track.bitrate > 0))
+ );
+ }
}
diff --git a/packages/hms-video-store/src/sdk/RoleChangeManager.ts b/packages/hms-video-store/src/sdk/RoleChangeManager.ts
index c4be09307b..c13b2519bf 100644
--- a/packages/hms-video-store/src/sdk/RoleChangeManager.ts
+++ b/packages/hms-video-store/src/sdk/RoleChangeManager.ts
@@ -1,6 +1,6 @@
import { Store } from './store';
import { DeviceManager } from '../device-manager';
-import { HMSRole } from '../interfaces';
+import { DeviceType, HMSRole } from '../interfaces';
import InitialSettings from '../interfaces/settings';
import { SimulcastLayers } from '../interfaces/simulcast-layers';
import { HMSPeerUpdate, HMSTrackUpdate, HMSUpdateListener } from '../interfaces/update-listener';
@@ -144,14 +144,49 @@ export default class RoleChangeManager {
}
private getSettings(): InitialSettings {
- const initialSettings = this.store.getConfig()?.settings;
+ const { isAudioMuted, isVideoMuted } = this.getMutedStatus();
+ const { audioInputDeviceId, audioOutputDeviceId } = this.getAudioDeviceSettings();
+ const videoDeviceId = this.getVideoInputDeviceId();
+ return {
+ isAudioMuted: isAudioMuted,
+ isVideoMuted: isVideoMuted,
+ audioInputDeviceId: audioInputDeviceId,
+ audioOutputDeviceId: audioOutputDeviceId,
+ videoDeviceId: videoDeviceId,
+ };
+ }
+ private getMutedStatus(): { isAudioMuted: boolean; isVideoMuted: boolean } {
+ const initialSettings = this.store.getConfig()?.settings;
return {
isAudioMuted: initialSettings?.isAudioMuted ?? true,
isVideoMuted: initialSettings?.isVideoMuted ?? true,
- audioInputDeviceId: initialSettings?.audioInputDeviceId || 'default',
- audioOutputDeviceId: initialSettings?.audioOutputDeviceId || 'default',
- videoDeviceId: initialSettings?.videoDeviceId || 'default',
};
}
+
+ private getAudioDeviceSettings(): { audioInputDeviceId: string; audioOutputDeviceId: string } {
+ const initialSettings = this.store.getConfig()?.settings;
+ const audioInputDeviceId =
+ this.deviceManager.currentSelection[DeviceType.audioInput]?.deviceId ||
+ initialSettings?.audioInputDeviceId ||
+ 'default';
+ const audioOutputDeviceId =
+ this.deviceManager.currentSelection[DeviceType.audioOutput]?.deviceId ||
+ initialSettings?.audioOutputDeviceId ||
+ 'default';
+
+ return {
+ audioInputDeviceId,
+ audioOutputDeviceId,
+ };
+ }
+
+ private getVideoInputDeviceId(): string {
+ const initialSettings = this.store.getConfig()?.settings;
+ return (
+ this.deviceManager.currentSelection[DeviceType.videoInput]?.deviceId ||
+ initialSettings?.videoDeviceId ||
+ 'default'
+ );
+ }
}
diff --git a/packages/hms-video-store/src/sdk/index.ts b/packages/hms-video-store/src/sdk/index.ts
index fe6040e751..50bd36b0eb 100644
--- a/packages/hms-video-store/src/sdk/index.ts
+++ b/packages/hms-video-store/src/sdk/index.ts
@@ -1350,6 +1350,7 @@ export class HMSSdk implements HMSInterface {
}
private handleLocalRoleUpdate = async ({ oldRole, newRole }: { oldRole: HMSRole; newRole: HMSRole }) => {
+ this.deviceManager.currentSelection = this.deviceManager.getCurrentSelection();
await this.transport.handleLocalRoleUpdate({ oldRole, newRole });
await this.roleChangeManager?.handleLocalPeerRoleUpdate({ oldRole, newRole });
await this.interactivityCenter.whiteboard.handleLocalRoleUpdate();
diff --git a/packages/hms-virtual-background/README.md b/packages/hms-virtual-background/README.md
index ac8a38d7c4..75ec13a8ec 100755
--- a/packages/hms-virtual-background/README.md
+++ b/packages/hms-virtual-background/README.md
@@ -1,13 +1,243 @@
-## installation
+# Virtual Background with Effects SDK
-**with npm:**
+[![Lint, Test and Build](https://github.com/100mslive/web-sdks/actions/workflows/lint-test-build.yml/badge.svg)](https://github.com/100mslive/web-sdks/actions/workflows/lint-test-build.yml)
+[![Bundle Size](https://badgen.net/bundlephobia/minzip/@100mslive/hms-virtual-background)](https://bundlephobia.com/result?p=@100mslive/hms-virtual-background)
+[![License](https://img.shields.io/npm/l/@100mslive/hms-virtual-background)](https://www.100ms.live/)
+![Tree shaking](https://badgen.net/bundlephobia/tree-shaking/@100mslive/hms-virtual-background)
-```npm i --save @100mslive/hms-virtual-background```
+Virtual background plugin helps in customising one’s background. The customising options are blurring the background or replacing it with a static image. This guide provides an overview of usage of the virtual background plugin of 100ms.
-**with yarn**
+## Pre-requisites
-```yarn add @100mslive/hms-virtual-background```
+Get the 100ms VirtualBackground Package** (Supported since version 1.11.28)
-## Usage
+```bash section=GetHMSVirtualBackgroundPackage sectionIndex=1
+npm install --save @100mslive/hms-virtual-background@latest
+```
-Refer our [docs](https://www.100ms.live/docs/javascript/v2/plugins/virtual-background#start-and-stop-virtual-background) for usage.
\ No newline at end of file
+## Features
+
+The following features are currently supported under the `HMSEffectsPlugin` class:
+
+```js
+/**
+ * Sets the blur intensity.
+ * @param {number} blur - The blur intensity, ranging from 0 to 1.
+ * @returns {void}
+ */
+setBlur(blur) {}
+
+/**
+ * Sets the virtual background using the provided media URL.
+ * @param {HMSEffectsBackground} url - The media URL to set as the virtual background.
+ * Alternatively, the background image can be downloaded beforehand and passed to setBackground as objectURL
+ * @returns {void}
+ */
+setBackground(url) {}
+
+/**
+ * Retrieves the name of the plugin.
+ * @returns {string} The name of the plugin, 'HMSEffects'.
+ */
+getName() {}
+
+/**
+ * Retrieves the currently enabled background type or media URL.
+ * @returns {string|MediaStream|MediaStreamTrack|HTMLVideoElement} The background type or media URL.
+ */
+getBackground() {}
+
+/**
+ * Sets the preset quality of the virtual background.
+ * Options: "balanced" | "quality"
+ * The 'quality' preset has a higher CPU usage than the 'balanced' preset which is the default
+ * @param {string} preset - The preset quality to set.
+ * @returns {Promise}
+ */
+setPreset(preset) {}
+
+/**
+ * Retrieves the active preset quality of the virtual background.
+ * @returns {string} The active preset quality.
+ */
+getPreset() {}
+
+/**
+ * Clears all applied filters.
+ * @returns {void}
+ */
+removeEffects() {}
+
+/**
+ * Stops the plugin.
+ * @returns {void}
+ */
+stop() {}
+
+```
+
+Callbacks supported by the plugin:
+
+On initialization, after the required resources are downloaded by the plugin:
+
+```
+const effectsPlugin = new HMSEffectsPlugin(, () => console.log("Plugin initialised"));
+
+```
+
+On resolution change (on device rotation or when the video aspect ratio changes):
+
+```
+effectsPlugin.onResolutionChange = (width: number, height: number) => {
+ console.log(`Resolution changed to ${width}x${height}`)
+}
+```
+
+## Instantiate Virtual Background
+
+The SDK key for effects is needed to instantiate the `HMSEffectsPlugin` class:
+
+```jsx
+ const effectsKey = useHMSStore(selectEffectsKey);
+```
+
+It is recommended to initialise the object in a separate file to prevent multiple initialisations on re-renders and keep the UI level code independent of internal calls by the SDK.
+
+
+```js
+import { HMSEffectsPlugin, HMSVirtualBackgroundTypes } from '@100mslive/hms-virtual-background';
+
+export class VBPlugin {
+ private effectsPlugin?: HMSEffectsPlugin | undefined;
+
+ initialisePlugin = (effectsSDKKey?: string) => {
+ if (this.getVBObject()) {
+ return;
+ }
+ if (effectsSDKKey) {
+ this.effectsPlugin = new HMSEffectsPlugin(effectsSDKKey);
+ }
+ };
+
+ getBackground = () => {
+ return this.effectsPlugin?.getBackground();
+ };
+
+ getBlurAmount = () => {
+ return this.effectsPlugin?.getBlurAmount();
+ };
+
+ getVBObject = () => {
+ return this.effectsPlugin;
+ };
+
+ getName = () => {
+ return this.effectsPlugin?.getName();
+ };
+
+ setBlur = async (blurPower: number) => {
+ this.effectsPlugin?.setBlur(blurPower);
+ };
+
+ setBackground = async (mediaURL: string) => {
+ this.effectsPlugin?.setBackground(mediaURL);
+ };
+
+ setPreset = (preset: string) => {
+ this.effectsPlugin.setPreset(preset);
+ };
+
+ getPreset = () => {
+ return this.effectsPlugin?.getPreset() || '';
+ };
+
+ removeEffects = async () => {
+ this.effectsPlugin?.removeEffects();
+ };
+
+ reset = () => {
+ this.effectsPlugin = undefined;
+ };
+}
+
+export const VBHandler = new VBPlugin();
+```
+
+## Building the UI
+
+The following snippet illustrates how to add the plugin to the video and manage the UI state to preserve configuration:
+
+```jsx
+import {
+ selectEffectsKey,
+ selectIsLocalVideoPluginPresent
+ selectLocalVideoTrackID,
+ useHMSStore,
+} from '@100mslive/react-sdk';
+import {
+ HMSEffectsPlugin,
+ HMSVirtualBackgroundTypes
+} from '@100mslive/hms-virtual-background';
+import { VBHandler } from './VBHandler';
+
+export const VirtualBackgroundPicker = () => {
+ const hmsActions = useHMSActions();
+ // Get the effects SDK key here
+ const effectsKey = useHMSStore(selectEffectsKey);
+ const trackId = useHMSStore(selectLocalVideoTrackID);
+ const isPluginAdded = useHMSStore(selectIsLocalVideoPluginPresent(VBHandler?.getName() || ''));
+
+ // State can be used to show active selection
+ const [background, setBackground] = useState(
+ VBHandler.getBackground() as string | HMSVirtualBackgroundTypes,
+ );
+
+ useEffect(() => {
+ if (!track?.id) {
+ return;
+ }
+ if (!isPluginAdded) {
+ let vbObject = VBHandler.getVBObject();
+ if (!vbObject) {
+ VBHandler.initialisePlugin(effectsKey);
+ vbObject = VBHandler.getVBObject();
+ if (effectsKey) {
+ hmsActions.addPluginsToVideoStream([vbObject as HMSEffectsPlugin]);
+ }
+ }
+ }
+ }, [hmsActions, isPluginAdded, effectsKey, track?.id]);
+
+ // UI code for media picker can go here
+}
+
+```
+
+This handles initialisation and adding the plugin to the video stream. The plugin takes a few seconds on first load during initialisation. Subsequent filter and effect selections should take less than a second to reflect.
+
+The methods can be called via the `VBHandler` object:
+```jsx
+const setBackground = async(mediaURL : string) => {
+ await VBHandler?.setBackground(mediaURL);
+ // The selection can be highlighted using the activeBackground state
+ setActiveBackground(mediaURL);
+}
+
+const setBlur = async(blurAmount: number) => {
+ await VBHandler?.setBlur(blurAmount);
+ setActiveBackground(HMSVirtualBackgroundTypes.BLUR);
+}
+
+const removeEffects = async() => {
+ await VBHandler.removeEffects();
+ setActiveBackground(HMSVirtualBackgroundTypes.NONE);
+}
+```
+
+
+The full implementation can be viewed in the [roomkit-react package](https://github.com/100mslive/web-sdks/blob/main/packages/roomkit-react/src/Prebuilt/components/VirtualBackground/VBPicker.tsx).
+
+
+## Supported Browsers
+
+Effects virtual background is supported on Safari, Firefox and Chromium based browsers.
diff --git a/packages/hms-virtual-background/package.json b/packages/hms-virtual-background/package.json
index 3948195b3d..0df815ac53 100755
--- a/packages/hms-virtual-background/package.json
+++ b/packages/hms-virtual-background/package.json
@@ -1,5 +1,5 @@
{
- "version": "1.13.24",
+ "version": "1.13.25",
"license": "MIT",
"name": "@100mslive/hms-virtual-background",
"author": "100ms",
@@ -62,10 +62,10 @@
"format": "prettier --write src/**/*.ts"
},
"peerDependencies": {
- "@100mslive/hms-video-store": "0.12.24"
+ "@100mslive/hms-video-store": "0.12.25"
},
"devDependencies": {
- "@100mslive/hms-video-store": "0.12.24"
+ "@100mslive/hms-video-store": "0.12.25"
},
"dependencies": {
"@mediapipe/selfie_segmentation": "^0.1.1632777926",
diff --git a/packages/hms-whiteboard/README.md b/packages/hms-whiteboard/README.md
new file mode 100644
index 0000000000..23137323d7
--- /dev/null
+++ b/packages/hms-whiteboard/README.md
@@ -0,0 +1,125 @@
+# 100ms Whiteboard
+
+[![Lint, Test and Build](https://github.com/100mslive/web-sdks/actions/workflows/lint-test-build.yml/badge.svg)](https://github.com/100mslive/web-sdks/actions/workflows/lint-test-build.yml)
+[![Bundle Size](https://badgen.net/bundlephobia/minzip/@100mslive/hms-whiteboard)](https://bundlephobia.com/result?p=@100mslive/hms-whiteboard)
+[![License](https://img.shields.io/npm/l/@100mslive/hms-whiteboard)](https://www.100ms.live/)
+![Tree shaking](https://badgen.net/bundlephobia/tree-shaking/@100mslive/hms-whiteboard)
+
+The 100ms SDK provides robust APIs for integrating whiteboard collaboration into your conferencing sessions. Participants can engage in real-time by drawing, writing, and collaborating on a shared digital whiteboard. This documentation outlines how to implement the start and stop functionality for a whiteboard and display it within an iframe or embed it as a React component.
+
+## Requirements
+
+- React 18 or higher
+- Webpack 5 or higher if you're using it to bundle your app
+- User roles must be configured to enable whiteboard functionality via the 100ms dashboard for starting or viewing the whiteboard. [Refer here](https://www.100ms.live/docs/get-started/v2/get-started/features/whiteboard#enabling-and-configuring-the-whiteboard).
+- If you're on React and are not using the `@100mslive/roomkit-react` package, install the `@100mslive/hms-whiteboard` package.
+
+```bash
+yarn add @100mslive/hms-whiteboard
+```
+
+## Opening and Closing the Whiteboard
+
+JavaScript users can use the `selectPermissions` selector which fetches the whiteboard specific permissions array from the local peer's role permissions.
+
+React users can check for the `toggle` function returned by the utility hook `useWhiteboard`.
+
+```js
+// Vanilla JavaScript Example
+import { selectPermissions, selectWhiteboard } from '@100mslive/hms-video-store';
+
+const permissions = hmsStore.getState(selectPermissions)?.whiteboard; // Array<'read' | 'write' | 'admin'>
+const isAdmin = !!permissions?.includes('admin');
+const whiteboard = hmsStore.getState(selectWhiteboard);
+const isOwner = whiteboard?.owner === localPeerUserId;
+
+const toggle = async () => {
+ if (!isAdmin) {
+ return;
+ }
+
+ if (whiteboard?.open) {
+ isOwner && (await actions.interactivityCenter.whiteboard.close());
+ } else {
+ await actions.interactivityCenter.whiteboard.open();
+ }
+};
+
+// usage
+const toggleButton = document.getElementById('toggle-whiteboard');
+// non-admin users cannot toggle the whiteboard
+toggleButton.disabled = !isAdmin;
+toggleButton.onclick = toggle;
+```
+
+```jsx
+// React Example
+import React from 'react';
+import { useWhiteboard } from '@100mslive/react-sdk';
+
+export const WhiteboardToggle = () => {
+ const { toggle, open, isOwner } = useWhiteboard();
+
+ // non-admin users cannot toggle the whiteboard
+ if (!toggle) {
+ return null;
+ }
+
+ return (
+
+ );
+};
+```
+
+## Displaying the Collaborative Whiteboard
+
+You can display the whiteboard when it's open by embedding it as an iframe or as a React component for more fine-grained controls, if your app is built using React.
+
+```js
+// Vanilla JavaScript Example
+import { selectWhiteboard } from '@100mslive/hms-video-store';
+
+const whiteboard = hmsStore.subscribe((whiteboard) => {
+ if (whiteboard?.open && whiteboard?.url) {
+ const whiteboardIframe = document.createElement('iframe');
+ whiteboardIframe.src = whiteboard.url;
+ } else {
+ const whiteboardIframe = document.getElementById('whiteboard');
+ whiteboardIframe?.remove();
+ }
+}, selectWhiteboard);
+```
+
+```jsx
+// React Example
+import React from 'react';
+import { useWhiteboard } from '@100mslive/react-sdk';
+import { Whiteboard } from '@100mslive/hms-whiteboard';
+import '@100mslive/hms-whiteboard/index.css';
+
+const WhiteboardEmbed = () => {
+ const { token, endpoint } = useWhiteboard();
+
+ if (!token) {
+ return null;
+ }
+
+ return (
+