import { getMotionTrackingEngine, type Rectangle, type TrackingOptions, type TrackingJob, type TrackingData, type Point, } from "@openreel/core"; export interface MotionTrackingState { isTracking: boolean; progress: number; currentJob: TrackingJob | null; trackingData: TrackingData | null; lostFrames: number[]; error: string | null; } export type MotionTrackingStateListener = (state: MotionTrackingState) => void; class MotionTrackingBridge { private engine = getMotionTrackingEngine(); private currentState: MotionTrackingState = { isTracking: false, progress: 0, currentJob: null, trackingData: null, lostFrames: [], error: null, }; private listeners: Set = new Set(); private clipTrackingMap: Map = new Map(); private unsubscribeProgress: (() => void) | null = null; private unsubscribeLost: (() => void) | null = null; constructor() { this.unsubscribeProgress = this.engine.onTrackingProgress( this.handleProgress, ); this.unsubscribeLost = this.engine.onTrackingLost(this.handleTrackingLost); } private handleProgress = (progress: number): void => { this.updateState({ progress }); }; private handleTrackingLost = (frameIndex: number): void => { const lostFrames = [...this.currentState.lostFrames, frameIndex]; this.updateState({ lostFrames }); }; private updateState(partial: Partial): void { this.currentState = { ...this.currentState, ...partial }; this.notifyListeners(); } private notifyListeners(): void { for (const listener of this.listeners) { listener(this.currentState); } } subscribe(listener: MotionTrackingStateListener): () => void { this.listeners.add(listener); listener(this.currentState); return () => this.listeners.delete(listener); } getState(): MotionTrackingState { return this.currentState; } async startTracking( clipId: string, region: Rectangle, options: TrackingOptions = {}, ): Promise { this.updateState({ isTracking: true, progress: 0, lostFrames: [], error: null, trackingData: null, }); try { const job = await this.engine.startTracking(clipId, region, options); this.updateState({ currentJob: job }); const pollInterval = setInterval(() => { const currentJob = this.engine.getTrackingJob(job.id); if (!currentJob) { clearInterval(pollInterval); return; } this.updateState({ currentJob, progress: currentJob.progress }); if ( currentJob.status === "completed" || currentJob.status === "failed" || currentJob.status === "cancelled" ) { clearInterval(pollInterval); if (currentJob.status === "completed") { const allTrackingData = this.engine.getTrackingDataForClip(clipId); const latestData = allTrackingData.length > 0 ? allTrackingData[allTrackingData.length - 1] : null; if (latestData) { this.clipTrackingMap.set(clipId, latestData.trackId); } this.updateState({ isTracking: false, trackingData: latestData, currentJob, }); } else if (currentJob.status === "failed") { this.updateState({ isTracking: false, error: currentJob.error || "Tracking failed", currentJob, }); } else { this.updateState({ isTracking: false, currentJob, }); } } }, 100); return job; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; this.updateState({ isTracking: false, error: errorMessage, }); throw error; } } cancelTracking(jobId: string): void { this.engine.cancelTracking(jobId); this.updateState({ isTracking: false, progress: 0, }); } applyTrackingToElement( trackId: string, elementId: string, offset: Point = { x: 0, y: 0 }, ): void { this.engine.applyTrackingToElement(trackId, elementId, offset); } applyTrackingToClip(clipId: string, offset: Point = { x: 0, y: 0 }): boolean { const trackId = this.clipTrackingMap.get(clipId); if (!trackId) { console.warn(`No tracking data found for clip ${clipId}`); return false; } try { this.applyTrackingToElement(trackId, clipId, offset); return true; } catch { return false; } } setTrackingOffset(elementId: string, offset: Point): void { this.engine.setTrackingOffset(elementId, offset); } getTrackingOffset(elementId: string): Point | null { return this.engine.getTrackingOffset(elementId); } setApplyScale(elementId: string, applyScale: boolean): void { this.engine.setApplyScale(elementId, applyScale); } setApplyRotation(elementId: string, applyRotation: boolean): void { this.engine.setApplyRotation(elementId, applyRotation); } getElementPositionAtTime( elementId: string, timeInSeconds: number, ): Point | null { return this.engine.getElementPositionAtTime(elementId, timeInSeconds); } correctTrackingPoint( trackId: string, frameIndex: number, position: Point, ): void { this.engine.correctTrackingPoint(trackId, frameIndex, position); } getTrackingDataForClip(clipId: string): TrackingData[] { return this.engine.getTrackingDataForClip(clipId); } getTrackingData(clipId: string, trackId: string): TrackingData | undefined { return this.engine.getTrackingData(clipId, trackId); } removeAttachment(elementId: string): void { this.engine.removeTrackingFromElement(elementId); } hasTrackingData(clipId: string): boolean { return this.clipTrackingMap.has(clipId); } getClipTrackId(clipId: string): string | null { return this.clipTrackingMap.get(clipId) || null; } reset(): void { this.updateState({ isTracking: false, progress: 0, currentJob: null, trackingData: null, lostFrames: [], error: null, }); } dispose(): void { if (this.unsubscribeProgress) { this.unsubscribeProgress(); } if (this.unsubscribeLost) { this.unsubscribeLost(); } this.listeners.clear(); } } let motionTrackingBridgeInstance: MotionTrackingBridge | null = null; export function getMotionTrackingBridge(): MotionTrackingBridge { if (!motionTrackingBridgeInstance) { motionTrackingBridgeInstance = new MotionTrackingBridge(); } return motionTrackingBridgeInstance; } export function resetMotionTrackingBridge(): void { if (motionTrackingBridgeInstance) { motionTrackingBridgeInstance.reset(); } }