Spaces:
Runtime error
Runtime error
/** | |
* 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); | |
// } | |
} | |