From 445fde7b6bb9ee6a1b33cf9a9475be556ab23a78 Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Wed, 2 Apr 2025 22:57:45 +0200 Subject: [PATCH] start video streaming --- bun.lock | 1 + package.json | 1 + src/api/stream/video.ts | 20 +++ src/features/overview/list-item.tsx | 2 +- src/features/overview/overview.tsx | 22 ++- src/features/player/index.ts | 1 + src/features/player/player.module.css | 0 src/features/player/player.tsx | 161 +++++++++++++++++++++ src/features/shell/nav.module.css | 171 +++++++++++++++++++++++ src/features/shell/nav.tsx | 25 ++++ src/features/shell/shell.module.css | 192 ++------------------------ src/features/shell/shell.tsx | 40 +----- src/features/shell/top.module.css | 9 ++ src/features/shell/top.tsx | 11 ++ src/routes/(shell)/watch/:item.tsx | 17 +++ 15 files changed, 448 insertions(+), 225 deletions(-) create mode 100644 src/api/stream/video.ts create mode 100644 src/features/player/index.ts create mode 100644 src/features/player/player.module.css create mode 100644 src/features/player/player.tsx create mode 100644 src/features/shell/nav.module.css create mode 100644 src/features/shell/nav.tsx create mode 100644 src/features/shell/top.module.css create mode 100644 src/features/shell/top.tsx create mode 100644 src/routes/(shell)/watch/:item.tsx diff --git a/bun.lock b/bun.lock index fba0103..93e2cbe 100644 --- a/bun.lock +++ b/bun.lock @@ -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", diff --git a/package.json b/package.json index f2690ef..2c9590d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/api/stream/video.ts b/src/api/stream/video.ts new file mode 100644 index 0000000..c9b98ae --- /dev/null +++ b/src/api/stream/video.ts @@ -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"; +}; diff --git a/src/features/overview/list-item.tsx b/src/features/overview/list-item.tsx index 218558e..8c196b8 100644 --- a/src/features/overview/list-item.tsx +++ b/src/features/overview/list-item.tsx @@ -10,7 +10,7 @@ export const ListItem: Component<{ entry: Entry }> = (props) => {
{props.entry.title} - Watch now + Watch now
); diff --git a/src/features/overview/overview.tsx b/src/features/overview/overview.tsx index efc8919..dd05c24 100644 --- a/src/features/overview/overview.tsx +++ b/src/features/overview/overview.tsx @@ -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 = (props) => { onMount(() => { new MutationObserver(() => { - container()?.querySelector(`.${css.list} > ul > div:nth-child(4) > main > a`)?.focus({ preventScroll: true }); + container() + ?.querySelector( + `.${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 = (props) => { {(category) => ( - + {(entry) => } )} ); -} +}; diff --git a/src/features/player/index.ts b/src/features/player/index.ts new file mode 100644 index 0000000..1372f50 --- /dev/null +++ b/src/features/player/index.ts @@ -0,0 +1 @@ +export { Player } from "./player"; diff --git a/src/features/player/player.module.css b/src/features/player/player.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/features/player/player.tsx b/src/features/player/player.tsx new file mode 100644 index 0000000..23c3d14 --- /dev/null +++ b/src/features/player/player.tsx @@ -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 = (props) => { + const [video, setVideo] = createSignal(); + 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 ( + <> +

{props.id}

+ + + + + + + + ); +}; diff --git a/src/features/shell/nav.module.css b/src/features/shell/nav.module.css new file mode 100644 index 0000000..cbcdb85 --- /dev/null +++ b/src/features/shell/nav.module.css @@ -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; + } +} diff --git a/src/features/shell/nav.tsx b/src/features/shell/nav.tsx new file mode 100644 index 0000000..1b48c46 --- /dev/null +++ b/src/features/shell/nav.tsx @@ -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 ( + + ); +}; diff --git a/src/features/shell/shell.module.css b/src/features/shell/shell.module.css index 0641077..e85cbdd 100644 --- a/src/features/shell/shell.module.css +++ b/src/features/shell/shell.module.css @@ -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; - } -} \ No newline at end of file diff --git a/src/features/shell/shell.tsx b/src/features/shell/shell.tsx index 0209a4c..ea2b48b 100644 --- a/src/features/shell/shell.tsx +++ b/src/features/shell/shell.tsx @@ -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) => { ); }; - -const Top: Component = (props) => { - return ( - - ); -}; - -const Nav: Component = (props) => { - return ( - - ); -}; diff --git a/src/features/shell/top.module.css b/src/features/shell/top.module.css new file mode 100644 index 0000000..cc33f93 --- /dev/null +++ b/src/features/shell/top.module.css @@ -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; +} diff --git a/src/features/shell/top.tsx b/src/features/shell/top.tsx new file mode 100644 index 0000000..2b9a335 --- /dev/null +++ b/src/features/shell/top.tsx @@ -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 ( + + ); +}; diff --git a/src/routes/(shell)/watch/:item.tsx b/src/routes/(shell)/watch/:item.tsx new file mode 100644 index 0000000..83d93ce --- /dev/null +++ b/src/routes/(shell)/watch/:item.tsx @@ -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(); + + return ( + <> + + + ); +}