@torrent-tv/proxy 2.88.2 → 2.88.3

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 CHANGED
@@ -1,3 +1,7 @@
1
+ ## 2.88.3
2
+
3
+ - **Fix**: Forecast audio continuity from coded-frame timestamps and durations using the browser's frame-join boundary. Adjacent AAC fragments that Chrome buffers continuously no longer cause an impossible-start forecast; actual gaps and missing deliveries still prevent an unsafe start. Verified against captured segment metadata and independent Chrome SourceBuffer appends.
4
+
1
5
  ## 2.88.2
2
6
 
3
7
  - **Fix**: Report the first failing playback forecast boundary and per-track ranges instead of only a generic no-safe-start result.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.88.2",
3
+ "version": "2.88.3",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -244,12 +244,20 @@ export function readPresentationRanges(raw) {
244
244
  sample.start - previous.end <= precision + Number.EPSILON * Math.max(1, sample.end) * 8)) {
245
245
  previous.end = Math.max(previous.end, sample.end);
246
246
  } else merged.push({ ...sample });
247
+ // Preserve coded-frame timing separately from the presentation interval.
248
+ // Chromium joins an adjacent frame within twice the maximum coded-frame
249
+ // duration of the stream (SourceBufferRange::GetFudgeRoom), measured from
250
+ // the last frame's timestamp, not from the segment's nominal cut.
251
+ const range = merged.at(-1);
252
+ range.lastFrameStart = Math.max(range.lastFrameStart ?? sample.start, sample.start);
253
+ range.maxFrameDuration = Math.max(range.maxFrameDuration ?? 0, sample.end - sample.start);
247
254
  }
248
255
  // Movie edits quantize independent pieces to movie ticks. Preserve that
249
256
  // declared resolution at joins; it is not a playback buffer threshold.
250
257
  for (const range of merged) range.end += precision;
251
258
  const decoded = decodeRanges.get(id);
252
- return { id, ranges: merged, kind: kinds.get(id), precision,
259
+ return { id, ranges: merged.map(({ start, end }) => ({ start, end })),
260
+ frameRanges: merged, kind: kinds.get(id), precision,
253
261
  decodeStart: Math.min(...decoded.map(({ start }) => start)),
254
262
  decodeEnd: Math.max(...decoded.map(({ end }) => end)) };
255
263
  });
@@ -280,8 +288,11 @@ export function continuePresentationRanges(ranges, nextRanges) {
280
288
  }
281
289
 
282
290
  function intersectPresentationTracks(tracks) {
283
- return tracks.map(({ ranges }) => ranges).reduce((common, ranges) => common.flatMap((left) => ranges
284
- .map((right) => ({ start: Math.max(left.start, right.start), end: Math.min(left.end, right.end) }))
291
+ return tracks.slice(1).map(({ ranges }) => ranges).reduce((common, ranges) => common.flatMap((left) => ranges
292
+ .map((right) => ({ start: Math.max(left.start, right.start), end: Math.min(left.end, right.end),
293
+ ...(left.joinEnd !== undefined || right.joinEnd !== undefined ? {
294
+ joinEnd: Math.min(left.joinEnd ?? left.end, right.joinEnd ?? right.end)
295
+ } : {}) }))
285
296
  .filter(({ start, end }) => end > start)), tracks[0]?.ranges ?? []);
286
297
  }
287
298
 
@@ -290,11 +301,15 @@ export function translatePresentationRanges(ranges, initBytes, timestampOffsetSe
290
301
  const tracks = presentationTracks.get(ranges);
291
302
  if (!tracks || !initBytes?.length || !Number.isFinite(timestampOffsetSeconds)) return ranges;
292
303
  const edits = readTrackEditOffsets(initBytes);
293
- return intersectPresentationTracks(tracks.map(({ id, ranges: held }) => ({
304
+ return intersectPresentationTracks(tracks.map(({ id, ranges: held, frameRanges, kind }) => ({
294
305
  id,
295
- ranges: held.map(({ start, end }) => ({
306
+ ranges: held.map(({ start, end }, index) => ({
296
307
  start: Math.max(0, start + timestampOffsetSeconds - (edits.get(id) ?? 0)),
297
- end: end + timestampOffsetSeconds - (edits.get(id) ?? 0)
308
+ end: end + timestampOffsetSeconds - (edits.get(id) ?? 0),
309
+ ...(kind === "soun" ? {
310
+ joinEnd: frameRanges[index].lastFrameStart + 2 * frameRanges[index].maxFrameDuration +
311
+ timestampOffsetSeconds - (edits.get(id) ?? 0)
312
+ } : {})
298
313
  })).filter(({ start, end }) => end > start)
299
314
  })));
300
315
  }
@@ -534,15 +534,15 @@ function isSafeSchedule(completions, productions, trackState, position, duration
534
534
  while (productionIndex < orderedProductions.length && orderedProductions[productionIndex].at <= time) {
535
535
  const { trackIndex, segment } = orderedProductions[productionIndex];
536
536
  for (const range of segment.mediaRanges ?? [{ start: segment.startSeconds, end: segment.endSeconds }]) {
537
- addRange(preparedRanges[trackIndex], range.start, range.end);
537
+ addRange(preparedRanges[trackIndex], range.start, range.end, range.joinEnd);
538
538
  }
539
539
  productionIndex += 1;
540
540
  }
541
541
  while (completionIndex < orderedCompletions.length && orderedCompletions[completionIndex].at <= time) {
542
542
  const { trackIndex, segment } = orderedCompletions[completionIndex];
543
543
  for (const range of segment.mediaRanges ?? [{ start: segment.startSeconds, end: segment.endSeconds }]) {
544
- addRange(clientRanges[trackIndex], range.start, range.end);
545
- addRange(preparedRanges[trackIndex], range.start, range.end);
544
+ addRange(clientRanges[trackIndex], range.start, range.end, range.joinEnd);
545
+ addRange(preparedRanges[trackIndex], range.start, range.end, range.joinEnd);
546
546
  }
547
547
  completionIndex += 1;
548
548
  }
@@ -625,21 +625,26 @@ function isSafeSchedule(completions, productions, trackState, position, duration
625
625
  return { safe: atEnd.safe, bufferedAtStart: atStart.available, neededSeconds: atStart.neededSeconds, failure: atStart.failure };
626
626
  }
627
627
 
628
- function addRange(ranges, start, end) {
628
+ function addRange(ranges, start, end, joinEnd) {
629
629
  const merged = [];
630
- let next = { start, end };
630
+ let next = { start, end, ...(Number.isFinite(joinEnd) ? { joinEnd } : {}) };
631
631
  let inserted = false;
632
632
  for (const range of ranges) {
633
- if (range.end < next.start) {
633
+ // The declared coded-frame join boundary connects two ranges only after
634
+ // both have arrived. It never extends an isolated range's playable end.
635
+ if (Math.max(range.end, range.joinEnd ?? range.end) < next.start) {
634
636
  merged.push(range);
635
- } else if (next.end < range.start) {
637
+ } else if (Math.max(next.end, next.joinEnd ?? next.end) < range.start) {
636
638
  if (!inserted) {
637
639
  merged.push(next);
638
640
  inserted = true;
639
641
  }
640
642
  merged.push(range);
641
643
  } else {
642
- next = { start: Math.min(next.start, range.start), end: Math.max(next.end, range.end) };
644
+ next = { start: Math.min(next.start, range.start), end: Math.max(next.end, range.end),
645
+ ...(next.joinEnd !== undefined || range.joinEnd !== undefined ? {
646
+ joinEnd: Math.max(next.joinEnd ?? next.end, range.joinEnd ?? range.end)
647
+ } : {}) };
643
648
  }
644
649
  }
645
650
  if (!inserted) {
@@ -0,0 +1,70 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import test from "node:test";
4
+ import { readPresentationRanges, translatePresentationRanges, walkBoxes } from "../services/encode/segment-formats/mp4-boxes.js";
5
+ import { predictPlaybackReadiness } from "../services/server/playback-readiness.js";
6
+
7
+ const evidence = JSON.parse(readFileSync(new URL("./fixtures/audio-frame-boundary.json", import.meta.url), "utf8"));
8
+ const header = Buffer.from(evidence.files["init.mp4"], "base64");
9
+
10
+ function ranges(name, shiftedFrames = 0) {
11
+ const bytes = Buffer.from(evidence.files[name], "base64");
12
+ let frameTicks;
13
+ walkBoxes(bytes, (type, start) => {
14
+ if (type === "tfhd") {
15
+ const flags = bytes.readUIntBE(start + 1, 3);
16
+ const at = start + 8 + ((flags & 1) ? 8 : 0) + ((flags & 2) ? 4 : 0);
17
+ if (flags & 8) frameTicks = bytes.readUInt32BE(at);
18
+ }
19
+ if (type === "tfdt" && shiftedFrames) {
20
+ assert.ok(frameTicks > 0);
21
+ bytes.writeBigUInt64BE(bytes.readBigUInt64BE(start + 4) + BigInt(frameTicks * shiftedFrames), start + 4);
22
+ }
23
+ });
24
+ return translatePresentationRanges(readPresentationRanges(Buffer.concat([header, bytes])), header, 0);
25
+ }
26
+
27
+ function state(second = ranges("segment-00172.mp4")) {
28
+ const first = ranges("segment-00171.mp4");
29
+ const now = 1000;
30
+ return {
31
+ now,
32
+ positionSeconds: first[0].start,
33
+ durationSeconds: second.at(-1).end,
34
+ bufferLimitSeconds: second.at(-1).end - first[0].start,
35
+ reserveSeconds: 0,
36
+ lookaheadSeconds: second.at(-1).end - first[0].start,
37
+ sources: [{ id: "source", complete: true }],
38
+ linkReadings: [{ at: now, value: 1_000_000 }],
39
+ tracks: [{
40
+ id: "audio", sourceIds: ["source"], clientRanges: [
41
+ { start: evidence.browserBufferedRanges[0][0], end: evidence.browserBufferedRanges[0][1] }
42
+ ],
43
+ readings: [{ at: now, value: 1 }],
44
+ readySegmentIndices: [0, 1], segmentSizesBytes: { 0: 170000, 1: 170000 },
45
+ segments: [
46
+ { index: 0, startSeconds: first[0].start, endSeconds: second[0].start, mediaRanges: first },
47
+ { index: 1, startSeconds: second[0].start, endSeconds: second.at(-1).end, mediaRanges: second }
48
+ ]
49
+ }]
50
+ };
51
+ }
52
+
53
+ test("captured AAC boundary agrees with Chrome's continuous buffered range", () => {
54
+ const input = state();
55
+ assert.equal(predictPlaybackReadiness(input).ready, true);
56
+ for (const segment of input.tracks[0].segments) {
57
+ segment.mediaRanges = segment.mediaRanges.map(({ start, end }) => ({ start, end }));
58
+ }
59
+ assert.equal(predictPlaybackReadiness(input).reason, "no-safe-start-found");
60
+ });
61
+
62
+ test("four missing AAC frames preserve the discontinuity observed in Chrome", () => {
63
+ assert.equal(predictPlaybackReadiness(state(ranges("segment-00172.mp4", 4))).reason, "no-safe-start-found");
64
+ });
65
+
66
+ test("a join boundary never supplies audio before the adjacent segment arrives", () => {
67
+ const input = state();
68
+ input.linkReadings[0].value = 1;
69
+ assert.notEqual(predictPlaybackReadiness(input).ready, true);
70
+ });
@@ -0,0 +1 @@
1
+ {"description":"MP4 metadata boxes captured from adjacent AAC segments. Media payloads are omitted.","files":{"init.mp4":"AAAAHGZ0eXBpc281AAACAGlzbzVpc282bXA0MQAAAvRtb292AAAAbG12aGQAAAAAAAAAAAAAAAAAAAPoAAAAAAABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAB9nRyYWsAAABcdGtoZAAAAAMAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAACRlZHRzAAAAHGVsc3QAAAAAAAAAAQAAAAAAAAAAAAEAAAAAAVttZGlhAAAAIG1kaGQAAAAAAAAAAAAAAAAAALuAAAAAAFXEAAAAAAAtaGRscgAAAAAAAAAAc291bgAAAAAAAAAAAAAAAFNvdW5kSGFuZGxlcgAAAAEGbWluZgAAABBzbWhkAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAADKc3RibAAAAH5zdHNkAAAAAAAAAAEAAABubXA0YQAAAAAAAAABAAAAAAAAAAAAAgAQAAAAALuAAAAAAAA2ZXNkcwAAAAADgICAJQABAASAgIAXQBUAAAAAAfQAAAH0AAWAgIAFEZBW5QAGgICAAQIAAAAUYnRydAAAAAAAAfQAAAH0AAAAABBzdHRzAAAAAAAAAAAAAAAQc3RzYwAAAAAAAAAAAAAAFHN0c3oAAAAAAAAAAAAAAAAAAAAQc3RjbwAAAAAAAAAAAAAAE3VkdGEAAAALbmFtZVJVUwAAAChtdmV4AAAAIHRyZXgAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAABidWR0YQAAAFptZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAAC1pbHN0AAAAJal0b28AAAAdZGF0YQAAAAEAAAAATGF2ZjYyLjEyLjEwMg==","segment-00171.mp4":"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","segment-00172.mp4":"AAAIAG1vb2YAAAAQbWZoZAAAAAAAAAABAAAH6HRyYWYAAAAcdGZoZAACADgAAAABAAAEAAAAARgCAAAAAAAAFHRmZHQBAAAAAAAAAAUhjaAAAAewdHJ1bgAAAgEAAAHnAAAICAAAARgAAAEIAAABGQAAATIAAAFaAAABUwAAAVEAAAFVAAABaQAAAVsAAAFfAAABRQAAAWYAAAFcAAABSwAAAVQAAAFOAAABVAAAAVMAAAFcAAABSwAAAU4AAAE+AAABLwAAATsAAAFQAAABUAAAAUYAAAFUAAABVAAAAVUAAAFsAAABeAAAATgAAAFuAAABRwAAAVoAAAFJAAABRgAAAVUAAAF9AAABNQAAAVgAAAF3AAABQgAAAVIAAAFQAAABWwAAAV4AAAEtAAABWQAAAWsAAAFEAAABcAAAAUUAAAFdAAABZQAAAWAAAAFiAAABQgAAAVEAAAFQAAABKwAAAV4AAAFTAAABbQAAAYIAAAEyAAABdQAAAUAAAAFSAAABbQAAAUoAAAFtAAABtgAAAPwAAAEwAAABewAAAT4AAAFLAAABRQAAAWAAAAFtAAABXQAAASsAAAFyAAABZAAAATEAAAFYAAABcwAAAU8AAAFaAAABSAAAAWYAAAFDAAABOwAAAWMAAAFxAAABQQAAAWgAAAF2AAABlAAAAQkAAAFFAAABMAAAAW8AAAFSAAABUgAAAVIAAAFOAAABfgAAAT4AAAFUAAABPAAAAXsAAAE2AAABUAAAAXAAAAFUAAABUwAAAXAAAAE6AAABQQAAAVsAAAFVAAABVgAAAWoAAAFMAAABQwAAAU0AAAFWAAABTAAAAXAAAAFbAAABVAAAAW4AAAE9AAABWAAAAWUAAAFBAAABXgAAAU8AAAFfAAABUwAAAVoAAAFLAAABaAAAAVgAAAFMAAABXAAAAU4AAAFjAAABRQAAAV8AAAFbAAABUgAAAU8AAAFeAAABSQAAAVkAAAFXAAABPQAAAWgAAAFRAAABVwAAAW0AAAE5AAABYAAAAWkAAAFFAAABXgAAAVgAAAFPAAABNwAAAUwAAAGdAAABHwAAAWEAAAF0AAABUgAAAScAAAF3AAABcAAAAVAAAAE5AAABVgAAAWcAAAFSAAABdAAAAU0AAAFpAAABMgAAAWsAAAFrAAABXQAAAU4AAAFiAAABSAAAAWgAAAFNAAABPAAAAUQAAAF7AAABQwAAAWEAAAFLAAABcQAAAUcAAAFfAAABVQAAAUkAAAFJAAABVwAAAVgAAAFaAAABSAAAAXUAAAFHAAABWwAAAWcAAAFhAAABPAAAAVgAAAFbAAABWwAAAVMAAAFZAAABVQAAAVgAAAFEAAABWwAAAUkAAAFcAAABVgAAAWsAAAE7AAABegAAAU4AAAFQAAABSgAAAUoAAAFaAAABTAAAAVoAAAFuAAABVAAAAUUAAAFQAAABaAAAAU8AAAFYAAABUAAAAVEAAAE8AAABeQAAAVIAAAFnAAABWQAAAVUAAAFfAAABUAAAAWUAAAE/AAABYgAAAUwAAAFPAAABYQAAAWcAAAFDAAABUwAAAVIAAAFPAAABXQAAAV4AAAFRAAABUAAAAUEAAAFTAAABXAAAAWwAAAFJAAABWAAAAVkAAAFTAAABVQAAAVYAAAFKAAABZgAAAVQAAAFlAAABUgAAAUwAAAFiAAABWQAAAU0AAAFRAAABZgAAATYAAAFoAAABZAAAAUgAAAFeAAABUQAAAU4AAAFgAAABVQAAAVsAAAFLAAABQgAAAVgAAAFJAAABMwAAAU8AAAFwAAABOQAAAX8AAAE7AAABaAAAAXIAAAFBAAABWgAAAU4AAAFTAAABagAAAT4AAAF3AAABZQAAAUQAAAFbAAABXwAAAUQAAAF6AAABVgAAAU0AAAFQAAABZAAAAT0AAAFsAAABTgAAAWcAAAFTAAABWQAAAUwAAAF5AAABWAAAAVkAAAE2AAABcwAAAUcAAAFrAAABQgAAAV8AAAE8AAABdQAAAVoAAAFGAAABUQAAAVwAAAFyAAABPQAAAUsAAAFPAAABTgAAAU4AAAFVAAABcAAAASwAAAGCAAABLAAAAXEAAAFRAAABZwAAAVIAAAE+AAABYwAAAXUAAAFrAAABUgAAAV4AAAEyAAABJQAAAXkAAAE+AAABpAAAATAAAAE9AAABXQAAAScAAAEzAAABLAAAAWAAAAE8AAABTwAAAUoAAAFCAAABSAAAAU8AAAFJAAABkgAAAVcAAAE9AAABQgAAAWIAAAFbAAABTAAAAU4AAAF7AAABawAAAVsAAAFXAAABNwAAAU4AAAFPAAABOAAAAXUAAAFjAAABTgAAAVQAAAFGAAABRAAAAVEAAAFkAAABQQAAAU0AAAFgAAABWwAAAYMAAAFFAAABWgAAAWUAAAFTAAABUgAAATkAAAF/AAABOAAAAUQAAAFFAAABSQAAAVIAAAFcAAABhgAAAVcAAAFZAAABRQAAAWQAAAFaAAABTwAAAV4AAAFcAAABVAAAAVYAAAFjAAABRQAAAUQAAAFiAAABVQAAAV8AAAF0AAABawAAAVMAAAFhAAABVAAAATsAAAE9AAABYwAAAVAAAAFWAAABbQAAAVUAAAFUAAABdgAAAU4AAAFGAAABYgAAAUwAAAFLAAABXQAAAUcAAAFYAAABWAAAAW8AAAFdAAABYQAAAVMAAAGgAAACQQAAAYs="},"browserBufferedRanges":[[1782.997,1793.428999],[1782.997,1803.827333]]}