MediaRouter / docs /content-studio-persistence-rendering.md
basyx's picture
Upload 794 files
1b2323a verified
|
Raw
History Blame Contribute Delete
8.84 kB
# Content Studio persistence and rendering
Content Studio Phase 2 adds authoritative PostgreSQL editor persistence and asynchronous FFmpeg rendering without changing the Phase 1 editor state boundary. TanStack Query owns editor envelopes/render jobs; Zustand still owns the in-session document, selection, playback, history, and dirty state.
## Database and migration
Apply `app/projects/migrations/0003_editor_persistence_rendering.sql` after the tenancy, generation, project-foundation, and project-resource migrations. Production startup never applies it automatically.
`project_editor_states` has one row per project (`project_id` is unique). It stores `revision`, `schema_version`, bounded validated JSON state, timestamps, and `updated_by`. `project_render_jobs` is a separate project job domain because provider-specific generation jobs cannot safely represent FFmpeg renders. A render row stores its exact editor revision, immutable editor snapshot, validated settings, request fingerprint, idempotency key, durable state, attempts, timestamps, errors, and canonical output asset ID.
Required order:
1. `app/security/migrations/0001_api_key_security.sql`
2. `app/security/migrations/0002_authoritative_tenancy_postgres.sql`
3. `app/security/migrations/0003_generation_domain_postgres.sql`
4. `app/security/migrations/0004_generation_provider_runtime_postgres.sql`
5. `app/projects/migrations/0001_projects_foundation.sql`
6. `app/projects/migrations/0002_project_resources.sql`
7. `app/projects/migrations/0003_editor_persistence_rendering.sql`
8. `app/projects/migrations/0004_ai_studio.sql`
9. `app/projects/migrations/0005_ai_copilot.sql`
10. Existing social migrations in their documented order.
## Editor API and revision model
- `GET /v1/projects/{project_id}/editor`
- `PUT /v1/projects/{project_id}/editor`
PUT accepts `expected_revision`, `schema_version`, and `state`. A new project starts with expected revision `0` and is persisted as revision `1`. Every successful update increments the revision. A stale expected revision returns `PROJECT_EDITOR_REVISION_CONFLICT` with the safe current and expected revision metadata; it never overwrites the newer row.
The schema accepts only version 1 and structurally validates project ID, millisecond timeline coordinates, tracks, discriminated clips, unique IDs/order, track references, source ranges, transforms, fades, transitions, render settings, scalar-only bounded metadata, asset ownership, MIME compatibility, known source duration, total duration, track/clip counts, and serialized byte size. Asset IDs are stored; binaries, signed URLs, OAuth credentials, and provider tokens are not.
## RLS and authorization
Both tables have PostgreSQL RLS enabled and forced. Policies require the request-scoped `app.workspace_id`, `app.user_id`, and an active membership. Viewer memberships may read but cannot insert/update. Application routes additionally require existing `projects:read` or `projects:update` scopes. Foreign project/render IDs return not-found behavior through workspace predicates. Workers use the existing backend-only database execution boundary.
Database triggers also require editor revisions to advance exactly once, bind the JSON document/snapshot identity to the row project and schema version, keep render snapshots/settings/idempotency fields immutable, validate render state transitions, and require completed output assets to belong to the same project/workspace. These checks supplement rather than replace service validation.
## Autosave and conflicts
The frontend loads the editor envelope when Studio opens. A missing row creates the existing empty Phase 1 document through PUT; localStorage is not authoritative. Meaningful document mutations are debounced for 1.2 seconds. Only one save runs at a time, and a saved snapshot is compared with the current local timeline so edits made during an in-flight request remain dirty and are coalesced into the next save.
On conflict, local work remains in Zustand. “Reload server version” refetches and hydrates the authoritative document. “Keep local version” first refetches the current revision, then resubmits the intact local document against that explicit revision. A failed initialization/autosave is shown as not saved/save failed, remains manually retryable, and is not retried in a tight loop until the document changes or the user retries.
## Render API and lifecycle
- `GET /v1/projects/{project_id}/renders`
- `POST /v1/projects/{project_id}/renders`
- `GET /v1/projects/{project_id}/renders/{render_id}`
- `POST /v1/projects/{project_id}/renders/{render_id}/cancel`
Creation requires `Idempotency-Key` and a specific current editor revision. The same project/revision/key/fingerprint returns the original job; a changed request with the same key returns a conflict. States are `queued`, `processing`, `completed`, `failed`, `cancelling`, and `cancelled`. The worker uses bounded attempts with exponential retry delay, durable cancellation, a heartbeat/stale lease, structured events, and shutdown-safe requeueing.
Render creation locks the project while checking idempotency and active-job count, preventing concurrent callers from racing the configured per-project limit. Worker retries reconcile an already-published deterministic output rather than overwriting it. If cancellation wins after output registration, the worker compensates by removing the just-created canonical row and local output before reporting cancellation.
The frontend render dialog submits format, resolution, quality, and encoding preset. It polls durable state and never invents progress; `progress` remains null because the shared FFmpeg runner does not yet expose reliable machine-readable progress. Completion invalidates project assets.
## Compiler and FFmpeg
The compiler is deterministic and shell-free. It supports visible video/image clips, explicit audio clips, source trims, timeline offsets, audio fades/volume, opacity, translation, scaling, rotation, track visibility/muting, MP4/WebM output, and a fixed-size canvas. It rejects captions, effects, and transitions until those primitives have reviewed render implementations. Markers are non-rendering metadata.
Each referenced canonical asset must still be attached to the same project and workspace when the worker resolves it. Paths are derived server-side through the existing canonical asset and cleanup services. FFmpeg runs asynchronously through the existing concurrency-limited service. Command arguments, media paths, signed URLs, stdout, and stderr are excluded from structured logs. Temporary staging is removed on success, failure, cancellation, and shutdown.
On success, the worker publishes the file through the current canonical output mechanism, registers it as a `media_assets` row for the same workspace/project, and stores only the asset ID on the render job. Internal paths are not returned by the API.
## Limits and rate limiting
Configuration defaults:
- `EDITOR_STATE_MAX_BYTES=1048576`
- `RENDER_MAX_TRACKS=32`
- `RENDER_MAX_CLIPS=500`
- `RENDER_MAX_DURATION_SECONDS=3600`
- `RENDER_MAX_INPUT_BYTES=4294967296`
- `MAX_RESOLUTION_PIXELS=33177600`
- `RENDER_MAX_ACTIVE_JOBS_PER_PROJECT=1`
- `RENDER_JOB_RETRY_LIMIT=2`
- `RENDER_JOB_STALE_AFTER_SECONDS=900`
- `RENDER_JOB_TIMEOUT_SECONDS=7200`
Render POSTs also use the existing per-key request/concurrent-job rate limiter. Deterministic validation/compiler failures are not retried.
## Audit, logs, and failure modes
Bounded audit events are `project.editor_created`, `project.editor_updated`, `project.editor_conflict`, `project.render_requested`, `project.render_completed`, `project.render_failed`, and `project.render_cancelled`. Editor documents, tokens, signed URLs, and credentials are excluded. Logs contain IDs, operation, revision/status, and safe error codes.
The current canonical media implementation persists locators in PostgreSQL but files in configured output storage. Hugging Face local files are ephemeral; production durability therefore still depends on configuring the platform's authoritative persistent media storage. The Phase 2 code does not introduce a second asset store.
## SDK and n8n
Python and TypeScript SDKs expose `client.projects.editor.get/save` and `client.projects.renders.create/get/list/cancel`. The n8n node is unchanged because its present resource model does not expose project-scoped operations cleanly; no placeholder operation was added.
## Future extensions
Historical editor versions, distributed render concurrency across multiple replicas, captions, transitions, effects, video-source audio mixing, reliable FFmpeg progress, and object-storage-backed canonical media are intentionally separate follow-up work. AI Studio, publishing, and collaboration are not part of this phase.