buninu-browser 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cdp-server.js +47 -14
- package/lib/happy-dom/document-base.js +33 -0
- package/lib/happy-dom/parser.js +15 -6
- package/lib/happy-dom/selector-matcher.js +2 -2
- package/lib/happy-dom/selector-pseudo-adapter.js +19 -0
- package/lib/happy-dom/svg-attribute-adapter.js +50 -0
- package/lib/headless-shell.js +9 -6
- package/lib/input/hit-test.js +67 -19
- package/lib/layout/text-layout.js +213 -91
- package/lib/media/byte-pipe.js +2 -2
- package/lib/media/position-audio.js +10 -1
- package/lib/media/source-snapshots.js +126 -0
- package/lib/media/video-keyframe.js +17 -4
- package/lib/media/webm-stream.js +106 -0
- package/lib/network/network-service.js +268 -120
- package/lib/paint/display-list.js +8 -28
- package/lib/paint/frame-cache.js +25 -0
- package/lib/paint/frame.js +42 -13
- package/lib/paint/paragraph-text.js +24 -0
- package/lib/paint/raster-readback.js +41 -0
- package/lib/paint/rounded-rect.js +82 -0
- package/lib/paint/shared-frame-pool.js +105 -0
- package/lib/render-tree/index.js +38 -6
- package/lib/render-tree/svg.js +15 -7
- package/lib/renderer/bindings.js +233 -80
- package/lib/renderer/broadcast-channel.js +76 -0
- package/lib/renderer/canvas-2d.js +257 -14
- package/lib/renderer/canvas-text.js +120 -0
- package/lib/renderer/compositor.js +13 -11
- package/lib/renderer/custom-element-reactions.js +25 -1
- package/lib/renderer/font-loading.js +227 -0
- package/lib/renderer/host.js +28 -4
- package/lib/renderer/image-resources.js +65 -0
- package/lib/renderer/interfaces.js +28 -0
- package/lib/renderer/layout-observers.js +236 -0
- package/lib/renderer/main.js +30 -4
- package/lib/renderer/media-source.js +126 -55
- package/lib/renderer/page-realm.js +270 -98
- package/lib/renderer/page-renderer.js +647 -235
- package/lib/renderer/prepared-script-cache.js +50 -0
- package/lib/renderer/rendered-text.js +108 -0
- package/lib/renderer/rewrite-worker.js +4 -3
- package/lib/renderer/script-rewrite.js +13 -2
- package/lib/renderer/scroll-into-view.js +28 -0
- package/lib/renderer/slot-assignment.js +60 -0
- package/lib/renderer/video-presentation.js +23 -4
- package/lib/renderer/workers.js +2 -2
- package/lib/style/computed-style.js +611 -104
- package/lib/style/selector-traits.js +49 -5
- package/package.json +1 -1
- package/test/broadcast-channel.test.js +26 -0
- package/test/canvas-2d.test.js +83 -2
- package/test/canvas-paint.test.js +9 -0
- package/test/canvas-text.test.js +41 -0
- package/test/cdp-stream.test.js +56 -0
- package/test/computed-style-binding.test.js +151 -0
- package/test/computed-style.test.js +258 -1
- package/test/display-list.test.js +12 -0
- package/test/document-base.test.js +87 -0
- package/test/dom-platform.test.js +461 -0
- package/test/fetch-lifecycle.test.js +161 -0
- package/test/fixtures/canvas-paint.js +117 -0
- package/test/fixtures/canvas-text.js +42 -0
- package/test/fixtures/image-lifetime.js +90 -0
- package/test/fixtures/media-seek-snapshot.js +77 -0
- package/test/fixtures/mse-audio-chirp.webm +0 -0
- package/test/fixtures/mse-switch-large.mp4 +0 -0
- package/test/fixtures/mse-switch-small.mp4 +0 -0
- package/test/fixtures/renderer-memory.js +110 -0
- package/test/fixtures/summary-style.js +28 -0
- package/test/font-loading.test.js +97 -0
- package/test/grid.test.js +58 -0
- package/test/hit-test.test.js +21 -1
- package/test/image-lifetime.test.js +12 -0
- package/test/image-resources.test.js +94 -0
- package/test/local-media.test.js +84 -0
- package/test/media-seek-snapshot.test.js +11 -0
- package/test/media-source-config.test.js +98 -0
- package/test/media-source-webm-accounting.test.js +98 -0
- package/test/media-source-webm.test.js +108 -0
- package/test/navigation.test.js +1 -0
- package/test/network-service.test.js +388 -0
- package/test/page-scripts.test.js +1 -1
- package/test/paint-frame.test.js +103 -0
- package/test/paint-paragraph-cache.test.js +87 -0
- package/test/position-audio.test.js +24 -0
- package/test/positioning.test.js +22 -0
- package/test/prepared-script-cache.test.js +45 -0
- package/test/pulse-client.test.js +10 -3
- package/test/raster-readback.test.js +40 -0
- package/test/render-tree.test.js +28 -0
- package/test/renderer-memory.test.js +11 -0
- package/test/selector-traits.test.js +24 -0
- package/test/shared-frame-pool.test.js +63 -0
- package/test/style-sheets.test.js +51 -0
- package/test/svg.test.js +29 -0
- package/test/text-layout.test.js +106 -0
- package/test/video-keyframe.test.js +61 -0
- package/test/video-presentation.test.js +69 -0
- package/tools/browser-lab.js +78 -0
- package/tools/browser-soak.js +75 -0
- package/tools/network-reference.js +49 -0
- package/tools/process-memory.js +38 -0
- package/tools/script-benchmark.js +25 -0
- package/ai-debug.log +0 -5537
package/lib/renderer/main.js
CHANGED
|
@@ -46,6 +46,7 @@ import { RENDERER_LOG_FLAG } from "../log.js";
|
|
|
46
46
|
import wasmpeg from "../../vendor/wasmpeg/src/js/index.js";
|
|
47
47
|
import { createBytePipe } from "../media/byte-pipe.js";
|
|
48
48
|
import { notifyRejectionHandled, notifyUnhandledRejection } from "./rejections.js";
|
|
49
|
+
import { SharedFramePool } from "../paint/shared-frame-pool.js";
|
|
49
50
|
|
|
50
51
|
const DANGEROUSLY_ALLOW_HOST_JS = "--dangerously-allow-host-js";
|
|
51
52
|
|
|
@@ -90,6 +91,7 @@ function fetchResource(url, kind) {
|
|
|
90
91
|
function pageFetch(request) {
|
|
91
92
|
let fetchId;
|
|
92
93
|
let streamController;
|
|
94
|
+
const completed = Promise.withResolvers();
|
|
93
95
|
const body = new ReadableStream({
|
|
94
96
|
start(controller) { streamController = controller; },
|
|
95
97
|
cancel() { promise.abort(); },
|
|
@@ -97,11 +99,17 @@ function pageFetch(request) {
|
|
|
97
99
|
const promise = new Promise((resolve, reject) => {
|
|
98
100
|
const id = nextFetchId++;
|
|
99
101
|
fetchId = id;
|
|
100
|
-
pendingFetches.set(id, { resolve, reject, page: true, body, streamController, head: false });
|
|
102
|
+
pendingFetches.set(id, { resolve, reject, page: true, body, streamController, head: false, finish: completed.resolve });
|
|
101
103
|
process.send({ type: "page-fetch", id, request });
|
|
102
104
|
});
|
|
103
|
-
promise.
|
|
104
|
-
|
|
105
|
+
promise.finished = completed.promise;
|
|
106
|
+
promise.abort = (reason = new DOMException("The operation was aborted.", "AbortError")) => {
|
|
107
|
+
const pending = pendingFetches.get(fetchId);
|
|
108
|
+
if (!pending) return;
|
|
109
|
+
pendingFetches.delete(fetchId);
|
|
110
|
+
if (pending.head) { try { streamController.error(reason); } catch {} }
|
|
111
|
+
else pending.reject(reason);
|
|
112
|
+
completed.resolve();
|
|
105
113
|
process.send({ type: "page-fetch-abort", id: fetchId });
|
|
106
114
|
};
|
|
107
115
|
return promise;
|
|
@@ -145,6 +153,7 @@ const audio = {
|
|
|
145
153
|
};
|
|
146
154
|
|
|
147
155
|
// Workers load their module from disk, so this happens before seccomp.
|
|
156
|
+
const frameBuffers = new SharedFramePool();
|
|
148
157
|
const compositor = process.argv.includes(COMPOSITOR_FLAG) ? await startCompositor() : null;
|
|
149
158
|
// Started before the sandbox closes: the worker loads its module from disk.
|
|
150
159
|
const rewriteScript = await startScriptRewriter();
|
|
@@ -167,9 +176,17 @@ async function startCompositor() {
|
|
|
167
176
|
worker.addEventListener("message", resolve, { once: true });
|
|
168
177
|
worker.addEventListener("error", reject, { once: true });
|
|
169
178
|
});
|
|
179
|
+
worker.addEventListener("error", (event) => {
|
|
180
|
+
frameBuffers.clear();
|
|
181
|
+
report(`buninu-browser: compositor worker failed: ${String(event?.message ?? "unknown error").slice(0, 500)}`);
|
|
182
|
+
// Its dedicated controller channel and outstanding frame leases cannot
|
|
183
|
+
// recover in place. Normal renderer exit rejects pending controller calls.
|
|
184
|
+
process.exit(1);
|
|
185
|
+
});
|
|
170
186
|
worker.addEventListener("message", ({ data }) => {
|
|
171
187
|
if (data?.type === "scroll") renderer.applyCompositorScroll(data.x, data.y);
|
|
172
188
|
else if (data?.type === "video-report") renderer.videoReport(data);
|
|
189
|
+
else if (data?.type === "frame-release" && Number.isInteger(data.serial)) frameBuffers.release(data.serial);
|
|
173
190
|
else if (data?.type === "frame-ack" && Number.isInteger(data.serial)) {
|
|
174
191
|
framesAcknowledged = Math.max(framesAcknowledged, data.serial);
|
|
175
192
|
frameWaiters = frameWaiters.filter((waiter) => {
|
|
@@ -232,7 +249,12 @@ const renderer = await createPageRenderer({
|
|
|
232
249
|
reportError: (message) => report(`buninu-browser: page error: ${logging ? message : String(message).split("\n")[0]}`),
|
|
233
250
|
consoleMessage: logging ? (level, text) => report(`page console.${level}: ${text}`) : null,
|
|
234
251
|
trace: logging ? (text) => report(`timing: ${text}`) : null,
|
|
235
|
-
|
|
252
|
+
allocateFramePixels: compositor ? bytes => frameBuffers.acquire(bytes) : null,
|
|
253
|
+
onFrame: compositor ? (frame) => {
|
|
254
|
+
const serial = ++framesSent;
|
|
255
|
+
frameBuffers.publish(serial, frame.tile.pixels);
|
|
256
|
+
compositor.postMessage({ type: "frame", serial, ...frame });
|
|
257
|
+
} : null,
|
|
236
258
|
onVideo: compositor ? (message) => compositor.postMessage(message) : null,
|
|
237
259
|
// Whoever waits for load (a screenshot, say) sees the frame painted at load.
|
|
238
260
|
onLifecycle: (name, generation) => {
|
|
@@ -317,6 +339,7 @@ process.on("message", async (message) => {
|
|
|
317
339
|
if (!pending?.page) return;
|
|
318
340
|
pendingFetches.delete(message.id);
|
|
319
341
|
pending.streamController.close();
|
|
342
|
+
pending.finish?.();
|
|
320
343
|
return;
|
|
321
344
|
}
|
|
322
345
|
if (message?.type === "page-fetch-error") {
|
|
@@ -326,6 +349,7 @@ process.on("message", async (message) => {
|
|
|
326
349
|
const error = new Error(message.error || "fetch failed");
|
|
327
350
|
if (pending.head) pending.streamController.error(error);
|
|
328
351
|
else pending.reject(error);
|
|
352
|
+
pending.finish?.();
|
|
329
353
|
return;
|
|
330
354
|
}
|
|
331
355
|
if (message?.type === "fetch-chunk") {
|
|
@@ -357,6 +381,7 @@ process.on("message", async (message) => {
|
|
|
357
381
|
const error = new Error(message.error);
|
|
358
382
|
pending.pipe?.fail(error);
|
|
359
383
|
if (!pending.head) pending.reject(error);
|
|
384
|
+
pending.finish?.();
|
|
360
385
|
return;
|
|
361
386
|
}
|
|
362
387
|
const result = message.result ?? {};
|
|
@@ -374,6 +399,7 @@ process.on("message", async (message) => {
|
|
|
374
399
|
return;
|
|
375
400
|
}
|
|
376
401
|
pending.resolve(result);
|
|
402
|
+
pending.finish?.();
|
|
377
403
|
return;
|
|
378
404
|
}
|
|
379
405
|
if (message?.type === "audio-event") {
|
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
* The event order follows the Media Source spec: appendBuffer returns with
|
|
6
6
|
* updating set, and a later task runs updatestart, the append, then update
|
|
7
7
|
* and updateend. sourceopen fires when an element attaches, not at
|
|
8
|
-
* construction. remove() and abort()
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* construction. remove() and abort() retain original fragment history for
|
|
9
|
+
* decoder reopens and backward seeks. MP4 initialization changes have
|
|
10
|
+
* separate snapshots selected by media time, so configurations do not mix.
|
|
11
11
|
*
|
|
12
12
|
* Codec claims stay inside what this build can demux and decode. Types the
|
|
13
13
|
* engine cannot play are rejected so a page picks another one.
|
|
14
14
|
*/
|
|
15
15
|
import { createBytePipe } from "../media/byte-pipe.js";
|
|
16
|
+
import { createSourceSnapshots } from "../media/source-snapshots.js";
|
|
17
|
+
import { createWebMStream, ebmlHeader } from "../media/webm-stream.js";
|
|
16
18
|
|
|
17
19
|
const BRAND = Symbol("MediaSource");
|
|
18
20
|
const BUFFER_BRAND = Symbol("SourceBuffer");
|
|
@@ -140,53 +142,55 @@ function ebmlUint(bytes, offset, length) {
|
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
/** Actual buffered end of WebM clusters (nanosecond TimecodeScale, millisecond default). */
|
|
143
|
-
function inspectWebMAppend(bytes, state) {
|
|
144
|
-
for (let offset = 0; offset + 4 < bytes.byteLength; offset++) {
|
|
145
|
-
if (bytes[offset] !== 0x2a || bytes[offset + 1] !== 0xd7 || bytes[offset + 2] !== 0xb1) continue;
|
|
146
|
-
const size = ebmlVint(bytes, offset + 3);
|
|
147
|
-
const value = size && ebmlUint(bytes, offset + 3 + size.length, size.value);
|
|
148
|
-
if (value) state.scale = value;
|
|
149
|
-
}
|
|
150
|
-
const scale = state.scale || 1_000_000;
|
|
145
|
+
function inspectWebMAppend(bytes, state, clusterOrigin = null) {
|
|
151
146
|
let latest = null;
|
|
152
147
|
let first = null;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (timeAt + 2 <= end) {
|
|
179
|
-
let relative = (bytes[timeAt] << 8) | bytes[timeAt + 1];
|
|
180
|
-
if (relative & 0x8000) relative -= 0x10000;
|
|
181
|
-
// Opus packets are normally 20 ms; this small tail keeps buffered
|
|
182
|
-
// from ending at the start timestamp of its final packet.
|
|
183
|
-
latest = Math.max(latest ?? 0, clusterTime + relative + 20);
|
|
148
|
+
const tracks = new Map();
|
|
149
|
+
const inspectCluster = (from, limit, origin = 0) => {
|
|
150
|
+
let clusterTime = origin;
|
|
151
|
+
const visit = (start, stop) => {
|
|
152
|
+
for (let cursor = start; cursor < stop;) {
|
|
153
|
+
const element = ebmlHeader(bytes, cursor);
|
|
154
|
+
if (!element) break;
|
|
155
|
+
const data = cursor + element.length, end = Math.min(stop, data + element.size);
|
|
156
|
+
if (element.id === 0xe7) {
|
|
157
|
+
clusterTime = ebmlUint(bytes, data, element.size) ?? clusterTime;
|
|
158
|
+
latest = Math.max(latest ?? 0, clusterTime);
|
|
159
|
+
} else if (element.id === 0xa0) visit(data, end); // BlockGroup
|
|
160
|
+
else if (element.id === 0xa3 || element.id === 0xa1) {
|
|
161
|
+
const track = ebmlVint(bytes, data);
|
|
162
|
+
const timeAt = track ? data + track.length : end;
|
|
163
|
+
if (timeAt + 2 <= end) {
|
|
164
|
+
let relative = (bytes[timeAt] << 8) | bytes[timeAt + 1];
|
|
165
|
+
if (relative & 0x8000) relative -= 0x10000;
|
|
166
|
+
first = Math.min(first ?? clusterTime + relative, clusterTime + relative);
|
|
167
|
+
// Opus packets are normally 20 ms; include the final packet.
|
|
168
|
+
latest = Math.max(latest ?? 0, clusterTime + relative + 20);
|
|
169
|
+
const range = tracks.get(track.value);
|
|
170
|
+
tracks.set(track.value, [Math.min(range?.[0] ?? Infinity, clusterTime + relative),
|
|
171
|
+
Math.max(range?.[1] ?? 0, clusterTime + relative + 20)]);
|
|
172
|
+
}
|
|
184
173
|
}
|
|
174
|
+
cursor = end;
|
|
185
175
|
}
|
|
186
|
-
|
|
176
|
+
};
|
|
177
|
+
visit(from, limit);
|
|
178
|
+
};
|
|
179
|
+
const visit = (from, limit) => {
|
|
180
|
+
for (let offset = from; offset < limit;) {
|
|
181
|
+
const element = ebmlHeader(bytes, offset);
|
|
182
|
+
if (!element) break;
|
|
183
|
+
const data = offset + element.length, end = Math.min(limit, data + element.size);
|
|
184
|
+
if (element.id === 0x18538067 || element.id === 0x1549a966) visit(data, end);
|
|
185
|
+
else if (element.id === 0x2ad7b1) state.scale = ebmlUint(bytes, data, element.size) || state.scale;
|
|
186
|
+
else if (element.id === 0x1f43b675) inspectCluster(data, end);
|
|
187
|
+
offset = end;
|
|
187
188
|
}
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
};
|
|
190
|
+
if (clusterOrigin != null) inspectCluster(0, bytes.length, clusterOrigin);
|
|
191
|
+
else visit(0, bytes.length);
|
|
192
|
+
const scale = state.scale || 1_000_000;
|
|
193
|
+
state.appendTracks = new Map([...tracks].map(([track, [start, end]]) => [track, [start * scale / 1e9, end * scale / 1e9]]));
|
|
190
194
|
state.appendStart = first == null ? null : first * scale / 1e9;
|
|
191
195
|
return latest == null ? null : latest * scale / 1e9;
|
|
192
196
|
}
|
|
@@ -422,7 +426,12 @@ export function createMediaSourceScope({ origin, schedule, DOMException, onAppen
|
|
|
422
426
|
Object.defineProperty(this, BUFFER_BRAND, { value: true });
|
|
423
427
|
this._source = source;
|
|
424
428
|
this._mime = String(mime);
|
|
425
|
-
this.
|
|
429
|
+
this._snapshots = /^(video|audio)\/mp4\b/i.test(this._mime) ? createSourceSnapshots(inspectMp4Append) : null;
|
|
430
|
+
this._pipe = createBytePipe({ retainBytes: !this._snapshots });
|
|
431
|
+
if (this._snapshots) {
|
|
432
|
+
this._pipe.snapshotAt = time => this._snapshots.at(time);
|
|
433
|
+
this._pipe.bytes = () => this._snapshots.at(0).bytes;
|
|
434
|
+
}
|
|
426
435
|
this._updating = false;
|
|
427
436
|
this._operation = 0;
|
|
428
437
|
this._removed = false;
|
|
@@ -433,6 +442,8 @@ export function createMediaSourceScope({ origin, schedule, DOMException, onAppen
|
|
|
433
442
|
this._accountedBytes = 0;
|
|
434
443
|
this._mp4 = {};
|
|
435
444
|
this._webm = {};
|
|
445
|
+
this._webmCovered = new Map();
|
|
446
|
+
this._webmStream = /^(video|audio)\/webm\b/i.test(this._mime) ? createWebMStream() : null;
|
|
436
447
|
this._hasSegmentTimeline = false;
|
|
437
448
|
this._hasMediaSegment = false;
|
|
438
449
|
this._appendWindowStart = 0;
|
|
@@ -504,16 +515,12 @@ export function createMediaSourceScope({ origin, schedule, DOMException, onAppen
|
|
|
504
515
|
const bytes = copyBytes(data);
|
|
505
516
|
const inspected = /^video\/mp4|^audio\/mp4/i.test(this._mime) ? this._mp4
|
|
506
517
|
: /^video\/webm|^audio\/webm/i.test(this._mime) ? this._webm : null;
|
|
507
|
-
|
|
508
|
-
: inspected === this._webm ? inspectWebMAppend(bytes, this._webm) : null;
|
|
518
|
+
let segmentEnd = inspected === this._mp4 ? inspectMp4Append(bytes, this._mp4) : null;
|
|
509
519
|
// Media the buffer already holds, sent again (players retry a segment
|
|
510
520
|
// on a slow network): the concatenation would play it twice, so it is
|
|
511
521
|
// left out. Its events fire as for any append.
|
|
512
522
|
const start = inspected && !inspected.appendHasHeader ? inspected.appendStart : null;
|
|
513
|
-
const duplicate = start != null && segmentEnd != null && covers(this._covered, start, segmentEnd);
|
|
514
|
-
const hasWebMCluster = /^video\/webm|^audio\/webm/i.test(this._mime)
|
|
515
|
-
&& bytes.indexOf(0x1f) >= 0 && bytes.some((value, index) => value === 0x1f
|
|
516
|
-
&& bytes[index + 1] === 0x43 && bytes[index + 2] === 0xb6 && bytes[index + 3] === 0x75);
|
|
523
|
+
const duplicate = !this._snapshots && start != null && segmentEnd != null && covers(this._covered, start, segmentEnd);
|
|
517
524
|
if (appended + bytes.byteLength > appendQuota) {
|
|
518
525
|
throw new DOMException("The SourceBuffer is full.", "QuotaExceededError");
|
|
519
526
|
}
|
|
@@ -525,13 +532,15 @@ export function createMediaSourceScope({ origin, schedule, DOMException, onAppen
|
|
|
525
532
|
schedule(() => {
|
|
526
533
|
if (this._removed || operation !== this._operation) return;
|
|
527
534
|
emitNow(this, "updatestart");
|
|
528
|
-
if (
|
|
535
|
+
if (this._webmStream) {
|
|
536
|
+
segmentEnd = this._appendWebM(this._webmStream.append(bytes), bytes.length);
|
|
537
|
+
} else if (!duplicate && (!this._snapshots || this._snapshots.append(bytes))) {
|
|
529
538
|
this._pipe.append(bytes);
|
|
530
539
|
appended += bytes.byteLength;
|
|
531
540
|
this._accountedBytes += bytes.byteLength;
|
|
532
541
|
if (start != null && segmentEnd != null) addCovered(this._covered, start, segmentEnd);
|
|
533
542
|
}
|
|
534
|
-
if (this._mp4.hasMediaSegment
|
|
543
|
+
if (this._mp4.hasMediaSegment) this._hasMediaSegment = true;
|
|
535
544
|
if (segmentEnd != null) {
|
|
536
545
|
if (!this._hasSegmentTimeline) this._end = segmentEnd;
|
|
537
546
|
else this._end = Math.max(this._end, segmentEnd);
|
|
@@ -548,10 +557,66 @@ export function createMediaSourceScope({ origin, schedule, DOMException, onAppen
|
|
|
548
557
|
});
|
|
549
558
|
}
|
|
550
559
|
|
|
560
|
+
/** Complete finite Clusters, or complete Blocks from an unknown-size
|
|
561
|
+
* live Cluster, enter the decoder and contribute buffered ranges.
|
|
562
|
+
* Retries may be split differently or share an append with new media;
|
|
563
|
+
* comparing complete elements handles both cases.
|
|
564
|
+
*/
|
|
565
|
+
_appendWebM(elements, incoming = 0) {
|
|
566
|
+
let retained = incoming, latest = null;
|
|
567
|
+
for (const { id, bytes, cluster, closedCluster } of elements) {
|
|
568
|
+
if (closedCluster) {
|
|
569
|
+
if (!closedCluster.accepted) retained -= closedCluster.prefix.reduce((sum, part) => sum + part.length, 0);
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const end = inspectWebMAppend(bytes, this._webm, cluster?.time);
|
|
573
|
+
const start = this._webm.appendStart;
|
|
574
|
+
if (id === 0x1f43b675) {
|
|
575
|
+
this._hasMediaSegment = true;
|
|
576
|
+
if (end != null) latest = Math.max(latest ?? end, end);
|
|
577
|
+
if (start != null && end != null) {
|
|
578
|
+
// Different tracks can carry samples at the same timestamp.
|
|
579
|
+
// In particular, incremental unknown-size Clusters expose one
|
|
580
|
+
// Block at a time; an audio Block must not discard a video Block.
|
|
581
|
+
const tracks = this._webm.appendTracks;
|
|
582
|
+
if (tracks.size && [...tracks].every(([track, range]) => covers(this._webmCovered.get(track) ?? [], ...range))) {
|
|
583
|
+
retained -= bytes.length; continue;
|
|
584
|
+
}
|
|
585
|
+
for (const [track, range] of tracks) {
|
|
586
|
+
if (!this._webmCovered.has(track)) this._webmCovered.set(track, []);
|
|
587
|
+
addCovered(this._webmCovered.get(track), ...range);
|
|
588
|
+
}
|
|
589
|
+
addCovered(this._covered, start, end);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (cluster && this._webmCluster !== cluster.token) {
|
|
593
|
+
for (const prefix of cluster.prefix) this._pipe.append(prefix);
|
|
594
|
+
this._webmCluster = cluster.token;
|
|
595
|
+
cluster.token.accepted = true;
|
|
596
|
+
}
|
|
597
|
+
this._pipe.append(bytes);
|
|
598
|
+
}
|
|
599
|
+
// A retry can span remove(), which may already have released the
|
|
600
|
+
// quota charged for its first pieces. Refund only this buffer's
|
|
601
|
+
// remaining charge, never another SourceBuffer's bytes.
|
|
602
|
+
retained = Math.max(-this._accountedBytes, retained);
|
|
603
|
+
appended += retained;
|
|
604
|
+
this._accountedBytes += retained;
|
|
605
|
+
return latest;
|
|
606
|
+
}
|
|
607
|
+
|
|
551
608
|
abort() {
|
|
552
609
|
if (this._source.readyState !== "open") {
|
|
553
610
|
throw new DOMException("MediaSource is not open.", "InvalidStateError");
|
|
554
611
|
}
|
|
612
|
+
// Reset parser input even between append operations: the next append
|
|
613
|
+
// may start a new initialization instead of completing this element.
|
|
614
|
+
if (this._webmStream) {
|
|
615
|
+
const released = Math.min(this._accountedBytes, this._webmStream.pendingSize);
|
|
616
|
+
this._webmStream.reset();
|
|
617
|
+
this._accountedBytes -= released;
|
|
618
|
+
appended -= released;
|
|
619
|
+
}
|
|
555
620
|
if (!this._updating) return;
|
|
556
621
|
this._operation += 1;
|
|
557
622
|
this._updating = false;
|
|
@@ -683,7 +748,13 @@ export function createMediaSourceScope({ origin, schedule, DOMException, onAppen
|
|
|
683
748
|
}
|
|
684
749
|
for (const buffer of this._buffers) {
|
|
685
750
|
if (error) buffer._pipe.fail(new Error(error));
|
|
686
|
-
else
|
|
751
|
+
else {
|
|
752
|
+
if (buffer._webmStream) {
|
|
753
|
+
const end = buffer._appendWebM(buffer._webmStream.finish());
|
|
754
|
+
if (end != null) { buffer._end = Math.max(buffer._end, end); buffer._hasSegmentTimeline = true; }
|
|
755
|
+
}
|
|
756
|
+
buffer._pipe.finish();
|
|
757
|
+
}
|
|
687
758
|
}
|
|
688
759
|
if (!Number.isFinite(this._duration)) {
|
|
689
760
|
const end = Math.max(0, ...this._buffers.map((buffer) => buffer._end));
|