diff options
Diffstat (limited to 'web/script/player')
-rw-r--r-- | web/script/player/mod.ts | 132 |
1 files changed, 85 insertions, 47 deletions
diff --git a/web/script/player/mod.ts b/web/script/player/mod.ts index aed504a..17cd77e 100644 --- a/web/script/player/mod.ts +++ b/web/script/player/mod.ts @@ -34,7 +34,8 @@ export interface SourceTrackKind { subtitles?: boolean, } -const TARGET_BUFFER_DURATION = 30 +const TARGET_BUFFER_DURATION = 15 +const MIN_BUFFER_DURATION = 1 document.addEventListener("DOMContentLoaded", () => { if (document.body.classList.contains("player")) { @@ -64,15 +65,19 @@ function initialize_player(el: HTMLElement, node_id: string) { ), 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) - } - }) - )) + player.tracks.map( + tracks => e("div", ...tracks.map((t, i) => t.buffered.map( + ranges => e("div", ...ranges.map( + r => e("div", { + class: ["jsp-pri-buffer", `jsp-pri-buffer-${r.status}`], + style: { + width: pri_map(r.end - r.start), + top: `calc(var(--pribufsize)*${i})`, + left: pri_map(r.start) + } + }) + )) + ))) ) ), e("button", "X", { @@ -147,12 +152,12 @@ interface BufferRange extends Range { status: "buffered" | "downloading" | "queu class Player { public video = e("video") private media_source = new MediaSource(); - public tracks: PlayerTrack[] = []; + public tracks = new OVar<PlayerTrack[]>([]); public position = new OVar(0) public duration = new OVar(1) public playing = new OVar(false) - public buffered = new OVar<BufferRange[]>([]) + public canplay = new OVar(false) public buffering_status = new OVar<string | undefined>(undefined) public error = new OVar<string | undefined>(undefined) @@ -162,13 +167,14 @@ class Player { 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; } this.video.onwaiting = () => { + console.log("waiting"); this.buffering_status.value = "Buffering..."; + this.canplay.value = false; } this.video.onplaying = () => { this.playing.value = true; @@ -176,19 +182,12 @@ 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.video.oncanplay = () => { + this.buffering_status.value = undefined + console.log("canplay"); + this.canplay.value = true } - this.buffered.value = o; + this.fetch_meta() } async fetch_meta() { @@ -200,15 +199,18 @@ class Player { this.duration.value = metadata.duration this.video.src = URL.createObjectURL(this.media_source) - this.media_source.addEventListener("sourceopen", () => { - 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.media_source.addEventListener("sourceopen", async () => { + this.tracks.value.push(new PlayerTrack(this.media_source, this.node_id, 0, metadata.tracks[0])) + this.tracks.value.push(new PlayerTrack(this.media_source, this.node_id, 1, metadata.tracks[1])) + this.tracks.change() this.buffering_status.value = "Fetching initial segments..." this.update() + await this.canplay.wait_for(true) + this.buffering_status.value = undefined }) } - update() { - this.tracks.forEach(t => t.update(this.video.currentTime)) + async update(newt?: number) { + await Promise.all(this.tracks.value.map(t => t.update(newt ?? this.video.currentTime))) } play() { @@ -219,61 +221,97 @@ class Player { console.log("pause"); this.video.pause() } - seek(p: number) { + async seek(p: number) { + this.pause() + await this.update(p) this.video.currentTime = p - this.update() + this.play() } } +interface AppendRange extends Range { buf: ArrayBuffer, index: number, cb: () => void } class PlayerTrack { private source_buffer: SourceBuffer - private loaded = new Set<number>() + private current_load?: AppendRange private loading = new Set<number>() - private append_queue: (Range & { buf: ArrayBuffer, cb: () => void })[] = [] + public buffered = new OVar<BufferRange[]>([]) + private append_queue: AppendRange[] = [] 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", () => { + if (this.current_load) { + this.current_load.cb() + this.loading.delete(this.current_load.index) + this.current_load = undefined + } + this.update_buf_ranges() this.tick_append() }) this.source_buffer.addEventListener("error", e => { console.error("sourcebuffer error", e); }) } + + update_buf_ranges() { + const ranges: BufferRange[] = [] + for (let i = 0; i < this.source_buffer.buffered.length; i++) { + ranges.push({ + start: this.source_buffer.buffered.start(i), + end: this.source_buffer.buffered.end(i), + status: "buffered" + }) + } + for (const r of this.loading) { + ranges.push({ ...this.metadata.segments[r], status: "downloading" }) + } + this.buffered.value = ranges + } + async update(target: number) { - console.log(`update track ${this.track_index}`, target, this.loaded, this.loading); + this.update_buf_ranges() // TODO required? + 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.has(i) && !this.loading.has(i)) { - this.loading.add(i) - this.load(i) - } - } + const seg = this.metadata.segments[i]; + if (seg.end < target) continue + if (seg.start >= target + TARGET_BUFFER_DURATION) break + if (!this.check_buf_collision(seg.start, seg.end)) continue + if (seg.start <= target + MIN_BUFFER_DURATION) + await this.load(i) + else + this.load(i) } } + check_buf_collision(start: number, end: number) { + for (const r of this.buffered.value) + if (r.end > start && r.start < end) + return false + return true + } + async load(index: number) { + this.loading.add(index) + console.log("load", index); 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() - this.loading.delete(index) - this.loaded.add(index) await new Promise<void>(cb => { - this.append_queue.push({ buf, ...this.metadata.segments[index], cb }) + this.append_queue.push({ buf, ...this.metadata.segments[index], index, cb }) this.tick_append() }) } tick_append() { - console.log("tick", this.append_queue); + // console.log("tick", this.append_queue); if (this.source_buffer.updating) return if (this.append_queue.length) { const seg = this.append_queue[0]; - console.log("append", this.track_index, seg); + // console.log("append", this.track_index, seg); this.source_buffer.timestampOffset = seg.start this.source_buffer.appendBuffer(seg.buf); this.append_queue.splice(0, 1) + this.current_load = seg } } }
\ No newline at end of file |