Member Junction
    Preparing search index...

    Module @memberjunction/video-studio

    @memberjunction/video-studio

    @memberjunction/video-studio is the server-side engine and action package for MemberJunction Video Studio, housing VideoRenderEngine drivers, pre-production asset actions, and data snapshot actions. The brand-kit and archetype caches (BrandKitEngine, VideoStudioEngine) live in @memberjunction/core-entities so browser and server share them. It runs inside MJAPI with standard contextUser authorization and delegates all headless Chromium and FFmpeg composition rendering over the wire contract to @memberjunction/video-studio-render-worker rather than executing untrusted composition bundles in-process. See plans/video-studio.md for the full specification.

    VideoRenderEngine (VideoRenderEngine.Instance.Driver) is the process-wide singleton port (BaseSingleton + IShutdownable) that resolves and manages the active VideoRenderDriverBase implementation via MJGlobal.Instance.ClassFactory.

    Add the optional videoStudio.render and videoStudio.outputs blocks to mj.config.cjs (omitted fields fall back to defaults; invalid values log a warning and fall back to defaults without throwing):

    module.exports = {
    // ...existing MJ configuration...
    videoStudio: {
    render: {
    // 'LocalWorker' (default) | 'Remote' (VS-RND-3) | 'Docker' (VS-RND-4)
    driver: 'LocalWorker',
    // Maximum output payload size in bytes for MP4 downloads (/videos/<VideoID>) and image-carrying JSON responses (/stills, /contact-sheet, /keyframes, which also stop at buffer.constants.MAX_STRING_LENGTH even when maxOutputBytes is higher); non-image JSON responses (/health, /check), NDJSON lines, and error bodies are capped at 1 MiB (512 MiB default)
    maxOutputBytes: 536870912,
    timeouts: {
    startupMs: 60000, // 60 s default
    renderMs: 2100000, // 35 min default
    requestMs: 600000, // 10 min default
    },
    local: {
    // Optional explicit path to @memberjunction/video-studio-render-worker dist/main.js
    // workerEntry: '/path/to/packages/AI/VideoStudio/RenderWorker/dist/main.js',
    // Optional V8 --max-old-space-size in MB for the spawned child process
    // maxOldSpaceMb: 4096,
    },
    remote: {
    // Single remote render worker HTTP/HTTPS URL (or env VIDEO_STUDIO_RENDER_URL)
    // url: 'https://render-worker.internal.example.com',
    // Or a pool of worker URLs (`urls` wins when both `url` and `urls` are set);
    // RemoteRenderDriver pins each job round-robin to one URL for both POST /render and GET /videos/<VideoID>,
    // and Health() probes every worker in the pool without advancing the job rotation.
    // urls: ['https://render-worker-1.internal.example.com', 'https://render-worker-2.internal.example.com'],
    // Bearer token sent in Authorization: Bearer <token> (or env VIDEO_STUDIO_RENDER_TOKEN)
    // token: process.env.VIDEO_STUDIO_RENDER_TOKEN,
    },
    docker: {
    // Docker image tag built from packages/AI/VideoStudio/RenderWorker/Dockerfile
    image: 'mj-video-render-worker:local',
    // Optional host workspace root directory bind-mounted into per-job containers
    // (when omitted, DockerRenderDriver creates and reuses a per-process 0o700 mkdtemp directory under os.tmpdir())
    // workspaceRoot: '/var/tmp/mj-video-render',
    cpus: 2, // --cpus 2
    memoryMb: 4096, // --memory 4096m
    pidsLimit: 512, // --pids-limit 512
    shmSizeMb: 1024, // --shm-size 1024m
    },
    },
    outputs: {
    // Max MP4 bytes allowed to fall back to InlineBase64 when no FileStorageAccount resolves or MJStorage fails
    // (default: 2097152 / 2 MB; set to 0 to disable InlineBase64 fallback).
    // VideoStudioAgent strips Output.Videos[*].InlineBase64 ('[inline-mp4]') from _CURRENT_PAYLOAD during the loop
    // and restores the real base64 bytes when the run exits (Delivered, G4 Plan, or Failed).
    // Intra-run intermediate files (narration clips, captions, poster, contact sheets) fall back to a bounded
    // in-process ephemeral store ('inline:<uuid>') when no FileStorageAccount is configured.
    inlineMaxBytes: 2097152,
    },
    },
    };
    Driver (videoStudio.render.driver) Description Network Isolation (Driver.IsNetworkIsolated)
    LocalWorker (default) Managed local child process (LocalWorkerRenderDriver). Spawns @memberjunction/video-studio-render-worker (mj-video-render-worker) on 127.0.0.1:0 with a sanitized environment allowlist (BuildWorkerEnv — never passes MJAPI database or vendor secrets) and a fresh 32-byte random bearer token per spawn. Cannot run as root (uid === 0): the worker refuses root unless VIDEO_STUDIO_RENDER_ALLOW_ROOT=1 is set, and BuildWorkerEnv never forwards that variable. Requires ffmpeg on PATH and a Chromium headless shell (pnpm --filter @memberjunction/video-studio-render-worker run browser:install or CHROME_PATH); @memberjunction/video-studio-render-worker is declared in optionalDependencies of @memberjunction/video-studio so npm install @memberjunction/video-studio installs it by default for LocalWorker, while container builds targeting Docker or Remote can skip it with --omit=optional. false — Development / single-node trusted compositions only. Logs a one-time startup warning (LOCAL_WORKER_NETWORK_WARNING) when selected.
    Remote Stateless HTTP/HTTPS client (RemoteRenderDriver) targeting one or more external render worker endpoints via RenderWorkerHttpClient. Configured via videoStudio.render.remote.url / .urls / .token (urls takes precedence when both url and urls are set; falls back to VIDEO_STUDIO_RENDER_URL / VIDEO_STUDIO_RENDER_TOKEN when neither is set; mj.config.cjs wins over env). Health() probes every worker in the pool without advancing the per-job rotation and reports Healthy: true only when all workers are healthy. Logs a one-time warning per distinct worker URL per driver instance if a plain http:// URL targets a non-loopback host because the bundle's frozen data snapshot travels in the request body. false — RemoteRenderDriver.IsNetworkIsolated is always false (VideoRenderDriverBase.IsNetworkIsolated).
    Docker Hardened one-container-per-job driver (DockerRenderDriver, Linux/macOS host only) provisioned via @memberjunction/ai-agent-harness (DockerSandboxProvider). Runs node /app/dist/main.js run <op> over a bind-mounted workspace with --network none, --read-only, --tmpfs /tmp:rw,size=256m, --cap-drop ALL, --security-opt no-new-privileges, --cpus, --memory, --pids-limit, and --shm-size, passing only TMPDIR and HOME in the process environment. Fails closed on Windows (process.platform === 'win32') and directs operators to Remote. true — Required boundary for untrusted compositions (Decision 4 / D15).

    Build the local render worker image from the repository root before using driver: 'Docker':

    docker build -f packages/AI/VideoStudio/RenderWorker/Dockerfile -t mj-video-render-worker:local .
    
    • Why Docker is the required boundary for untrusted compositions: Model-written HTML/CSS/JS compositions generated from untrusted prompts cannot rely on the static lint gate alone: static pattern checks cannot stop obfuscated or dynamically constructed runtime JavaScript (window['fet' + 'ch'], WebRTC, dynamic DOM injection) from making outbound requests in headless Chromium, nor do they stop HyperFrames' own Node-side Google Fonts and GSAP CDN fetches during compilation and check runs. Running each operation inside a --network none container removes all non-loopback network interfaces at the kernel namespace level so neither Node nor Chromium can exfiltrate the bundle's data.json snapshot or reach external hosts.
    • One container per operation: Every Render, CaptureStills, BuildContactSheet, Check, ExtractKeyframes, and Health call provisions a fresh container via ISandboxProvider.Provision, stages request.json and out/ / tmp/ / home/ into handle.HostWorkspacePath (chmod 0o700 on Linux; when videoStudio.render.docker.workspaceRoot is omitted, DockerRenderDriver creates and reuses a per-process 0o700 mkdtemp root under os.tmpdir()), runs node /app/dist/main.js run <op> --request /workspace/request.json --out /workspace/out, moves out/ into a newly created 0o700 <HostWorkspacePath>.out/out directory outside the bind mount, verifies regular-file O_NOFOLLOW handles and output byte caps (1 MiB for non-image JSON outputs and error.json, maxOutputBytes for image-carrying JSON results (stills, contact-sheet, keyframes), and maxOutputBytes plus VideoSizeBytes equality for video.mp4) before reading, and in finally calls ISandboxProvider.Finalize first and removes <HostWorkspacePath>.out after Finalize when the container is no longer running.
    • Container hardening flags: DockerRenderDriver passes NetworkPolicy: 'none' (--network none), ReadOnlyRoot: true (--read-only --tmpfs /tmp:rw,size=256m), DropCapabilities: true (--cap-drop ALL --security-opt no-new-privileges), Cpus (--cpus 2), MemoryMb (--memory 4096m), PidsLimit (--pids-limit 512), and ShmSizeMb (--shm-size 1024m). Only TMPDIR=/workspace/tmp and HOME=/workspace/home are passed into Executor.Run — parent process environment variables are never forwarded.
    • Host platform & --user behavior: On Linux (process.platform === 'linux'), DockerRenderDriver passes User: "${process.getuid()}:${process.getgid()}" (--user <uid:gid>) so files written by the container into the bind-mounted host workspace are owned by the host user and removable by Finalize. On macOS (process.platform === 'darwin', Docker Desktop VM), User is omitted (undefined) so the container runs as the image's default non-root node user (1000:1000), and Docker Desktop's VirtioFS/gRPC-FUSE mount translates host ownership automatically. On Windows (process.platform === 'win32'), DockerRenderDriver fails closed with VideoRenderError('WorkerUnavailable') before starting any container because Windows lacks O_NOFOLLOW symlink-safe host file reads; Windows hosts must use the Remote render driver instead.

    Build the local image (packages/AI/VideoStudio/RenderWorker/Dockerfile, running as USER node UID 1000 and listening on 0.0.0.0:8080) and run it with bearer token authentication and container hardening flags:

    # Build from the repository root (mj-video-render-worker:local is built locally, not published to a public registry)
    docker build -f packages/AI/VideoStudio/RenderWorker/Dockerfile -t mj-video-render-worker:local .

    # Run with bearer token authentication and container hardening
    docker run --rm -p 127.0.0.1:18080:8080 \
    -e VIDEO_STUDIO_RENDER_TOKEN="replace-with-strong-shared-secret" \
    --read-only \
    --tmpfs /tmp:rw,noexec,nosuid,size=2g \
    --cap-drop=ALL \
    --security-opt=no-new-privileges \
    mj-video-render-worker:local
    • Bearer token is the only credential: The worker refuses to bind to a non-loopback address without VIDEO_STUDIO_RENDER_TOKEN and checks Authorization: Bearer <token> on every route (including GET /health). The worker never holds database, storage, or AI vendor credentials; that bearer token is the only credential the worker ever holds.
    • TLS termination: Composition bundles include the frozen data.json snapshot. Terminate TLS at your ingress or reverse proxy and point videoStudio.render.remote.url / videoStudio.render.remote.urls (or VIDEO_STUDIO_RENDER_URL) at the https:// endpoint(s). Plain http:// to a non-loopback address is permitted for cluster-internal networks that terminate TLS at the mesh/ingress layer, and logs one warning per distinct worker URL per driver instance.
    • Ingress request body limit: An ingress or reverse-proxy body limit applies before the worker's 256 MB cap (VIDEO_STUDIO_RENDER_MAX_BODY_MB=256). For example, nginx ingress defaults client_max_body_size to 1m, so most composition bundles and keyframe payloads will fail with 413 BodyTooLarge until the ingress body limit is raised to match VIDEO_STUDIO_RENDER_MAX_BODY_MB.
    • Response size caps (maxOutputBytes vs 1 MiB): RenderWorkerHttpClient (used by both RemoteRenderDriver and LocalWorkerRenderDriver) caps /health, /check, NDJSON stream lines on /render, and all non-2xx error response bodies at 1 MiB (1,048,576 bytes), while capping successful 2xx image-carrying JSON response bodies (/stills, /contact-sheet, /keyframes) at Math.min(maxOutputBytes, buffer.constants.MAX_STRING_LENGTH) (so JSON bodies always stop at V8's string limit buffer.constants.MAX_STRING_LENGTH, 536,870,888 bytes on 64-bit Node, even when maxOutputBytes is higher) and streamed MP4 downloads (/videos/<VideoID>, additionally verified against any claimed VideoSizeBytes) at videoStudio.render.maxOutputBytes (512 MiB / 536,870,912 bytes default). Responses exceeding their cap abort the socket stream immediately and reject with BodyTooLarge.
    • Scaling & replica affinity: Each worker runs one render job at a time by default (VIDEO_STUDIO_RENDER_CONCURRENCY=1) and keeps rendered MP4 bytes in memory inside that replica's ephemeral store until the immediate GET /videos/<VideoID> fetch completes. Both POST /render and GET /videos/<VideoID> for a job must reach the same worker replica (RenderWorkerHttpClient stores no cookies, so cookie stickiness and per-request load balancing lose the video with NotFound). The supported way to scale a pool is to list each worker replica URL in videoStudio.render.remote.urls (urls takes precedence if both url and urls are configured): RemoteRenderDriver selects one worker URL per job (round-robin) and pins both POST /render and GET /videos/<VideoID> to that same worker with no load balancer required, while Health() probes every worker in urls without advancing the job rotation and returns Healthy: true only when all workers are healthy. If you place a load balancer in front of multiple workers under a single videoStudio.render.remote.url, configure client-IP affinity (e.g., Kubernetes Service sessionAffinity: ClientIP or nginx ip_hash) so all requests from an MJAPI host reach the same replica.

    Classes

    DockerRenderDriver
    FakeRenderDriver
    LocalWorkerRenderDriver
    ProduceVideoAction
    RemoteRenderDriver
    RenderWorkerHttpClient
    VideoRenderDriverBase
    VideoRenderEngine
    VideoRenderError
    VideoStudioAgent
    VideoStudioAgentBase
    VideoStudioArtDirectorAgent
    VideoStudioCriticAgent
    VideoStudioMotionEngineerAgent
    VideoStudioPreProductionAgent
    VideoStudioRenderAgent
    VideoStudioSetupHealthServerOperation

    Interfaces

    BuildRenderBundleContext
    BuiltReferenceMedia
    CaptureDataSnapshotOptions
    CaptureDataSnapshotResult
    CriticPromptCriterionScore
    CriticPromptOutput
    DataSnapshotIssue
    EphemeralFileEntry
    FakeRenderDriverOptions
    FilmDurationBounds
    FitTimelineInput
    FitTimelineResult
    GateEvaluationContext
    GeneratedAssetRequestSummary
    GenerateRequestedAssetsContext
    GenerateRequestedAssetsResult
    GenerationIssue
    GenerationScreenResult
    LoadDataSnapshotOptions
    MotionEngineerLintFixOutput
    NarrationClip
    NarrationIssue
    NarrationOptions
    NarrationResult
    ProduceVideoArchetypeLookup
    ProduceVideoBrandKitLookup
    ProduceVideoDataRefInput
    ProduceVideoInput
    ProduceVideoLookups
    ProduceVideoMappingError
    ProduceVideoPresetLookup
    ProduceVideoRequestError
    ProduceVideoRequestSuccess
    RenderWorkerHttpClientOptions
    ResolvedGenerationPolicy
    SetupHealthPromptPreviewRunner
    StoredFileWithType
    StoreFileInput
    StudioPromptRunOutcome
    VideoRenderDockerSettings
    VideoRenderErrorOptions
    VideoRenderLocalSettings
    VideoRenderRemoteSettings
    VideoRenderSettings
    VideoRenderTimeoutsSettings
    VideoStudioOutputSettings
    VideoStudioSettings

    Type Aliases

    DataSnapshotIssueSeverity
    ExtractKeyframesFn
    GenerationBlockCode
    NarrationIssueCode
    ProduceVideoMappingErrorCode
    ProduceVideoRequestResult
    ReferenceMediaKind
    VideoRenderErrorCode
    VideoStudioFormatSelection
    VideoStudioStageName

    Variables

    ALLOWED_WORKER_PARENT_ENV_KEYS
    DEFAULT_DOCKER_CPUS
    DEFAULT_DOCKER_MEMORY_MB
    DEFAULT_DOCKER_PIDS_LIMIT
    DEFAULT_DOCKER_RENDER_IMAGE
    DEFAULT_DOCKER_SHM_SIZE_MB
    DEFAULT_GENERATED_VIDEO_DURATION_SECONDS
    DEFAULT_RENDER_MAX_OUTPUT_BYTES
    DEFAULT_RENDER_REQUEST_TIMEOUT_MS
    DEFAULT_RENDER_STARTUP_TIMEOUT_MS
    DEFAULT_RENDER_TIMEOUT_MS
    DEFAULT_SETUP_HEALTH_RENDER_TIMEOUT_MS
    DEFAULT_VIDEO_RENDER_DRIVER
    DEFAULT_VIDEO_STUDIO_SETTINGS
    DOCKER_RENDER_IMAGE_BUILD_COMMAND
    FAKE_DRIVER_MP4_BYTES
    FAKE_DRIVER_ONE_BY_ONE_PNG_BASE64
    FILE_VALIDATION_BATCH_SIZE
    GENERATED_ASSET_INTEGRITY_CRITERION_KEY
    GENERATION_BLOCK_CODES
    INLINE_EPHEMERAL_FILE_PREFIX
    INLINE_VIDEO_BASE64_SENTINEL
    LOCAL_WORKER_NETWORK_WARNING
    MAX_CAPTION_CHARS_PER_CUE
    MAX_CAPTION_WORDS_PER_CUE
    MAX_GENERATION_REFERENCES
    MAX_INLINE_REFERENCE_VIDEO_BYTES
    MAX_KEYFRAME_IMAGES
    MAX_LINT_ATTEMPTS
    MAX_REFERENCES
    MAX_RENDER_JSON_RESPONSE_BYTES
    MAX_RENDER_NDJSON_LINE_BYTES
    MAX_ROWS_PER_BINDING
    MAX_SETUP_HEALTH_RENDER_TIMEOUT_MS
    MAX_STILL_POINTS
    MIN_SETUP_HEALTH_RENDER_TIMEOUT_MS
    MISSING_CRITIC_RUBRIC_ERROR_MESSAGE
    MOTION_ENGINEER_LINT_FIX_PROMPT_NAME
    NARRATION_LEAD_IN_SECONDS
    NARRATION_LINE_GAP_SECONDS
    NARRATION_OUTPUT_FORMAT
    NARRATION_TAIL_SECONDS
    NO_STORAGE_ACCOUNT_ERROR_MESSAGE
    PACKAGE_NAME
    PERSON_TERMS
    PRODUCE_VIDEO_DRIVER_CLASS
    REFERENCE_ANALYSIS_PROMPT_ID
    REFERENCE_ANALYSIS_PROMPT_NAME
    SCREEN_RULE_STOP_WORDS
    SETUP_HEALTH_NO_STORAGE_MESSAGE
    SETUP_HEALTH_RENDER_NETWORK_WARNING_ACTION
    SETUP_HEALTH_RENDER_NETWORK_WARNING_MESSAGE
    SETUP_HEALTH_STORAGE_ACTION
    SNAPSHOT_INLINE_MAX_BYTES
    SYNTHETIC_PERSON_TERMS
    VIDEO_STUDIO_AGENT_NAME
    VIDEO_STUDIO_INLINE_MAX_BYTES
    VIDEO_STUDIO_ORCHESTRATOR_PROMPT_NAME
    VIDEO_STUDIO_STAGES

    Functions

    BuildBeatsTable
    BuildBoundValuesTable
    BuildCheckRequest
    BuildGeneratedAssetsTable
    BuildProduceVideoRequest
    BuildReferenceBlocks
    BuildRenderBundle
    BuildRepairBrief
    BuildRubricCriteriaText
    BuildTimelineTable
    BuildWebVtt
    BuildWorkerEnv
    CaptureDataSnapshot
    CheckProductionRules
    CheckStagePrerequisite
    ClearStaleEvidence
    ComposeGenerationPrompt
    ComposeHumanReviewNotes
    CreateAbortError
    DeriveStatus
    DescribeStageProgress
    DescribeStoryboardForApproval
    EstimateGenerationCallCost
    EstimateLoadedGenerationCallCost
    EvaluateGates
    FindBrandKitForSpec
    FindIntegrityOrGateCriterionFailure
    FitTimelineToNarration
    FormatCheckIssue
    FormatLintIssue
    FormatWebVttTimestamp
    FrameSizeFor
    GenerateRequestedAssets
    GetEphemeralFileMetadata
    IsEphemeralFileID
    LoadDataSnapshotValues
    LoadProduceVideoAction
    LoadVideoStudioAgent
    LoadVideoStudioArtDirectorAgent
    LoadVideoStudioCriticAgent
    LoadVideoStudioMotionEngineerAgent
    LoadVideoStudioPreProductionAgent
    LoadVideoStudioRenderAgent
    LoadVideoStudioSetupHealthOperation
    MatchesScreenEntry
    ParseVideoStudioSettings
    PickReferences
    PickSheetsForCall
    ReadApprovalDecision
    ReadAssetBytes
    ReadStoredFile
    ReadStoredFileWithType
    ReadVideoRenderSettings
    ReadVideoStudioOutputSettings
    RequestRegeneration
    ResetLocalWorkerWarningState
    ResolveDeliverableFormats
    ResolveGenerationPolicy
    ResolveGenerationReferenceAssets
    ResolveNarrationModelID
    ResolvePromptPrimaryModelId
    ResolveRequestedVideoSeconds
    ResolveVideoStudioSettings
    RunNarrationPipeline
    SanitizeStageKey
    ScreenGenerationRequest
    StillPointsFor
    StoreEphemeralFile
    StoreFile
    SumAlreadyGeneratedVideoSeconds
    SumRecordedGenerationCost
    ThrowIfSignalAborted
    ValidateVideoGenerationSpecAsync