diff --git a/packages/vidstack/src/providers/vimeo/utils.test.ts b/packages/vidstack/src/providers/vimeo/utils.test.ts new file mode 100644 index 000000000..1392d5c45 --- /dev/null +++ b/packages/vidstack/src/providers/vimeo/utils.test.ts @@ -0,0 +1,88 @@ +import { vi } from 'vitest'; + +import type { VimeoOEmbedData } from './embed/misc'; +import { getVimeoVideoInfo } from './utils'; + +it('preserves the aspect ratio for landscape Vimeo posters', async () => { + mockOEmbed( + createOEmbedData({ + width: 1920, + height: 1080, + thumbnail_url: 'https://i.vimeocdn.com/video/123456789_640.jpg', + }), + ); + + const info = (await getVimeoVideoInfo('123', new AbortController()))!; + + expect(info.poster).to.equal('https://i.vimeocdn.com/video/123456789_1920x1080.webp'); +}); + +it('preserves the aspect ratio for portrait Vimeo posters', async () => { + mockOEmbed( + createOEmbedData({ + width: 1080, + height: 1920, + thumbnail_url: 'https://i.vimeocdn.com/video/987654321_640.jpg', + }), + ); + + const info = (await getVimeoVideoInfo('456', new AbortController()))!; + + expect(info.poster).to.equal('https://i.vimeocdn.com/video/987654321_1080x1920.webp'); +}); + +it('falls back to the original thumbnail when dimensions are missing', async () => { + const thumbnailUrl = 'https://i.vimeocdn.com/video/555555555_640.jpg'; + + mockOEmbed( + createOEmbedData({ + width: 0, + height: 0, + thumbnail_width: 0, + thumbnail_height: 0, + thumbnail_url: thumbnailUrl, + }), + ); + + const info = (await getVimeoVideoInfo('789', new AbortController()))!; + + expect(info.poster).to.equal(thumbnailUrl); +}); + +function mockOEmbed(data: VimeoOEmbedData) { + Object.defineProperty(window, 'fetch', { + configurable: true, + value: vi.fn(() => + Promise.resolve({ + json: () => Promise.resolve(data), + }), + ), + }); +} + +function createOEmbedData(overrides: Partial): VimeoOEmbedData { + return { + account_type: 'basic', + author_name: '', + author_url: '', + description: '', + duration: 0, + height: 1080, + html: '', + is_plus: '0', + provider_name: 'Vimeo', + provider_url: 'https://vimeo.com/', + thumbnail_height: 1080, + thumbnail_url_with_play_button: '', + thumbnail_url: 'https://i.vimeocdn.com/video/123456789_640.jpg', + thumbnail_width: 1920, + title: '', + type: 'video', + upload_date: '', + uri: '', + version: '1.0', + video_id: 0, + width: 1920, + ...overrides, + }; +} diff --git a/packages/vidstack/src/providers/vimeo/utils.ts b/packages/vidstack/src/providers/vimeo/utils.ts index 488cab8dc..c0866bea4 100644 --- a/packages/vidstack/src/providers/vimeo/utils.ts +++ b/packages/vidstack/src/providers/vimeo/utils.ts @@ -7,6 +7,8 @@ const infoCache = new Map(); const pendingFetch = new Map>(); +const posterMaxSize = 1920; + export function resolveVimeoVideoId(src: string) { const matches = src.match(videoIdRE); return { videoId: matches?.[1], hash: matches?.[2] }; @@ -33,15 +35,12 @@ export async function getVimeoVideoInfo( }) .then((response) => response.json()) .then((data: VimeoOEmbedData) => { - const thumnailRegex = /vimeocdn.com\/video\/(.*)?_/, - thumbnailId = data?.thumbnail_url?.match(thumnailRegex)?.[1], - poster = thumbnailId ? `https://i.vimeocdn.com/video/${thumbnailId}_1920x1080.webp` : '', - info = { - title: data?.title ?? '', - duration: data?.duration ?? 0, - poster, - pro: data.account_type !== 'basic', - }; + const info = { + title: data?.title ?? '', + duration: data?.duration ?? 0, + poster: resolveVimeoPoster(data), + pro: data.account_type !== 'basic', + }; infoCache.set(videoId, info); return info; @@ -51,3 +50,35 @@ export async function getVimeoVideoInfo( pendingFetch.set(videoId, promise); return promise; } + +function resolveVimeoPoster(data: VimeoOEmbedData) { + const thumbnailUrl = data?.thumbnail_url ?? '', + thumbnailId = thumbnailUrl.match(/vimeocdn\.com\/video\/([^_/?#]+)_/)?.[1]; + + if (!thumbnailId) return thumbnailUrl; + + const dimensions = resolvePosterDimensions(data); + + return dimensions + ? `https://i.vimeocdn.com/video/${thumbnailId}_${dimensions.width}x${dimensions.height}.webp` + : thumbnailUrl; +} + +function resolvePosterDimensions(data: VimeoOEmbedData) { + const width = data.width || data.thumbnail_width, + height = data.height || data.thumbnail_height; + + if (!width || !height) return null; + + if (width > height) { + return { + width: posterMaxSize, + height: Math.round((posterMaxSize * height) / width), + }; + } + + return { + width: Math.round((posterMaxSize * width) / height), + height: posterMaxSize, + }; +}