@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.
- package/CHANGELOG.md +42 -0
- package/README.md +35 -15
- package/dist/index.d.ts +21 -2
- package/dist/index.js +2 -2
- package/dist/packages/adapters/fetch/src/HttpByteStream.js +174 -0
- package/dist/packages/adapters/fetch/src/HttpRangeSource.js +14 -170
- package/dist/packages/adapters/fetch/src/HttpResource.js +192 -0
- package/dist/packages/adapters/fetch/src/contentRange.js +15 -0
- package/dist/packages/adapters/fetch/src/httpRequest.js +5 -3
- package/dist/packages/adapters/fetch/src/index.js +3 -1
- package/dist/packages/adapters/fetch/src/passingFailures.js +24 -0
- package/dist/packages/adapters/fetch/src/rangeFailures.js +17 -0
- package/dist/packages/adapters/fetch/src/recordingVersion.js +37 -0
- package/dist/packages/adapters/mediabunny/src/MediabunnyAudioPackager.js +129 -0
- package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +80 -0
- package/dist/packages/adapters/mediabunny/src/SegmentChannel.js +58 -6
- package/dist/packages/adapters/mediabunny/src/decoderConfigurations.js +28 -0
- package/dist/packages/adapters/mediabunny/src/index.js +3 -2
- package/dist/packages/adapters/webcodecs/src/WebCodecsVideoDecoderPort.js +12 -3
- package/dist/packages/core/src/application/download/BlockStore.js +97 -0
- package/dist/packages/core/src/application/download/DownloadedAudioSamples.js +50 -0
- package/dist/packages/core/src/application/download/DownloadedVideoTrack.js +122 -0
- package/dist/packages/core/src/application/download/FileDownload.js +183 -0
- package/dist/packages/core/src/application/download/RecordingBuffer.js +22 -0
- package/dist/packages/core/src/application/download/SampleCursor.js +88 -0
- package/dist/packages/core/src/application/download/SourceByteStream.js +46 -0
- package/dist/packages/core/src/application/download/Transfers.js +75 -0
- package/dist/packages/core/src/application/download/startFileDownload.js +37 -0
- package/dist/packages/core/src/application/playback/Buffering.js +53 -0
- package/dist/packages/core/src/application/playback/DecodePipeline.js +11 -7
- package/dist/packages/core/src/application/playback/DecodeRun.js +15 -0
- package/dist/packages/core/src/application/playback/PlaybackSession.js +18 -27
- package/dist/packages/core/src/application/playback/keyframeTimeAt.js +1 -1
- package/dist/packages/core/src/application/playback/probeDecoding.js +17 -3
- package/dist/packages/core/src/application/recording/readSampleTable.js +27 -0
- package/dist/packages/core/src/application/recording/timeRecording.js +3 -3
- package/dist/packages/core/src/domain/container/KeyframeRule.js +7 -0
- package/dist/packages/core/src/domain/container/SampleTable.js +29 -0
- package/dist/packages/core/src/domain/container/TrackSampleTable.js +96 -0
- package/dist/packages/core/src/domain/container/presentationOrder.js +10 -0
- package/dist/packages/core/src/domain/download/DownloadPolicy.js +41 -0
- package/dist/packages/core/src/domain/download/isReadyToResume.js +27 -0
- package/dist/packages/core/src/domain/download/planDownloads.js +175 -0
- package/dist/packages/core/src/domain/download/trackNeeds.js +32 -0
- package/dist/packages/core/src/domain/format/boxes/boxHeader.js +44 -0
- package/dist/packages/core/src/domain/format/boxes/scanBoxes.js +7 -34
- package/dist/packages/core/src/domain/format/mp4/keyframeRules.js +81 -0
- package/dist/packages/core/src/domain/format/mp4/movieBoxes.js +52 -0
- package/dist/packages/core/src/domain/format/mp4/mp4BoxTypes.js +46 -0
- package/dist/packages/core/src/domain/format/mp4/mp4Layouts.js +183 -0
- package/dist/packages/core/src/domain/format/mp4/parseMovie.js +67 -0
- package/dist/packages/core/src/domain/format/mp4/readTable.js +31 -0
- package/dist/packages/core/src/domain/format/mp4/sampleLocations.js +69 -0
- package/dist/packages/core/src/domain/format/mp4/sampleTiming.js +109 -0
- package/dist/packages/core/src/domain/format/mp4/trackHeaders.js +75 -0
- package/dist/packages/core/src/index.js +10 -2
- package/dist/packages/core/src/shared/async/iteration.js +24 -0
- package/dist/packages/core/src/shared/binary/ByteRange.js +12 -0
- package/dist/packages/core/src/shared/binary/ByteRangeSet.js +85 -0
- package/dist/packages/core/src/shared/binary/ByteReader.js +21 -3
- package/dist/packages/core/src/shared/binary/concatenated.js +15 -0
- package/dist/packages/core/src/shared/errors/GyroViewError.js +56 -3
- package/dist/packages/core/src/shared/math/countAtOrBelow.js +17 -0
- package/dist/packages/player/src/composition/browserPorts.js +23 -4
- package/dist/packages/player/src/composition/buildPipeline.js +15 -3
- package/dist/packages/player/src/composition/openInputs.js +51 -107
- package/dist/packages/player/src/composition/readInputs.js +111 -0
- package/dist/packages/player/src/controls/messages.js +3 -1
- package/dist/packages/player/src/inspectRecording.js +1 -1
- package/dist/standalone.js +58 -58
- package/package.json +1 -1
- package/dist/packages/adapters/mediabunny/src/MediabunnyAudioSegments.js +0 -133
- package/dist/packages/adapters/mediabunny/src/MediabunnyDemuxer.js +0 -63
- 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")
|
|
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
|
|
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 };
|