Fix nested optional chaining in lossless tag & improve video sync

This commit is contained in:
SrGooglo 2025-04-24 06:14:25 +00:00
parent 6e80fc67fa
commit 6100feb608
3 changed files with 161 additions and 141 deletions

View File

@ -177,7 +177,7 @@ const PlayerController = React.forwardRef((props, ref) => {
)} )}
<div className="lyrics-player-controller-tags"> <div className="lyrics-player-controller-tags">
{playerState.track_manifest?.metadata.lossless && ( {playerState.track_manifest?.metadata?.lossless && (
<Tag <Tag
icon={ icon={
<Icons.Lossless <Icons.Lossless

View File

@ -7,178 +7,197 @@ import { usePlayerStateContext } from "@contexts/WithPlayerContext"
const maxLatencyInMs = 55 const maxLatencyInMs = 55
const LyricsVideo = React.forwardRef((props, videoRef) => { const LyricsVideo = React.forwardRef((props, videoRef) => {
const [playerState] = usePlayerStateContext() const [playerState] = usePlayerStateContext()
const { lyrics } = props const { lyrics } = props
const [initialLoading, setInitialLoading] = React.useState(true) const [initialLoading, setInitialLoading] = React.useState(true)
const [syncInterval, setSyncInterval] = React.useState(null) const [syncInterval, setSyncInterval] = React.useState(null)
const [syncingVideo, setSyncingVideo] = React.useState(false) const [syncingVideo, setSyncingVideo] = React.useState(false)
const [currentVideoLatency, setCurrentVideoLatency] = React.useState(0) const [currentVideoLatency, setCurrentVideoLatency] = React.useState(0)
const hls = React.useRef(new HLS()) const hls = React.useRef(new HLS())
async function seekVideoToSyncAudio() { async function seekVideoToSyncAudio() {
if (!lyrics) { if (!lyrics) {
return null return null
} }
if (!lyrics.video_source || typeof lyrics.sync_audio_at_ms === "undefined") { if (
return null !lyrics.video_source ||
} typeof lyrics.sync_audio_at_ms === "undefined"
) {
return null
}
const currentTrackTime = app.cores.player.controls.seek() const currentTrackTime = app.cores.player.controls.seek()
setSyncingVideo(true) setSyncingVideo(true)
let newTime = currentTrackTime + (lyrics.sync_audio_at_ms / 1000) + app.cores.player.gradualFadeMs / 1000 let newTime =
currentTrackTime + lyrics.sync_audio_at_ms / 1000 + 150 / 1000
// dec some ms to ensure the video seeks correctly // dec some ms to ensure the video seeks correctly
newTime -= 5 / 1000 newTime -= 5 / 1000
videoRef.current.currentTime = newTime videoRef.current.currentTime = newTime
} }
async function syncPlayback() { async function syncPlayback() {
// if something is wrong, stop syncing // if something is wrong, stop syncing
if (videoRef.current === null || !lyrics || !lyrics.video_source || typeof lyrics.sync_audio_at_ms === "undefined" || playerState.playback_status !== "playing") { if (
return stopSyncInterval() videoRef.current === null ||
} !lyrics ||
!lyrics.video_source ||
typeof lyrics.sync_audio_at_ms === "undefined" ||
playerState.playback_status !== "playing"
) {
return stopSyncInterval()
}
const currentTrackTime = app.cores.player.controls.seek() const currentTrackTime = app.cores.player.controls.seek()
const currentVideoTime = videoRef.current.currentTime - (lyrics.sync_audio_at_ms / 1000) const currentVideoTime =
videoRef.current.currentTime - lyrics.sync_audio_at_ms / 1000
//console.log(`Current track time: ${currentTrackTime}, current video time: ${currentVideoTime}`) //console.log(`Current track time: ${currentTrackTime}, current video time: ${currentVideoTime}`)
const maxOffset = maxLatencyInMs / 1000 const maxOffset = maxLatencyInMs / 1000
const currentVideoTimeDiff = Math.abs(currentVideoTime - currentTrackTime) const currentVideoTimeDiff = Math.abs(
currentVideoTime - currentTrackTime,
)
setCurrentVideoLatency(currentVideoTimeDiff) setCurrentVideoLatency(currentVideoTimeDiff)
if (syncingVideo === true) { if (syncingVideo === true) {
return false return false
} }
if (currentVideoTimeDiff > maxOffset) { if (currentVideoTimeDiff > maxOffset) {
seekVideoToSyncAudio() seekVideoToSyncAudio()
} }
} }
function startSyncInterval() { function startSyncInterval() {
setSyncInterval(setInterval(syncPlayback, 300)) setSyncInterval(setInterval(syncPlayback, 300))
} }
function stopSyncInterval() { function stopSyncInterval() {
setSyncingVideo(false) setSyncingVideo(false)
setSyncInterval(null) setSyncInterval(null)
clearInterval(syncInterval) clearInterval(syncInterval)
} }
//* handle when player is loading //* handle when player is loading
React.useEffect(() => { React.useEffect(() => {
if (lyrics?.video_source && playerState.loading === true && playerState.playback_status === "playing") { if (
videoRef.current.pause() lyrics?.video_source &&
} playerState.loading === true &&
playerState.playback_status === "playing"
) {
videoRef.current.pause()
}
if (lyrics?.video_source && playerState.loading === false && playerState.playback_status === "playing") { if (
videoRef.current.play() lyrics?.video_source &&
} playerState.loading === false &&
}, [playerState.loading]) playerState.playback_status === "playing"
) {
videoRef.current.play()
}
}, [playerState.loading])
//* Handle when playback status change //* Handle when playback status change
React.useEffect(() => { React.useEffect(() => {
if (initialLoading === false) { if (initialLoading === false) {
console.log(`VIDEO:: Playback status changed to ${playerState.playback_status}`) console.log(
`VIDEO:: Playback status changed to ${playerState.playback_status}`,
)
if (lyrics && lyrics.video_source) { if (lyrics && lyrics.video_source) {
if (playerState.playback_status === "playing") { if (playerState.playback_status === "playing") {
videoRef.current.play() videoRef.current.play()
startSyncInterval() startSyncInterval()
} else { } else {
videoRef.current.pause() videoRef.current.pause()
stopSyncInterval() stopSyncInterval()
} }
} }
} }
}, [playerState.playback_status]) }, [playerState.playback_status])
//* Handle when lyrics object change //* Handle when lyrics object change
React.useEffect(() => { React.useEffect(() => {
setCurrentVideoLatency(0) setCurrentVideoLatency(0)
stopSyncInterval() stopSyncInterval()
if (lyrics) { if (lyrics) {
if (lyrics.video_source) { if (lyrics.video_source) {
console.log("Loading video source >", lyrics.video_source) console.log("Loading video source >", lyrics.video_source)
if (lyrics.video_source.endsWith(".mp4")) { if (lyrics.video_source.endsWith(".mp4")) {
videoRef.current.src = lyrics.video_source videoRef.current.src = lyrics.video_source
} else { } else {
hls.current.loadSource(lyrics.video_source) hls.current.loadSource(lyrics.video_source)
} }
if (typeof lyrics.sync_audio_at_ms !== "undefined") { if (typeof lyrics.sync_audio_at_ms !== "undefined") {
videoRef.current.loop = false videoRef.current.loop = false
videoRef.current.currentTime = lyrics.sync_audio_at_ms / 1000 videoRef.current.currentTime =
lyrics.sync_audio_at_ms / 1000
startSyncInterval() startSyncInterval()
} else { } else {
videoRef.current.loop = true videoRef.current.loop = true
videoRef.current.currentTime = 0 videoRef.current.currentTime = 0
} }
if (playerState.playback_status === "playing") { if (playerState.playback_status === "playing") {
videoRef.current.play() videoRef.current.play()
} }
} }
} }
setInitialLoading(false) setInitialLoading(false)
}, [lyrics]) }, [lyrics])
React.useEffect(() => { React.useEffect(() => {
videoRef.current.addEventListener("seeked", (event) => { videoRef.current.addEventListener("seeked", (event) => {
setSyncingVideo(false) setSyncingVideo(false)
}) })
hls.current.attachMedia(videoRef.current) hls.current.attachMedia(videoRef.current)
return () => { return () => {
stopSyncInterval() stopSyncInterval()
} }
}, []) }, [])
return <> return (
{ <>
props.lyrics?.sync_audio_at && <div {props.lyrics?.sync_audio_at && (
className={classnames( <div className={classnames("videoDebugOverlay")}>
"videoDebugOverlay", <div>
)} <p>Maximun latency</p>
> <p>{maxLatencyInMs}ms</p>
<div> </div>
<p>Maximun latency</p> <div>
<p>{maxLatencyInMs}ms</p> <p>Video Latency</p>
</div> <p>{(currentVideoLatency * 1000).toFixed(2)}ms</p>
<div> </div>
<p>Video Latency</p> {syncingVideo ? <p>Syncing video...</p> : null}
<p>{(currentVideoLatency * 1000).toFixed(2)}ms</p> </div>
</div> )}
{syncingVideo ? <p>Syncing video...</p> : null}
</div>
}
<video <video
className={classnames( className={classnames("lyrics-video", {
"lyrics-video", ["hidden"]: !lyrics || !lyrics?.video_source,
{ })}
["hidden"]: !lyrics || !lyrics?.video_source ref={videoRef}
} controls={false}
)} muted
ref={videoRef} preload="auto"
controls={false} />
muted </>
preload="auto" )
/>
</>
}) })
export default LyricsVideo export default LyricsVideo

View File

@ -89,7 +89,8 @@ const EnhancedLyricsPage = () => {
// Track manifest comparison // Track manifest comparison
useEffect(() => { useEffect(() => {
const newManifest = playerState.track_manifest?.toSeriableObject() const newManifest = playerState.track_manifest
if (JSON.stringify(newManifest) !== JSON.stringify(trackManifest)) { if (JSON.stringify(newManifest) !== JSON.stringify(trackManifest)) {
setTrackManifest(newManifest) setTrackManifest(newManifest)
} }