@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.
- package/CHANGELOG.md +26 -0
- package/README.md +16 -5
- package/dist/index.d.ts +1 -0
- 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/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 +15 -1
- 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 +9 -1
- 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 +1 -0
- 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 +1 -0
- 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
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
303
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
402
|
+
this.buffering.settleStart();
|
|
412
403
|
this.lifecycle.announce("error", asGyroViewError(error, "decode", "playback failed"));
|
|
413
404
|
});
|
|
414
405
|
}
|
|
@@ -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.
|
|
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
|
|
37
|
-
* (a track without one does not decode), and the rest follow at that
|
|
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.
|
|
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,29 @@
|
|
|
1
|
+
import { seconds } from "../../shared/units/time.js";
|
|
2
|
+
//#region ../../packages/core/src/domain/container/SampleTable.ts
|
|
3
|
+
/**
|
|
4
|
+
* Every sample of one file, track by track, as its movie box describes them: the player's own
|
|
5
|
+
* reading of the container, from which it downloads and decodes.
|
|
6
|
+
*/
|
|
7
|
+
var SampleTable = class {
|
|
8
|
+
tracks;
|
|
9
|
+
constructor(tracks) {
|
|
10
|
+
this.tracks = tracks;
|
|
11
|
+
}
|
|
12
|
+
get videoTracks() {
|
|
13
|
+
return this.tracks.filter((track) => track.kind === "video");
|
|
14
|
+
}
|
|
15
|
+
get audioTracks() {
|
|
16
|
+
return this.tracks.filter((track) => track.kind === "audio");
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Until the last sample of any track stops showing.
|
|
20
|
+
*/
|
|
21
|
+
get duration() {
|
|
22
|
+
return seconds(Math.max(0, ...this.tracks.map((track) => track.end)));
|
|
23
|
+
}
|
|
24
|
+
trackWithId(trackId) {
|
|
25
|
+
return this.tracks.find((track) => track.trackId === trackId);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { SampleTable };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ensureIndexInRange, ensureInvariant } from "../../shared/errors/GyroViewError.js";
|
|
2
|
+
import { seconds } from "../../shared/units/time.js";
|
|
3
|
+
import { ByteRange } from "../../shared/binary/ByteRange.js";
|
|
4
|
+
import { presentationOrderOf } from "./presentationOrder.js";
|
|
5
|
+
import { countAtOrBelow } from "../../shared/math/countAtOrBelow.js";
|
|
6
|
+
//#region ../../packages/core/src/domain/container/TrackSampleTable.ts
|
|
7
|
+
/**
|
|
8
|
+
* One track's samples as its sample table describes them: where each lies, when it shows and
|
|
9
|
+
* where decoding may start. Samples are numbered from zero in decode order; the presentation
|
|
10
|
+
* order, which composition offsets may make another, is kept beside it.
|
|
11
|
+
*/
|
|
12
|
+
var TrackSampleTable = class {
|
|
13
|
+
parts;
|
|
14
|
+
trackId;
|
|
15
|
+
kind;
|
|
16
|
+
end;
|
|
17
|
+
keyframeRule;
|
|
18
|
+
/**
|
|
19
|
+
* The samples in the order they show; ties keep decode order.
|
|
20
|
+
*/
|
|
21
|
+
presentationOrder;
|
|
22
|
+
constructor(parts) {
|
|
23
|
+
this.parts = parts;
|
|
24
|
+
const { length } = parts.offsets;
|
|
25
|
+
const columns = [
|
|
26
|
+
parts.sizes,
|
|
27
|
+
parts.timestamps,
|
|
28
|
+
parts.durations
|
|
29
|
+
];
|
|
30
|
+
ensureInvariant(columns.every((column) => column.length === length), `the sample table of track ${parts.trackId} disagrees on its sample count`);
|
|
31
|
+
ensureInvariant(isAscendingWithin(parts.syncSamples ?? [], length), `the sync samples of track ${parts.trackId} are not samples of it in order`);
|
|
32
|
+
this.trackId = parts.trackId;
|
|
33
|
+
this.kind = parts.kind;
|
|
34
|
+
this.end = parts.end;
|
|
35
|
+
this.keyframeRule = parts.keyframeRule;
|
|
36
|
+
this.presentationOrder = presentationOrderOf(parts.timestamps);
|
|
37
|
+
}
|
|
38
|
+
get sampleCount() {
|
|
39
|
+
return this.parts.offsets.length;
|
|
40
|
+
}
|
|
41
|
+
rangeOf(sample) {
|
|
42
|
+
this.ensureSample(sample);
|
|
43
|
+
return ByteRange.of(this.parts.offsets[sample] ?? 0, this.parts.sizes[sample] ?? 0);
|
|
44
|
+
}
|
|
45
|
+
timestampOf(sample) {
|
|
46
|
+
this.ensureSample(sample);
|
|
47
|
+
return seconds(this.parts.timestamps[sample] ?? 0);
|
|
48
|
+
}
|
|
49
|
+
durationOf(sample) {
|
|
50
|
+
this.ensureSample(sample);
|
|
51
|
+
return seconds(this.parts.durations[sample] ?? 0);
|
|
52
|
+
}
|
|
53
|
+
isSync(sample) {
|
|
54
|
+
this.ensureSample(sample);
|
|
55
|
+
return this.syncSampleAtOrBefore(sample) === sample;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The sample on screen at `time`: the last to show at or before it. Undefined before the first.
|
|
59
|
+
*/
|
|
60
|
+
sampleShownAt(time) {
|
|
61
|
+
const shownSoFar = countAtOrBelow(this.presentationOrder.length, (index) => this.parts.timestamps[this.presentationOrder[index] ?? 0] ?? Infinity, time);
|
|
62
|
+
return this.presentationOrder[shownSoFar - 1];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The sample decoding for `time` starts at: the sync sample at or before the one on screen.
|
|
66
|
+
*/
|
|
67
|
+
keyframeAt(time) {
|
|
68
|
+
const shown = this.sampleShownAt(time);
|
|
69
|
+
return shown === void 0 ? void 0 : this.syncSampleAtOrBefore(shown);
|
|
70
|
+
}
|
|
71
|
+
syncSampleAtOrBefore(sample) {
|
|
72
|
+
const { syncSamples } = this.parts;
|
|
73
|
+
if (!syncSamples) return sample >= 0 ? sample : void 0;
|
|
74
|
+
return syncSamples[countAtOrBelow(syncSamples.length, (index) => syncSamples[index] ?? 0, sample) - 1];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The first sync sample at or after `sample`; none where no sync sample follows.
|
|
78
|
+
*/
|
|
79
|
+
syncSampleAtOrAfter(sample) {
|
|
80
|
+
const { syncSamples } = this.parts;
|
|
81
|
+
const first = Math.max(0, sample);
|
|
82
|
+
if (!syncSamples) return first < this.sampleCount ? first : void 0;
|
|
83
|
+
return syncSamples[countAtOrBelow(syncSamples.length, (index) => syncSamples[index] ?? 0, first - 1)];
|
|
84
|
+
}
|
|
85
|
+
timestampsInPresentationOrder() {
|
|
86
|
+
return this.presentationOrder.map((sample) => seconds(this.parts.timestamps[sample] ?? 0));
|
|
87
|
+
}
|
|
88
|
+
ensureSample(sample) {
|
|
89
|
+
ensureIndexInRange(sample, this.sampleCount, `sample of track ${this.trackId}`);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
function isAscendingWithin(samples, count) {
|
|
93
|
+
return samples.every((sample, index) => Number.isSafeInteger(sample) && sample >= 0 && sample < count && sample > (samples[index - 1] ?? -1));
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { TrackSampleTable };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region ../../packages/core/src/domain/container/presentationOrder.ts
|
|
2
|
+
/**
|
|
3
|
+
* The samples in the order they show, given when each shows in decode order; samples that show
|
|
4
|
+
* at the same time keep their decode order, as a stable sort keeps them.
|
|
5
|
+
*/
|
|
6
|
+
function presentationOrderOf(times) {
|
|
7
|
+
return Array.from({ length: times.length }, (_, sample) => sample).toSorted((left, right) => (times[left] ?? 0) - (times[right] ?? 0));
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { presentationOrderOf };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { seconds } from "../../shared/units/time.js";
|
|
2
|
+
//#region ../../packages/core/src/domain/download/DownloadPolicy.ts
|
|
3
|
+
var MEBIBYTE = 1048576;
|
|
4
|
+
/**
|
|
5
|
+
* What the policy reads ahead and keeps behind, as time and at most as bytes: the byte limits
|
|
6
|
+
* bind first on the X5's 210 Mbit/s, the times on slower recordings. A split recording shares
|
|
7
|
+
* the byte limits among its files.
|
|
8
|
+
*/
|
|
9
|
+
var AHEAD_SECONDS = 10;
|
|
10
|
+
var MOST_AHEAD_MEBIBYTES = 128;
|
|
11
|
+
var KEEP_BEHIND_SECONDS = 2;
|
|
12
|
+
var MOST_KEPT_BEHIND_MEBIBYTES = 32;
|
|
13
|
+
var REQUEST_MEBIBYTES = 8;
|
|
14
|
+
var REQUESTS_IN_FLIGHT = 2;
|
|
15
|
+
var BRIDGED_GAP_MEBIBYTES = 1;
|
|
16
|
+
var REFILL_FRACTION = .25;
|
|
17
|
+
/**
|
|
18
|
+
* Long enough that a link slower than the recording plays in stretches rather than frame by
|
|
19
|
+
* frame, short enough that the wait does not feel like a stop.
|
|
20
|
+
*/
|
|
21
|
+
var RESUME_SECONDS = 4;
|
|
22
|
+
/**
|
|
23
|
+
* The policy for one file of a recording of `fileCount` files, by its bit rate.
|
|
24
|
+
*/
|
|
25
|
+
function downloadPolicyFor(file, fileCount) {
|
|
26
|
+
const bytesPerSecond = file.size / file.duration;
|
|
27
|
+
const aheadBytes = Math.min(Math.round(AHEAD_SECONDS * bytesPerSecond), MOST_AHEAD_MEBIBYTES * MEBIBYTE / fileCount);
|
|
28
|
+
return {
|
|
29
|
+
aheadSeconds: seconds(AHEAD_SECONDS),
|
|
30
|
+
aheadBytes,
|
|
31
|
+
keepBehindBytes: Math.min(Math.round(KEEP_BEHIND_SECONDS * bytesPerSecond), MOST_KEPT_BEHIND_MEBIBYTES * MEBIBYTE / fileCount),
|
|
32
|
+
keepBehindSeconds: seconds(KEEP_BEHIND_SECONDS),
|
|
33
|
+
requestSize: REQUEST_MEBIBYTES * MEBIBYTE,
|
|
34
|
+
requestsInFlight: REQUESTS_IN_FLIGHT,
|
|
35
|
+
bridgedGap: BRIDGED_GAP_MEBIBYTES * MEBIBYTE,
|
|
36
|
+
refillBytes: Math.round(aheadBytes * REFILL_FRACTION),
|
|
37
|
+
resumeSeconds: seconds(RESUME_SECONDS)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { downloadPolicyFor };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { seconds } from "../../shared/units/time.js";
|
|
2
|
+
import { ByteRangeSet } from "../../shared/binary/ByteRangeSet.js";
|
|
3
|
+
import { needsOf, withinBytes } from "./trackNeeds.js";
|
|
4
|
+
//#region ../../packages/core/src/domain/download/isReadyToResume.ts
|
|
5
|
+
/**
|
|
6
|
+
* What playback that starved at `time` waits for before it plays again (ADR 0011, ADR 0029):
|
|
7
|
+
* every picture reader's frames up to the policy's resume seconds past it, or as many of them as
|
|
8
|
+
* the budget holds less a request (a refill's worth, where that is less), so the download that
|
|
9
|
+
* keeps it held still tops up in large ranges. Sound needs nothing of its own.
|
|
10
|
+
*/
|
|
11
|
+
function resumeThresholdOf(need) {
|
|
12
|
+
const { policy } = need;
|
|
13
|
+
const until = seconds(need.time + policy.resumeSeconds);
|
|
14
|
+
const pictures = need.cursors.filter((cursor) => cursor.track.kind === "video");
|
|
15
|
+
const needs = ByteRangeSet.of(pictures.flatMap((cursor) => needsOf(cursor, until)));
|
|
16
|
+
const margin = Math.min(policy.requestSize, policy.refillBytes);
|
|
17
|
+
return withinBytes(needs, policy.aheadBytes - margin);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Whether the threshold is held: the download keeps it held once playing, so a download that
|
|
21
|
+
* reads ahead is ready as soon as its bytes have come.
|
|
22
|
+
*/
|
|
23
|
+
function isReadyToResume(state) {
|
|
24
|
+
return resumeThresholdOf(state).subtract(state.held).isEmpty;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { isReadyToResume, resumeThresholdOf };
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { seconds } from "../../shared/units/time.js";
|
|
2
|
+
import { ByteRange } from "../../shared/binary/ByteRange.js";
|
|
3
|
+
import { ByteRangeSet } from "../../shared/binary/ByteRangeSet.js";
|
|
4
|
+
import { needsOf, withinBytes } from "./trackNeeds.js";
|
|
5
|
+
import { resumeThresholdOf } from "./isReadyToResume.js";
|
|
6
|
+
//#region ../../packages/core/src/domain/download/planDownloads.ts
|
|
7
|
+
/**
|
|
8
|
+
* The download's plan, from where its readers stand (ADR 0029). The window starts at the
|
|
9
|
+
* slowest picture reader, where the picture last stood once its readers closed (so the sound
|
|
10
|
+
* after the last frame still comes), or at the sound where no picture was ever read; once
|
|
11
|
+
* playing it reaches as far ahead as the policy's seconds and bytes allow, before that only to
|
|
12
|
+
* what the readers wait for. Every picture reader's next frame is read, however far from the
|
|
13
|
+
* others, so none waits for ever; a reader a seek left behind wants nothing until it follows.
|
|
14
|
+
* What is wanted is asked for lowest first and in large ranges: at once when playback needs it
|
|
15
|
+
* now, otherwise once a refill's worth is missing.
|
|
16
|
+
*/
|
|
17
|
+
function planDownloads(state) {
|
|
18
|
+
const pictures = state.cursors.filter((cursor) => isPicture(cursor));
|
|
19
|
+
const anchor = pictures.length > 0 ? anchorOf(pictures) : state.anchor;
|
|
20
|
+
const windowStart = anchor ?? soundLeadOf(state);
|
|
21
|
+
if (!windowStart) return nothingToRead(state);
|
|
22
|
+
const ahead = state.isReadingAhead ? state.policy.aheadSeconds : 0;
|
|
23
|
+
const windowEnd = seconds(windowStart.time + ahead);
|
|
24
|
+
const wanted = wantedBytes({
|
|
25
|
+
state,
|
|
26
|
+
windowStart,
|
|
27
|
+
windowEnd
|
|
28
|
+
}).bridgingGapsBelow(state.policy.bridgedGap);
|
|
29
|
+
const kept = state.transfers.filter((transfer) => transfer.remaining.length === 0 || wanted.overlaps(transfer.remaining));
|
|
30
|
+
const coming = ByteRangeSet.of(kept.map((transfer) => transfer.remaining));
|
|
31
|
+
const missing = wanted.subtract(state.held).subtract(coming).subtract(state.unreadable);
|
|
32
|
+
const room = state.policy.requestsInFlight - kept.length;
|
|
33
|
+
return {
|
|
34
|
+
start: isTopUpDue({
|
|
35
|
+
state,
|
|
36
|
+
windowStart,
|
|
37
|
+
windowEnd,
|
|
38
|
+
missing,
|
|
39
|
+
threshold: thresholdOf(state, windowStart)
|
|
40
|
+
}) ? requestsFor(missing, state.policy, room) : [],
|
|
41
|
+
cancel: state.transfers.filter((transfer) => !kept.includes(transfer)).map((transfer) => transfer.id),
|
|
42
|
+
release: state.held.subtract(wanted.union(keptBehind(windowStart, state.policy))),
|
|
43
|
+
anchor
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Where the window starts once playing where no picture was ever read: at the sound, which then
|
|
48
|
+
* has no picture to follow, and follows it as it moves on.
|
|
49
|
+
*/
|
|
50
|
+
function soundLeadOf(state) {
|
|
51
|
+
return state.isReadingAhead && state.cursors.length > 0 ? anchorOf(state.cursors) : void 0;
|
|
52
|
+
}
|
|
53
|
+
function nothingToRead(state) {
|
|
54
|
+
return {
|
|
55
|
+
start: [],
|
|
56
|
+
cancel: state.transfers.map((transfer) => transfer.id),
|
|
57
|
+
release: ByteRangeSet.empty,
|
|
58
|
+
anchor: void 0
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function anchorOf(cursors) {
|
|
62
|
+
return {
|
|
63
|
+
time: seconds(Math.min(...cursors.map((cursor) => timeOf(cursor)))),
|
|
64
|
+
offset: Math.min(...cursors.map((cursor) => offsetOf(cursor))),
|
|
65
|
+
keyframeOffset: Math.min(...cursors.map((cursor) => keyframeOffsetOf(cursor)))
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Every sample the readers following the picture will hand out within the window, clipped to
|
|
70
|
+
* the bytes the policy allows ahead, nearest first; and the next sample of every picture reader
|
|
71
|
+
* and of every reader waiting within the window, whatever the budget and wherever the file
|
|
72
|
+
* puts it, so no reader waits for ever.
|
|
73
|
+
*/
|
|
74
|
+
function wantedBytes(window) {
|
|
75
|
+
const { state, windowEnd } = window;
|
|
76
|
+
const following = state.cursors.filter((cursor) => isFollowing(cursor, window));
|
|
77
|
+
const needs = following.flatMap((cursor) => needsOf(cursor, laterOf(windowEnd, cursor.target)));
|
|
78
|
+
const next = following.filter((cursor) => isNextSampleOwed(cursor, windowEnd));
|
|
79
|
+
const nextSamples = ByteRangeSet.of(next.map((cursor) => cursor.track.rangeOf(cursor.sample)));
|
|
80
|
+
return withinBytes(ByteRangeSet.of(needs), state.policy.aheadBytes).union(nextSamples);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A reader no further behind the picture than the policy allows: the sound lags the picture
|
|
84
|
+
* by the decoders' lead, and until it follows a seek it stands far behind.
|
|
85
|
+
*/
|
|
86
|
+
function isFollowing(cursor, window) {
|
|
87
|
+
return timeOf(cursor) >= window.windowStart.time - window.state.policy.keepBehindSeconds;
|
|
88
|
+
}
|
|
89
|
+
function isNextSampleOwed(cursor, windowEnd) {
|
|
90
|
+
const isWaitingWithin = cursor.isWaiting && timeOf(cursor) <= windowEnd;
|
|
91
|
+
return !isAtItsEnd(cursor) && (isPicture(cursor) || isWaitingWithin);
|
|
92
|
+
}
|
|
93
|
+
function laterOf(time, other) {
|
|
94
|
+
return other !== void 0 && other > time ? other : time;
|
|
95
|
+
}
|
|
96
|
+
function isPicture(cursor) {
|
|
97
|
+
return cursor.track.kind === "video";
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Once playing, what playback that starved would wait for, which the download keeps held; none
|
|
101
|
+
* before.
|
|
102
|
+
*/
|
|
103
|
+
function thresholdOf(state, windowStart) {
|
|
104
|
+
const need = {
|
|
105
|
+
cursors: state.cursors,
|
|
106
|
+
time: windowStart.time,
|
|
107
|
+
policy: state.policy
|
|
108
|
+
};
|
|
109
|
+
return state.isReadingAhead ? resumeThresholdOf(need) : ByteRangeSet.empty;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Whether to ask for what is missing now: a reader needs it at once, the resume threshold is
|
|
113
|
+
* not all held, or a refill's worth is missing. The last seconds of the tracks come as the
|
|
114
|
+
* threshold reaches them, however few their bytes.
|
|
115
|
+
*/
|
|
116
|
+
function isTopUpDue(topUp) {
|
|
117
|
+
const { state, missing, threshold } = topUp;
|
|
118
|
+
if (missing.ranges.some((range) => threshold.overlaps(range))) return true;
|
|
119
|
+
return state.cursors.some((cursor) => isNeededAtOnce(cursor, topUp) && missing.overlaps(cursor.track.rangeOf(cursor.sample))) || missing.totalLength >= state.policy.refillBytes;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A reader waiting on a sample playback needs now: a picture's, or one due within the resume
|
|
123
|
+
* seconds. Sound that buffers far ahead waits where the downloaded bytes end, and would
|
|
124
|
+
* otherwise ask for a small range every time the picture moved.
|
|
125
|
+
*/
|
|
126
|
+
function isNeededAtOnce(cursor, topUp) {
|
|
127
|
+
if (!cursor.isWaiting || isAtItsEnd(cursor)) return false;
|
|
128
|
+
const { windowStart, state } = topUp;
|
|
129
|
+
const horizon = windowStart.time + (state.isReadingAhead ? state.policy.resumeSeconds : 0);
|
|
130
|
+
return isPicture(cursor) || timeOf(cursor) <= horizon;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The missing ranges in order, cut to the request size, as many as there is room for.
|
|
134
|
+
*/
|
|
135
|
+
function requestsFor(missing, policy, room) {
|
|
136
|
+
const requests = [];
|
|
137
|
+
for (const range of missing.ranges) for (let offset = range.offset; offset < range.end && requests.length < room; offset += policy.requestSize) requests.push(ByteRange.of(offset, Math.min(policy.requestSize, range.end - offset)));
|
|
138
|
+
return requests;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The stretch behind the picture that is kept for short seeks back: its group of pictures from
|
|
142
|
+
* the key frame, or the policy's distance, whichever reaches further back.
|
|
143
|
+
*/
|
|
144
|
+
function keptBehind(anchor, policy) {
|
|
145
|
+
const start = keptBehindStart(anchor, policy);
|
|
146
|
+
return ByteRangeSet.of([ByteRange.of(start, anchor.offset - start)]);
|
|
147
|
+
}
|
|
148
|
+
function keptBehindStart(anchor, policy) {
|
|
149
|
+
return Math.max(0, Math.min(anchor.keyframeOffset, anchor.offset - policy.keepBehindBytes));
|
|
150
|
+
}
|
|
151
|
+
function isAtItsEnd(cursor) {
|
|
152
|
+
return cursor.sample >= cursor.track.sampleCount;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* When a picture reader stands: at its next frame, or at its track's end once it read them all.
|
|
156
|
+
*/
|
|
157
|
+
function timeOf(cursor) {
|
|
158
|
+
return isAtItsEnd(cursor) ? cursor.track.end : cursor.track.timestampOf(cursor.sample);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Where the key frame before a reader's next sample lies; a sound sample is its own.
|
|
162
|
+
*/
|
|
163
|
+
function keyframeOffsetOf(cursor) {
|
|
164
|
+
const { track } = cursor;
|
|
165
|
+
const last = Math.min(cursor.sample, track.sampleCount - 1);
|
|
166
|
+
const keyframe = track.syncSampleAtOrBefore(last);
|
|
167
|
+
return keyframe === void 0 ? offsetOf(cursor) : track.rangeOf(keyframe).offset;
|
|
168
|
+
}
|
|
169
|
+
function offsetOf(cursor) {
|
|
170
|
+
const { track } = cursor;
|
|
171
|
+
if (!isAtItsEnd(cursor)) return track.rangeOf(cursor.sample).offset;
|
|
172
|
+
return track.sampleCount > 0 ? track.rangeOf(track.sampleCount - 1).end : 0;
|
|
173
|
+
}
|
|
174
|
+
//#endregion
|
|
175
|
+
export { planDownloads };
|