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