import { OVar } from "../jshelper/mod.ts"; import { JhlsTrack, TimeRange } from "./jhls.d.ts"; import { BufferRange, Player } from "./player.ts"; import { EncodingProfileExt } from "./profiles.ts"; const TARGET_BUFFER_DURATION = 10 const MIN_BUFFER_DURATION = 1 export interface AppendRange extends TimeRange { buf: ArrayBuffer, index: number, cb: () => void } export class PlayerTrack { private source_buffer: SourceBuffer private current_load?: AppendRange private loading = new Set() public buffered = new OVar([]) private append_queue: AppendRange[] = [] public profile = new OVar(undefined) constructor( private player: Player, private node_id: string, private track_index: number, private metadata: JhlsTrack ) { this.source_buffer = this.player.media_source.addSourceBuffer("video/webm; codecs=\"opus,vorbis,vp8,vp9,av1\"") 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); }) this.source_buffer.addEventListener("abort", e => { console.error("sourcebuffer abort", 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: "loading" }) } this.buffered.value = ranges } async update(target: number) { this.update_buf_ranges() // TODO required? const blocking = [] for (let i = 0; i < this.metadata.segments.length; 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) blocking.push(this.load(i)) else this.load(i) } await Promise.all(blocking) } check_buf_collision(start: number, end: number) { const EPSILON = 0.01 for (const r of this.buffered.value) if (r.end - EPSILON > start && r.start < end - EPSILON) return false return true } async load(index: number) { this.loading.add(index) this.player.profile_selector.select_optimal_profile(this.track_index, this.profile) const url = `/n/${encodeURIComponent(this.node_id)}/stream?format=hlsseg&tracks=${this.track_index}&index=${index}${this.profile.value ? `&profile=${this.profile.value.id}` : ""}`; const buf = await this.player.downloader.download(url) await new Promise(cb => { this.append_queue.push({ buf, ...this.metadata.segments[index], index, cb }) this.tick_append() }) } tick_append() { if (this.source_buffer.updating) return if (this.append_queue.length) { const seg = this.append_queue[0]; this.append_queue.splice(0, 1) this.current_load = seg // TODO why is appending so unreliable?! sometimes it does not add it this.source_buffer.changeType("video/webm; codecs=\"opus,vorbis,vp8,vp9,av1\"") this.source_buffer.timestampOffset = seg.start this.source_buffer.appendBuffer(seg.buf); } } }