started a more proper implementation of the player
This commit is contained in:
parent
d902f19d35
commit
fbc040c317
6 changed files with 231 additions and 50 deletions
83
src/features/player/context.tsx
Normal file
83
src/features/player/context.tsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { isServer } from "solid-js/web";
|
||||
import {
|
||||
ContextProviderProps,
|
||||
createContextProvider,
|
||||
} from "@solid-primitives/context";
|
||||
import { Accessor, createMemo } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { createEventListenerMap } from "@solid-primitives/event-listener";
|
||||
|
||||
type State = "playing" | "paused";
|
||||
|
||||
interface Volume {
|
||||
value: number;
|
||||
muted: boolean;
|
||||
}
|
||||
|
||||
export interface VideoAPI {
|
||||
readonly state: Accessor<State>;
|
||||
readonly volume: Accessor<Volume>;
|
||||
|
||||
play(): void;
|
||||
pause(): void;
|
||||
togglePlayState(): void;
|
||||
}
|
||||
|
||||
interface VideoProviderProps extends ContextProviderProps {
|
||||
video: HTMLVideoElement | undefined;
|
||||
}
|
||||
|
||||
interface VideoStore {
|
||||
state: State;
|
||||
volume: Volume;
|
||||
}
|
||||
|
||||
export const [VideoProvider, useVideo] = createContextProvider<
|
||||
VideoAPI,
|
||||
VideoProviderProps
|
||||
>(
|
||||
(props) => {
|
||||
const video = props.video;
|
||||
const [store, setStore] = createStore<VideoStore>({
|
||||
state: "paused",
|
||||
volume: {
|
||||
value: 0.5,
|
||||
muted: false,
|
||||
},
|
||||
});
|
||||
|
||||
const api: VideoAPI = {
|
||||
state: createMemo(() => store.state),
|
||||
|
||||
play() {
|
||||
setStore("state", "playing");
|
||||
},
|
||||
|
||||
pause() {
|
||||
setStore("state", "paused");
|
||||
},
|
||||
|
||||
togglePlayState() {
|
||||
setStore("state", (state) =>
|
||||
state === "playing" ? "paused" : "playing"
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
if (isServer || video === undefined) {
|
||||
return api;
|
||||
}
|
||||
|
||||
createEventListenerMap(video, {
|
||||
play(e) {
|
||||
setStore("state", "playing");
|
||||
},
|
||||
pause(e) {
|
||||
setStore("state", "paused");
|
||||
},
|
||||
});
|
||||
|
||||
return api;
|
||||
},
|
||||
{ state: () => "paused" }
|
||||
);
|
15
src/features/player/controls/playState.tsx
Normal file
15
src/features/player/controls/playState.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Component, createMemo } from "solid-js";
|
||||
import { useVideo } from "../context";
|
||||
|
||||
export const PlayState: Component<{}> = (props) => {
|
||||
const video = useVideo();
|
||||
|
||||
const icon = createMemo(() => {
|
||||
return {
|
||||
playing: "⏵",
|
||||
paused: "⏸",
|
||||
}[video.state()];
|
||||
});
|
||||
|
||||
return <button onclick={(e) => video.togglePlayState()}>{icon()}</button>;
|
||||
};
|
26
src/features/player/controls/seekBar.tsx
Normal file
26
src/features/player/controls/seekBar.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { Component } from "solid-js";
|
||||
|
||||
interface SeekBarProps {
|
||||
video: HTMLVideoElement | undefined;
|
||||
}
|
||||
|
||||
export const SeekBar: Component<SeekBarProps> = () => {
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
list="chapters"
|
||||
type="range"
|
||||
max={duration().toFixed(0)}
|
||||
value={currentTime().toFixed(0)}
|
||||
oninput={(e) => setTime(e.target.valueAsNumber)}
|
||||
step="1"
|
||||
/>
|
||||
|
||||
<datalist id="chapters">
|
||||
<option value="100">Chapter 1</option>
|
||||
<option value="200">Chapter 2</option>
|
||||
<option value="300">Chapter 3</option>
|
||||
</datalist>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -2,15 +2,21 @@ import { Component, createEffect, createSignal, Show } from "solid-js";
|
|||
import css from "./volume.module.css";
|
||||
import { createStore, unwrap } from "solid-js/store";
|
||||
import { trackDeep } from "@solid-primitives/deep";
|
||||
import { useVideo } from "../context";
|
||||
|
||||
interface VolumeProps {
|
||||
value: number;
|
||||
muted?: boolean;
|
||||
onInput?: (next: { volume: number, muted: boolean }) => any;
|
||||
onInput?: (next: { volume: number; muted: boolean }) => any;
|
||||
}
|
||||
|
||||
export const Volume: Component<VolumeProps> = (props) => {
|
||||
const [state, setState] = createStore({ volume: props.value, muted: props.muted ?? false });
|
||||
const video = useVideo();
|
||||
|
||||
const [state, setState] = createStore({
|
||||
volume: props.value,
|
||||
muted: props.muted ?? false,
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
props.onInput?.(unwrap(trackDeep(state)));
|
||||
|
@ -18,8 +24,21 @@ export const Volume: Component<VolumeProps> = (props) => {
|
|||
|
||||
return (
|
||||
<div class={css.container}>
|
||||
<button onClick={() => setState('muted', m => !m)}><Show when={state.muted} fallback="mute">unmute</Show></button>
|
||||
<input type="range" value={state.volume} min="0" max="1" step="0.01" onInput={(e) => setState('volume', e.target.valueAsNumber)} />
|
||||
<button onClick={() => setState("muted", (m) => !m)}>
|
||||
<Show when={state.muted} fallback="mute">
|
||||
unmute
|
||||
</Show>
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
value={state.volume}
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
onInput={(e) =>
|
||||
setState({ muted: false, volume: e.target.valueAsNumber })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,10 +3,20 @@ import {
|
|||
createEventSignal,
|
||||
} from "@solid-primitives/event-listener";
|
||||
import { createAsync, json, query } from "@solidjs/router";
|
||||
import { Component, createEffect, createMemo, createSignal, on } from "solid-js";
|
||||
import {
|
||||
Component,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
on,
|
||||
} from "solid-js";
|
||||
import css from "./player.module.css";
|
||||
import { Volume } from "./controls/volume";
|
||||
import { Entry, getEntry } from "../content";
|
||||
import { PlayState } from "./controls/playState";
|
||||
import { createContextProvider } from "@solid-primitives/context";
|
||||
import { isServer } from "solid-js/web";
|
||||
import { VideoProvider } from "./context";
|
||||
|
||||
const metadata = query(async (id: string) => {
|
||||
"use server";
|
||||
|
@ -41,7 +51,7 @@ interface PlayerProps {
|
|||
|
||||
export const Player: Component<PlayerProps> = (props) => {
|
||||
const [video, setVideo] = createSignal<HTMLVideoElement>(
|
||||
undefined as unknown as HTMLVideoElement,
|
||||
undefined as unknown as HTMLVideoElement
|
||||
);
|
||||
|
||||
const data = createAsync(() => metadata(props.entry.id), {
|
||||
|
@ -63,12 +73,13 @@ export const Player: Component<PlayerProps> = (props) => {
|
|||
: "";
|
||||
});
|
||||
|
||||
createEffect(on(thumbnails, (thumbnails) => {
|
||||
createEffect(
|
||||
on(thumbnails, (thumbnails) => {
|
||||
// console.log(thumbnails, video()!.textTracks.getTrackById("thumbnails")?.cues);
|
||||
|
||||
// const captions = el.addTextTrack("captions", "English", "en");
|
||||
// captions.
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
const onDurationChange = createEventSignal(video, "durationchange");
|
||||
const onTimeUpdate = createEventSignal(video, "timeupdate");
|
||||
|
@ -99,10 +110,14 @@ export const Player: Component<PlayerProps> = (props) => {
|
|||
// console.log("ratechange", e);
|
||||
},
|
||||
seeked(e) {
|
||||
// console.log("seeked", e);
|
||||
console.log("seeked", "completed the seek interaction", e);
|
||||
},
|
||||
seeking(e) {
|
||||
// console.log("seeking", e);
|
||||
console.log(
|
||||
"seeking",
|
||||
"the time on the video has been set, now the content will be loaded, the seeked event will fire when this is done",
|
||||
e
|
||||
);
|
||||
},
|
||||
stalled(e) {
|
||||
// console.log(
|
||||
|
@ -133,11 +148,11 @@ export const Player: Component<PlayerProps> = (props) => {
|
|||
},
|
||||
|
||||
waiting(e) {
|
||||
// console.log("waiting", e);
|
||||
console.log("waiting", e);
|
||||
},
|
||||
|
||||
progress(e) {
|
||||
// console.log(e);
|
||||
console.log(e);
|
||||
},
|
||||
|
||||
// timeupdate(e) {
|
||||
|
@ -155,7 +170,18 @@ export const Player: Component<PlayerProps> = (props) => {
|
|||
el[el.paused ? "play" : "pause"]();
|
||||
};
|
||||
|
||||
const setTime = (time: number) => {
|
||||
const el = video();
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
el.currentTime = time;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<figure class={css.player}>
|
||||
<h1>{props.entry?.title}</h1>
|
||||
|
||||
|
@ -163,8 +189,9 @@ export const Player: Component<PlayerProps> = (props) => {
|
|||
ref={setVideo}
|
||||
muted
|
||||
autoplay
|
||||
controls
|
||||
src={`/api/content/stream?id=${props.id}`}
|
||||
// src="https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4"
|
||||
poster={props.entry?.image}
|
||||
lang="en"
|
||||
>
|
||||
<track
|
||||
|
@ -183,10 +210,17 @@ export const Player: Component<PlayerProps> = (props) => {
|
|||
</video>
|
||||
|
||||
<figcaption>
|
||||
<Volume value={video()?.volume ?? 0} muted={video()?.muted ?? false} onInput={({ volume, muted }) => {
|
||||
<VideoProvider video={video()}>
|
||||
<PlayState />
|
||||
<Volume
|
||||
value={video()?.volume ?? 0}
|
||||
muted={video()?.muted ?? false}
|
||||
onInput={({ volume, muted }) => {
|
||||
video().volume = volume;
|
||||
video().muted = muted;
|
||||
}} />
|
||||
}}
|
||||
/>
|
||||
</VideoProvider>
|
||||
</figcaption>
|
||||
|
||||
<button onclick={toggle}>play/pause</button>
|
||||
|
@ -194,8 +228,8 @@ export const Player: Component<PlayerProps> = (props) => {
|
|||
<span>
|
||||
{formatTime(currentTime())} / {formatTime(duration())}
|
||||
</span>
|
||||
<progress max={duration().toFixed(0)} value={currentTime().toFixed(0)} />
|
||||
</figure>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ import {
|
|||
RouteDefinition,
|
||||
useParams,
|
||||
} from "@solidjs/router";
|
||||
import { Show } from "solid-js";
|
||||
import { Details } from "~/components/details";
|
||||
import { createSlug, getEntry } from "~/features/content";
|
||||
import { Player } from "~/features/player";
|
||||
|
||||
|
@ -52,7 +54,9 @@ export default function Item() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Player entry={entry} />
|
||||
<Show when={entry()} fallback="Some kind of pretty 404 page I guess">{
|
||||
entry => <Player entry={entry()} />
|
||||
}</Show>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue