Files
jmartgraphix.com/client/src/components/VimeoEmbed.tsx
T
jmartin 738187d1d4 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.
2026-07-24 12:24:47 -04:00

121 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
/>
);
}