openvibers/openvibe.media CHANGELOG.md

CHANGELOG.md

main 10.5 KB · 115 lines · markdown

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