openvibers/openvibe.media CHANGELOG.md
CHANGELOG.md
| 1 | # Changelog |
| 2 | |
| 3 | ## Unreleased |
| 4 | |
| 5 | - **The shared composer's attachments (2026-11-10, composer.attachment@1).** `POST /api/v1/attachments` takes one |
| 6 | multipart `file` from a person (a Network user JWT, or a first-party site's service token naming the person in |
| 7 | `X-OV-Subject`), sniffs the kind from the bytes — image, video, audio, file or code — and refuses an executable. |
| 8 | Limits are image 20 MB, video 300 MB, audio 50 MB, anything else 25 MB; images get a 640 px JPEG thumbnail and a |
| 9 | video its first frame (ffmpeg). `POST /api/v1/attachments/:id/keep` names the message it belongs to, `DELETE` removes |
| 10 | the person's own file, and an unkept attachment is swept 24 hours after upload (migrations/0027_attachments.sql). |
| 11 | Bytes are served at `/a/<id>/<name>`; CORS is the first-party origin list (server/attachments/origins.js). |
| 12 | |
| 13 | - `scripts/orientation-backfill.js` inspects stored JPEG, PNG and WebP originals in batches (dry run by default), bakes |
| 14 | any surviving EXIF orientation into pixels, removes the embedded details, updates object/location hashes and sizes, |
| 15 | and rebuilds 320/640 WebP variants. Images without source orientation data are marked `orientation unknown` for the |
| 16 | owner's later AI review. `--apply` writes one batch; `--all` walks the range. |
| 17 | |
| 18 | - **A recording that does not decode is never published (2026-11-10).** A recording is a stream copy of a WebRTC |
| 19 | ingest, and WebRTC does not retransmit to a recording consumer: a lost RTP fragment arrives as a truncated H.264 |
| 20 | access unit, the muxer writes it, ffprobe calls the file healthy, and Chrome stops the whole playback on it |
| 21 | (MEDIA_ERR_DECODE; VODs 6297/6298, the robot phone's streams, each had a truncated IDR — the keyframe at 1.175 s / 3.120 s — which ffmpeg concealed until a later P-frame at 1.7 s / 3.6 s, while Chrome rejected the IDR). |
| 22 | `finalize` now decodes the opening 10 s with `ffmpeg -xerror` (`server/vod/decode-check.js`): a decoder error line |
| 23 | matters even when a single-threaded ffmpeg exits 0, while the null muxer's DTS warnings from variable frame timing |
| 24 | do not. A failing recording is measured, marked `needs_repair`, quarantined and hidden; `server/vod/vod-repair.js` |
| 25 | re-encodes it in place (H.264/AAC, original timing and duration, verified output), settles the row, lifts the |
| 26 | quarantine, restores visibility, announces `vod.ready` and re-cuts its timeline so the streaming playlist serves the |
| 27 | repaired bytes too. The health job repairs `needs_repair` rows first on its next idle pass, and a deep scan that |
| 28 | finds a decode fault repairs it instead of quarantining it as corrupt. Operators can use |
| 29 | `node scripts/repair-vod-decode.js --ids … [--full] [--apply]`. Tests: `test/decode-check.test.js` (a corrupt |
| 30 | P-frame fixture generated with ffmpeg) and `test/vod-decode-repair.test.js` (finalize marks, repair publishes). |
| 31 | The Media recorder also logs incomplete-RTP-frame warnings, so packet loss is visible at record time. |
| 32 | |
| 33 | - Avatar ingestion, image variants, and the upload-time `POST /api/v2/:app/objects/normalize-image` operation share |
| 34 | `server/images/orientation.js`. The operation applies EXIF orientation to pixels before Sharp drops the source |
| 35 | metadata, for first-party uploaders such as Community. Test: `test/image-orientation.test.js` covers all eight values. |
| 36 | |
| 37 | - A first-party `ai` tenant for OpenVibe.AI's shared image task (`image.picture`): found Commons/Openverse pictures |
| 38 | and made illustrations are stored by Media reference in the blog or wiki tenant for the person the run acts for. |
| 39 | Network grants AI `media.object.upload` there (the AI, blog and wiki namespaces). `server/auth.js`. |
| 40 | |
| 41 | - **Watch ids (plan track U, 2026-11-07): every VOD and clip answers to an 11-character random id.** YouTube's |
| 42 | `/watch?v=` shape, so a watch page cannot be found by counting: 8 random bytes as URL-safe base64 |
| 43 | (`crypto.randomBytes(8).toString('base64url')`), never derived from the numeric id. Media owns VODs and clips, so |
| 44 | Media mints it (`server/watch-ids.js`): at insert for every new row, and in idempotent batches — only where |
| 45 | `watch_id IS NULL` — for the rows that predate it, at boot. One id space across both tables (`mintUnique()` checks |
| 46 | both), so a watch id has exactly one meaning. `watch_id` is on every VOD and clip Media serves (`vodPublic`, |
| 47 | `clipPublic`, so every API read and list), and `GET /api/v1/:app/watch/:watch_id` → `{ kind, id }` resolves one; a |
| 48 | private item still resolves, and its own read applies the visibility rule it always did. Migration |
| 49 | `0024_watch_ids.sql` (expand: a nullable column and a partial unique index per table; no table rewrite). Tests: |
| 50 | `test/watch-ids.test.js` (format, insert, the resolver, the backfill idempotent and leaving none NULL, uniqueness |
| 51 | across both tables, the route). |
| 52 | |
| 53 | - **Audio description on a watch page (X1, consent plan 2026-10-31, "AI and data")**: the player now carries a |
| 54 | `<track kind="descriptions">` for Live's VODs and clips, served same-origin at `/v/:id/descriptions.vtt` (and |
| 55 | `/c/:id/descriptions.vtt`) from the WebVTT Live builds out of its scene notes and speech timeline. Public and link |
| 56 | items are cacheable; a private item's track answers the same 404 as a missing id unless the viewer's Network |
| 57 | session asks for it (that session is forwarded to Live, which checks the owner). Other apps' media is not described: only Live has the scene-note timeline behind it. The **AD** control and |
| 58 | its spoken cues are OpenVibe.Shared's player (openvibe-shared 4.126.0, a pin-round follow-up). Tests: |
| 59 | `test/audio-description.test.js` (public, private-owner, other app; track markup). |
| 60 | |
| 61 | - An avatar a person's profile points at cannot be deleted out from under them (incident 2026-11-06: a cleanup of |
| 62 | public screenshot copies deleted eleven avatars). `media_avatar_refs` (migration `0020_avatar_refs.sql`) records |
| 63 | each profile slot → object; `model.softDelete` refuses an avatar with an unreleased reference and `DELETE` answers |
| 64 | 409 `media.object.avatar_in_use`. The internal `POST /internal/avatar-adopt` and `/internal/avatar-release` routes |
| 65 | write it, and `/internal/avatar-ingest` records it when given `product` and `ref`. Replacing a picture releases the |
| 66 | previous reference in the same transaction; account deletion releases the person's references first. Ingest also |
| 67 | reads our own `/o/<id>` and `/p/<slug>/screenshot` sources locally instead of fetching them, reuses an avatar the |
| 68 | subject already owns, and stores a 1600×500 `shape: 'banner'` crop. Test: `test/avatar-guard.test.js`. |
| 69 | |
| 70 | - Operator retirement for an object with no good copy (`scripts/no-good-copy-report.js --retire <med_id> [--apply]`): |
| 71 | the dry run changes nothing; only an object in the report, not public and under 1 MiB is retired (a public v1 row, a |
| 72 | native public object, good-copy, 1 MiB+ and held objects are refused with the reason). For a legacy VOD/clip the v1 |
| 73 | row decides: a delisted row (`is_public` 0, even with `visibility` still public after the health quarantine), or a |
| 74 | private/unlisted one, makes the object not public even when its `media_objects` row still says public. The delete is |
| 75 | the ordinary soft delete (bytes kept, retention, `media.object.deleted`, restorable), and a |
| 76 | projected VOD/clip has its v1 row made private so the inherited route stops serving it. Test: |
| 77 | `test/retire-no-good-copy.test.js`. |
| 78 | |
| 79 | - A legacy Live paste screenshot can be retired for OpenVibe.Community (2026-11-06). The internal |
| 80 | `POST /internal/community/legacy-paste-screenshots/:id/retire` route is loopback and service-token only |
| 81 | (`media.object.delete`, principal `svc:community`) and soft-deletes exactly a `legacy:live:paste:<slug>` |
| 82 | screenshot: 410 at `/o/:id`, the ordinary retention window and `media.object.deleted` event, and a repeat |
| 83 | is a no-op. VODs, clips, files, thumbnails, avatars and native objects are refused, and so is any other principal. |
| 84 | |
| 85 | - The legacy retire route also accepts a Community-era v1 file object (`app_id community`, kind `file`, |
| 86 | `legacy_ref legacy:community:file:<key>`), for the same `svc:community` principal and with the same ordinary soft |
| 87 | delete, retention, hold refusal and idempotence. Community's screenshot move calls it for old pastes whose bytes were |
| 88 | stored before the Object API v2, after checking no other paste, post or attachment still references the id. |
| 89 | Test: `test/legacy-paste-retire.test.js`. |
| 90 | - Review follow-up: a category-3 result places an `evidence` hold in the quarantine transaction, so owner delete and |
| 91 | retention purge answer 409 held; an operator's overruling category releases it with who and when. Adds Vision's |
| 92 | logged 60-second `operator-view` and service-only `review-decision` routes, a separate operator signature purpose, |
| 93 | `media_review_views`, migration `0019_review_evidence.sql`, and Contracts v0.294.0. |
| 94 | - Unsafe-image review enforcement (VIS-6): a visible upload is pending until OpenVibe.Vision (`VISION_URL` plus |
| 95 | Media's service principal) sorts it into `fine`, `sex_acts` or `minor_sexual`. Sex acts become private and leave |
| 96 | every public surface; sexual content involving a minor is quarantined even from a signed read and keeps Vision's |
| 97 | operator report id. A reviewer outage leaves the object pending, never public. Migration |
| 98 | `0018_object_reviews.sql`, `server/objects/review.js`, `POST /api/v2/:app/objects/:id/review`, tests |
| 99 | `test/object-review.test.js`. |
| 100 | |
| 101 | - A tenant for the screenshot pass (plan L9, devblog §3.3): OpenVibe.Host stores the PNGs it captures (ovhost shots) in its own |
| 102 | tenant, `host`, through the Object API v2. The tenant is created at boot like the other first-party ones, and reached only |
| 103 | with a Network service token that holds the media.object.* verbs for the host namespace. |
| 104 | |
| 105 | - A deleted recording's or clip's thumbnail goes with it (2026-10-20). Migration 0013 adds a trigger that marks an |
| 106 | object's derivatives (`media_variants`) deleted whenever it is, and backfills the ones left behind; restoring a native |
| 107 | object brings back the derivatives deleted with it. `/t/vod-<id>-…` and `/t/clip-<id>-…` answer the empty pixel once |
| 108 | the item's row is gone (the file stays on disk). The reconcile found 57 such thumbnails still served, and the |
| 109 | thumbnail folder held files for 1,336 recordings and 73 clips that no longer exist. |
| 110 | - Bound local CMAF and packed timeline copies with a configurable byte budget and free-space floor. The hourly sweep |
| 111 | evicts least recently served copies only after durable size and SHA-256 verification, preserving all timeline rows |
| 112 | and durable playback. |
| 113 | - Limit lazy HLS backfill to 20 VODs per hour by default while recording finalize remains immediate. |
| 114 | - Add timeline cache readiness and metrics, environment documentation and production enablement guidance. |
| 115 |