aboutsummaryrefslogtreecommitdiff
path: root/web/script/player/mod.ts
blob: ce3c1136caffca20b9513f36fdc768a7d02ef43a (plain)
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
130
/*
    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 { Logger } from "../jshelper/src/log.ts";
import { EncodingProfile } from "./jhls.d.ts";
import { Player } from "./player.ts";

globalThis.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()
        globalThis.dispatchEvent(new Event("navigationrequiresreload"))
        initialize_player(main, node_id)
    }
})

function initialize_player(el: HTMLElement, node_id: string) {
    el.innerHTML = "" // clear the body

    const logger = new Logger<string>(s => e("p", s))
    const player = new Player(node_id, logger)
    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,
        show_stats.map(do_show => e("div", 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")} | ${show.metric(b * 8, "b/s")}`)),
                ...tracks.map((t, i) => t.profile.map(p =>
                    e("pre", `track ${i}: ` + (p ? `profile ${p.id} (${show_profile(p)})` : `remux`))
                ))
            )
        ))),
        logger.element,
        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} br=${show.metric(profile.audio.bitrate, "b/s")}${profile.audio.sample_rate ? ` sr=${show.metric(profile.audio.sample_rate, "Hz")}` : ""}`
    if (profile.video) return `codec=${profile.video.codec} br=${show.metric(profile.video.bitrate, "b/s")} w=${profile.video.width} preset=${profile.video.preset}`
    if (profile.subtitles) return `codec=${profile.subtitles.codec}`
    return `???`
}