From bcd59ca9d8632106360c7a59ec6b3e00cc8016f1 Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 28 Oct 2024 15:40:02 -0300 Subject: [PATCH 1/5] feat: initialize video by video meeting class --- packages/sdk/src/common/types/cdn.types.ts | 11 +- packages/sdk/src/components/index.ts | 1 + .../components/video-meeting/index.test.ts | 62 ++++++++ .../sdk/src/components/video-meeting/index.ts | 150 ++++++++++++++++++ packages/sdk/src/index.ts | 42 ++--- 5 files changed, 243 insertions(+), 23 deletions(-) create mode 100644 packages/sdk/src/components/video-meeting/index.test.ts create mode 100644 packages/sdk/src/components/video-meeting/index.ts diff --git a/packages/sdk/src/common/types/cdn.types.ts b/packages/sdk/src/common/types/cdn.types.ts index 5fbf41a..2f63433 100644 --- a/packages/sdk/src/common/types/cdn.types.ts +++ b/packages/sdk/src/common/types/cdn.types.ts @@ -1,3 +1,5 @@ +import { PresenceEvents } from '@superviz/socket-client'; + import type { CanvasPin, HTMLPin, @@ -7,7 +9,9 @@ import type { VideoConference, WhoIsOnline, FormElements, + VideoMeeting, } from '../../components'; +import { FieldEvents } from '../../components/form-elements/types'; import type { RealtimeComponentEvent, RealtimeComponentState, @@ -18,6 +22,7 @@ import type { LayoutMode, LayoutPosition, } from '../../services/video-conference-manager/types'; +import { PinMode } from '../../web-components/comments/components/types'; import type { DeviceEvent, @@ -34,10 +39,7 @@ import type { } from './events.types'; import { ParticipantType } from './participant.types'; import { SuperVizSdkOptions } from './sdk-options.types'; -import { StoreType } from '../types/stores.types'; -import { PresenceEvents } from '@superviz/socket-client'; -import { FieldEvents } from '../../components/form-elements/types'; -import { PinMode } from '../../web-components/comments/components/types'; +import { StoreType } from './stores.types'; export interface SuperVizCdn { init: (apiKey: string, options: SuperVizSdkOptions) => Promise; @@ -57,6 +59,7 @@ export interface SuperVizCdn { LayoutPosition: typeof LayoutPosition; CamerasPosition: typeof CamerasPosition; VideoConference: typeof VideoConference; + VideoMeeting: typeof VideoMeeting; MousePointers: typeof MousePointers; Realtime: typeof Realtime; Comments: typeof Comments; diff --git a/packages/sdk/src/components/index.ts b/packages/sdk/src/components/index.ts index 1cf6be1..eba82d7 100644 --- a/packages/sdk/src/components/index.ts +++ b/packages/sdk/src/components/index.ts @@ -6,3 +6,4 @@ export { MousePointers } from './presence-mouse'; export { Realtime } from './realtime'; export { WhoIsOnline } from './who-is-online'; export { FormElements } from './form-elements'; +export { VideoMeeting } from './video-meeting'; diff --git a/packages/sdk/src/components/video-meeting/index.test.ts b/packages/sdk/src/components/video-meeting/index.test.ts new file mode 100644 index 0000000..9c33681 --- /dev/null +++ b/packages/sdk/src/components/video-meeting/index.test.ts @@ -0,0 +1,62 @@ +import { MOCK_CONFIG } from '../../../__mocks__/config.mock'; +import { EVENT_BUS_MOCK } from '../../../__mocks__/event-bus.mock'; +import { LIMITS_MOCK } from '../../../__mocks__/limits.mock'; +import { MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; +import { MeetingEvent } from '../../common/types/events.types'; +import { useStore } from '../../common/utils/use-store'; +import { IOC } from '../../services/io'; +import { Presence3DManager } from '../../services/presence-3d-manager'; +import { VideoFrameState } from '../../services/video-conference-manager/types'; + +import { VideoMeeting } from '.'; + +describe('Video Meeting', () => { + let VideoMeetingInstance: VideoMeeting; + + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + + VideoMeetingInstance = new VideoMeeting(); + + VideoMeetingInstance.attach({ + ioc: new IOC(MOCK_LOCAL_PARTICIPANT), + config: MOCK_CONFIG, + eventBus: EVENT_BUS_MOCK, + Presence3DManagerService: Presence3DManager, + connectionLimit: LIMITS_MOCK.videoConference.maxParticipants, + useStore, + }); + + VideoMeetingInstance['start'](); + VideoMeetingInstance['startVideoConferenceManager'](); + VideoMeetingInstance['videoManager']['onFrameLoad'](); + VideoMeetingInstance['onFrameStateChange'](VideoFrameState.INITIALIZED); + }); + + test('should initialize the services', () => { + expect(VideoMeetingInstance['browserService']).not.toBe(undefined); + expect(VideoMeetingInstance['connectionService']).not.toBe(undefined); + expect(VideoMeetingInstance['videoManager']).not.toBe(undefined); + }); + + test('should destroy all services', () => { + const spyPublish = jest.spyOn(VideoMeetingInstance, 'publish' as any); + const spyUnsubscribeToVideoUpdates = jest.spyOn(VideoMeetingInstance, 'unsubscribeToVideoUpdates' as any); + const spyUnsubscribeToStoreUpdates = jest.spyOn(VideoMeetingInstance, 'unsubscribeToStoreUpdates' as any); + const spyUnsusbscribeToRealtimeUpdates = jest.spyOn(VideoMeetingInstance, 'unsusbscribeToRealtimeUpdates' as any); + + const spyVideoManagerLeave = jest.spyOn(VideoMeetingInstance['videoManager'], 'leave'); + + VideoMeetingInstance['destroy'](); + + expect(VideoMeetingInstance['browserService']).toBe(undefined); + expect(VideoMeetingInstance['connectionService']).toBe(undefined); + expect(VideoMeetingInstance['videoManager']).toBe(undefined); + expect(spyVideoManagerLeave).toHaveBeenCalled(); + expect(spyUnsubscribeToVideoUpdates).toHaveBeenCalled(); + expect(spyUnsubscribeToStoreUpdates).toHaveBeenCalled(); + expect(spyUnsusbscribeToRealtimeUpdates).toHaveBeenCalled(); + expect(spyPublish).toHaveBeenCalledWith(MeetingEvent.DESTROY); + }); +}); diff --git a/packages/sdk/src/components/video-meeting/index.ts b/packages/sdk/src/components/video-meeting/index.ts new file mode 100644 index 0000000..6c74ada --- /dev/null +++ b/packages/sdk/src/components/video-meeting/index.ts @@ -0,0 +1,150 @@ +import { MeetingEvent } from '../../common/types/events.types'; +import { ParticipantType, VideoParticipant } from '../../common/types/participant.types'; +import { StoreType } from '../../common/types/stores.types'; +import { Logger } from '../../common/utils'; +import { BrowserService } from '../../services/browser'; +import config from '../../services/config'; +import { ConnectionService } from '../../services/connection-status'; +import VideoConfereceManager from '../../services/video-conference-manager'; +import { CamerasPosition, LayoutMode, LayoutPosition, VideoFrameState, VideoManagerOptions } from '../../services/video-conference-manager/types'; +import { BaseComponent } from '../base'; +import { ComponentNames } from '../types'; + +export class VideoMeeting extends BaseComponent { + public name: ComponentNames; + protected logger: Logger; + + // services + private videoManager: VideoConfereceManager; + private connectionService: ConnectionService; + private browserService: BrowserService; + + // data + private localParticipant: VideoParticipant; + + constructor() { + super(); + + this.name = ComponentNames.VIDEO_CONFERENCE; + this.logger = new Logger(`@superviz/sdk/${ComponentNames.VIDEO_CONFERENCE}`); + + this.browserService = new BrowserService(); + this.connectionService = new ConnectionService(); + this.connectionService.addListeners(); + } + + protected start() { + this.subscribetToStoreUpdates(); + this.subscribeToRealtimeUpdates(); + + this.startVideoConferenceManager(); + } + + protected destroy(): void { + this.logger.log('video meeting @ destroy'); + + this.unsubscribeToStoreUpdates(); + this.unsusbscribeToRealtimeUpdates(); + this.unsubscribeToVideoUpdates(); + + this.browserService = undefined; + + this.videoManager?.leave(); + this.videoManager = undefined; + + this.connectionService?.removeListeners(); + this.connectionService = undefined; + + this.publish(MeetingEvent.DESTROY); + } + + private startVideoConferenceManager() { + const options: VideoManagerOptions = { + canUseChat: true, + canUseCams: true, + canShowAudienceList: true, + canUseRecording: true, + canUseScreenshare: true, + canUseDefaultAvatars: false, + canUseGather: true, + canUseFollow: true, + canUseGoTo: true, + canUseDefaultToolbar: true, + collaborationMode: false, + skipMeetingSettings: false, + camerasPosition: CamerasPosition.LEFT, + waterMark: config.get('waterMark'), + layoutPosition: LayoutPosition.CENTER, + layoutMode: LayoutMode.GRID, + devices: { audioInput: true, audioOutput: true, videoInput: true }, + browserService: this.browserService, + locales: [], + avatars: [], + styles: undefined, + offset: undefined, + callbacks: undefined, + }; + + this.videoManager = new VideoConfereceManager(options); + + this.subscribeToVideoUpdates(); + } + + private subscribeToVideoUpdates() { + this.logger.log('video conference @ subscribe to video events'); + this.videoManager.meetingConnectionObserver.subscribe( + this.connectionService.updateMeetingConnectionStatus, + ); + this.videoManager.participantListObserver.subscribe((data) => { console.log(data); }); + this.videoManager.waitingForHostObserver.subscribe((data) => { console.log(data); }); + this.videoManager.frameSizeObserver.subscribe((data) => { console.log(data); }); + this.videoManager.frameStateObserver.subscribe(this.onFrameStateChange); + this.videoManager.meetingStateObserver.subscribe((data) => { console.log(data); }); + this.videoManager.realtimeEventsObserver.subscribe((data) => { console.log(data); }); + this.videoManager.participantJoinedObserver.subscribe((data) => { console.log(data); }); + this.videoManager.participantLeftObserver.subscribe((data) => { console.log(data); }); + this.videoManager.sameAccountErrorObserver.subscribe((data) => { console.log(data); }); + this.videoManager.devicesObserver.subscribe((data) => { console.log(data); }); + } + + private unsubscribeToVideoUpdates() {} + + private subscribeToRealtimeUpdates() {} + private unsusbscribeToRealtimeUpdates() {} + + private subscribetToStoreUpdates() { + const { localParticipant } = this.useStore(StoreType.GLOBAL); + + localParticipant.subscribe((participant) => { + this.localParticipant = { + ...this.localParticipant, + ...participant, + type: 'host', + }; + }); + } + + private unsubscribeToStoreUpdates() {} + + // Video Listeners + + /** + * @function onFrameStateChange + * @description handler for frame state change event + * @param {VideoFrameState} state - frame state + * @returns + */ + private onFrameStateChange = (state: VideoFrameState): void => { + this.logger.log('video conference @ on frame state change', state); + + if (state !== VideoFrameState.INITIALIZED) return; + + this.videoManager.start({ + group: this.group, + participant: this.localParticipant, + roomId: config.get('roomId'), + }); + + this.publish(MeetingEvent.MEETING_START); + }; +} diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index a06d913..469ac53 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -3,6 +3,8 @@ import './web-components'; import './common/styles/global.css'; // #region enums +import { PresenceEvents } from '@superviz/socket-client'; + import { MeetingEvent, RealtimeEvent, @@ -16,19 +18,14 @@ import { ComponentLifeCycleEvent, WhoIsOnlineEvent, } from './common/types/events.types'; -import { - CamerasPosition, - LayoutMode, - LayoutPosition, -} from './services/video-conference-manager/types'; import { ParticipantType } from './common/types/participant.types'; -import { RealtimeComponentEvent, RealtimeComponentState } from './components/realtime/types'; -import { StoreType } from './common/types/stores.types'; -import { PresenceEvents } from '@superviz/socket-client'; -import { FieldEvents } from './components/form-elements/types'; -import { PinMode } from './web-components/comments/components/types'; // #region Classes + +// #region Types and Interfaces +import type { Participant, Group, Avatar } from './common/types/participant.types'; +import type { SuperVizSdkOptions, DevicesOptions } from './common/types/sdk-options.types'; +import { StoreType } from './common/types/stores.types'; import { VideoConference, MousePointers, @@ -38,16 +35,8 @@ import { HTMLPin, WhoIsOnline, FormElements, + VideoMeeting, } from './components'; -import type { Channel } from './components/realtime/channel'; -import type { Presence3DManager } from './services/presence-3d-manager'; - -// #region Types and Interfaces -import type { RealtimeMessage } from './components/realtime/types'; -import type { Participant, Group, Avatar } from './common/types/participant.types'; -import type { SuperVizSdkOptions, DevicesOptions } from './common/types/sdk-options.types'; -import type { BrowserStats } from './services/browser/types'; -import type { LauncherFacade } from './core/launcher/types'; import type { Annotation, Comment, @@ -56,7 +45,20 @@ import type { AnnotationPositionInfo, Offset, } from './components/comments/types'; +import { FieldEvents } from './components/form-elements/types'; import type { Transform } from './components/presence-mouse/types'; +import type { Channel } from './components/realtime/channel'; +import type { RealtimeMessage } from './components/realtime/types'; +import { RealtimeComponentEvent, RealtimeComponentState } from './components/realtime/types'; +import type { LauncherFacade } from './core/launcher/types'; +import type { BrowserStats } from './services/browser/types'; +import type { Presence3DManager } from './services/presence-3d-manager'; +import { + CamerasPosition, + LayoutMode, + LayoutPosition, +} from './services/video-conference-manager/types'; +import { PinMode } from './web-components/comments/components/types'; if (typeof window !== 'undefined') { window.SuperVizRoom = { @@ -79,6 +81,7 @@ if (typeof window !== 'undefined') { HTMLPin, WhoIsOnline, FormElements, + VideoMeeting, ParticipantType, LayoutPosition, CamerasPosition, @@ -120,6 +123,7 @@ export { MousePointers, WhoIsOnline, VideoConference, + VideoMeeting, Realtime, RealtimeMessage, Channel, From baf1e73f329c8690653154f3d1daec11558824f0 Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 28 Oct 2024 15:40:19 -0300 Subject: [PATCH 2/5] chore(playground): initialize video meeting --- apps/playground/src/lib/sdk/index.ts | 1 + apps/playground/src/pages/video-meeting.tsx | 66 +++++++++++++++++++++ apps/playground/src/router/router.tsx | 5 ++ 3 files changed, 72 insertions(+) create mode 100644 apps/playground/src/pages/video-meeting.tsx diff --git a/apps/playground/src/lib/sdk/index.ts b/apps/playground/src/lib/sdk/index.ts index 0ec9e30..aa56f73 100644 --- a/apps/playground/src/lib/sdk/index.ts +++ b/apps/playground/src/lib/sdk/index.ts @@ -1,6 +1,7 @@ export { default as Room, VideoConference, + VideoMeeting, WhoIsOnline, FormElements, Comments, diff --git a/apps/playground/src/pages/video-meeting.tsx b/apps/playground/src/pages/video-meeting.tsx new file mode 100644 index 0000000..3c2729b --- /dev/null +++ b/apps/playground/src/pages/video-meeting.tsx @@ -0,0 +1,66 @@ +import { + LauncherFacade, + ParticipantType, + Room, + VideoMeeting, +} from "../lib/sdk"; +import { v4 as generateId } from "uuid"; + +import { useCallback, useEffect, useRef } from "react"; +import { getConfig } from "../config"; +import { useSearchParams } from "react-router-dom"; + +const SUPERVIZ_KEY = getConfig("keys.superviz"); +const SUPERVIZ_ROOM_PREFIX = getConfig("roomPrefix"); + +const componentName = "video-conference"; + +export function VideoMeetingPage() { + const room = useRef(); + const loaded = useRef(false); + const video = useRef(); + const [searchParams] = useSearchParams(); + + const initializeSuperViz = useCallback(async () => { + const uuid = generateId(); + const type = + (searchParams.get("userType") as ParticipantType) || ParticipantType.HOST; + + room.current = await Room(SUPERVIZ_KEY, { + roomId: `${SUPERVIZ_ROOM_PREFIX}-${componentName}`, + participant: { + name: "Participant " + type, + id: uuid, + }, + group: { + name: SUPERVIZ_ROOM_PREFIX, + id: SUPERVIZ_ROOM_PREFIX, + }, + environment: "dev", + debug: true, + }); + + video.current = new VideoMeeting(); + + room.current.addComponent(video.current); + }, []); + + useEffect(() => { + if (loaded.current) return; + loaded.current = true; + initializeSuperViz(); + + return () => { + room.current?.removeComponent(video.current); + room.current?.destroy(); + }; + }, []); + + const initAgain = () => { + video.current = new VideoMeeting(); + + room.current!.addComponent(video.current); + }; + + return ; +} diff --git a/apps/playground/src/router/router.tsx b/apps/playground/src/router/router.tsx index 7714be3..3633b48 100644 --- a/apps/playground/src/router/router.tsx +++ b/apps/playground/src/router/router.tsx @@ -19,6 +19,7 @@ import { YjsMonacoWio } from "../pages/yjs-monaco-wio.tsx"; import { YjsQuillWio } from "../pages/yjs-quill-wio.tsx"; import { YjsQuillReact } from "../pages/yjs-quill-react.tsx"; import { ReactFlowWithReactSDK } from "../pages/react-flow-with-react-sdk.tsx"; +import { VideoMeetingPage } from "../pages/video-meeting.tsx"; export const routeList: RouteObject[] = [ { @@ -32,6 +33,10 @@ export const routeList: RouteObject[] = [ path: "video", element: