aboutsummaryrefslogtreecommitdiff
path: root/web/script/player
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2023-10-01 18:36:32 +0200
committermetamuffin <metamuffin@disroot.org>2023-10-01 18:36:32 +0200
commit86d21ecd6dd39e3fa73cbed3ce347d6cf51239b5 (patch)
tree33f81cd77fbc6f63f158f7bd047461a5950b65b5 /web/script/player
parent3c1c3325ecebd6f5c7bc33a107c6efc4cda8b3ea (diff)
downloadjellything-86d21ecd6dd39e3fa73cbed3ce347d6cf51239b5.tar
jellything-86d21ecd6dd39e3fa73cbed3ce347d6cf51239b5.tar.bz2
jellything-86d21ecd6dd39e3fa73cbed3ce347d6cf51239b5.tar.zst
jlhs forward playback works
Diffstat (limited to 'web/script/player')
-rw-r--r--web/script/player/mod.ts127
1 files changed, 92 insertions, 35 deletions
diff --git a/web/script/player/mod.ts b/web/script/player/mod.ts
index 6a73f5f..0ac1b90 100644
--- a/web/script/player/mod.ts
+++ b/web/script/player/mod.ts
@@ -1,12 +1,13 @@
import { OVar, e } from "../jshelper/mod.ts";
+export interface Range { start: number, end: number }
export interface JhlsMetadata {
tracks: JhlsTrack[],
duration: number,
}
export interface JhlsTrack {
info: SourceTrack,
- segments: { start: number, end: number }[],
+ segments: Range[],
}
export interface SourceTrack {
kind: SourceTrackKind,
@@ -28,7 +29,7 @@ export interface SourceTrackKind {
subtitles?: boolean,
}
-
+const TARGET_BUFFER_DURATION = 30
document.addEventListener("DOMContentLoaded", () => {
if (document.body.classList.contains("player")) {
@@ -46,6 +47,7 @@ function initialize_player(el: HTMLElement, node_id: string) {
const player = new Player(node_id)
const toggle_playing = () => player.playing.value ? player.pause() : player.play()
+ const pri_map = (v: number) => (v / player.duration.value * 100) + "%"
let pri_current: HTMLElement;
let pri: HTMLElement;
@@ -55,7 +57,19 @@ function initialize_player(el: HTMLElement, node_id: string) {
player.position.map(v => e("span", display_time(v))), e("br"),
player.position.map(v => e("span", display_time(v - player.duration.value)))
),
- pri = e("div", { class: "jsp-pri" }, pri_current = e("div", { class: "jsp-pri-current" })),
+ pri = e("div", { class: "jsp-pri" },
+ pri_current = e("div", { class: "jsp-pri-current" }),
+ player.buffered.map(
+ ranges => e("div", ...ranges.map(
+ r => e("div", {
+ class: "jsp-pri-buffered", style: {
+ width: pri_map(r.end - r.start),
+ left: pri_map(r.start)
+ }
+ })
+ ))
+ )
+ ),
e("button", "X", {
onclick() {
if (document.fullscreenElement) document.exitFullscreen()
@@ -64,7 +78,7 @@ function initialize_player(el: HTMLElement, node_id: string) {
})
)
- player.position.onchangeinit(p => pri_current.style.width = (p / player.duration.value * 100) + "%")
+ player.position.onchangeinit(p => pri_current.style.width = pri_map(p))
const pel = e("div", { class: "jsp" },
player.video,
@@ -124,20 +138,27 @@ function display_time(t: number): string {
return (h ? h + "h" : "") + (m ? m + "m" : "") + (s ? s + "s" : "")
}
+interface BufferRange extends Range { status: "buffered" | "downloading" | "queued" }
class Player {
public video = e("video")
private media_source = new MediaSource();
+ public tracks: PlayerTrack[] = [];
public position = new OVar(0)
public duration = new OVar(1)
public playing = new OVar(false)
+ public buffered = new OVar<BufferRange[]>([])
public buffering_status = new OVar<string | undefined>(undefined)
public error = new OVar<string | undefined>(undefined)
constructor(private node_id: string) {
this.video.onloadedmetadata = () => { }
this.video.ondurationchange = () => { }
- this.video.ontimeupdate = () => this.position.value = this.video.currentTime
+ this.video.ontimeupdate = () => {
+ this.position.value = this.video.currentTime
+ this.update() // TODO maybe not here
+ this.update_buffered_ranges()
+ }
this.video.onplay = () => {
this.buffering_status.value = undefined;
}
@@ -150,50 +171,39 @@ class Player {
this.video.onpause = () => {
this.playing.value = false
}
-
this.fetch_meta()
}
+ update_buffered_ranges() {
+ const o: BufferRange[] = []
+ for (let i = 0; i < this.video.buffered.length; i++) {
+ o.push({
+ start: this.video.buffered.start(i),
+ end: this.video.buffered.end(i),
+ status: "buffered"
+ })
+ }
+ this.buffered.value = o;
+ }
+
async fetch_meta() {
this.buffering_status.value = "Loading JHLS metadata..."
const res = await fetch(`/n/${encodeURIComponent(this.node_id)}/stream?format=jhls`)
if (!res.ok) return this.error.value = "Cannot download JHLS metadata"
const metadata = await res.json() as JhlsMetadata
- this.buffering_status.value = "Fetching initial segments..."
+ this.buffering_status.value = undefined
this.duration.value = metadata.duration
this.video.src = URL.createObjectURL(this.media_source)
this.media_source.addEventListener("sourceopen", () => {
- console.log("sourceopen");
- this.segment(metadata)
+ this.tracks.push(new PlayerTrack(this.media_source, this.node_id, 0, metadata.tracks[0]))
+ this.tracks.push(new PlayerTrack(this.media_source, this.node_id, 1, metadata.tracks[1]))
+ this.buffering_status.value = "Fetching initial segments..."
+ this.update()
})
-
}
- async segment(metadata: JhlsMetadata) {
- const srcbuf = this.media_source.addSourceBuffer("video/webm")
- srcbuf.mode = "segments"
- srcbuf.addEventListener("updatestart", () => {
- console.log("updatestart");
- })
- srcbuf.addEventListener("updateend", () => {
- console.log("updateend");
- })
- srcbuf.addEventListener("update", () => {
- console.log("update");
- })
- srcbuf.addEventListener("error", () => {
- console.log("error");
- })
- let i = 0;
- for (const seg of metadata.tracks[0].segments) {
- if (i++ > 5) break
- const res = await fetch(`/n/${encodeURIComponent(this.node_id)}/stream?format=hlsseg&index=${i}&tracks=0`)
- if (!res.ok) return this.error.value = "Cannot download media segment."
- const segbuf = await res.arrayBuffer()
- console.log("aaa");
- srcbuf.timestampOffset = seg.start
- srcbuf.appendBuffer(segbuf)
- }
+ update() {
+ this.tracks.forEach(t => t.update(this.video.currentTime))
}
play() {
@@ -207,3 +217,50 @@ class Player {
seek(_p: number) { }
}
+
+class PlayerTrack {
+ private source_buffer: SourceBuffer
+ private loaded_or_loading = new Set<number>()
+ private append_queue: (Range & { buf: ArrayBuffer })[] = []
+ constructor(media_source: MediaSource, private node_id: string, private track_index: number, private metadata: JhlsTrack) {
+ this.source_buffer = media_source.addSourceBuffer("video/webm")
+ this.source_buffer.mode = "segments"
+ this.source_buffer.addEventListener("updateend", () => {
+ console.log("updateend");
+
+ this.tick_append()
+ })
+ }
+ update(target: number) {
+ console.log("update", target, this.loaded_or_loading);
+
+ for (let i = 0; i < this.metadata.segments.length; i++) {
+ const segment = this.metadata.segments[i];
+ if (segment.start >= target - 1 && segment.end < target + TARGET_BUFFER_DURATION) {
+ if (!this.loaded_or_loading.has(i)) {
+ console.log(segment);
+ this.loaded_or_loading.add(i)
+ this.load(i)
+ }
+ }
+ }
+ }
+ async load(index: number) {
+ const res = await fetch(`/n/${encodeURIComponent(this.node_id)}/stream?format=hlsseg&tracks=${this.track_index}&index=${index}`)
+ if (!res.ok) throw new Error(`segment fail i=${index} t=${this.track_index}`);
+ const buf = await res.arrayBuffer()
+ console.log("push");
+ this.append_queue.push({ buf, ...this.metadata.segments[index] })
+ this.tick_append()
+ }
+ tick_append() {
+ console.log("tick", this.append_queue);
+ if (this.source_buffer.updating) return
+ if (this.append_queue.length) {
+ const seg = this.append_queue[0];
+ this.source_buffer.timestampOffset = seg.start
+ this.source_buffer.appendBuffer(seg.buf);
+ this.append_queue.splice(0, 1)
+ }
+ }
+} \ No newline at end of file