aboutsummaryrefslogtreecommitdiff
path: root/web/script/player/player.ts
blob: 7ffdb9704204d5f28e12fadbd4eca3fc3196b4da (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
/*
    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, e } from "../jshelper/mod.ts";
import { NodePublic, SourceTrack, TimeRange } from "./jhls.d.ts";
import { SegmentDownloader } from "./download.ts";
import { PlayerTrack } from "./track.ts";
import { Logger } from "../jshelper/src/log.ts";

export interface BufferRange extends TimeRange { status: "buffered" | "loading" | "queued" }
export class Player {
    public video = e("video")
    public media_source = new MediaSource();
    public tracks?: SourceTrack[];
    public active_tracks = new OVar<PlayerTrack[]>([]);
    public downloader: SegmentDownloader = new SegmentDownloader();

    public position = new OVar(0)
    public duration = new OVar(1)
    public playing = new OVar(false)
    public canplay = new OVar(false)
    public error = new OVar<string | undefined>(undefined)

    private cancel_buffering_pers: undefined | (() => void)
    set_pers(s?: string) {
        if (this.cancel_buffering_pers) this.cancel_buffering_pers(), this.cancel_buffering_pers = undefined
        if (s) this.cancel_buffering_pers = this.logger?.log_persistent(s)
    }

    constructor(public node_id: string, public logger?: Logger<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.set_pers("Resuming playback...")
        }
        this.video.onwaiting = () => {
            console.log("waiting");
            if (this.video.currentTime > this.duration.value - 0.2) return this.set_pers("Playback finished")
            this.set_pers("Buffering...")
            this.canplay.value = false;
        }
        this.video.onplaying = () => {
            console.log("playing");
            this.playing.value = true;
            this.set_pers()
        }
        this.video.onpause = () => {
            console.log("pause");
            this.playing.value = false
        }
        this.video.oncanplay = () => {
            console.log("canplay");
            this.set_pers()
            this.canplay.value = true
        }
        this.video.onseeking = () => {
            console.log("seeking");
            this.set_pers("Seeking...")
        }
        this.video.onseeked = () => {
            console.log("seeked");
            this.set_pers()
        }
        this.video.onerror = e => {
            console.error("video element error:", e);
        }
        this.video.onabort = e => {
            console.error("video element abort:", e);
        }
        this.fetch_meta()
    }

    async fetch_meta() {
        this.set_pers("Loading node...")
        const res = await fetch(`/n/${encodeURIComponent(this.node_id)}`, { headers: { "Accept": "application/json" } })
        if (!res.ok) return this.error.value = "Cannot download node."
        let metadata!: NodePublic & { error: string }
        try { metadata = await res.json() }
        catch (_) { this.set_pers("Error: Failed to fetch node") }
        if (metadata.error) return this.set_pers("server error: " + metadata.error)
        this.set_pers()
        this.tracks = metadata.media!.tracks

        this.duration.value = metadata.media!.duration
        this.video.src = URL.createObjectURL(this.media_source)
        this.media_source.addEventListener("sourceopen", async () => {
            this.set_pers("Initializing Media Extensions...")
            this.active_tracks.value.push((await PlayerTrack.new(this, this.node_id, 0, this.tracks![0]))!) // TODO unsafe and missing ui anyway
            this.active_tracks.value.push((await PlayerTrack.new(this, this.node_id, 1, this.tracks![1]))!)
            this.active_tracks.change()
            this.set_pers("Downloading initial segments...")
            this.update()
            await this.canplay.wait_for(true)
            this.set_pers()
        })
    }
    async update(newt?: number) {
        await Promise.all(this.active_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.set_pers("Buffering at target...")
        await this.update(p)
        this.video.currentTime = p
    }
}