diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/js-player.css | 10 | ||||
-rw-r--r-- | web/script/player/mod.ts | 187 |
2 files changed, 148 insertions, 49 deletions
diff --git a/web/js-player.css b/web/js-player.css index b6133f9..d6daa03 100644 --- a/web/js-player.css +++ b/web/js-player.css @@ -50,3 +50,13 @@ height: var(--csize); background-color: #ffffff20; } +.jsp-overlay { + position: absolute; + bottom: var(--csize); + left: 0px; +} +.jsp-overlay .jsp-buffering { + margin: 0.2em; + font-size: larger; + color: grey; +}
\ No newline at end of file diff --git a/web/script/player/mod.ts b/web/script/player/mod.ts index 20f47ca..6a73f5f 100644 --- a/web/script/player/mod.ts +++ b/web/script/player/mod.ts @@ -1,11 +1,41 @@ import { OVar, e } from "../jshelper/mod.ts"; +export interface JhlsMetadata { + tracks: JhlsTrack[], + duration: number, +} +export interface JhlsTrack { + info: SourceTrack, + segments: { start: number, end: number }[], +} +export interface SourceTrack { + kind: SourceTrackKind, + name: string, + codec: string, + language: string, +} +export interface SourceTrackKind { + video?: { + width: number, + height: number, + fps: number, + }, + audio?: { + channels: number, + sample_rate: number, + bit_depth: number, + }, + subtitles?: boolean, +} + document.addEventListener("DOMContentLoaded", () => { if (document.body.classList.contains("player")) { + if (!globalThis.MediaSource) return alert("Media Source Extension API required") const node_id = globalThis.location.pathname.split("/")[2]; const main = document.getElementById("main")!; + document.getElementsByTagName("footer")[0].remove() initialize_player(main, node_id) } }) @@ -13,24 +43,17 @@ document.addEventListener("DOMContentLoaded", () => { function initialize_player(el: HTMLElement, node_id: string) { el.innerHTML = "" // clear the body - const buffering_state = new OVar(true) - const playing_state = new OVar(false) - const current_time = new OVar(0) - const remaining_time = new OVar(0) - - const vel = e("video") - vel.src = `/n/${node_id}/stream?tracks=1&tracks=0&format=matroska&webm=true` - - const toggle_playing = () => vel.paused ? vel.play() : vel.pause() + const player = new Player(node_id) + const toggle_playing = () => player.playing.value ? player.pause() : player.play() let pri_current: HTMLElement; let pri: HTMLElement; const controls = e("div", { class: "jsp-controls" }, - playing_state.map(playing => e("button", playing ? "||" : "|>", { onclick: toggle_playing })), + player.playing.map(playing => e("button", playing ? "||" : "|>", { onclick() { } })), e("p", { class: "jsp-status" }, - current_time.map(v => e("span", display_time(v))), e("br"), - remaining_time.map(v => e("span", display_time(v))) + 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" })), e("button", "X", { @@ -41,57 +64,39 @@ function initialize_player(el: HTMLElement, node_id: string) { }) ) + player.position.onchangeinit(p => pri_current.style.width = (p / player.duration.value * 100) + "%") - vel.onloadedmetadata = () => { - - } - vel.ondurationchange = () => { - - } - vel.ontimeupdate = () => { - remaining_time.value = vel.currentTime - vel.duration; - current_time.value = vel.currentTime - pri_current.style.width = (vel.currentTime / vel.duration * 100) + "%" - } - vel.onplay = () => { - buffering_state.value = false; - } - vel.onwaiting = () => { - buffering_state.value = true; - } - vel.onplaying = () => { - playing_state.value = true; - } - vel.onpause = () => { - playing_state.value = false - } + const pel = e("div", { class: "jsp" }, + player.video, + player.buffering_status.map(b => e("div", { class: "jsp-overlay" }, + b ? e("p", { class: "jsp-buffering" }, b) : undefined + )), + controls, + ) + el.append(pel) - mouse_idle(vel, 1000, idle => { + mouse_idle(pel, 1000, idle => { controls.style.opacity = idle ? "0" : "1" - vel.style.cursor = idle ? "none" : "default" + pel.style.cursor = idle ? "none" : "default" }) - vel.addEventListener("click", toggle_playing) + player.video.addEventListener("click", toggle_playing) pri.addEventListener("mousedown", ev => { const r = pri.getBoundingClientRect() const p = (ev.clientX - r.left) / (r.right - r.left) - vel.currentTime = p * vel.duration + player.seek(p * player.duration.value) }) document.body.addEventListener("keydown", k => { - if (k.code == "Period") vel["seekToNextFrame" as "play"]() - else if (k.code == "Space") toggle_playing() - else if (k.code == "ArrowLeft") vel.currentTime -= 5 - else if (k.code == "ArrowRight") vel.currentTime += 5 - else if (k.code == "ArrowUp") vel.currentTime -= 60 - else if (k.code == "ArrowDown") vel.currentTime += 60 + // if (k.code == "Period") vel["seekToNextFrame" as "play"]() + if (k.code == "Space") toggle_playing() + else if (k.code == "ArrowLeft") player.seek(player.position.value - 5) + else if (k.code == "ArrowRight") player.seek(player.position.value + 5) + else if (k.code == "ArrowUp") player.seek(player.position.value - 60) + else if (k.code == "ArrowDown") player.seek(player.position.value + 60) else return; k.preventDefault() }) - el.append(e("div", { class: "jsp" }, - vel, - controls, - )) } function mouse_idle(e: HTMLElement, timeout: number, cb: (b: boolean) => unknown) { @@ -118,3 +123,87 @@ function display_time(t: number): string { while (t > 1) t -= 1, s++; return (h ? h + "h" : "") + (m ? m + "m" : "") + (s ? s + "s" : "") } + +class Player { + public video = e("video") + private media_source = new MediaSource(); + + public position = new OVar(0) + public duration = new OVar(1) + public playing = 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.video.onplay = () => { + this.buffering_status.value = undefined; + } + this.video.onwaiting = () => { + this.buffering_status.value = "Buffering..."; + } + this.video.onplaying = () => { + this.playing.value = true; + } + this.video.onpause = () => { + this.playing.value = false + } + + 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 = "Fetching initial segments..." + + this.duration.value = metadata.duration + this.video.src = URL.createObjectURL(this.media_source) + this.media_source.addEventListener("sourceopen", () => { + console.log("sourceopen"); + this.segment(metadata) + }) + + } + 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) + } + } + + play() { + console.log("play"); + this.video.play() + } + pause() { + console.log("pause"); + this.video.pause() + } + seek(_p: number) { } + +} |