mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-27 08:21:51 +01:00
fundementals for video player rewrite
This commit is contained in:
parent
0b4c47bbd4
commit
a813efe5ba
@ -9,8 +9,23 @@ These parts can be used to build any shape of a video player.
|
||||
|
||||
# internal parts
|
||||
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.
|
||||
- `/utils` - miscellaneous logic
|
||||
- `/hooks` - hooks only used for video player
|
||||
- `~/src/stores/player` - state for the video player. Should only be used by internal parts
|
||||
|
||||
### `/display`
|
||||
The display interface, abstraction on how to actually play the content (e.g Video element, chrome casting, etc)
|
||||
- It must be completely seperate from any react code
|
||||
- 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() {
|
||||
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;
|
||||
pause(): void;
|
||||
load(source: Source): void;
|
||||
processVideoElement(video: HTMLVideoElement): void;
|
||||
destroy(): void;
|
||||
}
|
||||
|
@ -9,11 +9,13 @@ export interface Source {
|
||||
|
||||
export function usePlayer() {
|
||||
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 {
|
||||
status,
|
||||
playMedia(source: Source) {
|
||||
setSource(source.url, source.type);
|
||||
display?.load(source);
|
||||
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 { SourceSliceSource } from "@/stores/player/slices/source";
|
||||
import { AllSlices } from "@/stores/player/slices/types";
|
||||
import { makeVideoElementDisplayInterface } from "@/components/player/display/base";
|
||||
import { playerStatus } from "@/stores/player/slices/source";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
|
||||
// should this video container show right now?
|
||||
function useShouldShow(source: SourceSliceSource | null): boolean {
|
||||
if (!source) return false;
|
||||
if (source.type !== MWStreamType.MP4) return false;
|
||||
// initialize display interface
|
||||
function useDisplayInterface() {
|
||||
const display = usePlayerStore((s) => s.display);
|
||||
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;
|
||||
}
|
||||
|
||||
// make video element up to par with the state
|
||||
function useRestoreVideo(
|
||||
videoRef: RefObject<HTMLVideoElement>,
|
||||
player: AllSlices
|
||||
) {
|
||||
function VideoElement() {
|
||||
const videoEl = useRef<HTMLVideoElement>(null);
|
||||
const display = usePlayerStore((s) => s.display);
|
||||
|
||||
// report video element to display interface
|
||||
useEffect(() => {
|
||||
const el = videoRef.current;
|
||||
const src = player.source?.url ?? "";
|
||||
if (!el) return;
|
||||
if (el.src !== src) el.src = src;
|
||||
}, [player.source?.url, videoRef]);
|
||||
if (display && videoEl.current) {
|
||||
display.processVideoElement(videoEl.current);
|
||||
}
|
||||
}, [display, videoEl]);
|
||||
|
||||
return <video autoPlay ref={videoEl} />;
|
||||
}
|
||||
|
||||
export function VideoContainer() {
|
||||
const videoEl = useRef<HTMLVideoElement>(null);
|
||||
const player = usePlayerStore();
|
||||
useRestoreVideo(videoEl, player);
|
||||
const show = useShouldShow(player.source);
|
||||
const show = useShouldShowVideoElement();
|
||||
useDisplayInterface();
|
||||
|
||||
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 { usePlayer } from "@/components/player/hooks/usePlayer";
|
||||
import { playerStatus } from "@/stores/player/slices/source";
|
||||
|
||||
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 (
|
||||
<Player.Container>
|
||||
<Player.Pause />
|
||||
|
||||
{status === playerStatus.IDLE ? (
|
||||
<div>
|
||||
<p>Its now scraping</p>
|
||||
<button type="button" onClick={scrape}>
|
||||
Finish scraping
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</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";
|
||||
|
||||
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 />;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { MWStreamType } from "@/backend/helpers/streams";
|
||||
import { DisplayInterface } from "@/components/player/display/displayInterface";
|
||||
import { MakeSlice } from "@/stores/player/slices/types";
|
||||
import { ValuesOf } from "@/utils/typeguard";
|
||||
|
||||
@ -18,13 +19,16 @@ export interface SourceSliceSource {
|
||||
export interface SourceSlice {
|
||||
status: PlayerStatus;
|
||||
source: SourceSliceSource | null;
|
||||
display: DisplayInterface | null;
|
||||
setStatus(status: PlayerStatus): 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,
|
||||
status: playerStatus.IDLE,
|
||||
display: null,
|
||||
setStatus(status: PlayerStatus) {
|
||||
set((s) => {
|
||||
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> {
|
||||
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> {
|
||||
|
Loading…
Reference in New Issue
Block a user