@torrent-tv/proxy 2.88.1 → 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,11 @@
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
+
5
+ ## 2.88.2
6
+
7
+ - **Fix**: Report the first failing playback forecast boundary and per-track ranges instead of only a generic no-safe-start result.
8
+
1
9
  ## 2.88.1
2
10
 
3
11
  - **New**: Preserve bounded ASS/SSA work-title and release-year header fields in a WebVTT metadata note, without additional subtitle reads.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.88.1",
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
  }
@@ -327,7 +327,7 @@ export function predictPlaybackReadiness(input = {}) {
327
327
  : productions;
328
328
  const safety = isSafeSchedule(usefulCompletions, usefulProductions, trackState, position, duration,
329
329
  buffered, capacity, reserve, startDelaySeconds);
330
- return { safe: safety.safe,
330
+ return { safe: safety.safe, failure: safety.failure,
331
331
  bufferedAtStart: safety.bufferedAtStart,
332
332
  neededSeconds: safety.neededSeconds,
333
333
  completionTimes: usefulCompletions.map(({ at }) => at),
@@ -346,7 +346,7 @@ export function predictPlaybackReadiness(input = {}) {
346
346
  }
347
347
  const searchLimit = maximumUsefulDelay;
348
348
  if (!(searchLimit > 0)) {
349
- return result(false, null, buffered, reserve, null, "no-safe-start-found", preparedSegments);
349
+ return { ...result(false, null, buffered, reserve, null, "no-safe-start-found", preparedSegments), failure: immediate.failure };
350
350
  }
351
351
 
352
352
  let low = 0;
@@ -369,7 +369,7 @@ export function predictPlaybackReadiness(input = {}) {
369
369
  }
370
370
  }
371
371
  if (high === null) {
372
- return result(false, null, buffered, reserve, null, "no-safe-start-found", preparedSegments);
372
+ return { ...result(false, null, buffered, reserve, null, "no-safe-start-found", preparedSegments), failure: immediate.failure };
373
373
  }
374
374
 
375
375
  while (true) {
@@ -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
  }
@@ -559,6 +559,12 @@ function isSafeSchedule(completions, productions, trackState, position, duration
559
559
  const available = Math.min(capacity, ...clientRanges.map((trackRanges, index) =>
560
560
  playerPosition >= trackEnd(index) ? Number.POSITIVE_INFINITY :
561
561
  Math.max(0, contiguousEnd(trackRanges, playerPosition) - playerPosition)));
562
+ const trackStatus = clientRanges.map((ranges, index) => ({
563
+ track: trackState[index].track.id,
564
+ end: contiguousEnd(ranges, playerPosition),
565
+ next: ranges.find(range => range.start > playerPosition)?.start ?? null,
566
+ last: trackEnd(index)
567
+ }));
562
568
  let neededSeconds = 0;
563
569
  let stockSafe = true;
564
570
  for (let trackIndex = 0; trackIndex < trackState.length; trackIndex += 1) {
@@ -575,14 +581,15 @@ function isSafeSchedule(completions, productions, trackState, position, duration
575
581
  safe: (!checkClient || available > 0) && stockSafe,
576
582
  stockSafe,
577
583
  available,
578
- neededSeconds
584
+ neededSeconds,
585
+ failure: { time, playerPosition, available, stockSafe, tracks: trackStatus }
579
586
  };
580
587
  };
581
588
 
582
589
  addThrough(startDelay);
583
590
  const atStart = safeAt(startDelay);
584
591
  if (!atStart.safe) {
585
- return { safe: false, bufferedAtStart: atStart.available, neededSeconds: atStart.neededSeconds };
592
+ return { safe: false, bufferedAtStart: atStart.available, neededSeconds: atStart.neededSeconds, failure: atStart.failure };
586
593
  }
587
594
 
588
595
  const playbackEnd = startDelay + Math.max(0, duration - position);
@@ -605,34 +612,39 @@ function isSafeSchedule(completions, productions, trackState, position, duration
605
612
  for (const time of eventTimes) {
606
613
  const beforeArrival = safeAt(time, false);
607
614
  if (!beforeArrival.stockSafe) {
608
- return { safe: false, bufferedAtStart: atStart.available, neededSeconds: beforeArrival.neededSeconds };
615
+ return { safe: false, bufferedAtStart: atStart.available, neededSeconds: beforeArrival.neededSeconds, failure: beforeArrival.failure };
609
616
  }
610
617
  addThrough(time);
611
618
  const afterArrival = safeAt(time);
612
619
  if (!afterArrival.safe) {
613
- return { safe: false, bufferedAtStart: atStart.available, neededSeconds: afterArrival.neededSeconds };
620
+ return { safe: false, bufferedAtStart: atStart.available, neededSeconds: afterArrival.neededSeconds, failure: afterArrival.failure };
614
621
  }
615
622
  }
616
623
 
617
624
  const atEnd = safeAt(playbackEnd);
618
- return { safe: atEnd.safe, bufferedAtStart: atStart.available, neededSeconds: atStart.neededSeconds };
625
+ return { safe: atEnd.safe, bufferedAtStart: atStart.available, neededSeconds: atStart.neededSeconds, failure: atStart.failure };
619
626
  }
620
627
 
621
- function addRange(ranges, start, end) {
628
+ function addRange(ranges, start, end, joinEnd) {
622
629
  const merged = [];
623
- let next = { start, end };
630
+ let next = { start, end, ...(Number.isFinite(joinEnd) ? { joinEnd } : {}) };
624
631
  let inserted = false;
625
632
  for (const range of ranges) {
626
- 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) {
627
636
  merged.push(range);
628
- } else if (next.end < range.start) {
637
+ } else if (Math.max(next.end, next.joinEnd ?? next.end) < range.start) {
629
638
  if (!inserted) {
630
639
  merged.push(next);
631
640
  inserted = true;
632
641
  }
633
642
  merged.push(range);
634
643
  } else {
635
- 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
+ } : {}) };
636
648
  }
637
649
  }
638
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":"AAAICG1vb2YAAAAQbWZoZAAAAAAAAAABAAAH8HRyYWYAAAAcdGZoZAACADgAAAABAAAEAAAAAVcCAAAAAAAAFHRmZHQBAAAAAAAAAAUZ5/AAAAe4dHJ1bgAAAgEAAAHpAAAIEAAAAVcAAAFOAAABXAAAAVUAAAFcAAABWQAAAUwAAAFfAAABVQAAAVwAAAFOAAABXAAAAVEAAAFdAAABYAAAAVwAAAFOAAABZQAAAV8AAAFSAAABYAAAAVoAAAFdAAABXQAAAWcAAAFbAAABUwAAAVwAAAFZAAABSQAAAU8AAAFGAAABSwAAAVUAAAFNAAABWwAAAVMAAAFXAAABWAAAAVgAAAFkAAABWgAAAVwAAAFtAAABUgAAAU4AAAFZAAABTwAAAUwAAAFMAAABSwAAAUMAAAFhAAABUwAAAVIAAAFoAAABbgAAAVgAAAFhAAABYAAAAUwAAAFiAAABYQAAAVMAAAFTAAABRAAAAUwAAAFQAAABVAAAAV0AAAFSAAABUAAAAVgAAAFaAAABVwAAAUUAAAFRAAABUwAAAVYAAAFRAAABWQAAAUIAAAGFAAABLwAAAUQAAAFBAAABSwAAAUYAAAFCAAABRQAAAVUAAAFPAAABUQAAAU8AAAFOAAABWwAAAV0AAAFTAAABVwAAAVQAAAFlAAABVQAAAVEAAAFOAAABUgAAAVkAAAFfAAABWgAAAUsAAAFLAAABTQAAAUkAAAFKAAABUAAAAUoAAAFFAAABWQAAAVgAAAFaAAABYgAAAVMAAAFeAAABVQAAAVsAAAFTAAABUwAAAVQAAAFZAAABXwAAAUkAAAFYAAABUAAAAU0AAAFVAAABZwAAAVkAAAFoAAABZgAAAVcAAAFtAAABZwAAAV4AAAFSAAABXwAAAU8AAAE7AAABOAAAAVoAAAFeAAABWwAAAWQAAAFYAAABWgAAAWYAAAFgAAABaAAAAVwAAAFiAAABXgAAAVkAAAFvAAABTgAAAUsAAAFEAAABRgAAATgAAAFMAAABTAAAAUgAAAE+AAABTQAAAUEAAAFDAAABagAAAUQAAAFoAAABRgAAAVQAAAFpAAABbAAAAWEAAAFUAAABWAAAAVwAAAExAAABQQAAAYkAAAFkAAABXwAAAV0AAAFTAAABaQAAAVwAAAFSAAABVAAAAUUAAAFPAAABSAAAAVQAAAFGAAABSwAAAVcAAAFZAAABSQAAAToAAAFYAAABTgAAAUgAAAFRAAABXAAAAVIAAAFaAAABXwAAAUkAAAFUAAABXQAAAU0AAAFKAAABRgAAAVgAAAFUAAABuAAAAf8AAAGTAAABHgAAASgAAAFiAAABRwAAAVkAAAFLAAABZwAAAVQAAAFGAAABRgAAAUAAAAE+AAABPgAAAUcAAAFRAAABSwAAAVEAAAFTAAABVQAAAW0AAAFqAAABVQAAAV0AAAFPAAABXgAAAUkAAAFTAAABZwAAAW8AAAFlAAABWQAAATkAAAFaAAABbQAAAW0AAAE2AAABNgAAATgAAAFKAAABbQAAAXwAAAFrAAABVgAAATAAAAFkAAABXAAAAVYAAAFhAAAB7AAAATUAAAFYAAABZAAAAVgAAAFIAAABTQAAAS4AAAFAAAABJwAAAT0AAAFVAAABbwAAAXQAAAFzAAABTQAAAUQAAAFlAAABgQAAAXYAAAF8AAABdAAAAWAAAAEsAAABPAAAAWkAAAFcAAABTAAAAVgAAAFLAAABUQAAAU8AAAFFAAABRgAAAVYAAAFBAAABNgAAATsAAAFKAAABTAAAAU4AAAFKAAABUQAAAVoAAAFXAAABagAAAWEAAAFcAAABYAAAAWMAAAFhAAABVAAAAVYAAAFYAAABXwAAAVkAAAFSAAABSQAAAUsAAAFYAAABPwAAAVMAAAFLAAABUQAAAU4AAAFFAAABUgAAAVAAAAFWAAABpQAAAWwAAAFcAAABOwAAATwAAAEwAAABbgAAAWAAAAFaAAABRgAAAU4AAAE+AAABSwAAAWQAAAE+AAABRAAAAUwAAAFDAAABVQAAAY0AAAFJAAABXQAAAWMAAAFrAAABYQAAAWYAAAFUAAABUwAAAU4AAAFMAAABTwAAAVMAAAFRAAABWwAAAU0AAAFMAAABTwAAAVUAAAFVAAABMwAAAWwAAAEmAAABYAAAAUMAAAF/AAABiQAAATYAAAFqAAABcAAAAVEAAAE5AAABZAAAAUAAAAFGAAABXQAAAU4AAAFMAAABbwAAAWcAAAGTAAABhAAAAYoAAAFUAAABZQAAAYMAAAGIAAABhAAAAYEAAAFwAAABWQAAAWIAAAFGAAABXQAAAVMAAAFdAAABZAAAAUEAAAFOAAABYgAAAWgAAAFXAAABbwAAATMAAAFuAAABYQAAAXUAAAFYAAABTwAAAT8AAAEvAAABJgAAAUoAAAE/AAABYgAAAbMAAAF0AAABhgAAAYwAAAFWAAABKAAAAV0AAAGEAAABfQAAAT8AAAFvAAABdAAAAXQAAAGNAAABTwAAAUoAAAFHAAABgAAAAWkAAAF3AAABggAAAUcAAAFLAAABhgAAAWcAAAFoAAABdAAAAU8AAAFrAAABZgAAAWoAAAFcAAABVAAAAUUAAAFzAAABRwAAAWUAAAFMAAABSgAAAUsAAAFhAAABSwAAAVoAAAFJAAABSQAAAUsAAAFKAAABZwAAAWIAAAGBAAABcQAAAUsAAAIOAAABWQ==","segment-00172.mp4":"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"},"browserBufferedRanges":[[1782.997,1793.428999],[1782.997,1803.827333]]}