diff --git a/src/components/video/controls/LoadingControl.tsx b/src/components/video/controls/LoadingControl.tsx new file mode 100644 index 00000000..489976b1 --- /dev/null +++ b/src/components/video/controls/LoadingControl.tsx @@ -0,0 +1,9 @@ +import { useVideoPlayerState } from "../VideoContext"; + +export function LoadingControl() { + const { videoState } = useVideoPlayerState(); + + if (!videoState.isLoading) return null; + + return

Loading...

; +} diff --git a/src/components/video/controls/TimeControl.tsx b/src/components/video/controls/TimeControl.tsx new file mode 100644 index 00000000..3cf151ea --- /dev/null +++ b/src/components/video/controls/TimeControl.tsx @@ -0,0 +1,36 @@ +import { useVideoPlayerState } from "../VideoContext"; + +function durationExceedsHour(secs: number): boolean { + return secs > 60 * 60; +} + +function formatSeconds(secs: number, showHours = false): string { + let time = secs; + const seconds = time % 60; + + time /= 60; + const minutes = time % 60; + + time /= 60; + const hours = minutes % 60; + + const minuteString = `${Math.round(minutes) + .toString() + .padStart(2)}:${Math.round(seconds).toString().padStart(2, "0")}`; + + if (!showHours) return minuteString; + return `${Math.round(hours).toString()}:${minuteString}`; +} + +export function TimeControl() { + const { videoState } = useVideoPlayerState(); + const hasHours = durationExceedsHour(videoState.duration); + const time = formatSeconds(videoState.time, hasHours); + const duration = formatSeconds(videoState.duration, hasHours); + + return ( +

+ {time} / {duration} +

+ ); +} diff --git a/src/components/video/hooks/useVideoPlayer.ts b/src/components/video/hooks/useVideoPlayer.ts index 9682b7bd..d3d2d95b 100644 --- a/src/components/video/hooks/useVideoPlayer.ts +++ b/src/components/video/hooks/useVideoPlayer.ts @@ -10,6 +10,7 @@ export type PlayerState = { isPlaying: boolean; isPaused: boolean; isSeeking: boolean; + isLoading: boolean; isFullscreen: boolean; time: number; duration: number; @@ -21,6 +22,7 @@ export const initialPlayerState: PlayerState = { isPlaying: false, isPaused: true, isFullscreen: false, + isLoading: false, isSeeking: false, time: 0, duration: 0, @@ -43,16 +45,26 @@ function readState(player: HTMLVideoElement, update: SetPlayer) { state.duration = player.duration; state.volume = player.volume; state.buffered = handleBuffered(player.currentTime, player.buffered); + state.isLoading = false; update(state); } function registerListeners(player: HTMLVideoElement, update: SetPlayer) { const pause = () => { - update((s) => ({ ...s, isPaused: true, isPlaying: false })); + update((s) => ({ + ...s, + isPaused: true, + isPlaying: false, + })); }; - const play = () => { - update((s) => ({ ...s, isPaused: false, isPlaying: true })); + const playing = () => { + update((s) => ({ + ...s, + isPaused: false, + isPlaying: true, + isLoading: false, + })); }; const seeking = () => { update((s) => ({ ...s, isSeeking: true })); @@ -60,6 +72,9 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { const seeked = () => { update((s) => ({ ...s, isSeeking: false })); }; + const waiting = () => { + update((s) => ({ ...s, isLoading: true })); + }; const fullscreenchange = () => { update((s) => ({ ...s, isFullscreen: !!document.fullscreenElement })); }; @@ -90,7 +105,7 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { }; player.addEventListener("pause", pause); - player.addEventListener("play", play); + player.addEventListener("playing", playing); player.addEventListener("seeking", seeking); player.addEventListener("seeked", seeked); document.addEventListener("fullscreenchange", fullscreenchange); @@ -98,10 +113,11 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { player.addEventListener("loadedmetadata", loadedmetadata); player.addEventListener("volumechange", volumechange); player.addEventListener("progress", progress); + player.addEventListener("waiting", waiting); return () => { player.removeEventListener("pause", pause); - player.removeEventListener("play", play); + player.removeEventListener("playing", playing); player.removeEventListener("seeking", seeking); player.removeEventListener("seeked", seeked); document.removeEventListener("fullscreenchange", fullscreenchange); @@ -109,6 +125,7 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { player.removeEventListener("loadedmetadata", loadedmetadata); player.removeEventListener("volumechange", volumechange); player.removeEventListener("progress", progress); + player.removeEventListener("waiting", waiting); }; } diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx index d2d8d387..bc0292a5 100644 --- a/src/views/TestView.tsx +++ b/src/views/TestView.tsx @@ -1,7 +1,9 @@ import { FullscreenControl } from "@/components/video/controls/FullscreenControl"; +import { LoadingControl } from "@/components/video/controls/LoadingControl"; import { PauseControl } from "@/components/video/controls/PauseControl"; import { ProgressControl } from "@/components/video/controls/ProgressControl"; import { SourceControl } from "@/components/video/controls/SourceControl"; +import { TimeControl } from "@/components/video/controls/TimeControl"; import { VolumeControl } from "@/components/video/controls/VolumeControl"; import { VideoPlayer } from "@/components/video/VideoPlayer"; import { useCallback, useState } from "react"; @@ -9,16 +11,13 @@ import { useCallback, useState } from "react"; // test videos: https://gist.github.com/jsturgis/3b19447b304616f18657 // TODO video todos: -// - captions // - make pretty // - better seeking // - improve seekables -// - buffering // - error handling // - middle pause button + click to pause // - improve pausing while seeking/buffering // - captions -// - show formatted time // - IOS support: (no volume, fullscreen video element instead of wrapper) // - IpadOS support: (fullscreen video wrapper should work, see (lookmovie.io) ) // - HLS support: feature detection otherwise use HLS.js @@ -39,6 +38,8 @@ export function TestView() { + +