Spaces:
Paused
Paused
| /** | |
| * Copyright (c) Meta Platforms, Inc. and affiliates. | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| import {EffectIndex, Effects} from '@/common/components/video/effects/Effects'; | |
| import {registerSerializableConstructors} from '@/common/error/ErrorSerializationUtils'; | |
| import { | |
| BaseTracklet, | |
| SegmentationPoint, | |
| StreamingState, | |
| } from '@/common/tracker/Tracker'; | |
| import { | |
| AbortStreamMasksRequest, | |
| AddPointsResponse, | |
| ClearPointsInFrameRequest, | |
| ClearPointsInVideoRequest, | |
| ClearPointsInVideoResponse, | |
| CloseSessionRequest, | |
| CreateTrackletRequest, | |
| DeleteTrackletRequest, | |
| InitializeTrackerRequest, | |
| LogAnnotationsRequest, | |
| SessionStartFailedResponse, | |
| SessionStartedResponse, | |
| StartSessionRequest, | |
| StreamMasksRequest, | |
| StreamingStateUpdateResponse, | |
| TrackerRequest, | |
| TrackerResponseMessageEvent, | |
| TrackletCreatedResponse, | |
| TrackletDeletedResponse, | |
| UpdatePointsRequest, | |
| } from '@/common/tracker/TrackerTypes'; | |
| import {TrackerOptions, Trackers} from '@/common/tracker/Trackers'; | |
| import {MP4ArrayBuffer} from 'mp4box'; | |
| import {deserializeError, type ErrorObject} from 'serialize-error'; | |
| import {EventEmitter} from './EventEmitter'; | |
| import { | |
| EncodeVideoRequest, | |
| FilmstripRequest, | |
| FilmstripResponse, | |
| FrameUpdateRequest, | |
| PauseRequest, | |
| PlayRequest, | |
| SetCanvasRequest, | |
| SetEffectRequest, | |
| SetSourceRequest, | |
| StopRequest, | |
| VideoWorkerRequest, | |
| VideoWorkerResponseMessageEvent, | |
| } from './VideoWorkerTypes'; | |
| import {EffectOptions} from './effects/Effect'; | |
| registerSerializableConstructors(); | |
| export type DecodeEvent = { | |
| totalFrames: number; | |
| numFrames: number; | |
| fps: number; | |
| width: number; | |
| height: number; | |
| done: boolean; | |
| }; | |
| export type LoadStartEvent = unknown; | |
| export type EffectUpdateEvent = { | |
| name: keyof Effects; | |
| index: EffectIndex; | |
| variant: number; | |
| numVariants: number; | |
| }; | |
| export type EncodingStateUpdateEvent = { | |
| progress: number; | |
| }; | |
| export type EncodingCompletedEvent = { | |
| file: MP4ArrayBuffer; | |
| }; | |
| export interface PlayEvent {} | |
| export interface PauseEvent {} | |
| export interface FilmstripEvent { | |
| filmstrip: ImageBitmap; | |
| } | |
| export interface FrameUpdateEvent { | |
| index: number; | |
| } | |
| export interface SessionStartedEvent { | |
| sessionId: string; | |
| } | |
| export interface SessionStartFailedEvent {} | |
| export interface TrackletCreatedEvent { | |
| // Do not send masks between workers and main thread because they are huge, | |
| // and sending them would eventually slow down the main thread. | |
| tracklet: BaseTracklet; | |
| } | |
| export interface TrackletsEvent { | |
| // Do not send masks between workers and main thread because they are huge, | |
| // and sending them would eventually slow down the main thread. | |
| tracklets: BaseTracklet[]; | |
| } | |
| export interface TrackletDeletedEvent { | |
| isSuccessful: boolean; | |
| } | |
| export interface AddPointsEvent { | |
| isSuccessful: boolean; | |
| } | |
| export interface ClearPointsInVideoEvent { | |
| isSuccessful: boolean; | |
| } | |
| export interface StreamingStartedEvent {} | |
| export interface StreamingCompletedEvent {} | |
| export interface StreamingStateUpdateEvent { | |
| state: StreamingState; | |
| } | |
| export interface RenderingErrorEvent { | |
| error: ErrorObject; | |
| } | |
| export interface VideoWorkerEventMap { | |
| error: ErrorEvent; | |
| decode: DecodeEvent; | |
| encodingStateUpdate: EncodingStateUpdateEvent; | |
| encodingCompleted: EncodingCompletedEvent; | |
| play: PlayEvent; | |
| pause: PauseEvent; | |
| filmstrip: FilmstripEvent; | |
| frameUpdate: FrameUpdateEvent; | |
| sessionStarted: SessionStartedEvent; | |
| sessionStartFailed: SessionStartFailedEvent; | |
| trackletCreated: TrackletCreatedEvent; | |
| trackletsUpdated: TrackletsEvent; | |
| trackletDeleted: TrackletDeletedEvent; | |
| addPoints: AddPointsEvent; | |
| clearPointsInVideo: ClearPointsInVideoEvent; | |
| streamingStarted: StreamingStartedEvent; | |
| streamingCompleted: StreamingCompletedEvent; | |
| streamingStateUpdate: StreamingStateUpdateEvent; | |
| // HTMLVideoElement events https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#events | |
| loadstart: LoadStartEvent; | |
| effectUpdate: EffectUpdateEvent; | |
| renderingError: RenderingErrorEvent; | |
| } | |
| type Metadata = { | |
| totalFrames: number; | |
| fps: number; | |
| width: number; | |
| height: number; | |
| }; | |
| export default class VideoWorkerBridge extends EventEmitter<VideoWorkerEventMap> { | |
| static create(workerFactory: () => Worker) { | |
| const worker = workerFactory(); | |
| return new VideoWorkerBridge(worker); | |
| } | |
| protected worker: Worker; | |
| private metadata: Metadata | null = null; | |
| private frameIndex: number = 0; | |
| private _sessionId: string | null = null; | |
| public get sessionId() { | |
| return this._sessionId; | |
| } | |
| public get width() { | |
| return this.metadata?.width ?? 0; | |
| } | |
| public get height() { | |
| return this.metadata?.height ?? 0; | |
| } | |
| public get numberOfFrames() { | |
| return this.metadata?.totalFrames ?? 0; | |
| } | |
| public get fps() { | |
| return this.metadata?.fps ?? 0; | |
| } | |
| public get frame() { | |
| return this.frameIndex; | |
| } | |
| constructor(worker: Worker) { | |
| super(); | |
| this.worker = worker; | |
| worker.addEventListener( | |
| 'message', | |
| ( | |
| event: VideoWorkerResponseMessageEvent | TrackerResponseMessageEvent, | |
| ) => { | |
| switch (event.data.action) { | |
| case 'error': | |
| // Deserialize error before triggering the event | |
| event.data.error = deserializeError(event.data.error); | |
| break; | |
| case 'decode': | |
| this.metadata = event.data; | |
| break; | |
| case 'frameUpdate': | |
| this.frameIndex = event.data.index; | |
| break; | |
| case 'sessionStarted': | |
| this._sessionId = event.data.sessionId; | |
| break; | |
| } | |
| this.trigger(event.data.action, event.data); | |
| }, | |
| ); | |
| } | |
| public setCanvas(canvas: HTMLCanvasElement): void { | |
| const offscreenCanvas = canvas.transferControlToOffscreen(); | |
| this.sendRequest<SetCanvasRequest>( | |
| 'setCanvas', | |
| { | |
| canvas: offscreenCanvas, | |
| }, | |
| [offscreenCanvas], | |
| ); | |
| } | |
| public setSource(source: string): void { | |
| this.sendRequest<SetSourceRequest>('setSource', { | |
| source, | |
| }); | |
| } | |
| public terminate(): void { | |
| super.destroy(); | |
| this.worker.terminate(); | |
| } | |
| public play(): void { | |
| this.sendRequest<PlayRequest>('play'); | |
| } | |
| public pause(): void { | |
| this.sendRequest<PauseRequest>('pause'); | |
| } | |
| public stop(): void { | |
| this.sendRequest<StopRequest>('stop'); | |
| } | |
| public goToFrame(index: number): void { | |
| this.sendRequest<FrameUpdateRequest>('frameUpdate', { | |
| index, | |
| }); | |
| } | |
| public previousFrame(): void { | |
| const index = Math.max(0, this.frameIndex - 1); | |
| this.goToFrame(index); | |
| } | |
| public nextFrame(): void { | |
| const index = Math.min(this.frameIndex + 1, this.numberOfFrames - 1); | |
| this.goToFrame(index); | |
| } | |
| public set frame(index: number) { | |
| this.sendRequest<FrameUpdateRequest>('frameUpdate', {index}); | |
| } | |
| createFilmstrip(width: number, height: number): Promise<ImageBitmap> { | |
| return new Promise((resolve, _reject) => { | |
| const handleFilmstripResponse = ( | |
| event: MessageEvent<FilmstripResponse>, | |
| ) => { | |
| if (event.data.action === 'filmstrip') { | |
| this.worker.removeEventListener('message', handleFilmstripResponse); | |
| resolve(event.data.filmstrip); | |
| } | |
| }; | |
| this.worker.addEventListener('message', handleFilmstripResponse); | |
| this.sendRequest<FilmstripRequest>('filmstrip', { | |
| width, | |
| height, | |
| }); | |
| }); | |
| } | |
| setEffect(name: keyof Effects, index: EffectIndex, options?: EffectOptions) { | |
| this.sendRequest<SetEffectRequest>('setEffect', { | |
| name, | |
| index, | |
| options, | |
| }); | |
| } | |
| encode(): void { | |
| this.sendRequest<EncodeVideoRequest>('encode'); | |
| } | |
| initializeTracker(name: keyof Trackers, options: TrackerOptions): void { | |
| this.sendRequest<InitializeTrackerRequest>('initializeTracker', { | |
| name, | |
| options, | |
| }); | |
| } | |
| startSession(videoUrl: string): Promise<string | null> { | |
| return new Promise(resolve => { | |
| const handleResponse = ( | |
| event: MessageEvent< | |
| SessionStartedResponse | SessionStartFailedResponse | |
| >, | |
| ) => { | |
| if (event.data.action === 'sessionStarted') { | |
| this.worker.removeEventListener('message', handleResponse); | |
| resolve(event.data.sessionId); | |
| } | |
| if (event.data.action === 'sessionStartFailed') { | |
| this.worker.removeEventListener('message', handleResponse); | |
| resolve(null); | |
| } | |
| }; | |
| this.worker.addEventListener('message', handleResponse); | |
| this.sendRequest<StartSessionRequest>('startSession', { | |
| videoUrl, | |
| }); | |
| }); | |
| } | |
| closeSession(): void { | |
| this.sendRequest<CloseSessionRequest>('closeSession'); | |
| } | |
| logAnnotations(): void { | |
| this.sendRequest<LogAnnotationsRequest>('logAnnotations'); | |
| } | |
| createTracklet(): Promise<BaseTracklet> { | |
| return new Promise(resolve => { | |
| const handleResponse = (event: MessageEvent<TrackletCreatedResponse>) => { | |
| if (event.data.action === 'trackletCreated') { | |
| this.worker.removeEventListener('message', handleResponse); | |
| resolve(event.data.tracklet); | |
| } | |
| }; | |
| this.worker.addEventListener('message', handleResponse); | |
| this.sendRequest<CreateTrackletRequest>('createTracklet'); | |
| }); | |
| } | |
| deleteTracklet(trackletId: number): Promise<void> { | |
| return new Promise((resolve, reject) => { | |
| const handleResponse = (event: MessageEvent<TrackletDeletedResponse>) => { | |
| if (event.data.action === 'trackletDeleted') { | |
| this.worker.removeEventListener('message', handleResponse); | |
| if (event.data.isSuccessful) { | |
| resolve(); | |
| } else { | |
| reject(`could not delete tracklet ${trackletId}`); | |
| } | |
| } | |
| }; | |
| this.worker.addEventListener('message', handleResponse); | |
| this.sendRequest<DeleteTrackletRequest>('deleteTracklet', {trackletId}); | |
| }); | |
| } | |
| updatePoints( | |
| objectId: number, | |
| points: SegmentationPoint[], | |
| ): Promise<boolean> { | |
| return new Promise(resolve => { | |
| const handleResponse = (event: MessageEvent<AddPointsResponse>) => { | |
| if (event.data.action === 'addPoints') { | |
| this.worker.removeEventListener('message', handleResponse); | |
| resolve(event.data.isSuccessful); | |
| } | |
| }; | |
| this.worker.addEventListener('message', handleResponse); | |
| this.sendRequest<UpdatePointsRequest>('updatePoints', { | |
| frameIndex: this.frame, | |
| objectId, | |
| points, | |
| }); | |
| }); | |
| } | |
| clearPointsInFrame(objectId: number) { | |
| this.sendRequest<ClearPointsInFrameRequest>('clearPointsInFrame', { | |
| frameIndex: this.frame, | |
| objectId, | |
| }); | |
| } | |
| clearPointsInVideo(): Promise<boolean> { | |
| return new Promise(resolve => { | |
| const handleResponse = ( | |
| event: MessageEvent<ClearPointsInVideoResponse>, | |
| ) => { | |
| if (event.data.action === 'clearPointsInVideo') { | |
| this.worker.removeEventListener('message', handleResponse); | |
| resolve(event.data.isSuccessful); | |
| } | |
| }; | |
| this.worker.addEventListener('message', handleResponse); | |
| this.sendRequest<ClearPointsInVideoRequest>('clearPointsInVideo'); | |
| }); | |
| } | |
| streamMasks(): void { | |
| this.sendRequest<StreamMasksRequest>('streamMasks', { | |
| frameIndex: this.frame, | |
| }); | |
| } | |
| abortStreamMasks(): Promise<void> { | |
| return new Promise(resolve => { | |
| const handleAbortResponse = ( | |
| event: MessageEvent<StreamingStateUpdateResponse>, | |
| ) => { | |
| if ( | |
| event.data.action === 'streamingStateUpdate' && | |
| event.data.state === 'aborted' | |
| ) { | |
| this.worker.removeEventListener('message', handleAbortResponse); | |
| resolve(); | |
| } | |
| }; | |
| this.worker.addEventListener('message', handleAbortResponse); | |
| this.sendRequest<AbortStreamMasksRequest>('abortStreamMasks'); | |
| }); | |
| } | |
| getWorker_ONLY_USE_WITH_CAUTION(): Worker { | |
| return this.worker; | |
| } | |
| /** | |
| * Convenient function to have typed postMessage. | |
| * | |
| * @param action Video worker action | |
| * @param message Actual payload | |
| * @param transfer Any object that should be transferred instead of cloned | |
| */ | |
| protected sendRequest<T extends VideoWorkerRequest | TrackerRequest>( | |
| action: T['action'], | |
| payload?: Omit<T, 'action'>, | |
| transfer?: Transferable[], | |
| ) { | |
| this.worker.postMessage( | |
| { | |
| action, | |
| ...payload, | |
| }, | |
| { | |
| transfer, | |
| }, | |
| ); | |
| } | |
| // // Override EventEmitter | |
| // addEventListener<K extends keyof WorkerEventMap>( | |
| // type: K, | |
| // listener: (ev: WorkerEventMap[K]) => unknown, | |
| // ): void { | |
| // switch (type) { | |
| // case 'frameUpdate': | |
| // { | |
| // const event: FrameUpdateEvent = { | |
| // index: this.frameIndex, | |
| // }; | |
| // // @ts-expect-error Incorrect typing. Not sure how to correctly type it | |
| // listener(event); | |
| // } | |
| // break; | |
| // case 'sessionStarted': { | |
| // if (this.sessionId !== null) { | |
| // const event: SessionStartedEvent = { | |
| // sessionId: this.sessionId, | |
| // }; | |
| // // @ts-expect-error Incorrect typing. Not sure how to correctly type it | |
| // listener(event); | |
| // } | |
| // } | |
| // } | |
| // super.addEventListener(type, listener); | |
| // } | |
| } | |