openvibers/openvibe.media CHANGELOG.md
CHANGELOG.md
Changelog
Unreleased
- The shared composer's attachments (2026-11-10, composer.attachment@1).
POST /api/v1/attachmentstakes one
multipartfilefrom a person (a Network user JWT, or a first-party site's service token naming the person in
X-OV-Subject), sniffs the kind from the bytes — image, video, audio, file or code — and refuses an executable.
Limits are image 20 MB, video 300 MB, audio 50 MB, anything else 25 MB; images get a 640 px JPEG thumbnail and a
video its first frame (ffmpeg).POST /api/v1/attachments/:id/keepnames the message it belongs to,DELETEremoves
the person's own file, and an unkept attachment is swept 24 hours after upload (migrations/0027_attachments.sql).
Bytes are served at/a/<id>/<name>; CORS is the first-party origin list (server/attachments/origins.js).
scripts/orientation-backfill.jsinspects stored JPEG, PNG and WebP originals in batches (dry run by default), bakes
any surviving EXIF orientation into pixels, removes the embedded details, updates object/location hashes and sizes,
and rebuilds 320/640 WebP variants. Images without source orientation data are markedorientation unknownfor the
owner's later AI review.--applywrites one batch;--allwalks the range.
- A recording that does not decode is never published (2026-11-10). A recording is a stream copy of a WebRTC
ingest, and WebRTC does not retransmit to a recording consumer: a lost RTP fragment arrives as a truncated H.264
access unit, the muxer writes it, ffprobe calls the file healthy, and Chrome stops the whole playback on it
(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).
finalizenow decodes the opening 10 s withffmpeg -xerror(server/vod/decode-check.js): a decoder error line
matters even when a single-threaded ffmpeg exits 0, while the null muxer's DTS warnings from variable frame timing
do not. A failing recording is measured, markedneeds_repair, quarantined and hidden;server/vod/vod-repair.js
re-encodes it in place (H.264/AAC, original timing and duration, verified output), settles the row, lifts the
quarantine, restores visibility, announcesvod.readyand re-cuts its timeline so the streaming playlist serves the
repaired bytes too. The health job repairsneeds_repairrows first on its next idle pass, and a deep scan that
finds a decode fault repairs it instead of quarantining it as corrupt. Operators can use
node scripts/repair-vod-decode.js --ids … [--full] [--apply]. Tests:test/decode-check.test.js(a corrupt
P-frame fixture generated with ffmpeg) andtest/vod-decode-repair.test.js(finalize marks, repair publishes).
The Media recorder also logs incomplete-RTP-frame warnings, so packet loss is visible at record time.
- Avatar ingestion, image variants, and the upload-time
POST /api/v2/:app/objects/normalize-imageoperation share
server/images/orientation.js. The operation applies EXIF orientation to pixels before Sharp drops the source
metadata, for first-party uploaders such as Community. Test:test/image-orientation.test.jscovers all eight values.
- A first-party
aitenant for OpenVibe.AI's shared image task (image.picture): found Commons/Openverse pictures
and made illustrations are stored by Media reference in the blog or wiki tenant for the person the run acts for.
Network grants AImedia.object.uploadthere (the AI, blog and wiki namespaces).server/auth.js.
- Watch ids (plan track U, 2026-11-07): every VOD and clip answers to an 11-character random id. YouTube's
/watch?v=shape, so a watch page cannot be found by counting: 8 random bytes as URL-safe base64
(crypto.randomBytes(8).toString('base64url')), never derived from the numeric id. Media owns VODs and clips, so
Media mints it (server/watch-ids.js): at insert for every new row, and in idempotent batches — only where
watch_id IS NULL— for the rows that predate it, at boot. One id space across both tables (mintUnique()checks
both), so a watch id has exactly one meaning.watch_idis on every VOD and clip Media serves (vodPublic,
clipPublic, so every API read and list), andGET /api/v1/:app/watch/:watch_id→{ kind, id }resolves one; a
private item still resolves, and its own read applies the visibility rule it always did. Migration
0024_watch_ids.sql(expand: a nullable column and a partial unique index per table; no table rewrite). Tests:
test/watch-ids.test.js(format, insert, the resolver, the backfill idempotent and leaving none NULL, uniqueness
across both tables, the route).
- Audio description on a watch page (X1, consent plan 2026-10-31, "AI and data"): the player now carries a
<track kind="descriptions">for Live's VODs and clips, served same-origin at/v/:id/descriptions.vtt(and
/c/:id/descriptions.vtt) from the WebVTT Live builds out of its scene notes and speech timeline. Public and link
items are cacheable; a private item's track answers the same 404 as a missing id unless the viewer's Network
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
its spoken cues are OpenVibe.Shared's player (openvibe-shared 4.126.0, a pin-round follow-up). Tests:
test/audio-description.test.js(public, private-owner, other app; track markup).
- An avatar a person's profile points at cannot be deleted out from under them (incident 2026-11-06: a cleanup of
public screenshot copies deleted eleven avatars).media_avatar_refs(migration0020_avatar_refs.sql) records
each profile slot → object;model.softDeleterefuses an avatar with an unreleased reference andDELETEanswers
409media.object.avatar_in_use. The internalPOST /internal/avatar-adoptand/internal/avatar-releaseroutes
write it, and/internal/avatar-ingestrecords it when givenproductandref. Replacing a picture releases the
previous reference in the same transaction; account deletion releases the person's references first. Ingest also
reads our own/o/<id>and/p/<slug>/screenshotsources locally instead of fetching them, reuses an avatar the
subject already owns, and stores a 1600×500shape: 'banner'crop. Test:test/avatar-guard.test.js.
- Operator retirement for an object with no good copy (
scripts/no-good-copy-report.js --retire <med_id> [--apply]):
the dry run changes nothing; only an object in the report, not public and under 1 MiB is retired (a public v1 row, a
native public object, good-copy, 1 MiB+ and held objects are refused with the reason). For a legacy VOD/clip the v1
row decides: a delisted row (is_public0, even withvisibilitystill public after the health quarantine), or a
private/unlisted one, makes the object not public even when itsmedia_objectsrow still says public. The delete is
the ordinary soft delete (bytes kept, retention,media.object.deleted, restorable), and a
projected VOD/clip has its v1 row made private so the inherited route stops serving it. Test:
test/retire-no-good-copy.test.js.
- A legacy Live paste screenshot can be retired for OpenVibe.Community (2026-11-06). The internal
POST /internal/community/legacy-paste-screenshots/:id/retireroute is loopback and service-token only
(media.object.delete, principalsvc:community) and soft-deletes exactly alegacy:live:paste:<slug>
screenshot: 410 at/o/:id, the ordinary retention window andmedia.object.deletedevent, and a repeat
is a no-op. VODs, clips, files, thumbnails, avatars and native objects are refused, and so is any other principal.
- The legacy retire route also accepts a Community-era v1 file object (
app_id community, kindfile,
legacy_ref legacy:community:file:<key>), for the samesvc:communityprincipal and with the same ordinary soft
delete, retention, hold refusal and idempotence. Community's screenshot move calls it for old pastes whose bytes were
stored before the Object API v2, after checking no other paste, post or attachment still references the id.
Test:test/legacy-paste-retire.test.js. - Review follow-up: a category-3 result places an
evidencehold in the quarantine transaction, so owner delete and
retention purge answer 409 held; an operator's overruling category releases it with who and when. Adds Vision's
logged 60-secondoperator-viewand service-onlyreview-decisionroutes, a separate operator signature purpose,
media_review_views, migration0019_review_evidence.sql, and Contracts v0.294.0. - Unsafe-image review enforcement (VIS-6): a visible upload is pending until OpenVibe.Vision (
VISION_URLplus
Media's service principal) sorts it intofine,sex_actsorminor_sexual. Sex acts become private and leave
every public surface; sexual content involving a minor is quarantined even from a signed read and keeps Vision's
operator report id. A reviewer outage leaves the object pending, never public. Migration
0018_object_reviews.sql,server/objects/review.js,POST /api/v2/:app/objects/:id/review, tests
test/object-review.test.js.
- A tenant for the screenshot pass (plan L9, devblog §3.3): OpenVibe.Host stores the PNGs it captures (ovhost shots) in its own
tenant,host, through the Object API v2. The tenant is created at boot like the other first-party ones, and reached only
with a Network service token that holds the media.object.* verbs for the host namespace.
- A deleted recording's or clip's thumbnail goes with it (2026-10-20). Migration 0013 adds a trigger that marks an
object's derivatives (media_variants) deleted whenever it is, and backfills the ones left behind; restoring a native
object brings back the derivatives deleted with it./t/vod-<id>-…and/t/clip-<id>-…answer the empty pixel once
the item's row is gone (the file stays on disk). The reconcile found 57 such thumbnails still served, and the
thumbnail folder held files for 1,336 recordings and 73 clips that no longer exist. - Bound local CMAF and packed timeline copies with a configurable byte budget and free-space floor. The hourly sweep
evicts least recently served copies only after durable size and SHA-256 verification, preserving all timeline rows
and durable playback. - Limit lazy HLS backfill to 20 VODs per hour by default while recording finalize remains immediate.
- Add timeline cache readiness and metrics, environment documentation and production enablement guidance.