@bubo-squared/gyroview 0.2.0 → 0.3.1

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 (74) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +35 -15
  3. package/dist/index.d.ts +21 -2
  4. package/dist/index.js +2 -2
  5. package/dist/packages/adapters/fetch/src/HttpByteStream.js +174 -0
  6. package/dist/packages/adapters/fetch/src/HttpRangeSource.js +14 -170
  7. package/dist/packages/adapters/fetch/src/HttpResource.js +192 -0
  8. package/dist/packages/adapters/fetch/src/contentRange.js +15 -0
  9. package/dist/packages/adapters/fetch/src/httpRequest.js +5 -3
  10. package/dist/packages/adapters/fetch/src/index.js +3 -1
  11. package/dist/packages/adapters/fetch/src/passingFailures.js +24 -0
  12. package/dist/packages/adapters/fetch/src/rangeFailures.js +17 -0
  13. package/dist/packages/adapters/fetch/src/recordingVersion.js +37 -0
  14. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioPackager.js +129 -0
  15. package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +80 -0
  16. package/dist/packages/adapters/mediabunny/src/SegmentChannel.js +58 -6
  17. package/dist/packages/adapters/mediabunny/src/decoderConfigurations.js +28 -0
  18. package/dist/packages/adapters/mediabunny/src/index.js +3 -2
  19. package/dist/packages/adapters/webcodecs/src/WebCodecsVideoDecoderPort.js +12 -3
  20. package/dist/packages/core/src/application/download/BlockStore.js +97 -0
  21. package/dist/packages/core/src/application/download/DownloadedAudioSamples.js +50 -0
  22. package/dist/packages/core/src/application/download/DownloadedVideoTrack.js +122 -0
  23. package/dist/packages/core/src/application/download/FileDownload.js +183 -0
  24. package/dist/packages/core/src/application/download/RecordingBuffer.js +22 -0
  25. package/dist/packages/core/src/application/download/SampleCursor.js +88 -0
  26. package/dist/packages/core/src/application/download/SourceByteStream.js +46 -0
  27. package/dist/packages/core/src/application/download/Transfers.js +75 -0
  28. package/dist/packages/core/src/application/download/startFileDownload.js +37 -0
  29. package/dist/packages/core/src/application/playback/Buffering.js +53 -0
  30. package/dist/packages/core/src/application/playback/DecodePipeline.js +11 -7
  31. package/dist/packages/core/src/application/playback/DecodeRun.js +15 -0
  32. package/dist/packages/core/src/application/playback/PlaybackSession.js +18 -27
  33. package/dist/packages/core/src/application/playback/keyframeTimeAt.js +1 -1
  34. package/dist/packages/core/src/application/playback/probeDecoding.js +17 -3
  35. package/dist/packages/core/src/application/recording/readSampleTable.js +27 -0
  36. package/dist/packages/core/src/application/recording/timeRecording.js +3 -3
  37. package/dist/packages/core/src/domain/container/KeyframeRule.js +7 -0
  38. package/dist/packages/core/src/domain/container/SampleTable.js +29 -0
  39. package/dist/packages/core/src/domain/container/TrackSampleTable.js +96 -0
  40. package/dist/packages/core/src/domain/container/presentationOrder.js +10 -0
  41. package/dist/packages/core/src/domain/download/DownloadPolicy.js +41 -0
  42. package/dist/packages/core/src/domain/download/isReadyToResume.js +27 -0
  43. package/dist/packages/core/src/domain/download/planDownloads.js +175 -0
  44. package/dist/packages/core/src/domain/download/trackNeeds.js +32 -0
  45. package/dist/packages/core/src/domain/format/boxes/boxHeader.js +44 -0
  46. package/dist/packages/core/src/domain/format/boxes/scanBoxes.js +7 -34
  47. package/dist/packages/core/src/domain/format/mp4/keyframeRules.js +81 -0
  48. package/dist/packages/core/src/domain/format/mp4/movieBoxes.js +52 -0
  49. package/dist/packages/core/src/domain/format/mp4/mp4BoxTypes.js +46 -0
  50. package/dist/packages/core/src/domain/format/mp4/mp4Layouts.js +183 -0
  51. package/dist/packages/core/src/domain/format/mp4/parseMovie.js +67 -0
  52. package/dist/packages/core/src/domain/format/mp4/readTable.js +31 -0
  53. package/dist/packages/core/src/domain/format/mp4/sampleLocations.js +69 -0
  54. package/dist/packages/core/src/domain/format/mp4/sampleTiming.js +109 -0
  55. package/dist/packages/core/src/domain/format/mp4/trackHeaders.js +75 -0
  56. package/dist/packages/core/src/index.js +10 -2
  57. package/dist/packages/core/src/shared/async/iteration.js +24 -0
  58. package/dist/packages/core/src/shared/binary/ByteRange.js +12 -0
  59. package/dist/packages/core/src/shared/binary/ByteRangeSet.js +85 -0
  60. package/dist/packages/core/src/shared/binary/ByteReader.js +21 -3
  61. package/dist/packages/core/src/shared/binary/concatenated.js +15 -0
  62. package/dist/packages/core/src/shared/errors/GyroViewError.js +56 -3
  63. package/dist/packages/core/src/shared/math/countAtOrBelow.js +17 -0
  64. package/dist/packages/player/src/composition/browserPorts.js +23 -4
  65. package/dist/packages/player/src/composition/buildPipeline.js +15 -3
  66. package/dist/packages/player/src/composition/openInputs.js +51 -107
  67. package/dist/packages/player/src/composition/readInputs.js +111 -0
  68. package/dist/packages/player/src/controls/messages.js +3 -1
  69. package/dist/packages/player/src/inspectRecording.js +1 -1
  70. package/dist/standalone.js +58 -58
  71. package/package.json +1 -1
  72. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioSegments.js +0 -133
  73. package/dist/packages/adapters/mediabunny/src/MediabunnyDemuxer.js +0 -63
  74. package/dist/packages/adapters/mediabunny/src/MediabunnyVideoTrackReader.js +0 -84
@@ -15,10 +15,11 @@ var HARDWARE_ACCELERATION = "no-preference";
15
15
  */
16
16
  var WebCodecsVideoDecoderPort = class {
17
17
  /**
18
- * A configuration WebCodecs finds malformed is one it does not support.
18
+ * A configuration WebCodecs finds malformed is one it does not support. A browser without
19
+ * WebCodecs refuses to answer, since no configuration would do.
19
20
  */
20
21
  async isSupported(configuration) {
21
- if (typeof VideoDecoder === "undefined") return false;
22
+ if (typeof VideoDecoder === "undefined") throw webCodecsUnavailable();
22
23
  try {
23
24
  return (await VideoDecoder.isConfigSupported(this.toWebCodecsConfig(configuration))).supported === true;
24
25
  } catch {
@@ -33,7 +34,7 @@ var WebCodecsVideoDecoderPort = class {
33
34
  }
34
35
  }
35
36
  configureDecoder(configuration, callbacks) {
36
- if (typeof VideoDecoder === "undefined") throw new GyroViewError("codec-unsupported", "this browser has no WebCodecs VideoDecoder");
37
+ if (typeof VideoDecoder === "undefined") throw webCodecsUnavailable();
37
38
  return new WebCodecsDecoderHandle(this.toWebCodecsConfig(configuration), callbacks);
38
39
  }
39
40
  toWebCodecsConfig(configuration) {
@@ -114,6 +115,14 @@ var WebCodecsDecoderHandle = class {
114
115
  if (this.decoder.state !== "closed") this.decoder.close();
115
116
  }
116
117
  };
118
+ /**
119
+ * Browsers expose WebCodecs only in secure contexts, so a page served over plain HTTP is the
120
+ * likelier cause than an old browser, and the one its developer can fix.
121
+ */
122
+ function webCodecsUnavailable() {
123
+ const reason = globalThis.isSecureContext ? "this browser has no WebCodecs VideoDecoder" : "WebCodecs exists only in secure contexts (HTTPS), and this page is not one";
124
+ return new GyroViewError("webcodecs-unavailable", reason);
125
+ }
117
126
  function chunkOf(packet) {
118
127
  return new EncodedVideoChunk({
119
128
  type: packet.isKeyFrame ? "key" : "delta",
@@ -0,0 +1,97 @@
1
+ import { ensureInvariant } from "../../shared/errors/GyroViewError.js";
2
+ import { ByteRange } from "../../shared/binary/ByteRange.js";
3
+ import { ByteRangeSet } from "../../shared/binary/ByteRangeSet.js";
4
+ //#region ../../packages/core/src/application/download/BlockStore.ts
5
+ var FillingBlock = class {
6
+ range;
7
+ bytes;
8
+ filled = 0;
9
+ /**
10
+ * No more of its bytes will come: all have, or its range was given up.
11
+ */
12
+ isComplete = false;
13
+ constructor(range, bytes) {
14
+ this.range = range;
15
+ this.bytes = bytes;
16
+ }
17
+ get filledRange() {
18
+ return ByteRange.of(this.range.offset, this.filled);
19
+ }
20
+ };
21
+ /**
22
+ * The bytes a download holds, a block for each range it asked for. A range within one block is
23
+ * handed out as a view of it; one across blocks is copied out of them.
24
+ */
25
+ var BlockStore = class {
26
+ blocks = [];
27
+ get held() {
28
+ return ByteRangeSet.of(this.blocks.map((block) => block.filledRange));
29
+ }
30
+ /**
31
+ * The memory the blocks take, whether their bytes have come or not.
32
+ */
33
+ get allocatedBytes() {
34
+ return this.blocks.reduce((total, block) => total + block.bytes.byteLength, 0);
35
+ }
36
+ allocate(range) {
37
+ const block = new FillingBlock(range, new Uint8Array(range.length));
38
+ this.blocks.push(block);
39
+ return block;
40
+ }
41
+ write(block, chunk) {
42
+ const filling = this.fillingOf(block);
43
+ filling.bytes.set(chunk, filling.filled);
44
+ filling.filled += chunk.byteLength;
45
+ }
46
+ /**
47
+ * Every byte of the block has come.
48
+ */
49
+ complete(block) {
50
+ this.fillingOf(block).isComplete = true;
51
+ }
52
+ /**
53
+ * Keeps of a block only what has come, as when its range is given up; a block that got
54
+ * nothing goes.
55
+ */
56
+ trim(block) {
57
+ const filling = this.fillingOf(block);
58
+ filling.isComplete = true;
59
+ filling.bytes = filling.bytes.slice(0, filling.filled);
60
+ this.blocks = this.blocks.filter((held) => held.bytes.byteLength > 0);
61
+ }
62
+ /**
63
+ * The bytes of `range`, if every one of them has come.
64
+ */
65
+ bytesOf(range) {
66
+ const within = this.blocks.find((block) => block.filledRange.contains(range));
67
+ if (within) return viewOf(within, range);
68
+ return this.held.covers(range) ? this.copyOf(range) : void 0;
69
+ }
70
+ /**
71
+ * Lets go of the complete blocks whose every byte `released` holds; a block still filling is
72
+ * its transfer's until it ends.
73
+ */
74
+ release(released) {
75
+ this.blocks = this.blocks.filter((block) => !block.isComplete || !released.covers(block.filledRange));
76
+ }
77
+ fillingOf(block) {
78
+ const filling = this.blocks.find((held) => held === block);
79
+ ensureInvariant(filling !== void 0, "the block is not one this store holds");
80
+ return filling;
81
+ }
82
+ copyOf(range) {
83
+ const copy = new Uint8Array(range.length);
84
+ for (const block of this.blocks) {
85
+ const start = Math.max(block.range.offset, range.offset);
86
+ const end = Math.min(block.filledRange.end, range.end);
87
+ if (start < end) copy.set(block.bytes.subarray(start - block.range.offset, end - block.range.offset), start - range.offset);
88
+ }
89
+ return copy;
90
+ }
91
+ };
92
+ function viewOf(block, range) {
93
+ const start = range.offset - block.range.offset;
94
+ return block.bytes.subarray(start, start + range.length);
95
+ }
96
+ //#endregion
97
+ export { BlockStore };
@@ -0,0 +1,50 @@
1
+ import { Ending, ITERATION_END } from "../../shared/async/iteration.js";
2
+ //#region ../../packages/core/src/application/download/DownloadedAudioSamples.ts
3
+ /**
4
+ * A sound track's samples read through the file's download, within the picture's reach.
5
+ */
6
+ var DownloadedAudioSamples = class {
7
+ parts;
8
+ constructor(parts) {
9
+ this.parts = parts;
10
+ }
11
+ get duration() {
12
+ return this.parts.track.end;
13
+ }
14
+ samplesFrom(time) {
15
+ const { download, track } = this.parts;
16
+ return { [Symbol.asyncIterator]: () => new SampleReading(track, download.openCursor(track, track.sampleShownAt(time) ?? 0, time)) };
17
+ }
18
+ };
19
+ /**
20
+ * One iteration over the samples; each is copied out of what was downloaded, so the samples a
21
+ * media pipeline keeps do not keep the download's blocks alive.
22
+ */
23
+ var SampleReading = class {
24
+ track;
25
+ cursor;
26
+ ending = new Ending();
27
+ constructor(track, cursor) {
28
+ this.track = track;
29
+ this.cursor = cursor;
30
+ }
31
+ async next() {
32
+ const read = await this.cursor.nextSample();
33
+ if (!read || this.ending.hasEnded()) return ITERATION_END;
34
+ return {
35
+ done: false,
36
+ value: {
37
+ timestamp: this.track.timestampOf(read.sample),
38
+ duration: this.track.durationOf(read.sample),
39
+ data: Uint8Array.from(read.bytes)
40
+ }
41
+ };
42
+ }
43
+ return() {
44
+ this.ending.end();
45
+ this.cursor.close();
46
+ return Promise.resolve(ITERATION_END);
47
+ }
48
+ };
49
+ //#endregion
50
+ export { DownloadedAudioSamples };
@@ -0,0 +1,122 @@
1
+ import { GyroViewError } from "../../shared/errors/GyroViewError.js";
2
+ import { Ending, ITERATION_END } from "../../shared/async/iteration.js";
3
+ //#region ../../packages/core/src/application/download/DownloadedVideoTrack.ts
4
+ /**
5
+ * One lens's track, its packets read through the file's download. Where decoding may start comes
6
+ * from the sample table; the first packet decoding starts from is checked against its own bytes,
7
+ * and a sync sample found not to be a keyframe is passed over from then on, so the times it tells
8
+ * agree with the packets it hands out.
9
+ */
10
+ var DownloadedVideoTrack = class {
11
+ parts;
12
+ rejected = /* @__PURE__ */ new Set();
13
+ constructor(parts) {
14
+ this.parts = parts;
15
+ }
16
+ get description() {
17
+ return this.parts.codec.description;
18
+ }
19
+ decoderConfiguration() {
20
+ return Promise.resolve(this.parts.codec.configuration);
21
+ }
22
+ keyframeAt(time) {
23
+ return Promise.resolve(this.timeOf(this.keyframeSampleAt(time)));
24
+ }
25
+ firstKeyframe() {
26
+ return Promise.resolve(this.timeOf(this.firstKeyframeSample()));
27
+ }
28
+ packetsFrom(time) {
29
+ return { [Symbol.asyncIterator]: () => new PacketReading({
30
+ track: this.parts.track,
31
+ open: () => this.cursorFrom(time),
32
+ reject: (sample) => {
33
+ this.rejected.add(sample);
34
+ },
35
+ packetOf: (read) => this.packetOf(read)
36
+ }) };
37
+ }
38
+ sampleTimestamps() {
39
+ return Promise.resolve(this.parts.track.timestampsInPresentationOrder());
40
+ }
41
+ frameCount() {
42
+ return Promise.resolve(this.parts.track.sampleCount);
43
+ }
44
+ cursorFrom(time) {
45
+ const start = this.keyframeSampleAt(time) ?? this.firstKeyframeSample();
46
+ return start === void 0 ? void 0 : this.parts.download.openCursor(this.parts.track, start, time);
47
+ }
48
+ /**
49
+ * The sync sample decoding for `time` starts at, past those found not to be keyframes.
50
+ */
51
+ keyframeSampleAt(time) {
52
+ const { track } = this.parts;
53
+ let sample = track.keyframeAt(time);
54
+ while (sample !== void 0 && this.rejected.has(sample)) sample = track.syncSampleAtOrBefore(sample - 1);
55
+ return sample;
56
+ }
57
+ firstKeyframeSample() {
58
+ const { track } = this.parts;
59
+ let sample = track.syncSampleAtOrAfter(0);
60
+ while (sample !== void 0 && this.rejected.has(sample)) sample = track.syncSampleAtOrAfter(sample + 1);
61
+ return sample;
62
+ }
63
+ timeOf(sample) {
64
+ const { track } = this.parts;
65
+ return sample === void 0 ? void 0 : {
66
+ timestamp: track.timestampOf(sample),
67
+ duration: track.durationOf(sample)
68
+ };
69
+ }
70
+ packetOf(read) {
71
+ const { track } = this.parts;
72
+ return {
73
+ timestamp: track.timestampOf(read.sample),
74
+ duration: track.durationOf(read.sample),
75
+ isKeyFrame: track.isSync(read.sample) && !this.rejected.has(read.sample),
76
+ data: read.bytes
77
+ };
78
+ }
79
+ };
80
+ /**
81
+ * One iteration over a track's packets, its cursor opened at once so the download reads for it
82
+ * straight away; returning it closes the cursor at once, even while a packet is awaited.
83
+ */
84
+ var PacketReading = class {
85
+ parts;
86
+ cursor;
87
+ isVerified = false;
88
+ ending = new Ending();
89
+ constructor(parts) {
90
+ this.parts = parts;
91
+ this.cursor = parts.open();
92
+ }
93
+ async next() {
94
+ if (this.ending.hasEnded()) return ITERATION_END;
95
+ const { cursor } = this;
96
+ if (!cursor) throw new GyroViewError("no-key-frame", `track ${this.parts.track.trackId} has no key frame`);
97
+ const read = await cursor.nextSample();
98
+ if (!read || this.ending.hasEnded()) return ITERATION_END;
99
+ if (!this.isVerified && !this.parts.track.keyframeRule.isKeyframe(read.bytes)) return this.startEarlier(cursor, read);
100
+ this.isVerified = true;
101
+ return {
102
+ done: false,
103
+ value: this.parts.packetOf(read)
104
+ };
105
+ }
106
+ return() {
107
+ this.ending.end();
108
+ this.cursor?.close();
109
+ return Promise.resolve(ITERATION_END);
110
+ }
111
+ /**
112
+ * The sample decoding was to start at is no keyframe: starts again at the one before it.
113
+ */
114
+ startEarlier(cursor, read) {
115
+ this.parts.reject(read.sample);
116
+ cursor.close();
117
+ this.cursor = this.parts.open();
118
+ return this.next();
119
+ }
120
+ };
121
+ //#endregion
122
+ export { DownloadedVideoTrack };
@@ -0,0 +1,183 @@
1
+ import { asGyroViewError, ensureInvariant } from "../../shared/errors/GyroViewError.js";
2
+ import { ByteRange } from "../../shared/binary/ByteRange.js";
3
+ import { ByteRangeSet } from "../../shared/binary/ByteRangeSet.js";
4
+ import { BlockStore } from "./BlockStore.js";
5
+ import { SampleCursor } from "./SampleCursor.js";
6
+ import { Transfers } from "./Transfers.js";
7
+ import { isReadyToResume } from "../../domain/download/isReadyToResume.js";
8
+ import { planDownloads } from "../../domain/download/planDownloads.js";
9
+ //#region ../../packages/core/src/application/download/FileDownload.ts
10
+ var EVERY_BYTE = ByteRange.of(0, Number.MAX_SAFE_INTEGER);
11
+ /**
12
+ * One file of a recording downloaded while it plays (ADR 0029): the only reader of its bytes.
13
+ * Its cursors say where the readers of its tracks stand; whenever that changes, or a range comes
14
+ * whole, it plans anew, once per turn, what to ask for, give up and let go of, and hands each
15
+ * waiting cursor its sample once the bytes have come. A range that failed is not asked for again
16
+ * until a cursor opens, a seek or a replay, lest a failing server be asked without end.
17
+ */
18
+ var FileDownload = class {
19
+ parts;
20
+ store = new BlockStore();
21
+ progressListeners = /* @__PURE__ */ new Set();
22
+ transfers;
23
+ cursors = /* @__PURE__ */ new Set();
24
+ host;
25
+ failures = [];
26
+ anchor;
27
+ isReadingAhead = false;
28
+ isPlanDue = false;
29
+ isDisposed = false;
30
+ constructor(parts) {
31
+ this.parts = parts;
32
+ this.transfers = new Transfers({
33
+ stream: parts.stream,
34
+ store: this.store,
35
+ listener: {
36
+ onChunk: () => {
37
+ this.serveCursors();
38
+ for (const listener of this.progressListeners) listener();
39
+ },
40
+ onEnd: () => {
41
+ this.schedulePlan();
42
+ },
43
+ onFailure: (range, error) => {
44
+ this.fail(range, error);
45
+ }
46
+ }
47
+ });
48
+ this.host = this.cursorHost();
49
+ }
50
+ /**
51
+ * A reader of `track` from `sample` on, reading toward `target` at once.
52
+ */
53
+ openCursor(track, sample, target) {
54
+ ensureInvariant(this.parts.table.tracks.includes(track), "the track is not one of this file");
55
+ const cursor = new SampleCursor(track, {
56
+ sample,
57
+ target
58
+ }, this.host);
59
+ if (this.isDisposed) {
60
+ cursor.close();
61
+ return cursor;
62
+ }
63
+ this.failures = [];
64
+ this.cursors.add(cursor);
65
+ this.schedulePlan();
66
+ return cursor;
67
+ }
68
+ /**
69
+ * Playing has started: from now on the download reads ahead of the picture.
70
+ */
71
+ startReadingAhead() {
72
+ this.isReadingAhead = true;
73
+ this.schedulePlan();
74
+ }
75
+ /**
76
+ * Enough is downloaded ahead of its pictures for playback that starved at `time` to play again.
77
+ */
78
+ isReadyToResumeAt(time) {
79
+ return isReadyToResume({
80
+ cursors: this.positions(),
81
+ held: this.store.held,
82
+ time,
83
+ policy: this.parts.policy
84
+ });
85
+ }
86
+ onProgress(listener) {
87
+ const own = () => {
88
+ listener();
89
+ };
90
+ this.progressListeners.add(own);
91
+ return () => {
92
+ this.progressListeners.delete(own);
93
+ };
94
+ }
95
+ dispose() {
96
+ this.isDisposed = true;
97
+ this.transfers.cancelAll();
98
+ for (const cursor of this.cursors) cursor.close();
99
+ }
100
+ cursorHost() {
101
+ return {
102
+ bytesOf: (range) => this.store.bytesOf(range),
103
+ failureOf: (range) => this.failures.find((failed) => failed.range.overlaps(range))?.error,
104
+ changed: () => {
105
+ this.schedulePlan();
106
+ },
107
+ closed: (cursor) => {
108
+ this.cursors.delete(cursor);
109
+ this.schedulePlan();
110
+ }
111
+ };
112
+ }
113
+ schedulePlan() {
114
+ if (this.isPlanDue || this.isDisposed) return;
115
+ this.isPlanDue = true;
116
+ this.planAfterThisTurn();
117
+ }
118
+ /**
119
+ * Plans once every change made in this turn is in: a seek closes its old cursors and opens
120
+ * its new ones together, and the plan sees both.
121
+ */
122
+ async planAfterThisTurn() {
123
+ await Promise.resolve();
124
+ this.isPlanDue = false;
125
+ if (this.isDisposed) return;
126
+ try {
127
+ this.plan();
128
+ } catch (error) {
129
+ this.failEveryRead(error);
130
+ }
131
+ }
132
+ /**
133
+ * A plan that could not be made fails every read, rather than leave them waiting for ever.
134
+ */
135
+ failEveryRead(cause) {
136
+ const error = asGyroViewError(cause, "invariant-violation", "the download could not plan");
137
+ this.failures.push({
138
+ range: EVERY_BYTE,
139
+ error
140
+ });
141
+ for (const cursor of this.cursors) cursor.fail(EVERY_BYTE, error);
142
+ }
143
+ plan() {
144
+ const decisions = planDownloads({
145
+ cursors: this.positions(),
146
+ held: this.store.held,
147
+ transfers: this.transfers.states,
148
+ unreadable: ByteRangeSet.of(this.failures.map((failure) => failure.range)),
149
+ isReadingAhead: this.isReadingAhead,
150
+ anchor: this.anchor,
151
+ policy: this.parts.policy
152
+ });
153
+ this.anchor = decisions.anchor;
154
+ this.carryOut(decisions);
155
+ }
156
+ positions() {
157
+ return [...this.cursors].map((cursor) => ({
158
+ track: cursor.track,
159
+ sample: cursor.position,
160
+ isWaiting: cursor.isWaiting,
161
+ target: cursor.target
162
+ }));
163
+ }
164
+ carryOut(decisions) {
165
+ for (const id of decisions.cancel) this.transfers.cancel(id);
166
+ this.store.release(decisions.release);
167
+ for (const range of decisions.start) this.transfers.start(range);
168
+ }
169
+ serveCursors() {
170
+ for (const cursor of this.cursors) cursor.serve();
171
+ }
172
+ fail(range, cause) {
173
+ const error = asGyroViewError(cause, "source-unreadable", "a range of the recording could not be read");
174
+ this.failures.push({
175
+ range,
176
+ error
177
+ });
178
+ for (const cursor of this.cursors) cursor.fail(range, error);
179
+ this.schedulePlan();
180
+ }
181
+ };
182
+ //#endregion
183
+ export { FileDownload };
@@ -0,0 +1,22 @@
1
+ //#region ../../packages/core/src/application/download/RecordingBuffer.ts
2
+ /**
3
+ * A recording's buffer, over the downloads of its files (ADR 0011): ready to resume once every
4
+ * file is, since the picture of a split pair needs both lenses.
5
+ */
6
+ var RecordingBuffer = class {
7
+ files;
8
+ constructor(files) {
9
+ this.files = files;
10
+ }
11
+ isReadyToResumeAt(time) {
12
+ return this.files.every((file) => file.isReadyToResumeAt(time));
13
+ }
14
+ onProgress(listener) {
15
+ const stops = this.files.map((file) => file.onProgress(listener));
16
+ return () => {
17
+ for (const stop of stops) stop();
18
+ };
19
+ }
20
+ };
21
+ //#endregion
22
+ export { RecordingBuffer };
@@ -0,0 +1,88 @@
1
+ import { ensureInvariant } from "../../shared/errors/GyroViewError.js";
2
+ //#region ../../packages/core/src/application/download/SampleCursor.ts
3
+ /**
4
+ * One reader of a track's samples, from a sample on, in decode order: the next sample is handed
5
+ * out once its bytes have come, a read at a time. Closing it hands out no more, a read awaited
6
+ * then coming as the end, so the download stops reading for it at once.
7
+ */
8
+ var SampleCursor = class {
9
+ track;
10
+ host;
11
+ target;
12
+ next;
13
+ waiter;
14
+ isClosed = false;
15
+ constructor(track, start, host) {
16
+ this.track = track;
17
+ this.host = host;
18
+ this.next = start.sample;
19
+ this.target = start.target;
20
+ }
21
+ /**
22
+ * The next sample it hands out.
23
+ */
24
+ get position() {
25
+ return this.next;
26
+ }
27
+ get isWaiting() {
28
+ return this.waiter !== void 0;
29
+ }
30
+ /**
31
+ * The next sample; undefined at the track's end or once closed. Rejects when its bytes failed.
32
+ */
33
+ nextSample() {
34
+ if (this.isClosed || this.next >= this.track.sampleCount) return Promise.resolve(void 0);
35
+ ensureInvariant(this.waiter === void 0, "a cursor reads one sample at a time");
36
+ const range = this.track.rangeOf(this.next);
37
+ const bytes = this.host.bytesOf(range);
38
+ if (bytes) return Promise.resolve(this.take(bytes));
39
+ const failure = this.host.failureOf(range);
40
+ return failure === void 0 ? this.waitForBytes() : Promise.reject(failure);
41
+ }
42
+ /**
43
+ * Bytes came: hands the awaited sample on if they were its.
44
+ */
45
+ serve() {
46
+ const { waiter } = this;
47
+ const bytes = waiter && this.host.bytesOf(this.track.rangeOf(this.next));
48
+ if (!waiter || !bytes) return;
49
+ this.waiter = void 0;
50
+ waiter.resolve(this.take(bytes));
51
+ }
52
+ /**
53
+ * A range failed: fails the awaited read if the sample lies there.
54
+ */
55
+ fail(range, error) {
56
+ const { waiter } = this;
57
+ if (waiter === void 0 || !range.overlaps(this.track.rangeOf(this.next))) return;
58
+ this.waiter = void 0;
59
+ waiter.reject(error);
60
+ }
61
+ close() {
62
+ if (this.isClosed) return;
63
+ this.isClosed = true;
64
+ this.waiter?.resolve(void 0);
65
+ this.waiter = void 0;
66
+ this.host.closed(this);
67
+ }
68
+ waitForBytes() {
69
+ return new Promise((resolve, reject) => {
70
+ this.waiter = {
71
+ resolve,
72
+ reject
73
+ };
74
+ this.host.changed();
75
+ });
76
+ }
77
+ take(bytes) {
78
+ const sample = this.next;
79
+ this.next += 1;
80
+ this.host.changed();
81
+ return {
82
+ sample,
83
+ bytes
84
+ };
85
+ }
86
+ };
87
+ //#endregion
88
+ export { SampleCursor };
@@ -0,0 +1,46 @@
1
+ import { Ending, ITERATION_END } from "../../shared/async/iteration.js";
2
+ //#region ../../packages/core/src/application/download/SourceByteStream.ts
3
+ /**
4
+ * A ByteStream over any random-access source, a range read whole and handed on as one chunk:
5
+ * for sources whose reads are quick (a local file, bytes in memory), where streaming a range
6
+ * piece by piece would gain nothing.
7
+ */
8
+ var SourceByteStream = class {
9
+ source;
10
+ constructor(source) {
11
+ this.source = source;
12
+ }
13
+ stream(range) {
14
+ return { [Symbol.asyncIterator]: () => new WholeRangeReading(this.source, range) };
15
+ }
16
+ };
17
+ /**
18
+ * One range, read on the first chunk asked for; given up, it reads nothing more and its read
19
+ * under way comes as the end.
20
+ */
21
+ var WholeRangeReading = class {
22
+ source;
23
+ range;
24
+ ending = new Ending();
25
+ constructor(source, range) {
26
+ this.source = source;
27
+ this.range = range;
28
+ }
29
+ async next() {
30
+ await Promise.resolve();
31
+ if (this.ending.hasEnded()) return ITERATION_END;
32
+ const bytes = await this.source.read(this.range);
33
+ if (this.ending.hasEnded()) return ITERATION_END;
34
+ this.ending.end();
35
+ return bytes.byteLength === 0 ? ITERATION_END : {
36
+ done: false,
37
+ value: bytes
38
+ };
39
+ }
40
+ return() {
41
+ this.ending.end();
42
+ return Promise.resolve(ITERATION_END);
43
+ }
44
+ };
45
+ //#endregion
46
+ export { SourceByteStream };