1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
/*
This file is part of jellything (https://codeberg.org/metamuffin/jellything)
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2023 metamuffin <metamuffin.org>
*/
import { OVar, show } from "../jshelper/mod.ts";
import { e } from "../jshelper/mod.ts";
import { EncodingProfile } from "./jhls.d.ts";
import { Player } from "./player.ts";
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 show_stats = new OVar(false);
const toggle_playing = () => player.playing.value ? player.pause() : player.play()
const pri_map = (v: number) => (v / player.duration.value * 100) + "%"
let pri_current: HTMLElement;
let pri: HTMLElement;
const controls = e("div", { class: "jsp-controls" },
player.playing.map(playing => e("button", playing ? "||" : "|>", { onclick: toggle_playing })),
e("p", { class: "jsp-status" },
player.position.map(v => e("span", show.duration(v))), e("br"),
player.position.map(v => e("span", show.duration(v - player.duration.value)))
),
pri = e("div", { class: "jsp-pri" },
pri_current = e("div", { class: "jsp-pri-current" }),
player.tracks.map(
tracks => e("div", ...tracks.map((t, i) => t.buffered.map(
ranges => e("div", ...ranges.map(
r => e("div", {
class: ["jsp-pri-buffer", `jsp-pri-buffer-${r.status}`],
style: {
width: pri_map(r.end - r.start),
top: `calc(var(--pribufsize)*${i})`,
left: pri_map(r.start)
}
})
))
)))
)
),
e("button", "X", {
onclick() {
if (document.fullscreenElement) document.exitFullscreen()
else document.documentElement.requestFullscreen()
}
})
)
player.position.onchangeinit(p => pri_current.style.width = pri_map(p))
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
)),
show_stats.map(do_show => player.tracks.map(tracks =>
!do_show ? e("div") : e("div", { class: "jsp-stats" },
player.downloader.bandwidth.map(b => e("pre", `estimated bandwidth: ${show.metric(b, "B/s")}`)),
...tracks.map((t, i) => t.profile.map(p =>
e("pre", `track ${i}: ` + (p ? `profile ${p.id} (${show_profile(p)})` : `remux`))
))
)
)),
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") player.pause(), player.frame_forward()
if (k.code == "Space") toggle_playing()
else if (k.code == "KeyV") show_stats.value = !show_stats.value
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 show_profile(profile: EncodingProfile): string {
if (profile.audio) return `codec=${profile.audio.codec} ar=${show.metric(profile.audio.sample_rate ?? -1, "Hz")} abr=${show.metric(profile.audio.bitrate, "b/s")}`
if (profile.video) return `codec=${profile.video.codec} vw=${show.metric(profile.video.width ?? -1, "Hz")} vbr=${show.metric(profile.video.bitrate, "b/s")} preset=${profile.video.preset}`
if (profile.subtitles) return `codec=${profile.subtitles.codec}`
return `???`
}
|