import * as React from 'react'; import {getFormValues} from '@theoryofnekomata/formxtra'; export interface UseMediaControlsOptions { controllerRef: React.Ref; actionFormKey?: string; visualizationMode?: string; } export const useMediaControls = ({ controllerRef: forwardedRef, actionFormKey = 'action' as const, }: UseMediaControlsOptions) => { const defaultRef = React.useRef(null); const ref = forwardedRef ?? defaultRef; const seekRef = React.useRef(null); const volumeRef = React.useRef(null); const filenameRef = React.useRef(null); const visualizationId = React.useId(); const formId = React.useId(); const [isPlaying, setIsPlaying] = React.useState(false); const [isSeeking, setIsSeeking] = React.useState(false); const [currentTimeDisplay, setCurrentTimeDisplay] = React.useState(); const [seekTimeDisplay, setSeekTimeDisplay] = React.useState(); const [durationDisplay, setDurationDisplay] = React.useState(); const [isSeekTimeCountingDown, setIsSeekTimeCountingDown] = React.useState(false); const refreshControls: React.ReactEventHandler = React.useCallback((e) => { const { currentTarget: mediaController } = e; setCurrentTimeDisplay(mediaController.currentTime); setDurationDisplay(mediaController.duration); }, []); const togglePlayback = React.useCallback(() => { setIsPlaying((p) => !p); }, []); const startSeek: React.MouseEventHandler = React.useCallback(() => { setIsSeeking(true); }, []); const doSetSeek = React.useCallback((thisElement: HTMLInputElement, mediaController: HTMLMediaElement) => { mediaController.currentTime = thisElement.valueAsNumber; }, []); const setSeek: React.ChangeEventHandler = React.useCallback((e) => { if (!(typeof ref === 'object' && ref)) { return; } const { current: mediaController } = ref; if (!mediaController) { return; } const { currentTarget: thisElement } = e; setSeekTimeDisplay(thisElement.valueAsNumber); if (isSeeking) { return; } doSetSeek(thisElement, mediaController); }, [ref, isSeeking, doSetSeek]); const endSeek: React.MouseEventHandler = React.useCallback((e) => { if (!(typeof ref === 'object' && ref)) { return; } const { current: mediaController } = ref; if (!mediaController) { return; } const { currentTarget: thisElement } = e; setIsSeeking(false); setCurrentTimeDisplay(thisElement.valueAsNumber); doSetSeek(thisElement, mediaController); }, [ref, doSetSeek]); const reset: React.ReactEventHandler = React.useCallback((e) => { const videoElement = e.currentTarget; setIsPlaying(false); videoElement.currentTime = 0; }, []); const updateSeekFromPlayback: React.ReactEventHandler = React.useCallback((e) => { if (isSeeking) { return; } const videoElement = e.currentTarget; const currentTime = videoElement.currentTime; setCurrentTimeDisplay(currentTime); if (!(typeof seekRef === 'object' && seekRef)) { return; } const { current: seek } = seekRef; if (!seek) { return; } seek.value = String(currentTime); }, [isSeeking, seekRef]); const toggleSeekTimeCountMode = React.useCallback(() => { setIsSeekTimeCountingDown((b) => !b); }, []); const download = React.useCallback(() => { if (!(typeof ref === 'object' && ref)) { return; } const { current: mediaController } = ref; if (!mediaController) { return; } if (!(typeof filenameRef === 'object' && filenameRef)) { return; } const { current: filename } = filenameRef; if (!filename) { return; } const downloadLink = window.document.createElement('a'); downloadLink.download = filename.textContent ?? 'media'; downloadLink.href = mediaController.currentSrc; downloadLink.addEventListener('click', () => { downloadLink.remove(); }); downloadLink.click(); }, [ref, filenameRef]); const actions = React.useMemo(() => ({ togglePlayback, toggleSeekTimeCountMode, download, }), [togglePlayback, toggleSeekTimeCountMode, download]); const handleAction: React.FormEventHandler = React.useCallback((e) => { e.preventDefault(); const nativeEvent = e.nativeEvent as unknown as { submitter: HTMLElement }; const formData = getFormValues( e.currentTarget, { submitter: nativeEvent.submitter, } ); const actionName = formData[actionFormKey] as keyof typeof actions; const { [actionName]: actionFunction } = actions; actionFunction?.(); }, [actions, actionFormKey]); const adjustVolume: React.ChangeEventHandler = React.useCallback((e) => { if (!(typeof ref === 'object' && ref)) { return; } const { current: mediaController } = ref; if (!mediaController) { return; } const { value } = e.currentTarget; mediaController.volume = Number(value); }, [ref]); React.useEffect(() => { if (!(typeof ref === 'object' && ref)) { return; } const { current: mediaController } = ref; if (!mediaController) { return; } if (isPlaying) { void mediaController.play(); return } mediaController.pause(); }, [isPlaying, ref]); React.useEffect(() => { if (!(typeof seekRef === 'object' && seekRef)) { return; } const { current: seek } = seekRef; if (!seek) { return; } seek.value = String(currentTimeDisplay); }, [currentTimeDisplay, seekRef]); React.useEffect(() => { if (!(typeof ref === 'object' && ref)) { return; } const { current: mediaController } = ref; if (!mediaController) { return; } if (!(typeof volumeRef === 'object' && volumeRef)) { return; } const { current: volume } = volumeRef; if (!volume) { return; } volume.value = String(mediaController.volume); }, [ref, volumeRef]); return React.useMemo(() => ({ seekRef, volumeRef, isPlaying, refreshControls, adjustVolume, reset, startSeek, endSeek, setSeek, updateSeekFromPlayback, durationDisplay, currentTimeDisplay, seekTimeDisplay, isSeeking, isSeekTimeCountingDown, mediaControllerRef: ref, handleAction, filenameRef, visualizationId, formId, }), [ refreshControls, isPlaying, isSeeking, adjustVolume, reset, startSeek, endSeek, setSeek, updateSeekFromPlayback, durationDisplay, currentTimeDisplay, seekTimeDisplay, isSeekTimeCountingDown, ref, handleAction, filenameRef, visualizationId, formId, ]); };