mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-29 16:07:40 +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);
|
const video = useContext(VideoPlayerContext);
|
||||||
|
|
||||||
return (
|
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.source ? <source src={video.source} type="video/mp4" /> : null}
|
||||||
</video>
|
</video>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,7 +9,7 @@ export function PauseControl() {
|
||||||
else videoState.play();
|
else videoState.play();
|
||||||
}, [videoState]);
|
}, [videoState]);
|
||||||
|
|
||||||
let text =
|
const text =
|
||||||
videoState.isPlaying || videoState.isSeeking ? "playing" : "paused";
|
videoState.isPlaying || videoState.isSeeking ? "playing" : "paused";
|
||||||
|
|
||||||
return (
|
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;
|
pause(): void;
|
||||||
exitFullscreen(): void;
|
exitFullscreen(): void;
|
||||||
enterFullscreen(): void;
|
enterFullscreen(): void;
|
||||||
|
setTime(time: number): void;
|
||||||
|
setVolume(volume: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initialControls: PlayerControls = {
|
export const initialControls: PlayerControls = {
|
||||||
|
@ -10,6 +12,8 @@ export const initialControls: PlayerControls = {
|
||||||
pause: () => null,
|
pause: () => null,
|
||||||
enterFullscreen: () => null,
|
enterFullscreen: () => null,
|
||||||
exitFullscreen: () => null,
|
exitFullscreen: () => null,
|
||||||
|
setTime: () => null,
|
||||||
|
setVolume: () => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function populateControls(
|
export function populateControls(
|
||||||
|
@ -31,5 +35,19 @@ export function populateControls(
|
||||||
if (!document.fullscreenElement) return;
|
if (!document.fullscreenElement) return;
|
||||||
document.exitFullscreen();
|
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,
|
PlayerControls,
|
||||||
populateControls,
|
populateControls,
|
||||||
} from "./controlVideo";
|
} from "./controlVideo";
|
||||||
|
import { handleBuffered } from "./utils";
|
||||||
|
|
||||||
export type PlayerState = {
|
export type PlayerState = {
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
isPaused: boolean;
|
isPaused: boolean;
|
||||||
isSeeking: boolean;
|
isSeeking: boolean;
|
||||||
isFullscreen: boolean;
|
isFullscreen: boolean;
|
||||||
|
time: number;
|
||||||
|
duration: number;
|
||||||
|
volume: number;
|
||||||
|
buffered: number;
|
||||||
} & PlayerControls;
|
} & PlayerControls;
|
||||||
|
|
||||||
export const initialPlayerState: PlayerState = {
|
export const initialPlayerState: PlayerState = {
|
||||||
|
@ -17,6 +22,10 @@ export const initialPlayerState: PlayerState = {
|
||||||
isPaused: true,
|
isPaused: true,
|
||||||
isFullscreen: false,
|
isFullscreen: false,
|
||||||
isSeeking: false,
|
isSeeking: false,
|
||||||
|
time: 0,
|
||||||
|
duration: 0,
|
||||||
|
volume: 0,
|
||||||
|
buffered: 0,
|
||||||
...initialControls,
|
...initialControls,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -30,26 +39,77 @@ function readState(player: HTMLVideoElement, update: SetPlayer) {
|
||||||
state.isPlaying = !player.paused;
|
state.isPlaying = !player.paused;
|
||||||
state.isFullscreen = !!document.fullscreenElement;
|
state.isFullscreen = !!document.fullscreenElement;
|
||||||
state.isSeeking = player.seeking;
|
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);
|
update(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerListeners(player: HTMLVideoElement, update: SetPlayer) {
|
function registerListeners(player: HTMLVideoElement, update: SetPlayer) {
|
||||||
player.addEventListener("pause", () => {
|
const pause = () => {
|
||||||
update((s) => ({ ...s, isPaused: true, isPlaying: false }));
|
update((s) => ({ ...s, isPaused: true, isPlaying: false }));
|
||||||
});
|
};
|
||||||
player.addEventListener("play", () => {
|
const play = () => {
|
||||||
update((s) => ({ ...s, isPaused: false, isPlaying: true }));
|
update((s) => ({ ...s, isPaused: false, isPlaying: true }));
|
||||||
});
|
};
|
||||||
player.addEventListener("seeking", () => {
|
const seeking = () => {
|
||||||
update((s) => ({ ...s, isSeeking: true }));
|
update((s) => ({ ...s, isSeeking: true }));
|
||||||
});
|
};
|
||||||
player.addEventListener("seeked", () => {
|
const seeked = () => {
|
||||||
update((s) => ({ ...s, isSeeking: false }));
|
update((s) => ({ ...s, isSeeking: false }));
|
||||||
});
|
};
|
||||||
document.addEventListener("fullscreenchange", () => {
|
const fullscreenchange = () => {
|
||||||
update((s) => ({ ...s, isFullscreen: !!document.fullscreenElement }));
|
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(
|
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 { FullscreenControl } from "@/components/video/controls/FullscreenControl";
|
||||||
import { PauseControl } from "@/components/video/controls/PauseControl";
|
import { PauseControl } from "@/components/video/controls/PauseControl";
|
||||||
|
import { ProgressControl } from "@/components/video/controls/ProgressControl";
|
||||||
import { SourceControl } from "@/components/video/controls/SourceControl";
|
import { SourceControl } from "@/components/video/controls/SourceControl";
|
||||||
|
import { VolumeControl } from "@/components/video/controls/VolumeControl";
|
||||||
import { VideoPlayer } from "@/components/video/VideoPlayer";
|
import { VideoPlayer } from "@/components/video/VideoPlayer";
|
||||||
|
|
||||||
// test videos: https://gist.github.com/jsturgis/3b19447b304616f18657
|
// 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() {
|
export function TestView() {
|
||||||
return (
|
return (
|
||||||
<div className="w-[40rem] max-w-full">
|
<div className="w-[40rem] max-w-full">
|
||||||
<VideoPlayer>
|
<VideoPlayer>
|
||||||
<PauseControl />
|
<PauseControl />
|
||||||
<FullscreenControl />
|
<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>
|
</VideoPlayer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue