feat(embeds): inline YouTube/Vimeo players + media-forward video tiles
CI / Build & Quality Checks (push) Successful in 11m17s
CI / Trigger Desktop Build (push) Successful in 10s

Video link tiles (YouTube, Shorts, Vimeo) now play in place instead of only
opening a browser tab. Adds a media-forward 16:9 (9:16 for Shorts) tile with a
privacy-friendly click-to-play facade: the homeserver's cached og:image thumbnail
+ a play button, and only on click does it swap in the cookie-less
youtube-nocookie / player.vimeo iframe — so nothing loads from Google/Vimeo until
the user presses play. Gated by a new 'Inline Media Players' setting (default on);
when off it falls back to a link that opens the video in a new tab.

Also sources YouTube thumbnails from the homeserver og:image instead of
img.youtube.com, which fixes the existing broken YouTube thumbnails on the web
build (nginx img-src has no YouTube host) and removes the pre-click Google request.

Pure URL parsing + embed-URL building moved to utils/videoEmbed.ts (unit-tested).

Note: the desktop app's Tauri CSP frame-src must allow the video hosts (separate
commit in cinny-desktop).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 19:48:12 -04:00
parent 29d74eda8f
commit 93f307cd63
6 changed files with 393 additions and 210 deletions
+69
View File
@@ -0,0 +1,69 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
getYouTubeVideoId,
getYoutubeShortsId,
isYouTubeShorts,
getVimeoVideoId,
parseVideoEmbed,
buildVideoEmbedUrl,
} from './videoEmbed';
test('getYouTubeVideoId: watch / youtu.be / embed / shorts', () => {
assert.equal(getYouTubeVideoId('https://www.youtube.com/watch?v=dQw4w9WgXcQ'), 'dQw4w9WgXcQ');
assert.equal(getYouTubeVideoId('https://youtu.be/dQw4w9WgXcQ'), 'dQw4w9WgXcQ');
assert.equal(getYouTubeVideoId('https://youtu.be/dQw4w9WgXcQ?t=42'), 'dQw4w9WgXcQ');
assert.equal(getYouTubeVideoId('https://www.youtube.com/embed/dQw4w9WgXcQ'), 'dQw4w9WgXcQ');
assert.equal(getYouTubeVideoId('https://youtube.com/shorts/abc123DEF_-'), 'abc123DEF_-');
});
test('getYouTubeVideoId: non-YouTube / malformed → null', () => {
assert.equal(getYouTubeVideoId('https://vimeo.com/123'), null);
assert.equal(getYouTubeVideoId('not a url'), null);
assert.equal(getYouTubeVideoId('https://www.youtube.com/'), null);
});
test('Shorts detection', () => {
assert.equal(isYouTubeShorts('https://www.youtube.com/shorts/abc123'), true);
assert.equal(isYouTubeShorts('https://www.youtube.com/watch?v=abc123'), false);
assert.equal(getYoutubeShortsId('https://youtube.com/shorts/abc123'), 'abc123');
});
test('getVimeoVideoId', () => {
assert.equal(getVimeoVideoId('https://vimeo.com/123456789'), '123456789');
assert.equal(getVimeoVideoId('https://vimeo.com/123456789/abcdef'), '123456789');
assert.equal(getVimeoVideoId('https://vimeo.com/channels/staffpicks'), null);
assert.equal(getVimeoVideoId('https://youtube.com/watch?v=x'), null);
});
test('parseVideoEmbed: routes provider + portrait for shorts', () => {
assert.deepEqual(parseVideoEmbed('https://youtube.com/shorts/abc'), {
provider: 'youtube',
id: 'abc',
portrait: true,
});
assert.deepEqual(parseVideoEmbed('https://www.youtube.com/watch?v=xyz'), {
provider: 'youtube',
id: 'xyz',
portrait: false,
});
assert.deepEqual(parseVideoEmbed('https://vimeo.com/42'), {
provider: 'vimeo',
id: '42',
portrait: false,
});
assert.equal(parseVideoEmbed('https://example.com/video'), null);
});
test('buildVideoEmbedUrl: cookie-less YouTube + Vimeo, id encoded', () => {
assert.equal(
buildVideoEmbedUrl('youtube', 'dQw4w9WgXcQ'),
'https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ?autoplay=1&rel=0',
);
assert.equal(
buildVideoEmbedUrl('vimeo', '123456789'),
'https://player.vimeo.com/video/123456789?autoplay=1',
);
// id is URL-encoded (defense-in-depth; ids are normally already safe)
assert.ok(buildVideoEmbedUrl('youtube', 'a/b').includes('a%2Fb'));
});