SrGooglo 0eaecf6fd3 Refactor player internals and sync
Replace `TrackInstance` with direct `TrackManifest` usage in the player
core. Introduce a `SyncRoom` class and related hooks (`useSyncRoom`,
`useLyrics`, etc.) for real-time state synchronization and shared lyrics
display. Enhance player indicators to show detailed audio format metadata
(codec, sample rate, bit depth). Relocate the Indicators component and update
the Lyrics page to utilize these new features and components.
2025-05-21 19:03:08 +00:00

166 lines
3.2 KiB
JavaScript

import React from "react"
import classnames from "classnames"
import { motion, AnimatePresence } from "motion/react"
import { usePlayerStateContext } from "@contexts/WithPlayerContext"
// eslint-disable-next-line
const LyricsText = React.forwardRef((props, textRef) => {
const [playerState] = usePlayerStateContext()
const { lyrics } = props
const [syncInterval, setSyncInterval] = React.useState(null)
const [currentLineIndex, setCurrentLineIndex] = React.useState(0)
const [visible, setVisible] = React.useState(false)
function syncPlayback() {
const currentTrackTime = app.cores.player.controls.seek() * 1000
const lineIndex = lyrics.synced_lyrics.findIndex((line) => {
return (
currentTrackTime >= line.startTimeMs &&
currentTrackTime <= line.endTimeMs
)
})
if (lineIndex === -1) {
if (!visible) {
setVisible(false)
}
return false
}
const line = lyrics.synced_lyrics[lineIndex]
setCurrentLineIndex(lineIndex)
if (line.break) {
return setVisible(false)
}
if (line.text) {
return setVisible(true)
}
}
function startSyncInterval() {
if (!lyrics || !lyrics.synced_lyrics) {
stopSyncInterval()
return false
}
if (playerState.playback_status !== "playing") {
stopSyncInterval()
return false
}
if (syncInterval) {
stopSyncInterval()
}
setSyncInterval(setInterval(syncPlayback, 100))
}
function stopSyncInterval() {
clearInterval(syncInterval)
setSyncInterval(null)
}
//* Handle when current line index change
React.useEffect(() => {
if (currentLineIndex === 0) {
setVisible(false)
} else {
setVisible(true)
if (textRef.current) {
// find line element by id
const lineElement = textRef.current.querySelector(
`#lyrics-line-${currentLineIndex}`,
)
// center scroll to current line
if (lineElement) {
lineElement.scrollIntoView({
behavior: "smooth",
block: "center",
})
} else {
// scroll to top
textRef.current.scrollTop = 0
}
}
}
}, [currentLineIndex])
//* Handle when playback status change
React.useEffect(() => {
startSyncInterval()
}, [playerState.playback_status])
//* Handle when manifest object change, reset...
React.useEffect(() => {
setVisible(false)
setCurrentLineIndex(0)
}, [playerState.track_manifest])
React.useEffect(() => {
startSyncInterval()
}, [lyrics])
React.useEffect(() => {
return () => {
clearInterval(syncInterval)
}
}, [])
if (!lyrics?.synced_lyrics) {
return null
}
return (
<div className="lyrics-text-wrapper">
<AnimatePresence>
{visible && (
<motion.div
ref={textRef}
className="lyrics-text"
animate={{
opacity: 1,
}}
initial={{
opacity: 0,
}}
exit={{
opacity: 0,
}}
transition={{
type: "spring",
stiffness: 100,
damping: 20,
}}
>
{lyrics.synced_lyrics.map((line, index) => {
return (
<p
key={index}
id={`lyrics-line-${index}`}
className={classnames("line", {
["current"]: currentLineIndex === index,
})}
>
{line.text}
</p>
)
})}
</motion.div>
)}
</AnimatePresence>
</div>
)
})
export default LyricsText