about summary refs log tree commit diff
path: root/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils.tsx
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2024-09-16 21:37:33 +0100
committerGitHub <noreply@github.com>2024-09-16 21:37:33 +0100
commit8241747fc22bb4363ff6cf48d54013cc72db7624 (patch)
treee6cd31d82100fb9c99f3443d7b2753672b55373c /src/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils.tsx
parent38c8f01594ff515fbe49d00a777d70449e804fd4 (diff)
downloadvoidsky-8241747fc22bb4363ff6cf48d54013cc72db7624.tar.zst
[Video] Volume controls on web (#5363)
* split up VideoWebControls

* add basic slider

* logarithmic volume

* integrate mute state

* fix typo

* shared video volume

* rm log

* animate in/out

* disable for touch devices

* remove flicker on touch devices

* more detailed comment

* move into correct context provider

* add minHeight

* hack

* bettern umber

---------

Co-authored-by: Hailey <me@haileyok.com>
Diffstat (limited to 'src/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils.tsx')
-rw-r--r--src/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils.tsx228
1 files changed, 228 insertions, 0 deletions
diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils.tsx
new file mode 100644
index 000000000..aa1b0b8cd
--- /dev/null
+++ b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils.tsx
@@ -0,0 +1,228 @@
+import React, {useCallback, useEffect, useRef, useState} from 'react'
+
+import {useVideoVolumeState} from '../../VideoVolumeContext'
+
+export function useVideoElement(ref: React.RefObject<HTMLVideoElement>) {
+  const [playing, setPlaying] = useState(false)
+  const [muted, setMuted] = useState(true)
+  const [currentTime, setCurrentTime] = useState(0)
+  const [volume, setVolume] = useVideoVolumeState()
+  const [duration, setDuration] = useState(0)
+  const [buffering, setBuffering] = useState(false)
+  const [error, setError] = useState(false)
+  const [canPlay, setCanPlay] = useState(false)
+  const playWhenReadyRef = useRef(false)
+
+  useEffect(() => {
+    if (!ref.current) return
+    ref.current.volume = volume
+  }, [ref, volume])
+
+  useEffect(() => {
+    if (!ref.current) return
+
+    let bufferingTimeout: ReturnType<typeof setTimeout> | undefined
+
+    function round(num: number) {
+      return Math.round(num * 100) / 100
+    }
+
+    // Initial values
+    setCurrentTime(round(ref.current.currentTime) || 0)
+    setDuration(round(ref.current.duration) || 0)
+    setMuted(ref.current.muted)
+    setPlaying(!ref.current.paused)
+    setVolume(ref.current.volume)
+
+    const handleTimeUpdate = () => {
+      if (!ref.current) return
+      setCurrentTime(round(ref.current.currentTime) || 0)
+    }
+
+    const handleDurationChange = () => {
+      if (!ref.current) return
+      setDuration(round(ref.current.duration) || 0)
+    }
+
+    const handlePlay = () => {
+      setPlaying(true)
+    }
+
+    const handlePause = () => {
+      setPlaying(false)
+    }
+
+    const handleVolumeChange = () => {
+      if (!ref.current) return
+      setMuted(ref.current.muted)
+    }
+
+    const handleError = () => {
+      setError(true)
+    }
+
+    const handleCanPlay = () => {
+      if (bufferingTimeout) clearTimeout(bufferingTimeout)
+      setBuffering(false)
+      setCanPlay(true)
+
+      if (!ref.current) return
+      if (playWhenReadyRef.current) {
+        ref.current.play()
+        playWhenReadyRef.current = false
+      }
+    }
+
+    const handleCanPlayThrough = () => {
+      if (bufferingTimeout) clearTimeout(bufferingTimeout)
+      setBuffering(false)
+    }
+
+    const handleWaiting = () => {
+      if (bufferingTimeout) clearTimeout(bufferingTimeout)
+      bufferingTimeout = setTimeout(() => {
+        setBuffering(true)
+      }, 200) // Delay to avoid frequent buffering state changes
+    }
+
+    const handlePlaying = () => {
+      if (bufferingTimeout) clearTimeout(bufferingTimeout)
+      setBuffering(false)
+      setError(false)
+    }
+
+    const handleStalled = () => {
+      if (bufferingTimeout) clearTimeout(bufferingTimeout)
+      bufferingTimeout = setTimeout(() => {
+        setBuffering(true)
+      }, 200) // Delay to avoid frequent buffering state changes
+    }
+
+    const handleEnded = () => {
+      setPlaying(false)
+      setBuffering(false)
+      setError(false)
+    }
+
+    const abortController = new AbortController()
+
+    ref.current.addEventListener('timeupdate', handleTimeUpdate, {
+      signal: abortController.signal,
+    })
+    ref.current.addEventListener('durationchange', handleDurationChange, {
+      signal: abortController.signal,
+    })
+    ref.current.addEventListener('play', handlePlay, {
+      signal: abortController.signal,
+    })
+    ref.current.addEventListener('pause', handlePause, {
+      signal: abortController.signal,
+    })
+    ref.current.addEventListener('volumechange', handleVolumeChange, {
+      signal: abortController.signal,
+    })
+    ref.current.addEventListener('error', handleError, {
+      signal: abortController.signal,
+    })
+    ref.current.addEventListener('canplay', handleCanPlay, {
+      signal: abortController.signal,
+    })
+    ref.current.addEventListener('canplaythrough', handleCanPlayThrough, {
+      signal: abortController.signal,
+    })
+    ref.current.addEventListener('waiting', handleWaiting, {
+      signal: abortController.signal,
+    })
+    ref.current.addEventListener('playing', handlePlaying, {
+      signal: abortController.signal,
+    })
+    ref.current.addEventListener('stalled', handleStalled, {
+      signal: abortController.signal,
+    })
+    ref.current.addEventListener('ended', handleEnded, {
+      signal: abortController.signal,
+    })
+    ref.current.addEventListener('volumechange', handleVolumeChange, {
+      signal: abortController.signal,
+    })
+
+    return () => {
+      abortController.abort()
+      clearTimeout(bufferingTimeout)
+    }
+  }, [ref, setVolume])
+
+  const play = useCallback(() => {
+    if (!ref.current) return
+
+    if (ref.current.ended) {
+      ref.current.currentTime = 0
+    }
+
+    if (ref.current.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) {
+      playWhenReadyRef.current = true
+    } else {
+      const promise = ref.current.play()
+      if (promise !== undefined) {
+        promise.catch(err => {
+          console.error('Error playing video:', err)
+        })
+      }
+    }
+  }, [ref])
+
+  const pause = useCallback(() => {
+    if (!ref.current) return
+
+    ref.current.pause()
+    playWhenReadyRef.current = false
+  }, [ref])
+
+  const togglePlayPause = useCallback(() => {
+    if (!ref.current) return
+
+    if (ref.current.paused) {
+      play()
+    } else {
+      pause()
+    }
+  }, [ref, play, pause])
+
+  const changeMuted = useCallback(
+    (newMuted: boolean | ((prev: boolean) => boolean)) => {
+      if (!ref.current) return
+
+      const value =
+        typeof newMuted === 'function' ? newMuted(ref.current.muted) : newMuted
+      ref.current.muted = value
+    },
+    [ref],
+  )
+
+  return {
+    play,
+    pause,
+    togglePlayPause,
+    duration,
+    currentTime,
+    playing,
+    muted,
+    changeMuted,
+    buffering,
+    error,
+    canPlay,
+  }
+}
+
+export function formatTime(time: number) {
+  if (isNaN(time)) {
+    return '--'
+  }
+
+  time = Math.round(time)
+
+  const minutes = Math.floor(time / 60)
+  const seconds = String(time % 60).padStart(2, '0')
+
+  return `${minutes}:${seconds}`
+}