@bubo-squared/gyroview 0.1.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 (95) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +21 -5
  3. package/dist/index.d.ts +24 -2
  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 +15 -4
  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/adapters/three/src/shaders/rawLenses.frag.js +1 -1
  19. package/dist/packages/core/src/application/download/BlockStore.js +97 -0
  20. package/dist/packages/core/src/application/download/DownloadedAudioSamples.js +50 -0
  21. package/dist/packages/core/src/application/download/DownloadedVideoTrack.js +122 -0
  22. package/dist/packages/core/src/application/download/FileDownload.js +183 -0
  23. package/dist/packages/core/src/application/download/RecordingBuffer.js +22 -0
  24. package/dist/packages/core/src/application/download/SampleCursor.js +88 -0
  25. package/dist/packages/core/src/application/download/SourceByteStream.js +46 -0
  26. package/dist/packages/core/src/application/download/Transfers.js +75 -0
  27. package/dist/packages/core/src/application/download/startFileDownload.js +37 -0
  28. package/dist/packages/core/src/application/playback/Buffering.js +53 -0
  29. package/dist/packages/core/src/application/playback/DecodePipeline.js +11 -7
  30. package/dist/packages/core/src/application/playback/DecodeRun.js +15 -0
  31. package/dist/packages/core/src/application/playback/PlaybackSession.js +18 -27
  32. package/dist/packages/core/src/application/playback/keyframeTimeAt.js +1 -1
  33. package/dist/packages/core/src/application/playback/probeDecoding.js +15 -1
  34. package/dist/packages/core/src/application/recording/readSampleTable.js +27 -0
  35. package/dist/packages/core/src/application/recording/timeRecording.js +3 -3
  36. package/dist/packages/core/src/domain/container/KeyframeRule.js +7 -0
  37. package/dist/packages/core/src/domain/container/SampleTable.js +29 -0
  38. package/dist/packages/core/src/domain/container/TrackSampleTable.js +96 -0
  39. package/dist/packages/core/src/domain/container/presentationOrder.js +10 -0
  40. package/dist/packages/core/src/domain/download/DownloadPolicy.js +41 -0
  41. package/dist/packages/core/src/domain/download/isReadyToResume.js +27 -0
  42. package/dist/packages/core/src/domain/download/planDownloads.js +175 -0
  43. package/dist/packages/core/src/domain/download/trackNeeds.js +32 -0
  44. package/dist/packages/core/src/domain/format/boxes/boxHeader.js +44 -0
  45. package/dist/packages/core/src/domain/format/boxes/scanBoxes.js +7 -34
  46. package/dist/packages/core/src/domain/format/mp4/keyframeRules.js +81 -0
  47. package/dist/packages/core/src/domain/format/mp4/movieBoxes.js +52 -0
  48. package/dist/packages/core/src/domain/format/mp4/mp4BoxTypes.js +46 -0
  49. package/dist/packages/core/src/domain/format/mp4/mp4Layouts.js +183 -0
  50. package/dist/packages/core/src/domain/format/mp4/parseMovie.js +67 -0
  51. package/dist/packages/core/src/domain/format/mp4/readTable.js +31 -0
  52. package/dist/packages/core/src/domain/format/mp4/sampleLocations.js +69 -0
  53. package/dist/packages/core/src/domain/format/mp4/sampleTiming.js +109 -0
  54. package/dist/packages/core/src/domain/format/mp4/trackHeaders.js +75 -0
  55. package/dist/packages/core/src/domain/view/lensTilesView.js +2 -2
  56. package/dist/packages/core/src/domain/view/magnification.js +1 -1
  57. package/dist/packages/core/src/domain/view/panoramaView.js +1 -1
  58. package/dist/packages/core/src/index.js +9 -1
  59. package/dist/packages/core/src/shared/async/iteration.js +24 -0
  60. package/dist/packages/core/src/shared/binary/ByteRange.js +12 -0
  61. package/dist/packages/core/src/shared/binary/ByteRangeSet.js +85 -0
  62. package/dist/packages/core/src/shared/binary/ByteReader.js +21 -3
  63. package/dist/packages/core/src/shared/binary/concatenated.js +15 -0
  64. package/dist/packages/core/src/shared/errors/GyroViewError.js +1 -0
  65. package/dist/packages/core/src/shared/math/countAtOrBelow.js +17 -0
  66. package/dist/packages/player/src/composition/browserPorts.js +38 -5
  67. package/dist/packages/player/src/composition/buildPipeline.js +15 -3
  68. package/dist/packages/player/src/composition/openInputs.js +51 -109
  69. package/dist/packages/player/src/composition/openRecording.js +13 -2
  70. package/dist/packages/player/src/composition/readInputs.js +111 -0
  71. package/dist/packages/player/src/controls/ChoiceMenu.js +16 -4
  72. package/dist/packages/player/src/controls/PictureMenus.js +22 -8
  73. package/dist/packages/player/src/controls/SeekBar.js +6 -3
  74. package/dist/packages/player/src/controls/SoundControls.js +2 -0
  75. package/dist/packages/player/src/controls/TransportButtons.js +2 -5
  76. package/dist/packages/player/src/controls/Wording.js +11 -9
  77. package/dist/packages/player/src/controls/controlParts.js +5 -3
  78. package/dist/packages/player/src/controls/controls.js +1 -1
  79. package/dist/packages/player/src/controls/controlsBar.js +4 -1
  80. package/dist/packages/player/src/controls/controlsMarkup.js +36 -21
  81. package/dist/packages/player/src/controls/icons.js +32 -8
  82. package/dist/packages/player/src/controls/messages.js +27 -4
  83. package/dist/packages/player/src/controls/sliderFill.js +16 -0
  84. package/dist/packages/player/src/element/ElementLoads.js +7 -0
  85. package/dist/packages/player/src/element/GyroViewElement.js +8 -3
  86. package/dist/packages/player/src/element/attributeNames.js +12 -1
  87. package/dist/packages/player/src/element/attributes.js +29 -7
  88. package/dist/packages/player/src/element/reflectedProperties.js +19 -1
  89. package/dist/packages/player/src/element/styles.js +1 -1
  90. package/dist/packages/player/src/inspectRecording.js +1 -1
  91. package/dist/standalone.js +385 -126
  92. package/package.json +1 -1
  93. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioSegments.js +0 -133
  94. package/dist/packages/adapters/mediabunny/src/MediabunnyDemuxer.js +0 -63
  95. package/dist/packages/adapters/mediabunny/src/MediabunnyVideoTrackReader.js +0 -84
@@ -0,0 +1,37 @@
1
+ import { GyroViewError } from "../../shared/errors/GyroViewError.js";
2
+ import { DownloadedAudioSamples } from "./DownloadedAudioSamples.js";
3
+ import { DownloadedVideoTrack } from "./DownloadedVideoTrack.js";
4
+ import { FileDownload } from "./FileDownload.js";
5
+ //#region ../../packages/core/src/application/download/startFileDownload.ts
6
+ /**
7
+ * Use case: download a file while it plays, its tracks joined to their codecs by track id.
8
+ */
9
+ function startFileDownload(start) {
10
+ const download = new FileDownload(start);
11
+ return {
12
+ download,
13
+ videoTracks: start.codecs.video.map((codec) => new DownloadedVideoTrack({
14
+ download,
15
+ track: trackOf(start.table, codec.trackId, "video"),
16
+ codec
17
+ })),
18
+ audioTracks: start.codecs.audio.map((codec) => ({
19
+ samples: new DownloadedAudioSamples({
20
+ download,
21
+ track: trackOf(start.table, codec.trackId, "audio")
22
+ }),
23
+ configuration: codec.configuration
24
+ })),
25
+ duration: start.table.duration,
26
+ dispose: () => {
27
+ download.dispose();
28
+ }
29
+ };
30
+ }
31
+ function trackOf(table, trackId, kind) {
32
+ const track = table.trackWithId(trackId);
33
+ if (track?.kind !== kind) throw new GyroViewError("unsupported-container", `the movie box has no ${kind} track ${trackId} to read`);
34
+ return track;
35
+ }
36
+ //#endregion
37
+ export { startFileDownload };
@@ -0,0 +1,53 @@
1
+ import { Deferred } from "../../shared/async/Deferred.js";
2
+ //#region ../../packages/core/src/application/playback/Buffering.ts
3
+ /**
4
+ * A session's `buffering`: entering it and why, the `play` waiting for the clock to start
5
+ * meanwhile, and what ends it, told in one place.
6
+ */
7
+ var Buffering = class {
8
+ lifecycle;
9
+ cause = "priming";
10
+ startAttempt;
11
+ constructor(lifecycle) {
12
+ this.lifecycle = lifecycle;
13
+ }
14
+ /**
15
+ * The `play` waiting for buffering to end, if one is.
16
+ */
17
+ get waitingStart() {
18
+ return this.startAttempt?.promise;
19
+ }
20
+ enter(cause) {
21
+ this.cause = cause;
22
+ this.lifecycle.moveTo("buffering");
23
+ }
24
+ /**
25
+ * Buffers for the first frames of a start, which the promise waits for.
26
+ */
27
+ enterForStart() {
28
+ const attempt = new Deferred();
29
+ this.startAttempt = attempt;
30
+ this.enter("priming");
31
+ return attempt.promise;
32
+ }
33
+ /**
34
+ * Buffering is over at `now`: what its cause waits for is there.
35
+ */
36
+ isOverAt(run, now) {
37
+ if (!run || !this.lifecycle.is("buffering")) return false;
38
+ return this.cause === "priming" ? run.isPrimedAt(now) : run.isReadyToResumeAt(now);
39
+ }
40
+ /**
41
+ * The waiting `play`, taken out to be settled by whoever ends the wait.
42
+ */
43
+ takeStart() {
44
+ const attempt = this.startAttempt;
45
+ this.startAttempt = void 0;
46
+ return attempt;
47
+ }
48
+ settleStart() {
49
+ this.takeStart()?.resolve();
50
+ }
51
+ };
52
+ //#endregion
53
+ export { Buffering };
@@ -16,6 +16,11 @@ var DecodePipeline = class {
16
16
  options;
17
17
  stop = new RunStop();
18
18
  hasRun = false;
19
+ /**
20
+ * The run's packet reads, which an abort returns at once: a read left to the run's unwinding
21
+ * would go on reading until its awaits settle.
22
+ */
23
+ iterators = [];
19
24
  constructor(frameSources, decoderPort, options) {
20
25
  this.frameSources = frameSources;
21
26
  this.decoderPort = decoderPort;
@@ -40,10 +45,12 @@ var DecodePipeline = class {
40
45
  }
41
46
  }
42
47
  /**
43
- * Ends the run early; pending decodes are discarded. Safe before, during and after the run.
48
+ * Ends the run early: its packet reads are let go of at once, pending decodes discarded. Safe
49
+ * before, during and after the run.
44
50
  */
45
51
  abort() {
46
52
  this.stop.stop();
53
+ closeIterators(this.iterators);
47
54
  }
48
55
  async openRun(from, output) {
49
56
  const { stop } = this;
@@ -59,7 +66,8 @@ var DecodePipeline = class {
59
66
  const pairer = new FramePairer(this.frameSources.length, this.options.pairTolerance, (pair) => {
60
67
  gate.push(pair);
61
68
  });
62
- const iterators = stop.wasStopped ? [] : await this.packetIteratorsFrom(from);
69
+ const iterators = stop.wasStopped ? [] : this.packetIteratorsFrom(from);
70
+ this.iterators = iterators;
63
71
  const onError = (error) => {
64
72
  failure.resolve(error);
65
73
  stop.stop();
@@ -123,11 +131,7 @@ var DecodePipeline = class {
123
131
  }
124
132
  }
125
133
  packetIteratorsFrom(from) {
126
- return Promise.all(this.frameSources.map(async (track) => {
127
- const start = await track.keyPacketAt(from) ?? await track.firstKeyPacket();
128
- if (!start) throw new GyroViewError("no-key-frame", `track ${track.description.trackIndex} has no key frame`);
129
- return track.packetsFrom(start)[Symbol.asyncIterator]();
130
- }));
134
+ return this.frameSources.map((track) => track.packetsFrom(from)[Symbol.asyncIterator]());
131
135
  }
132
136
  };
133
137
  function shouldStop(run) {
@@ -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 };