diff --git a/static/player/main.ts b/static/player/main.ts index 927df95..b8b3b78 100644 --- a/static/player/main.ts +++ b/static/player/main.ts @@ -66,9 +66,12 @@ const updatePreviewUI = (ratio: number): void => { const initPlayer = (): void => { const container = document.querySelector('[data-video-player]') as HTMLElement | null; if (!container || initialized) return; - initialized = true; - initState(container); + if (!initState(container)) { + console.error('Video player markup is missing required controls.'); + return; + } + initialized = true; const loading = container.querySelector('[data-loading]') as HTMLElement | null; const progressWrap = container.querySelector('[data-progress-wrap]') as HTMLElement | null; diff --git a/static/player/state.ts b/static/player/state.ts index 83694e4..a495077 100644 --- a/static/player/state.ts +++ b/static/player/state.ts @@ -42,14 +42,14 @@ export interface PlayerState { videoOverlay: HTMLElement | null; } -export const state: PlayerState = { - container: null as unknown as HTMLElement, - video: null as unknown as HTMLVideoElement, - progress: null as unknown as HTMLElement, - scrubber: null as unknown as HTMLElement, - buffered: null as unknown as HTMLElement, - timeDisplay: null as unknown as HTMLElement, - durationDisplay: null as unknown as HTMLElement, +const createInitialState = (): PlayerState => ({ + container: document.createElement('div'), + video: document.createElement('video'), + progress: document.createElement('div'), + scrubber: document.createElement('div'), + buffered: document.createElement('div'), + timeDisplay: document.createElement('div'), + durationDisplay: document.createElement('div'), modeSources: {}, availableModes: [], currentMode: 'dub', @@ -81,21 +81,69 @@ export const state: PlayerState = { previewPopover: null, previewTime: null, videoOverlay: null, +}); + +export const state: PlayerState = createInitialState(); + +interface RequiredPlayerElements { + video: HTMLVideoElement; + progress: HTMLElement; + scrubber: HTMLElement; + buffered: HTMLElement; + timeDisplay: HTMLElement; + durationDisplay: HTMLElement; +} + +const findElement = ( + container: HTMLElement, + selector: string, + elementType: new () => T +): T | null => { + const element = container.querySelector(selector); + if (element instanceof elementType) return element; + return null; +}; + +const requiredPlayerElements = (container: HTMLElement): RequiredPlayerElements | null => { + const elements = { + video: findElement(container, 'video', HTMLVideoElement), + progress: findElement(container, '[data-progress]', HTMLElement), + scrubber: findElement(container, '[data-scrubber]', HTMLElement), + buffered: findElement(container, '[data-buffered]', HTMLElement), + timeDisplay: findElement(container, '[data-time]', HTMLElement), + durationDisplay: findElement(container, '[data-duration]', HTMLElement), + }; + + if ( + !elements.video || + !elements.progress || + !elements.scrubber || + !elements.buffered || + !elements.timeDisplay || + !elements.durationDisplay + ) { + return null; + } + + return elements; }; /** * Initializes player state from DOM data attributes. * Called once on page load or htmx swap. */ -export const initState = (c: HTMLElement): void => { +export const initState = (c: HTMLElement): boolean => { + const elements = requiredPlayerElements(c); + if (!elements) return false; + // core elements state.container = c; - state.video = q(c, 'video')!; - state.progress = q(c, '[data-progress]'); - state.scrubber = q(c, '[data-scrubber]'); - state.buffered = q(c, '[data-buffered]'); - state.timeDisplay = q(c, '[data-time]'); - state.durationDisplay = q(c, '[data-duration]'); + state.video = elements.video; + state.progress = elements.progress; + state.scrubber = elements.scrubber; + state.buffered = elements.buffered; + state.timeDisplay = elements.timeDisplay; + state.durationDisplay = elements.durationDisplay; state.previewPopover = q(c, '[data-preview-popover]'); state.previewTime = q(c, '[data-preview-time]'); state.videoOverlay = q(c, '[data-video-overlay]'); @@ -143,4 +191,6 @@ export const initState = (c: HTMLElement): void => { state.parsedSegments = segments .map(s => ({ ...s, start: Number(s.start) || 0, end: Number(s.end) || 0 })) .filter(s => s.end > s.start); + + return true; };