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.
121 lines
3.5 KiB
TypeScript
121 lines
3.5 KiB
TypeScript
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 can’t 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"
|
||
/>
|
||
);
|
||
}
|