performance refactor for synchronized lyrics timestamp/status subscribers

This commit is contained in:
jeffvli
2026-07-08 10:41:44 -07:00
parent 7b5df97fa0
commit ef3f3043bc
@@ -6,13 +6,14 @@ import styles from './synchronized-lyrics.module.css';
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line'; import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
import { import {
subscribePlayerStatus,
useLyricsDisplaySettings, useLyricsDisplaySettings,
useLyricsSettings, useLyricsSettings,
usePlaybackType, usePlaybackType,
usePlayerActions, usePlayerActions,
usePlayerStatus, usePlayerStoreBase,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { usePlayerTimestamp } from '/@/renderer/store/timestamp.store'; import { subscribePlayerProgress, useTimestampStoreBase } from '/@/renderer/store/timestamp.store';
import { FullLyricsMetadata, SynchronizedLyricsArray } from '/@/shared/types/domain-types'; import { FullLyricsMetadata, SynchronizedLyricsArray } from '/@/shared/types/domain-types';
import { PlayerStatus, PlayerType } from '/@/shared/types/types'; import { PlayerStatus, PlayerType } from '/@/shared/types/types';
@@ -58,8 +59,6 @@ export const SynchronizedLyrics = ({
: 0.95, : 0.95,
}; };
const { mediaSeekToTimestamp } = usePlayerActions(); const { mediaSeekToTimestamp } = usePlayerActions();
const status = usePlayerStatus();
const timestamp = usePlayerTimestamp();
const effectiveOffsetMs = offsetMs ?? 0; const effectiveOffsetMs = offsetMs ?? 0;
@@ -75,7 +74,20 @@ export const SynchronizedLyrics = ({
[mediaSeekToTimestamp, playbackType], [mediaSeekToTimestamp, playbackType],
); );
// const seeked = useSeeked(); const handleContainerClick = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
const target = (event.target as HTMLElement).closest('[data-lyric-time]');
if (!target) {
return;
}
const time = Number((target as HTMLElement).dataset.lyricTime);
if (time > 0 && Number.isFinite(time)) {
handleSeek(time / 1000);
}
},
[handleSeek],
);
// A reference to the timeout handler // A reference to the timeout handler
const lyricTimer = useRef<null | ReturnType<typeof setTimeout>>(null); const lyricTimer = useRef<null | ReturnType<typeof setTimeout>>(null);
@@ -91,6 +103,7 @@ export const SynchronizedLyrics = ({
const delayMsRef = useRef(effectiveOffsetMs); const delayMsRef = useRef(effectiveOffsetMs);
const followRef = useRef(settings.follow); const followRef = useRef(settings.follow);
const statusRef = useRef(usePlayerStoreBase.getState().player.status);
const userScrollingRef = useRef(false); const userScrollingRef = useRef(false);
const scrollTimeoutRef = useRef<null | ReturnType<typeof setTimeout>>(null); const scrollTimeoutRef = useRef<null | ReturnType<typeof setTimeout>>(null);
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
@@ -197,6 +210,11 @@ export const SynchronizedLyrics = ({
[], [],
); );
const syncFromCurrentTimestamp = useCallback(() => {
const timestamp = useTimestampStoreBase.getState().timestamp;
setCurrentLyric(timestamp * 1000 + delayMsRef.current);
}, [setCurrentLyric]);
// Store the callback in a ref so it can be called recursively // Store the callback in a ref so it can be called recursively
useEffect(() => { useEffect(() => {
setCurrentLyricRef.current = setCurrentLyric; setCurrentLyricRef.current = setCurrentLyric;
@@ -208,32 +226,22 @@ export const SynchronizedLyrics = ({
}, [settings.follow]); }, [settings.follow]);
useEffect(() => { useEffect(() => {
// This handler is used to handle when lyrics change. It is in some sense the
// 'primary' handler for parsing lyrics, as unlike the other callbacks, it will
// ALSO remove listeners on close.
lyricRef.current = lyrics; lyricRef.current = lyrics;
if (status === PlayerStatus.PLAYING) { if (statusRef.current === PlayerStatus.PLAYING) {
// Use the current timestamp from player events syncFromCurrentTimestamp();
setCurrentLyric(timestamp * 1000 + delayMsRef.current);
return () => {
// Cleanup: clear the timer when lyrics change or component unmounts
if (lyricTimer.current) clearTimeout(lyricTimer.current);
};
} }
return () => {}; return () => {
}, [lyrics, setCurrentLyric, status, timestamp]); if (lyricTimer.current) {
clearTimeout(lyricTimer.current);
}
};
}, [lyrics, syncFromCurrentTimestamp]);
useEffect(() => { useEffect(() => {
// This handler is used to deal with changes to the current delay. If the offset
// changes, we should immediately stop the current listening set and calculate
// the correct one using the new offset. Afterwards, timing can be calculated like normal
const newOffset = offsetMs ?? 0; const newOffset = offsetMs ?? 0;
const changed = delayMsRef.current !== newOffset; if (delayMsRef.current === newOffset) {
if (!changed) {
return; return;
} }
@@ -242,14 +250,31 @@ export const SynchronizedLyrics = ({
} }
delayMsRef.current = newOffset; delayMsRef.current = newOffset;
syncFromCurrentTimestamp();
// Use the current timestamp from player events }, [offsetMs, syncFromCurrentTimestamp]);
setCurrentLyric(timestamp * 1000 + delayMsRef.current);
}, [setCurrentLyric, offsetMs, timestamp]);
useEffect(() => { useEffect(() => {
// This handler is used specifically for dealing with seeking and progress updates. const unsubscribe = subscribePlayerProgress(({ timestamp }) => {
// When the timestamp changes, update the current lyric position. if (statusRef.current !== PlayerStatus.PLAYING) {
return;
}
if (lyricTimer.current) {
clearTimeout(lyricTimer.current);
}
setCurrentLyric(timestamp * 1000 + delayMsRef.current);
});
return unsubscribe;
}, [setCurrentLyric]);
useEffect(() => {
statusRef.current = usePlayerStoreBase.getState().player.status;
const unsubscribe = subscribePlayerStatus(({ status }) => {
statusRef.current = status;
if (status !== PlayerStatus.PLAYING) { if (status !== PlayerStatus.PLAYING) {
if (lyricTimer.current) { if (lyricTimer.current) {
clearTimeout(lyricTimer.current); clearTimeout(lyricTimer.current);
@@ -262,8 +287,11 @@ export const SynchronizedLyrics = ({
clearTimeout(lyricTimer.current); clearTimeout(lyricTimer.current);
} }
setCurrentLyric(timestamp * 1000 + delayMsRef.current); syncFromCurrentTimestamp();
}, [timestamp, setCurrentLyric, status]); });
return unsubscribe;
}, [syncFromCurrentTimestamp]);
useEffect(() => { useEffect(() => {
// Guaranteed cleanup; stop the timer, and just in case also increment // Guaranteed cleanup; stop the timer, and just in case also increment
@@ -336,6 +364,7 @@ export const SynchronizedLyrics = ({
<div <div
className={clsx(styles.container, 'synchronized-lyrics overlay-scrollbar')} className={clsx(styles.container, 'synchronized-lyrics overlay-scrollbar')}
id="sychronized-lyrics-scroll-container" id="sychronized-lyrics-scroll-container"
onClick={handleContainerClick}
onMouseEnter={showScrollbar} onMouseEnter={showScrollbar}
onMouseLeave={hideScrollbar} onMouseLeave={hideScrollbar}
ref={containerRef} ref={containerRef}
@@ -372,14 +401,10 @@ export const SynchronizedLyrics = ({
<LyricLine <LyricLine
alignment={settings.alignment} alignment={settings.alignment}
className="lyric-line synchronized" className="lyric-line synchronized"
data-lyric-time={time}
fontSize={settings.fontSize} fontSize={settings.fontSize}
id={`lyric-${idx}`} id={`lyric-${idx}`}
key={idx} key={idx}
onClick={() => {
if (time > 0 && Number.isFinite(time)) {
handleSeek(time / 1000);
}
}}
romajiText={romajiLyrics?.[idx]?.[1]} romajiText={romajiLyrics?.[idx]?.[1]}
text={text} text={text}
translatedText={translatedLyrics?.split('\n')[idx]} translatedText={translatedLyrics?.split('\n')[idx]}