openvibers/openvibe.mediahub Commits
The player knows the streamer's face: an audio-only recording shows it (#247)
Changes
@@@ -103,6 +103,16 @@ function posterAttr(url) {return url ? html` poster="${url}" data-ov-ambient` : '';}++/**++ * The streamer's face for the player (openvibe-shared 4.90 player.js): a recording with no picture track (a browser++ * broadcast with no camera, plan R4) shows it in place of a black box. Network's /avatar/:username answers the image++ * (or a neutral placeholder), so no lookup is needed here.++ */++function avatarAttr(username, networkUrl) {++ const name = String(username || '');++ return /^[A-Za-z0-9_]{1,32}$/.test(name) && /^https?:\/\//.test(String(networkUrl || '')) ? html` data-ov-avatar="${String(networkUrl).replace(/\/+$/, '')}/avatar/${name}"` : '';++}++/*** An item's playable file. Live's `file_url` is always the file; its `playback_url` is the HLS playlist when Media has* one (Live T4 step 5), so it is only the fallback for answers from before `file_url` existed.@@@ -1240,7 +1250,7 @@ ${found ? sections.map((x) => (x.cards.length || kind ? html`<section class="ovv<div class="ovv-col ovv-col--main"><div class="ovv-stage">${savedNotice(req)}${ownOnly ? notice('Private: only you (and OpenVibe staff) can see this recording.', 'info') : ''}--<div class="ovv-player-wrap"><video class="ovv-player" data-ov-player controls preload="metadata" playsinline${posterAttr(httpsUrl(vod.thumbnail_url))}${shapeAttr} src="${fileOf(vod) || ''}" data-ovv-video${nextAttr}${viewAttr}${hlsAttr}>${segs.length ? html`<track kind="captions" label="Transcript (AI)" src="/v/${id}/captions.vtt" default>` : ''}${mems.length ? html`<track kind="chapters" src="/v/${id}/chapters.vtt">` : ''}</video>${pipsTag(pips)}</div>++<div class="ovv-player-wrap"><video class="ovv-player" data-ov-player controls preload="metadata" playsinline${posterAttr(httpsUrl(vod.thumbnail_url))}${avatarAttr(vod.username, config.networkUrl)}${shapeAttr} src="${fileOf(vod) || ''}" data-ovv-video${nextAttr}${viewAttr}${hlsAttr}>${segs.length ? html`<track kind="captions" label="Transcript (AI)" src="/v/${id}/captions.vtt" default>` : ''}${mems.length ? html`<track kind="chapters" src="/v/${id}/chapters.vtt">` : ''}</video>${pipsTag(pips)}</div><div class="ovv-head"><h1 class="ovv-title">${vod.title || 'Stream recording'}</h1>${what}@@@ -1363,7 +1373,7 @@ ${clips.length ? html`<section class="ovv-panel ovv-clips" aria-labelledby="h-cl<div class="ovv-stage">${savedNotice(req)}${req.query.made === '1' ? notice('Your clip was made. It is credited to you on OpenVibe.Live and here.', 'ok') : ''}${ownOnly ? notice('Private: only you (and OpenVibe staff) can see this clip.', 'info') : ''}${cutting ? html`<div class="ovv-player-wrap ovv-cutting" role="status" data-ovv-reload="8"><p><i class="fa-solid fa-scissors" aria-hidden="true"></i> This clip is being cut. It plays here in a moment; the page checks again by itself.</p></div>`-- : html`<div class="ovv-player-wrap"><video class="ovv-player" data-ov-player controls preload="metadata" playsinline${posterAttr(httpsUrl(clip.thumbnail_url))} src="${fileOf(clip) || ''}" data-ovv-video${nextAttr}${viewAttr}${hlsAttr}></video></div>`}++ : html`<div class="ovv-player-wrap"><video class="ovv-player" data-ov-player controls preload="metadata" playsinline${avatarAttr(clip.username, config.networkUrl)}${posterAttr(httpsUrl(clip.thumbnail_url))} src="${fileOf(clip) || ''}" data-ovv-video${nextAttr}${viewAttr}${hlsAttr}></video></div>`}<div class="ovv-head"><h1 class="ovv-title">${clip.title || 'Clip'}</h1>${what}@@@ -342,6 +342,8 @@ function countOccurrences(haystack, needle) {assert.ok(r.text.includes('data-ovv-video'));// The network's player (openvibe-shared player.js/css): marked, and both files load on the page.assert.ok(/<video class="ovv-player" data-ov-player controls /.test(r.text), 'the video takes the network player');++ // The streamer's face for an audio-only recording (openvibe-shared 4.90 player.js, plan R4): Network's avatar URL.++ assert.match(r.text, /<video class="ovv-player"[^>]* data-ov-avatar="[^"]+\/avatar\/streamer"/, 'the player knows the streamer\'s face');assert.ok(/<link rel="stylesheet" href="\/shared\/player\.css\?v=[0-9a-f]+">/.test(r.text) && /<script src="\/shared\/player\.js\?v=[0-9a-f]+" defer><\/script>/.test(r.text), 'player.css and player.js load');assert.ok(r.text.includes('data-ovv-view="https://openvibe.live/api/views/vod/101"'), 'playback counts a view on Live');const ld = graphOf(r.text).find((x) => x['@type'] === 'VideoObject');
@@ -103,6 +103,16 @@ function posterAttr(url) {
@@ -103,6 +103,16 @@ function posterAttr(url) {
return url ? html` poster="${url}" data-ov-ambient` : '';
return url ? html` poster="${url}" data-ov-ambient` : '';
}
}
+/**
+ * The streamer's face for the player (openvibe-shared 4.90 player.js): a recording with no picture track (a browser
+ * broadcast with no camera, plan R4) shows it in place of a black box. Network's /avatar/:username answers the image
+ * (or a neutral placeholder), so no lookup is needed here.
+ */
+function avatarAttr(username, networkUrl) {
+ const name = String(username || '');
+ return /^[A-Za-z0-9_]{1,32}$/.test(name) && /^https?:\/\//.test(String(networkUrl || '')) ? html` data-ov-avatar="${String(networkUrl).replace(/\/+$/, '')}/avatar/${name}"` : '';
+}
+
/**
/**
* An item's playable file. Live's `file_url` is always the file; its `playback_url` is the HLS playlist when Media has
* An item's playable file. Live's `file_url` is always the file; its `playback_url` is the HLS playlist when Media has
* one (Live T4 step 5), so it is only the fallback for answers from before `file_url` existed.
* one (Live T4 step 5), so it is only the fallback for answers from before `file_url` existed.
@@ -1240,7 +1250,7 @@ ${found ? sections.map((x) => (x.cards.length || kind ? html`<section class="ovv
@@ -1240,7 +1250,7 @@ ${found ? sections.map((x) => (x.cards.length || kind ? html`<section class="ovv
<div class="ovv-col ovv-col--main">
<div class="ovv-col ovv-col--main">
<div class="ovv-stage">
<div class="ovv-stage">
${savedNotice(req)}${ownOnly ? notice('Private: only you (and OpenVibe staff) can see this recording.', 'info') : ''}
${savedNotice(req)}${ownOnly ? notice('Private: only you (and OpenVibe staff) can see this recording.', 'info') : ''}
-<div class="ovv-player-wrap"><video class="ovv-player" data-ov-player controls preload="metadata" playsinline${posterAttr(httpsUrl(vod.thumbnail_url))}${shapeAttr} src="${fileOf(vod) || ''}" data-ovv-video${nextAttr}${viewAttr}${hlsAttr}>${segs.length ? html`<track kind="captions" label="Transcript (AI)" src="/v/${id}/captions.vtt" default>` : ''}${mems.length ? html`<track kind="chapters" src="/v/${id}/chapters.vtt">` : ''}</video>${pipsTag(pips)}</div>
+<div class="ovv-player-wrap"><video class="ovv-player" data-ov-player controls preload="metadata" playsinline${posterAttr(httpsUrl(vod.thumbnail_url))}${avatarAttr(vod.username, config.networkUrl)}${shapeAttr} src="${fileOf(vod) || ''}" data-ovv-video${nextAttr}${viewAttr}${hlsAttr}>${segs.length ? html`<track kind="captions" label="Transcript (AI)" src="/v/${id}/captions.vtt" default>` : ''}${mems.length ? html`<track kind="chapters" src="/v/${id}/chapters.vtt">` : ''}</video>${pipsTag(pips)}</div>
<div class="ovv-head">
<div class="ovv-head">
<h1 class="ovv-title">${vod.title || 'Stream recording'}</h1>
<h1 class="ovv-title">${vod.title || 'Stream recording'}</h1>
${what}
${what}
@@ -1363,7 +1373,7 @@ ${clips.length ? html`<section class="ovv-panel ovv-clips" aria-labelledby="h-cl
@@ -1363,7 +1373,7 @@ ${clips.length ? html`<section class="ovv-panel ovv-clips" aria-labelledby="h-cl
<div class="ovv-stage">
<div class="ovv-stage">
${savedNotice(req)}${req.query.made === '1' ? notice('Your clip was made. It is credited to you on OpenVibe.Live and here.', 'ok') : ''}${ownOnly ? notice('Private: only you (and OpenVibe staff) can see this clip.', 'info') : ''}
${savedNotice(req)}${req.query.made === '1' ? notice('Your clip was made. It is credited to you on OpenVibe.Live and here.', 'ok') : ''}${ownOnly ? notice('Private: only you (and OpenVibe staff) can see this clip.', 'info') : ''}
${cutting ? html`<div class="ovv-player-wrap ovv-cutting" role="status" data-ovv-reload="8"><p><i class="fa-solid fa-scissors" aria-hidden="true"></i> This clip is being cut. It plays here in a moment; the page checks again by itself.</p></div>`
${cutting ? html`<div class="ovv-player-wrap ovv-cutting" role="status" data-ovv-reload="8"><p><i class="fa-solid fa-scissors" aria-hidden="true"></i> This clip is being cut. It plays here in a moment; the page checks again by itself.</p></div>`
- : html`<div class="ovv-player-wrap"><video class="ovv-player" data-ov-player controls preload="metadata" playsinline${posterAttr(httpsUrl(clip.thumbnail_url))} src="${fileOf(clip) || ''}" data-ovv-video${nextAttr}${viewAttr}${hlsAttr}></video></div>`}
+ : html`<div class="ovv-player-wrap"><video class="ovv-player" data-ov-player controls preload="metadata" playsinline${avatarAttr(clip.username, config.networkUrl)}${posterAttr(httpsUrl(clip.thumbnail_url))} src="${fileOf(clip) || ''}" data-ovv-video${nextAttr}${viewAttr}${hlsAttr}></video></div>`}
<div class="ovv-head">
<div class="ovv-head">
<h1 class="ovv-title">${clip.title || 'Clip'}</h1>
<h1 class="ovv-title">${clip.title || 'Clip'}</h1>
${what}
${what}
@@ -342,6 +342,8 @@ function countOccurrences(haystack, needle) {
@@ -342,6 +342,8 @@ function countOccurrences(haystack, needle) {
assert.ok(r.text.includes('data-ovv-video'));
assert.ok(r.text.includes('data-ovv-video'));
// The network's player (openvibe-shared player.js/css): marked, and both files load on the page.
// The network's player (openvibe-shared player.js/css): marked, and both files load on the page.
assert.ok(/<video class="ovv-player" data-ov-player controls /.test(r.text), 'the video takes the network player');
assert.ok(/<video class="ovv-player" data-ov-player controls /.test(r.text), 'the video takes the network player');
+ // The streamer's face for an audio-only recording (openvibe-shared 4.90 player.js, plan R4): Network's avatar URL.
+ assert.match(r.text, /<video class="ovv-player"[^>]* data-ov-avatar="[^"]+\/avatar\/streamer"/, 'the player knows the streamer\'s face');
assert.ok(/<link rel="stylesheet" href="\/shared\/player\.css\?v=[0-9a-f]+">/.test(r.text) && /<script src="\/shared\/player\.js\?v=[0-9a-f]+" defer><\/script>/.test(r.text), 'player.css and player.js load');
assert.ok(/<link rel="stylesheet" href="\/shared\/player\.css\?v=[0-9a-f]+">/.test(r.text) && /<script src="\/shared\/player\.js\?v=[0-9a-f]+" defer><\/script>/.test(r.text), 'player.css and player.js load');
assert.ok(r.text.includes('data-ovv-view="https://openvibe.live/api/views/vod/101"'), 'playback counts a view on Live');
assert.ok(r.text.includes('data-ovv-view="https://openvibe.live/api/views/vod/101"'), 'playback counts a view on Live');
const ld = graphOf(r.text).find((x) => x['@type'] === 'VideoObject');
const ld = graphOf(r.text).find((x) => x['@type'] === 'VideoObject');