From 287cb9b903580d16847b3e78cb99baf7199a019c Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Tue, 21 Jan 2025 21:55:00 +0700 Subject: [PATCH] feat: Add Vidstack replacing Plyr --- src/components/Preview/Audio.tsx | 98 ---- src/components/Preview/Media.tsx | 754 +++++++++++++++++++++++++++++++ src/components/Preview/Video.tsx | 96 ---- tailwind.config.ts | 10 +- 4 files changed, 763 insertions(+), 195 deletions(-) delete mode 100644 src/components/Preview/Audio.tsx create mode 100644 src/components/Preview/Media.tsx delete mode 100644 src/components/Preview/Video.tsx diff --git a/src/components/Preview/Audio.tsx b/src/components/Preview/Audio.tsx deleted file mode 100644 index 87bcc1f..0000000 --- a/src/components/Preview/Audio.tsx +++ /dev/null @@ -1,98 +0,0 @@ -"use client"; - -import dynamic from "next/dynamic"; -import { useState } from "react"; -import "react-h5-audio-player/lib/styles.css"; -import { z } from "zod"; - -import { Loader, Status } from "~/components/global"; - -import useLoading from "~/hooks/useLoading"; - -import { Schema_File } from "~/types/schema"; - -import { CreateDownloadToken } from "actions"; - -const Plyr = dynamic(() => import("plyr-react"), { - ssr: false, - loading: () => , -}); - -type Props = { - file: z.infer; -}; -export default function PreviewAudio({ file }: Props) { - const [audioSrc, setAudioSrc] = useState(""); - const [error, setError] = useState(""); - - const loading = useLoading(async () => { - try { - if (!file.encryptedWebContentLink) { - setError("No audio to preview"); - return; - } - const token = await CreateDownloadToken(); - setAudioSrc(`/api/stream/${file.encryptedId}?token=${token}`); - } catch (error) { - const e = error as Error; - console.error(e); - setError(e.message); - } - }, [file]); - - return ( -
- {loading ? ( - - ) : error ? ( - - ) : ( -
- -
- )} -
- ); -} diff --git a/src/components/Preview/Media.tsx b/src/components/Preview/Media.tsx new file mode 100644 index 0000000..6d7d827 --- /dev/null +++ b/src/components/Preview/Media.tsx @@ -0,0 +1,754 @@ +"use client"; + +import { + type AudioMimeType, + MediaPlayer, + type MediaPlayerInstance, + type MediaPlayerQuery, + MediaProvider, + Menu, + PlayButton, + SeekButton, + Time, + TimeSlider, + type VideoMimeType, + useAudioGainOptions, + usePlaybackRateOptions, +} from "@vidstack/react"; +import { DefaultAudioLayout, DefaultVideoLayout, defaultLayoutIcons } from "@vidstack/react/player/layouts/default"; +import "@vidstack/react/player/styles/base.css"; +import "@vidstack/react/player/styles/default/layouts/audio.css"; +import "@vidstack/react/player/styles/default/layouts/video.css"; +import "@vidstack/react/player/styles/default/theme.css"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { useCallback, useRef, useState } from "react"; +import { type z } from "zod"; + +import Icon from "~/components/ui/icon"; + +import useLoading from "~/hooks/useLoading"; + +import { type Schema_File } from "~/types/schema"; + +import "~/styles/vidstack.css"; + +import { PageLoader } from "../layout"; + +export const MediaPlayerIcons = { + ...defaultLayoutIcons, + AirPlayButton: { + Default: () => ( + + ), + Connecting: () => ( + + ), + Connected: () => ( + + ), + }, + GoogleCastButton: { + Default: () => ( + + ), + Connecting: () => ( + + ), + Connected: () => ( + + ), + }, + PlayButton: { + Play: () => ( + + ), + Pause: () => ( + + ), + Replay: () => ( + + ), + }, + MuteButton: { + Mute: () => ( + + ), + VolumeLow: () => ( + + ), + VolumeHigh: () => ( + + ), + }, + CaptionButton: { + On: () => ( + + ), + Off: () => ( + + ), + }, + PIPButton: { + Enter: () => ( + + ), + Exit: () => ( + + ), + }, + FullscreenButton: { + Enter: () => ( + + ), + Exit: () => ( + + ), + }, + SeekButton: { + Backward: () => ( + + ), + Forward: () => ( + + ), + }, + DownloadButton: { + Default: () => ( + + ), + }, + Menu: { + Accessibility: () => ( + + ), + ArrowLeft: () => null, + ArrowRight: () => ( + + ), + Audio: () => ( + + ), + AudioBoostUp: () => ( + + ), + AudioBoostDown: () => ( + + ), + Chapters: () => ( + + ), + Captions: () => ( + + ), + Playback: () => ( + + ), + Settings: () => ( + + ), + SpeedUp: () => ( + + ), + SpeedDown: () => ( + + ), + QualityUp: () => null, + QualityDown: () => null, + FontSizeUp: () => null, + FontSizeDown: () => null, + OpacityUp: () => null, + OpacityDown: () => null, + RadioCheck: () => null, + }, + KeyboardDisplay: { + Play: () => ( +
+ +
+ ), + Pause: () => ( +
+ +
+ ), + Mute: () => ( +
+ +
+ ), + VolumeUp: () => ( +
+ +
+ ), + VolumeDown: () => ( +
+ +
+ ), + EnterFullscreen: () => ( +
+ +
+ ), + ExitFullscreen: () => ( +
+ +
+ ), + // EnterPiP: () => null, + // ExitPiP: () => null, + CaptionsOn: () => ( +
+ +
+ ), + CaptionsOff: () => ( +
+ +
+ ), + SeekForward: () => ( +
+ +
+ ), + SeekBackward: () => ( +
+ +
+ ), + }, +}; + +type Props = { + file: z.infer; + type: "video" | "audio"; +}; +export default function PreviewMedia({ file, type }: Props) { + const loading = useLoading(); + const player = useRef(null); + const [canPlay, setCanPlay] = useState(false); + + const [isLoop, setIsLoop] = useState(false); + + const smallAudioLayoutQuery = useCallback(({ width }) => { + return width < 576; + }, []); + const smallVideoLayoutQuery = useCallback(({ width, height }) => { + return width < 576 || height < 380; + }, []); + + return ( +
+ {loading ? ( + + ) : ( + { + setCanPlay(true); + }} + > + + + + + + + {canPlay ? ( + <> + + + + + ) : ( + + )} + + + + +
+ ), + timeSlider: ( +
+ + + + + + +
+ ), + beforeEndTime:
, + endTime: ( +
+
+
+ +
+ + + + + + {/* Loop */} + + + + + Loop + {isLoop ? "On" : "Off"} + + + + + setIsLoop(true)} + > + + On + + setIsLoop(false)} + > + + Off + + + + + + {/* Playback Speed */} + + + {/* Audio Gain */} + + + +
+
+ ), + settingsMenu: null, + }} + /> + + ), + endTime: ( +
+ ); +} + +function PlaybackMenu() { + const options = usePlaybackRateOptions(); + const hint = options.selectedValue === "1" ? "Normal" : `${options.selectedValue}x`; + + return ( + + + + + Playback Speed + {hint} + + + + + {/* eslint-disable-next-line @typescript-eslint/unbound-method */} + {options.map(({ value, select, label }) => ( + + + {label} + + ))} + + + + ); +} +function AudioGain() { + const options = useAudioGainOptions({ + disabledLabel: "100%", + }); + const hint = options.selectedValue ? `${Number(options.selectedValue) * 100}%` : "100%"; + + return ( + + + + + Audio Boost + {hint} + + + + + {/* eslint-disable-next-line @typescript-eslint/unbound-method */} + {options.map(({ value, select, label }) => ( + + + {label} + + ))} + + + + ); +} diff --git a/src/components/Preview/Video.tsx b/src/components/Preview/Video.tsx deleted file mode 100644 index 5c4e335..0000000 --- a/src/components/Preview/Video.tsx +++ /dev/null @@ -1,96 +0,0 @@ -"use client"; - -import dynamic from "next/dynamic"; -import { useState } from "react"; -import { z } from "zod"; - -import { Loader, Status } from "~/components/global"; - -import useLoading from "~/hooks/useLoading"; - -import { Schema_File } from "~/types/schema"; - -import { CreateDownloadToken } from "actions"; - -const Plyr = dynamic(() => import("plyr-react"), { - ssr: false, - loading: () => , -}); - -type Props = { - file: z.infer; -}; -export default function PreviewVideo({ file }: Props) { - const [videoSrc, setVideoSrc] = useState(""); - const [error, setError] = useState(""); - const loading = useLoading(async () => { - try { - if (!file.encryptedWebContentLink) { - setError("No video to preview"); - return; - } - const token = await CreateDownloadToken(); - setVideoSrc(`/api/stream/${file.encryptedId}?token=${token}`); - } catch (error) { - const e = error as Error; - console.error(e); - setError(e.message); - } - }, [file]); - - return ( -
- {loading ? ( - - ) : error ? ( - - ) : ( -
- -
- )} -
- ); -} diff --git a/tailwind.config.ts b/tailwind.config.ts index fefaa21..fc3d5ce 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,4 +1,5 @@ import twTypography from "@tailwindcss/typography"; +import twVidstack from "@vidstack/react/tailwind.cjs"; import type { Config } from "tailwindcss"; import twAnimate from "tailwindcss-animate"; import tw from "tailwindcss/defaultTheme"; @@ -103,7 +104,14 @@ const config: Config = { }, }, }, - plugins: [twAnimate, twTypography], + plugins: [ + twAnimate, + twTypography, + twVidstack({ + selector: ".media-player", + prefix: "media", + }), + ], }; export default config;