lovely. got a couple of partial implementations....
update git ignore kaas remove large file syncy sync
This commit is contained in:
parent
89f526e9d9
commit
98cd4d630c
24 changed files with 586 additions and 76 deletions
39
src/features/player/SampleVideo_1280x720_10mb.captions.vtt
Normal file
39
src/features/player/SampleVideo_1280x720_10mb.captions.vtt
Normal 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
|
BIN
src/features/player/SampleVideo_1280x720_10mb.mp4
Normal file
BIN
src/features/player/SampleVideo_1280x720_10mb.mp4
Normal file
Binary file not shown.
54
src/features/player/SampleVideo_1280x720_10mb.thumbnails.vtt
Normal file
54
src/features/player/SampleVideo_1280x720_10mb.thumbnails.vtt
Normal 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
|
3
src/features/player/controls/volume.module.css
Normal file
3
src/features/player/controls/volume.module.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.container {
|
||||
display: block grid;
|
||||
}
|
17
src/features/player/controls/volume.tsx
Normal file
17
src/features/player/controls/volume.tsx
Normal 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>
|
||||
);
|
||||
};
|
BIN
src/features/player/overview.jpg
Normal file
BIN
src/features/player/overview.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 MiB |
|
@ -0,0 +1,5 @@
|
|||
.player {
|
||||
& > video::cue {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
|
@ -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(":");
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue