91 lines
3.0 KiB
TypeScript
91 lines
3.0 KiB
TypeScript
// Pure helpers for detecting embeddable video URLs (YouTube, Shorts, Vimeo) and
|
|||
|
|
// building their privacy-friendly embed URLs. No React/DOM/CSS imports so this
|
||
|
|
// stays unit-testable in isolation.
|
||
|
|
|
||
|
|
export type VideoEmbedProvider = 'youtube' | 'vimeo';
|
||
|
|
|
||
|
|
export type VideoEmbed = {
|
||
|
|
provider: VideoEmbedProvider;
|
||
|
|
id: string;
|
||
|
|
portrait: boolean; // YouTube Shorts render 9:16
|
||
|
|
};
|
||
|
|
|
||
|
|
export function getYouTubeVideoId(url: string): string | null {
|
||
|
|
try {
|
||
|
|
const { hostname, pathname, searchParams } = new URL(url);
|
||
|
|
// youtu.be/<id>
|
||
|
|
if (hostname === 'youtu.be') {
|
||
|
|
const id = pathname.slice(1).split('/')[0];
|
||
|
|
return id || null;
|
||
|
|
}
|
||
|
|
if (hostname === 'www.youtube.com' || hostname === 'youtube.com') {
|
||
|
|
// youtube.com/watch?v=<id>
|
||
|
|
if (pathname === '/watch') return searchParams.get('v');
|
||
|
|
// youtube.com/embed/<id>
|
||
|
|
const embedMatch = pathname.match(/^\/embed\/([A-Za-z0-9_-]+)/);
|
||
|
|
if (embedMatch) return embedMatch[1];
|
||
|
|
// youtube.com/shorts/<id> (also matched by getYoutubeShortsId)
|
||
|
|
const shortsMatch = pathname.match(/^\/shorts\/([A-Za-z0-9_-]+)/);
|
||
|
|
if (shortsMatch) return shortsMatch[1];
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
// ignore malformed URLs
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function isYouTubeShorts(url: string): boolean {
|
||
|
|
try {
|
||
|
|
const { hostname, pathname } = new URL(url);
|
||
|
|
if (hostname !== 'www.youtube.com' && hostname !== 'youtube.com') return false;
|
||
|
|
return /^\/shorts\/[A-Za-z0-9_-]+/.test(pathname);
|
||
|
|
} catch {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getYoutubeShortsId(url: string): string | null {
|
||
|
|
try {
|
||
|
|
const { hostname, pathname } = new URL(url);
|
||
|
|
if (hostname !== 'www.youtube.com' && hostname !== 'youtube.com') return null;
|
||
|
|
const m = pathname.match(/^\/shorts\/([A-Za-z0-9_-]+)/);
|
||
|
|
return m ? m[1] : null;
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getVimeoVideoId(url: string): string | null {
|
||
|
|
try {
|
||
|
|
const { hostname, pathname } = new URL(url);
|
||
|
|
if (hostname !== 'vimeo.com' && hostname !== 'www.vimeo.com') return null;
|
||
|
|
const m = pathname.match(/^\/(\d+)/);
|
||
|
|
return m ? m[1] : null;
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Detect an embeddable video from any URL, or null if it isn't one. */
|
||
|
|
export function parseVideoEmbed(url: string): VideoEmbed | null {
|
||
|
|
const shortsId = getYoutubeShortsId(url);
|
||
|
|
if (shortsId) return { provider: 'youtube', id: shortsId, portrait: true };
|
||
|
|
const ytId = getYouTubeVideoId(url);
|
||
|
|
if (ytId) return { provider: 'youtube', id: ytId, portrait: false };
|
||
|
|
const vimeoId = getVimeoVideoId(url);
|
||
|
|
if (vimeoId) return { provider: 'vimeo', id: vimeoId, portrait: false };
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Build the click-to-play embed URL. YouTube uses the cookie-less
|
||
|
|
* youtube-nocookie host so nothing is set until the user presses play.
|
||
|
|
*/
|
||
|
|
export function buildVideoEmbedUrl(provider: VideoEmbedProvider, id: string): string {
|
||
|
|
const safeId = encodeURIComponent(id);
|
||
|
|
if (provider === 'vimeo') {
|
||
|
|
return `https://player.vimeo.com/video/${safeId}?autoplay=1`;
|
||
|
|
}
|
||
|
|
return `https://www.youtube-nocookie.com/embed/${safeId}?autoplay=1&rel=0`;
|
||
|
|
}
|