@bubo-squared/gyroview 0.2.0 → 0.3.0

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +16 -5
  3. package/dist/index.d.ts +1 -0
  4. package/dist/packages/adapters/fetch/src/HttpByteStream.js +174 -0
  5. package/dist/packages/adapters/fetch/src/HttpRangeSource.js +14 -170
  6. package/dist/packages/adapters/fetch/src/HttpResource.js +192 -0
  7. package/dist/packages/adapters/fetch/src/contentRange.js +15 -0
  8. package/dist/packages/adapters/fetch/src/httpRequest.js +5 -3
  9. package/dist/packages/adapters/fetch/src/index.js +3 -1
  10. package/dist/packages/adapters/fetch/src/passingFailures.js +24 -0
  11. package/dist/packages/adapters/fetch/src/rangeFailures.js +17 -0
  12. package/dist/packages/adapters/fetch/src/recordingVersion.js +37 -0
  13. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioPackager.js +129 -0
  14. package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +80 -0
  15. package/dist/packages/adapters/mediabunny/src/SegmentChannel.js +58 -6
  16. package/dist/packages/adapters/mediabunny/src/decoderConfigurations.js +28 -0
  17. package/dist/packages/adapters/mediabunny/src/index.js +3 -2
  18. package/dist/packages/core/src/application/download/BlockStore.js +97 -0
  19. package/dist/packages/core/src/application/download/DownloadedAudioSamples.js +50 -0
  20. package/dist/packages/core/src/application/download/DownloadedVideoTrack.js +122 -0
  21. package/dist/packages/core/src/application/download/FileDownload.js +183 -0
  22. package/dist/packages/core/src/application/download/RecordingBuffer.js +22 -0
  23. package/dist/packages/core/src/application/download/SampleCursor.js +88 -0
  24. package/dist/packages/core/src/application/download/SourceByteStream.js +46 -0
  25. package/dist/packages/core/src/application/download/Transfers.js +75 -0
  26. package/dist/packages/core/src/application/download/startFileDownload.js +37 -0
  27. package/dist/packages/core/src/application/playback/Buffering.js +53 -0
  28. package/dist/packages/core/src/application/playback/DecodePipeline.js +11 -7
  29. package/dist/packages/core/src/application/playback/DecodeRun.js +15 -0
  30. package/dist/packages/core/src/application/playback/PlaybackSession.js +18 -27
  31. package/dist/packages/core/src/application/playback/keyframeTimeAt.js +1 -1
  32. package/dist/packages/core/src/application/playback/probeDecoding.js +15 -1
  33. package/dist/packages/core/src/application/recording/readSampleTable.js +27 -0
  34. package/dist/packages/core/src/application/recording/timeRecording.js +3 -3
  35. package/dist/packages/core/src/domain/container/KeyframeRule.js +7 -0
  36. package/dist/packages/core/src/domain/container/SampleTable.js +29 -0
  37. package/dist/packages/core/src/domain/container/TrackSampleTable.js +96 -0
  38. package/dist/packages/core/src/domain/container/presentationOrder.js +10 -0
  39. package/dist/packages/core/src/domain/download/DownloadPolicy.js +41 -0
  40. package/dist/packages/core/src/domain/download/isReadyToResume.js +27 -0
  41. package/dist/packages/core/src/domain/download/planDownloads.js +175 -0
  42. package/dist/packages/core/src/domain/download/trackNeeds.js +32 -0
  43. package/dist/packages/core/src/domain/format/boxes/boxHeader.js +44 -0
  44. package/dist/packages/core/src/domain/format/boxes/scanBoxes.js +7 -34
  45. package/dist/packages/core/src/domain/format/mp4/keyframeRules.js +81 -0
  46. package/dist/packages/core/src/domain/format/mp4/movieBoxes.js +52 -0
  47. package/dist/packages/core/src/domain/format/mp4/mp4BoxTypes.js +46 -0
  48. package/dist/packages/core/src/domain/format/mp4/mp4Layouts.js +183 -0
  49. package/dist/packages/core/src/domain/format/mp4/parseMovie.js +67 -0
  50. package/dist/packages/core/src/domain/format/mp4/readTable.js +31 -0
  51. package/dist/packages/core/src/domain/format/mp4/sampleLocations.js +69 -0
  52. package/dist/packages/core/src/domain/format/mp4/sampleTiming.js +109 -0
  53. package/dist/packages/core/src/domain/format/mp4/trackHeaders.js +75 -0
  54. package/dist/packages/core/src/index.js +9 -1
  55. package/dist/packages/core/src/shared/async/iteration.js +24 -0
  56. package/dist/packages/core/src/shared/binary/ByteRange.js +12 -0
  57. package/dist/packages/core/src/shared/binary/ByteRangeSet.js +85 -0
  58. package/dist/packages/core/src/shared/binary/ByteReader.js +21 -3
  59. package/dist/packages/core/src/shared/binary/concatenated.js +15 -0
  60. package/dist/packages/core/src/shared/errors/GyroViewError.js +1 -0
  61. package/dist/packages/core/src/shared/math/countAtOrBelow.js +17 -0
  62. package/dist/packages/player/src/composition/browserPorts.js +23 -4
  63. package/dist/packages/player/src/composition/buildPipeline.js +15 -3
  64. package/dist/packages/player/src/composition/openInputs.js +51 -107
  65. package/dist/packages/player/src/composition/readInputs.js +111 -0
  66. package/dist/packages/player/src/controls/messages.js +1 -0
  67. package/dist/packages/player/src/inspectRecording.js +1 -1
  68. package/dist/standalone.js +58 -58
  69. package/package.json +1 -1
  70. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioSegments.js +0 -133
  71. package/dist/packages/adapters/mediabunny/src/MediabunnyDemuxer.js +0 -63
  72. package/dist/packages/adapters/mediabunny/src/MediabunnyVideoTrackReader.js +0 -84
@@ -17,19 +17,25 @@ var STARVATION_LAG_SECONDS = .25;
17
17
  * what arrives late and reports nothing, so a run a seek replaced never speaks for its successor.
18
18
  */
19
19
  var DecodeRun = class DecodeRun {
20
+ parts;
20
21
  listener;
21
22
  queue;
22
23
  pipeline;
23
24
  primingPairs;
25
+ stopFollowingBuffer;
24
26
  isAborted = false;
25
27
  hasReachedEnd = false;
26
28
  hasHandedOutPair = false;
27
29
  constructor(parts, listener) {
30
+ this.parts = parts;
28
31
  this.listener = listener;
29
32
  this.primingPairs = Math.min(PRIMING_PAIRS, parts.queueCapacity);
30
33
  this.queue = new FramePairQueue(parts.queueCapacity, () => {
31
34
  if (!this.isAborted) listener.onProgress();
32
35
  });
36
+ this.stopFollowingBuffer = parts.buffer?.onProgress(() => {
37
+ listener.onProgress();
38
+ }) ?? (() => void 0);
33
39
  this.pipeline = new DecodePipeline(parts.frameSources, parts.decoderPort, parts.pipeline);
34
40
  }
35
41
  static start(parts, from, listener) {
@@ -53,6 +59,14 @@ var DecodeRun = class DecodeRun {
53
59
  return this.hasReachedEnd || hasCaughtUp && this.queue.length >= this.primingPairs;
54
60
  }
55
61
  /**
62
+ * Primed, and after a starvation enough downloaded ahead to go on with (ADR 0011), unless the
63
+ * run is over and nothing more will come.
64
+ */
65
+ isReadyToResumeAt(now) {
66
+ const isDownloaded = () => this.parts.buffer?.isReadyToResumeAt(now) ?? true;
67
+ return this.isPrimedAt(now) && (this.hasReachedEnd || isDownloaded());
68
+ }
69
+ /**
56
70
  * Nothing is queued, the run is not over, and the frame on screen (shown for `shownAt`, if any)
57
71
  * is well behind the clock: the decoders cannot keep up, even when each tick still finds a
58
72
  * pair to show.
@@ -75,6 +89,7 @@ var DecodeRun = class DecodeRun {
75
89
  }
76
90
  abort() {
77
91
  this.isAborted = true;
92
+ this.stopFollowingBuffer();
78
93
  this.pipeline.abort();
79
94
  this.queue.close();
80
95
  }
@@ -2,11 +2,11 @@ import { asGyroViewError } from "../../shared/errors/GyroViewError.js";
2
2
  import { seconds } from "../../shared/units/time.js";
3
3
  import { clamp } from "../../shared/math/clamp.js";
4
4
  import { closeFramePair } from "../../ports/FramePair.js";
5
- import { Deferred } from "../../shared/async/Deferred.js";
6
5
  import { TypedEmitter } from "../../shared/events/TypedEmitter.js";
7
6
  import { ClockWatch, isAtEndOfMedia, isStoppedFromOutside } from "./ClockWatch.js";
8
7
  import { SeekOrder } from "./SeekOrder.js";
9
8
  import { TimeUpdates } from "./TimeUpdates.js";
9
+ import { Buffering } from "./Buffering.js";
10
10
  import { SessionLifecycle } from "./SessionLifecycle.js";
11
11
  import { DecodeRun } from "./DecodeRun.js";
12
12
  import { keyframeTimeAt } from "./keyframeTimeAt.js";
@@ -35,10 +35,7 @@ var PlaybackSession = class {
35
35
  */
36
36
  presented;
37
37
  timeUpdates = new TimeUpdates(this.lifecycle);
38
- /**
39
- * The `play` call waiting for the clock to start once frames are ready.
40
- */
41
- startAttempt;
38
+ buffering = new Buffering(this.lifecycle);
42
39
  clockWatch = new ClockWatch();
43
40
  seeks = new SeekOrder();
44
41
  constructor(parts) {
@@ -60,7 +57,7 @@ var PlaybackSession = class {
60
57
  async play() {
61
58
  if (this.lifecycle.current === "playing") return;
62
59
  if (this.lifecycle.current === "buffering") {
63
- await this.startAttempt?.promise;
60
+ await this.buffering.waitingStart;
64
61
  return;
65
62
  }
66
63
  await this.lifecycle.change(() => this.beginStart());
@@ -77,7 +74,7 @@ var PlaybackSession = class {
77
74
  if (wasRunning) this.clockWatch.stoppedAt(this.parts.clock.currentTime);
78
75
  if (!this.lifecycle.canMoveTo("paused")) return;
79
76
  this.lifecycle.moveTo("paused");
80
- this.settleStartAttempt();
77
+ this.buffering.settleStart();
81
78
  this.timeUpdates.announce(this.parts.clock.currentTime);
82
79
  });
83
80
  }
@@ -102,7 +99,8 @@ var PlaybackSession = class {
102
99
  this.moveClockTo(target);
103
100
  this.startRun(target);
104
101
  this.lifecycle.moveTo("seeking");
105
- this.lifecycle.moveTo(this.seeks.resumesPlaying ? "buffering" : "paused");
102
+ if (this.seeks.resumesPlaying) this.buffering.enter("priming");
103
+ else this.lifecycle.moveTo("paused");
106
104
  this.timeUpdates.announce(target);
107
105
  });
108
106
  }
@@ -176,7 +174,7 @@ var PlaybackSession = class {
176
174
  this.presented = void 0;
177
175
  this.parts.clock.pause();
178
176
  this.lifecycle.moveTo("disposed");
179
- this.settleStartAttempt();
177
+ this.buffering.settleStart();
180
178
  });
181
179
  this.events.removeAll();
182
180
  }
@@ -197,11 +195,8 @@ var PlaybackSession = class {
197
195
  * started (a media key's play). A pause settles the wait.
198
196
  */
199
197
  startOncePrimed() {
200
- const attempt = new Deferred();
201
- this.startAttempt = attempt;
202
198
  this.parts.clock.pause();
203
- this.lifecycle.moveTo("buffering");
204
- return attempt.promise;
199
+ return this.buffering.enterForStart();
205
200
  }
206
201
  /**
207
202
  * Starts the clock once `playing` has been heard, unless a listener paused on it: an audio
@@ -239,7 +234,7 @@ var PlaybackSession = class {
239
234
  }
240
235
  if (this.run?.isStarvedAt(now, this.presented?.pair.timestamp) === true) {
241
236
  this.parts.clock.pause();
242
- this.lifecycle.moveTo("buffering");
237
+ this.buffering.enter("starvation");
243
238
  return;
244
239
  }
245
240
  this.timeUpdates.followPlayback(now);
@@ -299,8 +294,12 @@ var PlaybackSession = class {
299
294
  const wasRunning = clock.isRunning;
300
295
  clock.pause();
301
296
  this.startRun(now);
302
- this.lifecycle.moveTo(wasRunning ? "buffering" : "paused");
303
- if (!wasRunning) this.timeUpdates.announce(now);
297
+ if (wasRunning) {
298
+ this.buffering.enter("priming");
299
+ return;
300
+ }
301
+ this.lifecycle.moveTo("paused");
302
+ this.timeUpdates.announce(now);
304
303
  }
305
304
  /**
306
305
  * Ended, or paused at the end: a play from there starts over, as a media element does.
@@ -315,14 +314,14 @@ var PlaybackSession = class {
315
314
  * Called as pairs arrive and when a run ends: the moment `buffering` has what it waits for.
316
315
  */
317
316
  resumeIfPrimed() {
318
- if (this.lifecycle.current === "buffering" && this.isPrimed()) this.resume();
317
+ if (this.buffering.isOverAt(this.run, this.parts.clock.currentTime)) this.resume();
319
318
  }
320
319
  /**
321
320
  * Leaves `buffering` for `playing` and starts the clock; a refusal (only possible before the
322
321
  * first start) lands `paused` and is reported to the waiting `play`.
323
322
  */
324
323
  async resume() {
325
- const attempt = this.takeStartAttempt();
324
+ const attempt = this.buffering.takeStart();
326
325
  this.enterPlaying();
327
326
  try {
328
327
  await this.startClockOnceHeard();
@@ -339,14 +338,6 @@ var PlaybackSession = class {
339
338
  throw error;
340
339
  }
341
340
  }
342
- settleStartAttempt() {
343
- this.takeStartAttempt()?.resolve();
344
- }
345
- takeStartAttempt() {
346
- const attempt = this.startAttempt;
347
- this.startAttempt = void 0;
348
- return attempt;
349
- }
350
341
  /**
351
342
  * The clock's own media ran out (an audio track shorter than the video: the pairs after it are
352
343
  * unreachable, and the decoders may wait for room to decode them forever), or everything
@@ -408,7 +399,7 @@ var PlaybackSession = class {
408
399
  this.parts.clock.pause();
409
400
  this.abortRun();
410
401
  this.lifecycle.moveTo("error");
411
- this.settleStartAttempt();
402
+ this.buffering.settleStart();
412
403
  this.lifecycle.announce("error", asGyroViewError(error, "decode", "playback failed"));
413
404
  });
414
405
  }
@@ -6,7 +6,7 @@
6
6
  */
7
7
  async function keyframeTimeAt(track, time) {
8
8
  try {
9
- return (await track.keyPacketAt(time))?.timestamp ?? time;
9
+ return (await track.keyframeAt(time))?.timestamp ?? time;
10
10
  } catch {
11
11
  return time;
12
12
  }
@@ -93,8 +93,10 @@ var SourceProbe = class {
93
93
  const configuration = await this.track.decoderConfiguration();
94
94
  if (!await this.decoderPort.isSupported(configuration)) return unsupported(configuration);
95
95
  if (this.isClosed) return TIMED_OUT;
96
+ const firstKeyframe = await this.track.firstKeyframe();
97
+ if (!firstKeyframe) return NO_KEY_FRAME;
96
98
  this.isReadingKeyFrame = true;
97
- const keyPacket = await this.track.firstKeyPacket();
99
+ const keyPacket = await firstPacketFrom(this.track, firstKeyframe.timestamp);
98
100
  this.isReadingKeyFrame = false;
99
101
  return keyPacket ? await this.decodeFirst(configuration, keyPacket) : NO_KEY_FRAME;
100
102
  }
@@ -164,5 +166,17 @@ function failedWith(error) {
164
166
  detail: message
165
167
  };
166
168
  }
169
+ /**
170
+ * The first packet the track hands out from `time`, its iteration let go of at once.
171
+ */
172
+ async function firstPacketFrom(track, time) {
173
+ const packets = track.packetsFrom(time)[Symbol.asyncIterator]();
174
+ try {
175
+ const first = await packets.next();
176
+ return first.done === true ? void 0 : first.value;
177
+ } finally {
178
+ packets.return?.();
179
+ }
180
+ }
167
181
  //#endregion
168
182
  export { probeDecoding };
@@ -0,0 +1,27 @@
1
+ import { GyroViewError } from "../../shared/errors/GyroViewError.js";
2
+ import { scanBoxes } from "../../domain/format/boxes/scanBoxes.js";
3
+ import { Mp4BoxType } from "../../domain/format/mp4/mp4BoxTypes.js";
4
+ import { parseMovie } from "../../domain/format/mp4/parseMovie.js";
5
+ import { concatenated } from "../../shared/binary/concatenated.js";
6
+ //#region ../../packages/core/src/application/recording/readSampleTable.ts
7
+ /**
8
+ * Use case: read where every sample of a file lies and when it shows. The top-level boxes are
9
+ * walked header by header, then the file type and the movie box are read whole, once each; not
10
+ * a byte of the media data is read.
11
+ */
12
+ async function readSampleTable(source) {
13
+ const boxes = await scanBoxes(source, await source.size());
14
+ const movie = boxes.find((box) => box.type === Mp4BoxType.Movie);
15
+ if (!movie) throw new GyroViewError("unsupported-container", "the file has no movie box to read its samples from");
16
+ const fileType = boxes.find((box) => box.type === Mp4BoxType.FileType);
17
+ const [fileTypeBytes, movieBox] = await Promise.all([bytesOf(source, fileType), bytesOf(source, movie)]);
18
+ return {
19
+ table: parseMovie(movieBox),
20
+ movieBytes: concatenated([fileTypeBytes, movieBox])
21
+ };
22
+ }
23
+ function bytesOf(source, box) {
24
+ return box ? source.read(box.range) : Promise.resolve(/* @__PURE__ */ new Uint8Array());
25
+ }
26
+ //#endregion
27
+ export { readSampleTable };
@@ -33,12 +33,12 @@ async function timeByTheClock(recording, frameSource) {
33
33
  };
34
34
  }
35
35
  /**
36
- * Where the frame source's frames lie in time: the first shows where the first key packet does
37
- * (a track without one does not decode), and the rest follow at that packet's duration, the
36
+ * Where the frame source's frames lie in time: the first shows where the first key frame does
37
+ * (a track without one does not decode), and the rest follow at that frame's duration, the
38
38
  * cameras recording at a constant rate.
39
39
  */
40
40
  async function timelineOf(recordingClock, frameSource) {
41
- const firstKey = await frameSource.firstKeyPacket();
41
+ const firstKey = await frameSource.firstKeyframe();
42
42
  const hasDuration = firstKey !== void 0 && firstKey.duration > 0;
43
43
  return {
44
44
  clock: recordingClock.withFirstFrameAt(firstKey?.timestamp ?? seconds(0)),
@@ -0,0 +1,7 @@
1
+ //#region ../../packages/core/src/domain/container/KeyframeRule.ts
2
+ /**
3
+ * For tracks whose every sample decodes on its own, as audio does.
4
+ */
5
+ var EVERY_SAMPLE_IS_A_KEYFRAME = { isKeyframe: () => true };
6
+ //#endregion
7
+ export { EVERY_SAMPLE_IS_A_KEYFRAME };
@@ -0,0 +1,29 @@
1
+ import { seconds } from "../../shared/units/time.js";
2
+ //#region ../../packages/core/src/domain/container/SampleTable.ts
3
+ /**
4
+ * Every sample of one file, track by track, as its movie box describes them: the player's own
5
+ * reading of the container, from which it downloads and decodes.
6
+ */
7
+ var SampleTable = class {
8
+ tracks;
9
+ constructor(tracks) {
10
+ this.tracks = tracks;
11
+ }
12
+ get videoTracks() {
13
+ return this.tracks.filter((track) => track.kind === "video");
14
+ }
15
+ get audioTracks() {
16
+ return this.tracks.filter((track) => track.kind === "audio");
17
+ }
18
+ /**
19
+ * Until the last sample of any track stops showing.
20
+ */
21
+ get duration() {
22
+ return seconds(Math.max(0, ...this.tracks.map((track) => track.end)));
23
+ }
24
+ trackWithId(trackId) {
25
+ return this.tracks.find((track) => track.trackId === trackId);
26
+ }
27
+ };
28
+ //#endregion
29
+ export { SampleTable };
@@ -0,0 +1,96 @@
1
+ import { ensureIndexInRange, ensureInvariant } from "../../shared/errors/GyroViewError.js";
2
+ import { seconds } from "../../shared/units/time.js";
3
+ import { ByteRange } from "../../shared/binary/ByteRange.js";
4
+ import { presentationOrderOf } from "./presentationOrder.js";
5
+ import { countAtOrBelow } from "../../shared/math/countAtOrBelow.js";
6
+ //#region ../../packages/core/src/domain/container/TrackSampleTable.ts
7
+ /**
8
+ * One track's samples as its sample table describes them: where each lies, when it shows and
9
+ * where decoding may start. Samples are numbered from zero in decode order; the presentation
10
+ * order, which composition offsets may make another, is kept beside it.
11
+ */
12
+ var TrackSampleTable = class {
13
+ parts;
14
+ trackId;
15
+ kind;
16
+ end;
17
+ keyframeRule;
18
+ /**
19
+ * The samples in the order they show; ties keep decode order.
20
+ */
21
+ presentationOrder;
22
+ constructor(parts) {
23
+ this.parts = parts;
24
+ const { length } = parts.offsets;
25
+ const columns = [
26
+ parts.sizes,
27
+ parts.timestamps,
28
+ parts.durations
29
+ ];
30
+ ensureInvariant(columns.every((column) => column.length === length), `the sample table of track ${parts.trackId} disagrees on its sample count`);
31
+ ensureInvariant(isAscendingWithin(parts.syncSamples ?? [], length), `the sync samples of track ${parts.trackId} are not samples of it in order`);
32
+ this.trackId = parts.trackId;
33
+ this.kind = parts.kind;
34
+ this.end = parts.end;
35
+ this.keyframeRule = parts.keyframeRule;
36
+ this.presentationOrder = presentationOrderOf(parts.timestamps);
37
+ }
38
+ get sampleCount() {
39
+ return this.parts.offsets.length;
40
+ }
41
+ rangeOf(sample) {
42
+ this.ensureSample(sample);
43
+ return ByteRange.of(this.parts.offsets[sample] ?? 0, this.parts.sizes[sample] ?? 0);
44
+ }
45
+ timestampOf(sample) {
46
+ this.ensureSample(sample);
47
+ return seconds(this.parts.timestamps[sample] ?? 0);
48
+ }
49
+ durationOf(sample) {
50
+ this.ensureSample(sample);
51
+ return seconds(this.parts.durations[sample] ?? 0);
52
+ }
53
+ isSync(sample) {
54
+ this.ensureSample(sample);
55
+ return this.syncSampleAtOrBefore(sample) === sample;
56
+ }
57
+ /**
58
+ * The sample on screen at `time`: the last to show at or before it. Undefined before the first.
59
+ */
60
+ sampleShownAt(time) {
61
+ const shownSoFar = countAtOrBelow(this.presentationOrder.length, (index) => this.parts.timestamps[this.presentationOrder[index] ?? 0] ?? Infinity, time);
62
+ return this.presentationOrder[shownSoFar - 1];
63
+ }
64
+ /**
65
+ * The sample decoding for `time` starts at: the sync sample at or before the one on screen.
66
+ */
67
+ keyframeAt(time) {
68
+ const shown = this.sampleShownAt(time);
69
+ return shown === void 0 ? void 0 : this.syncSampleAtOrBefore(shown);
70
+ }
71
+ syncSampleAtOrBefore(sample) {
72
+ const { syncSamples } = this.parts;
73
+ if (!syncSamples) return sample >= 0 ? sample : void 0;
74
+ return syncSamples[countAtOrBelow(syncSamples.length, (index) => syncSamples[index] ?? 0, sample) - 1];
75
+ }
76
+ /**
77
+ * The first sync sample at or after `sample`; none where no sync sample follows.
78
+ */
79
+ syncSampleAtOrAfter(sample) {
80
+ const { syncSamples } = this.parts;
81
+ const first = Math.max(0, sample);
82
+ if (!syncSamples) return first < this.sampleCount ? first : void 0;
83
+ return syncSamples[countAtOrBelow(syncSamples.length, (index) => syncSamples[index] ?? 0, first - 1)];
84
+ }
85
+ timestampsInPresentationOrder() {
86
+ return this.presentationOrder.map((sample) => seconds(this.parts.timestamps[sample] ?? 0));
87
+ }
88
+ ensureSample(sample) {
89
+ ensureIndexInRange(sample, this.sampleCount, `sample of track ${this.trackId}`);
90
+ }
91
+ };
92
+ function isAscendingWithin(samples, count) {
93
+ return samples.every((sample, index) => Number.isSafeInteger(sample) && sample >= 0 && sample < count && sample > (samples[index - 1] ?? -1));
94
+ }
95
+ //#endregion
96
+ export { TrackSampleTable };
@@ -0,0 +1,10 @@
1
+ //#region ../../packages/core/src/domain/container/presentationOrder.ts
2
+ /**
3
+ * The samples in the order they show, given when each shows in decode order; samples that show
4
+ * at the same time keep their decode order, as a stable sort keeps them.
5
+ */
6
+ function presentationOrderOf(times) {
7
+ return Array.from({ length: times.length }, (_, sample) => sample).toSorted((left, right) => (times[left] ?? 0) - (times[right] ?? 0));
8
+ }
9
+ //#endregion
10
+ export { presentationOrderOf };
@@ -0,0 +1,41 @@
1
+ import { seconds } from "../../shared/units/time.js";
2
+ //#region ../../packages/core/src/domain/download/DownloadPolicy.ts
3
+ var MEBIBYTE = 1048576;
4
+ /**
5
+ * What the policy reads ahead and keeps behind, as time and at most as bytes: the byte limits
6
+ * bind first on the X5's 210 Mbit/s, the times on slower recordings. A split recording shares
7
+ * the byte limits among its files.
8
+ */
9
+ var AHEAD_SECONDS = 10;
10
+ var MOST_AHEAD_MEBIBYTES = 128;
11
+ var KEEP_BEHIND_SECONDS = 2;
12
+ var MOST_KEPT_BEHIND_MEBIBYTES = 32;
13
+ var REQUEST_MEBIBYTES = 8;
14
+ var REQUESTS_IN_FLIGHT = 2;
15
+ var BRIDGED_GAP_MEBIBYTES = 1;
16
+ var REFILL_FRACTION = .25;
17
+ /**
18
+ * Long enough that a link slower than the recording plays in stretches rather than frame by
19
+ * frame, short enough that the wait does not feel like a stop.
20
+ */
21
+ var RESUME_SECONDS = 4;
22
+ /**
23
+ * The policy for one file of a recording of `fileCount` files, by its bit rate.
24
+ */
25
+ function downloadPolicyFor(file, fileCount) {
26
+ const bytesPerSecond = file.size / file.duration;
27
+ const aheadBytes = Math.min(Math.round(AHEAD_SECONDS * bytesPerSecond), MOST_AHEAD_MEBIBYTES * MEBIBYTE / fileCount);
28
+ return {
29
+ aheadSeconds: seconds(AHEAD_SECONDS),
30
+ aheadBytes,
31
+ keepBehindBytes: Math.min(Math.round(KEEP_BEHIND_SECONDS * bytesPerSecond), MOST_KEPT_BEHIND_MEBIBYTES * MEBIBYTE / fileCount),
32
+ keepBehindSeconds: seconds(KEEP_BEHIND_SECONDS),
33
+ requestSize: REQUEST_MEBIBYTES * MEBIBYTE,
34
+ requestsInFlight: REQUESTS_IN_FLIGHT,
35
+ bridgedGap: BRIDGED_GAP_MEBIBYTES * MEBIBYTE,
36
+ refillBytes: Math.round(aheadBytes * REFILL_FRACTION),
37
+ resumeSeconds: seconds(RESUME_SECONDS)
38
+ };
39
+ }
40
+ //#endregion
41
+ export { downloadPolicyFor };
@@ -0,0 +1,27 @@
1
+ import { seconds } from "../../shared/units/time.js";
2
+ import { ByteRangeSet } from "../../shared/binary/ByteRangeSet.js";
3
+ import { needsOf, withinBytes } from "./trackNeeds.js";
4
+ //#region ../../packages/core/src/domain/download/isReadyToResume.ts
5
+ /**
6
+ * What playback that starved at `time` waits for before it plays again (ADR 0011, ADR 0029):
7
+ * every picture reader's frames up to the policy's resume seconds past it, or as many of them as
8
+ * the budget holds less a request (a refill's worth, where that is less), so the download that
9
+ * keeps it held still tops up in large ranges. Sound needs nothing of its own.
10
+ */
11
+ function resumeThresholdOf(need) {
12
+ const { policy } = need;
13
+ const until = seconds(need.time + policy.resumeSeconds);
14
+ const pictures = need.cursors.filter((cursor) => cursor.track.kind === "video");
15
+ const needs = ByteRangeSet.of(pictures.flatMap((cursor) => needsOf(cursor, until)));
16
+ const margin = Math.min(policy.requestSize, policy.refillBytes);
17
+ return withinBytes(needs, policy.aheadBytes - margin);
18
+ }
19
+ /**
20
+ * Whether the threshold is held: the download keeps it held once playing, so a download that
21
+ * reads ahead is ready as soon as its bytes have come.
22
+ */
23
+ function isReadyToResume(state) {
24
+ return resumeThresholdOf(state).subtract(state.held).isEmpty;
25
+ }
26
+ //#endregion
27
+ export { isReadyToResume, resumeThresholdOf };
@@ -0,0 +1,175 @@
1
+ import { seconds } from "../../shared/units/time.js";
2
+ import { ByteRange } from "../../shared/binary/ByteRange.js";
3
+ import { ByteRangeSet } from "../../shared/binary/ByteRangeSet.js";
4
+ import { needsOf, withinBytes } from "./trackNeeds.js";
5
+ import { resumeThresholdOf } from "./isReadyToResume.js";
6
+ //#region ../../packages/core/src/domain/download/planDownloads.ts
7
+ /**
8
+ * The download's plan, from where its readers stand (ADR 0029). The window starts at the
9
+ * slowest picture reader, where the picture last stood once its readers closed (so the sound
10
+ * after the last frame still comes), or at the sound where no picture was ever read; once
11
+ * playing it reaches as far ahead as the policy's seconds and bytes allow, before that only to
12
+ * what the readers wait for. Every picture reader's next frame is read, however far from the
13
+ * others, so none waits for ever; a reader a seek left behind wants nothing until it follows.
14
+ * What is wanted is asked for lowest first and in large ranges: at once when playback needs it
15
+ * now, otherwise once a refill's worth is missing.
16
+ */
17
+ function planDownloads(state) {
18
+ const pictures = state.cursors.filter((cursor) => isPicture(cursor));
19
+ const anchor = pictures.length > 0 ? anchorOf(pictures) : state.anchor;
20
+ const windowStart = anchor ?? soundLeadOf(state);
21
+ if (!windowStart) return nothingToRead(state);
22
+ const ahead = state.isReadingAhead ? state.policy.aheadSeconds : 0;
23
+ const windowEnd = seconds(windowStart.time + ahead);
24
+ const wanted = wantedBytes({
25
+ state,
26
+ windowStart,
27
+ windowEnd
28
+ }).bridgingGapsBelow(state.policy.bridgedGap);
29
+ const kept = state.transfers.filter((transfer) => transfer.remaining.length === 0 || wanted.overlaps(transfer.remaining));
30
+ const coming = ByteRangeSet.of(kept.map((transfer) => transfer.remaining));
31
+ const missing = wanted.subtract(state.held).subtract(coming).subtract(state.unreadable);
32
+ const room = state.policy.requestsInFlight - kept.length;
33
+ return {
34
+ start: isTopUpDue({
35
+ state,
36
+ windowStart,
37
+ windowEnd,
38
+ missing,
39
+ threshold: thresholdOf(state, windowStart)
40
+ }) ? requestsFor(missing, state.policy, room) : [],
41
+ cancel: state.transfers.filter((transfer) => !kept.includes(transfer)).map((transfer) => transfer.id),
42
+ release: state.held.subtract(wanted.union(keptBehind(windowStart, state.policy))),
43
+ anchor
44
+ };
45
+ }
46
+ /**
47
+ * Where the window starts once playing where no picture was ever read: at the sound, which then
48
+ * has no picture to follow, and follows it as it moves on.
49
+ */
50
+ function soundLeadOf(state) {
51
+ return state.isReadingAhead && state.cursors.length > 0 ? anchorOf(state.cursors) : void 0;
52
+ }
53
+ function nothingToRead(state) {
54
+ return {
55
+ start: [],
56
+ cancel: state.transfers.map((transfer) => transfer.id),
57
+ release: ByteRangeSet.empty,
58
+ anchor: void 0
59
+ };
60
+ }
61
+ function anchorOf(cursors) {
62
+ return {
63
+ time: seconds(Math.min(...cursors.map((cursor) => timeOf(cursor)))),
64
+ offset: Math.min(...cursors.map((cursor) => offsetOf(cursor))),
65
+ keyframeOffset: Math.min(...cursors.map((cursor) => keyframeOffsetOf(cursor)))
66
+ };
67
+ }
68
+ /**
69
+ * Every sample the readers following the picture will hand out within the window, clipped to
70
+ * the bytes the policy allows ahead, nearest first; and the next sample of every picture reader
71
+ * and of every reader waiting within the window, whatever the budget and wherever the file
72
+ * puts it, so no reader waits for ever.
73
+ */
74
+ function wantedBytes(window) {
75
+ const { state, windowEnd } = window;
76
+ const following = state.cursors.filter((cursor) => isFollowing(cursor, window));
77
+ const needs = following.flatMap((cursor) => needsOf(cursor, laterOf(windowEnd, cursor.target)));
78
+ const next = following.filter((cursor) => isNextSampleOwed(cursor, windowEnd));
79
+ const nextSamples = ByteRangeSet.of(next.map((cursor) => cursor.track.rangeOf(cursor.sample)));
80
+ return withinBytes(ByteRangeSet.of(needs), state.policy.aheadBytes).union(nextSamples);
81
+ }
82
+ /**
83
+ * A reader no further behind the picture than the policy allows: the sound lags the picture
84
+ * by the decoders' lead, and until it follows a seek it stands far behind.
85
+ */
86
+ function isFollowing(cursor, window) {
87
+ return timeOf(cursor) >= window.windowStart.time - window.state.policy.keepBehindSeconds;
88
+ }
89
+ function isNextSampleOwed(cursor, windowEnd) {
90
+ const isWaitingWithin = cursor.isWaiting && timeOf(cursor) <= windowEnd;
91
+ return !isAtItsEnd(cursor) && (isPicture(cursor) || isWaitingWithin);
92
+ }
93
+ function laterOf(time, other) {
94
+ return other !== void 0 && other > time ? other : time;
95
+ }
96
+ function isPicture(cursor) {
97
+ return cursor.track.kind === "video";
98
+ }
99
+ /**
100
+ * Once playing, what playback that starved would wait for, which the download keeps held; none
101
+ * before.
102
+ */
103
+ function thresholdOf(state, windowStart) {
104
+ const need = {
105
+ cursors: state.cursors,
106
+ time: windowStart.time,
107
+ policy: state.policy
108
+ };
109
+ return state.isReadingAhead ? resumeThresholdOf(need) : ByteRangeSet.empty;
110
+ }
111
+ /**
112
+ * Whether to ask for what is missing now: a reader needs it at once, the resume threshold is
113
+ * not all held, or a refill's worth is missing. The last seconds of the tracks come as the
114
+ * threshold reaches them, however few their bytes.
115
+ */
116
+ function isTopUpDue(topUp) {
117
+ const { state, missing, threshold } = topUp;
118
+ if (missing.ranges.some((range) => threshold.overlaps(range))) return true;
119
+ return state.cursors.some((cursor) => isNeededAtOnce(cursor, topUp) && missing.overlaps(cursor.track.rangeOf(cursor.sample))) || missing.totalLength >= state.policy.refillBytes;
120
+ }
121
+ /**
122
+ * A reader waiting on a sample playback needs now: a picture's, or one due within the resume
123
+ * seconds. Sound that buffers far ahead waits where the downloaded bytes end, and would
124
+ * otherwise ask for a small range every time the picture moved.
125
+ */
126
+ function isNeededAtOnce(cursor, topUp) {
127
+ if (!cursor.isWaiting || isAtItsEnd(cursor)) return false;
128
+ const { windowStart, state } = topUp;
129
+ const horizon = windowStart.time + (state.isReadingAhead ? state.policy.resumeSeconds : 0);
130
+ return isPicture(cursor) || timeOf(cursor) <= horizon;
131
+ }
132
+ /**
133
+ * The missing ranges in order, cut to the request size, as many as there is room for.
134
+ */
135
+ function requestsFor(missing, policy, room) {
136
+ const requests = [];
137
+ for (const range of missing.ranges) for (let offset = range.offset; offset < range.end && requests.length < room; offset += policy.requestSize) requests.push(ByteRange.of(offset, Math.min(policy.requestSize, range.end - offset)));
138
+ return requests;
139
+ }
140
+ /**
141
+ * The stretch behind the picture that is kept for short seeks back: its group of pictures from
142
+ * the key frame, or the policy's distance, whichever reaches further back.
143
+ */
144
+ function keptBehind(anchor, policy) {
145
+ const start = keptBehindStart(anchor, policy);
146
+ return ByteRangeSet.of([ByteRange.of(start, anchor.offset - start)]);
147
+ }
148
+ function keptBehindStart(anchor, policy) {
149
+ return Math.max(0, Math.min(anchor.keyframeOffset, anchor.offset - policy.keepBehindBytes));
150
+ }
151
+ function isAtItsEnd(cursor) {
152
+ return cursor.sample >= cursor.track.sampleCount;
153
+ }
154
+ /**
155
+ * When a picture reader stands: at its next frame, or at its track's end once it read them all.
156
+ */
157
+ function timeOf(cursor) {
158
+ return isAtItsEnd(cursor) ? cursor.track.end : cursor.track.timestampOf(cursor.sample);
159
+ }
160
+ /**
161
+ * Where the key frame before a reader's next sample lies; a sound sample is its own.
162
+ */
163
+ function keyframeOffsetOf(cursor) {
164
+ const { track } = cursor;
165
+ const last = Math.min(cursor.sample, track.sampleCount - 1);
166
+ const keyframe = track.syncSampleAtOrBefore(last);
167
+ return keyframe === void 0 ? offsetOf(cursor) : track.rangeOf(keyframe).offset;
168
+ }
169
+ function offsetOf(cursor) {
170
+ const { track } = cursor;
171
+ if (!isAtItsEnd(cursor)) return track.rangeOf(cursor.sample).offset;
172
+ return track.sampleCount > 0 ? track.rangeOf(track.sampleCount - 1).end : 0;
173
+ }
174
+ //#endregion
175
+ export { planDownloads };