openvibers/openvibe.media Commits
openvibe-shared v4.7.0, and openvibe.media plays videos in the network's player (#148)
Changes
@@@ -46,7 +46,7 @@ without `DATABASE_URL`, development uses an embedded PGlite database in `data/pgthe account and revocation subscriptions)- Backblaze B2 and Cloudflare R2 (S3 API) when configured; `ffmpeg`/`ffprobe` on the host- `openvibe-contracts` v0.200.0, `openvibe-sdk` v0.50.1 (tokens, events outbox, usage readings, per-actor limits),-- `openvibe-shared` v4.4.0, pinned by release tarball++ `openvibe-shared` v4.7.0, pinned by release tarball## Capabilities@@@ -129,7 +129,7 @@ server/storage.orphans.scan (monthly storage orphan report, service-wide, report only),storage.move.cleanup (the alert for placement moves whose delete keeps failing, service-wide)client-ip.js trust proxy = loopback; req.ip is the only client address--(openvibe-shared v4.4.0, openvibe-contracts v0.200.0, openvibe-sdk v0.50.1: pinned release tarballs, installed by npm)++(openvibe-shared v4.7.0, openvibe-contracts v0.200.0, openvibe-sdk v0.50.1: pinned release tarballs, installed by npm)scripts/smoke-test.sh end-to-end smoke test (boots a temp instance)scripts/reconcile-objects.js / object-invariant.js / no-good-copy-report.js object-model operator toolsscripts/media-jobs.js list jobs, run the size-invariant scan (dry run by default), approve/cancel proposals@@@ -756,5 +756,5 @@ restart; afterwards `sudo ovhost rollback media --to <sha>`.<!-- versions:start -->- openvibe-contracts: v0.200.0- openvibe-sdk: v0.50.1--- openvibe-shared: v4.4.0++- openvibe-shared: v4.7.0<!-- versions:end -->@@@ -8,7 +8,7 @@"domain": "openvibe.media","contracts": "openvibe-contracts v0.200.0","sdk": "openvibe-sdk v0.50.1",-- "shared": "openvibe-shared v4.4.0",++ "shared": "openvibe-shared v4.7.0","capabilities": ["media.object.upload","media.object.read",@@@ -48,5 +48,5 @@"HLS discovery on the v1 shapes (F3.c, behind MEDIA_HLS_ENABLED): the VOD and clip answers (vodPublic/clipPublic — get, list, create, update) carry hls_url (and hls_expires_at when the object is private or sandbox), the same field and access rules as GET /o/:id/download?format=json, read through one shared discovery (server/objects/hls.js); an object without a timeline gets object.cmaf queued and the field appears once it ran, and a virtual clip plays its source's window without being cut a timeline of its own","materialized clips (F3.6, behind MEDIA_HLS_ENABLED + MEDIA_MATERIALIZED_CLIPS, off by default): clip.cut over a source timeline writes the clip's own media_timeline rows — the interior copies the source's segments (packed chunk ranges included), only the two window edges are re-encoded into fMP4 with their own inits (init-head.mp4/init-tail.mp4, negative seq, EXT-X-MAP + EXT-X-DISCONTINUITY in the playlist) — inserting them in one transaction that locks the named source rows FOR SHARE and aborts with a retryable error when one is gone or any location column moved (a pack), so a racing delete either waits and keeps the bytes or aborts the insert; the projection keeps virtual: true and gains materialized: true (readiness, playableSql and the verify job treat it as a virtual clip), sprites decode each edge with its own init, deleting the clip removes its rows and edge bytes while the source's bytes stay, and download?format=mp4 answers the playlist for a timeline-only clip instead of queueing a remux that cannot read its input"],-- "updated": "2026-10-17"++ "updated": "2026-10-20"}@@@ -18,7 +18,7 @@"multer": "^1.4.5-lts.1","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.200.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.50.1",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.4.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.7.0","pg": "^8.23.0","sharp": "^0.33.4"},@@@ -1891,9 +1891,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.4.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.4.0",-- "integrity": "sha512-zhC3wzRdI3HG+TzLEEXTzZquBoHJHzE/67kGgmrYCXmouqcugKA+RZZ3gFQXMsIx79IQNrbE/tlg0hqiyUCJmw==",++ "version": "4.7.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.7.0",++ "integrity": "sha512-oobo+p+UH7ImewGliZOloJ0lSSNP7NXxCejWLYtq93cT0BwvOs0hRxkOdSb91S4CRQ1cJUiGuXlhGh2M9injsQ==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -22,7 +22,7 @@"multer": "^1.4.5-lts.1","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.200.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.50.1",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.4.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.7.0","pg": "^8.23.0","sharp": "^0.33.4"},@@@ -92,6 +92,8 @@ function extraHead(seo, css) {appIcon.headTags({ site: 'media', iconBase: '/assets' }),// openvibe-shared/showcase's stylesheet (the index's hero; the kit's sections on any page).`<link rel="stylesheet" href="${ovServe.url('showcase.css')}">`,++ // A page with a video plays it in the network's player (openvibe-shared player.js/css on video[data-ov-player]).++ seo.video ? `<link rel="stylesheet" href="${ovServe.url('player.css')}">\n<script src="${ovServe.url('player.js')}" defer></script>` : '',`<style>${baseCss()}${css}</style>`,videoTags,// The default share card is 1200×630; a media thumbnail carries its own dimensions.@@@ -144,7 +144,7 @@ function renderWatchPage(kind, record, readiness = null) {const [stateTitle, stateText] = playable ? [null, null] : unavailableText(kind, readiness.reason);const player = playable? `<div class="player">-- <video controls playsinline preload="metadata"${thumb ? ` poster="${esc(thumb)}"` : ''} src="${esc(rawUrl)}">++ <video data-ov-player controls playsinline preload="metadata"${thumb ? ` poster="${esc(thumb)}"` : ''} src="${esc(rawUrl)}">Your browser cannot play this video. <a href="${esc(rawUrl)}">Download it</a> instead.</video></div>`@@@ -143,10 +143,14 @@ const http = require('http');const hasPlayer = (html) => /<video[\s>]/.test(html);res = await get('/v/1', nav);assert.ok(hasPlayer(res.body) && res.body.includes('<meta property="og:video"'), 'a playable VOD gets its player');++ // The network's player (openvibe-shared player.js/css): the video is marked and both files load.++ assert.ok(res.body.includes('<video data-ov-player controls playsinline'), 'the video takes the network player');++ assert.ok(/<link rel="stylesheet" href="\/shared\/player\.css\?v=[0-9a-f]+">/.test(res.body) && /<script src="\/shared\/player\.js\?v=[0-9a-f]+" defer><\/script>/.test(res.body));res = await get('/v/2', nav);assert.strictEqual(res.status, 200);assert.ok(!hasPlayer(res.body), 'no player for bytes nobody verified');assert.ok(res.body.includes('Still processing') && res.body.includes('data-readiness="verification_pending"'));++ assert.ok(!res.body.includes('/shared/player.js'), 'no player files where nothing plays');assert.ok(res.body.includes('content="noindex, follow"') && !res.body.includes('og:video') && !res.body.includes('application/ld+json') && !res.body.includes(' download>'),'no index, no video metadata, no structured data, no download');res = await get('/v/3', nav);
@@ -46,7 +46,7 @@ without `DATABASE_URL`, development uses an embedded PGlite database in `data/pg
@@ -46,7 +46,7 @@ without `DATABASE_URL`, development uses an embedded PGlite database in `data/pg
the account and revocation subscriptions)
the account and revocation subscriptions)
- Backblaze B2 and Cloudflare R2 (S3 API) when configured; `ffmpeg`/`ffprobe` on the host
- Backblaze B2 and Cloudflare R2 (S3 API) when configured; `ffmpeg`/`ffprobe` on the host
- `openvibe-contracts` v0.200.0, `openvibe-sdk` v0.50.1 (tokens, events outbox, usage readings, per-actor limits),
- `openvibe-contracts` v0.200.0, `openvibe-sdk` v0.50.1 (tokens, events outbox, usage readings, per-actor limits),
- `openvibe-shared` v4.4.0, pinned by release tarball
+ `openvibe-shared` v4.7.0, pinned by release tarball
## Capabilities
## Capabilities
@@ -129,7 +129,7 @@ server/
@@ -129,7 +129,7 @@ server/
storage.orphans.scan (monthly storage orphan report, service-wide, report only),
storage.orphans.scan (monthly storage orphan report, service-wide, report only),
storage.move.cleanup (the alert for placement moves whose delete keeps failing, service-wide)
storage.move.cleanup (the alert for placement moves whose delete keeps failing, service-wide)
client-ip.js trust proxy = loopback; req.ip is the only client address
client-ip.js trust proxy = loopback; req.ip is the only client address
-(openvibe-shared v4.4.0, openvibe-contracts v0.200.0, openvibe-sdk v0.50.1: pinned release tarballs, installed by npm)
+(openvibe-shared v4.7.0, openvibe-contracts v0.200.0, openvibe-sdk v0.50.1: pinned release tarballs, installed by npm)
scripts/smoke-test.sh end-to-end smoke test (boots a temp instance)
scripts/smoke-test.sh end-to-end smoke test (boots a temp instance)
scripts/reconcile-objects.js / object-invariant.js / no-good-copy-report.js object-model operator tools
scripts/reconcile-objects.js / object-invariant.js / no-good-copy-report.js object-model operator tools
scripts/media-jobs.js list jobs, run the size-invariant scan (dry run by default), approve/cancel proposals
scripts/media-jobs.js list jobs, run the size-invariant scan (dry run by default), approve/cancel proposals
@@ -756,5 +756,5 @@ restart; afterwards `sudo ovhost rollback media --to <sha>`.
@@ -756,5 +756,5 @@ restart; afterwards `sudo ovhost rollback media --to <sha>`.
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.200.0
- openvibe-contracts: v0.200.0
- openvibe-sdk: v0.50.1
- openvibe-sdk: v0.50.1
-- openvibe-shared: v4.4.0
+- openvibe-shared: v4.7.0
<!-- versions:end -->
<!-- versions:end -->
@@ -8,7 +8,7 @@
@@ -8,7 +8,7 @@
"domain": "openvibe.media",
"domain": "openvibe.media",
"contracts": "openvibe-contracts v0.200.0",
"contracts": "openvibe-contracts v0.200.0",
"sdk": "openvibe-sdk v0.50.1",
"sdk": "openvibe-sdk v0.50.1",
- "shared": "openvibe-shared v4.4.0",
+ "shared": "openvibe-shared v4.7.0",
"capabilities": [
"capabilities": [
"media.object.upload",
"media.object.upload",
"media.object.read",
"media.object.read",
@@ -48,5 +48,5 @@
@@ -48,5 +48,5 @@
"HLS discovery on the v1 shapes (F3.c, behind MEDIA_HLS_ENABLED): the VOD and clip answers (vodPublic/clipPublic — get, list, create, update) carry hls_url (and hls_expires_at when the object is private or sandbox), the same field and access rules as GET /o/:id/download?format=json, read through one shared discovery (server/objects/hls.js); an object without a timeline gets object.cmaf queued and the field appears once it ran, and a virtual clip plays its source's window without being cut a timeline of its own",
"HLS discovery on the v1 shapes (F3.c, behind MEDIA_HLS_ENABLED): the VOD and clip answers (vodPublic/clipPublic — get, list, create, update) carry hls_url (and hls_expires_at when the object is private or sandbox), the same field and access rules as GET /o/:id/download?format=json, read through one shared discovery (server/objects/hls.js); an object without a timeline gets object.cmaf queued and the field appears once it ran, and a virtual clip plays its source's window without being cut a timeline of its own",
"materialized clips (F3.6, behind MEDIA_HLS_ENABLED + MEDIA_MATERIALIZED_CLIPS, off by default): clip.cut over a source timeline writes the clip's own media_timeline rows — the interior copies the source's segments (packed chunk ranges included), only the two window edges are re-encoded into fMP4 with their own inits (init-head.mp4/init-tail.mp4, negative seq, EXT-X-MAP + EXT-X-DISCONTINUITY in the playlist) — inserting them in one transaction that locks the named source rows FOR SHARE and aborts with a retryable error when one is gone or any location column moved (a pack), so a racing delete either waits and keeps the bytes or aborts the insert; the projection keeps virtual: true and gains materialized: true (readiness, playableSql and the verify job treat it as a virtual clip), sprites decode each edge with its own init, deleting the clip removes its rows and edge bytes while the source's bytes stay, and download?format=mp4 answers the playlist for a timeline-only clip instead of queueing a remux that cannot read its input"
"materialized clips (F3.6, behind MEDIA_HLS_ENABLED + MEDIA_MATERIALIZED_CLIPS, off by default): clip.cut over a source timeline writes the clip's own media_timeline rows — the interior copies the source's segments (packed chunk ranges included), only the two window edges are re-encoded into fMP4 with their own inits (init-head.mp4/init-tail.mp4, negative seq, EXT-X-MAP + EXT-X-DISCONTINUITY in the playlist) — inserting them in one transaction that locks the named source rows FOR SHARE and aborts with a retryable error when one is gone or any location column moved (a pack), so a racing delete either waits and keeps the bytes or aborts the insert; the projection keeps virtual: true and gains materialized: true (readiness, playableSql and the verify job treat it as a virtual clip), sprites decode each edge with its own init, deleting the clip removes its rows and edge bytes while the source's bytes stay, and download?format=mp4 answers the playlist for a timeline-only clip instead of queueing a remux that cannot read its input"
],
],
- "updated": "2026-10-17"
+ "updated": "2026-10-20"
}
}
@@ -18,7 +18,7 @@
@@ -18,7 +18,7 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.200.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.200.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.50.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.50.1",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.4.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.7.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"sharp": "^0.33.4"
"sharp": "^0.33.4"
},
},
@@ -1891,9 +1891,9 @@
@@ -1891,9 +1891,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.4.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.4.0",
- "integrity": "sha512-zhC3wzRdI3HG+TzLEEXTzZquBoHJHzE/67kGgmrYCXmouqcugKA+RZZ3gFQXMsIx79IQNrbE/tlg0hqiyUCJmw==",
+ "version": "4.7.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.7.0",
+ "integrity": "sha512-oobo+p+UH7ImewGliZOloJ0lSSNP7NXxCejWLYtq93cT0BwvOs0hRxkOdSb91S4CRQ1cJUiGuXlhGh2M9injsQ==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -22,7 +22,7 @@
@@ -22,7 +22,7 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.200.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.200.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.50.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.50.1",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.4.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.7.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"sharp": "^0.33.4"
"sharp": "^0.33.4"
},
},
@@ -92,6 +92,8 @@ function extraHead(seo, css) {
@@ -92,6 +92,8 @@ function extraHead(seo, css) {
appIcon.headTags({ site: 'media', iconBase: '/assets' }),
appIcon.headTags({ site: 'media', iconBase: '/assets' }),
// openvibe-shared/showcase's stylesheet (the index's hero; the kit's sections on any page).
// openvibe-shared/showcase's stylesheet (the index's hero; the kit's sections on any page).
`<link rel="stylesheet" href="${ovServe.url('showcase.css')}">`,
`<link rel="stylesheet" href="${ovServe.url('showcase.css')}">`,
+ // A page with a video plays it in the network's player (openvibe-shared player.js/css on video[data-ov-player]).
+ seo.video ? `<link rel="stylesheet" href="${ovServe.url('player.css')}">\n<script src="${ovServe.url('player.js')}" defer></script>` : '',
`<style>${baseCss()}${css}</style>`,
`<style>${baseCss()}${css}</style>`,
videoTags,
videoTags,
// The default share card is 1200×630; a media thumbnail carries its own dimensions.
// The default share card is 1200×630; a media thumbnail carries its own dimensions.
@@ -144,7 +144,7 @@ function renderWatchPage(kind, record, readiness = null) {
@@ -144,7 +144,7 @@ function renderWatchPage(kind, record, readiness = null) {
const [stateTitle, stateText] = playable ? [null, null] : unavailableText(kind, readiness.reason);
const [stateTitle, stateText] = playable ? [null, null] : unavailableText(kind, readiness.reason);
const player = playable
const player = playable
? `<div class="player">
? `<div class="player">
- <video controls playsinline preload="metadata"${thumb ? ` poster="${esc(thumb)}"` : ''} src="${esc(rawUrl)}">
+ <video data-ov-player controls playsinline preload="metadata"${thumb ? ` poster="${esc(thumb)}"` : ''} src="${esc(rawUrl)}">
Your browser cannot play this video. <a href="${esc(rawUrl)}">Download it</a> instead.
Your browser cannot play this video. <a href="${esc(rawUrl)}">Download it</a> instead.
</video>
</video>
</div>`
</div>`
@@ -143,10 +143,14 @@ const http = require('http');
@@ -143,10 +143,14 @@ const http = require('http');
const hasPlayer = (html) => /<video[\s>]/.test(html);
const hasPlayer = (html) => /<video[\s>]/.test(html);
res = await get('/v/1', nav);
res = await get('/v/1', nav);
assert.ok(hasPlayer(res.body) && res.body.includes('<meta property="og:video"'), 'a playable VOD gets its player');
assert.ok(hasPlayer(res.body) && res.body.includes('<meta property="og:video"'), 'a playable VOD gets its player');
+ // The network's player (openvibe-shared player.js/css): the video is marked and both files load.
+ assert.ok(res.body.includes('<video data-ov-player controls playsinline'), 'the video takes the network player');
+ assert.ok(/<link rel="stylesheet" href="\/shared\/player\.css\?v=[0-9a-f]+">/.test(res.body) && /<script src="\/shared\/player\.js\?v=[0-9a-f]+" defer><\/script>/.test(res.body));
res = await get('/v/2', nav);
res = await get('/v/2', nav);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.status, 200);
assert.ok(!hasPlayer(res.body), 'no player for bytes nobody verified');
assert.ok(!hasPlayer(res.body), 'no player for bytes nobody verified');
assert.ok(res.body.includes('Still processing') && res.body.includes('data-readiness="verification_pending"'));
assert.ok(res.body.includes('Still processing') && res.body.includes('data-readiness="verification_pending"'));
+ assert.ok(!res.body.includes('/shared/player.js'), 'no player files where nothing plays');
assert.ok(res.body.includes('content="noindex, follow"') && !res.body.includes('og:video') && !res.body.includes('application/ld+json') && !res.body.includes(' download>'),
assert.ok(res.body.includes('content="noindex, follow"') && !res.body.includes('og:video') && !res.body.includes('application/ld+json') && !res.body.includes(' download>'),
'no index, no video metadata, no structured data, no download');
'no index, no video metadata, no structured data, no download');
res = await get('/v/3', nav);
res = await get('/v/3', nav);