@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
@@ -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 };
@@ -0,0 +1,75 @@
1
+ import { ByteRange } from "../../shared/binary/ByteRange.js";
2
+ //#region ../../packages/core/src/application/download/Transfers.ts
3
+ /**
4
+ * The ranges a download has asked for and is receiving, each streamed into its block. A range
5
+ * given up is returned to the stream at once, so nothing more of it comes.
6
+ */
7
+ var Transfers = class {
8
+ parts;
9
+ nextId = 1;
10
+ running = /* @__PURE__ */ new Map();
11
+ constructor(parts) {
12
+ this.parts = parts;
13
+ }
14
+ get states() {
15
+ return [...this.running.values()].map((transfer) => ({
16
+ id: transfer.id,
17
+ remaining: remainingOf(transfer)
18
+ }));
19
+ }
20
+ start(range) {
21
+ const transfer = {
22
+ id: this.nextId,
23
+ block: this.parts.store.allocate(range),
24
+ chunks: this.parts.stream.stream(range)[Symbol.asyncIterator](),
25
+ delivered: 0
26
+ };
27
+ this.nextId += 1;
28
+ this.running.set(transfer.id, transfer);
29
+ this.pump(transfer);
30
+ }
31
+ cancel(id) {
32
+ const transfer = this.running.get(id);
33
+ if (!transfer) return;
34
+ this.running.delete(id);
35
+ transfer.chunks.return?.();
36
+ this.parts.store.trim(transfer.block);
37
+ }
38
+ cancelAll() {
39
+ for (const id of this.running.keys()) this.cancel(id);
40
+ }
41
+ async pump(transfer) {
42
+ try {
43
+ for (let next = await transfer.chunks.next(); next.done !== true; next = await transfer.chunks.next()) {
44
+ if (!this.isRunning(transfer)) return;
45
+ this.parts.store.write(transfer.block, next.value);
46
+ transfer.delivered += next.value.byteLength;
47
+ this.parts.listener.onChunk();
48
+ }
49
+ this.finish(transfer);
50
+ } catch (error) {
51
+ this.fail(transfer, error);
52
+ }
53
+ }
54
+ finish(transfer) {
55
+ if (!this.isRunning(transfer)) return;
56
+ this.running.delete(transfer.id);
57
+ this.parts.store.complete(transfer.block);
58
+ this.parts.listener.onEnd();
59
+ }
60
+ fail(transfer, error) {
61
+ if (!this.isRunning(transfer)) return;
62
+ this.running.delete(transfer.id);
63
+ this.parts.store.trim(transfer.block);
64
+ this.parts.listener.onFailure(remainingOf(transfer), error);
65
+ }
66
+ isRunning(transfer) {
67
+ return this.running.get(transfer.id) === transfer;
68
+ }
69
+ };
70
+ function remainingOf(transfer) {
71
+ const { range } = transfer.block;
72
+ return ByteRange.of(range.offset + transfer.delivered, range.length - transfer.delivered);
73
+ }
74
+ //#endregion
75
+ export { Transfers };
@@ -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) {