From fbc040c317b211bb8128da7ee565cef1e6b1cd79 Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Mon, 26 May 2025 16:25:39 +0200 Subject: [PATCH] started a more proper implementation of the player --- src/features/player/context.tsx | 83 ++++++++++++++ src/features/player/controls/playState.tsx | 15 +++ src/features/player/controls/seekBar.tsx | 26 +++++ src/features/player/controls/volume.tsx | 27 ++++- src/features/player/player.tsx | 124 +++++++++++++-------- src/routes/(shell)/watch/[slug].tsx | 6 +- 6 files changed, 231 insertions(+), 50 deletions(-) create mode 100644 src/features/player/context.tsx create mode 100644 src/features/player/controls/playState.tsx create mode 100644 src/features/player/controls/seekBar.tsx diff --git a/src/features/player/context.tsx b/src/features/player/context.tsx new file mode 100644 index 0000000..eec04a5 --- /dev/null +++ b/src/features/player/context.tsx @@ -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; + readonly volume: Accessor; + + 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({ + 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" } +); diff --git a/src/features/player/controls/playState.tsx b/src/features/player/controls/playState.tsx new file mode 100644 index 0000000..846b198 --- /dev/null +++ b/src/features/player/controls/playState.tsx @@ -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 ; +}; diff --git a/src/features/player/controls/seekBar.tsx b/src/features/player/controls/seekBar.tsx new file mode 100644 index 0000000..6c3dc05 --- /dev/null +++ b/src/features/player/controls/seekBar.tsx @@ -0,0 +1,26 @@ +import { Component } from "solid-js"; + +interface SeekBarProps { + video: HTMLVideoElement | undefined; +} + +export const SeekBar: Component = () => { + return ( + <> + setTime(e.target.valueAsNumber)} + step="1" + /> + + + + + + + + ); +}; diff --git a/src/features/player/controls/volume.tsx b/src/features/player/controls/volume.tsx index a99a803..7175af6 100644 --- a/src/features/player/controls/volume.tsx +++ b/src/features/player/controls/volume.tsx @@ -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 = (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 = (props) => { return (
- - setState('volume', e.target.valueAsNumber)} /> + + + setState({ muted: false, volume: e.target.valueAsNumber }) + } + />
); }; diff --git a/src/features/player/player.tsx b/src/features/player/player.tsx index 3da6022..9d66f10 100644 --- a/src/features/player/player.tsx +++ b/src/features/player/player.tsx @@ -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 = (props) => { const [video, setVideo] = createSignal( - 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 = (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 = (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 = (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 = (props) => { el[el.paused ? "play" : "pause"](); }; - return ( -
-

{props.entry?.title}

+ const setTime = (time: number) => { + const el = video(); -
+ ); }; diff --git a/src/routes/(shell)/watch/[slug].tsx b/src/routes/(shell)/watch/[slug].tsx index 3680186..55f19bb 100644 --- a/src/routes/(shell)/watch/[slug].tsx +++ b/src/routes/(shell)/watch/[slug].tsx @@ -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 ( <> - + { + entry => + } ); }