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) } }) function initialize_player(el: HTMLElement, node_id: string) { el.innerHTML = "" // clear the body 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" }, player.playing.map(playing => e("button", playing ? "||" : "|>", { onclick() { } })), e("p", { class: "jsp-status" }, 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", { onclick() { if (document.fullscreenElement) document.exitFullscreen() else document.documentElement.requestFullscreen() } }) ) player.position.onchangeinit(p => pri_current.style.width = (p / player.duration.value * 100) + "%") 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(pel, 1000, idle => { controls.style.opacity = idle ? "0" : "1" pel.style.cursor = idle ? "none" : "default" }) player.video.addEventListener("click", toggle_playing) pri.addEventListener("mousedown", ev => { const r = pri.getBoundingClientRect() const p = (ev.clientX - r.left) / (r.right - r.left) player.seek(p * player.duration.value) }) document.body.addEventListener("keydown", k => { // 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() }) } function mouse_idle(e: HTMLElement, timeout: number, cb: (b: boolean) => unknown) { let ct: number; let idle = false e.onmouseleave = () => { clearTimeout(ct) } e.onmousemove = () => { clearTimeout(ct) if (idle) { idle = false cb(idle) } ct = setTimeout(() => { idle = true cb(idle) }, timeout) } } function display_time(t: number): string { if (t < 0) return "-" + display_time(-t) let h = 0, m = 0, s = 0; while (t > 3600) t -= 3600, h++; while (t > 60) t -= 60, m++; 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(undefined) public error = new OVar(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) { } }