From 6dddaea73668c71ec481a07edd0b8c2e14151b28 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 12 Oct 2022 18:24:23 -0500 Subject: [PATCH] Audio: convert to TSX+FC --- app/soapbox/features/audio/index.js | Bin 15957 -> 0 bytes app/soapbox/features/audio/index.tsx | 600 +++++++++++++++++++++++++++ 2 files changed, 600 insertions(+) delete mode 100644 app/soapbox/features/audio/index.js create mode 100644 app/soapbox/features/audio/index.tsx diff --git a/app/soapbox/features/audio/index.js b/app/soapbox/features/audio/index.js deleted file mode 100644 index 98bfc66b505f820681dc5705ab505c5d4bdc4cc9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15957 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 O2tIlr>^;Wj(f void, + backgroundColor?: string, + foregroundColor?: string, + accentColor?: string, + currentTime?: number, + autoPlay?: boolean, + volume?: number, + muted?: boolean, + deployPictureInPicture?: (type: string, opts: Record) => void, +} + +const Audio: React.FC = (props) => { + const { + src, + alt = '', + poster, + accentColor, + backgroundColor, + foregroundColor, + cacheWidth, + fullscreen, + autoPlay, + editable, + deployPictureInPicture = false, + } = props; + + const intl = useIntl(); + + const [width, setWidth] = useState(props.width); + const [height, setHeight] = useState(props.height); + const [currentTime, setCurrentTime] = useState(0); + const [buffer, setBuffer] = useState(0); + const [duration, setDuration] = useState(undefined); + const [paused, setPaused] = useState(true); + const [muted, setMuted] = useState(false); + const [volume, setVolume] = useState(0.5); + const [dragging, setDragging] = useState(false); + const [hovered, setHovered] = useState(false); + + const visualizer = useRef(new Visualizer(TICK_SIZE)); + const audioContext = useRef(null); + + const player = useRef(null); + const audio = useRef(null); + const seek = useRef(null); + const slider = useRef(null); + const canvas = useRef(null); + + const _pack = () => ({ + src: props.src, + volume: audio.current?.volume, + muted: audio.current?.muted, + currentTime: audio.current?.currentTime, + poster: props.poster, + backgroundColor: props.backgroundColor, + foregroundColor: props.foregroundColor, + accentColor: props.accentColor, + }); + + const _setDimensions = () => { + if (player.current) { + const width = player.current.offsetWidth; + const height = fullscreen ? player.current.offsetHeight : (width / (16 / 9)); + + if (cacheWidth) { + cacheWidth(width); + } + + setWidth(width); + setHeight(height); + } + }; + + useEffect(() => { + if (player.current) { + _setDimensions(); + } + }, [player.current]); + + useEffect(() => { + if (audio.current) { + setVolume(audio.current.volume); + setMuted(audio.current.muted); + } + }, [audio.current]); + + useEffect(() => { + if (canvas.current && visualizer.current) { + visualizer.current.setCanvas(canvas.current); + } + }, [canvas.current, visualizer.current]); + + useEffect(() => { + window.addEventListener('scroll', handleScroll); + window.addEventListener('resize', handleResize, { passive: true }); + + return () => { + window.removeEventListener('scroll', handleScroll); + window.removeEventListener('resize', handleResize); + + if (!paused && audio.current && deployPictureInPicture) { + deployPictureInPicture('audio', _pack()); + } + }; + }, []); + + useEffect(() => { + _clear(); + _draw(); + }, [src, width, height, accentColor]); + + const togglePlay = () => { + if (!audioContext.current) { + _initAudioContext(); + } + + if (paused) { + audio.current?.play(); + } else { + audio.current?.pause(); + } + + setPaused(!paused); + }; + + const handleResize = debounce(() => { + if (player.current) { + _setDimensions(); + } + }, 250, { + trailing: true, + }); + + const handlePlay = () => { + setPaused(false); + + if (audioContext.current?.state === 'suspended') { + audioContext.current?.resume(); + } + + _renderCanvas(); + }; + + const handlePause = () => { + setPaused(true); + audioContext.current?.suspend(); + }; + + const handleProgress = () => { + if (audio.current) { + const lastTimeRange = audio.current.buffered.length - 1; + + if (lastTimeRange > -1) { + setBuffer(Math.ceil(audio.current.buffered.end(lastTimeRange) / audio.current.duration * 100)); + } + } + }; + + const toggleMute = () => { + const nextMuted = !muted; + + setMuted(nextMuted); + + if (audio.current) { + audio.current.muted = nextMuted; + } + }; + + const handleVolumeMouseDown: React.MouseEventHandler = e => { + document.addEventListener('mousemove', handleMouseVolSlide, true); + document.addEventListener('mouseup', handleVolumeMouseUp, true); + document.addEventListener('touchmove', handleMouseVolSlide, true); + document.addEventListener('touchend', handleVolumeMouseUp, true); + + handleMouseVolSlide(e); + + e.preventDefault(); + e.stopPropagation(); + }; + + const handleVolumeMouseUp = () => { + document.removeEventListener('mousemove', handleMouseVolSlide, true); + document.removeEventListener('mouseup', handleVolumeMouseUp, true); + document.removeEventListener('touchmove', handleMouseVolSlide, true); + document.removeEventListener('touchend', handleVolumeMouseUp, true); + }; + + const handleMouseDown: React.MouseEventHandler = e => { + document.addEventListener('mousemove', handleMouseMove, true); + document.addEventListener('mouseup', handleMouseUp, true); + document.addEventListener('touchmove', handleMouseMove, true); + document.addEventListener('touchend', handleMouseUp, true); + + setDragging(true); + audio.current?.pause(); + handleMouseMove(e); + + e.preventDefault(); + e.stopPropagation(); + }; + + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseMove, true); + document.removeEventListener('mouseup', handleMouseUp, true); + document.removeEventListener('touchmove', handleMouseMove, true); + document.removeEventListener('touchend', handleMouseUp, true); + + setDragging(false); + audio.current?.play(); + }; + + const handleMouseMove = throttle((e) => { + if (audio.current && seek.current) { + const { x } = getPointerPosition(seek.current, e); + const currentTime = audio.current.duration * x; + + if (!isNaN(currentTime)) { + setCurrentTime(currentTime); + audio.current.currentTime = currentTime; + } + } + }, 15); + + const handleTimeUpdate = () => { + if (audio.current) { + setCurrentTime(audio.current.currentTime); + setDuration(audio.current.duration); + } + }; + + const handleMouseVolSlide = throttle(e => { + if (audio.current && slider.current) { + const { x } = getPointerPosition(slider.current, e); + + if (!isNaN(x)) { + setVolume(x); + audio.current.volume = x; + } + } + }, 15); + + const handleScroll = throttle(() => { + if (!canvas.current || !audio.current) { + return; + } + + const { top, height } = canvas.current.getBoundingClientRect(); + const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0); + + if (!paused && !inView) { + audio.current.pause(); + + if (deployPictureInPicture) { + deployPictureInPicture('audio', _pack()); + } + + setPaused(true); + } + }, 150, { trailing: true }); + + const handleMouseEnter = () => { + setHovered(true); + }; + + const handleMouseLeave = () => { + setHovered(false); + }; + + const handleLoadedData = () => { + if (audio.current) { + setDuration(audio.current.duration); + + if (currentTime) { + audio.current.currentTime = currentTime; + } + + if (volume !== undefined) { + audio.current.volume = volume; + } + + if (muted !== undefined) { + audio.current.muted = muted; + } + + if (autoPlay) { + togglePlay(); + } + } + }; + + const _initAudioContext = () => { + if (audio.current) { + // @ts-ignore + // eslint-disable-next-line compat/compat + const AudioContext = window.AudioContext || window.webkitAudioContext; + const context = new AudioContext(); + const source = context.createMediaElementSource(audio.current); + + visualizer.current.setAudioContext(context, source); + source.connect(context.destination); + + audioContext.current = context; + } + }; + + // const handleDownload = () => { + // fetch(src).then(res => res.blob()).then(blob => { + // const element = document.createElement('a'); + // const objectURL = URL.createObjectURL(blob); + + // element.setAttribute('href', objectURL); + // element.setAttribute('download', fileNameFromURL(src)); + + // document.body.appendChild(element); + // element.click(); + // document.body.removeChild(element); + + // URL.revokeObjectURL(objectURL); + // }).catch(err => { + // console.error(err); + // }); + // }; + + const _renderCanvas = () => { + requestAnimationFrame(() => { + if (!audio.current) return; + + handleTimeUpdate(); + _clear(); + _draw(); + + if (!paused) { + _renderCanvas(); + } + }); + }; + + const _clear = () => { + visualizer.current?.clear(width || 0, height || 0); + }; + + const _draw = () => { + visualizer.current?.draw(_getCX(), _getCY(), _getAccentColor(), _getRadius(), _getScaleCoefficient()); + }; + + const _getRadius = (): number => { + return ((height || props.height || 0) - (PADDING * _getScaleCoefficient()) * 2) / 2; + }; + + const _getScaleCoefficient = (): number => { + return (height || props.height || 0) / 982; + }; + + const _getCX = (): number => { + return Math.floor((width || 0) / 2); + }; + + const _getCY = (): number => { + return Math.floor(_getRadius() + (PADDING * _getScaleCoefficient())); + }; + + const _getAccentColor = (): string => { + return accentColor || '#ffffff'; + }; + + const _getBackgroundColor = (): string => { + return backgroundColor || '#000000'; + }; + + const _getForegroundColor = (): string => { + return foregroundColor || '#ffffff'; + }; + + const seekBy = (time: number) => { + if (audio.current) { + const currentTime = audio.current.currentTime + time; + + if (!isNaN(currentTime)) { + setCurrentTime(currentTime); + audio.current.currentTime = currentTime; + } + } + }; + + const handleAudioKeyDown: React.KeyboardEventHandler = e => { + // On the audio element or the seek bar, we can safely use the space bar + // for playback control because there are no buttons to press + + if (e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + togglePlay(); + } + }; + + const handleKeyDown: React.KeyboardEventHandler = e => { + switch (e.key) { + case 'k': + e.preventDefault(); + e.stopPropagation(); + togglePlay(); + break; + case 'm': + e.preventDefault(); + e.stopPropagation(); + toggleMute(); + break; + case 'j': + e.preventDefault(); + e.stopPropagation(); + seekBy(-10); + break; + case 'l': + e.preventDefault(); + e.stopPropagation(); + seekBy(10); + break; + } + }; + + const getDuration = () => duration || props.duration || 0; + const progress = Math.min((currentTime / getDuration()) * 100, 100); + + return ( +
+