start video streaming

This commit is contained in:
Chris Kruining 2025-04-02 22:57:45 +02:00
parent 4b51fbc908
commit 445fde7b6b
Signed by: chris
SSH key fingerprint: SHA256:nG82MUfuVdRVyCKKWqhY+pCrbz9nbX6uzUns4RKa1Pg
15 changed files with 448 additions and 225 deletions

View file

@ -5,6 +5,7 @@
"name": "streamarr",
"dependencies": {
"@solid-primitives/context": "^0.3.0",
"@solid-primitives/event-listener": "^2.4.0",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.3",
"@solidjs/start": "^1.1.3",

View file

@ -15,6 +15,7 @@
},
"dependencies": {
"@solid-primitives/context": "^0.3.0",
"@solid-primitives/event-listener": "^2.4.0",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.3",
"@solidjs/start": "^1.1.3",

20
src/api/stream/video.ts Normal file
View file

@ -0,0 +1,20 @@
import { json } from "@solidjs/router";
import vid from "../../../public/videos/bbb_sunflower_2160p_60fps_normal.mp4";
import { APIEvent } from "@solidjs/start/server";
export const GET = async (event: APIEvent) => {
"use server";
console.log(event);
// async function* packetGenerator() {
// for (let i = 0; i < 10; i++) {
// yield `packet ${i}`;
// await new Promise((res) => setTimeout(res, 1000));
// }
// }
// console.log(vid);
return "OK";
};

View file

@ -10,7 +10,7 @@ export const ListItem: Component<{ entry: Entry }> = (props) => {
<main>
<strong>{props.entry.title}</strong>
<a href={`/content/${props.entry.id}`}>Watch now</a>
<a href={`/watch/${props.entry.id}`}>Watch now</a>
</main>
</div>
);

View file

@ -1,4 +1,10 @@
import { Component, createEffect, createSignal, Index, onMount } from "solid-js";
import {
Component,
createEffect,
createSignal,
Index,
onMount,
} from "solid-js";
import type { Entry, Category } from "../content";
import { ListItem } from "./list-item";
import { List } from "~/components/list";
@ -15,7 +21,11 @@ export const Overview: Component<OverviewProps> = (props) => {
onMount(() => {
new MutationObserver(() => {
container()?.querySelector(`.${css.list} > ul > div:nth-child(4) > main > a`)?.focus({ preventScroll: true });
container()
?.querySelector<HTMLElement>(
`.${css.list} > ul > div:nth-child(4) > main > a`,
)
?.focus({ preventScroll: true });
}).observe(document.body, { subtree: true, childList: true });
});
@ -25,11 +35,15 @@ export const Overview: Component<OverviewProps> = (props) => {
<Index each={props.categories}>
{(category) => (
<List class={css.list} label={category().label} items={category().entries}>
<List
class={css.list}
label={category().label}
items={category().entries}
>
{(entry) => <ListItem entry={entry()} />}
</List>
)}
</Index>
</div>
);
}
};

View file

@ -0,0 +1 @@
export { Player } from "./player";

View file

View file

@ -0,0 +1,161 @@
import {
createEventListenerMap,
makeEventListener,
makeEventListenerStack,
} from "@solid-primitives/event-listener";
import { createAsync, json, query } from "@solidjs/router";
import {
Component,
createEffect,
createMemo,
createResource,
createSignal,
onMount,
} from "solid-js";
import { isServer } from "solid-js/web";
const streamKaas = query(async () => {
"use server";
const stream = new WritableStream();
async function* packetGenerator() {
for (let i = 0; i < 10; i++) {
yield `packet ${i}`;
await new Promise((res) => setTimeout(res, 1000));
}
}
(async () => {
const writer = stream.getWriter();
try {
await writer.ready;
for await (const packet of packetGenerator()) {
writer.write(packet);
}
} finally {
writer.releaseLock();
}
// response.body.wr
})();
return new Response(packetGenerator());
}, "kaas");
interface PlayerProps {
id: string;
}
export const Player: Component<PlayerProps> = (props) => {
const [video, setVideo] = createSignal<HTMLVideoElement>();
const stream = createAsync(async () => {
const res = await streamKaas();
console.log(res);
return "";
});
// const [kaas, { refetch }] = createResource(async () => {
// if (isServer) {
// return "";
// }
// const response = await fetch("http://localhost:3000/api/stream/video", {
// method: "GET",
// });
// console.log(response.body);
// for await (const packet of response.body) {
// console.log(new TextDecoder().decode(packet));
// }
// return "";
// });
// onMount(() => refetch());
// createEffect(() => console.log(stream()));
// const progress = createMemo(() => {
// const
// });
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", e);
},
seeking(e) {
console.log("seeking", e);
},
stalled(e) {
console.log("stalled", e);
},
play(e) {
console.log("play", e);
},
playing(e) {
console.log("playing", e);
},
pause(e) {
console.log("pause", e);
},
suspend(e) {
console.log("suspend", e);
},
volumechange(e) {
console.log("volumechange", e);
},
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"]();
};
return (
<>
<h1>{props.id}</h1>
<video ref={setVideo} muted preload="metadata">
<source src="/videos/bbb_sunflower_2160p_60fps_normal.mp4" />
</video>
<button onclick={toggle}>play/pause</button>
<progress />
</>
);
};

View file

@ -0,0 +1,171 @@
.nav {
grid-area: 2 / 1 / 3 / 2;
display: block grid;
grid-auto-flow: row;
justify-content: space-between;
inline-size: 5em;
block-size: 100%;
padding: 1em;
background: inherit;
z-index: 0;
transition: z-index 0.3s step-end;
& > ul {
position: relative;
display: block grid;
grid-template-columns: 2.5rem auto;
align-content: center;
inline-size: 4rem;
gap: 1rem;
transform-origin: left center;
padding: 0;
padding-inline-start: 0.5rem;
margin: 0;
&::before {
content: "";
position: absolute;
inset-inline-start: 100%;
inset-block: 0;
inline-size: 20vw;
/* background:
radial-gradient(ellipse at left center 100% 100%, #f00, transparent),
linear-gradient(to right, #0003, transparent); */
background-image: linear-gradient(to right, #0003, transparent);
mask: radial-gradient(
ellipse 20vw 100% at left center,
black,
transparent
);
backdrop-filter: blur(5px);
opacity: 0;
transition: opacity 0.3s var(--ease-3);
}
& > a {
position: relative;
grid-column: span 2;
display: block grid;
grid-template-columns: subgrid;
align-items: center;
text-decoration: none;
transform-origin: center left;
transition:
transform 2s var(--ease-spring-5),
opacity 0.3s var(--ease-3);
color: var(--stone-4);
font-size: 2rem;
line-height: 1.5;
& > span {
opacity: 0;
transition: opacity 0.3s var(--ease-3);
text-shadow: 0 0 1em #000;
}
& > svg {
fill: var(--stone-4);
inline-size: 2.5rem;
block-size: 2.5rem;
}
&.active {
color: var(--yellow-4);
list-style: disc;
&::before {
content: "•";
position: absolute;
inset-inline-start: -1rem;
}
& > svg {
fill: var(--yellow-4);
}
}
}
&:has(a:is(:hover, :focus))::before {
opacity: 1;
}
&:has(a:is(:hover, :focus)) > a:not(:is(:hover, :focus)) {
opacity: 0.25;
}
&:has(a:is(:hover, :focus)) > a {
transform: scale(
max(1, calc(1.5 - (0.2 * abs(var(--target) - var(--sibling-index)))))
);
& > span {
opacity: 1;
}
}
&:has(a:is(:hover, :focus):nth-child(1)) {
--target: 1;
}
&:has(a:is(:hover, :focus):nth-child(2)) {
--target: 2;
}
&:has(a:is(:hover, :focus):nth-child(3)) {
--target: 3;
}
&:has(a:is(:hover, :focus):nth-child(4)) {
--target: 4;
}
&:has(a:is(:hover, :focus):nth-child(5)) {
--target: 5;
}
&:has(a:is(:hover, :focus):nth-child(6)) {
--target: 6;
}
&:has(a:is(:hover, :focus):nth-child(7)) {
--target: 7;
}
&:has(a:is(:hover, :focus):nth-child(8)) {
--target: 8;
}
&:has(a:is(:hover, :focus):nth-child(9)) {
--target: 9;
}
&:has(a:is(:hover, :focus):nth-child(10)) {
--target: 10;
}
&:has(a:is(:hover, :focus):nth-child(11)) {
--target: 11;
}
&:has(a:is(:hover, :focus):nth-child(12)) {
--target: 12;
}
&:has(a:is(:hover, :focus):nth-child(13)) {
--target: 13;
}
&:has(a:is(:hover, :focus):nth-child(14)) {
--target: 14;
}
&:has(a:is(:hover, :focus):nth-child(15)) {
--target: 15;
}
}
&:has(a:hover, :focus-within) {
z-index: 1;
transition: z-index 0.3s step-start;
}
}

View file

@ -0,0 +1,25 @@
import { A } from "@solidjs/router";
import { AiOutlineHome, AiOutlineStar, AiOutlineSearch } from "solid-icons/ai";
import { Component } from "solid-js";
import css from "./nav.module.css";
export const Nav: Component = (props) => {
return (
<nav class={css.nav}>
<ul>
<A href="/" activeClass={css.active} end={true}>
<AiOutlineHome />
<span>Home</span>
</A>
<A href="/library" activeClass={css.active}>
<AiOutlineStar />
<span>Library</span>
</A>
<A href="/search" activeClass={css.active}>
<AiOutlineSearch />
<span>Search</span>
</A>
</ul>
</nav>
);
};

View file

@ -3,27 +3,27 @@
display: grid;
grid: auto 1fr / 5em 1fr;
grid-template-areas:
'top top'
'nav content'
;
"top top"
"nav content";
inline-size: 100%;
block-size: 100%;
z-index: 0;
overflow: clip;
container-type: inline-size;
background-color: var(--surface-1);
/* &:has(.nav a:hover) > .body {
filter: blur(3px);
} */
}
.body {
grid-area: 2 / 1 / 3 / 3;
inline-size: 100%;
block-size: 100%;
background: linear-gradient(180deg, transparent, transparent 90vh, var(--surface-500) 90vh, var(--surface-500));
background: linear-gradient(
180deg,
transparent,
transparent 90vh,
var(--surface-500) 90vh,
var(--surface-500)
);
overflow: clip auto;
padding-inline-start: 5em;
transition: filter var(--duration-moderate-1) var(--ease-3);
@ -37,177 +37,3 @@
min-block-size: 100%;
}
}
.top {
grid-area: top;
display: block grid;
grid-auto-flow: column;
justify-content: end;
z-index: 1;
background-color: inherit;
padding: .5em;
}
.nav {
grid-area: nav;
display: block grid;
grid-auto-flow: row;
justify-content: space-between;
inline-size: 5em;
block-size: 100%;
padding: 1em;
background: inherit;
z-index: 0;
transition: z-index .3s step-end;
& > ul {
position: relative;
display: block grid;
grid-template-columns: 2.5rem auto;
align-content: center;
inline-size: 4rem;
gap: 1rem;
transform-origin: left center;
padding: 0;
padding-inline-start: .5rem;
margin: 0;
&::before {
content: '';
position: absolute;
inset-inline-start: 100%;
inset-block: 0;
inline-size: 20vw;
/* background:
radial-gradient(ellipse at left center 100% 100%, #f00, transparent),
linear-gradient(to right, #0003, transparent); */
background-image: linear-gradient(to right, #0003, transparent);
mask: radial-gradient(ellipse 20vw 100% at left center, black, transparent);
backdrop-filter: blur(5px);
opacity: 0;
transition: opacity .3s var(--ease-3);
}
& > a {
position: relative;
grid-column: span 2;
display: block grid;
grid-template-columns: subgrid;
align-items: center;
text-decoration: none;
transform-origin: center left;
transition: transform 2s var(--ease-spring-5), opacity 0.3s var(--ease-3);
color: var(--stone-4);
font-size: 2rem;
line-height: 1.5;
& > span {
opacity: 0;
transition: opacity .3s var(--ease-3);
text-shadow: 0 0 1em #000;
}
& > svg {
fill: var(--stone-4);
inline-size: 2.5rem;
block-size: 2.5rem;
}
&.active {
color: var(--yellow-4);
list-style: disc;
&::before {
content: '•';
position: absolute;
inset-inline-start: -1rem;
}
& > svg {
fill: var(--yellow-4);
}
}
}
&:has(a:is(:hover, :focus))::before {
opacity: 1;
}
&:has(a:is(:hover, :focus)) > a:not(:is(:hover, :focus)) {
opacity: .25;
}
&:has(a:is(:hover, :focus)) > a {
transform: scale(max(1, calc(1.5 - (.2 * abs(var(--target) - var(--sibling-index))))));
& > span {
opacity: 1;
}
}
&:has(a:is(:hover, :focus):nth-child(1)) {
--target: 1;
}
&:has(a:is(:hover, :focus):nth-child(2)) {
--target: 2;
}
&:has(a:is(:hover, :focus):nth-child(3)) {
--target: 3;
}
&:has(a:is(:hover, :focus):nth-child(4)) {
--target: 4;
}
&:has(a:is(:hover, :focus):nth-child(5)) {
--target: 5;
}
&:has(a:is(:hover, :focus):nth-child(6)) {
--target: 6;
}
&:has(a:is(:hover, :focus):nth-child(7)) {
--target: 7;
}
&:has(a:is(:hover, :focus):nth-child(8)) {
--target: 8;
}
&:has(a:is(:hover, :focus):nth-child(9)) {
--target: 9;
}
&:has(a:is(:hover, :focus):nth-child(10)) {
--target: 10;
}
&:has(a:is(:hover, :focus):nth-child(11)) {
--target: 11;
}
&:has(a:is(:hover, :focus):nth-child(12)) {
--target: 12;
}
&:has(a:is(:hover, :focus):nth-child(13)) {
--target: 13;
}
&:has(a:is(:hover, :focus):nth-child(14)) {
--target: 14;
}
&:has(a:is(:hover, :focus):nth-child(15)) {
--target: 15;
}
}
&:has(a:hover, :focus-within) {
z-index: 1;
transition: z-index .3s step-start;
}
}

View file

@ -1,11 +1,6 @@
import { A } from "@solidjs/router";
import {
AiOutlineHome,
AiOutlineStar,
AiOutlineSearch,
} from "solid-icons/ai";
import { ParentComponent, Component } from "solid-js";
import { ColorSchemePicker } from "../theme";
import { ParentComponent } from "solid-js";
import { Top } from "./top";
import { Nav } from "./nav";
import css from "./shell.module.css";
export const Shell: ParentComponent = (props) => {
@ -20,32 +15,3 @@ export const Shell: ParentComponent = (props) => {
</main>
);
};
const Top: Component = (props) => {
return (
<aside class={css.top}>
<ColorSchemePicker />
</aside>
);
};
const Nav: Component = (props) => {
return (
<nav class={css.nav}>
<ul>
<A href="/" activeClass={css.active} end={true}>
<AiOutlineHome />
<span>Home</span>
</A>
<A href="/library" activeClass={css.active}>
<AiOutlineStar />
<span>Library</span>
</A>
<A href="/search" activeClass={css.active}>
<AiOutlineSearch />
<span>Search</span>
</A>
</ul>
</nav>
);
};

View file

@ -0,0 +1,9 @@
.top {
grid-area: 1 / 1 / 2 / 3;
display: block grid;
grid-auto-flow: column;
justify-content: end;
z-index: 1;
background-color: inherit;
padding: 0.5em;
}

View file

@ -0,0 +1,11 @@
import { Component } from "solid-js";
import { ColorSchemePicker } from "../theme";
import css from "./top.module.css";
export const Top: Component = (props) => {
return (
<aside class={css.top}>
<ColorSchemePicker />
</aside>
);
};

View file

@ -0,0 +1,17 @@
import { Params, useParams } from "@solidjs/router";
import { createEffect } from "solid-js";
import { Player } from "~/features/player";
interface ItemParams extends Params {
item: string;
}
export default function Item() {
const params = useParams<ItemParams>();
return (
<>
<Player id={params.item} />
</>
);
}