aboutsummaryrefslogtreecommitdiff
path: root/web/script/player/player.ts
blob: c07fa374479e6c5c3c4dbac073542b9286391f1d (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, e } from "../jshelper/mod.ts";
import { JhlsMetadata, JhlsTrack, TimeRange } from "./jhls.d.ts";
import { SegmentDownloader } from "./download.ts";
import { PlayerTrack } from "./track.ts";
import { ProfileSelector } from "./profiles.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?: JhlsTrack[];
    public active_tracks = new OVar<PlayerTrack[]>([]);
    public downloader: SegmentDownloader = new SegmentDownloader();
    public profile_selector!: ProfileSelector

    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(private 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 media manifest...")
        const res = await fetch(`/n/${encodeURIComponent(this.node_id)}/stream?format=jhls`, { headers: { "Accept": "application/json" } })
        if (!res.ok) return this.error.value = "Cannot download JHLS metadata"
        let metadata!: JhlsMetadata & { error: string }
        try { metadata = await res.json() }
        catch (_) { this.set_pers("Error: Failed to fetch stream info") }
        if (metadata.error) return this.set_pers("server error: " + metadata.error)
        this.set_pers()
        this.tracks = metadata.tracks

        this.profile_selector = new ProfileSelector(this, this.downloader.bandwidth, metadata)
        this.set_pers("Checking codec support...")
        await this.profile_selector.init()

        this.duration.value = metadata.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, metadata.tracks[0]))
            this.active_tracks.value.push(await PlayerTrack.new(this, this.node_id, 1, metadata.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
    }
}