kaas
This commit is contained in:
parent
fbc040c317
commit
826a30f95f
19 changed files with 430 additions and 170 deletions
|
@ -178,12 +178,43 @@ export const getItem = query(
|
||||||
overview: data.Overview!,
|
overview: data.Overview!,
|
||||||
thumbnail: new URL(`/Items/${itemId}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'),
|
thumbnail: new URL(`/Items/${itemId}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'),
|
||||||
image: new URL(`/Items/${itemId}/Images/Backdrop`, getBaseUrl()),
|
image: new URL(`/Items/${itemId}/Images/Backdrop`, getBaseUrl()),
|
||||||
|
providers: {
|
||||||
|
jellyfin: data.Id
|
||||||
|
}
|
||||||
// ...data,
|
// ...data,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
"jellyfin.getItem",
|
"jellyfin.getItem",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
export const getItemStream = query(
|
||||||
|
async (userId: string, itemId: string): Promise<string | undefined> => {
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
const item = await getItem(userId, itemId);
|
||||||
|
|
||||||
|
console.log(item);
|
||||||
|
|
||||||
|
if (item === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await getClient().GET("/Videos/{itemId}/stream", {
|
||||||
|
params: {
|
||||||
|
path: {
|
||||||
|
itemId: item.providers.jellyfin,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
"jellyfin.getItemStream",
|
||||||
|
);
|
||||||
|
|
||||||
export const getItemImage = query(
|
export const getItemImage = query(
|
||||||
async (
|
async (
|
||||||
itemId: string,
|
itemId: string,
|
||||||
|
@ -254,6 +285,23 @@ export const queryItems = query(async () => {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
}, "jellyfin.queryItems");
|
}, "jellyfin.queryItems");
|
||||||
|
|
||||||
|
export const getItemIds = query(async () => {
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
const { data, error } = await getClient().GET("/Items", {
|
||||||
|
params: {
|
||||||
|
query: {
|
||||||
|
mediaTypes: ["Video"],
|
||||||
|
fields: ["ProviderIds"],
|
||||||
|
includeItemTypes: ["Series", "Movie"],
|
||||||
|
recursive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
}, "jellyfin.getItemIds");
|
||||||
|
|
||||||
export const getContinueWatching = query(
|
export const getContinueWatching = query(
|
||||||
async (userId: string): Promise<Entry[]> => {
|
async (userId: string): Promise<Entry[]> => {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
|
@ -41,6 +41,8 @@ export const getEntry = query(
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
if (data === undefined) {
|
if (data === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import type { Category, Entry } from "./types";
|
import type { Category, Entry } from "./types";
|
||||||
import { query } from "@solidjs/router";
|
import { query } from "@solidjs/router";
|
||||||
import { getContinueWatching, getRandomItems } from "./apis/jellyfin";
|
import { getContinueWatching, getItemStream, getRandomItems } from "./apis/jellyfin";
|
||||||
import {
|
import {
|
||||||
getDiscovery,
|
getDiscovery,
|
||||||
getRecommendations,
|
getRecommendations,
|
||||||
|
@ -14,6 +14,7 @@ const jellyfinUserId = "a9c51af84bf54578a99ab4dd0ebf0763";
|
||||||
|
|
||||||
// export const getHighlights = () => getRandomItems(jellyfinUserId);
|
// export const getHighlights = () => getRandomItems(jellyfinUserId);
|
||||||
export const getHighlights = () => getContinueWatching(jellyfinUserId);
|
export const getHighlights = () => getContinueWatching(jellyfinUserId);
|
||||||
|
export const getStream = (id: string) => getItemStream(jellyfinUserId, id);
|
||||||
|
|
||||||
export const listCategories = query(async (): Promise<Category[]> => {
|
export const listCategories = query(async (): Promise<Category[]> => {
|
||||||
return [
|
return [
|
||||||
|
|
|
@ -3,24 +3,37 @@ import {
|
||||||
ContextProviderProps,
|
ContextProviderProps,
|
||||||
createContextProvider,
|
createContextProvider,
|
||||||
} from "@solid-primitives/context";
|
} from "@solid-primitives/context";
|
||||||
import { Accessor, createMemo } from "solid-js";
|
import { Accessor, createEffect, onMount, Setter } from "solid-js";
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
import { createEventListenerMap } from "@solid-primitives/event-listener";
|
import { createEventListenerMap } from "@solid-primitives/event-listener";
|
||||||
|
|
||||||
type State = "playing" | "paused";
|
type State = "playing" | "paused";
|
||||||
|
|
||||||
interface Volume {
|
type Volume = {
|
||||||
value: number;
|
value: number;
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface VideoAPI {
|
export interface VideoAPI {
|
||||||
readonly state: Accessor<State>;
|
readonly duration: Accessor<number>;
|
||||||
readonly volume: Accessor<Volume>;
|
readonly buffered: Accessor<number>;
|
||||||
|
readonly currentTime: Accessor<number>;
|
||||||
|
setTime(time: number): void;
|
||||||
|
|
||||||
play(): void;
|
readonly state: {
|
||||||
|
readonly state: Accessor<State>;
|
||||||
|
readonly setState: Setter<State>;
|
||||||
pause(): void;
|
pause(): void;
|
||||||
togglePlayState(): void;
|
play(): void;
|
||||||
|
};
|
||||||
|
readonly volume: {
|
||||||
|
readonly value: Accessor<number>;
|
||||||
|
readonly muted: Accessor<boolean>;
|
||||||
|
readonly setValue: Setter<number>;
|
||||||
|
readonly setMuted: Setter<boolean>;
|
||||||
|
unmute(): void;
|
||||||
|
mute(): void;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VideoProviderProps extends ContextProviderProps {
|
interface VideoProviderProps extends ContextProviderProps {
|
||||||
|
@ -28,6 +41,9 @@ interface VideoProviderProps extends ContextProviderProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VideoStore {
|
interface VideoStore {
|
||||||
|
duration: number;
|
||||||
|
buffered: number;
|
||||||
|
currentTime: number;
|
||||||
state: State;
|
state: State;
|
||||||
volume: Volume;
|
volume: Volume;
|
||||||
}
|
}
|
||||||
|
@ -39,15 +55,28 @@ export const [VideoProvider, useVideo] = createContextProvider<
|
||||||
(props) => {
|
(props) => {
|
||||||
const video = props.video;
|
const video = props.video;
|
||||||
const [store, setStore] = createStore<VideoStore>({
|
const [store, setStore] = createStore<VideoStore>({
|
||||||
|
duration: 0,
|
||||||
|
buffered: 0,
|
||||||
|
currentTime: 0,
|
||||||
state: "paused",
|
state: "paused",
|
||||||
volume: {
|
volume: {
|
||||||
value: 0.5,
|
value: 0.1,
|
||||||
muted: false,
|
muted: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const api: VideoAPI = {
|
const api: VideoAPI = {
|
||||||
state: createMemo(() => store.state),
|
duration: () => store.duration,
|
||||||
|
buffered: () => store.buffered,
|
||||||
|
currentTime: () => store.currentTime,
|
||||||
|
|
||||||
|
setTime(time) {
|
||||||
|
video!.currentTime = time;
|
||||||
|
},
|
||||||
|
|
||||||
|
state: {
|
||||||
|
state: () => store.state,
|
||||||
|
setState: setStore.bind(null, "state"),
|
||||||
|
|
||||||
play() {
|
play() {
|
||||||
setStore("state", "playing");
|
setStore("state", "playing");
|
||||||
|
@ -56,11 +85,20 @@ export const [VideoProvider, useVideo] = createContextProvider<
|
||||||
pause() {
|
pause() {
|
||||||
setStore("state", "paused");
|
setStore("state", "paused");
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
volume: {
|
||||||
|
value: () => store.volume.value,
|
||||||
|
muted: () => store.volume.muted,
|
||||||
|
|
||||||
togglePlayState() {
|
setValue: setStore.bind(null, "volume", "value"),
|
||||||
setStore("state", (state) =>
|
setMuted: setStore.bind(null, "volume", "muted"),
|
||||||
state === "playing" ? "paused" : "playing"
|
|
||||||
);
|
mute() {
|
||||||
|
setStore("volume", "muted", true);
|
||||||
|
},
|
||||||
|
unmute() {
|
||||||
|
setStore("volume", "muted", false);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -68,6 +106,23 @@ export const [VideoProvider, useVideo] = createContextProvider<
|
||||||
return api;
|
return api;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
video[store.state === "playing" ? "play" : "pause"]();
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
video.muted = store.volume.muted;
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
video.volume = store.volume.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
setStore("duration", video.duration);
|
||||||
|
setStore("currentTime", video.currentTime);
|
||||||
|
});
|
||||||
|
|
||||||
createEventListenerMap(video, {
|
createEventListenerMap(video, {
|
||||||
play(e) {
|
play(e) {
|
||||||
setStore("state", "playing");
|
setStore("state", "playing");
|
||||||
|
@ -75,9 +130,49 @@ export const [VideoProvider, useVideo] = createContextProvider<
|
||||||
pause(e) {
|
pause(e) {
|
||||||
setStore("state", "paused");
|
setStore("state", "paused");
|
||||||
},
|
},
|
||||||
|
durationchange(e) {
|
||||||
|
setStore("duration", video.duration);
|
||||||
|
},
|
||||||
|
timeupdate(e) {
|
||||||
|
setStore("currentTime", video.currentTime);
|
||||||
|
},
|
||||||
|
volumeChange() {
|
||||||
|
setStore("volume", { muted: video.muted, value: video.volume });
|
||||||
|
},
|
||||||
|
progress(e) {
|
||||||
|
const timeRanges = video.buffered;
|
||||||
|
|
||||||
|
setStore(
|
||||||
|
"buffered",
|
||||||
|
timeRanges.length > 0 ? timeRanges.end(timeRanges.length - 1) : 0
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return api;
|
return api;
|
||||||
},
|
},
|
||||||
{ state: () => "paused" }
|
{
|
||||||
|
duration: () => 0,
|
||||||
|
buffered: () => 0,
|
||||||
|
currentTime: () => 0,
|
||||||
|
|
||||||
|
setTime() {},
|
||||||
|
|
||||||
|
state: {
|
||||||
|
state: () => "playing",
|
||||||
|
setState() {},
|
||||||
|
play() {},
|
||||||
|
pause() {},
|
||||||
|
},
|
||||||
|
volume: {
|
||||||
|
value: () => 0.5,
|
||||||
|
muted: () => false,
|
||||||
|
|
||||||
|
setValue() {},
|
||||||
|
setMuted() {},
|
||||||
|
|
||||||
|
mute() {},
|
||||||
|
unmute() {},
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
13
src/features/player/controls/playState.module.css
Normal file
13
src/features/player/controls/playState.module.css
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
.play {
|
||||||
|
font-size: var(--size-7);
|
||||||
|
text-shadow: 0 0 .5rem #000;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
|
||||||
|
transition: background-color .2s var(--ease-in-out-1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(from var(--gray-2) r g b / .25);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,23 @@
|
||||||
import { Component, createMemo } from "solid-js";
|
import { Component, Show } from "solid-js";
|
||||||
import { useVideo } from "../context";
|
import { useVideo } from "../context";
|
||||||
|
import { FaSolidPause, FaSolidPlay } from "solid-icons/fa";
|
||||||
|
import css from "./playState.module.css";
|
||||||
|
|
||||||
export const PlayState: Component<{}> = (props) => {
|
export const PlayState: Component<{}> = (props) => {
|
||||||
const video = useVideo();
|
const video = useVideo();
|
||||||
|
|
||||||
const icon = createMemo(() => {
|
return (
|
||||||
return {
|
<button
|
||||||
playing: "⏵",
|
class={css.play}
|
||||||
paused: "⏸",
|
onclick={(e) =>
|
||||||
}[video.state()];
|
video.state.setState((last) =>
|
||||||
});
|
last === "playing" ? "paused" : "playing"
|
||||||
|
)
|
||||||
return <button onclick={(e) => video.togglePlayState()}>{icon()}</button>;
|
}
|
||||||
|
>
|
||||||
|
<Show when={video.state.state() === "playing"} fallback={<FaSolidPlay />}>
|
||||||
|
<FaSolidPause />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
68
src/features/player/controls/seekBar.module.css
Normal file
68
src/features/player/controls/seekBar.module.css
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
display: block grid;
|
||||||
|
grid: auto var(--size-2) / auto auto;
|
||||||
|
place-content: space-between;
|
||||||
|
|
||||||
|
gap: var(--size-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration {
|
||||||
|
grid-area: 1 / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
--_v: calc(1% * attr(data-value type(<number>), 0));
|
||||||
|
grid-area: 2 / span 2;
|
||||||
|
position: absolute;
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
background: linear-gradient(var(--blue-3)) top left / var(--_v) 100% no-repeat transparent;
|
||||||
|
border-radius: var(--radius-round);
|
||||||
|
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
appearance: none;
|
||||||
|
display: block;
|
||||||
|
inline-size: var(--size-3);
|
||||||
|
block-size: var(--size-3);
|
||||||
|
background-color: var(--blue-7);
|
||||||
|
border-radius: var(--radius-round);
|
||||||
|
box-shadow: var(--shadow-2);
|
||||||
|
/* No clue why this offset is what works... */
|
||||||
|
margin-top: -.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buffered {
|
||||||
|
grid-area: 2 / span 2;
|
||||||
|
position: absolute;
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: 100%;
|
||||||
|
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
&::-webkit-progress-bar {
|
||||||
|
background-color: rgba(from var(--gray-4) r g b / .5);
|
||||||
|
border-radius: var(--radius-round);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-progress-value {
|
||||||
|
background-color: rgba(from var(--gray-2) r g b / .75);
|
||||||
|
border-radius: var(--radius-round);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-progress-bar {
|
||||||
|
background-color: rgba(from var(--surface-4) r g b / .5);
|
||||||
|
border-radius: var(--radius-round);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +1,32 @@
|
||||||
import { Component } from "solid-js";
|
import { Component } from "solid-js";
|
||||||
|
import { useVideo } from "../context";
|
||||||
|
import css from "./seekBar.module.css";
|
||||||
|
|
||||||
interface SeekBarProps {
|
interface SeekBarProps {}
|
||||||
video: HTMLVideoElement | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SeekBar: Component<SeekBarProps> = () => {
|
export const SeekBar: Component<SeekBarProps> = () => {
|
||||||
|
const video = useVideo();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div class={css.container}>
|
||||||
|
<span class={css.time}>{formatTime(video.currentTime())}</span>
|
||||||
|
<span class={css.duration}>{formatTime(video.duration())}</span>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
class={css.bar}
|
||||||
list="chapters"
|
list="chapters"
|
||||||
type="range"
|
type="range"
|
||||||
max={duration().toFixed(0)}
|
max={video.duration().toFixed(2)}
|
||||||
value={currentTime().toFixed(0)}
|
value={video.currentTime().toFixed(2)}
|
||||||
oninput={(e) => setTime(e.target.valueAsNumber)}
|
data-value={((video.currentTime() / video.duration()) * 100).toFixed(2)}
|
||||||
step="1"
|
oninput={(e) => video.setTime(e.target.valueAsNumber)}
|
||||||
|
step="0.01"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<progress
|
||||||
|
class={css.buffered}
|
||||||
|
max={video.duration().toFixed(2)}
|
||||||
|
value={video.buffered().toFixed(2)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<datalist id="chapters">
|
<datalist id="chapters">
|
||||||
|
@ -21,6 +34,20 @@ export const SeekBar: Component<SeekBarProps> = () => {
|
||||||
<option value="200">Chapter 2</option>
|
<option value="200">Chapter 2</option>
|
||||||
<option value="300">Chapter 3</option>
|
<option value="300">Chapter 3</option>
|
||||||
</datalist>
|
</datalist>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatTime = (subject: number) => {
|
||||||
|
if (Number.isNaN(subject)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = Math.floor(subject / 3600);
|
||||||
|
const minutes = Math.floor((subject % 3600) / 60);
|
||||||
|
const seconds = Math.floor(subject % 60);
|
||||||
|
|
||||||
|
const sections = hours !== 0 ? [hours, minutes, seconds] : [minutes, seconds];
|
||||||
|
|
||||||
|
return sections.map((section) => String(section).padStart(2, "0")).join(":");
|
||||||
|
};
|
||||||
|
|
|
@ -1,3 +1,18 @@
|
||||||
.container {
|
.container {
|
||||||
display: block grid;
|
display: block grid;
|
||||||
|
grid: 100% / auto 1fr;
|
||||||
|
|
||||||
|
& > button {
|
||||||
|
font-size: var(--size-7);
|
||||||
|
text-shadow: 0 0 .5rem #000;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
|
||||||
|
transition: background-color .2s var(--ease-in-out-1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(from var(--gray-2) r g b / .25);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { Component, createEffect, createSignal, Show } from "solid-js";
|
import { Component, Show } from "solid-js";
|
||||||
import css from "./volume.module.css";
|
import css from "./volume.module.css";
|
||||||
import { createStore, unwrap } from "solid-js/store";
|
|
||||||
import { trackDeep } from "@solid-primitives/deep";
|
|
||||||
import { useVideo } from "../context";
|
import { useVideo } from "../context";
|
||||||
|
import { FaSolidVolumeOff, FaSolidVolumeXmark } from "solid-icons/fa";
|
||||||
|
|
||||||
interface VolumeProps {
|
interface VolumeProps {
|
||||||
value: number;
|
value: number;
|
||||||
|
@ -13,31 +12,23 @@ interface VolumeProps {
|
||||||
export const Volume: Component<VolumeProps> = (props) => {
|
export const Volume: Component<VolumeProps> = (props) => {
|
||||||
const video = useVideo();
|
const video = useVideo();
|
||||||
|
|
||||||
const [state, setState] = createStore({
|
|
||||||
volume: props.value,
|
|
||||||
muted: props.muted ?? false,
|
|
||||||
});
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
props.onInput?.(unwrap(trackDeep(state)));
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={css.container}>
|
<div class={css.container}>
|
||||||
<button onClick={() => setState("muted", (m) => !m)}>
|
<button onClick={() => video.volume.setMuted((m) => !m)}>
|
||||||
<Show when={state.muted} fallback="mute">
|
<Show when={video.volume.muted()} fallback={<FaSolidVolumeOff />}>
|
||||||
unmute
|
<FaSolidVolumeXmark />
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
value={state.volume}
|
value={video.volume.value()}
|
||||||
min="0"
|
min="0"
|
||||||
max="1"
|
max="1"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
onInput={(e) =>
|
onInput={(e) => {
|
||||||
setState({ muted: false, volume: e.target.valueAsNumber })
|
video.volume.setValue(e.target.valueAsNumber);
|
||||||
}
|
video.volume.setMuted(false);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,47 @@
|
||||||
.player {
|
.player {
|
||||||
|
position: relative;
|
||||||
|
container-type: inline-size;
|
||||||
|
isolation: isolate;
|
||||||
|
|
||||||
|
display: block grid;
|
||||||
|
grid: 100% / 100%;
|
||||||
|
overflow: clip;
|
||||||
|
|
||||||
|
block-size: max-content;
|
||||||
|
|
||||||
|
background-color: black;
|
||||||
|
|
||||||
& > video::cue {
|
& > video::cue {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > video {
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: 100%;
|
||||||
|
object-position: center;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > figcaption {
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
display: block flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
position: absolute;
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: 100%;
|
||||||
|
max-inline-size: none;
|
||||||
|
align-content: end;
|
||||||
|
|
||||||
|
gap: var(--size-2);
|
||||||
|
padding: var(--size-2);
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > :nth-child(1) {
|
||||||
|
inline-size: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { PlayState } from "./controls/playState";
|
||||||
import { createContextProvider } from "@solid-primitives/context";
|
import { createContextProvider } from "@solid-primitives/context";
|
||||||
import { isServer } from "solid-js/web";
|
import { isServer } from "solid-js/web";
|
||||||
import { VideoProvider } from "./context";
|
import { VideoProvider } from "./context";
|
||||||
|
import { SeekBar } from "./controls/seekBar";
|
||||||
|
|
||||||
const metadata = query(async (id: string) => {
|
const metadata = query(async (id: string) => {
|
||||||
"use server";
|
"use server";
|
||||||
|
@ -81,21 +82,6 @@ export const Player: Component<PlayerProps> = (props) => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDurationChange = createEventSignal(video, "durationchange");
|
|
||||||
const onTimeUpdate = createEventSignal(video, "timeupdate");
|
|
||||||
|
|
||||||
const duration = createMemo(() => {
|
|
||||||
onDurationChange();
|
|
||||||
|
|
||||||
return video()?.duration ?? 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentTime = createMemo(() => {
|
|
||||||
onTimeUpdate();
|
|
||||||
|
|
||||||
return video()?.currentTime ?? 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
createEventListenerMap(() => video()!, {
|
createEventListenerMap(() => video()!, {
|
||||||
durationchange(e) {
|
durationchange(e) {
|
||||||
// console.log("durationchange", e);
|
// console.log("durationchange", e);
|
||||||
|
@ -110,14 +96,14 @@ export const Player: Component<PlayerProps> = (props) => {
|
||||||
// console.log("ratechange", e);
|
// console.log("ratechange", e);
|
||||||
},
|
},
|
||||||
seeked(e) {
|
seeked(e) {
|
||||||
console.log("seeked", "completed the seek interaction", e);
|
// console.log("seeked", "completed the seek interaction", e);
|
||||||
},
|
},
|
||||||
seeking(e) {
|
seeking(e) {
|
||||||
console.log(
|
// console.log(
|
||||||
"seeking",
|
// "seeking",
|
||||||
"the time on the video has been set, now the content will be loaded, the seeked event will fire when this is done",
|
// "the time on the video has been set, now the content will be loaded, the seeked event will fire when this is done",
|
||||||
e
|
// e
|
||||||
);
|
// );
|
||||||
},
|
},
|
||||||
stalled(e) {
|
stalled(e) {
|
||||||
// console.log(
|
// console.log(
|
||||||
|
@ -126,72 +112,29 @@ export const Player: Component<PlayerProps> = (props) => {
|
||||||
// video()!.error,
|
// video()!.error,
|
||||||
// );
|
// );
|
||||||
},
|
},
|
||||||
|
// suspend(e) {
|
||||||
play(e) {
|
|
||||||
// console.log("play", e);
|
|
||||||
},
|
|
||||||
canplay(e) {
|
|
||||||
// console.log("canplay", e);
|
|
||||||
},
|
|
||||||
playing(e) {
|
|
||||||
// console.log("playing", e);
|
|
||||||
},
|
|
||||||
pause(e) {
|
|
||||||
// console.log("pause", e);
|
|
||||||
},
|
|
||||||
suspend(e) {
|
|
||||||
// console.log("suspend", e);
|
// console.log("suspend", e);
|
||||||
},
|
// },
|
||||||
|
// canplay(e) {
|
||||||
volumechange(e) {
|
// console.log("canplay", e);
|
||||||
// console.log("volumechange", e);
|
// },
|
||||||
},
|
|
||||||
|
|
||||||
waiting(e) {
|
waiting(e) {
|
||||||
console.log("waiting", e);
|
// console.log("waiting", e);
|
||||||
},
|
},
|
||||||
|
|
||||||
progress(e) {
|
|
||||||
console.log(e);
|
|
||||||
},
|
|
||||||
|
|
||||||
// timeupdate(e) {
|
|
||||||
// console.log("timeupdate", e);
|
|
||||||
// },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggle = () => {
|
|
||||||
const el = video();
|
|
||||||
|
|
||||||
if (!el) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
el[el.paused ? "play" : "pause"]();
|
|
||||||
};
|
|
||||||
|
|
||||||
const setTime = (time: number) => {
|
|
||||||
const el = video();
|
|
||||||
|
|
||||||
if (!el) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
el.currentTime = time;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<figure class={css.player}>
|
<figure class={css.player}>
|
||||||
<h1>{props.entry?.title}</h1>
|
{/* <h1>{props.entry.title}</h1> */}
|
||||||
|
|
||||||
<video
|
<video
|
||||||
ref={setVideo}
|
ref={setVideo}
|
||||||
muted
|
playsinline
|
||||||
autoplay
|
src={`/api/content/${props.entry.id}/stream`}
|
||||||
src={`/api/content/stream?id=${props.id}`}
|
|
||||||
// src="https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4"
|
// src="https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4"
|
||||||
poster={props.entry?.image}
|
poster={props.entry.image}
|
||||||
lang="en"
|
lang="en"
|
||||||
>
|
>
|
||||||
<track
|
<track
|
||||||
|
@ -211,6 +154,7 @@ export const Player: Component<PlayerProps> = (props) => {
|
||||||
|
|
||||||
<figcaption>
|
<figcaption>
|
||||||
<VideoProvider video={video()}>
|
<VideoProvider video={video()}>
|
||||||
|
<SeekBar />
|
||||||
<PlayState />
|
<PlayState />
|
||||||
<Volume
|
<Volume
|
||||||
value={video()?.volume ?? 0}
|
value={video()?.volume ?? 0}
|
||||||
|
@ -222,23 +166,7 @@ export const Player: Component<PlayerProps> = (props) => {
|
||||||
/>
|
/>
|
||||||
</VideoProvider>
|
</VideoProvider>
|
||||||
</figcaption>
|
</figcaption>
|
||||||
|
|
||||||
<button onclick={toggle}>play/pause</button>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
{formatTime(currentTime())} / {formatTime(duration())}
|
|
||||||
</span>
|
|
||||||
</figure>
|
</figure>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTime = (subject: number) => {
|
|
||||||
const hours = Math.floor(subject / 3600);
|
|
||||||
const minutes = Math.floor((subject % 3600) / 60);
|
|
||||||
const seconds = Math.floor(subject % 60);
|
|
||||||
|
|
||||||
const sections = hours !== 0 ? [hours, minutes, seconds] : [minutes, seconds];
|
|
||||||
|
|
||||||
return sections.map((section) => String(section).padStart(2, "0")).join(":");
|
|
||||||
};
|
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
container-type: inline-size;
|
container-type: inline-size;
|
||||||
background-color: var(--surface-1);
|
background-color: var(--surface-1);
|
||||||
|
contain: layout style paint;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
|
@ -38,10 +39,13 @@
|
||||||
overflow: clip auto;
|
overflow: clip auto;
|
||||||
padding-inline-start: 5em;
|
padding-inline-start: 5em;
|
||||||
transition: filter var(--duration-moderate-1) var(--ease-3);
|
transition: filter var(--duration-moderate-1) var(--ease-3);
|
||||||
|
contain: layout style paint;
|
||||||
|
container-type: size;
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
background-color: var(--surface-2);
|
background-color: var(--surface-2);
|
||||||
isolation: isolate;
|
container-type: inline-size;
|
||||||
|
contain: layout style paint;
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
block-size: fit-content;
|
block-size: fit-content;
|
||||||
min-block-size: 100%;
|
min-block-size: 100%;
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
grid: 100% / 100%;
|
grid: 100% / 100%;
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
block-size: 100%;
|
block-size: 100%;
|
||||||
|
contain: layout style paint;
|
||||||
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
|
|
|
@ -8,9 +8,9 @@ import {
|
||||||
useParams,
|
useParams,
|
||||||
} from "@solidjs/router";
|
} from "@solidjs/router";
|
||||||
import { Show } from "solid-js";
|
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";
|
||||||
|
import css from "./slug.module.css";
|
||||||
|
|
||||||
const healUrl = query(async (slug: string) => {
|
const healUrl = query(async (slug: string) => {
|
||||||
const entry = await getEntry(slug.slice(slug.lastIndexOf("-") + 1));
|
const entry = await getEntry(slug.slice(slug.lastIndexOf("-") + 1));
|
||||||
|
@ -53,10 +53,10 @@ export default function Item() {
|
||||||
const entry = createAsync(() => getEntry(id));
|
const entry = createAsync(() => getEntry(id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div class={css.page}>
|
||||||
<Show when={entry()} fallback="Some kind of pretty 404 page I guess">{
|
<Show when={entry()} fallback="Some kind of pretty 404 page I guess">
|
||||||
entry => <Player entry={entry()} />
|
{(entry) => <Player entry={entry()} />}
|
||||||
}</Show>
|
</Show>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
12
src/routes/(shell)/watch/slug.module.css
Normal file
12
src/routes/(shell)/watch/slug.module.css
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
.page {
|
||||||
|
contain: layout style paint;
|
||||||
|
display: block grid;
|
||||||
|
grid: 100cqb / 100%;
|
||||||
|
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: 100%;
|
||||||
|
|
||||||
|
& > figure {
|
||||||
|
max-block-size: 100cqb;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,17 @@
|
||||||
import { APIEvent } from "@solidjs/start/server";
|
import { APIEvent } from "@solidjs/start/server";
|
||||||
|
import { getStream } from "~/features/content";
|
||||||
|
|
||||||
const CHUNK_SIZE = 1 * 1e6; // 1MB
|
const CHUNK_SIZE = 1 * 1e6; // 1MB
|
||||||
|
|
||||||
export const GET = async ({ request, ...event }: APIEvent) => {
|
export const GET = async ({ request, params }: APIEvent) => {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
console.log('api endpoind called')
|
||||||
|
|
||||||
|
const res = await getStream(params.id);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
|
||||||
const range = request.headers.get("range");
|
const range = request.headers.get("range");
|
||||||
|
|
||||||
if (range === null) {
|
if (range === null) {
|
||||||
|
@ -21,22 +28,20 @@ export const GET = async ({ request, ...event }: APIEvent) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoSize = file.size;
|
const videoSize = file.size;
|
||||||
|
|
||||||
const start = Number.parseInt(range.replace(/\D/g, ""));
|
const start = Number.parseInt(range.replace(/\D/g, ""));
|
||||||
const end = Math.min(start + CHUNK_SIZE, videoSize - 1);
|
const end = Math.min(start + CHUNK_SIZE - 1, videoSize - 1);
|
||||||
const contentLength = end - start + 1;
|
|
||||||
|
|
||||||
return new Response(file.stream());
|
console.log(`streaming slice(${start}, ${end})`);
|
||||||
|
|
||||||
// return new Response(video.slice(start, end).stream(), {
|
return new Response(file.slice(start, end), {
|
||||||
// status: 206,
|
status: 206,
|
||||||
// headers: {
|
headers: {
|
||||||
// 'Accept-Ranges': 'bytes',
|
'Accept-Ranges': 'bytes',
|
||||||
// 'Content-Range': `bytes ${start}-${end}/${videoSize}`,
|
'Content-Range': `bytes ${start}-${end}/${videoSize}`,
|
||||||
// 'Content-Length': `${contentLength}`,
|
'Content-Length': `${end - start + 1}`,
|
||||||
// 'Content-type': 'video/mp4',
|
'Content-Type': 'video/mp4',
|
||||||
// },
|
},
|
||||||
// });
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue