Files
eranos/src/components/ArchiveOrgEmbed.tsx
T
Alex Gleason fd1b78834e Add autoplay=1 to archive.org embed iframes
When users click the play overlay, the iframe now loads with autoplay=1
so the content plays immediately, matching the YouTube embed behavior.
Also adds allow='autoplay' to the iframe attribute so browsers permit
the autoplay request from the embedded frame.
2026-03-22 15:39:01 -05:00

177 lines
6.2 KiB
TypeScript

import { useCallback, useRef, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { cn } from '@/lib/utils';
interface ArchiveOrgEmbedProps {
identifier: string;
className?: string;
}
interface ArchiveOrgMeta {
/** Native width/height of the primary media file, if available. */
dims: { width: number; height: number } | null;
/** The item's mediatype (e.g. "software", "movies", "audio", "texts"). */
mediatype: string | null;
}
/** Fetch metadata for an archive.org item: dimensions and mediatype. */
function useArchiveOrgMeta(identifier: string) {
return useQuery({
queryKey: ['archive-org-meta', identifier],
queryFn: async ({ signal }): Promise<ArchiveOrgMeta> => {
const res = await fetch(`https://archive.org/metadata/${identifier}`, { signal });
if (!res.ok) return { dims: null, mediatype: null };
const json: {
metadata?: { mediatype?: string };
files?: { width?: string; height?: string; source?: string }[];
} = await res.json();
const mediatype = json.metadata?.mediatype ?? null;
// Extract dimensions from the files list.
const files = json.files ?? [];
const withDims = files.filter((f) => f.width && f.height);
const original = withDims.find((f) => f.source === 'original') ?? withDims[0];
let dims: { width: number; height: number } | null = null;
if (original) {
const w = Number(original.width);
const h = Number(original.height);
if (w && h) dims = { width: w, height: h };
}
return { dims, mediatype };
},
staleTime: 1000 * 60 * 60,
gcTime: 1000 * 60 * 60 * 24,
retry: false,
});
}
/**
* Renders an Internet Archive embed with a click-to-load facade.
*
* Shows a thumbnail and play button instead of mounting the iframe immediately,
* so no requests are made to archive.org until the user explicitly clicks.
*
* Archive.org provides:
* - Thumbnail: `https://archive.org/services/img/{identifier}`
* - Embed: `https://archive.org/embed/{identifier}`
* - Metadata: `https://archive.org/metadata/{identifier}`
*
* The embed page renders the appropriate player for the content type
* (video, audio, software emulation, book reader, etc.).
*
* For software/games, the archive.org embed renders content at its native
* pixel size and doesn't scale to fill the iframe. We use CSS
* `transform: scale()` to shrink these to fit narrow viewports.
*
* For videos and other media, the embed page has a responsive player,
* so we use a standard responsive iframe approach.
*/
export function ArchiveOrgEmbed({ identifier, className }: ArchiveOrgEmbedProps) {
const [activated, setActivated] = useState(false);
const { data: meta } = useArchiveOrgMeta(identifier);
const [containerWidth, setContainerWidth] = useState(0);
// Measure the outer container width on mount/resize via ResizeObserver.
const containerRef = useCallback((node: HTMLDivElement | null) => {
if (!node) return;
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
setContainerWidth(entry.contentRect.width);
}
});
ro.observe(node);
setContainerWidth(node.getBoundingClientRect().width);
observerRef.current = ro;
}, []);
const observerRef = useRef<ResizeObserver | null>(null);
const thumbnailSrc = `https://archive.org/services/img/${identifier}`;
// Software/games need the transform-scale approach because their embed
// renders at fixed native pixel dimensions.
const isSoftware = meta?.mediatype === 'software';
const nativeW = meta?.dims?.width ?? 640;
const nativeH = meta?.dims?.height ?? 480;
const paddingBottom = `${(nativeH / nativeW) * 100}%`;
// Scale factor for software embeds.
const scale = containerWidth > 0 ? Math.min(1, containerWidth / nativeW) : 1;
const scaledH = nativeH * scale;
return (
<div
ref={containerRef}
className={cn('rounded-2xl overflow-hidden border border-border', className)}
onClick={(e) => e.stopPropagation()}
>
{activated ? (
isSoftware ? (
// Software/games: fixed-size iframe scaled down with CSS transform.
<div className="relative overflow-hidden bg-black" style={{ height: scaledH }}>
<iframe
src={`https://archive.org/embed/${identifier}&autoplay=1`}
title="Internet Archive"
allow="autoplay"
allowFullScreen
style={{
width: nativeW,
height: nativeH,
transform: `scale(${scale})`,
transformOrigin: 'top left',
border: 'none',
}}
/>
</div>
) : (
// Videos and other media: standard responsive iframe.
<div className="relative w-full" style={{ paddingBottom }}>
<iframe
src={`https://archive.org/embed/${identifier}&autoplay=1`}
title="Internet Archive"
allow="autoplay"
allowFullScreen
className="absolute inset-0 w-full h-full"
/>
</div>
)
) : (
<div className="relative w-full" style={{ paddingBottom }}>
<button
type="button"
className="absolute inset-0 w-full h-full cursor-pointer bg-black group"
onClick={() => setActivated(true)}
aria-label="Load content"
>
<img
src={thumbnailSrc}
alt=""
className="absolute inset-0 w-full h-full object-contain"
/>
{/* Play button */}
<div className="absolute inset-0 flex items-center justify-center">
<div
className={cn(
'flex items-center justify-center',
'w-[68px] h-[48px] rounded-xl',
'bg-[#212121]/80 group-hover:bg-[#428bca] transition-colors duration-200',
)}
>
<svg viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6 text-white ml-0.5">
<path d="M8 5v14l11-7z" />
</svg>
</div>
</div>
</button>
</div>
)}
</div>
);
}