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 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue