@torrent-tv/proxy 2.87.18 → 2.87.19
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/services/encode/EncodeRuns.js +6 -1
- package/services/encode/run-command.js +12 -5
- package/services/encode/segment-formats/fmp4.js +3 -1
- package/services/encode/segment-formats/index.js +1 -1
- package/services/encode/segment-formats/mp4-boxes.js +29 -1
- package/services/server/ViewerRequests.js +6 -3
- package/services/server/playback-readiness.js +20 -1
- package/services/storage/piece-store/shared-piece-store.js +13 -10
- package/services/torrent/worker/client.js +11 -11
- package/test/fmp4-presentation-clock.test.js +12 -1
- package/test/piece-store-reservations.test.js +22 -0
- package/test/playback-readiness.test.js +11 -0
- package/test/quality-variants.test.js +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 2.87.19
|
|
2
|
+
|
|
3
|
+
- **Fix**: Return shared source bytes after whole-file assembly and retain the buffer held by concurrent readers instead of replacing it; fail malformed fragments explicitly rather than skipping source bytes.
|
|
4
|
+
- **Fix**: Establish decode timestamp ordering in a private preceding cut before a copied-video restart, preserving leading reordered frames at the requested boundary.
|
|
5
|
+
- **Fix**: Carry variable-rate video presentation across cuts only when decode timestamps prove continuity, and predict unproduced cuts in the measured track clock within the readiness integral.
|
|
6
|
+
|
|
1
7
|
## 2.87.18
|
|
2
8
|
|
|
3
9
|
- **Fix**: Interpret variable-rate video sample presentation holds and movie-clock resolution as media coverage; project prepared intervals onto the browser's reported HLS timestamp offsets per track.
|
package/package.json
CHANGED
|
@@ -832,7 +832,12 @@ export class EncodeRuns {
|
|
|
832
832
|
// Why this encoder exists, recorded with its argument list. It used to be
|
|
833
833
|
// handed to a separate `start` call; there is no separate call now.
|
|
834
834
|
because,
|
|
835
|
-
onClosed: (name) =>
|
|
835
|
+
onClosed: (name) => {
|
|
836
|
+
const index = session.segmentFormat.segmentIndexFromName(
|
|
837
|
+
session.segmentFormat.servedNameOf?.(name) ?? name);
|
|
838
|
+
if (Number.isInteger(index) && index < safeIndex) return null;
|
|
839
|
+
return this.#host.segmentFiles.publish(session.outputKey ?? "", name, session.segmentFormat);
|
|
840
|
+
},
|
|
836
841
|
onEnded: (ended) => this.noteRunEnded(session, run, ended)
|
|
837
842
|
});
|
|
838
843
|
// THE ONE FAULT THAT IS OTHERWISE SILENT, asked before this run produces a
|
|
@@ -358,6 +358,11 @@ export function buildRunCommand({
|
|
|
358
358
|
// the cut points and, when re-encoding, as the keyframes to force — one
|
|
359
359
|
// list, so the two cannot drift apart.
|
|
360
360
|
const explicitTimes = segmentFormat.explicitTimesMuxerArgs?.() ?? null;
|
|
361
|
+
// Matroska seeks begin without the preceding decode timestamps. Copy one
|
|
362
|
+
// preceding cut privately so reordered frames have established DTS before
|
|
363
|
+
// the first requested cut. It is never published over another run's output.
|
|
364
|
+
const inputIndex = !transcodeVideo && !audioOnly && explicitTimes && safeIndex > 0
|
|
365
|
+
? safeIndex - 1 : safeIndex;
|
|
361
366
|
// A COPY is cut by this list whatever grid it ended up on. Even when no
|
|
362
367
|
// keyframe index could be read and the boundaries are a plain grid, saying
|
|
363
368
|
// them outright is what keeps the playlist and the muxer agreeing — ffmpeg
|
|
@@ -382,7 +387,7 @@ export function buildRunCommand({
|
|
|
382
387
|
// playlist and its own cuts agree from the start. What they may not do is
|
|
383
388
|
// move the cuts of a session whose playlist is already being read.
|
|
384
389
|
const gridCutTimes = explicitTimes && (!transcodeVideo || timeline.cutGrid === "keyframe")
|
|
385
|
-
? segmentCutTimesFrom(publishedGridFor(timeline),
|
|
390
|
+
? segmentCutTimesFrom(publishedGridFor(timeline), inputIndex)
|
|
386
391
|
: null;
|
|
387
392
|
// Cut times are stated on the grid, for both branches.
|
|
388
393
|
//
|
|
@@ -443,9 +448,11 @@ export function buildRunCommand({
|
|
|
443
448
|
// back on to reach it; on the uniform grid it is a plain offset. This
|
|
444
449
|
// follows the GRID, not whether the video is re-encoded — a variant cut on
|
|
445
450
|
// the source's keyframes has to seek to them like the copy it accompanies.
|
|
451
|
+
const inputStartSeconds = inputIndex === safeIndex ? startSeconds :
|
|
452
|
+
publishedStartTime(timeline, inputIndex, segmentDurationSec);
|
|
446
453
|
const seekSeconds = timeline.cutGrid === "keyframe"
|
|
447
|
-
?
|
|
448
|
-
:
|
|
454
|
+
? inputStartSeconds + sourceStartTime
|
|
455
|
+
: inputStartSeconds;
|
|
449
456
|
// Two-step seek when we have a real keyframe map: jump to a KNOWN-valid
|
|
450
457
|
// keyframe (coarse, before -i — safe because WE sourced it from ffprobe,
|
|
451
458
|
// not the container's own on-the-fly seek/index) and trim the short
|
|
@@ -477,7 +484,7 @@ export function buildRunCommand({
|
|
|
477
484
|
// The search stays for a grid restored without its source clock, which is the
|
|
478
485
|
// only case that has nothing to carry.
|
|
479
486
|
const carriedKeyframe = timeline.cutGrid === "keyframe" && typeof timeline.sourceStartOf === "function"
|
|
480
|
-
? timeline.sourceStartOf(
|
|
487
|
+
? timeline.sourceStartOf(inputIndex)
|
|
481
488
|
: null;
|
|
482
489
|
const snappedKeyframe = Number.isFinite(carriedKeyframe)
|
|
483
490
|
? carriedKeyframe
|
|
@@ -689,7 +696,7 @@ export function buildRunCommand({
|
|
|
689
696
|
"-segment_time_delta",
|
|
690
697
|
"0.05",
|
|
691
698
|
"-segment_start_number",
|
|
692
|
-
String(
|
|
699
|
+
String(inputIndex),
|
|
693
700
|
// THE ENCODER SAYS WHEN A PIECE IS FINISHED, on a channel of its own.
|
|
694
701
|
//
|
|
695
702
|
// Measured on the addon host 2026-09-05: a name appears in this list when
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
|
+
continuePresentationRanges,
|
|
14
15
|
rebaseSegmentDecodeTimes,
|
|
15
16
|
neutralizeEmptyEdits,
|
|
16
17
|
readPresentationRanges,
|
|
@@ -287,7 +288,8 @@ export const fmp4Format = {
|
|
|
287
288
|
return readPresentationRanges(Buffer.concat([initBytes, served]));
|
|
288
289
|
},
|
|
289
290
|
clientMediaRanges(ranges, context) {
|
|
290
|
-
return translatePresentationRanges(ranges, context.
|
|
291
|
+
return translatePresentationRanges(continuePresentationRanges(ranges, context.nextRanges),
|
|
292
|
+
context.initBytes, context.timestampOffsetSeconds);
|
|
291
293
|
},
|
|
292
294
|
|
|
293
295
|
/**
|
|
@@ -53,7 +53,7 @@ import { mpegtsFormat } from "./mpegts.js";
|
|
|
53
53
|
* { start: number, end: number }[] | undefined} [readMediaRanges] - Actual
|
|
54
54
|
* served presentation intervals, or undefined until their header is available.
|
|
55
55
|
* @property {(ranges: { start: number, end: number }[], context: {
|
|
56
|
-
* initBytes: Buffer | null, timestampOffsetSeconds?: number }) =>
|
|
56
|
+
* initBytes: Buffer | null, timestampOffsetSeconds?: number, nextRanges?: Array<{ start: number, end: number }> }) =>
|
|
57
57
|
* { start: number, end: number }[]} [clientMediaRanges] - Project parsed
|
|
58
58
|
* presentation intervals onto the clock reported by this viewer's player.
|
|
59
59
|
*/
|
|
@@ -176,6 +176,7 @@ export function readPresentationRanges(raw) {
|
|
|
176
176
|
const scales = readTrackTimescales(raw);
|
|
177
177
|
const edits = readTrackEditOffsets(raw);
|
|
178
178
|
const byTrack = new Map();
|
|
179
|
+
const decodeRanges = new Map();
|
|
179
180
|
const defaultDurations = new Map();
|
|
180
181
|
let trackId = null;
|
|
181
182
|
let decodeTime = 0;
|
|
@@ -221,6 +222,10 @@ export function readPresentationRanges(raw) {
|
|
|
221
222
|
if (!(duration > 0)) return;
|
|
222
223
|
const position = (decodeTime + composition) / scale + (edits.get(trackId) ?? 0);
|
|
223
224
|
ranges.push({ start: position, end: position + duration / scale });
|
|
225
|
+
const decode = decodeRanges.get(trackId) ?? [];
|
|
226
|
+
decode.push({ start: decodeTime / scale + (edits.get(trackId) ?? 0),
|
|
227
|
+
end: (decodeTime + duration) / scale + (edits.get(trackId) ?? 0) });
|
|
228
|
+
decodeRanges.set(trackId, decode);
|
|
224
229
|
decodeTime += duration;
|
|
225
230
|
}
|
|
226
231
|
byTrack.set(trackId, ranges);
|
|
@@ -243,7 +248,10 @@ export function readPresentationRanges(raw) {
|
|
|
243
248
|
// Movie edits quantize independent pieces to movie ticks. Preserve that
|
|
244
249
|
// declared resolution at joins; it is not a playback buffer threshold.
|
|
245
250
|
for (const range of merged) range.end += precision;
|
|
246
|
-
|
|
251
|
+
const decoded = decodeRanges.get(id);
|
|
252
|
+
return { id, ranges: merged, kind: kinds.get(id), precision,
|
|
253
|
+
decodeStart: Math.min(...decoded.map(({ start }) => start)),
|
|
254
|
+
decodeEnd: Math.max(...decoded.map(({ end }) => end)) };
|
|
247
255
|
});
|
|
248
256
|
// A multiplexed segment is playable only where every declared track exists.
|
|
249
257
|
const result = intersectPresentationTracks(mergedTracks);
|
|
@@ -251,6 +259,26 @@ export function readPresentationRanges(raw) {
|
|
|
251
259
|
return result;
|
|
252
260
|
}
|
|
253
261
|
|
|
262
|
+
/** A video frame can cross a file boundary when decoding remains continuous. */
|
|
263
|
+
export function continuePresentationRanges(ranges, nextRanges) {
|
|
264
|
+
const tracks = presentationTracks.get(ranges);
|
|
265
|
+
const next = presentationTracks.get(nextRanges);
|
|
266
|
+
if (!tracks || !next) return ranges;
|
|
267
|
+
const continued = tracks.map((track) => {
|
|
268
|
+
const following = next.find(({ id }) => id === track.id);
|
|
269
|
+
const held = track.ranges.map((range) => ({ ...range }));
|
|
270
|
+
if (track.kind === "vide" && following && held.length > 0 && following.ranges.length > 0 &&
|
|
271
|
+
Math.abs(track.decodeEnd - following.decodeStart) <= track.precision + following.precision +
|
|
272
|
+
Number.EPSILON * Math.max(1, following.decodeStart) * 8) {
|
|
273
|
+
held.at(-1).end = Math.max(held.at(-1).end, following.ranges[0].start);
|
|
274
|
+
}
|
|
275
|
+
return { ...track, ranges: held };
|
|
276
|
+
});
|
|
277
|
+
const result = intersectPresentationTracks(continued);
|
|
278
|
+
presentationTracks.set(result, continued);
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
254
282
|
function intersectPresentationTracks(tracks) {
|
|
255
283
|
return tracks.map(({ ranges }) => ranges).reduce((common, ranges) => common.flatMap((left) => ranges
|
|
256
284
|
.map((right) => ({ start: Math.max(left.start, right.start), end: Math.min(left.end, right.end) }))
|
|
@@ -566,10 +566,12 @@ export class ViewerRequests {
|
|
|
566
566
|
const trackRates = this.#trackRateReadings(measurement, output, now);
|
|
567
567
|
const timeline = output.timeline;
|
|
568
568
|
const trackName = output === session ? "video" : "audio";
|
|
569
|
-
const
|
|
570
|
-
|
|
569
|
+
const nativeRanges = Array.from({ length: timeline?.segmentCount ?? 0 }, (_, index) =>
|
|
570
|
+
this.#host.segmentStore.mediaRangesOf(output.outputKey, index, {
|
|
571
571
|
startSeconds: timeline.publishedStartOf(index)
|
|
572
|
-
});
|
|
572
|
+
}));
|
|
573
|
+
const segments = Array.from({ length: timeline?.segmentCount ?? 0 }, (_, index) => {
|
|
574
|
+
const ranges = nativeRanges[index];
|
|
573
575
|
return {
|
|
574
576
|
index,
|
|
575
577
|
startSeconds: timeline.publishedStartOf(index),
|
|
@@ -577,6 +579,7 @@ export class ViewerRequests {
|
|
|
577
579
|
mediaRanges: ranges && output.segmentFormat?.clientMediaRanges ?
|
|
578
580
|
output.segmentFormat.clientMediaRanges(ranges, {
|
|
579
581
|
initBytes: this.#host.segmentStore.initOf(output.outputKey),
|
|
582
|
+
nextRanges: nativeRanges[index + 1],
|
|
580
583
|
timestampOffsetSeconds: viewerReading?.timestampOffsets?.[trackName]
|
|
581
584
|
}) : ranges
|
|
582
585
|
};
|
|
@@ -128,7 +128,7 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
128
128
|
curve: rateCurve(track.readings, now),
|
|
129
129
|
bitsPerMediaSecond: averageBitsPerMediaSecond(track, track.segments,
|
|
130
130
|
new Set(Array.isArray(track.readySegmentIndices) ? track.readySegmentIndices : [])),
|
|
131
|
-
segments: track.segments
|
|
131
|
+
segments: predictedMediaSegments(track.segments)
|
|
132
132
|
.filter((segment) => Number.isInteger(segment?.index) &&
|
|
133
133
|
Number.isFinite(segment?.startSeconds) && Number.isFinite(segment?.endSeconds) &&
|
|
134
134
|
segment.endSeconds > segment.startSeconds && segment.endSeconds > position)
|
|
@@ -388,6 +388,25 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
388
388
|
"minimum-safe-delay", preparedSegments, readiness.bufferedAtStart);
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
/** Predict only unknown cuts in the same clock as the measured track. */
|
|
392
|
+
function predictedMediaSegments(segments) {
|
|
393
|
+
const anchors = segments.filter(({ mediaRanges }) => mediaRanges?.length > 0);
|
|
394
|
+
return segments.map((segment) => {
|
|
395
|
+
if (segment.mediaRanges !== undefined && segment.mediaRanges !== null || anchors.length === 0) return segment;
|
|
396
|
+
const previous = anchors.findLast(({ index }) => index < segment.index);
|
|
397
|
+
const next = anchors.find(({ index }) => index > segment.index);
|
|
398
|
+
const nominalLeft = previous?.endSeconds ?? next.startSeconds;
|
|
399
|
+
const actualLeft = previous?.mediaRanges.at(-1).end ?? next.mediaRanges[0].start;
|
|
400
|
+
const nominalRight = next?.startSeconds ?? nominalLeft;
|
|
401
|
+
const actualRight = next?.mediaRanges[0].start ?? actualLeft;
|
|
402
|
+
const scale = nominalRight > nominalLeft ? (actualRight - actualLeft) / (nominalRight - nominalLeft) : 1;
|
|
403
|
+
return { ...segment, mediaRanges: [{
|
|
404
|
+
start: actualLeft + (segment.startSeconds - nominalLeft) * scale,
|
|
405
|
+
end: actualLeft + (segment.endSeconds - nominalLeft) * scale
|
|
406
|
+
}] };
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
391
410
|
function rateCurve(readings, now) {
|
|
392
411
|
if (readings && Number.isFinite(readings.count) && readings.count > 0 &&
|
|
393
412
|
Number.isFinite(readings.lastAt) && Number.isFinite(readings.lastValue) &&
|
|
@@ -933,13 +933,19 @@ export class SharedPieceStore {
|
|
|
933
933
|
// THREE pieces reported 812 MB committed against 68 MB allowed, 739 blocks
|
|
934
934
|
// allocated and 676 still alive, and the process was killed at 4.37 GB.
|
|
935
935
|
const displaced = this.#buffers.get(index);
|
|
936
|
-
this.#buffers.set(index, buffer);
|
|
937
936
|
if (displaced !== undefined && displaced !== buffer) {
|
|
938
937
|
this.#counters.blocksDisplaced += 1;
|
|
939
|
-
|
|
938
|
+
// A reader may still hold the existing buffer pinned. Verified bytes for
|
|
939
|
+
// this index are identical, so retain that buffer and return the duplicate.
|
|
940
|
+
this.#returnBlock(buffer);
|
|
941
|
+
this.#lru.touch(index);
|
|
942
|
+
this.#noteProgress();
|
|
943
|
+
return displaced;
|
|
940
944
|
}
|
|
945
|
+
this.#buffers.set(index, buffer);
|
|
941
946
|
this.#lru.touch(index);
|
|
942
947
|
this.#noteProgress();
|
|
948
|
+
return buffer;
|
|
943
949
|
}
|
|
944
950
|
|
|
945
951
|
/**
|
|
@@ -1394,12 +1400,9 @@ export class SharedPieceStore {
|
|
|
1394
1400
|
return null;
|
|
1395
1401
|
}
|
|
1396
1402
|
this.#counters.fromWholeFile += 1;
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
// loses is only that the next read pays for this one again.
|
|
1401
|
-
return whole;
|
|
1402
|
-
}
|
|
1403
|
+
// Fragment readers require shared memory, including when the bytes now
|
|
1404
|
+
// live in an assembled file. Wait for the same reservation as disk reads.
|
|
1405
|
+
const release = await this.#claimSlot();
|
|
1403
1406
|
try {
|
|
1404
1407
|
return this.#registerPiece(index, this.#copyIntoNewBuffer(index, whole));
|
|
1405
1408
|
} finally {
|
|
@@ -1432,11 +1435,11 @@ export class SharedPieceStore {
|
|
|
1432
1435
|
this.#returnBlock(target);
|
|
1433
1436
|
throw error;
|
|
1434
1437
|
}
|
|
1435
|
-
this.#registerPiece(index, target);
|
|
1438
|
+
const registered = this.#registerPiece(index, target);
|
|
1436
1439
|
this.#counters.fromDisk += 1;
|
|
1437
1440
|
this.#counters.revivals += 1;
|
|
1438
1441
|
this.#noteRevival(index);
|
|
1439
|
-
return
|
|
1442
|
+
return registered;
|
|
1440
1443
|
} finally {
|
|
1441
1444
|
release?.();
|
|
1442
1445
|
releaseDisk();
|
|
@@ -149,18 +149,18 @@ export class TorrentWorkerClient {
|
|
|
149
149
|
switch (message?.type) {
|
|
150
150
|
case Event.FRAGMENT: {
|
|
151
151
|
const buffer = message.buffer;
|
|
152
|
-
if (!buffer)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
);
|
|
152
|
+
if (!buffer || !Number.isInteger(message.offset) || message.offset < 0 ||
|
|
153
|
+
!Number.isInteger(message.length) || message.length <= 0 ||
|
|
154
|
+
message.offset + message.length > buffer.byteLength) {
|
|
155
|
+
const error = new Error(`Invalid source fragment ${message.offset}+${message.length} ` +
|
|
156
|
+
`in a buffer of ${buffer?.byteLength ?? 0}B (read ${message.id}).`);
|
|
157
|
+
logger.warn(`torrent-worker: ${error.message}`);
|
|
158
|
+
this.#fragmentReaders.get(message.id)?.fail(error);
|
|
159
|
+
this.#fragmentReaders.delete(message.id);
|
|
160
|
+
this.#reads.get(message.id)?.fail(error);
|
|
161
|
+
this.#reads.delete(message.id);
|
|
163
162
|
this.#worker.postMessage({ type: Event.FRAGMENT_DONE, id: message.id });
|
|
163
|
+
void this.#caller.call(Command.CANCEL_READ, { readId: message.id }).catch(() => undefined);
|
|
164
164
|
break;
|
|
165
165
|
}
|
|
166
166
|
// A sequential read walks the file forwards, so each fragment either
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
4
|
-
import { readPresentationRanges, walkBoxes } from "../services/encode/segment-formats/mp4-boxes.js";
|
|
4
|
+
import { continuePresentationRanges, readPresentationRanges, walkBoxes } from "../services/encode/segment-formats/mp4-boxes.js";
|
|
5
5
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
@@ -127,6 +127,17 @@ test("client clock projection accounts for shared edits and audio initPTS", () =
|
|
|
127
127
|
assert.deepEqual(projected, video);
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
+
test("a variable-rate frame crosses a cut only with continuous decode samples", () => {
|
|
131
|
+
const first = readPresentationRanges(Buffer.concat([init(0, 0), fragment(0, 0)]));
|
|
132
|
+
const continuous = readPresentationRanges(Buffer.concat([init(0, 0), fragment(1000, 72000)]));
|
|
133
|
+
assert.equal(continuePresentationRanges(first, continuous)[0].end, continuous[0].start);
|
|
134
|
+
const missing = readPresentationRanges(Buffer.concat([init(0, 0), fragment(24000, 72000)]));
|
|
135
|
+
assert.deepEqual(continuePresentationRanges(first, missing), first);
|
|
136
|
+
const audioFirst = readPresentationRanges(Buffer.concat([init(0, 0, "soun"), fragment(0, 0)]));
|
|
137
|
+
const audioNext = readPresentationRanges(Buffer.concat([init(0, 0, "soun"), fragment(1000, 72000)]));
|
|
138
|
+
assert.deepEqual(continuePresentationRanges(audioFirst, audioNext), audioFirst);
|
|
139
|
+
});
|
|
140
|
+
|
|
130
141
|
test("a viewer retains only finite per-track player clock offsets", () => {
|
|
131
142
|
const viewer = new Viewer("clock-reading", 1);
|
|
132
143
|
viewer.report({ timestampOffsets: { video: -0.083, audio: 0, media: 400 } }, 2);
|
|
@@ -148,6 +148,28 @@ test("a revival holds disk bytes across the asynchronous memory reservation", as
|
|
|
148
148
|
}
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
+
test("an assembled-file revival returns shared bytes and preserves a concurrent pinned reader", async () => {
|
|
152
|
+
const disk = makeDisk();
|
|
153
|
+
const store = new SharedPieceStore(CHUNK, {
|
|
154
|
+
length: CHUNK * 16, memoryBytes: CHUNK * 4, disk,
|
|
155
|
+
readPieceElsewhere: async ({ index }) => piece(index), name: "whole-file-read"
|
|
156
|
+
});
|
|
157
|
+
try {
|
|
158
|
+
store.pin(0);
|
|
159
|
+
const [first, second] = await Promise.all([store.reside(0), store.reside(0)]);
|
|
160
|
+
assert.ok(first.buffer instanceof SharedArrayBuffer);
|
|
161
|
+
assert.equal(first.buffer, second.buffer);
|
|
162
|
+
await put(store, 1);
|
|
163
|
+
await put(store, 2);
|
|
164
|
+
assert.deepEqual(Buffer.from(first.buffer, first.offset, first.length), piece(0));
|
|
165
|
+
assert.equal(store.locate(0).buffer, first.buffer);
|
|
166
|
+
assert.equal(store.stats().outstanding, 0);
|
|
167
|
+
store.unpin(0);
|
|
168
|
+
} finally {
|
|
169
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
151
173
|
/**
|
|
152
174
|
* Wait until a condition holds, rather than for a chosen interval — a test that
|
|
153
175
|
* sleeps samples, it does not check (roadmap item 54).
|
|
@@ -39,6 +39,17 @@ test("finds the first safe start from segments delivered before playback", () =>
|
|
|
39
39
|
assert.ok(Math.abs(forecast.delaySeconds - 1) < 1e-8);
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
test("future cuts follow measured track time without inventing a permanent clock gap", () => {
|
|
43
|
+
const state = input({ bufferedAheadSeconds: 3.9, reserveSeconds: 0 });
|
|
44
|
+
state.tracks[0].clientRanges = [{ start: 0, end: 3.9 }];
|
|
45
|
+
state.tracks[0].segments[0].mediaRanges = [{ start: 0, end: 3.9 }];
|
|
46
|
+
const forecast = predictPlaybackReadiness(state);
|
|
47
|
+
assert.notEqual(forecast.reason, "no-safe-start-found");
|
|
48
|
+
state.tracks[0].segments[1].mediaRanges = [{ start: 4, end: 8 }];
|
|
49
|
+
state.tracks[0].readySegmentIndices = [0, 1];
|
|
50
|
+
assert.equal(predictPlaybackReadiness(state).reason, "no-safe-start-found");
|
|
51
|
+
});
|
|
52
|
+
|
|
42
53
|
test("keeps source-stall reserve in the proxy's prepared timeline, not the capped browser buffer", () => {
|
|
43
54
|
const now = 10_000;
|
|
44
55
|
const forecast = predictPlaybackReadiness({
|
|
@@ -761,8 +761,7 @@ test("a run on the keyframe grid is given no trim to apply", () => {
|
|
|
761
761
|
videoEncoder: { name: "libx264" },
|
|
762
762
|
segmentDurationSec: SEGMENT_SECONDS
|
|
763
763
|
});
|
|
764
|
-
//
|
|
765
|
-
// inherited by the cuts.
|
|
764
|
+
// One input seek establishes decode order in the preceding private cut.
|
|
766
765
|
assert.equal(
|
|
767
766
|
args.filter((one) => one === "-ss").length,
|
|
768
767
|
1,
|
|
@@ -770,9 +769,10 @@ test("a run on the keyframe grid is given no trim to apply", () => {
|
|
|
770
769
|
);
|
|
771
770
|
const seek = Number(args[args.indexOf("-ss") + 1]);
|
|
772
771
|
assert.ok(
|
|
773
|
-
Math.abs(seek - grid.sourceTimes[index]) < 0.2,
|
|
774
|
-
`run at #${index} seeks to ${seek}, not to
|
|
772
|
+
Math.abs(seek - grid.sourceTimes[index - 1]) < 0.2,
|
|
773
|
+
`run at #${index} seeks to ${seek}, not to the preceding keyframe ${grid.sourceTimes[index - 1]}`
|
|
775
774
|
);
|
|
775
|
+
assert.equal(Number(args[args.indexOf("-segment_start_number") + 1]), index - 1);
|
|
776
776
|
}
|
|
777
777
|
});
|
|
778
778
|
|