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!,
|
||||
thumbnail: new URL(`/Items/${itemId}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'),
|
||||
image: new URL(`/Items/${itemId}/Images/Backdrop`, getBaseUrl()),
|
||||
providers: {
|
||||
jellyfin: data.Id
|
||||
}
|
||||
// ...data,
|
||||
};
|
||||
},
|
||||
"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(
|
||||
async (
|
||||
itemId: string,
|
||||
|
@ -254,6 +285,23 @@ export const queryItems = query(async () => {
|
|||
console.log(data);
|
||||
}, "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(
|
||||
async (userId: string): Promise<Entry[]> => {
|
||||
"use server";
|
||||
|
|
|
@ -41,6 +41,8 @@ export const getEntry = query(
|
|||
},
|
||||
});
|
||||
|
||||
console.log(data);
|
||||
|
||||
if (data === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import type { Category, Entry } from "./types";
|
||||
import { query } from "@solidjs/router";
|
||||
import { getContinueWatching, getRandomItems } from "./apis/jellyfin";
|
||||
import { getContinueWatching, getItemStream, getRandomItems } from "./apis/jellyfin";
|
||||
import {
|
||||
getDiscovery,
|
||||
getRecommendations,
|
||||
|
@ -14,6 +14,7 @@ const jellyfinUserId = "a9c51af84bf54578a99ab4dd0ebf0763";
|
|||
|
||||
// export const getHighlights = () => getRandomItems(jellyfinUserId);
|
||||
export const getHighlights = () => getContinueWatching(jellyfinUserId);
|
||||
export const getStream = (id: string) => getItemStream(jellyfinUserId, id);
|
||||
|
||||
export const listCategories = query(async (): Promise<Category[]> => {
|
||||
return [
|
||||
|
|
|
@ -3,24 +3,37 @@ import {
|
|||
ContextProviderProps,
|
||||
createContextProvider,
|
||||
} 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 { createEventListenerMap } from "@solid-primitives/event-listener";
|
||||
|
||||
type State = "playing" | "paused";
|
||||
|
||||
interface Volume {
|
||||
type Volume = {
|
||||
value: number;
|
||||
muted: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export interface VideoAPI {
|
||||
readonly state: Accessor<State>;
|
||||
readonly volume: Accessor<Volume>;
|
||||
readonly duration: Accessor<number>;
|
||||
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;
|
||||
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 {
|
||||
|
@ -28,6 +41,9 @@ interface VideoProviderProps extends ContextProviderProps {
|
|||
}
|
||||
|
||||
interface VideoStore {
|
||||
duration: number;
|
||||
buffered: number;
|
||||
currentTime: number;
|
||||
state: State;
|
||||
volume: Volume;
|
||||
}
|
||||
|
@ -39,15 +55,28 @@ export const [VideoProvider, useVideo] = createContextProvider<
|
|||
(props) => {
|
||||
const video = props.video;
|
||||
const [store, setStore] = createStore<VideoStore>({
|
||||
duration: 0,
|
||||
buffered: 0,
|
||||
currentTime: 0,
|
||||
state: "paused",
|
||||
volume: {
|
||||
value: 0.5,
|
||||
value: 0.1,
|
||||
muted: false,
|
||||
},
|
||||
});
|
||||
|
||||
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() {
|
||||
setStore("state", "playing");
|
||||
|
@ -56,11 +85,20 @@ export const [VideoProvider, useVideo] = createContextProvider<
|
|||
pause() {
|
||||
setStore("state", "paused");
|
||||
},
|
||||
},
|
||||
volume: {
|
||||
value: () => store.volume.value,
|
||||
muted: () => store.volume.muted,
|
||||
|
||||
togglePlayState() {
|
||||
setStore("state", (state) =>
|
||||
state === "playing" ? "paused" : "playing"
|
||||
);
|
||||
setValue: setStore.bind(null, "volume", "value"),
|
||||
setMuted: setStore.bind(null, "volume", "muted"),
|
||||
|
||||
mute() {
|
||||
setStore("volume", "muted", true);
|
||||
},
|
||||
unmute() {
|
||||
setStore("volume", "muted", false);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -68,6 +106,23 @@ export const [VideoProvider, useVideo] = createContextProvider<
|
|||
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, {
|
||||
play(e) {
|
||||
setStore("state", "playing");
|
||||
|
@ -75,9 +130,49 @@ export const [VideoProvider, useVideo] = createContextProvider<
|
|||
pause(e) {
|
||||
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;
|
||||
},
|
||||
{ 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 { FaSolidPause, FaSolidPlay } from "solid-icons/fa";
|
||||
import css from "./playState.module.css";
|
||||
|
||||
export const PlayState: Component<{}> = (props) => {
|
||||
const video = useVideo();
|
||||
|
||||
const icon = createMemo(() => {
|
||||
return {
|
||||
playing: "⏵",
|
||||
paused: "⏸",
|
||||
}[video.state()];
|
||||
});
|
||||
|
||||
return <button onclick={(e) => video.togglePlayState()}>{icon()}</button>;
|
||||
return (
|
||||
<button
|
||||
class={css.play}
|
||||
onclick={(e) =>
|
||||
video.state.setState((last) =>
|
||||
last === "playing" ? "paused" : "playing"
|
||||
)
|
||||
}
|
||||
>
|
||||
<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 { useVideo } from "../context";
|
||||
import css from "./seekBar.module.css";
|
||||
|
||||
interface SeekBarProps {
|
||||
video: HTMLVideoElement | undefined;
|
||||
}
|
||||
interface SeekBarProps {}
|
||||
|
||||
export const SeekBar: Component<SeekBarProps> = () => {
|
||||
const video = useVideo();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class={css.container}>
|
||||
<span class={css.time}>{formatTime(video.currentTime())}</span>
|
||||
<span class={css.duration}>{formatTime(video.duration())}</span>
|
||||
|
||||
<input
|
||||
class={css.bar}
|
||||
list="chapters"
|
||||
type="range"
|
||||
max={duration().toFixed(0)}
|
||||
value={currentTime().toFixed(0)}
|
||||
oninput={(e) => setTime(e.target.valueAsNumber)}
|
||||
step="1"
|
||||
max={video.duration().toFixed(2)}
|
||||
value={video.currentTime().toFixed(2)}
|
||||
data-value={((video.currentTime() / video.duration()) * 100).toFixed(2)}
|
||||
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">
|
||||
|
@ -21,6 +34,20 @@ export const SeekBar: Component<SeekBarProps> = () => {
|
|||
<option value="200">Chapter 2</option>
|
||||
<option value="300">Chapter 3</option>
|
||||
</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 {
|
||||
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 { createStore, unwrap } from "solid-js/store";
|
||||
import { trackDeep } from "@solid-primitives/deep";
|
||||
import { useVideo } from "../context";
|
||||
import { FaSolidVolumeOff, FaSolidVolumeXmark } from "solid-icons/fa";
|
||||
|
||||
interface VolumeProps {
|
||||
value: number;
|
||||
|
@ -13,31 +12,23 @@ interface VolumeProps {
|
|||
export const Volume: Component<VolumeProps> = (props) => {
|
||||
const video = useVideo();
|
||||
|
||||
const [state, setState] = createStore({
|
||||
volume: props.value,
|
||||
muted: props.muted ?? false,
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
props.onInput?.(unwrap(trackDeep(state)));
|
||||
});
|
||||
|
||||
return (
|
||||
<div class={css.container}>
|
||||
<button onClick={() => setState("muted", (m) => !m)}>
|
||||
<Show when={state.muted} fallback="mute">
|
||||
unmute
|
||||
<button onClick={() => video.volume.setMuted((m) => !m)}>
|
||||
<Show when={video.volume.muted()} fallback={<FaSolidVolumeOff />}>
|
||||
<FaSolidVolumeXmark />
|
||||
</Show>
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
value={state.volume}
|
||||
value={video.volume.value()}
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
onInput={(e) =>
|
||||
setState({ muted: false, volume: e.target.valueAsNumber })
|
||||
}
|
||||
onInput={(e) => {
|
||||
video.volume.setValue(e.target.valueAsNumber);
|
||||
video.volume.setMuted(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,47 @@
|
|||
.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 {
|
||||
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 { isServer } from "solid-js/web";
|
||||
import { VideoProvider } from "./context";
|
||||
import { SeekBar } from "./controls/seekBar";
|
||||
|
||||
const metadata = query(async (id: string) => {
|
||||
"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()!, {
|
||||
durationchange(e) {
|
||||
// console.log("durationchange", e);
|
||||
|
@ -110,14 +96,14 @@ export const Player: Component<PlayerProps> = (props) => {
|
|||
// console.log("ratechange", e);
|
||||
},
|
||||
seeked(e) {
|
||||
console.log("seeked", "completed the seek interaction", e);
|
||||
// console.log("seeked", "completed the seek interaction", e);
|
||||
},
|
||||
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
|
||||
);
|
||||
// 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(
|
||||
|
@ -126,72 +112,29 @@ export const Player: Component<PlayerProps> = (props) => {
|
|||
// video()!.error,
|
||||
// );
|
||||
},
|
||||
|
||||
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) {
|
||||
// suspend(e) {
|
||||
// console.log("suspend", e);
|
||||
},
|
||||
|
||||
volumechange(e) {
|
||||
// console.log("volumechange", e);
|
||||
},
|
||||
// },
|
||||
// canplay(e) {
|
||||
// console.log("canplay", 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 (
|
||||
<>
|
||||
<figure class={css.player}>
|
||||
<h1>{props.entry?.title}</h1>
|
||||
{/* <h1>{props.entry.title}</h1> */}
|
||||
|
||||
<video
|
||||
ref={setVideo}
|
||||
muted
|
||||
autoplay
|
||||
src={`/api/content/stream?id=${props.id}`}
|
||||
playsinline
|
||||
src={`/api/content/${props.entry.id}/stream`}
|
||||
// src="https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4"
|
||||
poster={props.entry?.image}
|
||||
poster={props.entry.image}
|
||||
lang="en"
|
||||
>
|
||||
<track
|
||||
|
@ -211,6 +154,7 @@ export const Player: Component<PlayerProps> = (props) => {
|
|||
|
||||
<figcaption>
|
||||
<VideoProvider video={video()}>
|
||||
<SeekBar />
|
||||
<PlayState />
|
||||
<Volume
|
||||
value={video()?.volume ?? 0}
|
||||
|
@ -222,23 +166,7 @@ export const Player: Component<PlayerProps> = (props) => {
|
|||
/>
|
||||
</VideoProvider>
|
||||
</figcaption>
|
||||
|
||||
<button onclick={toggle}>play/pause</button>
|
||||
|
||||
<span>
|
||||
{formatTime(currentTime())} / {formatTime(duration())}
|
||||
</span>
|
||||
</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;
|
||||
container-type: inline-size;
|
||||
background-color: var(--surface-1);
|
||||
contain: layout style paint;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
|
@ -38,10 +39,13 @@
|
|||
overflow: clip auto;
|
||||
padding-inline-start: 5em;
|
||||
transition: filter var(--duration-moderate-1) var(--ease-3);
|
||||
contain: layout style paint;
|
||||
container-type: size;
|
||||
|
||||
& > div {
|
||||
background-color: var(--surface-2);
|
||||
isolation: isolate;
|
||||
container-type: inline-size;
|
||||
contain: layout style paint;
|
||||
inline-size: 100%;
|
||||
block-size: fit-content;
|
||||
min-block-size: 100%;
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
grid: 100% / 100%;
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
contain: layout style paint;
|
||||
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
|
|
|
@ -8,9 +8,9 @@ import {
|
|||
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";
|
||||
import css from "./slug.module.css";
|
||||
|
||||
const healUrl = query(async (slug: string) => {
|
||||
const entry = await getEntry(slug.slice(slug.lastIndexOf("-") + 1));
|
||||
|
@ -53,10 +53,10 @@ export default function Item() {
|
|||
const entry = createAsync(() => getEntry(id));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show when={entry()} fallback="Some kind of pretty 404 page I guess">{
|
||||
entry => <Player entry={entry()} />
|
||||
}</Show>
|
||||
</>
|
||||
<div class={css.page}>
|
||||
<Show when={entry()} fallback="Some kind of pretty 404 page I guess">
|
||||
{(entry) => <Player entry={entry()} />}
|
||||
</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 { getStream } from "~/features/content";
|
||||
|
||||
const CHUNK_SIZE = 1 * 1e6; // 1MB
|
||||
|
||||
export const GET = async ({ request, ...event }: APIEvent) => {
|
||||
export const GET = async ({ request, params }: APIEvent) => {
|
||||
"use server";
|
||||
|
||||
console.log('api endpoind called')
|
||||
|
||||
const res = await getStream(params.id);
|
||||
|
||||
return res;
|
||||
|
||||
const range = request.headers.get("range");
|
||||
|
||||
if (range === null) {
|
||||
|
@ -21,22 +28,20 @@ export const GET = async ({ request, ...event }: APIEvent) => {
|
|||
}
|
||||
|
||||
const videoSize = file.size;
|
||||
|
||||
const start = Number.parseInt(range.replace(/\D/g, ""));
|
||||
const end = Math.min(start + CHUNK_SIZE, videoSize - 1);
|
||||
const contentLength = end - start + 1;
|
||||
const end = Math.min(start + CHUNK_SIZE - 1, videoSize - 1);
|
||||
|
||||
return new Response(file.stream());
|
||||
console.log(`streaming slice(${start}, ${end})`);
|
||||
|
||||
// return new Response(video.slice(start, end).stream(), {
|
||||
// status: 206,
|
||||
// headers: {
|
||||
// 'Accept-Ranges': 'bytes',
|
||||
// 'Content-Range': `bytes ${start}-${end}/${videoSize}`,
|
||||
// 'Content-Length': `${contentLength}`,
|
||||
// 'Content-type': 'video/mp4',
|
||||
// },
|
||||
// });
|
||||
return new Response(file.slice(start, end), {
|
||||
status: 206,
|
||||
headers: {
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Range': `bytes ${start}-${end}/${videoSize}`,
|
||||
'Content-Length': `${end - start + 1}`,
|
||||
'Content-Type': 'video/mp4',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue