fundementals for video player rewrite

This commit is contained in:
mrjvs 2023-09-30 20:57:00 +02:00
parent 0b4c47bbd4
commit a813efe5ba
10 changed files with 168 additions and 46 deletions

View File

@ -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

View File

@ -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>
);
} }

View File

@ -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();
},
};
}

View File

@ -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;
} }

View File

@ -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);
}, },
}; };

View File

@ -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 />;
} }

View File

@ -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>
); );
} }

View File

@ -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 />;
} }

View File

@ -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;
});
},
}); });

View File

@ -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> {