Always embed Vimeo first; link out only on player error

Prefer on-site player.vimeo.com embeds. Use the Vimeo Player API to
detect privacy blocks and fall back to Watch on Vimeo only then.
This commit is contained in:
2026-07-24 12:24:47 -04:00
parent 1b29b1d104
commit 738187d1d4
3 changed files with 161 additions and 39 deletions
+120
View File
@@ -0,0 +1,120 @@
import { useEffect, useRef, useState } from "react";
import { mediaSrc } from "../lib/api";
type Props = {
videoId: string;
title: string;
watchUrl: string;
poster?: string;
};
declare global {
interface Window {
Vimeo?: {
Player: new (
el: HTMLIFrameElement | HTMLElement,
opts?: { id?: string | number; url?: string }
) => {
on: (event: string, cb: (data?: unknown) => void) => void;
destroy: () => void;
};
};
}
}
let playerSdkPromise: Promise<void> | null = null;
function loadVimeoSdk(): Promise<void> {
if (window.Vimeo?.Player) return Promise.resolve();
if (playerSdkPromise) return playerSdkPromise;
playerSdkPromise = new Promise((resolve, reject) => {
const existing = document.querySelector<HTMLScriptElement>(
'script[src="https://player.vimeo.com/api/player.js"]'
);
if (existing) {
existing.addEventListener("load", () => resolve());
existing.addEventListener("error", () => reject(new Error("Vimeo SDK failed")));
if (window.Vimeo?.Player) resolve();
return;
}
const s = document.createElement("script");
s.src = "https://player.vimeo.com/api/player.js";
s.async = true;
s.onload = () => resolve();
s.onerror = () => reject(new Error("Vimeo SDK failed"));
document.body.appendChild(s);
});
return playerSdkPromise;
}
/**
* Always embeds first. Only if Vimeo reports a privacy/error does the UI
* fall back to opening the video on vimeo.com (last resort).
*/
export function VimeoEmbed({ videoId, title, watchUrl, poster }: Props) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [blocked, setBlocked] = useState(false);
const embedSrc = `https://player.vimeo.com/video/${videoId}?title=0&byline=0&portrait=0`;
useEffect(() => {
let player: { destroy: () => void } | null = null;
let cancelled = false;
setBlocked(false);
loadVimeoSdk()
.then(() => {
if (cancelled || !iframeRef.current || !window.Vimeo?.Player) return;
player = new window.Vimeo.Player(iframeRef.current);
player.on("error", (data: unknown) => {
// Privacy / password / unavailable — leave site only as last resort
console.warn("Vimeo player error", data);
if (!cancelled) setBlocked(true);
});
})
.catch(() => {
// SDK load failure still keep iframe; browser may still play
});
return () => {
cancelled = true;
try {
player?.destroy();
} catch {
/* ignore */
}
};
}, [videoId]);
if (blocked) {
return (
<a href={watchUrl} target="_blank" rel="noreferrer" className="project-page__video-link">
{poster ? (
<img src={mediaSrc(poster)} alt="" className="project-page__video-poster" />
) : (
<div className="project-page__video-poster project-page__video-poster--empty" />
)}
<span className="project-page__video-cta">
<span className="project-page__play" aria-hidden>
</span>
Watch on Vimeo
<span className="project-page__video-note">
This video cant be embedded here (Vimeo privacy). Opens on Vimeo as a last resort.
</span>
</span>
</a>
);
}
return (
<iframe
ref={iframeRef}
src={embedSrc}
title={title}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen"
allowFullScreen
referrerPolicy="strict-origin-when-cross-origin"
/>
);
}