woooot, got streaming via the jellyfin api working!

This commit is contained in:
Chris Kruining 2025-05-28 13:16:54 +02:00
parent 826a30f95f
commit d96f89d4b3
No known key found for this signature in database
GPG key ID: EB894A3560CCCAD2
21 changed files with 282 additions and 169 deletions

View file

@ -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=="],

View file

@ -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
View 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
View 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

View file

@ -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}`}

View file

@ -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>

View file

@ -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";

View file

@ -41,8 +41,6 @@ export const getEntry = query(
},
});
console.log(data);
if (data === undefined) {
return undefined;
}

View file

@ -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",
);

View file

@ -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,

View 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>
);
};

View file

@ -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);
}
}

View file

@ -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"

View 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>
);
};

View file

@ -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();

View file

@ -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;
}
}

View file

@ -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>
);
};

View file

@ -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 {

View file

@ -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>
</>
);
}

View file

@ -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>

View file

@ -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;
// }
};