mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +01:00
fundementals for video player rewrite
This commit is contained in:
parent
0b4c47bbd4
commit
a813efe5ba
10 changed files with 168 additions and 46 deletions
|
@ -9,8 +9,23 @@ These parts can be used to build any shape of a video player.
|
||||||
|
|
||||||
# internal parts
|
# internal parts
|
||||||
These parts are internally used, they aren't exported. Do not use them outside of player internals.
|
These parts are internally used, they aren't exported. Do not use them outside of player internals.
|
||||||
- `/display` - display interface, abstraction on how to actually play the content (e.g Video element, HLS player, Standard video element, etc)
|
|
||||||
- `/internals` - Internal components that are always rendered on every player.
|
### `/display`
|
||||||
- `/utils` - miscellaneous logic
|
The display interface, abstraction on how to actually play the content (e.g Video element, chrome casting, etc)
|
||||||
- `/hooks` - hooks only used for video player
|
- It must be completely seperate from any react code
|
||||||
- `~/src/stores/player` - state for the video player. Should only be used by internal parts
|
- It must not interact with state, pass async data back with events
|
||||||
|
|
||||||
|
### `/internals`
|
||||||
|
Internal components that are always rendered on every player.
|
||||||
|
- Only components that are always present on the player instance, they must never unmount
|
||||||
|
|
||||||
|
### `/utils`
|
||||||
|
miscellaneous logic, put anything that is unique to the video player internals.
|
||||||
|
|
||||||
|
### `/hooks`
|
||||||
|
Hooks only used for video player.
|
||||||
|
- only exception is usePlayer, as its used outside of the player to control the player
|
||||||
|
|
||||||
|
### `~/src/stores/player`
|
||||||
|
State for the video player.
|
||||||
|
- Only parts related to the video player may utilize the state
|
||||||
|
|
|
@ -1,3 +1,17 @@
|
||||||
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
||||||
export function Pause() {
|
export function Pause() {
|
||||||
return <button type="button" />;
|
const display = usePlayerStore((s) => s.display);
|
||||||
|
const { isPaused } = usePlayerStore((s) => s.mediaPlaying);
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
if (isPaused) display?.play();
|
||||||
|
else display?.pause();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button type="button" onClick={toggle}>
|
||||||
|
play/pause
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,45 @@
|
||||||
export {};
|
import {
|
||||||
|
DisplayInterface,
|
||||||
|
DisplayInterfaceEvents,
|
||||||
|
} from "@/components/player/display/displayInterface";
|
||||||
|
import { Source } from "@/components/player/hooks/usePlayer";
|
||||||
|
import { makeEmitter } from "@/utils/events";
|
||||||
|
|
||||||
|
export function makeVideoElementDisplayInterface(): DisplayInterface {
|
||||||
|
const { emit, on, off } = makeEmitter<DisplayInterfaceEvents>();
|
||||||
|
let source: Source | null = null;
|
||||||
|
let videoElement: HTMLVideoElement | null = null;
|
||||||
|
|
||||||
|
function setSource() {
|
||||||
|
if (!videoElement || !source) return;
|
||||||
|
videoElement.src = source.url;
|
||||||
|
videoElement.addEventListener("play", () => emit("play", undefined));
|
||||||
|
videoElement.addEventListener("pause", () => emit("pause", undefined));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
on,
|
||||||
|
off,
|
||||||
|
|
||||||
|
// no need to destroy anything
|
||||||
|
destroy: () => {},
|
||||||
|
|
||||||
|
load(newSource) {
|
||||||
|
source = newSource;
|
||||||
|
setSource();
|
||||||
|
},
|
||||||
|
|
||||||
|
processVideoElement(video) {
|
||||||
|
videoElement = video;
|
||||||
|
setSource();
|
||||||
|
},
|
||||||
|
|
||||||
|
pause() {
|
||||||
|
videoElement?.pause();
|
||||||
|
},
|
||||||
|
|
||||||
|
play() {
|
||||||
|
videoElement?.play();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -10,4 +10,6 @@ export interface DisplayInterface extends Listener<DisplayInterfaceEvents> {
|
||||||
play(): void;
|
play(): void;
|
||||||
pause(): void;
|
pause(): void;
|
||||||
load(source: Source): void;
|
load(source: Source): void;
|
||||||
|
processVideoElement(video: HTMLVideoElement): void;
|
||||||
|
destroy(): void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,13 @@ export interface Source {
|
||||||
|
|
||||||
export function usePlayer() {
|
export function usePlayer() {
|
||||||
const setStatus = usePlayerStore((s) => s.setStatus);
|
const setStatus = usePlayerStore((s) => s.setStatus);
|
||||||
const setSource = usePlayerStore((s) => s.setSource);
|
const status = usePlayerStore((s) => s.status);
|
||||||
|
const display = usePlayerStore((s) => s.display);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
status,
|
||||||
playMedia(source: Source) {
|
playMedia(source: Source) {
|
||||||
setSource(source.url, source.type);
|
display?.load(source);
|
||||||
setStatus(playerStatus.PLAYING);
|
setStatus(playerStatus.PLAYING);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,36 +1,46 @@
|
||||||
import { RefObject, useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
import { MWStreamType } from "@/backend/helpers/streams";
|
import { makeVideoElementDisplayInterface } from "@/components/player/display/base";
|
||||||
import { SourceSliceSource } from "@/stores/player/slices/source";
|
import { playerStatus } from "@/stores/player/slices/source";
|
||||||
import { AllSlices } from "@/stores/player/slices/types";
|
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
||||||
// should this video container show right now?
|
// initialize display interface
|
||||||
function useShouldShow(source: SourceSliceSource | null): boolean {
|
function useDisplayInterface() {
|
||||||
if (!source) return false;
|
const display = usePlayerStore((s) => s.display);
|
||||||
if (source.type !== MWStreamType.MP4) return false;
|
const setDisplay = usePlayerStore((s) => s.setDisplay);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!display) {
|
||||||
|
setDisplay(makeVideoElementDisplayInterface());
|
||||||
|
}
|
||||||
|
}, [display, setDisplay]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useShouldShowVideoElement() {
|
||||||
|
const status = usePlayerStore((s) => s.status);
|
||||||
|
|
||||||
|
if (status !== playerStatus.PLAYING) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// make video element up to par with the state
|
function VideoElement() {
|
||||||
function useRestoreVideo(
|
const videoEl = useRef<HTMLVideoElement>(null);
|
||||||
videoRef: RefObject<HTMLVideoElement>,
|
const display = usePlayerStore((s) => s.display);
|
||||||
player: AllSlices
|
|
||||||
) {
|
// report video element to display interface
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = videoRef.current;
|
if (display && videoEl.current) {
|
||||||
const src = player.source?.url ?? "";
|
display.processVideoElement(videoEl.current);
|
||||||
if (!el) return;
|
}
|
||||||
if (el.src !== src) el.src = src;
|
}, [display, videoEl]);
|
||||||
}, [player.source?.url, videoRef]);
|
|
||||||
|
return <video autoPlay ref={videoEl} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VideoContainer() {
|
export function VideoContainer() {
|
||||||
const videoEl = useRef<HTMLVideoElement>(null);
|
const show = useShouldShowVideoElement();
|
||||||
const player = usePlayerStore();
|
useDisplayInterface();
|
||||||
useRestoreVideo(videoEl, player);
|
|
||||||
const show = useShouldShow(player.source);
|
|
||||||
|
|
||||||
if (!show) return null;
|
if (!show) return null;
|
||||||
return <video autoPlay ref={videoEl} />;
|
return <VideoElement />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,30 @@
|
||||||
|
import { MWStreamType } from "@/backend/helpers/streams";
|
||||||
import { Player } from "@/components/player";
|
import { Player } from "@/components/player";
|
||||||
|
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
||||||
|
import { playerStatus } from "@/stores/player/slices/source";
|
||||||
|
|
||||||
export function PlayerView() {
|
export function PlayerView() {
|
||||||
|
const { status, playMedia } = usePlayer();
|
||||||
|
|
||||||
|
function scrape() {
|
||||||
|
playMedia({
|
||||||
|
type: MWStreamType.MP4,
|
||||||
|
url: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Player.Container>
|
<Player.Container>
|
||||||
<Player.Pause />
|
<Player.Pause />
|
||||||
|
|
||||||
|
{status === playerStatus.IDLE ? (
|
||||||
|
<div>
|
||||||
|
<p>Its now scraping</p>
|
||||||
|
<button type="button" onClick={scrape}>
|
||||||
|
Finish scraping
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</Player.Container>
|
</Player.Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,5 @@
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
import { MWStreamType } from "@/backend/helpers/streams";
|
|
||||||
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
|
||||||
import { PlayerView } from "@/pages/PlayerView";
|
import { PlayerView } from "@/pages/PlayerView";
|
||||||
|
|
||||||
export default function VideoTesterView() {
|
export default function VideoTesterView() {
|
||||||
const player = usePlayer();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
player.playMedia({
|
|
||||||
type: MWStreamType.MP4,
|
|
||||||
url: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return <PlayerView />;
|
return <PlayerView />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { MWStreamType } from "@/backend/helpers/streams";
|
import { MWStreamType } from "@/backend/helpers/streams";
|
||||||
|
import { DisplayInterface } from "@/components/player/display/displayInterface";
|
||||||
import { MakeSlice } from "@/stores/player/slices/types";
|
import { MakeSlice } from "@/stores/player/slices/types";
|
||||||
import { ValuesOf } from "@/utils/typeguard";
|
import { ValuesOf } from "@/utils/typeguard";
|
||||||
|
|
||||||
|
@ -18,13 +19,16 @@ export interface SourceSliceSource {
|
||||||
export interface SourceSlice {
|
export interface SourceSlice {
|
||||||
status: PlayerStatus;
|
status: PlayerStatus;
|
||||||
source: SourceSliceSource | null;
|
source: SourceSliceSource | null;
|
||||||
|
display: DisplayInterface | null;
|
||||||
setStatus(status: PlayerStatus): void;
|
setStatus(status: PlayerStatus): void;
|
||||||
setSource(url: string, type: MWStreamType): void;
|
setSource(url: string, type: MWStreamType): void;
|
||||||
|
setDisplay(display: DisplayInterface): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createSourceSlice: MakeSlice<SourceSlice> = (set) => ({
|
export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
|
||||||
source: null,
|
source: null,
|
||||||
status: playerStatus.IDLE,
|
status: playerStatus.IDLE,
|
||||||
|
display: null,
|
||||||
setStatus(status: PlayerStatus) {
|
setStatus(status: PlayerStatus) {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.status = status;
|
s.status = status;
|
||||||
|
@ -38,4 +42,26 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set) => ({
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setDisplay(newDisplay: DisplayInterface) {
|
||||||
|
const display = get().display;
|
||||||
|
if (display) display.destroy();
|
||||||
|
|
||||||
|
// make display events update the state
|
||||||
|
newDisplay.on("pause", () =>
|
||||||
|
set((s) => {
|
||||||
|
s.mediaPlaying.isPaused = true;
|
||||||
|
s.mediaPlaying.isPlaying = false;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
newDisplay.on("play", () =>
|
||||||
|
set((s) => {
|
||||||
|
s.mediaPlaying.isPaused = false;
|
||||||
|
s.mediaPlaying.isPlaying = true;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
set((s) => {
|
||||||
|
s.display = newDisplay;
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,6 +10,7 @@ export interface Emitter<T extends EventMap> {
|
||||||
|
|
||||||
export interface Listener<T extends EventMap> {
|
export interface Listener<T extends EventMap> {
|
||||||
on<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>): void;
|
on<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>): void;
|
||||||
|
off<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeEmitter<T extends EventMap>(): Emitter<T> {
|
export function makeEmitter<T extends EventMap>(): Emitter<T> {
|
||||||
|
|
Loading…
Reference in a new issue