Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(html5) userCameraDropdown enhancements, user-camera dom-element and screenshareHelper #106

Merged
merged 5 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ So the idea is that we have a `uiCommands` object and at a point, there will be
### Dom Element Manipulation

- `useChatMessageDomElements` hook: This hook will return the dom element of a chat message reactively, so one can modify whatever is inside, such as text, css, js, etc.;
- `useUserCameraDomElements` hook: This hook will return the dom element of each of the user's webcam corresponding to the streamIds passed reactively, so one can modify whatever is inside, such as text, css, js, etc., and also can get the video element within it;

### Learning Analytics Dashboard integration

Expand Down
9 changes: 9 additions & 0 deletions samples/sample-user-camera-dropdown-plugin/src/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const VIDEO_STREAMS_SUBSCRIPTION = `subscription VideoStreams {
user_camera {
streamId
user {
name
userId
}
}
}`;
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,114 @@ import * as React from 'react';
import { useEffect } from 'react';

import {
BbbPluginSdk, PluginApi, pluginLogger, UserCameraDropdownOption, UserCameraDropdownSeparator,
BbbPluginSdk,
PluginApi,
pluginLogger,
ScreenshareHelperItemPosition,
ScreenshareHelperButton,
UserCameraDropdownOption,
UserCameraDropdownSeparator,
} from 'bigbluebutton-html-plugin-sdk';
import { SampleUserCameraDropdownPluginProps } from './types';
import { SampleUserCameraDropdownPluginProps, VideoStreamsSubscriptionResultType } from './types';
import { VIDEO_STREAMS_SUBSCRIPTION } from '../queries';

function SampleUserCameraDropdownPlugin({ pluginUuid: uuid }: SampleUserCameraDropdownPluginProps):
React.ReactElement<SampleUserCameraDropdownPluginProps> {
BbbPluginSdk.initialize(uuid);
const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(uuid);

const { data: videoStreams } = pluginApi.useCustomSubscription<
VideoStreamsSubscriptionResultType
>(VIDEO_STREAMS_SUBSCRIPTION);

const userCamera = pluginApi.useUserCameraDomElements(
videoStreams?.user_camera.map((vs) => vs.streamId),
);

pluginLogger.info(`logging the domElements manipulation for userCamera: (${userCamera}) for streams (${videoStreams})`);

useEffect(() => {
const buttonScreenshare1 = new ScreenshareHelperButton({
icon: 'user',
disabled: false,
label: 'This will log on the console',
tooltip: 'this is a button injected by plugin',
position: ScreenshareHelperItemPosition.TOP_RIGHT,
onClick: () => {
pluginLogger.info('Log from nav bar plugin');
},
hasSeparator: true,
});

const buttonScreenshare5 = new ScreenshareHelperButton({
icon: 'popout_window',
disabled: false,
label: 'This will log on the console',
tooltip: 'this is a button injected by plugin',
position: ScreenshareHelperItemPosition.TOP_RIGHT,
onClick: () => {
pluginLogger.info('Log from nav bar plugin');
},
hasSeparator: true,
});

const buttonScreenshare2 = new ScreenshareHelperButton({
icon: 'undo',
disabled: false,
label: 'This will log on the console',
tooltip: 'this is a button injected by plugin',
position: ScreenshareHelperItemPosition.TOP_LEFT,
onClick: () => {
pluginLogger.info('Log from nav bar plugin');
},
hasSeparator: true,
});

const buttonScreenshare3 = new ScreenshareHelperButton({
icon: 'settings',
disabled: false,
label: 'This will log on the console',
tooltip: 'this is a button injected by plugin',
position: ScreenshareHelperItemPosition.BOTTOM_LEFT,
onClick: () => {
pluginLogger.info('Log from nav bar plugin');
},
hasSeparator: true,
});

const buttonScreenshare4 = new ScreenshareHelperButton({
icon: 'plus',
disabled: false,
label: 'This will log on the console',
tooltip: 'this is a button injected by plugin',
position: ScreenshareHelperItemPosition.BOTTOM_RIGHT,
onClick: () => {
pluginLogger.info('Log from nav bar plugin');
},
hasSeparator: true,
});
pluginApi.setScreenshareHelperItems([buttonScreenshare1, buttonScreenshare5,
buttonScreenshare2, buttonScreenshare3, buttonScreenshare4,
]);
}, []);

useEffect(() => {
const randomElement = videoStreams?.user_camera[
Math.floor(Math.random() * (videoStreams?.user_camera.length ?? 0))
];

pluginApi.setUserCameraDropdownItems([
new UserCameraDropdownSeparator(),
new UserCameraDropdownOption({
label: 'This will log on the console',
icon: 'user',
onClick: () => {
pluginLogger.info('Alert sent from plugin');
displayFunction: ({ userId }) => randomElement?.user.userId === userId,
onClick: ({ userId }) => {
GuiLeme marked this conversation as resolved.
Show resolved Hide resolved
pluginLogger.info(`Alert sent from plugin, see userId: ${userId}`);
},
}),
]);
}, []);
}, [videoStreams]);

return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,14 @@ interface SampleUserCameraDropdownPluginProps {
pluginUuid: string,
}

export { SampleUserCameraDropdownPluginProps };
interface VideoStreamsSubscriptionResultType {
user_camera?: {
streamId: string
user: {
name: string
userId: string
};
}[];
}

export { SampleUserCameraDropdownPluginProps, VideoStreamsSubscriptionResultType };
5 changes: 5 additions & 0 deletions src/core/api/BbbPluginSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { usePluginSettings } from '../../data-consumption/domain/settings';
import { UseLoadedChatMessagesFunction } from '../../data-consumption/domain/chat/loaded-chat-messages/types';
import { useLoadedChatMessages } from '../../data-consumption/domain/chat/loaded-chat-messages/hooks';
import { useChatMessageDomElements } from '../../dom-element-manipulation/chat/message/hooks';
import { useUserCameraDomElements } from '../../dom-element-manipulation/user-camera/hooks';
import { UseTalkingIndicatorFunction } from '../../data-consumption/domain/user-voice/talking-indicator/types';
import { useTalkingIndicator } from '../../data-consumption/domain/user-voice/talking-indicator/hooks';
import { useUiData } from '../../ui-data-hooks/hooks';
Expand Down Expand Up @@ -86,6 +87,9 @@ export abstract class BbbPluginSdk {
pluginApi.useChatMessageDomElements = (
messageIds: string[],
) => useChatMessageDomElements(messageIds);
pluginApi.useUserCameraDomElements = (
streamIds: string[],
) => useUserCameraDomElements(streamIds);
pluginApi.uiCommands = uiCommands;
pluginApi.serverCommands = serverCommands;
pluginApi.useUiData = useUiData;
Expand Down Expand Up @@ -149,6 +153,7 @@ export abstract class BbbPluginSdk {
setAudioSettingsDropdownItems: () => [],
setPresentationDropdownItems: () => [],
setNavBarItems: () => [],
setScreenshareHelperItems: () => [],
setOptionsDropdownItems: () => [],
setCameraSettingsDropdownItems: () => [],
setUserCameraDropdownItems: () => [],
Expand Down
18 changes: 17 additions & 1 deletion src/core/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { UseUiDataFunction } from '../../ui-data-hooks/types';
import { UseMeetingFunction } from '../../data-consumption/domain/meeting/from-core/types';
import { ServerCommands } from '../../server-commands/types';
import { SendGenericDataForLearningAnalyticsDashboard } from '../../learning-analytics-dashboard/types';
import { UseUserCameraDomElementsFunction } from '../../dom-element-manipulation/user-camera/types';
import { ScreenshareHelperInterface } from '../../extensible-areas';

// Setter Functions for the API
export type SetPresentationToolbarItems = (presentationToolbarItem:
Expand All @@ -54,7 +56,11 @@ export type SetPresentationDropdownItems = (
) => string[];

export type SetNavBarItems = (
userListDropdownItem: NavBarInterface[]
navBarItem: NavBarInterface[]
) => string[];

export type SetScreenshareHelperItems = (
screenshareHelperItem: ScreenshareHelperInterface[]
) => string[];

export type SetOptionsDropdownItems = (
Expand Down Expand Up @@ -90,6 +96,7 @@ export interface PluginApi {
setAudioSettingsDropdownItems: SetAudioSettingsDropdownItems;
setPresentationDropdownItems: SetPresentationDropdownItems;
setNavBarItems: SetNavBarItems;
setScreenshareHelperItems: SetScreenshareHelperItems;
setOptionsDropdownItems: SetOptionsDropdownItems;
setCameraSettingsDropdownItems: SetCameraSettingsDropdownItems;
setUserCameraDropdownItems: SetUserCameraDropdownItems;
Expand Down Expand Up @@ -211,6 +218,15 @@ export interface PluginApi {
*
*/
useChatMessageDomElements?: UseChatMessageDomElementsFunction;
/**
* Returns an array with the DOM elements for the webcams corresponding to each streamId passed.
*
* @param streamIds - Ids of the user-camera streams one wants to retrieve in the form of an array
* @returns The array of an object with DOM elements (in this case, div) and the id
* of the stream
*
*/
useUserCameraDomElements?: UseUserCameraDomElementsFunction;
// --- Auxiliary functions ---
getSessionToken?: GetSessionTokenFunction;
getJoinUrl?: GetJoinUrlFunction;
Expand Down
1 change: 1 addition & 0 deletions src/dom-element-manipulation/enums.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export enum DomElementManipulationHooks {
CHAT_MESSAGE = 'CHAT_MESSAGE',
USER_CAMERA = 'USER_CAMERA',
}
4 changes: 3 additions & 1 deletion src/dom-element-manipulation/type.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ChatMessageDomElementsArguments } from './chat/message/types';
import { UserCameraDomElementsArguments } from './user-camera/types';

export type DomElementManipulationArguments = ChatMessageDomElementsArguments;
export type DomElementManipulationArguments = ChatMessageDomElementsArguments
| UserCameraDomElementsArguments;
62 changes: 62 additions & 0 deletions src/dom-element-manipulation/user-camera/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useEffect, useState } from 'react';
import { DomElementManipulationHooks } from '../enums';
import { HookEvents } from '../../core/enum';
import {
HookEventWrapper, SubscribedEventDetails, UnsubscribedEventDetails, UpdatedEventDetails,
} from '../../core/types';
import { UserCameraDomElementsArguments, UpdatedEventDetailsForUserCameraDomElement } from './types';
import { sortedStringify } from '../../data-consumption/utils';

export const useUserCameraDomElements = (streamIds: string[]) => {
const [domElement, setDomElement] = useState<HTMLDivElement[]>();
const [streamIdsState, setStreamIdsState] = useState<string[]>(streamIds);

const handleCustomSubscriptionUpdateEvent: EventListener = (
(event: HookEventWrapper<
UpdatedEventDetails<UpdatedEventDetailsForUserCameraDomElement[]>>) => {
const detail = event.detail as UpdatedEventDetails<
UpdatedEventDetailsForUserCameraDomElement[]>;
if (detail.hook === DomElementManipulationHooks.USER_CAMERA
&& sortedStringify(
detail.data.map((userCamera) => userCamera.streamId),
) === sortedStringify(streamIdsState)) {
setDomElement(detail.data.map((userCamera) => userCamera.userCameraDomElement));
}
}) as EventListener;

useEffect(() => {
if (streamIdsState) {
window.addEventListener(HookEvents.UPDATED, handleCustomSubscriptionUpdateEvent);
return () => {
window.removeEventListener(HookEvents.UPDATED, handleCustomSubscriptionUpdateEvent);
window.dispatchEvent(new CustomEvent<UnsubscribedEventDetails>(HookEvents.UNSUBSCRIBED, {
detail: {
hook: DomElementManipulationHooks.USER_CAMERA,
hookArguments: {
streamIds,
} as UserCameraDomElementsArguments,
},
}));
};
}
return () => {};
}, []);
useEffect(() => {
if (streamIdsState) {
window.removeEventListener(HookEvents.UPDATED, handleCustomSubscriptionUpdateEvent);
window.addEventListener(HookEvents.UPDATED, handleCustomSubscriptionUpdateEvent);
window.dispatchEvent(new CustomEvent<SubscribedEventDetails>(HookEvents.SUBSCRIBED, {
detail: {
hook: DomElementManipulationHooks.USER_CAMERA,
hookArguments: {
streamIds: streamIdsState,
} as UserCameraDomElementsArguments,
},
}));
}
}, [streamIdsState]);
if (sortedStringify(streamIds) !== sortedStringify(streamIdsState)) {
setStreamIdsState(streamIds);
}
return domElement;
};
12 changes: 12 additions & 0 deletions src/dom-element-manipulation/user-camera/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export type UseUserCameraDomElementsFunction = (
streamIds: string[]
) => HTMLDivElement[] | undefined;

export interface UserCameraDomElementsArguments {
streamIds: string[];
}

export interface UpdatedEventDetailsForUserCameraDomElement {
streamId: string;
userCameraDomElement: HTMLDivElement;
}
4 changes: 3 additions & 1 deletion src/extensible-areas/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { NavBarItemType } from './nav-bar-item/enums';
import { OptionsDropdownItemType } from './options-dropdown-item/enums';
import { PresentationDropdownItemType } from './presentation-dropdown-item/enums';
import { PresentationToolbarItemType } from './presentation-toolbar-item/enums';
import { ScreenshareHelperItemType } from './screenshare-helper-item/enums';
import { UserCameraDropdownItemType } from './user-camera-dropdown-item/enums';
import { UserListDropdownItemType } from './user-list-dropdown-item/enums';
import { UserListItemAdditionalInformationType } from './user-list-item-additional-information/enums';
Expand All @@ -17,7 +18,8 @@ type PluginProvidedUiItemType = PresentationToolbarItemType |
ActionsBarItemType | AudioSettingsDropdownItemType |
PresentationDropdownItemType | NavBarItemType | OptionsDropdownItemType |
CameraSettingsDropdownItemType | UserCameraDropdownItemType |
UserListItemAdditionalInformationType | FloatingWindowType | GenericContentType;
UserListItemAdditionalInformationType | FloatingWindowType | GenericContentType |
ScreenshareHelperItemType;

export interface PluginProvidedUiItemDescriptor {
/** Defined by BigBlueButton Plugin Engine. */
Expand Down
1 change: 1 addition & 0 deletions src/extensible-areas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './actions-bar-item';
export * from './audio-settings-dropdown-item';
export * from './presentation-dropdown-item';
export * from './nav-bar-item';
export * from './screenshare-helper-item';
export * from './options-dropdown-item';
export * from './camera-settings-dropdown-item';
export * from './user-camera-dropdown-item';
Expand Down
58 changes: 58 additions & 0 deletions src/extensible-areas/screenshare-helper-item/component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ScreenshareHelperItemType, ScreenshareHelperItemPosition } from './enums';
import {
ScreenshareHelperButtonProps,
ScreenshareHelperButtonInterface,
} from './types';

// ScreenshareHelper Extensible Area

export class ScreenshareHelperButton implements ScreenshareHelperButtonInterface {
id: string = '';

type: ScreenshareHelperItemType;

label: string;

icon: string;

tooltip: string;

disabled: boolean;

position: ScreenshareHelperItemPosition;

onClick: () => void;

/**
* Returns object to be used in the setter for the Screenshare Helper. In this case,
* a button.
*
* @param label - label to be displayed in screenshare helper button (Not mandatory).
* @param tooltip - label to be displayed when hovering the screenshare helper button.
* @param icon - icon to be used in the screenshare helper button. It goes in the left side of it.
* @param onClick - function to be called when clicking the button.
* @param position - position to place the screenshare helper button.
* See {@link ScreenshareHelperItemPosition}
* @param hasSeparator - boolean indicating whether the screenshare helper button has separator
* (vertical bar)
* @param disabled - if true, the screenshare helper button will not be clickable
*
* @returns Object that will be interpreted by the core of Bigbluebutton (HTML5).
*/
constructor({
label = '', icon = '', tooltip = '', disabled = true, onClick = () => {},
position = ScreenshareHelperItemPosition.TOP_RIGHT,
}: ScreenshareHelperButtonProps) {
this.label = label;
this.icon = icon;
this.tooltip = tooltip;
this.disabled = disabled;
this.onClick = onClick;
this.type = ScreenshareHelperItemType.BUTTON;
this.position = position;
}

setItemId: (id: string) => void = (id: string) => {
this.id = `ScreenshareHelperButton_${id}`;
};
}
Loading