diff options
author | metamuffin <metamuffin@disroot.org> | 2023-10-02 20:07:09 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2023-10-02 20:07:09 +0200 |
commit | f783143b4adf22be662db1af2ca00b34a868cf72 (patch) | |
tree | 180ea44b0f7e7052231643da6e929b1be5f8def1 /web | |
parent | e25beb3e0c2531b09d8efd70e858396dcc631dd2 (diff) | |
download | jellything-f783143b4adf22be662db1af2ca00b34a868cf72.tar jellything-f783143b4adf22be662db1af2ca00b34a868cf72.tar.bz2 jellything-f783143b4adf22be662db1af2ca00b34a868cf72.tar.zst |
player: split files
Diffstat (limited to 'web')
-rw-r--r-- | web/script/player/jhls.d.ts | 2 | ||||
-rw-r--r-- | web/script/player/mod.ts | 186 | ||||
-rw-r--r-- | web/script/player/player.ts | 98 | ||||
-rw-r--r-- | web/script/player/track.ts | 92 |
4 files changed, 193 insertions, 185 deletions
diff --git a/web/script/player/jhls.d.ts b/web/script/player/jhls.d.ts index b1b6a57..a8fc3ac 100644 --- a/web/script/player/jhls.d.ts +++ b/web/script/player/jhls.d.ts @@ -43,4 +43,4 @@ export interface EncodingProfile { subtitles?: { codec: string, }, -}
\ No newline at end of file +} diff --git a/web/script/player/mod.ts b/web/script/player/mod.ts index caad781..02b8a12 100644 --- a/web/script/player/mod.ts +++ b/web/script/player/mod.ts @@ -3,11 +3,8 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2023 metamuffin <metamuffin.org> */ -import { OVar, e } from "../jshelper/mod.ts"; -import { JhlsMetadata, JhlsTrack, TimeRange } from "./jhls.d.ts"; - -const TARGET_BUFFER_DURATION = 15 -const MIN_BUFFER_DURATION = 1 +import { e } from "../jshelper/mod.ts"; +import { Player } from "./player.ts"; document.addEventListener("DOMContentLoaded", () => { if (document.body.classList.contains("player")) { @@ -120,182 +117,3 @@ function display_time(t: number): string { return (h ? h + "h" : "") + (m ? m + "m" : "") + (s ? s + "s" : "") } -interface BufferRange extends TimeRange { status: "buffered" | "loading" | "queued" } -class Player { - public video = e("video") - private media_source = new MediaSource(); - public tracks = new OVar<PlayerTrack[]>([]); - - public position = new OVar(0) - public duration = new OVar(1) - public playing = new OVar(false) - public canplay = new OVar(false) - 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.update() // TODO maybe not here - } - this.video.onplay = () => { - console.log("play"); - this.buffering_status.value = "Resuming playback..."; - } - this.video.onwaiting = () => { - console.log("waiting"); - this.buffering_status.value = "Buffering..."; - this.canplay.value = false; - } - this.video.onplaying = () => { - console.log("playing"); - this.playing.value = true; - this.buffering_status.value = undefined; - } - this.video.onpause = () => { - console.log("pause"); - this.playing.value = false - } - this.video.oncanplay = () => { - console.log("canplay"); - this.buffering_status.value = undefined - this.canplay.value = true - } - this.video.onseeking = () => { - console.log("seeking"); - this.buffering_status.value = "Seeking..." - } - this.video.onseeked = () => { - console.log("seeked"); - this.buffering_status.value = undefined - } - this.fetch_meta() - } - - 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 = undefined - - this.duration.value = metadata.duration - this.video.src = URL.createObjectURL(this.media_source) - 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 - }) - } - async update(newt?: number) { - await Promise.all(this.tracks.value.map(t => t.update(newt ?? this.video.currentTime))) - } - - play() { - this.video.play() - } - pause() { - this.video.pause() - } - frame_forward() { - //@ts-ignore trust me bro - this.video["seekToNextFrame"]() - } - async seek(p: number) { - this.buffering_status.value = "Buffering at target..." - await this.update(p) - this.video.currentTime = p - } -} - -interface AppendRange extends TimeRange { buf: ArrayBuffer, index: number, cb: () => void } -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 diff --git a/web/script/player/player.ts b/web/script/player/player.ts new file mode 100644 index 0000000..5c38dc8 --- /dev/null +++ b/web/script/player/player.ts @@ -0,0 +1,98 @@ +import { OVar, e } from "../jshelper/mod.ts"; +import { JhlsMetadata, TimeRange } from "./jhls.d.ts"; +import { PlayerTrack } from "./track.ts"; + +export interface BufferRange extends TimeRange { status: "buffered" | "loading" | "queued" } +export class Player { + public video = e("video") + private media_source = new MediaSource(); + public tracks = new OVar<PlayerTrack[]>([]); + + public position = new OVar(0) + public duration = new OVar(1) + public playing = new OVar(false) + public canplay = new OVar(false) + 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.update() // TODO maybe not here + } + this.video.onplay = () => { + console.log("play"); + this.buffering_status.value = "Resuming playback..."; + } + this.video.onwaiting = () => { + console.log("waiting"); + this.buffering_status.value = "Buffering..."; + this.canplay.value = false; + } + this.video.onplaying = () => { + console.log("playing"); + this.playing.value = true; + this.buffering_status.value = undefined; + } + this.video.onpause = () => { + console.log("pause"); + this.playing.value = false + } + this.video.oncanplay = () => { + console.log("canplay"); + this.buffering_status.value = undefined + this.canplay.value = true + } + this.video.onseeking = () => { + console.log("seeking"); + this.buffering_status.value = "Seeking..." + } + this.video.onseeked = () => { + console.log("seeked"); + this.buffering_status.value = undefined + } + this.fetch_meta() + } + + 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 = undefined + + this.duration.value = metadata.duration + this.video.src = URL.createObjectURL(this.media_source) + 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 + }) + } + async update(newt?: number) { + await Promise.all(this.tracks.value.map(t => t.update(newt ?? this.video.currentTime))) + } + + play() { + this.video.play() + } + pause() { + this.video.pause() + } + frame_forward() { + //@ts-ignore trust me bro + this.video["seekToNextFrame"]() + } + async seek(p: number) { + this.buffering_status.value = "Buffering at target..." + await this.update(p) + this.video.currentTime = p + } +} + 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 |