aboutsummaryrefslogtreecommitdiff
path: root/web/script/player/track.ts
diff options
context:
space:
mode:
Diffstat (limited to 'web/script/player/track.ts')
-rw-r--r--web/script/player/track.ts92
1 files changed, 92 insertions, 0 deletions
diff --git a/web/script/player/track.ts b/web/script/player/track.ts
new file mode 100644
index 0000000..b089932
--- /dev/null
+++ b/web/script/player/track.ts
@@ -0,0 +1,92 @@
+import { OVar } from "../jshelper/mod.ts";
+import { JhlsTrack, TimeRange } from "./jhls.d.ts";
+import { BufferRange } from "./player.ts";
+
+const TARGET_BUFFER_DURATION = 15
+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<number>()
+ 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: "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)
+ 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()
+
+ await new Promise<void>(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.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