mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
fix(playback): harden external-subtitle handling from PR #1471 review
Addresses the confirmed code-review findings: - Deselect the hls.js engine track BEFORE activating an external subtitle track: hls.js reacts to subtitleTrack=-1 by disabling every subtitle-kind TextTrack, which undid the just-selected external track. - Track ownership of every TextTrack the session ever created (addTextTrack tracks cannot leave the element), so stale or attach-failed tracks stay excluded from the native enumeration instead of reappearing as ghost engine tracks after source changes; failed attaches are silenced and their partial cues removed. - Guard the file pick with a source generation so a pick that outlives a stream change (Up Next, zapping, failover) is discarded instead of attaching the previous stream's subtitles to the next one. - Decode picked files encoding-aware (UTF-16 BOMs, strict UTF-8, then a Windows-1251/1252 heuristic) instead of Blob.text()'s silent UTF-8 substitution that rendered legacy-encoded SRT files as mojibake. - Gate the delay row on an external track being SELECTED, not merely loaded, so it can no longer sit enabled while visually inert. - Keep real (possibly negative) cue times under a negative delay instead of clamping pre-roll cues into a simultaneous stack at t=0. - Fix subtitleDelayLabel returning a signed negative zero for sub-tenth values. - Hoist the canonical PlayerSubtitleStyle shape plus clamp/normalize rules into @iptvnator/shared/interfaces (subtitle-style.util.ts); the renderer and the Electron main process now share one implementation, removing the triplicated literals and the toLowerCase divergence. Adds regression coverage for each fix; updates the player-controls contract doc and CLAUDE.md accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
404deb92b0
commit
10bd5acd6f
16 files changed
+481
-119
No files matched your search
@@ -5,6 +5,7 @@ export * from './lib/content-metadata.interface';
|
||||
export * from './lib/dev-logger.util';
|
||||
export * from './lib/download-metadata.interface';
|
||||
export * from './lib/embedded-mpv-session.interface';
|
||||
export * from './lib/subtitle-style.util';
|
||||
export * from './lib/electron-api.interface';
|
||||
export * from './lib/epg-channel-metadata.model';
|
||||
export * from './lib/epg-channel-with-programs.interface';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { PlayerSubtitleStyle } from './subtitle-style.util';
|
||||
|
||||
export type EmbeddedMpvSessionStatus =
|
||||
| 'idle'
|
||||
| 'loading'
|
||||
@@ -29,14 +31,12 @@ export interface EmbeddedMpvCapabilities {
|
||||
}
|
||||
|
||||
/**
|
||||
* Engine-neutral subtitle presentation preferences forwarded to mpv.
|
||||
* `sizePercent` maps to `sub-scale` (100 = 1.0); `color` maps to `sub-color`,
|
||||
* null restores mpv's default.
|
||||
* Subtitle presentation preferences forwarded to mpv: `sizePercent` maps to
|
||||
* `sub-scale` (100 = 1.0); `color` maps to `sub-color`, null restores mpv's
|
||||
* default. Alias of the canonical shared shape so the renderer controls and
|
||||
* the IPC contract cannot drift structurally.
|
||||
*/
|
||||
export interface EmbeddedMpvSubtitleStyle {
|
||||
sizePercent: number;
|
||||
color: string | null;
|
||||
}
|
||||
export type EmbeddedMpvSubtitleStyle = PlayerSubtitleStyle;
|
||||
|
||||
export type EmbeddedMpvEngine = 'native' | 'frame-copy';
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Canonical subtitle presentation contract shared by the renderer controls
|
||||
* (`libs/ui/playback`) and the Electron main process. The clamp/normalize
|
||||
* rules live HERE so the two sides cannot drift: the renderer applies them to
|
||||
* user input, and the main process re-applies the exact same rules to
|
||||
* untrusted IPC payloads (deliberate defense-in-depth, same implementation).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Engine-neutral subtitle presentation preferences. `sizePercent` is relative
|
||||
* to the engine's default rendering size (100 = default); `color` is a
|
||||
* lowercase CSS hex color, or null for the engine default.
|
||||
*/
|
||||
export interface PlayerSubtitleStyle {
|
||||
sizePercent: number;
|
||||
color: string | null;
|
||||
}
|
||||
|
||||
export const DEFAULT_SUBTITLE_STYLE: PlayerSubtitleStyle = {
|
||||
sizePercent: 100,
|
||||
color: null,
|
||||
};
|
||||
|
||||
export const SUBTITLE_DELAY_LIMIT_SECONDS = 60;
|
||||
export const SUBTITLE_SIZE_MIN_PERCENT = 25;
|
||||
export const SUBTITLE_SIZE_MAX_PERCENT = 400;
|
||||
|
||||
const HEX_COLOR_PATTERN = /^#[0-9a-f]{6}$/i;
|
||||
|
||||
export function clampSubtitleDelay(seconds: number): number {
|
||||
if (!Number.isFinite(seconds)) {
|
||||
return 0;
|
||||
}
|
||||
const clamped = Math.max(
|
||||
-SUBTITLE_DELAY_LIMIT_SECONDS,
|
||||
Math.min(SUBTITLE_DELAY_LIMIT_SECONDS, seconds)
|
||||
);
|
||||
// Avoid float drift from repeated ±0.5 steps ("0.30000000000000004").
|
||||
return Math.round(clamped * 1000) / 1000;
|
||||
}
|
||||
|
||||
export function normalizeSubtitleStyle(value: unknown): PlayerSubtitleStyle {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return { ...DEFAULT_SUBTITLE_STYLE };
|
||||
}
|
||||
const candidate = value as Partial<PlayerSubtitleStyle>;
|
||||
const sizePercent =
|
||||
typeof candidate.sizePercent === 'number' &&
|
||||
Number.isFinite(candidate.sizePercent)
|
||||
? Math.max(
|
||||
SUBTITLE_SIZE_MIN_PERCENT,
|
||||
Math.min(
|
||||
SUBTITLE_SIZE_MAX_PERCENT,
|
||||
Math.round(candidate.sizePercent)
|
||||
)
|
||||
)
|
||||
: DEFAULT_SUBTITLE_STYLE.sizePercent;
|
||||
const color =
|
||||
typeof candidate.color === 'string' &&
|
||||
HEX_COLOR_PATTERN.test(candidate.color)
|
||||
? candidate.color.toLowerCase()
|
||||
: null;
|
||||
return { sizePercent, color };
|
||||
}
|
||||
|
||||
export function isDefaultSubtitleStyle(style: PlayerSubtitleStyle): boolean {
|
||||
return (
|
||||
style.sizePercent === DEFAULT_SUBTITLE_STYLE.sizePercent &&
|
||||
style.color === DEFAULT_SUBTITLE_STYLE.color
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user