fix(overlay): prevent secondary subtitle duplication

- Reject decimal subtitle track IDs
- Preserve repeated short dialogue lines
This commit is contained in:
2026-08-17 23:49:08 -07:00
parent f335b26fe3
commit fc7fde30d5
4 changed files with 19 additions and 19 deletions
+12
View File
@@ -181,6 +181,18 @@ test('dispatchMpvProtocolMessage emits subtitle track changes', async () => {
]);
});
test('dispatchMpvProtocolMessage rejects decimal subtitle track IDs', async () => {
const { deps, state } = createDeps();
await dispatchMpvProtocolMessage({ event: 'property-change', name: 'sid', data: '4.5' }, deps);
await dispatchMpvProtocolMessage(
{ event: 'property-change', name: 'secondary-sid', data: '4.5' },
deps,
);
assert.deepEqual(state.events, [{ sid: null }, { sid: null }]);
});
test('dispatchMpvProtocolMessage enforces sub-visibility hidden when overlay suppression is enabled', async () => {
const { deps, state } = createDeps({
isVisibleOverlayVisible: () => true,
+2 -2
View File
@@ -283,7 +283,7 @@ export async function dispatchMpvProtocolMessage(
: typeof msg.data === 'string'
? Number(msg.data)
: null;
deps.emitSubtitleTrackChange({ sid: sid !== null && Number.isFinite(sid) ? sid : null });
deps.emitSubtitleTrackChange({ sid: sid !== null && Number.isInteger(sid) ? sid : null });
} else if (msg.name === 'secondary-sid') {
const sid =
typeof msg.data === 'number'
@@ -292,7 +292,7 @@ export async function dispatchMpvProtocolMessage(
? Number(msg.data)
: null;
deps.emitSecondarySubtitleTrackChange({
sid: sid !== null && Number.isFinite(sid) ? sid : null,
sid: sid !== null && Number.isInteger(sid) ? sid : null,
});
} else if (msg.name === 'secondary-sub-delay') {
const delay =
+3 -3
View File
@@ -1434,10 +1434,10 @@ test('prepareSecondarySubtitleLines collapses karaoke syllable spam into one ded
assert.deepEqual(prepareSecondarySubtitleLines(karaoke), ['ya This no ma ups']);
});
test('prepareSecondarySubtitleLines collapses a short stack of repeated ASS layers', () => {
const layeredSign = ['Your', 'Your', 'Your', 'Your', 'mosaic'].join('\\N');
test('prepareSecondarySubtitleLines preserves repeated short dialogue without layer metadata', () => {
const dialogue = ['Wait', 'Wait', 'Wait'];
assert.deepEqual(prepareSecondarySubtitleLines(layeredSign), ['Your mosaic']);
assert.deepEqual(prepareSecondarySubtitleLines(dialogue.join('\\N')), dialogue);
});
test('prepareSecondarySubtitleLines preserves short simultaneous dialogue without repeats', () => {
+2 -14
View File
@@ -657,24 +657,12 @@ function renderPlainTextPreserveLineBreaks(root: ParentNode, text: string): void
// hover-pause band into a full-screen trap.
const KARAOKE_MIN_LINE_COUNT = 8;
const KARAOKE_MAX_MEDIAN_LINE_LENGTH = 4;
const LAYERED_REPEAT_MIN_LINE_COUNT = 4;
const LAYERED_REPEAT_MIN_COUNT = 3;
function isKaraokeLikeLineSet(lines: string[]): boolean {
if (lines.length < KARAOKE_MIN_LINE_COUNT) return false;
const lengths = lines.map((line) => line.length).sort((a, b) => a - b);
const median = lengths[Math.floor(lengths.length / 2)] ?? 0;
if (median > KARAOKE_MAX_MEDIAN_LINE_LENGTH) return false;
if (lines.length >= KARAOKE_MIN_LINE_COUNT) return true;
if (lines.length < LAYERED_REPEAT_MIN_LINE_COUNT) return false;
const repeatCounts = new Map<string, number>();
for (const line of lines) {
const count = (repeatCounts.get(line) ?? 0) + 1;
if (count >= LAYERED_REPEAT_MIN_COUNT) return true;
repeatCounts.set(line, count);
}
return false;
return median <= KARAOKE_MAX_MEDIAN_LINE_LENGTH;
}
export function prepareSecondarySubtitleLines(text: string): string[] {