aboutsummaryrefslogtreecommitdiff
path: root/web/script/player
diff options
context:
space:
mode:
Diffstat (limited to 'web/script/player')
-rw-r--r--web/script/player/jhls.d.ts2
-rw-r--r--web/script/player/mod.ts186
-rw-r--r--web/script/player/player.ts98
-rw-r--r--web/script/player/track.ts92
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