diff options
Diffstat (limited to 'web/script/player/mod.ts')
-rw-r--r-- | web/script/player/mod.ts | 127 |
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 |