openvibers/openvibe.live Commits
A live channel's shared link shows the stream as it is now and says "Live now" (it showed the avatar); the person's structured data keeps the avatar and bio (#274)
Changes
@@@ -286,12 +286,17 @@ async function _channelMeta(username, page = 1) {const canonicalPath = page > 1 && !pastEnd ? `${basePath}?page=${page}` : basePath;const pageLabel = page > 1 ? ` — videos, page ${page}` : '';-- const desc = clean(bio || aiShort || `${name} (${handle}) streams live on ${SITE_NAME}. Watch their live streams, VODs and clips.`, 200);-- const image = acct.avatarUrl ? abs(acct.avatarUrl) : DEFAULT_OG_IMAGE;++ const about = clean(bio || aiShort || `${name} (${handle}) streams live on ${SITE_NAME}. Watch their live streams, VODs and clips.`, 200);++ const avatar = acct.avatarUrl ? abs(acct.avatarUrl) : null;++ // A live channel's shared link shows the stream as it is now (16:9) and says so; offline, the avatar. The page is++ // kept a minute (CHANNEL_CACHE_MS), so the card follows the stream going live or ending.++ const liveNow = live.length ? live[0] : null;++ const image = (liveNow && liveNow.thumbnail_url ? abs(liveNow.thumbnail_url) : null) || avatar || DEFAULT_OG_IMAGE;++ const desc = liveNow ? clean(`Live now: ${liveNow.title || `${name} is streaming`}. ${about}`, 200) : about;const person = {'@type': 'Person', name: clean(name, 80), alternateName: handle, url: abs(basePath),-- image: image !== DEFAULT_OG_IMAGE ? image : undefined,-- description: desc,++ image: avatar || undefined,++ description: about,interactionStatistic: followers ? { '@type': 'InteractionCounter', interactionType: 'https://schema.org/FollowAction', userInteractionCount: followers } : undefined,};const profile = { ...seo.jsonLd.webPage({ type: 'ProfilePage', name: `${name} (${handle})`, url: abs(canonicalPath) }), mainEntity: person };@@@ -244,6 +244,20 @@ const server = http.createServer(app).listen(0, '127.0.0.1', async () => {}});++ await check('a live channel\'s shared link shows the stream as it is now and says so; the person keeps their avatar', async () => {++ await addUser(6, 'carol', 'Carol', 'I paint.');++ await db.run("UPDATE users SET avatar_url = 'https://openvibe.media/avatars/carol.png' WHERE id = 6");++ await db.run("INSERT INTO streams (user_id, title, protocol, is_live, thumbnail_url, started_at, last_heartbeat) VALUES (6, 'Painting a sunset', 'webrtc', 1, '/api/thumbnails/stream-9-1.jpg', ov_now(), ov_now())");++ const html = (await get('/@carol')).body;++ assert.ok(head(html).includes('<meta property="og:image" content="https://openvibe.live/api/thumbnails/stream-9-1.jpg">'), 'the live picture');++ assert.match(head(html), /<meta property="og:description" content="Live now: Painting a sunset\. I paint\.">/);++ const person = ldOf(html).flatMap((n) => (n['@graph'] || [n])).find((n) => n['@type'] === 'Person' || (n.mainEntity && n.mainEntity['@type'] === 'Person'));++ const p = person && (person['@type'] === 'Person' ? person : person.mainEntity);++ assert.ok(p, 'a Person node');++ assert.strictEqual(p.image, 'https://openvibe.media/avatars/carol.png', 'the person is still pictured by their avatar');++ assert.strictEqual(p.description, 'I paint.', 'and described by their bio');++ });++await check('the home page\'s JSON-LD lists are named by the snapshot\'s visible headings', async () => {const home = (await get('/')).body;const lists = ldOf(home).filter((n) => n['@type'] === 'ItemList');
@@ -286,12 +286,17 @@ async function _channelMeta(username, page = 1) {
@@ -286,12 +286,17 @@ async function _channelMeta(username, page = 1) {
const canonicalPath = page > 1 && !pastEnd ? `${basePath}?page=${page}` : basePath;
const canonicalPath = page > 1 && !pastEnd ? `${basePath}?page=${page}` : basePath;
const pageLabel = page > 1 ? ` — videos, page ${page}` : '';
const pageLabel = page > 1 ? ` — videos, page ${page}` : '';
- const desc = clean(bio || aiShort || `${name} (${handle}) streams live on ${SITE_NAME}. Watch their live streams, VODs and clips.`, 200);
- const image = acct.avatarUrl ? abs(acct.avatarUrl) : DEFAULT_OG_IMAGE;
+ const about = clean(bio || aiShort || `${name} (${handle}) streams live on ${SITE_NAME}. Watch their live streams, VODs and clips.`, 200);
+ const avatar = acct.avatarUrl ? abs(acct.avatarUrl) : null;
+ // A live channel's shared link shows the stream as it is now (16:9) and says so; offline, the avatar. The page is
+ // kept a minute (CHANNEL_CACHE_MS), so the card follows the stream going live or ending.
+ const liveNow = live.length ? live[0] : null;
+ const image = (liveNow && liveNow.thumbnail_url ? abs(liveNow.thumbnail_url) : null) || avatar || DEFAULT_OG_IMAGE;
+ const desc = liveNow ? clean(`Live now: ${liveNow.title || `${name} is streaming`}. ${about}`, 200) : about;
const person = {
const person = {
'@type': 'Person', name: clean(name, 80), alternateName: handle, url: abs(basePath),
'@type': 'Person', name: clean(name, 80), alternateName: handle, url: abs(basePath),
- image: image !== DEFAULT_OG_IMAGE ? image : undefined,
- description: desc,
+ image: avatar || undefined,
+ description: about,
interactionStatistic: followers ? { '@type': 'InteractionCounter', interactionType: 'https://schema.org/FollowAction', userInteractionCount: followers } : undefined,
interactionStatistic: followers ? { '@type': 'InteractionCounter', interactionType: 'https://schema.org/FollowAction', userInteractionCount: followers } : undefined,
};
};
const profile = { ...seo.jsonLd.webPage({ type: 'ProfilePage', name: `${name} (${handle})`, url: abs(canonicalPath) }), mainEntity: person };
const profile = { ...seo.jsonLd.webPage({ type: 'ProfilePage', name: `${name} (${handle})`, url: abs(canonicalPath) }), mainEntity: person };
@@ -244,6 +244,20 @@ const server = http.createServer(app).listen(0, '127.0.0.1', async () => {
@@ -244,6 +244,20 @@ const server = http.createServer(app).listen(0, '127.0.0.1', async () => {
}
}
});
});
+ await check('a live channel\'s shared link shows the stream as it is now and says so; the person keeps their avatar', async () => {
+ await addUser(6, 'carol', 'Carol', 'I paint.');
+ await db.run("UPDATE users SET avatar_url = 'https://openvibe.media/avatars/carol.png' WHERE id = 6");
+ await db.run("INSERT INTO streams (user_id, title, protocol, is_live, thumbnail_url, started_at, last_heartbeat) VALUES (6, 'Painting a sunset', 'webrtc', 1, '/api/thumbnails/stream-9-1.jpg', ov_now(), ov_now())");
+ const html = (await get('/@carol')).body;
+ assert.ok(head(html).includes('<meta property="og:image" content="https://openvibe.live/api/thumbnails/stream-9-1.jpg">'), 'the live picture');
+ assert.match(head(html), /<meta property="og:description" content="Live now: Painting a sunset\. I paint\.">/);
+ const person = ldOf(html).flatMap((n) => (n['@graph'] || [n])).find((n) => n['@type'] === 'Person' || (n.mainEntity && n.mainEntity['@type'] === 'Person'));
+ const p = person && (person['@type'] === 'Person' ? person : person.mainEntity);
+ assert.ok(p, 'a Person node');
+ assert.strictEqual(p.image, 'https://openvibe.media/avatars/carol.png', 'the person is still pictured by their avatar');
+ assert.strictEqual(p.description, 'I paint.', 'and described by their bio');
+ });
+
await check('the home page\'s JSON-LD lists are named by the snapshot\'s visible headings', async () => {
await check('the home page\'s JSON-LD lists are named by the snapshot\'s visible headings', async () => {
const home = (await get('/')).body;
const home = (await get('/')).body;
const lists = ldOf(home).filter((n) => n['@type'] === 'ItemList');
const lists = ldOf(home).filter((n) => n['@type'] === 'ItemList');