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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
|
import { JhlsTrackIndex, SourceTrack } from "../jhls.d.ts";
import { OVar } from "../../jshelper/mod.ts";
import { profile_to_partial_track, track_to_content_type } from "../mediacaps.ts";
import { BufferRange, Player } from "../player.ts";
import { EncodingProfileExt, ProfileSelector } from "../profiles.ts";
import { PlayerTrack, AppendRange, TARGET_BUFFER_DURATION, MIN_BUFFER_DURATION } from "./mod.ts";
export class MSEPlayerTrack extends PlayerTrack {
public source_buffer!: SourceBuffer;
private current_load?: AppendRange;
private loading = new Set<number>();
private append_queue: AppendRange[] = [];
public profile_selector: ProfileSelector;
public profile = new OVar<EncodingProfileExt | undefined>(undefined);
public index?: JhlsTrackIndex
constructor(
private player: Player,
private node_id: string,
track_index: number,
private metadata: SourceTrack,
) {
super(track_index);
this.profile_selector = new ProfileSelector(player, this, player.downloader.bandwidth);
this.init()
}
async init() {
this.buffered.value = [{ start: 0, end: this.player.duration.value, status: "loading" }]
try {
const res = await fetch(`/n/${encodeURIComponent(this.node_id)}/stream?format=jhlsi&track=${this.track_index}`, { headers: { "Accept": "application/json" } });
if (!res.ok) return this.player.error.value = "Cannot download index.", undefined;
let index!: JhlsTrackIndex & { error: string; };
try { index = await res.json(); }
catch (_) { this.player.set_pers("Error: Failed to fetch node"); }
if (index.error) return this.player.set_pers("server error: " + index.error), undefined;
this.index = index
} catch (e) {
if (e instanceof TypeError) {
this.player.set_pers("Cannot download index: Network Error");
} else throw e;
}
this.buffered.value = []
await this.profile_selector.select_optimal_profile(this.track_index, this.profile);
const ct = track_to_content_type(this.track_from_profile())!;
console.log(`track ${this.track_index} source buffer content-type: ${ct}`);
this.source_buffer = this.player.media_source.addSourceBuffer(ct);
this.abort.signal.addEventListener("abort", () => {
console.log(`destroy source buffer for track ${this.track_index}`);
this.player.media_source.removeSourceBuffer(this.source_buffer);
});
this.source_buffer.mode = "segments";
this.source_buffer.addEventListener("updateend", () => {
if (this.abort.signal.aborted) return;
if (this.current_load) {
this.loading.delete(this.current_load.index);
const cb = this.current_load.cb;
this.current_load = undefined;
cb()
} else {
console.warn("updateend but nothing is loading")
}
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);
});
this.update(this.player.video.currentTime)
}
track_from_profile(): SourceTrack {
if (this.profile.value) return profile_to_partial_track(this.profile.value);
else return this.metadata;
}
update_buf_ranges() {
if (!this.index) return;
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.index.segments[r], status: "loading" });
}
this.buffered.value = ranges;
}
async update(target: number) {
if (!this.index) return;
this.update_buf_ranges(); // TODO required?
const blocking = [];
for (let i = 0; i < this.index.segments.length; i++) {
const seg = this.index.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.profile_selector.select_optimal_profile(this.track_index, this.profile);
const url = `/n/${encodeURIComponent(this.node_id)}/stream?format=frag&webm=true&track=${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 => {
if (!this.index) return;
if (this.abort.signal.aborted) return;
this.append_queue.push({ buf, ...this.index.segments[index], index, cb });
this.tick_append();
});
}
tick_append() {
if (this.source_buffer.updating || this.current_load) 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);
}
}
}
|