Skip to content

Instantly share code, notes, and snippets.

@jonoroboto
Created July 18, 2025 21:10
Show Gist options
  • Save jonoroboto/afecba723af5491cbd528d8726311b20 to your computer and use it in GitHub Desktop.
Save jonoroboto/afecba723af5491cbd528d8726311b20 to your computer and use it in GitHub Desktop.
/* eslint-disable @next/next/no-img-element */
"use client";
import { Loader2, PlayIcon } from "lucide-react";
import dynamic from "next/dynamic";
import { type FC, useCallback, useState } from "react";
import { preload } from "react-dom";
import type { MuxVideo } from "@/lib/sanity/sanity.types";
// Types
interface VideoPlayProps {
poster: string;
vidUrl: string;
loop?: boolean;
muted?: boolean;
control?: boolean;
}
interface VideoProps extends VideoPlayProps {
playbackId: string;
autoPlay: boolean;
loading?: "lazy" | "eager";
}
interface VideoThumbnailProps extends VideoPlayProps {
loading?: "lazy" | "eager";
onPlay: () => void;
}
// Constants
const VIDEO_DIMENSIONS = {
width: 1600,
height: 900,
} as const;
const MUX_BASE_URL = "https://image.mux.com";
const MUX_STREAM_URL = "https://stream.mux.com";
// Utilities
const getMuxVideoProps = (
muxId: string,
): Pick<VideoPlayProps, "poster" | "vidUrl"> => ({
poster: `${MUX_BASE_URL}/${muxId}/thumbnail.webp?fit_mode=smartcrop&time=0`,
vidUrl: `${MUX_STREAM_URL}/${muxId}.m3u8`,
});
// Components
const LoadingPlaceholder: FC = () => (
<div className="relative aspect-video w-full">
<div className="absolute inset-0 flex items-center justify-center bg-zinc-100">
<Loader2 className="h-8 w-8 animate-spin text-zinc-400" />
</div>
</div>
);
const PlayButton: FC<{ onClick: () => void }> = ({ onClick }) => (
<button
type="button"
onClick={onClick}
className="absolute inset-0 m-auto flex h-[54px] w-[54px] items-center justify-center rounded-full bg-zinc-900/70 ring-1 ring-zinc-600 transition hover:scale-105 hover:bg-zinc-900/80"
aria-label="Play video"
>
<svg
width="11"
height="14"
viewBox="0 0 11 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="ml-0.5"
>
<path d="M0 14V0L11 7L0 14Z" className="fill-white" />
</svg>
</button>
);
const HlsVideoPlayer = dynamic(
() => import("./hls-video").then((mod) => ({ default: mod.HlsVideo })),
{
loading: LoadingPlaceholder,
ssr: false,
},
);
const VideoPlayer: FC<VideoPlayProps> = ({
control,
loop,
muted,
poster,
vidUrl,
}) => (
<HlsVideoPlayer
autoPlay
poster={poster}
vidUrl={vidUrl}
loop={control ? loop : undefined}
muted={control ? muted : undefined}
controls={control}
width={VIDEO_DIMENSIONS.width}
height={VIDEO_DIMENSIONS.height}
className="w-full object-cover"
/>
);
const VideoThumbnail: FC<VideoThumbnailProps> = ({
poster,
loading,
onPlay,
}) => (
<div className="relative w-full ">
<img
src={poster}
width={VIDEO_DIMENSIONS.width}
height={VIDEO_DIMENSIONS.height}
loading={loading === "lazy" ? "lazy" : "eager"}
alt="Video thumbnail"
className="h-full w-full object-cover transition-opacity duration-300 hover:opacity-90"
/>
<PlayButton onClick={onPlay} />
</div>
);
const VideoWithControls: FC<VideoPlayProps & { loading?: "lazy" | "eager" }> = (
props,
) => {
const [isPlaying, setIsPlaying] = useState(false);
const handlePlay = useCallback(() => setIsPlaying(true), []);
return (
<div className="relative w-full">
{isPlaying ? (
<VideoPlayer {...props} />
) : (
<VideoThumbnail {...props} onPlay={handlePlay} />
)}
</div>
);
};
export const Video: FC<VideoProps> = ({
playbackId,
control,
autoPlay,
loop,
muted,
loading = "eager",
}) => {
const { poster, vidUrl } = getMuxVideoProps(playbackId);
// Preload critical assets
preload(vidUrl, {
as: "video",
fetchPriority: "high",
});
preload(poster, {
as: "image",
fetchPriority: "high",
});
const videoProps = { poster, loading, vidUrl, loop, muted, control };
return (
<div className="mx-auto max-w-7xl">
{control && autoPlay ? (
<VideoPlayer {...videoProps} />
) : (
<VideoWithControls {...videoProps} loading={loading} />
)}
</div>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment