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

View file

@ -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) => {
// console.log(thumbnails, video()!.textTracks.getTrackById("thumbnails")?.cues);
// const captions = el.addTextTrack("captions", "English", "en");
// captions.
}));
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,47 +170,66 @@ export const Player: Component<PlayerProps> = (props) => {
el[el.paused ? "play" : "pause"]();
};
return (
<figure class={css.player}>
<h1>{props.entry?.title}</h1>
const setTime = (time: number) => {
const el = video();
<video
ref={setVideo}
muted
autoplay
controls
src={`/api/content/stream?id=${props.id}`}
lang="en"
>
<track
default
kind="captions"
label="English"
srclang="en"
src={captionUrl()}
/>
<track default kind="chapters" src={thumbnails()} id="thumbnails" />
{/* <track kind="captions" />
if (!el) {
return;
}
el.currentTime = time;
};
return (
<>
<figure class={css.player}>
<h1>{props.entry?.title}</h1>
<video
ref={setVideo}
muted
autoplay
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="descriptions" />
<track kind="metadata" />
<track kind="subtitles" /> */}
</video>
</video>
<figcaption>
<Volume value={video()?.volume ?? 0} muted={video()?.muted ?? false} onInput={({ volume, muted }) => {
video().volume = volume;
video().muted = muted;
}} />
</figcaption>
<figcaption>
<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>
<button onclick={toggle}>play/pause</button>
<span>
{formatTime(currentTime())} / {formatTime(duration())}
</span>
<progress max={duration().toFixed(0)} value={currentTime().toFixed(0)} />
</figure>
<span>
{formatTime(currentTime())} / {formatTime(duration())}
</span>
</figure>
</>
);
};

View file

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