started a more proper implementation of the player

This commit is contained in:
Chris Kruining 2025-05-26 16:25:39 +02:00
parent d902f19d35
commit fbc040c317
No known key found for this signature in database
GPG key ID: EB894A3560CCCAD2
6 changed files with 231 additions and 50 deletions

View 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" }
);

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

View 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>
</>
);
};

View file

@ -2,15 +2,21 @@ import { Component, createEffect, createSignal, Show } from "solid-js";
import css from "./volume.module.css"; import css from "./volume.module.css";
import { createStore, unwrap } from "solid-js/store"; import { createStore, unwrap } from "solid-js/store";
import { trackDeep } from "@solid-primitives/deep"; import { trackDeep } from "@solid-primitives/deep";
import { useVideo } from "../context";
interface VolumeProps { interface VolumeProps {
value: number; value: number;
muted?: boolean; muted?: boolean;
onInput?: (next: { volume: number, muted: boolean }) => any; onInput?: (next: { volume: number; muted: boolean }) => any;
} }
export const Volume: Component<VolumeProps> = (props) => { 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(() => { createEffect(() => {
props.onInput?.(unwrap(trackDeep(state))); props.onInput?.(unwrap(trackDeep(state)));
@ -18,8 +24,21 @@ export const Volume: Component<VolumeProps> = (props) => {
return ( return (
<div class={css.container}> <div class={css.container}>
<button onClick={() => setState('muted', m => !m)}><Show when={state.muted} fallback="mute">unmute</Show></button> <button onClick={() => setState("muted", (m) => !m)}>
<input type="range" value={state.volume} min="0" max="1" step="0.01" onInput={(e) => setState('volume', e.target.valueAsNumber)} /> <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> </div>
); );
}; };

View file

@ -3,10 +3,20 @@ import {
createEventSignal, createEventSignal,
} from "@solid-primitives/event-listener"; } from "@solid-primitives/event-listener";
import { createAsync, json, query } from "@solidjs/router"; 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 css from "./player.module.css";
import { Volume } from "./controls/volume"; import { Volume } from "./controls/volume";
import { Entry, getEntry } from "../content"; 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) => { const metadata = query(async (id: string) => {
"use server"; "use server";
@ -41,7 +51,7 @@ interface PlayerProps {
export const Player: Component<PlayerProps> = (props) => { export const Player: Component<PlayerProps> = (props) => {
const [video, setVideo] = createSignal<HTMLVideoElement>( const [video, setVideo] = createSignal<HTMLVideoElement>(
undefined as unknown as HTMLVideoElement, undefined as unknown as HTMLVideoElement
); );
const data = createAsync(() => metadata(props.entry.id), { const data = createAsync(() => metadata(props.entry.id), {
@ -63,12 +73,13 @@ export const Player: Component<PlayerProps> = (props) => {
: ""; : "";
}); });
createEffect(on(thumbnails, (thumbnails) => { createEffect(
// console.log(thumbnails, video()!.textTracks.getTrackById("thumbnails")?.cues); on(thumbnails, (thumbnails) => {
// console.log(thumbnails, video()!.textTracks.getTrackById("thumbnails")?.cues);
// const captions = el.addTextTrack("captions", "English", "en"); // const captions = el.addTextTrack("captions", "English", "en");
// captions. // captions.
})); })
);
const onDurationChange = createEventSignal(video, "durationchange"); const onDurationChange = createEventSignal(video, "durationchange");
const onTimeUpdate = createEventSignal(video, "timeupdate"); const onTimeUpdate = createEventSignal(video, "timeupdate");
@ -99,10 +110,14 @@ export const Player: Component<PlayerProps> = (props) => {
// console.log("ratechange", e); // console.log("ratechange", e);
}, },
seeked(e) { seeked(e) {
// console.log("seeked", e); console.log("seeked", "completed the seek interaction", e);
}, },
seeking(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) { stalled(e) {
// console.log( // console.log(
@ -133,11 +148,11 @@ export const Player: Component<PlayerProps> = (props) => {
}, },
waiting(e) { waiting(e) {
// console.log("waiting", e); console.log("waiting", e);
}, },
progress(e) { progress(e) {
// console.log(e); console.log(e);
}, },
// timeupdate(e) { // timeupdate(e) {
@ -155,47 +170,66 @@ export const Player: Component<PlayerProps> = (props) => {
el[el.paused ? "play" : "pause"](); el[el.paused ? "play" : "pause"]();
}; };
return ( const setTime = (time: number) => {
<figure class={css.player}> const el = video();
<h1>{props.entry?.title}</h1>
<video if (!el) {
ref={setVideo} return;
muted }
autoplay
controls el.currentTime = time;
src={`/api/content/stream?id=${props.id}`} };
lang="en"
> return (
<track <>
default <figure class={css.player}>
kind="captions" <h1>{props.entry?.title}</h1>
label="English"
srclang="en" <video
src={captionUrl()} ref={setVideo}
/> muted
<track default kind="chapters" src={thumbnails()} id="thumbnails" /> autoplay
{/* <track kind="captions" /> 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
default
kind="captions"
label="English"
srclang="en"
src={captionUrl()}
/>
<track default kind="chapters" src={thumbnails()} id="thumbnails" />
{/* <track kind="captions" />
<track kind="chapters" /> <track kind="chapters" />
<track kind="descriptions" /> <track kind="descriptions" />
<track kind="metadata" /> <track kind="metadata" />
<track kind="subtitles" /> */} <track kind="subtitles" /> */}
</video> </video>
<figcaption> <figcaption>
<Volume value={video()?.volume ?? 0} muted={video()?.muted ?? false} onInput={({ volume, muted }) => { <VideoProvider video={video()}>
video().volume = volume; <PlayState />
video().muted = muted; <Volume
}} /> value={video()?.volume ?? 0}
</figcaption> muted={video()?.muted ?? false}
onInput={({ volume, muted }) => {
video().volume = volume;
video().muted = muted;
}}
/>
</VideoProvider>
</figcaption>
<button onclick={toggle}>play/pause</button> <button onclick={toggle}>play/pause</button>
<span> <span>
{formatTime(currentTime())} / {formatTime(duration())} {formatTime(currentTime())} / {formatTime(duration())}
</span> </span>
<progress max={duration().toFixed(0)} value={currentTime().toFixed(0)} /> </figure>
</figure> </>
); );
}; };

View file

@ -7,6 +7,8 @@ import {
RouteDefinition, RouteDefinition,
useParams, useParams,
} from "@solidjs/router"; } from "@solidjs/router";
import { Show } from "solid-js";
import { Details } from "~/components/details";
import { createSlug, getEntry } from "~/features/content"; import { createSlug, getEntry } from "~/features/content";
import { Player } from "~/features/player"; import { Player } from "~/features/player";
@ -52,7 +54,9 @@ export default function Item() {
return ( return (
<> <>
<Player entry={entry} /> <Show when={entry()} fallback="Some kind of pretty 404 page I guess">{
entry => <Player entry={entry()} />
}</Show>
</> </>
); );
} }