Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
40174e3
feat(video-player): 🎥 integrate media-chrome for enhanced video playback
rogerantony-dev Feb 22, 2026
96ed2cf
feat(video-player): 🎨 add new icons and enhance video player controls
rogerantony-dev Feb 23, 2026
1963385
fix(video-player): 🛠️ enhance layout and visibility of video player c…
rogerantony-dev Feb 23, 2026
f744741
fix(video-player): 🛠️ prevent slider drags from triggering lightbox n…
rogerantony-dev Feb 23, 2026
9196e6f
chore(cspell): 📝 add new project-specific words to cspell dictionary
rogerantony-dev Feb 23, 2026
c2ff0c2
chore(dependencies): update media-chrome to version 4.18.0 in package…
rogerantony-dev Feb 23, 2026
d052e14
refactor(video-player): 🛠️ remove isActive prop from VideoPlayer
rogerantony-dev Feb 23, 2026
e26c1af
fix(video-player): 🛠️ improve YouTube video loading logic and adjust …
rogerantony-dev Feb 23, 2026
51bdeb3
fix(video-player): 🛠️ enable autoPlay for YouTube and HTML5 videos
rogerantony-dev Feb 23, 2026
fe646ea
style(video-player): 🎨 add invisible bridge for improved layout betwe…
rogerantony-dev Feb 23, 2026
476e8c6
refactor(video-player): 🛠️ simplify SettingsIcon component by removin…
rogerantony-dev Feb 23, 2026
1a3a9ac
fix(video-player): 🛠️ ensure media reference is cleared on error even…
rogerantony-dev Feb 23, 2026
109d6c7
fix(video-player): 🛠️ handle errors during YouTube video import to im…
rogerantony-dev Feb 23, 2026
5edcdf7
feat(video-player): 🎥 add isActive prop to control video playback state
rogerantony-dev Feb 23, 2026
63a8ac5
fix(video-player): 🛠️ update event handler to use onPointerDown for b…
rogerantony-dev Feb 23, 2026
6a31b7e
feat(video-player): 🎨 add MEDIA_STYLE for responsive video element si…
rogerantony-dev Feb 23, 2026
fae593c
style(video-player): 🎨 update PiP visibility comment for clarity on b…
rogerantony-dev Feb 23, 2026
5e5d77d
feat(video-player): 🎥 add loading spinner and bottom gradient
rogerantony-dev Feb 23, 2026
f57a308
style(video-player): 🎨 adjust bottom gradient height
rogerantony-dev Feb 23, 2026
fe985b8
feat(media-player): 🎥 implement MediaPlayer component for audio, vid…
rogerantony-dev Feb 24, 2026
8cbe01c
style(media-player): 🎨 update media control button backgrounds to tra…
rogerantony-dev Feb 27, 2026
c7ce44a
style(media-player): 🎨 add font-family to media time and preview disp…
rogerantony-dev Feb 27, 2026
4fdbcd2
Merge branch 'dev' into feat/media-chrome-video-player
rogerantony-dev Feb 27, 2026
5d12bb2
Merge branch 'dev' into feat/media-chrome-video-player
rogerantony-dev Feb 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .cspell/project-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ asteasolutions
Atleast
attrelid
blks
breakpointmd
btrim
BTRIM
catagories
Expand Down Expand Up @@ -57,6 +58,7 @@ infile
jobid
kieran
Loggedin
mediaisfullscreen
mhehjvu
micnncim
msclkid
Expand All @@ -73,6 +75,7 @@ Navin
noimageindex
normalised
Normalised
notooltip
nuqs
Nuqs
ogimage
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"jwt-decode": "4.0.0",
"lodash": "4.17.23",
"match-sorter": "8.2.0",
"media-chrome": "4.18.0",
"motion": "12.29.2",
"next": "16.1.6",
"next-themes": "0.4.6",
Expand All @@ -132,14 +133,15 @@
"react-infinite-scroll-component": "6.1.1",
"react-mentions": "4.4.10",
"react-modern-drawer": "1.4.0",
"react-player": "3.4.0",
"react-stately": "3.43.0",
"react-toastify": "11.0.5",
"resend": "6.9.1",
"slugify": "1.6.6",
"spotify-audio-element": "1.0.4",
"tailwind-merge": "3.4.0",
"uniqid": "5.4.0",
"yet-another-react-lightbox": "3.28.0",
"youtube-video-element": "1.9.0",
"zod": "4.3.6",
"zustand": "5.0.10"
},
Expand Down
446 changes: 20 additions & 426 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

294 changes: 294 additions & 0 deletions src/components/MediaPlayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import {
MediaControlBar,
MediaController,
MediaFullscreenButton,
MediaMuteButton,
MediaPipButton,
MediaPlayButton,
MediaPreviewChapterDisplay,
MediaPreviewThumbnail,
MediaPreviewTimeDisplay,
MediaTimeDisplay,
MediaTimeRange,
MediaVolumeRange,
} from "media-chrome/react";
import { MediaProvider, useMediaRef } from "media-chrome/react/media-store";
import {
MediaPlaybackRateMenu,
MediaPlaybackRateMenuButton,
} from "media-chrome/react/menu";

import {
FullscreenIcon,
MuteIcon,
PipIcon,
PlayPauseIcon,
SettingsIcon,
} from "./media-player-icons";
import {
AUDIO_CONTROL_BAR_STYLE,
AUDIO_CONTROLLER_STYLE,
CONTROL_BAR_STYLE,
CONTROLLER_STYLE,
MEDIA_STYLE,
YOUTUBE_CONTROLLER_STYLE,
} from "./media-player-theme";
import { Spinner } from "./spinner";

import "./media-player-theme.css";

export type MediaType = "audio" | "spotify" | "video" | "youtube";

export interface MediaPlayerProps {
isActive?: boolean;
mediaType: MediaType;
onError?: () => void;
src: string;
}

function isAudioType(mediaType: MediaType): boolean {
return mediaType === "audio" || mediaType === "spotify";
}

function getControllerStyle(mediaType: MediaType) {
if (isAudioType(mediaType)) {
return AUDIO_CONTROLLER_STYLE;
}

if (mediaType === "youtube") {
return YOUTUBE_CONTROLLER_STYLE;
}

return CONTROLLER_STYLE;
}

/* ---- Main player ---- */

function MediaPlayerInner({
isActive,
mediaType,
onError,
src,
}: MediaPlayerProps) {
const mediaRef = useMediaRef();
const mediaElRef = useRef<HTMLElement | null>(null);
const onErrorRef = useRef(onError);
onErrorRef.current = onError;
const [loading, setLoading] = useState(true);
const [youTubeReady, setYouTubeReady] = useState(false);
const [spotifyReady, setSpotifyReady] = useState(false);

const isAudio = isAudioType(mediaType);

useEffect(() => {
if (mediaType !== "youtube") {
return;
}

void (async () => {
try {
await import("youtube-video-element");
setYouTubeReady(true);
} catch {
onErrorRef.current?.();
}
})();
}, [mediaType]);

useEffect(() => {
if (mediaType !== "spotify") {
return;
}

void (async () => {
try {
await import("spotify-audio-element");
setSpotifyReady(true);
} catch {
onErrorRef.current?.();
}
})();
}, [mediaType]);

useEffect(() => {
const el = mediaElRef.current;
if (!el || !("play" in el)) {
return;
}

if (isActive) {
void (el as HTMLMediaElement).play();
} else {
(el as HTMLMediaElement).pause();
}
}, [isActive]);

const ref = useCallback(
(el: HTMLElement | null) => {
mediaElRef.current = el;
// Custom elements (spotify-audio) implement the HTMLMediaElement API
// at runtime but extend HTMLElement, so cast for media-chrome
mediaRef(el as HTMLMediaElement | null);

if (!el) {
return undefined;
}

const handleError = () => onErrorRef.current?.();
const handleLoaded = () => setLoading(false);
el.addEventListener("error", handleError);
el.addEventListener("loadeddata", handleLoaded);
return () => {
el.removeEventListener("error", handleError);
el.removeEventListener("loadeddata", handleLoaded);
mediaRef(null);
};
},
[mediaRef],
);

let mediaElement: React.ReactNode = null;
switch (mediaType) {
case "youtube": {
if (youTubeReady) {
mediaElement = (
<youtube-video crossOrigin="" ref={ref} slot="media" src={src} />
);
}

break;
}

case "spotify": {
if (spotifyReady) {
mediaElement = <spotify-audio ref={ref} slot="media" src={src} />;
}

break;
}

case "audio": {
mediaElement = (
<audio ref={ref} slot="media" src={src}>
<track default kind="captions" label="No captions" srcLang="en" />
</audio>
);
break;
}

case "video": {
mediaElement = (
<video ref={ref} slot="media" src={src} style={MEDIA_STYLE}>
<track default kind="captions" label="No captions" srcLang="en" />
</video>
);
break;
}
}

// Audio types don't need the loading gate — the <audio> element and
// spotify-audio custom element may never fire "loadeddata", so show
// the control bar immediately.
const showLoading = loading && !isAudio;

return (
<>
{showLoading && (
<div className="flex items-center justify-center py-16">
<Spinner className="size-3" />
</div>
)}
<MediaController
audio={isAudio || undefined}
autohide={isAudio ? "-1" : undefined}
breakpoints="pip:400 sm:384 md:576 lg:768 xl:960"
onPointerDown={(event) => event.stopPropagation()}
style={{
...getControllerStyle(mediaType),
...(showLoading ? { position: "absolute", opacity: 0 } : undefined),
}}
>
{mediaElement}

{!isAudio && <div className="video-gradient-bottom" />}

<MediaControlBar
style={isAudio ? AUDIO_CONTROL_BAR_STYLE : CONTROL_BAR_STYLE}
>
<MediaPlayButton ref={(el) => el?.setAttribute("notooltip", "")}>
<PlayPauseIcon />
</MediaPlayButton>

<div className="mute-group">
<div className="mute-group-inner">
<MediaMuteButton ref={(el) => el?.setAttribute("notooltip", "")}>
<MuteIcon />
</MediaMuteButton>
<div className="vol-wrap">
<MediaVolumeRange />
</div>
</div>
</div>

<MediaTimeDisplay />
<MediaTimeDisplay showDuration />

<MediaTimeRange>
{!isAudio && (
<>
<MediaPreviewThumbnail slot="preview" />
<MediaPreviewChapterDisplay slot="preview" />
</>
)}
<MediaPreviewTimeDisplay slot="preview" />
</MediaTimeRange>

{mediaType !== "spotify" && (
<div className="settings-group">
<div className="settings-group-inner">
<MediaPlaybackRateMenuButton
ref={(el) => el?.setAttribute("notooltip", "")}
>
<span
className="flex size-full items-center justify-center"
slot="icon"
>
<SettingsIcon />
</span>
</MediaPlaybackRateMenuButton>
<div className="settings-menu-wrap">
<MediaPlaybackRateMenu hidden />
</div>
</div>
</div>
)}

{mediaType === "video" && (
<MediaPipButton ref={(el) => el?.setAttribute("notooltip", "")}>
<PipIcon />
</MediaPipButton>
)}

{!isAudio && (
<MediaFullscreenButton
ref={(el) => el?.setAttribute("notooltip", "")}
>
<FullscreenIcon />
</MediaFullscreenButton>
)}
</MediaControlBar>
</MediaController>
</>
);
}

export function MediaPlayer(props: MediaPlayerProps) {
return (
<MediaProvider>
<MediaPlayerInner {...props} />
</MediaProvider>
);
}
25 changes: 0 additions & 25 deletions src/components/VideoPlayer.tsx

This file was deleted.

Loading