mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +01:00
buffering
This commit is contained in:
parent
218a14d5f6
commit
61abce9386
8 changed files with 197 additions and 13 deletions
|
@ -9,7 +9,7 @@ const VideoPlayerInternals = forwardRef<HTMLVideoElement>((_, ref) => {
|
|||
const video = useContext(VideoPlayerContext);
|
||||
|
||||
return (
|
||||
<video ref={ref} className="h-full w-full">
|
||||
<video ref={ref} preload="auto" playsInline className="h-full w-full">
|
||||
{video.source ? <source src={video.source} type="video/mp4" /> : null}
|
||||
</video>
|
||||
);
|
||||
|
|
|
@ -9,7 +9,7 @@ export function PauseControl() {
|
|||
else videoState.play();
|
||||
}, [videoState]);
|
||||
|
||||
let text =
|
||||
const text =
|
||||
videoState.isPlaying || videoState.isSeeking ? "playing" : "paused";
|
||||
|
||||
return (
|
||||
|
|
47
src/components/video/controls/ProgressControl.tsx
Normal file
47
src/components/video/controls/ProgressControl.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { useCallback, useRef } from "react";
|
||||
import { useVideoPlayerState } from "../VideoContext";
|
||||
|
||||
export function ProgressControl() {
|
||||
const { videoState } = useVideoPlayerState();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const watchProgress = `${(
|
||||
(videoState.time / videoState.duration) *
|
||||
100
|
||||
).toFixed(2)}%`;
|
||||
const bufferProgress = `${(
|
||||
(videoState.buffered / videoState.duration) *
|
||||
100
|
||||
).toFixed(2)}%`;
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLElement>) => {
|
||||
if (!ref.current) return;
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const pos = (e.pageX - rect.left) / ref.current.offsetWidth;
|
||||
videoState.setTime(pos * videoState.duration);
|
||||
},
|
||||
[videoState, ref]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="relative m-1 my-4 h-4 w-48 overflow-hidden rounded-full border border-white bg-denim-100"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 bg-denim-700 opacity-50"
|
||||
style={{
|
||||
width: bufferProgress,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 bg-denim-700"
|
||||
style={{
|
||||
width: watchProgress,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
34
src/components/video/controls/VolumeControl.tsx
Normal file
34
src/components/video/controls/VolumeControl.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { useCallback, useRef } from "react";
|
||||
import { useVideoPlayerState } from "../VideoContext";
|
||||
|
||||
export function VolumeControl() {
|
||||
const { videoState } = useVideoPlayerState();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const percentage = `${(videoState.volume * 100).toFixed(2)}%`;
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLElement>) => {
|
||||
if (!ref.current) return;
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const pos = (e.pageX - rect.left) / ref.current.offsetWidth;
|
||||
videoState.setVolume(pos);
|
||||
},
|
||||
[videoState, ref]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="relative m-1 my-4 h-4 w-48 overflow-hidden rounded-full border border-white bg-bink-300"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 bg-bink-700"
|
||||
style={{
|
||||
width: percentage,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -3,6 +3,8 @@ export interface PlayerControls {
|
|||
pause(): void;
|
||||
exitFullscreen(): void;
|
||||
enterFullscreen(): void;
|
||||
setTime(time: number): void;
|
||||
setVolume(volume: number): void;
|
||||
}
|
||||
|
||||
export const initialControls: PlayerControls = {
|
||||
|
@ -10,6 +12,8 @@ export const initialControls: PlayerControls = {
|
|||
pause: () => null,
|
||||
enterFullscreen: () => null,
|
||||
exitFullscreen: () => null,
|
||||
setTime: () => null,
|
||||
setVolume: () => null,
|
||||
};
|
||||
|
||||
export function populateControls(
|
||||
|
@ -31,5 +35,19 @@ export function populateControls(
|
|||
if (!document.fullscreenElement) return;
|
||||
document.exitFullscreen();
|
||||
},
|
||||
setTime(t) {
|
||||
// clamp time between 0 and max duration
|
||||
let time = Math.min(t, player.duration);
|
||||
time = Math.max(0, time);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
player.currentTime = time;
|
||||
},
|
||||
setVolume(v) {
|
||||
// clamp time between 0 and 1
|
||||
let volume = Math.min(v, 1);
|
||||
volume = Math.max(0, volume);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
player.volume = volume;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,12 +4,17 @@ import {
|
|||
PlayerControls,
|
||||
populateControls,
|
||||
} from "./controlVideo";
|
||||
import { handleBuffered } from "./utils";
|
||||
|
||||
export type PlayerState = {
|
||||
isPlaying: boolean;
|
||||
isPaused: boolean;
|
||||
isSeeking: boolean;
|
||||
isFullscreen: boolean;
|
||||
time: number;
|
||||
duration: number;
|
||||
volume: number;
|
||||
buffered: number;
|
||||
} & PlayerControls;
|
||||
|
||||
export const initialPlayerState: PlayerState = {
|
||||
|
@ -17,6 +22,10 @@ export const initialPlayerState: PlayerState = {
|
|||
isPaused: true,
|
||||
isFullscreen: false,
|
||||
isSeeking: false,
|
||||
time: 0,
|
||||
duration: 0,
|
||||
volume: 0,
|
||||
buffered: 0,
|
||||
...initialControls,
|
||||
};
|
||||
|
||||
|
@ -30,26 +39,77 @@ function readState(player: HTMLVideoElement, update: SetPlayer) {
|
|||
state.isPlaying = !player.paused;
|
||||
state.isFullscreen = !!document.fullscreenElement;
|
||||
state.isSeeking = player.seeking;
|
||||
state.time = player.currentTime;
|
||||
state.duration = player.duration;
|
||||
state.volume = player.volume;
|
||||
state.buffered = handleBuffered(player.currentTime, player.buffered);
|
||||
|
||||
update(state);
|
||||
}
|
||||
|
||||
function registerListeners(player: HTMLVideoElement, update: SetPlayer) {
|
||||
player.addEventListener("pause", () => {
|
||||
const pause = () => {
|
||||
update((s) => ({ ...s, isPaused: true, isPlaying: false }));
|
||||
});
|
||||
player.addEventListener("play", () => {
|
||||
};
|
||||
const play = () => {
|
||||
update((s) => ({ ...s, isPaused: false, isPlaying: true }));
|
||||
});
|
||||
player.addEventListener("seeking", () => {
|
||||
};
|
||||
const seeking = () => {
|
||||
update((s) => ({ ...s, isSeeking: true }));
|
||||
});
|
||||
player.addEventListener("seeked", () => {
|
||||
};
|
||||
const seeked = () => {
|
||||
update((s) => ({ ...s, isSeeking: false }));
|
||||
});
|
||||
document.addEventListener("fullscreenchange", () => {
|
||||
};
|
||||
const fullscreenchange = () => {
|
||||
update((s) => ({ ...s, isFullscreen: !!document.fullscreenElement }));
|
||||
});
|
||||
};
|
||||
const timeupdate = () => {
|
||||
update((s) => ({
|
||||
...s,
|
||||
duration: player.duration,
|
||||
time: player.currentTime,
|
||||
}));
|
||||
};
|
||||
const loadedmetadata = () => {
|
||||
update((s) => ({
|
||||
...s,
|
||||
duration: player.duration,
|
||||
}));
|
||||
};
|
||||
const volumechange = () => {
|
||||
update((s) => ({
|
||||
...s,
|
||||
volume: player.volume,
|
||||
}));
|
||||
};
|
||||
const progress = () => {
|
||||
update((s) => ({
|
||||
...s,
|
||||
buffered: handleBuffered(player.currentTime, player.buffered),
|
||||
}));
|
||||
};
|
||||
|
||||
player.addEventListener("pause", pause);
|
||||
player.addEventListener("play", play);
|
||||
player.addEventListener("seeking", seeking);
|
||||
player.addEventListener("seeked", seeked);
|
||||
document.addEventListener("fullscreenchange", fullscreenchange);
|
||||
player.addEventListener("timeupdate", timeupdate);
|
||||
player.addEventListener("loadedmetadata", loadedmetadata);
|
||||
player.addEventListener("volumechange", volumechange);
|
||||
player.addEventListener("progress", progress);
|
||||
|
||||
return () => {
|
||||
player.removeEventListener("pause", pause);
|
||||
player.removeEventListener("play", play);
|
||||
player.removeEventListener("seeking", seeking);
|
||||
player.removeEventListener("seeked", seeked);
|
||||
document.removeEventListener("fullscreenchange", fullscreenchange);
|
||||
player.removeEventListener("timeupdate", timeupdate);
|
||||
player.removeEventListener("loadedmetadata", loadedmetadata);
|
||||
player.removeEventListener("volumechange", volumechange);
|
||||
player.removeEventListener("progress", progress);
|
||||
};
|
||||
}
|
||||
|
||||
export function useVideoPlayer(
|
||||
|
|
8
src/components/video/hooks/utils.ts
Normal file
8
src/components/video/hooks/utils.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export function handleBuffered(time: number, buffered: TimeRanges): number {
|
||||
for (let i = 0; i < buffered.length; i += 1) {
|
||||
if (buffered.start(buffered.length - 1 - i) < time) {
|
||||
return buffered.end(buffered.length - 1 - i);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
|
@ -1,17 +1,34 @@
|
|||
import { FullscreenControl } from "@/components/video/controls/FullscreenControl";
|
||||
import { PauseControl } from "@/components/video/controls/PauseControl";
|
||||
import { ProgressControl } from "@/components/video/controls/ProgressControl";
|
||||
import { SourceControl } from "@/components/video/controls/SourceControl";
|
||||
import { VolumeControl } from "@/components/video/controls/VolumeControl";
|
||||
import { VideoPlayer } from "@/components/video/VideoPlayer";
|
||||
|
||||
// test videos: https://gist.github.com/jsturgis/3b19447b304616f18657
|
||||
|
||||
// TODO video todos:
|
||||
// - captions
|
||||
// - make pretty
|
||||
// - show fullscreen button depending on is available (document.fullscreenEnabled)
|
||||
// - better seeking
|
||||
// - improve seekables
|
||||
// - buffering
|
||||
// - error handling
|
||||
// - auto-play prop option
|
||||
// - middle pause button
|
||||
// - improve pausing while seeking/buffering
|
||||
// - captions
|
||||
// - show formatted time
|
||||
export function TestView() {
|
||||
return (
|
||||
<div className="w-[40rem] max-w-full">
|
||||
<VideoPlayer>
|
||||
<PauseControl />
|
||||
<FullscreenControl />
|
||||
<SourceControl source="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" />
|
||||
<ProgressControl />
|
||||
<VolumeControl />
|
||||
<SourceControl source="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4" />
|
||||
</VideoPlayer>
|
||||
</div>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue