"use client"; import Link from "next/link"; import { ChangeEvent, DragEvent, useEffect, useMemo, useRef, useState } from "react"; import mediaData from "../data/media-player.json"; import styles from "./MediaPlayer.module.css"; import lyricStyles from "./MediaLyrics.module.css"; import playlistStyles from "./MediaPlaylist.module.css"; type Mode = "video" | "music"; type PlayMode = "sequence" | "list-loop" | "single-loop" | "shuffle"; type VideoItem = (typeof mediaData.videos)[number] & { local?: boolean }; type TrackItem = (typeof mediaData.tracks)[number] & { local?: boolean }; export default function MediaPlayerExperience({ mode }: { mode: Mode }) { const videoRef = useRef(null); const audioRef = useRef(null); const videoShellRef = useRef(null); const videoUploadRef = useRef(null); const audioUploadRef = useRef(null); const lyricsRef = useRef(null); const localUrlsRef = useRef([]); const [videos, setVideos] = useState(mediaData.videos); const [currentVideoId, setCurrentVideoId] = useState(mediaData.videos[0].id); const [tracks, setTracks] = useState(mediaData.tracks); const [trackIndex, setTrackIndex] = useState(0); const [videoPlaying, setVideoPlaying] = useState(false); const [musicPlaying, setMusicPlaying] = useState(false); const [videoTime, setVideoTime] = useState(0); const [videoDuration, setVideoDuration] = useState(0); const [musicTime, setMusicTime] = useState(0); const [musicDuration, setMusicDuration] = useState(0); const [volume, setVolume] = useState(0.76); const [muted, setMuted] = useState(false); const [speed, setSpeed] = useState(1); const [musicPlayMode, setMusicPlayMode] = useState("sequence"); const [videoPlayMode, setVideoPlayMode] = useState("sequence"); const [draggedTrackId, setDraggedTrackId] = useState(null); const [trackDropId, setTrackDropId] = useState(null); const [draggedVideoId, setDraggedVideoId] = useState(null); const [videoDropId, setVideoDropId] = useState(null); const [videoError, setVideoError] = useState(false); const [audioError, setAudioError] = useState(false); const [toast, setToast] = useState(""); const currentVideo = videos.find((video) => video.id === currentVideoId) ?? videos[0]; const currentTrack = tracks[trackIndex] ?? tracks[0]; const waveform = useMemo(() => Array.from({ length: 68 }, (_, index) => 18 + ((index * 29 + 17) % 70)), []); const activeLyricIndex = useMemo(() => { if (!currentTrack?.lyrics?.length) return -1; let active = 0; currentTrack.lyrics.forEach((line, index) => { if (line.time <= musicTime) active = index; }); return active; }, [currentTrack, musicTime]); useEffect(() => () => localUrlsRef.current.forEach((url) => URL.revokeObjectURL(url)), []); useEffect(() => { if (videoRef.current) { videoRef.current.volume = volume; videoRef.current.muted = muted; } if (audioRef.current) { audioRef.current.volume = volume; audioRef.current.muted = muted; } }, [volume, muted]); useEffect(() => { setMusicTime(0); setMusicDuration(0); setAudioError(false); }, [trackIndex]); useEffect(() => { const container = lyricsRef.current; const line = container?.querySelector(`[data-lyric-index="${activeLyricIndex}"]`); if (!container || !line) return; container.scrollTo({ top: line.offsetTop - container.clientHeight / 2 + line.clientHeight / 2, behavior: "smooth" }); }, [activeLyricIndex]); const notify = (message: string) => { setToast(message); window.setTimeout(() => setToast(""), 1800); }; const goToMode = (next: Mode) => { if (next === mode) return; videoRef.current?.pause(); audioRef.current?.pause(); window.location.href = next === "video" ? "/media-player" : "/music-player"; }; const toggleVideo = async () => { const video = videoRef.current; if (!video) return; if (video.paused) { try { await video.play(); } catch { notify("视频尚未加载,请选择本地视频重试"); } } else video.pause(); }; const toggleMusic = async () => { const audio = audioRef.current; if (!audio) return; if (audio.paused) { try { await audio.play(); } catch { notify("音乐尚未加载,请选择本地音乐重试"); } } else audio.pause(); }; const changeVideoTime = (value: number) => { if (!videoRef.current) return; videoRef.current.currentTime = value; setVideoTime(value); }; const changeMusicTime = (value: number) => { if (!audioRef.current) return; audioRef.current.currentTime = value; setMusicTime(value); }; const changeVolume = (value: number) => { setVolume(value); setMuted(value === 0); }; const toggleMute = () => setMuted((value) => !value); const changeSpeed = (value: number) => { setSpeed(value); if (videoRef.current) videoRef.current.playbackRate = value; }; const openPictureInPicture = async () => { const video = videoRef.current; if (!video || !document.pictureInPictureEnabled) return notify("当前浏览器不支持画中画"); try { await video.requestPictureInPicture(); } catch { notify("暂时无法进入画中画"); } }; const openFullscreen = async () => { try { await videoShellRef.current?.requestFullscreen(); } catch { notify("暂时无法进入全屏"); } }; const selectTrack = async (index: number, autoPlay = true) => { setTrackIndex(index); setMusicPlaying(false); if (autoPlay) window.setTimeout(() => audioRef.current?.play().catch(() => undefined), 30); }; const nextTrack = () => { const next = musicPlayMode === "shuffle" ? randomOtherIndex(tracks.length, trackIndex) : (trackIndex + 1) % tracks.length; selectTrack(next); }; const previousTrack = () => selectTrack((trackIndex - 1 + tracks.length) % tracks.length); const handleTrackEnded = () => { if (musicPlayMode === "single-loop" && audioRef.current) { audioRef.current.currentTime = 0; audioRef.current.play().catch(() => undefined); } else if (musicPlayMode === "sequence" && trackIndex === tracks.length - 1) { setMusicPlaying(false); } else nextTrack(); }; const selectVideo = (video: VideoItem, autoPlay = false) => { videoRef.current?.pause(); setCurrentVideoId(video.id); setVideoTime(0); setVideoDuration(0); setVideoError(false); setVideoPlaying(false); if (autoPlay) window.setTimeout(() => videoRef.current?.play().catch(() => undefined), 30); }; const handleVideoEnded = () => { const currentIndex = videos.findIndex((video) => video.id === currentVideo.id); if (videoPlayMode === "single-loop" && videoRef.current) { videoRef.current.currentTime = 0; videoRef.current.play().catch(() => undefined); return; } if (videoPlayMode === "sequence" && currentIndex === videos.length - 1) { setVideoPlaying(false); return; } const nextIndex = videoPlayMode === "shuffle" ? randomOtherIndex(videos.length, currentIndex) : (currentIndex + 1) % videos.length; selectVideo(videos[nextIndex], true); }; const reorderTracks = (sourceId: string, targetId: string) => { if (sourceId === targetId) return; const activeId = currentTrack.id; setTracks((current) => moveItem(current, sourceId, targetId)); setTrackIndex(moveItem(tracks, sourceId, targetId).findIndex((track) => track.id === activeId)); notify("歌单顺序已更新"); }; const reorderVideos = (sourceId: string, targetId: string) => { if (sourceId === targetId) return; setVideos((current) => moveItem(current, sourceId, targetId)); notify("视频顺序已更新"); }; const handleTrackDrop = (event: DragEvent, targetId: string) => { event.preventDefault(); const sourceId = event.dataTransfer.getData("text/plain") || draggedTrackId; if (sourceId) reorderTracks(sourceId, targetId); setDraggedTrackId(null); setTrackDropId(null); }; const handleVideoDrop = (event: DragEvent, targetId: string) => { event.preventDefault(); const sourceId = event.dataTransfer.getData("text/plain") || draggedVideoId; if (sourceId) reorderVideos(sourceId, targetId); setDraggedVideoId(null); setVideoDropId(null); }; const uploadVideo = (event: ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; const url = URL.createObjectURL(file); localUrlsRef.current.push(url); videoRef.current?.pause(); const video: VideoItem = { id: `local-video-${Date.now()}`, title: file.name.replace(/\.[^.]+$/, ""), subtitle: "本地视频文件", src: url, poster: "/anime/star-echoes-hero.png", durationLabel: "本地", quality: file.type.split("/")[1]?.toUpperCase() || "VIDEO", views: "刚刚添加", local: true }; setVideos((current) => [...current, video]); setCurrentVideoId(video.id); setVideoTime(0); setVideoDuration(0); setVideoError(false); setVideoPlaying(false); event.target.value = ""; notify("本地视频已载入"); }; const uploadTracks = (event: ChangeEvent) => { const files = Array.from(event.target.files ?? []).filter((file) => file.type.startsWith("audio/")); if (!files.length) return; const additions = files.map((file, index) => { const url = URL.createObjectURL(file); localUrlsRef.current.push(url); return { id: `local-track-${Date.now()}-${index}`, title: file.name.replace(/\.[^.]+$/, ""), artist: "本地音乐", album: "本地播放列表", src: url, cover: "/anime/star-echoes-merch.png", position: "50% center", durationLabel: "本地", lyrics: [], local: true }; }); setTracks((current) => [...current, ...additions]); setTrackIndex(tracks.length); event.target.value = ""; notify(`已添加 ${additions.length} 首本地音乐`); }; return (
Orbit PlayerMEDIA EXPERIENCE
HTML5 · CUSTOM UI
{mode === "video" &&

VIDEO PLAYER

{currentVideo.title}

{currentVideo.subtitle}
{currentVideo.quality}{currentVideo.views}
} {mode === "music" && currentTrack &&
} {toast &&
✓ {toast}
}
); } function formatTime(value: number) { if (!Number.isFinite(value) || value <= 0) return "00:00"; const minutes = Math.floor(value / 60); const seconds = Math.floor(value % 60); return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`; } const playModeOptions: { id: PlayMode; label: string; icon: string }[] = [ { id: "sequence", label: "顺序播放", icon: "→" }, { id: "list-loop", label: "列表循环", icon: "↻" }, { id: "single-loop", label: "单个循环", icon: "↺" }, { id: "shuffle", label: "随机播放", icon: "⌘" }, ]; function moveItem(items: T[], sourceId: string, targetId: string) { const sourceIndex = items.findIndex((item) => item.id === sourceId); const targetIndex = items.findIndex((item) => item.id === targetId); if (sourceIndex < 0 || targetIndex < 0 || sourceIndex === targetIndex) return items; const next = [...items]; const [moved] = next.splice(sourceIndex, 1); next.splice(targetIndex, 0, moved); return next; } function randomOtherIndex(length: number, current: number) { if (length <= 1) return current; let next = current; while (next === current) next = Math.floor(Math.random() * length); return next; }