Embed a custom plugin

Updated

Develop a custom plugin, such as a countdown plugin or a dice, and embed the plugin in the flexible classroom.

In online classroom applications, some customization is often necessary to meet the needs of a particular use case. Agora provides Widgets to help users develop plugins according to their specific needs and embed them into Flexible Classroom.

Electron

In online classroom applications, some customization is often necessary to meet the needs of a particular use-case. Agora provides Widgets to help users develop plug-ins according to their specific needs and embed them into Flexible Classroom.

Widgets are stand-alone plugins that contain interface and functionality. Developers can implement a widget based on customization of the base class, and then register the widget in the Agora Classroom SDK. Agora Classroom SDK supports registering multiple widgets. Widgets can communicate with other widgets, as well as with other plugins in the UI layer.

This document applies to Flexible Classroom version 2.8.0 or later.

Understand the tech

Flexible Classroom provides a plugin mechanism to help developers expand classroom capabilities under various use-cases. This mechanism also reduces the degree of code coupling between the custom application and the classroom, thereby reducing the difficulty of subsequent upgrading of the integrated source code. See Plugin technology principles.

Built-in plugins

This section introduces the built-in plugins of Flexible Classroom. They extend classroom capabilities and fall into two main categories:

  • Common class plugins, for example:

    • Interactive whiteboard
    • Agora Chat
    • Embedded browser
    • Video sync player (currently supports Youtube videos only)
  • Teaching-aid plugins, for example:

    • Clicker
    • Voter
    • Timer

The source codes of the built-in plugins is located in the apaas-widgets-web repository.

Plugin nameSource code folder
Interactive Whiteboard (adapted to AgoraEduSDK)classroom/whiteboard
IM module (adapted to AgoraEduSDK)classroom/im
Embedded browser (adapted to AgoraEduSDK)classroom/webview
Video synchronization player (adapted to AgoraEduSDK)classroom/stream-media
Clicker (adapted to AgoraEduSDK)classroom/answer
Voting device (adapted to AgoraEduSDK)classroom/vote
Timer (Adapted to AgoraEduSDK)classroom/counter
Watermark plug-in (adapted to AgoraEduSDK)classroom/watermark
Embedded browser (adapted to AgoraProctorSDK)proctor/webview
Chat room (adapted to FcrUIScene)scene/chatroom
Timer (adapted to FcrUIScene)scene/countdown
Voter (adapted to FcrUIScene)scene/polling
Clicker (adapted to FcrUIScene)scene/quiz
Video sync player (adapted to FcrUIScene)scene/stream-media
Embedded browser (adapted to FcrUIScene)scene/webview
Interactive whiteboard (adapted to FcrUIScene)scene/whiteboard

Custom plugins

If you need to add a custom plugin or a widget, you can inherit the class provided by Flexible Classroom AgoraWidgetBase and implement its abstract methods.

The AgoraWidgetBase class

The AgoraWidgetBase class provides the widget-related operation API. It is defined as follows:

export declare abstract class AgoraWidgetBase implements AgoraWidgetRenderable, AgoraMultiInstanceWidget, AgoraWidgetLifecycle {
  constructor(_widgetController: AgoraWidgetController, _classroomStore: EduClassroomStore);

  onInstall(controller: AgoraWidgetController): void {}
  onCreate(properties: any, userProperties: any): void {}
  onPropertiesUpdate(properties: any): void {}
  onUserPropertiesUpdate(userProperties: any): void {}
  onDestroy(): void {}
  onUninstall(controller: AgoraWidgetController): void {}

  locate(): HTMLElement | undefined | null;
  render(dom: HTMLElement): void;
  unload(): void;

  setInstanceId(instId: string): void;
  get instanceId(): string;

  // Widget Name
  abstract get widgetName(): string;

  // Unique widget ID
  get widgetId(): string;

  // Container level
  get zContainer(): 0 | 10;

  // Track synchronization controller
  get trackController(): AgoraWidgetTrackController | undefined;

  // Widget Controller
  get widgetController(): AgoraWidgetController;

  // Classroom Store
  get classroomStore(): EduClassroomStore;

  // Share UIStore
  get shareUIStore(): EduShareUIStore;

  // Classroom configuration
  get classroomConfig(): import("agora-edu-core").EduClassroomConfig;

  // UI configuration
  get uiConfig(): FcrUIConfig;

  // Theme
  get theme(): FcrTheme;

  // Broadcast message
  broadcast(messageType: string, message: unknown): void;

  // Add a broadcast listener
  addBroadcastListener(listener: Omit<AgoraWidgetMessageListener, 'widgetId'>): void;

  // Remove broadcast listener
  removeBroadcastListener(listener: Omit<AgoraWidgetMessageListener, 'widgetId'>): void;

  // Update widget properties
  updateWidgetProperties(properties: any): Promise<{
    data: any;
  }>;

  // Update widget User Properties
  updateWidgetUserProperties(userProperties: any): void;

  // Delete the widget
  deleteWidget(): Promise<{
    data: any;
  }>;

  // Delete widget User Properties
  removeWidgetUserProperties(keys: string[]): Promise<{
    data: any;
  }>;

  // Remove widget Extended Properties
  removeWidgetExtraProperties(keys: string[]): Promise<{
    data: any;
  }>;

  // Set widget as active
  setActive(props?: any): void;

  // Set the widget to the inactive state
  setInactive(props?: any): void;

  // Get the latest component Z-index level
  get latestZIndex(): number;
}

Using the AgoraWidgetBase class

The following code shows the definition of an ExampleWidget to implement a basic widget:

import { AgoraWidgetBase } from "agora-common-libs";

export class ExampleWidget extends AgoraWidgetBase {
  private _dom?: HTMLElement
  // Globally unique widget name
  get widgetName(): string {
    return 'example'

  // Mount point
  // Rewrite the locate method to return a node, the widget will be rendered inside this node
  // ExampleWidget is mounted to the whiteboard area
  locate(): HTMLElement | null | undefined {
    return document.querySelector(".widget-slot-board") as HTMLElement;

  // Widget node is mounted
  // At this point, custom rendering can be performed on the DOM node
  render(dom: HTMLElement): void {
    dom.innerHTML = 'This is a custom widget';
    dom.style.height = '100%';
    dom.style.display = "flex";
    dom.style.alignItems = "center";
    dom.style.justifyContent = "center";
    this._dom = dom;

  // Uninstall components
  // Relevant resources can be released here
  unload(): void {
    this._dom = undefined;
  }
}

Pass in the custom widget in launch

Use the following code to pass in your custom widget through the widgets parameter in launch:

const widgets = {
  // Need to import the ExampleWidget class defined above
  'example': ExampleWidget
};

AgoraEduSDK.launch(dom, {
  ...
  widgets: widgets
  ...
});

Create the custom widget on install

Since ExampleWidget is a custom widget, you need to manually call the createWidget method to create a widget:

// Modify the WidgetUIStore code file path packages/agora-classroom-sdk/src/infra/stores/common/widget/index.ts to add code
onInstall() {
  ...
  // Add this code at the end of the onInstall method to open the specified widget after the room is successfully added
  this._disposers.push(
    reaction(
      () => this.classroomStore.widgetStore.widgetController,
      () => {
        // Open our new widget, pass in widgetName here
        this.createWidget('example');
      },
    ),
  );
}

Test your implementation

Start the classroom, the effect after the plug-in is mounted is as follows:

Create a teaching-aid plugin

Flexible Classroom provides developers with an abstract class called AgoraEduToolWidget. The class implements the encapsulation of the general capabilities of teaching-aid plugins. Inheriting this class gives the plugin track synchronization, hierarchical control capabilities, and control logic for the display of UI windows.

With the ControlledModal component, you can also quickly implement the outer window of the built-in teaching-aid plugin, reducing a lot of general logic code development. For specific implementation, please refer to the answer, vote and counter folders in the Github repository, which provide the source code of the built-in teaching-aid plugins in Flexible Classroom.

The following code shows the AgoraEduToolWidget class definition:

// Use the AgoraEduToolWidget abstract class as the base class to implement draggable and track-synchronized widgets

export abstract class AgoraEduToolWidget
  extends AgoraWidgetBase
  implements AgoraWidgetLifecycle, AgoraTrackSyncedWidget
{
  private _controlStateCallbacks: CallableFunction[] = [];
  onUninstall(controller: AgoraWidgetController) {}
  onInstall(controller: AgoraWidgetController) {}
  onCreate(properties: any, userProperties: any): void {}
  onPropertiesUpdate(properties: any): void {}
  onUserPropertiesUpdate(userProperties: any): void {}
  onDestroy(): void {}
  get track(): Track {
    return this.trackController?.track!;
  }
  get zIndex(): number {
    return this.trackController?.zIndex || 0;
  }
  @bound
  updateZIndexToRemote(zIndex: number) {
    this.trackController?.updateRemoteZIndex(zIndex);
    this.widgetController.zIndexController.setZIndex(zIndex);
  }
  @bound
  updateZIndexToLocal(zIndex: number) {
    this.trackController?.updateLocalZIndex(zIndex);
    this.widgetController.zIndexController.setZIndex(zIndex);
  }
  get draggable(): boolean {
    return true;
  }
  get resizable(): boolean {
    return false;
  }
  get dragHandleClassName(): string {
    return 'modal-title';
  }
  get dragCancelClassName(): string {
    return 'modal-title-close';
  }
  get boundaryClassName(): string {
    return 'widget-slot-board';
  }
  get minWidth(): number {
    return 0;
  }
  get minHeight(): number {
    return 0;
  }
  get trackMode(): AgoraWidgetTrackMode {
    return AgoraWidgetTrackMode.TrackPositionOnly;
  }

  @bound
  updateToRemote(
    end: boolean,
    pos: Point,
    dimensions?: Dimensions | undefined,
    options?: TrackOptions | undefined,
  ): void {
    this.trackController?.updateRemoteTrack(end, pos, dimensions, options);
  }

  @bound
  updateToLocal(trackProps: AgoraWidgetTrack): void {
    this.trackController?.updateLocalTrack(trackProps);
  }

  @bound
  handleResize({ width, height }: { width: number; height: number }) {
    this.track.setRealDimensions({ width, height });
    this.track.reposition(false);
  }

  @bound
  handleClose() {
    this.widgetController.broadcast(AgoraExtensionWidgetEvent.WidgetBecomeInactive, this.widgetId);

    this.deleteWidget();
  }

  @bound
  setVisibility(visible: boolean) {
    this.track.setVisibility(visible);
  }

  get controlled() {
    return this.hasPrivilege;
  }

  addControlStateListener(cb: (controlled: boolean) => void) {
    this._controlStateCallbacks.push(cb);
  }

  removeControlStateListener(cb: (controlled: boolean) => void) {
    this._controlStateCallbacks = this._controlStateCallbacks.filter((c) => c !== cb);
  }

  fireControlStateChanged() {
    const controled = this.controlled;
    this._controlStateCallbacks.forEach((cb) => {
      cb(controled);
    });
  }
}

Using AgoraEduToolWidget

The following code shows how to create a teaching-aid plugin for pre-class roll calls based on the existing widget capabilities of Flexible Classroom. See the full source code in rollbook-widget.

// Create the RollbookWidget class based on the AgoraEduToolWidget class
// packages/agora-plugin-gallery/src/gallery/rollbook/index.tsx
import { render, unmountComponentAtNode } from 'react-dom';
import { App } from './app';
import { AgoraEduToolWidget } from '../../common/edu-tool-widget';
import { observable, action, computed } from 'mobx';
import type { AgoraWidgetController } from 'agora-edu-core';
import { bound } from 'agora-common-libs';
import { AgoraExtensionWidgetEvent } from '../../events';
import { SvgIconEnum } from '../../components/svg-img';

// Roll call:
// Teachers can use this plugin to know the participation of students in the classroom
export class RollbookWidget extends AgoraEduToolWidget {
  private _dom?: HTMLElement;
  @observable
  started = false;
  @observable
  checkInList: string[] = [];
  // Have you checked in?
  @computed
  get isCheckedIn() {
    const { userUuid } = this.classroomConfig.sessionInfo;
    return this.checkInList.includes(userUuid);
  }
  // Check-in username list
  @computed
  get checkInUserNames() {
    return this.checkInList.map((userUuid) => {
      // Get the username corresponding to the user ID from the UserStore (this method does not apply to large classes)
      const user = this.classroomStore.userStore.studentList.get(userUuid);
      return user?.userName || 'Unknown';
    });
  }
  // Initial window width
  get minWidth(): number {
    return 400;
  }
  // Initial window height
  get minHeight(): number {
    return 200;
  }
  // Globally unique widget name
  get widgetName(): string {
     return 'rollbook';
  }
  // Control whether the Widget is controllable
  get hasPrivilege(): boolean {
     return [1, 3].includes(this.classroomConfig.sessionInfo.role);
  }
  get checkInPropKey() {
    const { userUuid } = this.classroomConfig.sessionInfo;
    const key = `checkIn-${userUuid}`;
    return key;
  }

   // The widget node has been mounted, and you can now perform custom rendering on the DOM node.
   render(dom: HTMLElement): void {
     this._dom = dom;
     dom.style.width = '100%';
     dom.style.height = '100%';
     // Rendering UI components using React
     render(<App widget={this} />, dom);
   }

   // Uninstall the component and release related resources at this time
   unload(): void {
     if (this._dom) {
       // Uninstall React components
       unmountComponentAtNode(this._dom);
     }
     this._dom = undefined;
   }

  onCreate(properties: any, userProperties: any): void {
    this._handlePropertiesChange(properties);
  }

  onPropertiesUpdate(properties: any): void {
    this._handlePropertiesChange(properties);
  }

  onInstall(controller: AgoraWidgetController) {
    // Register the plug-in entry to the toolbox
    controller.broadcast(AgoraExtensionWidgetEvent.RegisterCabinetTool, {
      id: this.widgetName,
      name: 'Rollbook',
      iconType: SvgIconEnum.ANSWER,
    });
  }

  onUninstall(controller: AgoraWidgetController) {
    // Remove the plugin entry from the toolbox
    controller.broadcast(AgoraExtensionWidgetEvent.UnregisterCabinetTool, this.widgetName);
  }

 @action
  private _handlePropertiesChange(properties: any) {
    const list: string[] = [];
    Object.keys(properties.extra || {}).forEach((k) => {
      if (k.startsWith('checkIn-')) {
        const userUuid = k.replace('checkIn-', '');
        list.push(userUuid);
      }
    });
    this.checkInList = list;
    this.started = !!properties.extra?.started;
  }

  // Students click to sign in to update the sign-in list
  @bound
  checkIn() {
    this.updateWidgetProperties({
      extra: {
        // Widgets can be updated incrementally using the Key-Value method
        [this.checkInPropKey]: true,
      },
    });
  }

  // The teacher clicks to start signing in and updates the Widget status
  @bound
  startCheckIn() {
    this.setActive({ extra: { started: 1 } });
  }
}

Add rendering components

Add an App component for rendering in the plugin window:

// packages/agora-plugin-gallery/src/gallery/rollbook/app.tsx
import React, { FC } from 'react';
import { observer } from 'mobx-react';
import { ControlledModal } from '../../common/edu-tool-modal';
import { EduRoleTypeEnum } from 'agora-edu-core';
import { RollbookWidget } from '.';
import { Button } from '../../components/button';

// Roll roster component

  const view = () =>
    [EduRoleTypeEnum.teacher, EduRoleTypeEnum.assistant].includes(
      widget.classroomConfig.sessionInfo.role,
    ) ? (
      <TeacherView widget={widget} />
    ) : (
      <StudentView widget={widget} />
    );
  return (
    <ControlledModal
      onFullScreen={() => {}}
      canRefresh={false}
      widget={widget}
      title="Rollbook"
      onCancel={widget.handleClose}>
      {view()}
    </ControlledModal>
  );
};

// The teacher interface shows the actual check-in list

  const started = widget.started;
  return (
    <div>
      {started ? (
        <React.Fragment>
          <div>Check-In List:</div>
          <ul>
            {widget.checkInUserNames.map((item, i) => (
              <li key={i.toString()}>{item}</li>
            ))}
          </ul>
        </React.Fragment>
      ) : (
        <Button onClick={widget.startCheckIn}>Start Check-In</Button>
      )}
    </div>
  );
});

// StudentView showing check in button

  const isCheckedIn = widget.isCheckedIn;
   return (
     <div>
       <Button onClick={widget.checkIn} disabled={isCheckedIn}>
         {isCheckedIn ? 'Checked-In' : 'Check-In'}
       </Button>
     </div>
   );
});

Pass in the custom widget in launch

Use the following code to pass in your custom widget through the widgets parameter in launch:

const widgets = {
    // Need to import the RollbookWidget class defined above
    'rollbook': RollbookWidget
};

AgoraEduSDK.launch(dom, {
...
widgets: widgets
...
});

Test your implementation

Start the classroom, you see this plugin in the toolbox as shown in the figure:

The teacher clicks and opens the interface effect of the plugin:

After the teacher clicks Start Check-In, the effect of the plugin is displayed on the student side:

After the student clicks Check-In, the teacher's check-in list displays the student's effect: