feat: media processing pipeline, spatial indexing, canvas-based video rendering

- Background media worker: polls media_jobs table, runs ffprobe+ffmpeg
  with concurrency limit, generates video posters, emits socket events
- Non-blocking video upload: stores file + enqueues job, returns immediately
- Poster hydration on board load: GET /boards/:id injects poster/dimensions
  from DB into canvas_state video objects
- SpatialGrid: fixed-cell (512px) spatial hash for O(nearby) culling instead
  of O(all) item scanning, eliminates setTimeout violations
- Canvas-based video rendering: draws video frames to offscreen canvas then
  uploads to GPU, completely eliminates GL_INVALID_OPERATION errors from
  PixiJS VideoSource auto-update mechanism
- Server poster upgrade path: culling ticker and applyProcessedMedia() both
  upgrade client-captured posters to server posters when available
- Pause restores server poster (paused video behaves like an image)
- Selection drag-end persistence: onObjectDragEnd broadcasts + saves + undo
- Live media:job:update socket handler patches scene data + VideoSprite
  dimensions without broadcast fanout
This commit is contained in:
Hiren Kangad
2026-03-10 14:30:01 +05:30
parent fdcee3536f
commit de418b2ba5
12 changed files with 761 additions and 110 deletions
+7
View File
@@ -41,6 +41,7 @@ export class SelectionManager {
private _onSelectionChange: ((ids: string[]) => void) | null = null;
private _onItemTransform: ((item: SceneItem) => void) | null = null;
private _onItemsTransform: ((items: SceneItem[]) => void) | null = null;
private _onObjectDragEnd: ((ids: string[]) => void) | null = null;
// Pointer state
private _pointerDown = false;
@@ -122,6 +123,11 @@ export class SelectionManager {
this._onItemsTransform = fn;
}
/** Called when object drag ends (for persist/save/spatial refresh). */
set onObjectDragEnd(fn: (ids: string[]) => void) {
this._onObjectDragEnd = fn;
}
/** Called on double-click of a text item (for inline editing). */
set onDoubleClickText(fn: (item: SceneItem) => void) {
this._onDoubleClickText = fn;
@@ -313,6 +319,7 @@ export class SelectionManager {
// Notify change (positions updated)
this._emitChange();
this._onObjectDragEnd?.(Array.from(this.selectedIds));
} else if (this._rubberBanding) {
// Finish rubber band selection
const currentWorld = this._viewport.toWorld(e.global.x, e.global.y);