@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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ByteRange } from "./ByteRange.js";
|
|
2
|
+
//#region ../../packages/core/src/shared/binary/ByteRangeSet.ts
|
|
3
|
+
/**
|
|
4
|
+
* A set of bytes held as disjoint ranges in order, with overlapping and touching ones joined:
|
|
5
|
+
* the bytes a download holds, has asked for, or still needs. Immutable; every operation answers
|
|
6
|
+
* with a new set.
|
|
7
|
+
*/
|
|
8
|
+
var ByteRangeSet = class ByteRangeSet {
|
|
9
|
+
ranges;
|
|
10
|
+
static empty = new ByteRangeSet([]);
|
|
11
|
+
constructor(ranges) {
|
|
12
|
+
this.ranges = ranges;
|
|
13
|
+
}
|
|
14
|
+
static of(ranges) {
|
|
15
|
+
const ordered = ranges.filter((range) => range.length > 0).toSorted((left, right) => left.offset - right.offset);
|
|
16
|
+
return new ByteRangeSet(joined(ordered, 0));
|
|
17
|
+
}
|
|
18
|
+
get isEmpty() {
|
|
19
|
+
return this.ranges.length === 0;
|
|
20
|
+
}
|
|
21
|
+
get totalLength() {
|
|
22
|
+
return this.ranges.reduce((total, range) => total + range.length, 0);
|
|
23
|
+
}
|
|
24
|
+
union(other) {
|
|
25
|
+
return ByteRangeSet.of([...this.ranges, ...other.ranges]);
|
|
26
|
+
}
|
|
27
|
+
subtract(other) {
|
|
28
|
+
return new ByteRangeSet(this.ranges.flatMap((range) => withoutAny(range, other.ranges)));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Whether every byte of `range` is in the set; an empty range always is.
|
|
32
|
+
*/
|
|
33
|
+
covers(range) {
|
|
34
|
+
return range.length === 0 || this.ranges.some((held) => held.contains(range));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Whether any byte of `range` is in the set; an empty range never is.
|
|
38
|
+
*/
|
|
39
|
+
overlaps(range) {
|
|
40
|
+
return this.ranges.some((held) => held.overlaps(range));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The set with every gap between two of its ranges shorter than `length` filled: fetching a
|
|
44
|
+
* few unneeded bytes costs less than asking for the ranges on either side apart.
|
|
45
|
+
*/
|
|
46
|
+
bridgingGapsBelow(length) {
|
|
47
|
+
return new ByteRangeSet(joined(this.ranges, length));
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Ranges in order, each joined with the next when the gap between them is shorter than
|
|
52
|
+
* `bridgedGap` (or when they overlap or touch, for a gap of zero).
|
|
53
|
+
*/
|
|
54
|
+
function joined(ordered, bridgedGap) {
|
|
55
|
+
const result = [];
|
|
56
|
+
for (const range of ordered) {
|
|
57
|
+
const last = result.at(-1);
|
|
58
|
+
const isJoined = last !== void 0 && range.offset - last.end < Math.max(bridgedGap, 1);
|
|
59
|
+
if (last && isJoined) result[result.length - 1] = spanning(last.offset, range.end, last.end);
|
|
60
|
+
else result.push(range);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* `range` with every byte of `holes` taken out, in pieces in order.
|
|
66
|
+
*/
|
|
67
|
+
function withoutAny(range, holes) {
|
|
68
|
+
const pieces = [];
|
|
69
|
+
let start = range.offset;
|
|
70
|
+
for (const hole of holes) {
|
|
71
|
+
if (hole.end <= start || hole.offset >= range.end) continue;
|
|
72
|
+
if (hole.offset > start) pieces.push(ByteRange.of(start, hole.offset - start));
|
|
73
|
+
start = Math.max(start, hole.end);
|
|
74
|
+
}
|
|
75
|
+
if (start < range.end) pieces.push(ByteRange.of(start, range.end - start));
|
|
76
|
+
return pieces;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The range from `offset` to the further of two ends.
|
|
80
|
+
*/
|
|
81
|
+
function spanning(offset, end, otherEnd) {
|
|
82
|
+
return ByteRange.of(offset, Math.max(end, otherEnd) - offset);
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
export { ByteRangeSet };
|
|
@@ -4,12 +4,15 @@ var UINT8_SIZE = 1;
|
|
|
4
4
|
var UINT16_SIZE = 2;
|
|
5
5
|
var UINT32_SIZE = 4;
|
|
6
6
|
var UINT64_SIZE = 8;
|
|
7
|
+
var INT32_SIZE = 4;
|
|
8
|
+
var INT64_SIZE = 8;
|
|
7
9
|
var FLOAT64_SIZE = 8;
|
|
8
10
|
var IS_LITTLE_ENDIAN = true;
|
|
9
11
|
var IS_BIG_ENDIAN = false;
|
|
10
12
|
/**
|
|
11
|
-
* Bounds-checked
|
|
12
|
-
*
|
|
13
|
+
* Bounds-checked reads over a byte array: little-endian for Insta360's records, big-endian for
|
|
14
|
+
* ISO BMFF boxes. Every read is positional, so callers express the layouts they parse as named
|
|
15
|
+
* offsets rather than as a moving cursor.
|
|
13
16
|
*/
|
|
14
17
|
var ByteReader = class {
|
|
15
18
|
bytes;
|
|
@@ -38,6 +41,13 @@ var ByteReader = class {
|
|
|
38
41
|
return this.view.getUint32(offset, IS_BIG_ENDIAN);
|
|
39
42
|
}
|
|
40
43
|
/**
|
|
44
|
+
* A signed big-endian 32-bit integer, as edit lists and composition offsets store them.
|
|
45
|
+
*/
|
|
46
|
+
int32BeAt(offset) {
|
|
47
|
+
this.ensureAvailable(offset, INT32_SIZE);
|
|
48
|
+
return this.view.getInt32(offset, IS_BIG_ENDIAN);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
41
51
|
* Reads an unsigned 64-bit integer as a JavaScript number; rejects values beyond 2^53 - 1.
|
|
42
52
|
*/
|
|
43
53
|
uint64LeAt(offset) {
|
|
@@ -51,6 +61,14 @@ var ByteReader = class {
|
|
|
51
61
|
this.ensureAvailable(offset, UINT64_SIZE);
|
|
52
62
|
return this.toSafeNumber(this.view.getBigUint64(offset, IS_BIG_ENDIAN), offset);
|
|
53
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* A signed big-endian 64-bit integer (a version 1 edit list's media time) as a safe
|
|
66
|
+
* JavaScript number; rejects values beyond ±(2^53 - 1).
|
|
67
|
+
*/
|
|
68
|
+
int64BeAt(offset) {
|
|
69
|
+
this.ensureAvailable(offset, INT64_SIZE);
|
|
70
|
+
return this.toSafeNumber(this.view.getBigInt64(offset, IS_BIG_ENDIAN), offset);
|
|
71
|
+
}
|
|
54
72
|
float64LeAt(offset) {
|
|
55
73
|
this.ensureAvailable(offset, FLOAT64_SIZE);
|
|
56
74
|
return this.view.getFloat64(offset, IS_LITTLE_ENDIAN);
|
|
@@ -66,7 +84,7 @@ var ByteReader = class {
|
|
|
66
84
|
return Array.from(this.bytesAt(offset, length), (byte) => String.fromCodePoint(byte)).join("");
|
|
67
85
|
}
|
|
68
86
|
toSafeNumber(value, offset) {
|
|
69
|
-
if (value
|
|
87
|
+
if (!(value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER))) throw new GyroViewError("binary-unsafe-integer", `64-bit integer at offset ${offset} (${value.toString()}) exceeds the safe integer range`);
|
|
70
88
|
return Number(value);
|
|
71
89
|
}
|
|
72
90
|
ensureAvailable(offset, length) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region ../../packages/core/src/shared/binary/concatenated.ts
|
|
2
|
+
/**
|
|
3
|
+
* The parts one after the other in a single array.
|
|
4
|
+
*/
|
|
5
|
+
function concatenated(parts) {
|
|
6
|
+
const whole = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
|
|
7
|
+
let offset = 0;
|
|
8
|
+
for (const part of parts) {
|
|
9
|
+
whole.set(part, offset);
|
|
10
|
+
offset += part.byteLength;
|
|
11
|
+
}
|
|
12
|
+
return whole;
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { concatenated };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region ../../packages/core/src/shared/errors/GyroViewError.ts
|
|
2
2
|
/**
|
|
3
|
-
* Stable machine-readable failure
|
|
3
|
+
* Stable machine-readable failure codes. Embedders switch on these; messages are for humans.
|
|
4
4
|
*/
|
|
5
5
|
var GYRO_VIEW_ERROR_CODES = [
|
|
6
6
|
"binary-out-of-bounds",
|
|
@@ -23,23 +23,76 @@ var GYRO_VIEW_ERROR_CODES = [
|
|
|
23
23
|
"playback-blocked",
|
|
24
24
|
"range-unsupported",
|
|
25
25
|
"render-unavailable",
|
|
26
|
+
"source-changed",
|
|
26
27
|
"source-truncated",
|
|
27
28
|
"source-unreadable",
|
|
28
29
|
"unsupported-calibration",
|
|
29
30
|
"unsupported-container",
|
|
30
31
|
"unsupported-gyro-record",
|
|
31
32
|
"unsupported-info-format",
|
|
32
|
-
"unsupported-layout"
|
|
33
|
+
"unsupported-layout",
|
|
34
|
+
"webcodecs-unavailable"
|
|
33
35
|
];
|
|
36
|
+
/**
|
|
37
|
+
* Whose side a failure is on, for embedders that handle failures by kind: the browser cannot
|
|
38
|
+
* decode or draw the recording, the file is not one the player can play, its bytes could not be
|
|
39
|
+
* read, the page misused the API, or the player failed in a way it did not expect.
|
|
40
|
+
*/
|
|
41
|
+
var GYRO_VIEW_ERROR_CATEGORIES = [
|
|
42
|
+
"browser",
|
|
43
|
+
"recording",
|
|
44
|
+
"source",
|
|
45
|
+
"usage",
|
|
46
|
+
"internal"
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* A record, so a new code cannot be added without its category.
|
|
50
|
+
*/
|
|
51
|
+
var CATEGORY_OF_CODE = {
|
|
52
|
+
"binary-out-of-bounds": "recording",
|
|
53
|
+
"binary-unsafe-integer": "recording",
|
|
54
|
+
"codec-unsupported": "browser",
|
|
55
|
+
cors: "source",
|
|
56
|
+
decode: "browser",
|
|
57
|
+
"embed-destroyed": "usage",
|
|
58
|
+
"index-out-of-range": "internal",
|
|
59
|
+
"invalid-argument": "usage",
|
|
60
|
+
"invalid-byte-range": "recording",
|
|
61
|
+
"invalid-calibration": "recording",
|
|
62
|
+
"invalid-protobuf": "recording",
|
|
63
|
+
"invalid-trailer": "recording",
|
|
64
|
+
"invariant-violation": "internal",
|
|
65
|
+
"missing-second-file": "recording",
|
|
66
|
+
"no-calibration": "recording",
|
|
67
|
+
"no-info-record": "recording",
|
|
68
|
+
"no-key-frame": "recording",
|
|
69
|
+
"playback-blocked": "browser",
|
|
70
|
+
"range-unsupported": "source",
|
|
71
|
+
"render-unavailable": "browser",
|
|
72
|
+
"source-changed": "source",
|
|
73
|
+
"source-truncated": "source",
|
|
74
|
+
"source-unreadable": "source",
|
|
75
|
+
"unsupported-calibration": "recording",
|
|
76
|
+
"unsupported-container": "recording",
|
|
77
|
+
"unsupported-gyro-record": "recording",
|
|
78
|
+
"unsupported-info-format": "recording",
|
|
79
|
+
"unsupported-layout": "recording",
|
|
80
|
+
"webcodecs-unavailable": "browser"
|
|
81
|
+
};
|
|
34
82
|
function isGyroViewErrorCode(value) {
|
|
35
83
|
return typeof value === "string" && GYRO_VIEW_ERROR_CODES.includes(value);
|
|
36
84
|
}
|
|
37
85
|
var GyroViewError = class extends Error {
|
|
38
86
|
code;
|
|
87
|
+
/**
|
|
88
|
+
* Its own property rather than a getter, so a serialized or logged copy of the error keeps it.
|
|
89
|
+
*/
|
|
90
|
+
category;
|
|
39
91
|
constructor(code, message, options) {
|
|
40
92
|
super(message, options);
|
|
41
93
|
this.code = code;
|
|
42
94
|
this.name = "GyroViewError";
|
|
95
|
+
this.category = CATEGORY_OF_CODE[code];
|
|
43
96
|
}
|
|
44
97
|
};
|
|
45
98
|
/**
|
|
@@ -78,4 +131,4 @@ function ensureIndexInRange(index, length, subject) {
|
|
|
78
131
|
if (!Number.isSafeInteger(index) || index < 0 || index >= length) throw new GyroViewError("index-out-of-range", `${subject} index ${index} is outside 0..${length - 1}`);
|
|
79
132
|
}
|
|
80
133
|
//#endregion
|
|
81
|
-
export { GYRO_VIEW_ERROR_CODES, GyroViewError, asGyroViewError, ensureIndexInRange, ensureInvariant, hasErrorCode, isAbortError, isGyroViewErrorCode, messageOf };
|
|
134
|
+
export { GYRO_VIEW_ERROR_CATEGORIES, GYRO_VIEW_ERROR_CODES, GyroViewError, asGyroViewError, ensureIndexInRange, ensureInvariant, hasErrorCode, isAbortError, isGyroViewErrorCode, messageOf };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region ../../packages/core/src/shared/math/countAtOrBelow.ts
|
|
2
|
+
/**
|
|
3
|
+
* How many of `count` values in ascending order, read through `valueAt`, are at or below
|
|
4
|
+
* `target`: the index of the first value above it. Binary search, for long sorted columns.
|
|
5
|
+
*/
|
|
6
|
+
function countAtOrBelow(count, valueAt, target) {
|
|
7
|
+
let low = 0;
|
|
8
|
+
let high = count;
|
|
9
|
+
while (low < high) {
|
|
10
|
+
const middle = low + high >>> 1;
|
|
11
|
+
if (valueAt(middle) <= target) low = middle + 1;
|
|
12
|
+
else high = middle;
|
|
13
|
+
}
|
|
14
|
+
return low;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { countAtOrBelow };
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { SourceByteStream } from "../../../core/src/application/download/SourceByteStream.js";
|
|
1
2
|
import { Deferred } from "../../../core/src/shared/async/Deferred.js";
|
|
2
3
|
import "../../../core/src/index.js";
|
|
3
4
|
import { BlobRandomAccessSource } from "../../../adapters/blob/src/BlobRandomAccessSource.js";
|
|
4
5
|
import "../../../adapters/blob/src/index.js";
|
|
6
|
+
import { HttpByteStream } from "../../../adapters/fetch/src/HttpByteStream.js";
|
|
5
7
|
import { HttpRangeSource } from "../../../adapters/fetch/src/HttpRangeSource.js";
|
|
8
|
+
import { HttpResource } from "../../../adapters/fetch/src/HttpResource.js";
|
|
6
9
|
import { HttpResourceLocator } from "../../../adapters/fetch/src/HttpResourceLocator.js";
|
|
7
10
|
import "../../../adapters/fetch/src/index.js";
|
|
8
|
-
import {
|
|
11
|
+
import { MediabunnyCodecReader } from "../../../adapters/mediabunny/src/MediabunnyCodecReader.js";
|
|
12
|
+
import { MediabunnyAudioPackager } from "../../../adapters/mediabunny/src/MediabunnyAudioPackager.js";
|
|
9
13
|
import "../../../adapters/mediabunny/src/index.js";
|
|
10
14
|
import { WebCodecsVideoDecoderPort } from "../../../adapters/webcodecs/src/WebCodecsVideoDecoderPort.js";
|
|
11
15
|
import "../../../adapters/webcodecs/src/index.js";
|
|
@@ -20,17 +24,32 @@ function browserPorts(options = {}) {
|
|
|
20
24
|
const probeTimeoutMs = options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
21
25
|
return {
|
|
22
26
|
sources: browserSources(http),
|
|
23
|
-
|
|
27
|
+
codecReader: new MediabunnyCodecReader(),
|
|
28
|
+
audioPackager: new MediabunnyAudioPackager(),
|
|
24
29
|
decoderPort: new WebCodecsVideoDecoderPort(),
|
|
25
30
|
locatorFor: (input) => new HttpResourceLocator(requestOptionsFor(http, input)),
|
|
26
31
|
probeDeadline: () => deadlineIn(probeTimeoutMs)
|
|
27
32
|
};
|
|
28
33
|
}
|
|
29
34
|
/**
|
|
30
|
-
* URLs are read with HTTP ranges, blobs by slicing.
|
|
35
|
+
* URLs are read with HTTP ranges, both ways over one resource; blobs by slicing, a range whole.
|
|
31
36
|
*/
|
|
32
37
|
function browserSources(http) {
|
|
33
|
-
return { open: (input, signal) => isUrlInput(input) ?
|
|
38
|
+
return { open: (input, signal) => isUrlInput(input) ? urlSource(input, http, signal) : blobSource(input) };
|
|
39
|
+
}
|
|
40
|
+
function urlSource(input, http, signal) {
|
|
41
|
+
const resource = new HttpResource(input.url, requestOptionsFor(http, input), signal);
|
|
42
|
+
return {
|
|
43
|
+
source: new HttpRangeSource(resource),
|
|
44
|
+
stream: new HttpByteStream(resource)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function blobSource(input) {
|
|
48
|
+
const source = new BlobRandomAccessSource(input.blob);
|
|
49
|
+
return {
|
|
50
|
+
source,
|
|
51
|
+
stream: new SourceByteStream(source)
|
|
52
|
+
};
|
|
34
53
|
}
|
|
35
54
|
/**
|
|
36
55
|
* The shared request settings, with the input's own credentials in place of theirs when it names
|
|
@@ -5,6 +5,7 @@ import { buildStitchingSetup } from "../../../core/src/domain/stitching/Stitchin
|
|
|
5
5
|
import { StabilizingFrameSink } from "../../../core/src/application/stabilization/StabilizingFrameSink.js";
|
|
6
6
|
import { GainMatchingFrameSink } from "../../../core/src/application/gainMatching/GainMatchingFrameSink.js";
|
|
7
7
|
import { WallClock } from "../../../core/src/application/playback/WallClock.js";
|
|
8
|
+
import { isFlowing } from "../../../core/src/domain/playback/PlayerState.js";
|
|
8
9
|
import { PlaybackSession } from "../../../core/src/application/playback/PlaybackSession.js";
|
|
9
10
|
import "../../../core/src/index.js";
|
|
10
11
|
import { MediaSourceAudioClock } from "../../../adapters/mse-audio/src/MediaSourceAudioClock.js";
|
|
@@ -37,6 +38,7 @@ async function buildPipeline(parts) {
|
|
|
37
38
|
disposables.add(() => {
|
|
38
39
|
session.dispose();
|
|
39
40
|
});
|
|
41
|
+
readAheadOnceFlowing(session, parts.opened);
|
|
40
42
|
return {
|
|
41
43
|
session,
|
|
42
44
|
renderer: drawing.renderer,
|
|
@@ -94,10 +96,10 @@ function pictureSettingsOf(drawing, session) {
|
|
|
94
96
|
* otherwise a wall clock, with a warning saying why there is no sound.
|
|
95
97
|
*/
|
|
96
98
|
async function clockFor(parts) {
|
|
97
|
-
const {
|
|
98
|
-
if (!
|
|
99
|
+
const { soundSegments } = parts.opened;
|
|
100
|
+
if (!soundSegments) return wallClock(NO_AUDIO_WARNING);
|
|
99
101
|
try {
|
|
100
|
-
const segments =
|
|
102
|
+
const segments = soundSegments();
|
|
101
103
|
if (!MediaSourceAudioClock.isSupported(segments)) return wallClock(AUDIO_UNSUPPORTED_WARNING);
|
|
102
104
|
parts.signal.throwIfAborted();
|
|
103
105
|
return {
|
|
@@ -138,6 +140,15 @@ function sinkOver(renderer, opened) {
|
|
|
138
140
|
stabilizing
|
|
139
141
|
};
|
|
140
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* The downloads read only what the picture waits for until playing first starts, so a page of
|
|
145
|
+
* players that were never played stays quiet; from then on they read ahead, paused or not.
|
|
146
|
+
*/
|
|
147
|
+
function readAheadOnceFlowing(session, opened) {
|
|
148
|
+
session.events.on("statechange", (state) => {
|
|
149
|
+
if (isFlowing(state)) opened.readAhead();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
141
152
|
function sessionFor(parts, clock, sink) {
|
|
142
153
|
return new PlaybackSession({
|
|
143
154
|
frameSources: parts.opened.frameSources,
|
|
@@ -145,6 +156,7 @@ function sessionFor(parts, clock, sink) {
|
|
|
145
156
|
clock,
|
|
146
157
|
sink,
|
|
147
158
|
duration: parts.opened.duration,
|
|
159
|
+
buffer: parts.opened.buffer,
|
|
148
160
|
pipeline: DECODE_PIPELINE_OPTIONS,
|
|
149
161
|
queueCapacity: 4
|
|
150
162
|
});
|
|
@@ -1,130 +1,70 @@
|
|
|
1
|
-
import { GyroViewError, ensureInvariant
|
|
1
|
+
import { GyroViewError, ensureInvariant } from "../../../core/src/shared/errors/GyroViewError.js";
|
|
2
2
|
import { seconds } from "../../../core/src/shared/units/time.js";
|
|
3
|
-
import { readRecording } from "../../../core/src/application/recording/readRecording.js";
|
|
4
3
|
import { timeRecording } from "../../../core/src/application/recording/timeRecording.js";
|
|
5
4
|
import { detectLensLayout } from "../../../core/src/domain/format/layout/detectLensLayout.js";
|
|
6
5
|
import { lensFrameOrder } from "../../../core/src/domain/stitching/StitchingSetup.js";
|
|
6
|
+
import { startFileDownload } from "../../../core/src/application/download/startFileDownload.js";
|
|
7
|
+
import { downloadPolicyFor } from "../../../core/src/domain/download/DownloadPolicy.js";
|
|
8
|
+
import { RecordingBuffer } from "../../../core/src/application/download/RecordingBuffer.js";
|
|
7
9
|
import "../../../core/src/index.js";
|
|
8
|
-
import { inputName } from "../PlayerSource.js";
|
|
9
10
|
import { Disposables } from "./Disposables.js";
|
|
10
11
|
import { ensureDecodable } from "./ensureDecodable.js";
|
|
12
|
+
import { describedInput, readInputs } from "./readInputs.js";
|
|
11
13
|
//#region ../../packages/player/src/composition/openInputs.ts
|
|
12
14
|
/**
|
|
13
|
-
* Opens the given inputs as one recording
|
|
14
|
-
*
|
|
15
|
+
* Opens the given inputs as one recording (ADR 0029): their sample tables and codecs read, the
|
|
16
|
+
* lens layout and calibration decided, then one download a file, which the decode probe reads
|
|
17
|
+
* through first. Everything opened is released again when any step fails or the attempt is
|
|
18
|
+
* aborted.
|
|
15
19
|
*/
|
|
16
20
|
async function openInputs(inputs, attempt) {
|
|
17
21
|
const disposables = new Disposables();
|
|
18
22
|
try {
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const calibration = calibrationOf(
|
|
23
|
+
const read = await readInputs(inputs, attempt);
|
|
24
|
+
const described = read.inputs.map((input) => describedInput(input));
|
|
25
|
+
const layout = detectLensLayout(described, read.recording.info);
|
|
26
|
+
const calibration = calibrationOf(read.recording);
|
|
27
|
+
const files = startDownloads(read.inputs, disposables);
|
|
28
|
+
const frameSources = lensFrameOrder(layout).map((key) => trackAt(files, key));
|
|
23
29
|
await ensureDecodable(frameSources, attempt);
|
|
24
|
-
const timing = await timeRecording(
|
|
30
|
+
const timing = await timeRecording(read.recording, frameSources[0]);
|
|
25
31
|
attempt.signal.throwIfAborted();
|
|
26
32
|
return assemble({
|
|
27
|
-
|
|
33
|
+
read,
|
|
34
|
+
files,
|
|
28
35
|
layout,
|
|
29
36
|
frameSources,
|
|
30
37
|
calibration,
|
|
31
38
|
timing
|
|
32
|
-
}, disposables);
|
|
39
|
+
}, attempt.ports.audioPackager, disposables);
|
|
33
40
|
} catch (error) {
|
|
34
41
|
disposables.disposeAll();
|
|
35
42
|
throw error;
|
|
36
43
|
}
|
|
37
44
|
}
|
|
38
|
-
async function demuxInputs(inputs, attempt, disposables) {
|
|
39
|
-
const { ports, signal } = attempt;
|
|
40
|
-
const given = inputs.map((input) => ({
|
|
41
|
-
input,
|
|
42
|
-
source: ports.sources.open(input, signal)
|
|
43
|
-
}));
|
|
44
|
-
const recording = await readRecordingOf(given, ports);
|
|
45
|
-
signal.throwIfAborted();
|
|
46
|
-
const openings = [...given, ...await declaredSecond(given, recording, attempt)];
|
|
47
|
-
const settled = await Promise.allSettled(openings.map(({ input, source }) => ports.demuxer.open(source, inputName(input))));
|
|
48
|
-
const opened = settled.flatMap((result) => result.status === "fulfilled" ? [result.value] : []);
|
|
49
|
-
for (const input of opened) disposables.add(() => {
|
|
50
|
-
input.dispose();
|
|
51
|
-
});
|
|
52
|
-
const failure = settled.find((result) => result.status === "rejected");
|
|
53
|
-
if (failure) throw failure.reason;
|
|
54
|
-
signal.throwIfAborted();
|
|
55
|
-
return {
|
|
56
|
-
recording,
|
|
57
|
-
inputs: opened
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* The other lens file a lone file's info record says the recording is split into, when the
|
|
62
|
-
* server has it: found before the lone file's tracks are read, which finding it only once they
|
|
63
|
-
* fell short would read twice. A file that does not say so still gets its sibling looked for
|
|
64
|
-
* once its tracks fall short (see openRecording).
|
|
65
|
-
*/
|
|
66
|
-
async function declaredSecond(given, recording, attempt) {
|
|
67
|
-
if (!(given.length === 1 && recording.info.fileLayout === "split-files") || !attempt.findSecondFile) return [];
|
|
68
|
-
const input = await attempt.findSecondFile();
|
|
69
|
-
attempt.signal.throwIfAborted();
|
|
70
|
-
return input === void 0 ? [] : [{
|
|
71
|
-
input,
|
|
72
|
-
source: attempt.ports.sources.open(input, attempt.signal)
|
|
73
|
-
}];
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* The layout hints of a file without an info record: the tracks alone decide.
|
|
77
|
-
*/
|
|
78
|
-
var NO_HINTS = {
|
|
79
|
-
fileLayout: void 0,
|
|
80
|
-
trackOrder: void 0
|
|
81
|
-
};
|
|
82
|
-
async function readRecordingOf(openings, ports) {
|
|
83
|
-
try {
|
|
84
|
-
return await readTrailerCarrier(openings.map(({ source }) => source));
|
|
85
|
-
} catch (error) {
|
|
86
|
-
const [only] = openings;
|
|
87
|
-
if (openings.length === 1 && hasErrorCode(error, "invalid-trailer") && only) await refuseAsLoneHalf(only, ports);
|
|
88
|
-
throw error;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
45
|
/**
|
|
92
|
-
*
|
|
93
|
-
* trailer to _00_ alone: its one square track tells, and the refusal asks for the other file
|
|
94
|
-
* instead of calling it damaged. Anything else leaves the trailer's own refusal standing.
|
|
46
|
+
* One download a file, each with its share of the budget.
|
|
95
47
|
*/
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
* first has none, as the _10_ file of an older camera's pair, which writes it to _00_ alone,
|
|
114
|
-
* when the pair is given the other way round.
|
|
115
|
-
*/
|
|
116
|
-
async function readTrailerCarrier(sources) {
|
|
117
|
-
const [first, second] = sources;
|
|
118
|
-
ensureInvariant(first !== void 0, "a recording needs at least one input");
|
|
119
|
-
try {
|
|
120
|
-
return await readRecording(first);
|
|
121
|
-
} catch (error) {
|
|
122
|
-
if (second === void 0 || !hasErrorCode(error, "invalid-trailer")) throw error;
|
|
123
|
-
return readRecording(second);
|
|
124
|
-
}
|
|
48
|
+
function startDownloads(inputs, disposables) {
|
|
49
|
+
return inputs.map(({ opened, table, codecs, size }) => {
|
|
50
|
+
const policy = downloadPolicyFor({
|
|
51
|
+
size,
|
|
52
|
+
duration: table.duration
|
|
53
|
+
}, inputs.length);
|
|
54
|
+
const file = startFileDownload({
|
|
55
|
+
table,
|
|
56
|
+
stream: opened.stream,
|
|
57
|
+
codecs,
|
|
58
|
+
policy
|
|
59
|
+
});
|
|
60
|
+
disposables.add(() => {
|
|
61
|
+
file.dispose();
|
|
62
|
+
});
|
|
63
|
+
return file;
|
|
64
|
+
});
|
|
125
65
|
}
|
|
126
|
-
function trackAt(
|
|
127
|
-
const track =
|
|
66
|
+
function trackAt(files, key) {
|
|
67
|
+
const track = files[key.inputIndex]?.videoTracks[key.trackIndex];
|
|
128
68
|
ensureInvariant(track !== void 0, `the layout points at track ${key.trackIndex} of input ${key.inputIndex}, which does not exist`);
|
|
129
69
|
return track;
|
|
130
70
|
}
|
|
@@ -134,26 +74,30 @@ function calibrationOf(recording) {
|
|
|
134
74
|
const detail = warnings.length > 0 ? ` (${warnings.join("; ")})` : "";
|
|
135
75
|
throw new GyroViewError("no-calibration", `the recording carries no usable lens calibration, so it cannot be stitched${detail}`);
|
|
136
76
|
}
|
|
137
|
-
function assemble(parts, disposables) {
|
|
138
|
-
const {
|
|
139
|
-
const duration = seconds(Math.min(...
|
|
140
|
-
const [
|
|
77
|
+
function assemble(parts, packager, disposables) {
|
|
78
|
+
const { read, files, layout, frameSources, calibration, timing } = parts;
|
|
79
|
+
const duration = seconds(Math.min(...files.map((file) => file.duration)));
|
|
80
|
+
const [sound] = files.flatMap((file) => file.audioTracks);
|
|
141
81
|
return {
|
|
142
|
-
recording:
|
|
82
|
+
recording: read.recording,
|
|
143
83
|
layout,
|
|
144
84
|
frameSources,
|
|
145
85
|
calibration,
|
|
146
86
|
duration,
|
|
147
87
|
frameTimes: timing.frameTimes,
|
|
148
88
|
motion: timing.motion,
|
|
149
|
-
|
|
150
|
-
metadata: metadataOf(parts, duration,
|
|
89
|
+
soundSegments: sound && (() => packager.segmentsOf(sound.samples, sound.configuration)),
|
|
90
|
+
metadata: metadataOf(parts, duration, sound !== void 0),
|
|
151
91
|
warnings: timing.warnings,
|
|
92
|
+
readAhead: () => {
|
|
93
|
+
for (const file of files) file.download.startReadingAhead();
|
|
94
|
+
},
|
|
95
|
+
buffer: new RecordingBuffer(files.map((file) => file.download)),
|
|
152
96
|
dispose: disposables.toDisposer()
|
|
153
97
|
};
|
|
154
98
|
}
|
|
155
99
|
function metadataOf(parts, duration, hasAudio) {
|
|
156
|
-
const { info } = parts.
|
|
100
|
+
const { info } = parts.read.recording;
|
|
157
101
|
const { motion } = parts.timing;
|
|
158
102
|
return {
|
|
159
103
|
model: info.model,
|