9uv0f8oll: Fit mobile videos to the screen without cropping
This commit is contained in:
@@ -76,18 +76,53 @@ function setRef<T>(ref: Ref<T> | undefined, value: T | null) {
|
||||
else if (ref) (ref as { current: T | null }).current = value;
|
||||
}
|
||||
|
||||
/** Aspect ratio (width / height) at or below which a video fills the slide (4:5 or taller). */
|
||||
export const COVER_MAX_RATIO = 0.8;
|
||||
|
||||
/** "cover" for portrait and near-portrait video, "contain" for wider, "cover" while the size is unknown. */
|
||||
export function videoFit(width: number, height: number): "cover" | "contain" {
|
||||
if (!(width > 0) || !(height > 0)) return "cover";
|
||||
return width / height <= COVER_MAX_RATIO ? "cover" : "contain";
|
||||
}
|
||||
|
||||
function SlideMedia({ media, author }: { media: CardMedia; author: string }) {
|
||||
const box = useRef<HTMLDivElement>(null);
|
||||
const [started, setStarted] = useState(false);
|
||||
// Real video size once metadata loads; until then the thumbnail's size decides.
|
||||
const [videoSize, setVideoSize] = useState<[number, number] | null>(null);
|
||||
const [thumbSize, setThumbSize] = useState<[number, number] | null>(null);
|
||||
const playable = isPlayableVideo(media);
|
||||
useEffect(() => {
|
||||
const video = box.current?.querySelector("video");
|
||||
if (!video) return;
|
||||
const read = () => {
|
||||
if (video.videoWidth > 0 && video.videoHeight > 0) setVideoSize([video.videoWidth, video.videoHeight]);
|
||||
};
|
||||
read();
|
||||
video.addEventListener("loadedmetadata", read);
|
||||
return () => video.removeEventListener("loadedmetadata", read);
|
||||
}, [playable, media.mp4Url, media.hlsUrl]);
|
||||
if (playable) {
|
||||
const size = videoSize ?? thumbSize;
|
||||
const fit = size ? videoFit(size[0], size[1]) : "cover";
|
||||
const duration = formatDuration(media.durationSeconds);
|
||||
const start = () => {
|
||||
setStarted(true);
|
||||
void box.current?.querySelector("video")?.play().catch(() => {});
|
||||
};
|
||||
return (
|
||||
<div className="slide-media slide-video" ref={box}>
|
||||
<div className="slide-media slide-video" data-fit={fit} ref={box}>
|
||||
{media.src ? (
|
||||
// The thumbnail doubles as the blurred backdrop behind a contained video, and as the size fallback.
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
className="slide-backdrop"
|
||||
src={media.src}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
onLoad={(e) => setThumbSize([e.currentTarget.naturalWidth, e.currentTarget.naturalHeight])}
|
||||
/>
|
||||
) : null}
|
||||
<div className="slide-art">
|
||||
<VideoPlayer mp4Url={media.mp4Url} hlsUrl={media.hlsUrl} poster={media.src} label={`Video from ${author}`} controls={started} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user