import type { SubtitleData } from '../../types'; import { resolveAutoplayReadyMaxReleaseAttempts } from './startup-autoplay-release-policy'; type MpvClientLike = { connected?: boolean; requestProperty: (property: string) => Promise; send: (payload: { command: Array }) => void; }; export type AutoplayReadyGateDeps = { isAppOwnedFlowInFlight: () => boolean; getCurrentMediaPath: () => string | null; getCurrentVideoPath: () => string | null; getPlaybackPaused: () => boolean | null; getMpvClient: () => MpvClientLike | null; signalPluginAutoplayReady: () => void; schedule: (callback: () => void, delayMs: number) => ReturnType; logDebug: (message: string) => void; }; export function createAutoplayReadyGate(deps: AutoplayReadyGateDeps) { let autoPlayReadySignalMediaPath: string | null = null; let autoPlayReadySignalGeneration = 0; const invalidatePendingAutoplayReadyFallbacks = (): void => { autoPlayReadySignalMediaPath = null; autoPlayReadySignalGeneration += 1; }; const maybeSignalPluginAutoplayReady = ( payload: SubtitleData, options?: { forceWhilePaused?: boolean }, ): void => { if (deps.isAppOwnedFlowInFlight()) { deps.logDebug('[autoplay-ready] suppressed while app-owned YouTube flow is active'); return; } if (!payload.text.trim()) { return; } const mediaPath = deps.getCurrentMediaPath()?.trim() || deps.getCurrentVideoPath()?.trim() || '__unknown__'; const duplicateMediaSignal = autoPlayReadySignalMediaPath === mediaPath; const releaseRetryDelayMs = 200; const maxReleaseAttempts = resolveAutoplayReadyMaxReleaseAttempts({ forceWhilePaused: options?.forceWhilePaused === true, retryDelayMs: releaseRetryDelayMs, }); const isPlaybackPaused = async (client: MpvClientLike): Promise => { try { const pauseProperty = await client.requestProperty('pause'); if (typeof pauseProperty === 'boolean') { return pauseProperty; } if (typeof pauseProperty === 'string') { return pauseProperty.toLowerCase() !== 'no' && pauseProperty !== '0'; } if (typeof pauseProperty === 'number') { return pauseProperty !== 0; } } catch (error) { deps.logDebug( `[autoplay-ready] failed to read pause property for media ${mediaPath}: ${ error instanceof Error ? error.message : String(error) }`, ); } return true; }; const attemptRelease = (playbackGeneration: number, attempt: number): void => { void (async () => { if ( autoPlayReadySignalMediaPath !== mediaPath || playbackGeneration !== autoPlayReadySignalGeneration ) { return; } const mpvClient = deps.getMpvClient(); if (!mpvClient?.connected) { if (attempt < maxReleaseAttempts) { deps.schedule(() => attemptRelease(playbackGeneration, attempt + 1), releaseRetryDelayMs); } return; } const shouldUnpause = await isPlaybackPaused(mpvClient); if (!shouldUnpause) { return; } mpvClient.send({ command: ['set_property', 'pause', false] }); if (attempt < maxReleaseAttempts) { deps.schedule(() => attemptRelease(playbackGeneration, attempt + 1), releaseRetryDelayMs); } })(); }; if (duplicateMediaSignal) { return; } autoPlayReadySignalMediaPath = mediaPath; const playbackGeneration = ++autoPlayReadySignalGeneration; deps.signalPluginAutoplayReady(); attemptRelease(playbackGeneration, 0); }; return { getAutoPlayReadySignalMediaPath: (): string | null => autoPlayReadySignalMediaPath, invalidatePendingAutoplayReadyFallbacks, maybeSignalPluginAutoplayReady, }; }