@torrent-tv/proxy 2.87.14 → 2.87.16
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 +13 -0
- package/docs/browser-proxy-contract.md +9 -0
- package/package.json +1 -1
- package/routes/api/transcode-sessions/net-report/post.js +1 -0
- package/services/encode/EncodeRun.js +43 -6
- package/services/encode/EncodeRuns.js +2 -1
- package/services/encode/segment-formats/fmp4.js +10 -2
- package/services/encode/segment-formats/index.js +1 -0
- package/services/encode/segment-formats/mp4-boxes.js +137 -1
- package/services/server/SegmentServing.js +5 -1
- package/services/server/ViewerRequests.js +26 -6
- package/services/server/playback-readiness.js +139 -143
- package/services/storage/piece-store/piece-disk-store.js +23 -0
- package/services/storage/piece-store/shared-piece-store.js +7 -2
- package/services/storage/segment-store/SegmentStore.js +36 -1
- package/services/viewer/Viewer.js +6 -0
- package/test/encode-orchestrator.test.js +3 -3
- package/test/encode-plan-viewers.test.js +1 -1
- package/test/encode-run.test.js +1 -0
- package/test/fmp4-presentation-clock.test.js +72 -0
- package/test/piece-disk-store.test.js +20 -0
- package/test/piece-store-reservations.test.js +26 -1
- package/test/playback-readiness.test.js +82 -6
- package/test/playback-start-regressions.test.js +99 -0
- package/test/segment-store.test.js +1 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
4
|
+
import { readPresentationRanges, walkBoxes } from "../services/encode/segment-formats/mp4-boxes.js";
|
|
5
|
+
|
|
6
|
+
function box(type, body) {
|
|
7
|
+
const header = Buffer.alloc(8);
|
|
8
|
+
header.writeUInt32BE(body.length + 8);
|
|
9
|
+
header.write(type, 4);
|
|
10
|
+
return Buffer.concat([header, body]);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function init(emptyMs, mediaTime) {
|
|
14
|
+
const mvhd = Buffer.alloc(24);
|
|
15
|
+
mvhd.writeUInt32BE(1000, 12);
|
|
16
|
+
const tkhd = Buffer.alloc(20);
|
|
17
|
+
tkhd.writeUInt32BE(1, 12);
|
|
18
|
+
const mdhd = Buffer.alloc(24);
|
|
19
|
+
mdhd.writeUInt32BE(24000, 12);
|
|
20
|
+
const elst = Buffer.alloc(32);
|
|
21
|
+
elst.writeUInt32BE(2, 4);
|
|
22
|
+
elst.writeUInt32BE(emptyMs, 8);
|
|
23
|
+
elst.writeInt32BE(-1, 12);
|
|
24
|
+
elst.writeUInt32BE(1000, 20);
|
|
25
|
+
elst.writeInt32BE(mediaTime, 24);
|
|
26
|
+
return box("moov", Buffer.concat([box("mvhd", mvhd), box("trak", Buffer.concat([
|
|
27
|
+
box("tkhd", tkhd), box("edts", box("elst", elst)), box("mdia", box("mdhd", mdhd))
|
|
28
|
+
]))]));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function fragment(decodeTime, composition) {
|
|
32
|
+
const tfhd = Buffer.alloc(12);
|
|
33
|
+
tfhd.writeUIntBE(8, 1, 3);
|
|
34
|
+
tfhd.writeUInt32BE(1, 4);
|
|
35
|
+
tfhd.writeUInt32BE(1000, 8);
|
|
36
|
+
const tfdt = Buffer.alloc(12);
|
|
37
|
+
tfdt[0] = 1;
|
|
38
|
+
tfdt.writeBigUInt64BE(BigInt(decodeTime), 4);
|
|
39
|
+
const trun = Buffer.alloc(12);
|
|
40
|
+
trun.writeUIntBE(0x800, 1, 3);
|
|
41
|
+
trun.writeUInt32BE(1, 4);
|
|
42
|
+
trun.writeUInt32BE(composition, 8);
|
|
43
|
+
return box("moof", box("traf", Buffer.concat([
|
|
44
|
+
box("tfhd", tfhd), box("tfdt", tfdt), box("trun", trun)
|
|
45
|
+
])));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
test("a shared init preserves presentation times without adding composition delay twice", () => {
|
|
49
|
+
const shared = fmp4Format.prepareSharedInit(init(83, 2000));
|
|
50
|
+
const own = init(4125, 3000);
|
|
51
|
+
const media = Buffer.concat([fragment(0, 3000), fragment(48000, 3000)]);
|
|
52
|
+
const raw = Buffer.concat([own, media]);
|
|
53
|
+
const prepared = fmp4Format.prepareSegmentBytes(media, {
|
|
54
|
+
startSeconds: 4.125, initBytes: shared, rawBytes: raw
|
|
55
|
+
});
|
|
56
|
+
assert.deepEqual(readPresentationRanges(Buffer.concat([shared, prepared])), readPresentationRanges(raw));
|
|
57
|
+
const times = [];
|
|
58
|
+
walkBoxes(prepared, (type, start) => {
|
|
59
|
+
if (type === "tfdt") times.push(Number(prepared.readBigUInt64BE(start + 4)));
|
|
60
|
+
});
|
|
61
|
+
assert.deepEqual(times, [98000, 146000]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("a shared init first read after a seek also supports returning to the beginning", () => {
|
|
65
|
+
const shared = fmp4Format.prepareSharedInit(init(304083, 2000));
|
|
66
|
+
const media = fragment(0, 2000);
|
|
67
|
+
const raw = Buffer.concat([init(83, 2000), media]);
|
|
68
|
+
const prepared = fmp4Format.prepareSegmentBytes(media, {
|
|
69
|
+
startSeconds: 0.083, initBytes: shared, rawBytes: raw
|
|
70
|
+
});
|
|
71
|
+
assert.deepEqual(readPresentationRanges(Buffer.concat([shared, prepared])), readPresentationRanges(raw));
|
|
72
|
+
});
|
|
@@ -18,6 +18,26 @@ import { PieceDiskStore } from "../services/storage/piece-store/piece-disk-store
|
|
|
18
18
|
|
|
19
19
|
const PIECE = 4096;
|
|
20
20
|
|
|
21
|
+
test("bytes held before a memory wait cannot be forgotten or evicted", async () => {
|
|
22
|
+
const { store } = await makeStore(PIECE);
|
|
23
|
+
try {
|
|
24
|
+
await store.write(0, Buffer.alloc(PIECE, 7));
|
|
25
|
+
const release = store.hold(0);
|
|
26
|
+
store.forget(0);
|
|
27
|
+
store.forgetBehind([1]);
|
|
28
|
+
await store.write(1, Buffer.alloc(PIECE, 8));
|
|
29
|
+
const target = Buffer.alloc(PIECE);
|
|
30
|
+
assert.equal(await store.read(0, target), PIECE);
|
|
31
|
+
assert.equal(target[0], 7);
|
|
32
|
+
release();
|
|
33
|
+
release();
|
|
34
|
+
store.forget(0);
|
|
35
|
+
assert.equal(store.has(0), false);
|
|
36
|
+
} finally {
|
|
37
|
+
await store.destroy();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
21
41
|
/**
|
|
22
42
|
* @param {number | null} allowanceBytes
|
|
23
43
|
* @returns {Promise<{ store: PieceDiskStore, directory: string, clock: { at: number } }>}
|
|
@@ -22,6 +22,7 @@ const CHUNK = 1024;
|
|
|
22
22
|
*/
|
|
23
23
|
function makeDisk({ failWrite = false, failRead = false, holdWrites = false } = {}) {
|
|
24
24
|
const stored = new Map();
|
|
25
|
+
const reading = new Map();
|
|
25
26
|
/** @type {Array<() => void>} */
|
|
26
27
|
const held = [];
|
|
27
28
|
return {
|
|
@@ -61,6 +62,15 @@ function makeDisk({ failWrite = false, failRead = false, holdWrites = false } =
|
|
|
61
62
|
has(index) {
|
|
62
63
|
return stored.has(index);
|
|
63
64
|
},
|
|
65
|
+
hold(index) {
|
|
66
|
+
if (!stored.has(index)) return null;
|
|
67
|
+
reading.set(index, (reading.get(index) ?? 0) + 1);
|
|
68
|
+
return () => {
|
|
69
|
+
const count = reading.get(index) - 1;
|
|
70
|
+
if (count > 0) reading.set(index, count);
|
|
71
|
+
else reading.delete(index);
|
|
72
|
+
};
|
|
73
|
+
},
|
|
64
74
|
async write(index, bytes) {
|
|
65
75
|
if (holdWrites) {
|
|
66
76
|
await new Promise((resolve) => held.push(resolve));
|
|
@@ -82,7 +92,7 @@ function makeDisk({ failWrite = false, failRead = false, holdWrites = false } =
|
|
|
82
92
|
return bytes.length;
|
|
83
93
|
},
|
|
84
94
|
forget(index) {
|
|
85
|
-
stored.delete(index);
|
|
95
|
+
if (!reading.has(index)) stored.delete(index);
|
|
86
96
|
},
|
|
87
97
|
async close() {},
|
|
88
98
|
async destroy() {
|
|
@@ -123,6 +133,21 @@ const get = (store, index) =>
|
|
|
123
133
|
store.get(index, undefined, (error, bytes) => (error ? reject(error) : resolve(bytes)))
|
|
124
134
|
);
|
|
125
135
|
|
|
136
|
+
test("a revival holds disk bytes across the asynchronous memory reservation", async () => {
|
|
137
|
+
const { store, disk } = makeStore();
|
|
138
|
+
disk.stored.set(0, piece(0));
|
|
139
|
+
try {
|
|
140
|
+
const revival = store.reside(0);
|
|
141
|
+
disk.forget(0);
|
|
142
|
+
const located = await revival;
|
|
143
|
+
assert.deepEqual(Buffer.from(located.buffer, located.offset, located.length), piece(0));
|
|
144
|
+
disk.forget(0);
|
|
145
|
+
assert.equal(disk.has(0), false);
|
|
146
|
+
} finally {
|
|
147
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
126
151
|
/**
|
|
127
152
|
* Wait until a condition holds, rather than for a chosen interval — a test that
|
|
128
153
|
* sleeps samples, it does not check (roadmap item 54).
|
|
@@ -107,12 +107,14 @@ test("does not produce an ETA without a client-link measurement", () => {
|
|
|
107
107
|
assert.equal(forecast.reason, "link-rate-unavailable");
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
-
test("rate
|
|
110
|
+
test("rate forecast does not extrapolate acceleration beyond observed service", () => {
|
|
111
111
|
const trend = new RateTrend();
|
|
112
112
|
trend.add(9_000, 2);
|
|
113
113
|
trend.add(10_000, 4);
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
const predicted = forecastRate(trend.snapshot(), 10_000, 2);
|
|
116
|
+
assert.ok(predicted > 3 && predicted < 4);
|
|
117
|
+
assert.ok(forecastRate(trend.snapshot(), 100_000, 2) >= 3);
|
|
116
118
|
});
|
|
117
119
|
|
|
118
120
|
test("uses each track's own segment boundaries and requires continuous coverage on both tracks", () => {
|
|
@@ -331,7 +333,7 @@ test("shares one torrent download rate across distinct video and audio files", (
|
|
|
331
333
|
assert.ok(Math.abs(forecast.delaySeconds - 4.032) < 1e-8);
|
|
332
334
|
});
|
|
333
335
|
|
|
334
|
-
test("
|
|
336
|
+
test("does not invent future encode acceleration to start an unsustainable output", () => {
|
|
335
337
|
const now = 1_000;
|
|
336
338
|
const forecast = predictPlaybackReadiness({
|
|
337
339
|
now,
|
|
@@ -360,7 +362,81 @@ test("finds a safe interval when rising encode speed and falling link speed make
|
|
|
360
362
|
});
|
|
361
363
|
|
|
362
364
|
assert.equal(forecast.ready, false);
|
|
363
|
-
assert.equal(forecast.reason, "
|
|
364
|
-
assert.
|
|
365
|
-
|
|
365
|
+
assert.equal(forecast.reason, "no-safe-start-found");
|
|
366
|
+
assert.equal(forecast.delaySeconds, null);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("a faster old probe cannot turn positive delivery into a permanent zero rate", () => {
|
|
370
|
+
const trend = new RateTrend(120);
|
|
371
|
+
trend.add(0, 112_400_000);
|
|
372
|
+
trend.add(66_757, 37_640_000);
|
|
373
|
+
assert.equal(forecastRate(trend.snapshot(), 20 * 60_000, 1443.97), 37_640_000);
|
|
374
|
+
assert.equal(trend.add(66_757, 37_640_000), false);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("the measurement window retains one boundary sample and discards older history", () => {
|
|
378
|
+
const trend = new RateTrend(2);
|
|
379
|
+
trend.add(0, 100);
|
|
380
|
+
trend.add(1_000, 40);
|
|
381
|
+
trend.add(4_000, 20);
|
|
382
|
+
assert.equal(trend.snapshot().count, 2);
|
|
383
|
+
assert.equal(trend.snapshot().meanValue, 30);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
function preparedEpisode(bufferLimitSeconds, linkReadings) {
|
|
387
|
+
const durationSeconds = 1443.97;
|
|
388
|
+
const boundaries = [0, 10.01, 18.977, 29.488];
|
|
389
|
+
for (let index = 4; index <= 197; index += 1) {
|
|
390
|
+
boundaries.push(29.488 + (durationSeconds - 29.488) * (index - 3) / 194);
|
|
391
|
+
}
|
|
392
|
+
const segments = boundaries.slice(0, -1).map((startSeconds, index) => ({
|
|
393
|
+
index, startSeconds, endSeconds: boundaries[index + 1]
|
|
394
|
+
}));
|
|
395
|
+
const tracks = [9_257_176.8, 189_000].map((bitsPerMediaSecond, index) => ({
|
|
396
|
+
id: String(index),
|
|
397
|
+
sourceIds: ["source"],
|
|
398
|
+
processedSeconds: 1437.269,
|
|
399
|
+
bitsPerMediaSecond,
|
|
400
|
+
readings: [],
|
|
401
|
+
segments,
|
|
402
|
+
readySegmentIndices: segments.map(({ index }) => index),
|
|
403
|
+
segmentSizesBytes: new Map(segments.map((segment) => [segment.index,
|
|
404
|
+
(segment.endSeconds - segment.startSeconds) * bitsPerMediaSecond / 8]))
|
|
405
|
+
}));
|
|
406
|
+
return {
|
|
407
|
+
now: 20 * 60_000,
|
|
408
|
+
positionSeconds: 0,
|
|
409
|
+
durationSeconds,
|
|
410
|
+
bufferedAheadSeconds: 19.187,
|
|
411
|
+
bufferLimitSeconds,
|
|
412
|
+
reserveSeconds: 5.674,
|
|
413
|
+
lookaheadSeconds: 120,
|
|
414
|
+
requiredAudio: true,
|
|
415
|
+
sources: [{ id: "source", complete: false, bytesPerMediaSecond: 1, readings: [{ at: 0, value: 0 }] }],
|
|
416
|
+
tracks,
|
|
417
|
+
linkReadings
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
test("starts a prepared episode despite an old declining link measurement", () => {
|
|
422
|
+
const forecast = predictPlaybackReadiness(preparedEpisode(120, [
|
|
423
|
+
{ at: 0, value: 112_400_000 }, { at: 66_757, value: 37_640_000 }
|
|
424
|
+
]));
|
|
425
|
+
assert.equal(forecast.preparedSegments, 394);
|
|
426
|
+
assert.equal(forecast.ready, true);
|
|
427
|
+
assert.equal(forecast.reason, "trajectory-safe-now");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("the measured buffer proves capacity even when the loader reports a smaller target", () => {
|
|
431
|
+
const forecast = predictPlaybackReadiness(preparedEpisode(9.092, [{ at: 0, value: 37_640_000 }]));
|
|
432
|
+
assert.equal(forecast.ready, true);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("a measured source stop is not replaced by hypothetical future recovery", () => {
|
|
436
|
+
const forecast = predictPlaybackReadiness(input({ sources: [{
|
|
437
|
+
id: "source", complete: false, bytesPerMediaSecond: 1,
|
|
438
|
+
readings: [{ at: 9_000, value: 10 }, { at: 10_000, value: 0 }]
|
|
439
|
+
}] }));
|
|
440
|
+
assert.equal(forecast.ready, false);
|
|
441
|
+
assert.equal(forecast.delaySeconds, null);
|
|
366
442
|
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import { EncodeRun } from "../services/encode/EncodeRun.js";
|
|
5
|
+
import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
|
|
6
|
+
import { predictPlaybackReadiness } from "../services/server/playback-readiness.js";
|
|
7
|
+
|
|
8
|
+
function encode() {
|
|
9
|
+
const child = new EventEmitter();
|
|
10
|
+
child.stdio = [null, new EventEmitter(), new EventEmitter(), new EventEmitter()];
|
|
11
|
+
child.stderr = child.stdio[2];
|
|
12
|
+
child.kill = () => {};
|
|
13
|
+
const published = [];
|
|
14
|
+
const ended = [];
|
|
15
|
+
const run = new EncodeRun({
|
|
16
|
+
address: "fake", from: 0, to: 1, encoder: new SoftwareEncoder(),
|
|
17
|
+
buildArgs: () => [], spawn: () => child, logger: { info() {}, warn() {} },
|
|
18
|
+
indexOfName: (name) => Number(name),
|
|
19
|
+
onClosed: (name) => { published.push(name); return name; },
|
|
20
|
+
onEnded: (value) => ended.push(value)
|
|
21
|
+
});
|
|
22
|
+
return { child, run, published, ended };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("a clean short input never publishes its flushed tail", () => {
|
|
26
|
+
const { child, published, ended } = encode();
|
|
27
|
+
child.stderr.emit("data", "Stream ends prematurely at 1048576");
|
|
28
|
+
child.stdio[3].emit("data", "0\n");
|
|
29
|
+
child.emit("close", 0, null);
|
|
30
|
+
assert.deepEqual(published, []);
|
|
31
|
+
assert.equal(ended[0].ending, "short");
|
|
32
|
+
assert.equal(ended[0].reached, -1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("completed cuts survive a failure but its last flushed file does not", () => {
|
|
36
|
+
const { child, published } = encode();
|
|
37
|
+
child.stdio[3].emit("data", "0\n");
|
|
38
|
+
child.stderr.emit("data", "Stream ends prematurely at 1048576");
|
|
39
|
+
child.stdio[3].emit("data", "1\n");
|
|
40
|
+
child.emit("close", 0, null);
|
|
41
|
+
assert.deepEqual(published, ["0"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("normal completion drains the filename pipe before publishing the tail", () => {
|
|
45
|
+
const { child, published, ended } = encode();
|
|
46
|
+
child.emit("exit", 0, null);
|
|
47
|
+
assert.deepEqual(ended, []);
|
|
48
|
+
child.stdio[3].emit("data", "0\n1\n");
|
|
49
|
+
child.emit("close", 0, null);
|
|
50
|
+
assert.deepEqual(published, ["0", "1"]);
|
|
51
|
+
assert.equal(ended[0].ending, "complete");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("stopping retains an earlier closed cut and rejects the signal-flushed tail", () => {
|
|
55
|
+
const { child, run, published } = encode();
|
|
56
|
+
child.stdio[3].emit("data", "0\n");
|
|
57
|
+
run.stop("a different range is needed");
|
|
58
|
+
child.stdio[3].emit("data", "1\n");
|
|
59
|
+
child.emit("close", null, "SIGTERM");
|
|
60
|
+
assert.deepEqual(published, ["0"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function state(clientRanges) {
|
|
64
|
+
return {
|
|
65
|
+
now: 10_000, positionSeconds: 0, durationSeconds: 8,
|
|
66
|
+
bufferedAheadSeconds: 8, bufferLimitSeconds: 8, reserveSeconds: 0, lookaheadSeconds: 8,
|
|
67
|
+
sources: [{ id: "source", complete: true }],
|
|
68
|
+
linkReadings: [{ at: 10_000, value: 80 }],
|
|
69
|
+
tracks: [{ id: "video", sourceIds: ["source"], processedSeconds: 8,
|
|
70
|
+
bitsPerMediaSecond: 80, readings: [{ at: 10_000, value: 2 }],
|
|
71
|
+
readySegmentIndices: [0, 1], segmentSizesBytes: new Map([[0, 40], [1, 40]]),
|
|
72
|
+
clientRanges, segments: [{ index: 0, startSeconds: 0, endSeconds: 4 },
|
|
73
|
+
{ index: 1, startSeconds: 4, endSeconds: 8 }] }]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
test("future browser ranges are retained rather than transferred again", () => {
|
|
78
|
+
const forecast = predictPlaybackReadiness(state([{ start: 4, end: 8 }]));
|
|
79
|
+
assert.equal(forecast.ready, false);
|
|
80
|
+
assert.equal(forecast.reason, "minimum-safe-delay");
|
|
81
|
+
assert.ok(Math.abs(forecast.delaySeconds - 4) < 1e-8);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("a real media hole is not hidden by the scalar buffer or ready file count", () => {
|
|
85
|
+
const input = state([{ start: 0, end: 3 }, { start: 4, end: 8 }]);
|
|
86
|
+
input.tracks[0].segments[0].mediaRanges = [{ start: 0, end: 3 }];
|
|
87
|
+
const forecast = predictPlaybackReadiness(input);
|
|
88
|
+
assert.equal(forecast.ready, false);
|
|
89
|
+
assert.equal(forecast.bufferedSeconds, 3);
|
|
90
|
+
assert.equal(forecast.reason, "no-safe-start-found");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("missing work before global progress can still be produced", () => {
|
|
94
|
+
const input = state([]);
|
|
95
|
+
input.tracks[0].readySegmentIndices = [1];
|
|
96
|
+
const forecast = predictPlaybackReadiness(input);
|
|
97
|
+
assert.equal(forecast.reason, "minimum-safe-delay");
|
|
98
|
+
assert.ok(Number.isFinite(forecast.delaySeconds));
|
|
99
|
+
});
|
|
@@ -113,7 +113,7 @@ test("a piece still being written is not a piece, and its name says so", (t) =>
|
|
|
113
113
|
assert.equal(store.pathOf(KEY, 1), null);
|
|
114
114
|
|
|
115
115
|
// And closing it is one rename, after which it is servable.
|
|
116
|
-
assert.equal(store.publish(KEY, "making-0-00001.mp4", fmp4Format), "segment-00001.mp4");
|
|
116
|
+
assert.equal(store.publish(KEY, "making-0-00001.mp4", { ...fmp4Format, readMediaRanges: undefined }), "segment-00001.mp4");
|
|
117
117
|
assert.equal(store.isClosed(KEY, 1), true);
|
|
118
118
|
});
|
|
119
119
|
|