woooot, got streaming via the jellyfin api working!
This commit is contained in:
parent
826a30f95f
commit
d96f89d4b3
21 changed files with 282 additions and 169 deletions
11
bun.lock
11
bun.lock
|
@ -7,30 +7,31 @@
|
|||
"@solid-primitives/context": "^0.3.1",
|
||||
"@solid-primitives/deep": "^0.3.2",
|
||||
"@solid-primitives/event-listener": "^2.4.1",
|
||||
"@solid-primitives/fullscreen": "^1.3.1",
|
||||
"@solid-primitives/pagination": "^0.4.1",
|
||||
"@solid-primitives/scheduled": "^1.5.1",
|
||||
"@solidjs/meta": "^0.29.4",
|
||||
"@solidjs/router": "^0.15.3",
|
||||
"@solidjs/start": "^1.1.4",
|
||||
"better-auth": "^1.2.7",
|
||||
"better-auth": "^1.2.8",
|
||||
"bindings": "^1.5.0",
|
||||
"open-props": "^1.7.15",
|
||||
"openapi-fetch": "^0.13.8",
|
||||
"sitemap": "^8.0.0",
|
||||
"solid-icons": "^1.1.0",
|
||||
"solid-js": "^1.9.6",
|
||||
"solid-js": "^1.9.7",
|
||||
"vinxi": "^0.5.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"browserslist": "^4.24.5",
|
||||
"bun-types": "^1.2.13",
|
||||
"bun-types": "^1.2.14",
|
||||
"lightningcss": "^1.30.1",
|
||||
"openapi-typescript": "^7.8.0",
|
||||
"solid-devtools": "^0.33.0",
|
||||
"vite-plugin-solid-svg": "^0.8.1",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.1.3",
|
||||
"vitest": "^3.1.4",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -344,6 +345,8 @@
|
|||
|
||||
"@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.1", "", { "dependencies": { "@solid-primitives/utils": "^6.3.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Xc/lBCeuh9LwzR4lYbMDtopwWK7N9b4o+FmI4uoI8DOtVGYi0Ip20DG8PtwHk+g31lHgvwtFFVKfnUx2UaqZJg=="],
|
||||
|
||||
"@solid-primitives/fullscreen": ["@solid-primitives/fullscreen@1.3.1", "", { "dependencies": { "@solid-primitives/utils": "^6.3.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UMHIPekaNuBQk8YVuHQa28ExZnC7xdGLhBI7MTEY38pYcIQGcTMQ4B6zpZhEkRcoYudaFHSGDBLCcHlELV7f3g=="],
|
||||
|
||||
"@solid-primitives/keyboard": ["@solid-primitives/keyboard@1.3.0", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.0", "@solid-primitives/rootless": "^1.5.0", "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-0QX9O3eUaQorNNmXZn8a4efSByayIScVq+iGSwheD7m3SL/ACLM5oZlCNpTPLcemnVVfUPAHFiViEj86XpN5qw=="],
|
||||
|
||||
"@solid-primitives/media": ["@solid-primitives/media@2.3.0", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.0", "@solid-primitives/rootless": "^1.5.0", "@solid-primitives/static-store": "^0.1.0", "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-7+C3wfbWnGE/WPoNsqcp/EeOP2aNNB92RCpsWhBth8E5lZo/J+rK6jMb7umVsK0zguT8HBpeXp1pFyFbcsHStA=="],
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"@solid-primitives/context": "^0.3.1",
|
||||
"@solid-primitives/deep": "^0.3.2",
|
||||
"@solid-primitives/event-listener": "^2.4.1",
|
||||
"@solid-primitives/fullscreen": "^1.3.1",
|
||||
"@solid-primitives/pagination": "^0.4.1",
|
||||
"@solid-primitives/scheduled": "^1.5.1",
|
||||
"@solidjs/meta": "^0.29.4",
|
||||
|
|
3
public/favicon-dark.svg
Normal file
3
public/favicon-dark.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="white">
|
||||
<path d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM48 368l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16zm368-16c-8.8 0-16 7.2-16 16l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0zM48 240l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16zm368-16c-8.8 0-16 7.2-16 16l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0zM48 112l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16L64 96c-8.8 0-16 7.2-16 16zM416 96c-8.8 0-16 7.2-16 16l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0zM160 128l0 64c0 17.7 14.3 32 32 32l128 0c17.7 0 32-14.3 32-32l0-64c0-17.7-14.3-32-32-32L192 96c-17.7 0-32 14.3-32 32zm32 160c-17.7 0-32 14.3-32 32l0 64c0 17.7 14.3 32 32 32l128 0c17.7 0 32-14.3 32-32l0-64c0-17.7-14.3-32-32-32l-128 0z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
3
public/favicon-light.svg
Normal file
3
public/favicon-light.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="black">
|
||||
<path d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM48 368l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16zm368-16c-8.8 0-16 7.2-16 16l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0zM48 240l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16zm368-16c-8.8 0-16 7.2-16 16l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0zM48 112l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16L64 96c-8.8 0-16 7.2-16 16zM416 96c-8.8 0-16 7.2-16 16l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0zM160 128l0 64c0 17.7 14.3 32 32 32l128 0c17.7 0 32-14.3 32-32l0-64c0-17.7-14.3-32-32-32L192 96c-17.7 0-32 14.3-32 32zm32 160c-17.7 0-32 14.3-32 32l0 64c0 17.7 14.3 32 32 32l128 0c17.7 0 32-14.3 32-32l0-64c0-17.7-14.3-32-32-32l-128 0z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -21,10 +21,6 @@ export function Hero(props: HeroProps) {
|
|||
const Page: Component<{ entry: Entry }> = (props) => {
|
||||
const slug = createMemo(() => createSlug(props.entry));
|
||||
|
||||
createEffect(() => {
|
||||
// console.log(props.entry);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
class={`${css.page}`}
|
||||
|
|
|
@ -8,7 +8,16 @@ export default createHandler(() => (
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link
|
||||
rel="icon"
|
||||
href="/favicon-light.svg"
|
||||
media="(prefers-color-scheme:light)"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
href="/favicon-dark.svg"
|
||||
media="(prefers-color-scheme:dark)"
|
||||
/>
|
||||
{assets}
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -57,6 +57,41 @@ export const listUsers = query(async () => {
|
|||
return data ?? [];
|
||||
}, "jellyfin.listUsers");
|
||||
|
||||
export const listItemIds = query(
|
||||
async (): Promise<Record<string, { jellyfin: string }>> => {
|
||||
"use server";
|
||||
|
||||
const { data, error } = await getClient().GET("/Items", {
|
||||
params: {
|
||||
query: {
|
||||
hasImdbId: true,
|
||||
recursive: true,
|
||||
includeItemTypes: ["Movie", "Series"],
|
||||
fields: [
|
||||
"ProviderIds",
|
||||
"Genres",
|
||||
"DateLastMediaAdded",
|
||||
"DateCreated",
|
||||
"MediaSources",
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (data === undefined) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
data.Items?.map((item) => ([
|
||||
item.ProviderIds!["Tmdb"]!,
|
||||
{ jellyfin: item.Id! },
|
||||
])) ?? []
|
||||
);
|
||||
},
|
||||
"jellyfin.listItemIds",
|
||||
);
|
||||
|
||||
export const listItems = query(
|
||||
async (userId: string): Promise<Entry[] | undefined> => {
|
||||
"use server";
|
||||
|
@ -189,28 +224,28 @@ export const getItem = query(
|
|||
|
||||
|
||||
export const getItemStream = query(
|
||||
async (userId: string, itemId: string): Promise<string | undefined> => {
|
||||
async (itemId: string, range: string): Promise<Response> => {
|
||||
"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", {
|
||||
// I don't really know what is the big difference between mp4 and mkv.
|
||||
// But mkv is able to use ranges an can report the video's length, whereas mp4 doesn't
|
||||
const { response } = await getClient().GET("/Videos/{itemId}/stream", {
|
||||
params: {
|
||||
path: {
|
||||
itemId: item.providers.jellyfin,
|
||||
itemId,
|
||||
},
|
||||
query: {
|
||||
static: true,
|
||||
container: 'mkv',
|
||||
},
|
||||
},
|
||||
parseAs: 'stream',
|
||||
headers: {
|
||||
Range: range
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
return response;
|
||||
},
|
||||
"jellyfin.getItemStream",
|
||||
);
|
||||
|
@ -267,7 +302,7 @@ export const getItemPlaybackInfo = query(
|
|||
);
|
||||
|
||||
export const queryItems = query(async () => {
|
||||
"use server";
|
||||
"use server";
|
||||
|
||||
const { data, error } = await getClient().GET("/Items", {
|
||||
params: {
|
||||
|
@ -285,23 +320,6 @@ 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,8 +41,6 @@ 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, getItemStream, getRandomItems } from "./apis/jellyfin";
|
||||
import { listItemIds, getContinueWatching, getItemStream, getRandomItems } from "./apis/jellyfin";
|
||||
import {
|
||||
getDiscovery,
|
||||
getRecommendations,
|
||||
|
@ -12,9 +12,15 @@ import {
|
|||
|
||||
const jellyfinUserId = "a9c51af84bf54578a99ab4dd0ebf0763";
|
||||
|
||||
const lookupTable = query(async () => listItemIds(), 'content.lookupTable');
|
||||
|
||||
// export const getHighlights = () => getRandomItems(jellyfinUserId);
|
||||
export const getHighlights = () => getContinueWatching(jellyfinUserId);
|
||||
export const getStream = (id: string) => getItemStream(jellyfinUserId, id);
|
||||
export const getStream = query(async (id: string, range: string) => {
|
||||
const table = await lookupTable();
|
||||
|
||||
return getItemStream(table[id].jellyfin, range);
|
||||
}, 'content.stream');
|
||||
|
||||
export const listCategories = query(async (): Promise<Category[]> => {
|
||||
return [
|
||||
|
@ -31,7 +37,6 @@ export const listCategories = query(async (): Promise<Category[]> => {
|
|||
export const getEntry = query(
|
||||
async (id: Entry["id"]): Promise<Entry | undefined> => {
|
||||
return getTmdbEntry(id);
|
||||
// return getItem(jellyfinUserId, id);
|
||||
},
|
||||
"content.get",
|
||||
);
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
import { Accessor, createEffect, onMount, Setter } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { createEventListenerMap } from "@solid-primitives/event-listener";
|
||||
import { createFullscreen } from "@solid-primitives/fullscreen";
|
||||
|
||||
type State = "playing" | "paused";
|
||||
|
||||
|
@ -15,9 +16,14 @@ type Volume = {
|
|||
};
|
||||
|
||||
export interface VideoAPI {
|
||||
readonly fullscreen: Accessor<boolean>;
|
||||
readonly setFullscreen: Setter<boolean>;
|
||||
|
||||
readonly loading: Accessor<boolean>;
|
||||
readonly duration: Accessor<number>;
|
||||
readonly buffered: Accessor<number>;
|
||||
readonly currentTime: Accessor<number>;
|
||||
|
||||
setTime(time: number): void;
|
||||
|
||||
readonly state: {
|
||||
|
@ -37,10 +43,13 @@ export interface VideoAPI {
|
|||
}
|
||||
|
||||
interface VideoProviderProps extends ContextProviderProps {
|
||||
root: HTMLElement | undefined;
|
||||
video: HTMLVideoElement | undefined;
|
||||
}
|
||||
|
||||
interface VideoStore {
|
||||
fullscreen: boolean;
|
||||
loading: boolean;
|
||||
duration: number;
|
||||
buffered: number;
|
||||
currentTime: number;
|
||||
|
@ -55,6 +64,8 @@ export const [VideoProvider, useVideo] = createContextProvider<
|
|||
(props) => {
|
||||
const video = props.video;
|
||||
const [store, setStore] = createStore<VideoStore>({
|
||||
fullscreen: false,
|
||||
loading: true,
|
||||
duration: 0,
|
||||
buffered: 0,
|
||||
currentTime: 0,
|
||||
|
@ -65,7 +76,17 @@ export const [VideoProvider, useVideo] = createContextProvider<
|
|||
},
|
||||
});
|
||||
|
||||
const fullscreen = createFullscreen(
|
||||
() => props.root,
|
||||
() => store.fullscreen
|
||||
);
|
||||
|
||||
const api: VideoAPI = {
|
||||
fullscreen,
|
||||
setFullscreen: setStore.bind(null, "fullscreen"),
|
||||
|
||||
loading: () => store.loading,
|
||||
|
||||
duration: () => store.duration,
|
||||
buffered: () => store.buffered,
|
||||
currentTime: () => store.currentTime,
|
||||
|
@ -147,11 +168,22 @@ export const [VideoProvider, useVideo] = createContextProvider<
|
|||
timeRanges.length > 0 ? timeRanges.end(timeRanges.length - 1) : 0
|
||||
);
|
||||
},
|
||||
canplay() {
|
||||
setStore("loading", false);
|
||||
},
|
||||
waiting() {
|
||||
setStore("loading", true);
|
||||
},
|
||||
});
|
||||
|
||||
return api;
|
||||
},
|
||||
{
|
||||
fullscreen: () => false,
|
||||
setFullscreen() {},
|
||||
|
||||
loading: () => false,
|
||||
|
||||
duration: () => 0,
|
||||
buffered: () => 0,
|
||||
currentTime: () => 0,
|
||||
|
|
15
src/features/player/controls/fullscreen.tsx
Normal file
15
src/features/player/controls/fullscreen.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Component, Show } from "solid-js";
|
||||
import { useVideo } from "../context";
|
||||
import { FaSolidCompress, FaSolidExpand } from "solid-icons/fa";
|
||||
|
||||
export const Fullscreen: Component<{}> = (props) => {
|
||||
const video = useVideo();
|
||||
|
||||
return (
|
||||
<button onclick={(e) => video.setFullscreen((last) => !last)}>
|
||||
<Show when={video.fullscreen()} fallback={<FaSolidExpand />}>
|
||||
<FaSolidCompress />
|
||||
</Show>
|
||||
</button>
|
||||
);
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
.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,14 +1,12 @@
|
|||
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();
|
||||
|
||||
return (
|
||||
<button
|
||||
class={css.play}
|
||||
onclick={(e) =>
|
||||
video.state.setState((last) =>
|
||||
last === "playing" ? "paused" : "playing"
|
||||
|
|
19
src/features/player/controls/settings.tsx
Normal file
19
src/features/player/controls/settings.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Component } from "solid-js";
|
||||
import { useVideo } from "../context";
|
||||
import { FaSolidEllipsisVertical } from "solid-icons/fa";
|
||||
|
||||
export const Settings: Component<{}> = (props) => {
|
||||
const video = useVideo();
|
||||
|
||||
return (
|
||||
<button
|
||||
onclick={(e) =>
|
||||
video.state.setState((last) =>
|
||||
last === "playing" ? "paused" : "playing"
|
||||
)
|
||||
}
|
||||
>
|
||||
<FaSolidEllipsisVertical />
|
||||
</button>
|
||||
);
|
||||
};
|
|
@ -1,13 +1,9 @@
|
|||
import { Component, Show } from "solid-js";
|
||||
import css from "./volume.module.css";
|
||||
import { useVideo } from "../context";
|
||||
import { FaSolidVolumeOff, FaSolidVolumeXmark } from "solid-icons/fa";
|
||||
import css from "./volume.module.css";
|
||||
|
||||
interface VolumeProps {
|
||||
value: number;
|
||||
muted?: boolean;
|
||||
onInput?: (next: { volume: number; muted: boolean }) => any;
|
||||
}
|
||||
interface VolumeProps {}
|
||||
|
||||
export const Volume: Component<VolumeProps> = (props) => {
|
||||
const video = useVideo();
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
block-size: max-content;
|
||||
|
||||
background-color: black;
|
||||
color: var(--gray-3);
|
||||
|
||||
& > video::cue {
|
||||
font-size: 1.5rem;
|
||||
|
@ -25,8 +26,8 @@
|
|||
|
||||
& > figcaption {
|
||||
grid-area: 1 / 1;
|
||||
display: block flex;
|
||||
flex-flow: row wrap;
|
||||
display: block grid;
|
||||
grid: auto 1fr auto / 100%;
|
||||
position: absolute;
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
|
@ -36,12 +37,63 @@
|
|||
gap: var(--size-2);
|
||||
padding: var(--size-2);
|
||||
|
||||
& > * {
|
||||
flex: 0 0 auto;
|
||||
& > header {
|
||||
display: block grid;
|
||||
place-items: center;
|
||||
|
||||
& > h1 {
|
||||
text-align: center;
|
||||
font-size: 8rem;
|
||||
text-shadow: 0 0 .5rem #000;
|
||||
}
|
||||
}
|
||||
|
||||
& > :nth-child(1) {
|
||||
inline-size: 100%;
|
||||
& > section {
|
||||
display: block grid;
|
||||
place-items: center;
|
||||
|
||||
& > svg {
|
||||
font-size: 10rem;
|
||||
animation: spin 1s infinite steps(8) normal;
|
||||
filter: drop-shadow(0 0 .5rem #000);
|
||||
}
|
||||
}
|
||||
|
||||
& > footer {
|
||||
display: block grid;
|
||||
grid: auto auto / auto auto auto;
|
||||
place-content: space-between;
|
||||
gap: var(--size-2);
|
||||
|
||||
& > :nth-child(1) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
& > section {
|
||||
& > 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
rotate: 0deg;
|
||||
}
|
||||
|
||||
100% {
|
||||
rotate: 360deg;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
createMemo,
|
||||
createSignal,
|
||||
on,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import css from "./player.module.css";
|
||||
import { Volume } from "./controls/volume";
|
||||
|
@ -16,8 +17,11 @@ 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";
|
||||
import { useVideo, VideoProvider } from "./context";
|
||||
import { SeekBar } from "./controls/seekBar";
|
||||
import { Fullscreen } from "./controls/fullscreen";
|
||||
import { Settings } from "./controls/settings";
|
||||
import { FaSolidCompress, FaSolidExpand, FaSolidSpinner } from "solid-icons/fa";
|
||||
|
||||
const metadata = query(async (id: string) => {
|
||||
"use server";
|
||||
|
@ -51,6 +55,9 @@ interface PlayerProps {
|
|||
}
|
||||
|
||||
export const Player: Component<PlayerProps> = (props) => {
|
||||
const [player, setPlayer] = createSignal<HTMLElement>(
|
||||
undefined as unknown as HTMLElement
|
||||
);
|
||||
const [video, setVideo] = createSignal<HTMLVideoElement>(
|
||||
undefined as unknown as HTMLVideoElement
|
||||
);
|
||||
|
@ -74,64 +81,15 @@ 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.
|
||||
})
|
||||
);
|
||||
|
||||
createEventListenerMap(() => video()!, {
|
||||
durationchange(e) {
|
||||
// console.log("durationchange", e);
|
||||
},
|
||||
loadeddata(e) {
|
||||
// console.log("loadeddata", e);
|
||||
},
|
||||
loadedmetadata(e) {
|
||||
// console.log("loadedmetadata", e);
|
||||
},
|
||||
ratechange(e) {
|
||||
// console.log("ratechange", e);
|
||||
},
|
||||
seeked(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
|
||||
// );
|
||||
},
|
||||
stalled(e) {
|
||||
// console.log(
|
||||
// "stalled (meaning downloading data failed)",
|
||||
// e,
|
||||
// video()!.error,
|
||||
// );
|
||||
},
|
||||
// suspend(e) {
|
||||
// console.log("suspend", e);
|
||||
// },
|
||||
// canplay(e) {
|
||||
// console.log("canplay", e);
|
||||
// },
|
||||
|
||||
waiting(e) {
|
||||
// console.log("waiting", e);
|
||||
},
|
||||
});
|
||||
createEffect(on(thumbnails, (thumbnails) => {}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<figure class={css.player}>
|
||||
<figure ref={setPlayer} class={css.player}>
|
||||
{/* <h1>{props.entry.title}</h1> */}
|
||||
|
||||
<video
|
||||
ref={setVideo}
|
||||
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}
|
||||
|
@ -153,20 +111,41 @@ export const Player: Component<PlayerProps> = (props) => {
|
|||
</video>
|
||||
|
||||
<figcaption>
|
||||
<VideoProvider video={video()}>
|
||||
<SeekBar />
|
||||
<PlayState />
|
||||
<Volume
|
||||
value={video()?.volume ?? 0}
|
||||
muted={video()?.muted ?? false}
|
||||
onInput={({ volume, muted }) => {
|
||||
video().volume = volume;
|
||||
video().muted = muted;
|
||||
}}
|
||||
/>
|
||||
<VideoProvider root={player()} video={video()}>
|
||||
<header>
|
||||
<h1>{props.entry.title}</h1>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<Loader />
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<SeekBar />
|
||||
<section>
|
||||
<Volume />
|
||||
</section>
|
||||
<section>
|
||||
<PlayState />
|
||||
</section>
|
||||
<section>
|
||||
<Fullscreen />
|
||||
<Settings />
|
||||
</section>
|
||||
</footer>
|
||||
</VideoProvider>
|
||||
</figcaption>
|
||||
</figure>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Loader: Component = () => {
|
||||
const video = useVideo();
|
||||
|
||||
return (
|
||||
<Show when={video.loading()}>
|
||||
<FaSolidSpinner />
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -39,7 +39,6 @@
|
|||
overflow: clip auto;
|
||||
padding-inline-start: 5em;
|
||||
transition: filter var(--duration-moderate-1) var(--ease-3);
|
||||
contain: layout style paint;
|
||||
container-type: size;
|
||||
|
||||
& > div {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { Title } from "@solidjs/meta";
|
||||
import {
|
||||
createAsync,
|
||||
json,
|
||||
|
@ -51,9 +52,10 @@ export default function Item() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Show when={entry()} fallback="Some kind of pretty 404 page I guess">{
|
||||
entry => <Details entry={entry()} />
|
||||
}</Show>
|
||||
<Title>{entry()?.title}</Title>
|
||||
<Show when={entry()} fallback="Some kind of pretty 404 page I guess">
|
||||
{(entry) => <Details entry={entry()} />}
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { Show } from "solid-js";
|
|||
import { createSlug, getEntry } from "~/features/content";
|
||||
import { Player } from "~/features/player";
|
||||
import css from "./slug.module.css";
|
||||
import { Title } from "@solidjs/meta";
|
||||
|
||||
const healUrl = query(async (slug: string) => {
|
||||
const entry = await getEntry(slug.slice(slug.lastIndexOf("-") + 1));
|
||||
|
@ -54,6 +55,7 @@ export default function Item() {
|
|||
|
||||
return (
|
||||
<div class={css.page}>
|
||||
<Title>{entry()?.title}</Title>
|
||||
<Show when={entry()} fallback="Some kind of pretty 404 page I guess">
|
||||
{(entry) => <Player entry={entry()} />}
|
||||
</Show>
|
||||
|
|
|
@ -1,50 +1,46 @@
|
|||
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, 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) {
|
||||
return new Response("Requires Range header", { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const file = Bun.file(
|
||||
import.meta.dirname + "/SampleVideo_1280x720_10mb.mp4",
|
||||
);
|
||||
return getStream(params.id, range);
|
||||
|
||||
if ((await file.exists()) !== true) {
|
||||
return new Response("File not found", { status: 404 });
|
||||
}
|
||||
// try {
|
||||
// const file = Bun.file(
|
||||
// import.meta.dirname + "/SampleVideo_1280x720_10mb.mp4",
|
||||
// );
|
||||
|
||||
const videoSize = file.size;
|
||||
const start = Number.parseInt(range.replace(/\D/g, ""));
|
||||
const end = Math.min(start + CHUNK_SIZE - 1, videoSize - 1);
|
||||
// if ((await file.exists()) !== true) {
|
||||
// return new Response("File not found", { status: 404 });
|
||||
// }
|
||||
|
||||
console.log(`streaming slice(${start}, ${end})`);
|
||||
// const videoSize = file.size;
|
||||
// const start = Number.parseInt(range.replace(/\D/g, ""));
|
||||
// const end = Math.min(start + CHUNK_SIZE - 1, videoSize - 1);
|
||||
|
||||
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);
|
||||
// console.log(`streaming slice(${start}, ${end})`);
|
||||
|
||||
throw e;
|
||||
}
|
||||
// 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);
|
||||
|
||||
// throw e;
|
||||
// }
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue