# ── OpenVibe.Media — environment ───────────────────────────── # Copy to .env (or /etc/openvibe/media.env in production) and adjust. # HTTP PORT=4100 HOST=0.0.0.0 NODE_ENV=development # Public base URL used when building absolute playback/asset URLs. MEDIA_PUBLIC_URL=https://openvibe.media # PostgreSQL (ADR-035), written by OpenVibe.Host roles/data/add-service.sh media: the runtime role through PgBouncer, # the owner on a direct connection (migrations), and Valkey (shared limit counters). # Without DATABASE_URL, development uses an embedded PGlite database in data/pglite (MEDIA_PGLITE_DIR overrides it). DATABASE_URL= DATABASE_DIRECT_URL= VALKEY_URL= VALKEY_PREFIX= # The service's data directory: the reports (duration reconcile, storage orphans, H15 repair) and the drill # artifacts are written under it. MEDIA_DATA_DIR=./data # Restore-drill mode, set only by `ovhost drill media` (OpenVibe.Host docs/restore-drills.md): reads # only from a restored copy, no stored bytes, no jobs, events or ffmpeg; refuses to start # unless DATABASE_URL names the restored copy (never ov_media on 5432/6432), DATABASE_DIRECT_URL is unset, # HOST is loopback and PORT is not 4100 (server/drill.js). # MEDIA_DRILL=1 # Storage paths (all created on boot) VOD_PATH=./data/vods CLIPS_PATH=./data/clips PASTES_PATH=./data/pastes # Where pastes live now (2026-09-22): /p/:slug and /p/:slug/raw are 301s there, always — the default # below applies when unset. Stored screenshot bytes under PASTES_PATH/screenshots are still served, # and every write to the paste API answers 404. #PASTES_MOVED_TO=https://openvibe.community THUMBNAILS_PATH=./data/thumbnails FILES_PATH=./data/files # RTP ingest port pool (UDP, bound on 127.0.0.1 by ffmpeg) RTP_PORT_MIN=12000 RTP_PORT_MAX=12199 # Identity provider (OpenVibe.Network) — JWKS fetched at boot for offline # RS256 verification of user JWTs on browser endpoints. OV_NETWORK_URL=https://openvibe.network OV_NETWORK_INTERNAL_URL=http://127.0.0.1:4000 # Owner subjects (docs/object-model.md#owner-subjects): objects that name only an app-local owner # get the owner's Network subject from POST /internal/identity/resolve-batch, with a service token # (OV_OAUTH_CLIENT_ID / OV_OAUTH_CLIENT_SECRET, capability identity.subject.resolve). # MEDIA_OWNER_SUBJECT_SYNC=0 stops the reconcile job (before rolling a backfill back). MEDIA_OWNER_SUBJECT_SYNC= MEDIA_OWNER_SUBJECT_INTERVAL_MIN=10 # OpenVibe.Live lineage (server/lineage-client.js): channel owners are resolved through Live's canonical # resolver with Media's service token (OV_OAUTH_CLIENT_ID / OV_OAUTH_CLIENT_SECRET, audience # openvibe.live, OV_LIVE_INTERNAL_URL) instead of a local username mapping. MEDIA_LINEAGE=off disables it. MEDIA_LINEAGE= # ── First-party tenants ────────────────────────────────────── # Created at boot (server/auth.js ensureTokenOnlyApps) and reached only with each service's Network service token. # No app key is issued or seeded (MEDIA_APPS_SEED and MEDIA_APP_KEYS are gone, compatibility register C-71). # ── Developer-project tenants (ADR-014) ────────────────────── # Created on first use by a developer app token (Network, audience openvibe.media) for its own # project: /api/v1/prj_/files, /api/v2/prj_/objects. Default quotas in MB, applied # when the tenant is created (change apps.quota_bytes afterwards to adjust one tenant). MEDIA_APP_TENANT_QUOTA_MB=1024 MEDIA_APP_SANDBOX_QUOTA_MB=100 # ── Per-actor rate limits (server/actor-limits.js, roadmap WS-R task 4) ── # Requests each caller (a Network token's principal, a signed-in person, an app acting for one of its # users) may make to one read route per minute and per hour; past them: 429 rate_limited + Retry-After. # Uploads, deletes and jobs have their own, tighter numbers in the routes. An app's own API key is not counted. MEDIA_LIMITS_MINUTE=120 MEDIA_LIMITS_HOUR=3000 # ── Object storage tiering (optional; local-only when unset) ─ # Backblaze B2 (canonical cold tier) MEDIA_B2_ENDPOINT= MEDIA_B2_REGION=us-west-004 MEDIA_B2_BUCKET= MEDIA_B2_KEY_ID= MEDIA_B2_APP_KEY= # Cloudflare R2 (popularity cache, free egress) MEDIA_R2_ENDPOINT= MEDIA_R2_REGION=auto MEDIA_R2_BUCKET= MEDIA_R2_ACCESS_KEY_ID= MEDIA_R2_SECRET_ACCESS_KEY= # Placement capability probe (server/placement/providers.js): each provider is probed for range/206/ETag/If-Range. # report (default) records the results (health, admin placement view) and takes nothing out of routing; enforce # removes a provider from every class whose requirements its last completed probe failed. Enforce only after the # production results were read: S3-compatible APIs differ, and If-Range especially. MEDIA_CAPABILITY_GATE=report # Placement demand (server/placement/demand.js): the region tag on the Valkey demand counters and the hot sorted # set (object × region × 5-minute bucket). One Media host is one region; default local. #MEDIA_DEMAND_REGION=local # Media shield (server/placement/shield.js): hand eligible public reads to the nginx edge cache # (X-Accel-Redirect) instead of streaming them from this process, for the comma-separated providers # listed. It answers only on MEDIA_SHIELD_HOST (a DNS-only edge name, never through Cloudflare's proxy); # MEDIA_SHIELD_REFRESH_URL is the loopback purge listener used to refresh cached slices. #MEDIA_SHIELD=b2 #MEDIA_SHIELD_HOST=edge.openvibe.media #MEDIA_SHIELD_REFRESH_URL=http://127.0.0.1:8479 # ── Recording knobs (defaults match the inherited recorder) ── # VOD_STOP_GRACE_MS=60000 # VOD_DISK_WARN_GB=15 # VOD_DISK_CRIT_GB=5 # Under this much free space the largest running recording is stopped (one per minute), so a long stream cannot fill # the disk PostgreSQL and every service share. Under VOD_DISK_CRIT_GB only new recordings are refused. # VOD_DISK_STOP_GB=3 # While an MP4 recording is written, its DVR is the playlist over the file itself (/v/:id/dvr.m3u8). 0 also stops the # periodic seekable copy of MP4 recordings (twice the disk and the I/O of a long stream) once every DVR player reads # the playlist; WebM recordings keep their copy. # VOD_DVR_SNAPSHOT_MP4=1 # MAX_VOD_SIZE_MB=2048 # VOD_NO_NORMALIZE=0 # OpenVibe.Events (durable events): vod/clip/object/storage outcomes are also published through # Media's outbox with the OAuth client credentials (grant events.event.publish). Unset = off. #EVENTS_URL=http://127.0.0.1:4300 # IndexNow (openvibe-shared/indexnow): a key makes search engines recrawl a public page when it # appears or changes. 8–128 hex/alphanumeric characters; the key file is served at /.txt. # Unset = off, no key file, nothing sent. #INDEXNOW_KEY= # ── Object uploads (docs/object-model.md#presigned-uploads, #multipart-uploads) ── # MEDIA_SIGNING_SECRET signs presigned upload/download URLs; set it in production. #MEDIA_SIGNING_SECRET= #MEDIA_OBJECT_MAX_MB=256 # single-part limit; larger objects use multipart #MEDIA_UPLOAD_TOKEN_TTL_S=3600 # default presigned upload URL lifetime #MEDIA_MULTIPART_MAX_MB=20480 #MEDIA_MULTIPART_PART_MB=64 # bounds: MEDIA_MULTIPART_MIN_PART_MB=5, MEDIA_MULTIPART_MAX_PART_MB=256 # ── S3 surface and zones (ADR-031; server/s3/, docs/s3.md) ── # MEDIA_S3_KEY_SECRET (32+ characters) encrypts the S3 access keys' secrets; unset = the S3 surface answers 503 and no # key can be issued. Changing it makes every issued key unusable. MEDIA_S3_HOSTS: the Host names the surface answers. #MEDIA_S3_KEY_SECRET= #MEDIA_S3_HOSTS=s3.openvibe.media,s3.openvibe.network #MEDIA_MULTIPART_TTL_HOURS=24 #MEDIA_UPLOAD_MIN_FREE_MB=10240 # free disk kept in reserve by multipart uploads and split/remux jobs # ── Jobs (docs/object-model.md#jobs) ───────────────────────── #MEDIA_JOBS_ENABLED=1 #MEDIA_JOBS_POLL_MS=5000 #MEDIA_JOBS_LIGHT_CONCURRENCY=2 #MEDIA_JOBS_HEAVY_CONCURRENCY=1 #MEDIA_JOBS_HEAVY_WHILE_RECORDING=0 #MEDIA_INVARIANT_SCAN_HOURS=24 # the size-invariant validator (proposes split/remux; never runs them) #MEDIA_MOVE_CLEANUP_MINUTES=60 # storage.move.cleanup: alert on objects whose placement move failed 3 times (0 = never) # ── Segment-native video (docs/media-fabric.md §3, F3.1) ───── # 1 = the object.cmaf job (CMAF segments of a finished video, indexed in media_timeline), the HLS routes # /o/:id/master.m3u8, /o/:id/source/… and the hls_url field of GET …/download?format=json ; off (default): # the job is refused and the routes 404, nothing else changes. #MEDIA_HLS_ENABLED=0 # Encode incompatible source streams as H.264/AAC (CPU intensive; off by default). #MEDIA_CMAF_TRANSCODE=0 # Maximum duration in seconds of a source needing transcoding (default 4 h). #MEDIA_CMAF_TRANSCODE_MAX_SECONDS=14400 # libx264 threads for one transcode (it also runs at nice 15: the heavy lane only waits for recordings before starting). #MEDIA_CMAF_TRANSCODE_THREADS=2 # Maximum bytes of local timeline segments and packed chunks (default 10 GiB; 0 keeps only durable copies). #MEDIA_TIMELINE_LOCAL_MAX_BYTES=10737418240 # Stop keeping new local timeline copies below this free space (default 8 GiB). #MEDIA_TIMELINE_LOCAL_MIN_FREE_BYTES=8589934592 # At most this many old VODs are lazily queued per hour; recording finalize is exempt (default 20). #MEDIA_HLS_BACKFILL_PER_HOUR=20 # Lifetime in seconds of a signed HLS playlist (download?format=json → hls_url of a private or sandbox object; its own # signature purpose, accepted only by the HLS routes). Default 21600 (6 h), clamped to 60..43200. #MEDIA_HLS_PLAYLIST_TTL_S=21600 # Materialized clips (docs/materialized-clips.md, F3.6): 1 = with MEDIA_HLS_ENABLED and a source that has a source # timeline, clip.cut materializes the clip over the source's bytes — the interior rows name the source's segments and # only the two window edges are re-encoded; 0 (default) = every clip is cut with the full re-encode as before. #MEDIA_MATERIALIZED_CLIPS=0