aboutsummaryrefslogtreecommitdiff
path: root/web/script/player/track.ts
blob: cc21763fc5a7eabf054fb32c7161a66607b00bb5 (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
import { SourceTrack, TimeRange } from "./jhls.d.ts";
import { OVar } from "../jshelper/mod.ts";
import { JhlsTrack } from "./jhls.d.ts";
import { profile_to_partial_track, track_to_content_type } from "./mediacaps.ts";
import { BufferRange, Player } from "./player.ts";
import { EncodingProfileExt } from "./profiles.ts";

export const TARGET_BUFFER_DURATION = 10
export const MIN_BUFFER_DURATION = 1

export interface AppendRange extends TimeRange { buf: ArrayBuffer, index: number, cb: () => void }

export class PlayerTrack {
    private source_buffer!: SourceBuffer;
    private current_load?: AppendRange;
    private loading = new Set<number>();
    public buffered = new OVar<BufferRange[]>([]);
    private append_queue: AppendRange[] = [];
    public profile = new OVar<EncodingProfileExt | undefined>(undefined);

    public static async new(player: Player, node_id: string, track_index: number, metadata: JhlsTrack) {
        const t = new PlayerTrack(player, node_id, track_index, metadata)
        await t.init()
        return t
    }
    constructor(
        private player: Player,
        private node_id: string,
        private track_index: number,
        private metadata: JhlsTrack
    ) { }
    async init() {
        await this.player.profile_selector.select_optimal_profile(this.track_index, this.profile);
        const ct = track_to_content_type(this.track_from_profile())!
        console.log("source buffer content-type: " + ct);
        this.source_buffer = this.player.media_source.addSourceBuffer(ct);
        this.source_buffer.mode = "segments";
        this.source_buffer.addEventListener("updateend", () => {
            if (this.current_load) {
                this.current_load.cb();
                this.loading.delete(this.current_load.index);
                this.current_load = undefined;
            }
            this.update_buf_ranges();
            this.tick_append();
        });
        this.source_buffer.addEventListener("error", e => {
            console.error("sourcebuffer error", e);
        });
        this.source_buffer.addEventListener("abort", e => {
            console.error("sourcebuffer abort", e);
        });
    }

    track_from_profile(): SourceTrack {
        if (this.profile.value) return profile_to_partial_track(this.profile.value)
        else return this.metadata.info
    }

    update_buf_ranges() {
        const ranges: BufferRange[] = [];
        for (let i = 0; i < this.source_buffer.buffered.length; i++) {
            ranges.push({
                start: this.source_buffer.buffered.start(i),
                end: this.source_buffer.buffered.end(i),
                status: "buffered"
            });
        }
        for (const r of this.loading) {
            ranges.push({ ...this.metadata.segments[r], status: "loading" });
        }
        this.buffered.value = ranges;
    }

    async update(target: number) {
        this.update_buf_ranges(); // TODO required?

        const blocking = [];
        for (let i = 0; i < this.metadata.segments.length; i++) {
            const seg = this.metadata.segments[i];
            if (seg.end < target) continue;
            if (seg.start >= target + TARGET_BUFFER_DURATION) break;
            if (!this.check_buf_collision(seg.start, seg.end)) continue;
            if (seg.start <= target + MIN_BUFFER_DURATION)
                blocking.push(this.load(i));
            else
                this.load(i);
        }
        await Promise.all(blocking);
    }
    check_buf_collision(start: number, end: number) {
        const EPSILON = 0.01;
        for (const r of this.buffered.value)
            if (r.end - EPSILON > start && r.start < end - EPSILON)
                return false;
        return true;
    }

    async load(index: number) {
        this.loading.add(index);
        await this.player.profile_selector.select_optimal_profile(this.track_index, this.profile);
        const url = `/n/${encodeURIComponent(this.node_id)}/stream?format=hlsseg&webm=true&tracks=${this.track_index}&index=${index}${this.profile.value ? `&profile=${this.profile.value.id}` : ""}`;
        const buf = await this.player.downloader.download(url);
        await new Promise<void>(cb => {
            this.append_queue.push({ buf, ...this.metadata.segments[index], index, cb });
            this.tick_append();
        });
    }
    tick_append() {
        if (this.source_buffer.updating) return;
        if (this.append_queue.length) {
            const seg = this.append_queue[0];
            this.append_queue.splice(0, 1);
            this.current_load = seg;
            // TODO why is appending so unreliable?! sometimes it does not add it
            this.source_buffer.changeType(track_to_content_type(this.track_from_profile())!);
            this.source_buffer.timestampOffset = seg.start;
            this.source_buffer.appendBuffer(seg.buf);

        }
    }
}