openvibers/openvibe.media Commits
docs/media-fabric.md: the accepted design for Media's storage and delivery (owner, 2026-09-29). Two fabrics (online: B2 canonical, R2 Standard hot, Bunny/CDN, the Media host's NVMe edge; archive/backup separately, deep archive never online); provider classes with capability probes and live prices in media.cost_tiers; segment-native CMAF/HLS video with a growing DVR playlist, write-behind segment durability, packing after finalize, keyframes forced on the source's boundaries and one timeline index; clips that reference the source; derivatives as cost-managed computed caches (AV1 for viral VODs); one placement engine per object x region x segment bucket under budgets with two-phase moves, dry-run and a simulator; sticky, canaried multi-CDN routing with a single-flight origin shield and tokenized private media; one telemetry schema, Valkey rollups, player QoE and seek SLOs, cascaded control loops and provider-bill calibration. Build order F1-F6.
Changes
nnew file mode 100644@@@ -0,0 +1,189 @@++# The Media Fabric++++**Status:** accepted design, 2026-09-29 (owner direction: "R2 is for faster storage than B2 and it should auto detect++and load balance automatically while maximizing cost effectiveness and not wasting", plus the Media Fabric write-up of++the same day). Supersedes the VOD-only R2 popularity sweep and the separate native-object tiering. Build order at the++end; each phase ships alone.++++Media is the authority for every stored byte on the network: objects, timelines, variants, manifests, clips,++thumbnails, captions, placement, storage and delivery. Products (Live, Video, Pics, Download, Community, Games) are++presentation; they ask Media for an object and a delivery URL and never know where the bytes live.++++## 1. Two fabrics, never mixed++++**Online (the Media Fabric).** Every object a viewer can ask for has an immediately retrievable copy:++++```++viewer → media.openvibe.network → delivery router++ ├─ OpenVibe edge (the Media host's NVMe cache; later edge nodes) capacity-bounded, prepaid bandwidth++ ├─ global CDN (Bunny in front of B2; Cloudflare only in front of R2)++ └─ hot object tier: R2 Standard++ └─ origin shield (nginx slice + cache lock) → B2: the canonical online copy++```++++**Archive/backup (separate).** PostgreSQL (pgBackRest), host snapshots, configs and optional long-term archives go to++B2 (warm) and, optionally, a deep-archive provider (Glacier Deep Archive, GCS Archive). Nothing online ever fails over++to deep archive; a restore from it is an operator action, drilled like every other restore.++++## 2. Provider classes, not provider names++++Business logic asks for a class; providers declare capabilities; the fabric picks.++++| Class | Today | Capabilities that make a provider eligible |++|---|---|---|++| `online-canonical` | B2 | durable (≥ 11 nines), no minimum duration, free ops, range + 206 + ETag |++| `online-hot` | R2 Standard | low TTFB, zero egress, range + 206; **not** R2 Infrequent Access for anything seekable (retrieval fee, 30-day minimum) |++| `global-cdn` | Bunny (origin B2, free Alliance egress); Cloudflare with R2 as origin | range from cache or cache slicing, request coalescing, origin shield, token auth, purge API |++| `regional-cache` | Media host NVMe (nginx) | prepaid bandwidth up to the port; single-flight per object+range |++| `bulk-download` | B2/R2 + CDN mirrors | range/resume, checksums |++| `warm-backup`, `deep-archive` | B2; Glacier/GCS Archive | backup fabric only |++++Each adapter passes a capability probe before it becomes eligible for a class: `supports_range`, `supports_206`,++`supports_if_range`, `supports_etag`, `supports_cache_range`, `token_auth`, `purge`. The probe runs at boot and every++hour; a failed capability removes the provider from that class until it passes again.++++**Economic facts the engine is built on (official price pages, fetched 2026-09-29; kept in the revisioned++`media.cost_tiers` config, never in code):** R2 Standard $0.015/GB-month, Class A $4.50/M, Class B $0.36/M, zero egress;++R2 IA $0.01/GB-month + $0.01/GB retrieval + 30-day minimum; B2 ≈ $6–6.95/TB-month, API calls free, egress free up to 3×++stored data then $0.01/GB, and **unlimited free egress to Alliance CDNs (Bunny, Cloudflare, Fastly, …)**. Consequences:++++- Behind a CDN, B2 egress costs nothing. R2 earns its place through **latency** (cold B2 TTFB is hundreds of ms) and by++ **shielding B2's request ceiling** (≈ 500 requests/s per account by default), not through egress savings.++- For small objects (segments, thumbnails) **R2 request charges dominate**, not storage: promote by requests saved per++ dollar, cache presigned URLs, and let the CDN absorb repeat reads.++- Writes go to B2 first (free PUTs); R2 PUTs (Class A) are spent only on promotion.++- **Cloudflare's terms restrict video through its CDN to its paid media products (R2, Stream, Images):** video cached by++ Cloudflare must come from an R2 origin (custom domain); B2-origin video goes through Bunny. Re-verify the terms before++ enabling a Cloudflare video path.++- The Media host's bandwidth is prepaid: the router fills the OpenVibe edge first, up to a capacity ceiling (70 % of the++ port by default), then spills to CDN/R2.++++## 3. Video is segment-native (CMAF under HLS)++++A video object is a timeline of fragmented-MP4 (CMAF) segments, not a file:++++```++med_xyz/master.m3u8++med_xyz/source/{init.mp4, 000001.m4s, …} canonical, durable++med_xyz/{720p,480p,360p,av1-…}/… derivatives, rebuildable++med_xyz/{captions/*.vtt, storyboard.webp+.vtt, waveform, metadata}++```++++- **Live → DVR → VOD without a conversion step.** OpenRe segments continuously; the playlist grows while live (seek++ anywhere from the start to the live edge) and gets `#EXT-X-ENDLIST` at the end. Live latency modes: WebRTC (sub-second,++ SFU), LL-HLS (partial segments, ~2–4 s), HLS DVR. When the SFU nears capacity, new viewers move to LL-HLS.++- **Write-behind durability for live.** A segment lands on the ingest node's NVMe first (served from RAM/NVMe for the++ first minutes: `segment_age` is a routing input), then uploads to B2 asynchronously. A segment is *durable* once B2++ confirms it; the timeline records that. A crash loses only the not-yet-uploaded tail, bounded by the upload lag,++ which is a metric with an alert.++- **Packing after finalize.** Two-second segments are perfect while live but produce thousands of objects per hour.++ After finalize, segments are packed into ~60-second chunk objects (byte-range playlists, `EXT-X-BYTERANGE`): ~30×++ fewer objects, PUTs, HEADs and metadata rows, while placement stays at minute granularity.++- **Keyframes.** Browser (WHIP) sources have irregular keyframes, so the source representation segments on the source's++ own keyframes (variable segment duration is legal HLS). Every derived rendition forces keyframes at exactly the++ source's segment boundaries (`-force_key_frames` from the timeline), so ABR switches and clips line up everywhere.++- **The timeline index** (`media_timeline`: object, rendition, segment number, start/end time, keyframe time, packed++ object + byte offset/length, sha256, durability) answers "the bytes for 01:47:12.350" without opening a file, and is++ the shared axis for DVR, seek, clips, sprites, captions, transcripts, chapters, AI moments and moderation markers.++- **MP4 stays for downloads and old clients:** a remuxed faststart MP4 (lazy, cached like any derivative) with plain++ HTTP ranges.++++## 4. Clips reuse the source++++- **Virtual clip** (default): `{source, start, end}` → a clipped manifest over the source's segments. No bytes copied.++- **Materialized clip** (shared externally, downloaded, edited, popular): middle GOPs are **referenced**, not copied++ (the new manifest points at the source's packed chunks); only the two boundary segments are re-encoded for++ frame-accurate cuts. Segments and chunks are content-addressed (sha256) with reference counts, so deleting a VOD++ that a clip still references keeps the referenced chunks (and holds still freeze everything).++++## 5. Derivatives are cost-managed computed caches++++Canonical source media is durable. Renditions, codecs (AV1/HEVC), storyboards and sprites, waveforms, image sizes and++formats (AVIF/WebP), preview clips and AI thumbnails are `rebuildable = true`:++++- generated **on demand** (the first request enqueues the job and is served from the source meanwhile) or **ahead of++ demand** when the class predicts it (a live stream with a big audience gets 480p/720p while live; a trending VOD gets++ its ladder prewarmed);++- **kept only while cheaper than regenerating**: keep if `storage_cost(horizon) + delivery_cost < p(regenerate) ×++ regeneration_cost`, else drop and rebuild on the next request;++- **codec economics:** for a viral VOD, an AV1 rendition is produced when the egress/CDN bytes it saves exceed its encode++ cost; for a cold VOD, never;++- compute is placed by the Compute fabric (cheapest available CPU/GPU, `openvibe-sdk/placement`).++++## 6. Placement: one engine, every class, down to the segment bucket++++- **Hotness is per object × region × segment bucket** (5-minute buckets for video: beginnings are hotter than middles).++ A two-hour VOD can have its first 10 minutes on R2 + CDN + edge, minutes 10–40 on R2, the rest only on B2, and++ promote minute 60 when viewers start landing there.++- **Decision rule:** value-per-dollar greedy under per-class budgets (the knapsack is near-optimal when costs are++ monotone): expected requests and bytes saved × latency value, against storage + requests + retrieval + minimum++ residency, with hysteresis (promote above P, demote below D < P), minimum residency, and rate limits on moves.++- **Intents** set class defaults: `video` (segments, seek-sensitive), `image` (tiny, cache hard), `download`++ (throughput + cost, mirrors, resume), `game-asset` (small hot assets global; large packs mirrored), `attachment`,++ `backup` (archive fabric only).++- **Every move is two-phase:** verify the new copy (size + sha256), commit the location in one transaction, then++ delete old bytes after commit, with a retrying cleanup job and an alert after three failures. The row never points++ at bytes that do not exist.++- **Dry-run and simulator:** every policy can run in dry-run mode (logs what it would do and what it would cost), and a++ simulator replays recorded telemetry against a candidate policy before it is enabled.++++## 7. Delivery: sticky, measured, canaried++++- **Route epochs:** a playback session gets a route (primary + fallbacks, TTL ~30 s) and keeps it until a health++ failure, a significant latency change, a capacity or cost guard, or TTL; changes happen at segment boundaries.++- **Canary shifts:** moving traffic between routes goes 1 % → 5 % → 25 % → 50 % → 100 %, watching errors, TTFB,++ throughput, seek failures, player stalls and cost, and reverts automatically.++- **Origin protection:** the OpenVibe edge collapses concurrent misses (nginx `proxy_cache_lock` + `slice` = one upstream++ fetch per object+range per shield); Bunny's origin shield and request coalescing in front of B2; presigned-URL LRU in++ Media for provider request costs.++- **Access control never leaks to caches:** private and unlisted media use tokenized URLs (Bunny token auth, signed R2++ URLs); the cache key excludes the token; private bytes are never pushed to a public cache.++- **Deletion reaches everything:** a delete, DMCA takedown or account erasure (ADR-033) removes every replica and++ derivative and purges every CDN path, and is verified.++++## 8. The closed loop++++```++request/playback → telemetry → Events → {Hotness, Health, Cost} engines → Placement engine → route/replica change → Delivery++```++++- **One telemetry sample schema** (`openvibe.telemetry.sample@1`: service, project, resource, provider, node, region,++ operation, bytes, duration, ttfb, throughput, status, cache status, cost estimate, route epoch, trace id), shared by++ Media, OpenRe, Host, AI, Events and Compute.++- **Aggregation, not rows per request:** nginx JSON access logs and player beacons are folded into rolling Valkey++ counters (EWMA + p50/p95/p99 over 10 s, 1 min, 5 min, 1 h, 24 h), HyperLogLog for unique viewers, sorted sets for the++ top-N hot objects and buckets; PostgreSQL gets 5-minute rollups per object × region × bucket; raw samples go to++ object storage in batches.++- **Player QoE counts, not only HTTP:** startup time, rebuffers, fatal errors, seek latency, quality switches, bitrate,++ live-edge delay (privacy-bounded, a random session id, no user id). **SLOs:** cold seek p95 < 500 ms to the first++ playable segment, hot seek < 200 ms, live rewind < 150 ms, startup p95 < 1.5 s.++- **Loops cooperate by time constant and precedence:** player ABR (seconds) ⊂ delivery routing (30 s–5 min) ⊂ placement++ (minutes–hours) ⊂ cost (hours–days). Each outer loop only constrains the inner (budgets bound placement; placement++ bounds routes); every loop uses hysteresis and rate limits.++- **Events emitted:** `media.object.hot`, `media.replica.requested|ready|draining|evicted`, `media.variant.requested|ready`,++ `media.region.hot`, `media.delivery.surge|degraded`, `provider.health.degraded`, `provider.capacity.warning`,++ `provider.cost.threshold`.++++## 9. Cost truth++++`media.cost_tiers` holds list prices; estimates drive decisions; **provider bills calibrate them**: R2 analytics++(GraphQL), B2 usage reports, Bunny statistics API are imported daily, compared with the estimate, and the gap is a++metric. Budgets per class and provider with a forecast; `provider.cost.threshold` at 90 % shifts non-critical traffic.++++## 10. Build order++++- **F1 foundation (now):** provider registry + capability probes; `media.cost_tiers` + per-class budgets in a revisioned++ `media.storage_policy` (retiring `media.storage_tier`/`media.object_tier`); the read router (EWMA latency, error++ circuit breaker, continuous health probe, fastest healthy copy) in `server/placement/`; presigned-URL LRU; two-phase++ moves with a cleanup job; per-class hysteresis bands; `media_storage_alerts_total`, provider latency histograms; the++ corrected price table; the nginx shield (slice + cache lock) on the Media host.++- **F2 one placement engine:** every class through one sweep, value-per-dollar under budgets, dry-run + simulator,++ decision log with class and reason, the events above, Valkey rollups of demand.++- **F3 segment-native video:** CMAF/HLS recording with the growing DVR playlist, the timeline index, packing after++ finalize, virtual clips, sprites and captions on the timeline, MP4 fallback; Live's player moves to HLS.++- **F4 reactive derivatives:** on-demand renditions and image variants, keep-vs-regenerate economics, AV1 for viral++ VODs, compute placement.++- **F5 multi-CDN delivery:** Bunny path (B2 origin), R2 custom-domain path, OpenVibe edge first up to capacity, route++ epochs, canary shifts with auto-revert, player QoE beacons, seek SLOs.++- **F6 cost truth:** bill imports, forecasts, budget guards, the owner dashboard.++- **Archive fabric:** backups and snapshots to B2; optional deep archive with restore drills; never online.
new file mode 100644
new file mode 100644
@@ -0,0 +1,189 @@
@@ -0,0 +1,189 @@
+# The Media Fabric
+
+**Status:** accepted design, 2026-09-29 (owner direction: "R2 is for faster storage than B2 and it should auto detect
+and load balance automatically while maximizing cost effectiveness and not wasting", plus the Media Fabric write-up of
+the same day). Supersedes the VOD-only R2 popularity sweep and the separate native-object tiering. Build order at the
+end; each phase ships alone.
+
+Media is the authority for every stored byte on the network: objects, timelines, variants, manifests, clips,
+thumbnails, captions, placement, storage and delivery. Products (Live, Video, Pics, Download, Community, Games) are
+presentation; they ask Media for an object and a delivery URL and never know where the bytes live.
+
+## 1. Two fabrics, never mixed
+
+**Online (the Media Fabric).** Every object a viewer can ask for has an immediately retrievable copy:
+
+```
+viewer → media.openvibe.network → delivery router
+ ├─ OpenVibe edge (the Media host's NVMe cache; later edge nodes) capacity-bounded, prepaid bandwidth
+ ├─ global CDN (Bunny in front of B2; Cloudflare only in front of R2)
+ └─ hot object tier: R2 Standard
+ └─ origin shield (nginx slice + cache lock) → B2: the canonical online copy
+```
+
+**Archive/backup (separate).** PostgreSQL (pgBackRest), host snapshots, configs and optional long-term archives go to
+B2 (warm) and, optionally, a deep-archive provider (Glacier Deep Archive, GCS Archive). Nothing online ever fails over
+to deep archive; a restore from it is an operator action, drilled like every other restore.
+
+## 2. Provider classes, not provider names
+
+Business logic asks for a class; providers declare capabilities; the fabric picks.
+
+| Class | Today | Capabilities that make a provider eligible |
+|---|---|---|
+| `online-canonical` | B2 | durable (≥ 11 nines), no minimum duration, free ops, range + 206 + ETag |
+| `online-hot` | R2 Standard | low TTFB, zero egress, range + 206; **not** R2 Infrequent Access for anything seekable (retrieval fee, 30-day minimum) |
+| `global-cdn` | Bunny (origin B2, free Alliance egress); Cloudflare with R2 as origin | range from cache or cache slicing, request coalescing, origin shield, token auth, purge API |
+| `regional-cache` | Media host NVMe (nginx) | prepaid bandwidth up to the port; single-flight per object+range |
+| `bulk-download` | B2/R2 + CDN mirrors | range/resume, checksums |
+| `warm-backup`, `deep-archive` | B2; Glacier/GCS Archive | backup fabric only |
+
+Each adapter passes a capability probe before it becomes eligible for a class: `supports_range`, `supports_206`,
+`supports_if_range`, `supports_etag`, `supports_cache_range`, `token_auth`, `purge`. The probe runs at boot and every
+hour; a failed capability removes the provider from that class until it passes again.
+
+**Economic facts the engine is built on (official price pages, fetched 2026-09-29; kept in the revisioned
+`media.cost_tiers` config, never in code):** R2 Standard $0.015/GB-month, Class A $4.50/M, Class B $0.36/M, zero egress;
+R2 IA $0.01/GB-month + $0.01/GB retrieval + 30-day minimum; B2 ≈ $6–6.95/TB-month, API calls free, egress free up to 3×
+stored data then $0.01/GB, and **unlimited free egress to Alliance CDNs (Bunny, Cloudflare, Fastly, …)**. Consequences:
+
+- Behind a CDN, B2 egress costs nothing. R2 earns its place through **latency** (cold B2 TTFB is hundreds of ms) and by
+ **shielding B2's request ceiling** (≈ 500 requests/s per account by default), not through egress savings.
+- For small objects (segments, thumbnails) **R2 request charges dominate**, not storage: promote by requests saved per
+ dollar, cache presigned URLs, and let the CDN absorb repeat reads.
+- Writes go to B2 first (free PUTs); R2 PUTs (Class A) are spent only on promotion.
+- **Cloudflare's terms restrict video through its CDN to its paid media products (R2, Stream, Images):** video cached by
+ Cloudflare must come from an R2 origin (custom domain); B2-origin video goes through Bunny. Re-verify the terms before
+ enabling a Cloudflare video path.
+- The Media host's bandwidth is prepaid: the router fills the OpenVibe edge first, up to a capacity ceiling (70 % of the
+ port by default), then spills to CDN/R2.
+
+## 3. Video is segment-native (CMAF under HLS)
+
+A video object is a timeline of fragmented-MP4 (CMAF) segments, not a file:
+
+```
+med_xyz/master.m3u8
+med_xyz/source/{init.mp4, 000001.m4s, …} canonical, durable
+med_xyz/{720p,480p,360p,av1-…}/… derivatives, rebuildable
+med_xyz/{captions/*.vtt, storyboard.webp+.vtt, waveform, metadata}
+```
+
+- **Live → DVR → VOD without a conversion step.** OpenRe segments continuously; the playlist grows while live (seek
+ anywhere from the start to the live edge) and gets `#EXT-X-ENDLIST` at the end. Live latency modes: WebRTC (sub-second,
+ SFU), LL-HLS (partial segments, ~2–4 s), HLS DVR. When the SFU nears capacity, new viewers move to LL-HLS.
+- **Write-behind durability for live.** A segment lands on the ingest node's NVMe first (served from RAM/NVMe for the
+ first minutes: `segment_age` is a routing input), then uploads to B2 asynchronously. A segment is *durable* once B2
+ confirms it; the timeline records that. A crash loses only the not-yet-uploaded tail, bounded by the upload lag,
+ which is a metric with an alert.
+- **Packing after finalize.** Two-second segments are perfect while live but produce thousands of objects per hour.
+ After finalize, segments are packed into ~60-second chunk objects (byte-range playlists, `EXT-X-BYTERANGE`): ~30×
+ fewer objects, PUTs, HEADs and metadata rows, while placement stays at minute granularity.
+- **Keyframes.** Browser (WHIP) sources have irregular keyframes, so the source representation segments on the source's
+ own keyframes (variable segment duration is legal HLS). Every derived rendition forces keyframes at exactly the
+ source's segment boundaries (`-force_key_frames` from the timeline), so ABR switches and clips line up everywhere.
+- **The timeline index** (`media_timeline`: object, rendition, segment number, start/end time, keyframe time, packed
+ object + byte offset/length, sha256, durability) answers "the bytes for 01:47:12.350" without opening a file, and is
+ the shared axis for DVR, seek, clips, sprites, captions, transcripts, chapters, AI moments and moderation markers.
+- **MP4 stays for downloads and old clients:** a remuxed faststart MP4 (lazy, cached like any derivative) with plain
+ HTTP ranges.
+
+## 4. Clips reuse the source
+
+- **Virtual clip** (default): `{source, start, end}` → a clipped manifest over the source's segments. No bytes copied.
+- **Materialized clip** (shared externally, downloaded, edited, popular): middle GOPs are **referenced**, not copied
+ (the new manifest points at the source's packed chunks); only the two boundary segments are re-encoded for
+ frame-accurate cuts. Segments and chunks are content-addressed (sha256) with reference counts, so deleting a VOD
+ that a clip still references keeps the referenced chunks (and holds still freeze everything).
+
+## 5. Derivatives are cost-managed computed caches
+
+Canonical source media is durable. Renditions, codecs (AV1/HEVC), storyboards and sprites, waveforms, image sizes and
+formats (AVIF/WebP), preview clips and AI thumbnails are `rebuildable = true`:
+
+- generated **on demand** (the first request enqueues the job and is served from the source meanwhile) or **ahead of
+ demand** when the class predicts it (a live stream with a big audience gets 480p/720p while live; a trending VOD gets
+ its ladder prewarmed);
+- **kept only while cheaper than regenerating**: keep if `storage_cost(horizon) + delivery_cost < p(regenerate) ×
+ regeneration_cost`, else drop and rebuild on the next request;
+- **codec economics:** for a viral VOD, an AV1 rendition is produced when the egress/CDN bytes it saves exceed its encode
+ cost; for a cold VOD, never;
+- compute is placed by the Compute fabric (cheapest available CPU/GPU, `openvibe-sdk/placement`).
+
+## 6. Placement: one engine, every class, down to the segment bucket
+
+- **Hotness is per object × region × segment bucket** (5-minute buckets for video: beginnings are hotter than middles).
+ A two-hour VOD can have its first 10 minutes on R2 + CDN + edge, minutes 10–40 on R2, the rest only on B2, and
+ promote minute 60 when viewers start landing there.
+- **Decision rule:** value-per-dollar greedy under per-class budgets (the knapsack is near-optimal when costs are
+ monotone): expected requests and bytes saved × latency value, against storage + requests + retrieval + minimum
+ residency, with hysteresis (promote above P, demote below D < P), minimum residency, and rate limits on moves.
+- **Intents** set class defaults: `video` (segments, seek-sensitive), `image` (tiny, cache hard), `download`
+ (throughput + cost, mirrors, resume), `game-asset` (small hot assets global; large packs mirrored), `attachment`,
+ `backup` (archive fabric only).
+- **Every move is two-phase:** verify the new copy (size + sha256), commit the location in one transaction, then
+ delete old bytes after commit, with a retrying cleanup job and an alert after three failures. The row never points
+ at bytes that do not exist.
+- **Dry-run and simulator:** every policy can run in dry-run mode (logs what it would do and what it would cost), and a
+ simulator replays recorded telemetry against a candidate policy before it is enabled.
+
+## 7. Delivery: sticky, measured, canaried
+
+- **Route epochs:** a playback session gets a route (primary + fallbacks, TTL ~30 s) and keeps it until a health
+ failure, a significant latency change, a capacity or cost guard, or TTL; changes happen at segment boundaries.
+- **Canary shifts:** moving traffic between routes goes 1 % → 5 % → 25 % → 50 % → 100 %, watching errors, TTFB,
+ throughput, seek failures, player stalls and cost, and reverts automatically.
+- **Origin protection:** the OpenVibe edge collapses concurrent misses (nginx `proxy_cache_lock` + `slice` = one upstream
+ fetch per object+range per shield); Bunny's origin shield and request coalescing in front of B2; presigned-URL LRU in
+ Media for provider request costs.
+- **Access control never leaks to caches:** private and unlisted media use tokenized URLs (Bunny token auth, signed R2
+ URLs); the cache key excludes the token; private bytes are never pushed to a public cache.
+- **Deletion reaches everything:** a delete, DMCA takedown or account erasure (ADR-033) removes every replica and
+ derivative and purges every CDN path, and is verified.
+
+## 8. The closed loop
+
+```
+request/playback → telemetry → Events → {Hotness, Health, Cost} engines → Placement engine → route/replica change → Delivery
+```
+
+- **One telemetry sample schema** (`openvibe.telemetry.sample@1`: service, project, resource, provider, node, region,
+ operation, bytes, duration, ttfb, throughput, status, cache status, cost estimate, route epoch, trace id), shared by
+ Media, OpenRe, Host, AI, Events and Compute.
+- **Aggregation, not rows per request:** nginx JSON access logs and player beacons are folded into rolling Valkey
+ counters (EWMA + p50/p95/p99 over 10 s, 1 min, 5 min, 1 h, 24 h), HyperLogLog for unique viewers, sorted sets for the
+ top-N hot objects and buckets; PostgreSQL gets 5-minute rollups per object × region × bucket; raw samples go to
+ object storage in batches.
+- **Player QoE counts, not only HTTP:** startup time, rebuffers, fatal errors, seek latency, quality switches, bitrate,
+ live-edge delay (privacy-bounded, a random session id, no user id). **SLOs:** cold seek p95 < 500 ms to the first
+ playable segment, hot seek < 200 ms, live rewind < 150 ms, startup p95 < 1.5 s.
+- **Loops cooperate by time constant and precedence:** player ABR (seconds) ⊂ delivery routing (30 s–5 min) ⊂ placement
+ (minutes–hours) ⊂ cost (hours–days). Each outer loop only constrains the inner (budgets bound placement; placement
+ bounds routes); every loop uses hysteresis and rate limits.
+- **Events emitted:** `media.object.hot`, `media.replica.requested|ready|draining|evicted`, `media.variant.requested|ready`,
+ `media.region.hot`, `media.delivery.surge|degraded`, `provider.health.degraded`, `provider.capacity.warning`,
+ `provider.cost.threshold`.
+
+## 9. Cost truth
+
+`media.cost_tiers` holds list prices; estimates drive decisions; **provider bills calibrate them**: R2 analytics
+(GraphQL), B2 usage reports, Bunny statistics API are imported daily, compared with the estimate, and the gap is a
+metric. Budgets per class and provider with a forecast; `provider.cost.threshold` at 90 % shifts non-critical traffic.
+
+## 10. Build order
+
+- **F1 foundation (now):** provider registry + capability probes; `media.cost_tiers` + per-class budgets in a revisioned
+ `media.storage_policy` (retiring `media.storage_tier`/`media.object_tier`); the read router (EWMA latency, error
+ circuit breaker, continuous health probe, fastest healthy copy) in `server/placement/`; presigned-URL LRU; two-phase
+ moves with a cleanup job; per-class hysteresis bands; `media_storage_alerts_total`, provider latency histograms; the
+ corrected price table; the nginx shield (slice + cache lock) on the Media host.
+- **F2 one placement engine:** every class through one sweep, value-per-dollar under budgets, dry-run + simulator,
+ decision log with class and reason, the events above, Valkey rollups of demand.
+- **F3 segment-native video:** CMAF/HLS recording with the growing DVR playlist, the timeline index, packing after
+ finalize, virtual clips, sprites and captions on the timeline, MP4 fallback; Live's player moves to HLS.
+- **F4 reactive derivatives:** on-demand renditions and image variants, keep-vs-regenerate economics, AV1 for viral
+ VODs, compute placement.
+- **F5 multi-CDN delivery:** Bunny path (B2 origin), R2 custom-domain path, OpenVibe edge first up to capacity, route
+ epochs, canary shifts with auto-revert, player QoE beacons, seek SLOs.
+- **F6 cost truth:** bill imports, forecasts, budget guards, the owner dashboard.
+- **Archive fabric:** backups and snapshots to B2; optional deep archive with restore drills; never online.