import { useCallback, useEffect, useRef, useState, } from '../lib/teact/teact'; import { getActions, getGlobal } from '../global'; import { PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION } from '../config'; import type { Track, TrackId } from '../util/audioPlayer'; import { register } from '../util/audioPlayer'; import { isSafariPatchInProgress } from '../util/patchSafariProgressiveAudio'; import type { MediaSessionHandlers } from '../util/mediaSession'; import { registerMediaSession, setPlaybackState, setPositionState, updateMetadata, } from '../util/mediaSession'; import useEffectWithPrevDeps from './useEffectWithPrevDeps'; import useOnChange from './useOnChange'; type Handler = (e: Event) => void; const DEFAULT_SKIP_TIME = 10; const useAudioPlayer = ( trackId: TrackId, originalDuration: number, // Sometimes incorrect for voice messages trackType: Track['type'], src?: string, handlers?: Record, metadata?: MediaMetadata, onInit?: (element: HTMLAudioElement) => void, shouldPlay = false, onForcePlay?: NoneToVoidFunction, onTrackChange?: NoneToVoidFunction, noPlaylist = false, noProgressUpdates = false, ) => { // eslint-disable-next-line no-null/no-null const controllerRef = useRef>(null); const [isPlaying, setIsPlaying] = useState(false); let isPlayingSync = isPlaying; const [playProgress, setPlayProgress] = useState(0); const handleTrackChange = useCallback(() => { setIsPlaying(false); if (onTrackChange) onTrackChange(); }, [onTrackChange]); useOnChange(() => { controllerRef.current = register(trackId, trackType, (eventName, e) => { switch (eventName) { case 'onPlay': { const { setVolume, setPlaybackRate, toggleMuted, proxy, } = controllerRef.current!; setIsPlaying(true); registerMediaSession(metadata, makeMediaHandlers(controllerRef)); setPlaybackState('playing'); setVolume(getGlobal().audioPlayer.volume); toggleMuted(Boolean(getGlobal().audioPlayer.isMuted)); const duration = proxy.duration && Number.isFinite(proxy.duration) ? proxy.duration : originalDuration; if (trackType === 'voice' || duration > PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION) { setPlaybackRate(getGlobal().audioPlayer.playbackRate); } setPositionState({ duration: proxy.duration || 0, playbackRate: proxy.playbackRate, position: proxy.currentTime, }); break; } case 'onRateChange': { const { proxy } = controllerRef.current!; setPositionState({ duration: proxy.duration || 0, playbackRate: proxy.playbackRate, position: proxy.currentTime, }); break; } case 'onPause': setIsPlaying(false); setPlaybackState('paused'); break; case 'onTimeUpdate': { const { proxy } = controllerRef.current!; const duration = proxy.duration && Number.isFinite(proxy.duration) ? proxy.duration : originalDuration; if (!noProgressUpdates) setPlayProgress(proxy.currentTime / duration); break; } case 'onEnded': { setPlaybackState('paused'); break; } } handlers?.[eventName]?.(e); }, onForcePlay, handleTrackChange); const { proxy } = controllerRef.current!; if (!isPlaying && !proxy.paused) { setIsPlaying(true); isPlayingSync = true; } if (onInit) { onInit(proxy); } }, [trackId]); useEffect(() => { if (isPlaying && metadata) updateMetadata(metadata); }, [metadata, isPlaying]); const { play, pause, setCurrentTime, proxy, destroy, setVolume, stop, isFirst, isLast, requestNextTrack, requestPreviousTrack, setPlaybackRate, toggleMuted, } = controllerRef.current!; const duration = proxy.duration && Number.isFinite(proxy.duration) ? proxy.duration : originalDuration; // RAF progress useEffect(() => { if (duration && !isSafariPatchInProgress(proxy) && !noProgressUpdates) { setPlayProgress(proxy.currentTime / duration); } }, [duration, playProgress, proxy, noProgressUpdates]); // Cleanup useEffect(() => () => { destroy(noPlaylist); }, [destroy, noPlaylist]); // Autoplay once `src` is present useEffectWithPrevDeps(([prevShouldPlay, prevSrc]) => { if (prevShouldPlay === shouldPlay && src === prevSrc) { return; } // When paused by another player if (proxy.src && proxy.paused) { return; } if (shouldPlay && src && !isPlaying) { play(src); } }, [shouldPlay, src, isPlaying, play, proxy.src, proxy.paused]); const playIfPresent = useCallback(() => { if (src) { play(src); } }, [src, play]); const playPause = useCallback(() => { if (isPlaying) { pause(); } else { playIfPresent(); } }, [pause, playIfPresent, isPlaying]); const setTime = useCallback((time: number) => { setCurrentTime(time); if (duration) { setPlayProgress(proxy.currentTime / duration); } }, [duration, proxy, setCurrentTime]); return { isPlaying: isPlayingSync, playProgress, playPause, pause, play: playIfPresent, stop, setCurrentTime: setTime, setVolume, audioProxy: proxy, duration, requestNextTrack, requestPreviousTrack, isFirst, isLast, setPlaybackRate, toggleMuted, }; }; function makeMediaHandlers(controllerRef: React.RefObject>) { let mediaHandlers: MediaSessionHandlers = {}; if (controllerRef && controllerRef.current) { const { play, pause, setCurrentTime, proxy, requestNextTrack, requestPreviousTrack, isFirst, isLast, } = controllerRef.current; mediaHandlers = { play: () => { play(proxy.src); }, pause: () => { pause(); }, stop: () => { pause(); setCurrentTime(0); getActions().closeAudioPlayer(); }, seekbackward: (event) => { const skipTime = event.seekOffset || DEFAULT_SKIP_TIME; setCurrentTime(Math.max(proxy.currentTime - skipTime, 0)); }, seekforward: (event) => { const skipTime = event.seekOffset || DEFAULT_SKIP_TIME; setCurrentTime(Math.min(proxy.currentTime + skipTime, proxy.duration)); }, seekTo: (event) => { if (event.seekTime) { setCurrentTime(event.seekTime); } }, }; if (!isLast()) { mediaHandlers.nexttrack = () => { requestNextTrack(); }; } if (!isFirst()) { mediaHandlers.previoustrack = () => { requestPreviousTrack(); }; } } return mediaHandlers; } export default useAudioPlayer;