lovely. got a couple of partial implementations....

update git ignore

kaas

remove large file

syncy sync
This commit is contained in:
Chris Kruining 2025-04-03 17:27:35 +02:00 committed by Chris Kruining
parent 89f526e9d9
commit 98cd4d630c
Signed by: chris
SSH key fingerprint: SHA256:nG82MUfuVdRVyCKKWqhY+pCrbz9nbX6uzUns4RKa1Pg
24 changed files with 586 additions and 76 deletions

View file

@ -0,0 +1,39 @@
WEBVTT
00:02.170 --> 00:04.136
Emo, close your eyes
00:04.136 --> 00:05.597
Why?
NOW!
00:05.597 --> 00:07.405
Ok
00:07.405 --> 00:08.803
Good
00:08.803 --> 00:11.541
What do you see at your left side Emo?
00:11.541 --> 00:13.287
Well?
00:13.287 --> 00:16.110
Er nothing?
Really?
00:16.110 --> 00:18.514
No, nothing at all!
00:18.514 --> 00:22.669
Really? and at your right? What do you see at your right side Emo?
00:22.669 --> 00:26.111
Umm, the same Proog
00:26.111 --> 00:28.646
Exactly the same! Nothing!
00:28.646 --> 00:30.794
Great

Binary file not shown.

View file

@ -0,0 +1,54 @@
VTT
1
00:00:00.000 --> 00:00:01.000
overview.jpg#xywh=0,0,320,180
2
00:00:01.000 --> 00:00:02.000
overview.jpg#xywh=320,0,320,180
3
00:00:02.000 --> 00:00:03.000
overview.jpg#xywh=640,0,320,180
00:00:03.000 --> 00:00:04.000
overview.jpg#xywh=960,0,320,180
00:00:04.000 --> 00:00:05.000
overview.jpg#xywh=1280,0,320,180
00:00:05.000 --> 00:00:06.000
overview.jpg#xywh=1600,0,320,180
00:00:06.000 --> 00:00:07.000
overview.jpg#xywh=1920,0,320,180
00:00:07.000 --> 00:00:08.000
overview.jpg#xywh=2240,0,320,180
00:00:08.000 --> 00:00:09.000
overview.jpg#xywh=0,180,320,180
00:00:09.000 --> 00:00:10.000
overview.jpg#xywh=320,180,320,180
00:00:10.000 --> 00:00:11.000
overview.jpg#xywh=640,180,320,180
00:00:11.000 --> 00:00:12.000
overview.jpg#xywh=960,180,320,180
00:00:12.000 --> 00:00:13.000
overview.jpg#xywh=1280,180,320,180
00:00:13.000 --> 00:00:14.000
overview.jpg#xywh=1600,180,320,180
00:00:14.000 --> 00:00:15.000
overview.jpg#xywh=1920,180,320,180
00:00:15.000 --> 00:00:16.000
overview.jpg#xywh=2240,180,320,180

View file

@ -0,0 +1,3 @@
.container {
display: block grid;
}

View file

@ -0,0 +1,17 @@
import { Component, createSignal } from "solid-js";
import css from "./volume.module.css";
interface VolumeProps {
value: number;
}
export const Volume: Component<VolumeProps> = (props) => {
const [volume, setVolume] = createSignal(props.value);
return (
<div class={css.container}>
<button>mute</button>
<input type="range" value={volume()} min="0" max="1" step="0.01" />
</div>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -0,0 +1,5 @@
.player {
& > video::cue {
font-size: 1.5rem;
}
}

View file

@ -12,22 +12,90 @@ import {
onMount,
untrack,
} from "solid-js";
import css from "./player.module.css";
import { Volume } from "./controls/volume";
const metadata = query(async (id: string) => {
"use server";
// thumbnail sprite image created with
// ```bash
// mkdir -p thumbs \
// && ffmpeg -i SampleVideo_1280x720_10mb.mp4 -r 1 -s 320x180 -f image2 thumbs/thumb-%d.jpg \
// && montage thumbs/*.jpg -geometry 320x180 -tile 8x overview.jpg \
// && rm -rf thumbs
// ```
//
// 1. create thumbs directory
// 2. create image every 1 second
// 3. create sprite from images
// 4. remove thumbs
const path = `${import.meta.dirname}/SampleVideo_1280x720_10mb`;
return json({
captions: await Bun.file(`${path}.captions.vtt`).bytes(),
thumbnails: {
track: await Bun.file(`${path}.thumbnails.vtt`).text(),
image: await Bun.file(`${import.meta.dirname}/overview.jpg`).bytes(),
},
});
}, "player.metadata");
interface PlayerProps {
id: string;
}
export const Player: Component<PlayerProps> = (props) => {
const [video, setVideo] = createSignal<HTMLVideoElement>(undefined as unknown as HTMLVideoElement);
const [video, setVideo] = createSignal<HTMLVideoElement>(
undefined as unknown as HTMLVideoElement,
);
const data = createAsync(() => metadata(props.id), {
deferStream: true,
initialValue: {},
});
const captionUrl = createMemo(() => {
const { captions } = data();
const onDurationChange = createEventSignal(video, 'durationchange');
const onTimeUpdate = createEventSignal(video, 'timeupdate');
return captions !== undefined
? URL.createObjectURL(new Blob([captions], { type: "text/vtt" }))
: "";
});
const thumbnails = createMemo(() => {
const { thumbnails } = data();
return thumbnails !== undefined
? URL.createObjectURL(new Blob([thumbnails.track], { type: "text/vtt" }))
: "";
});
createEffect(() => {
const metadata = data();
const el = video();
if (metadata === undefined || el === undefined) {
return;
}
console.log(metadata);
});
createEffect(() => {
thumbnails();
console.log(video()!.textTracks.getTrackById("thumbnails")?.cues);
// const captions = el.addTextTrack("captions", "English", "en");
// captions.
});
const onDurationChange = createEventSignal(video, "durationchange");
const onTimeUpdate = createEventSignal(video, "timeupdate");
const duration = createMemo(() => {
onDurationChange();
onTimeUpdate();
return video()?.duration ?? 100;
return video()?.duration ?? 0;
});
const currentTime = createMemo(() => {
@ -36,10 +104,6 @@ export const Player: Component<PlayerProps> = (props) => {
return video()?.currentTime ?? 0;
});
createEffect(() => {
console.log(duration(), currentTime());
});
createEventListenerMap(() => video()!, {
durationchange(e) {
console.log("durationchange", e);
@ -60,7 +124,11 @@ export const Player: Component<PlayerProps> = (props) => {
console.log("seeking", e);
},
stalled(e) {
console.log("stalled (meaning downloading data failed)", e, video()!.error);
console.log(
"stalled (meaning downloading data failed)",
e,
video()!.error,
);
},
play(e) {
@ -107,17 +175,52 @@ export const Player: Component<PlayerProps> = (props) => {
};
return (
<>
<figure class={css.player}>
<h1>{props.id}</h1>
<video ref={setVideo} width="1280px" height="720px" muted src="/api/stream/video" />
<video
ref={setVideo}
muted
autoplay
controls
src={`/api/content/stream?id=${props.id}`}
lang="en"
>
<track
default
kind="captions"
label="English"
srclang="en"
src={captionUrl()}
/>
<track default kind="chapters" src={thumbnails()} id="thumbnails" />
{/* <track kind="captions" />
<track kind="chapters" />
<track kind="descriptions" />
<track kind="metadata" />
<track kind="subtitles" /> */}
</video>
<figcaption>
<Volume value={0.5} />
</figcaption>
<button onclick={toggle}>play/pause</button>
<span style={{ '--duration': duration(), '--current-time': currentTime() }} />
<span data-duration={duration()} data-current-time={currentTime()} />
<progress max={duration()} value={currentTime()} />
</>
<span>
{formatTime(currentTime())} / {formatTime(duration())}
</span>
<progress max={duration().toFixed(0)} value={currentTime().toFixed(0)} />
</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(":");
};