@bubo-squared/gyroview 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +35 -15
  3. package/dist/index.d.ts +21 -2
  4. package/dist/index.js +2 -2
  5. package/dist/packages/adapters/fetch/src/HttpByteStream.js +174 -0
  6. package/dist/packages/adapters/fetch/src/HttpRangeSource.js +14 -170
  7. package/dist/packages/adapters/fetch/src/HttpResource.js +192 -0
  8. package/dist/packages/adapters/fetch/src/contentRange.js +15 -0
  9. package/dist/packages/adapters/fetch/src/httpRequest.js +5 -3
  10. package/dist/packages/adapters/fetch/src/index.js +3 -1
  11. package/dist/packages/adapters/fetch/src/passingFailures.js +24 -0
  12. package/dist/packages/adapters/fetch/src/rangeFailures.js +17 -0
  13. package/dist/packages/adapters/fetch/src/recordingVersion.js +37 -0
  14. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioPackager.js +129 -0
  15. package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +80 -0
  16. package/dist/packages/adapters/mediabunny/src/SegmentChannel.js +58 -6
  17. package/dist/packages/adapters/mediabunny/src/decoderConfigurations.js +28 -0
  18. package/dist/packages/adapters/mediabunny/src/index.js +3 -2
  19. package/dist/packages/adapters/webcodecs/src/WebCodecsVideoDecoderPort.js +12 -3
  20. package/dist/packages/core/src/application/download/BlockStore.js +97 -0
  21. package/dist/packages/core/src/application/download/DownloadedAudioSamples.js +50 -0
  22. package/dist/packages/core/src/application/download/DownloadedVideoTrack.js +122 -0
  23. package/dist/packages/core/src/application/download/FileDownload.js +183 -0
  24. package/dist/packages/core/src/application/download/RecordingBuffer.js +22 -0
  25. package/dist/packages/core/src/application/download/SampleCursor.js +88 -0
  26. package/dist/packages/core/src/application/download/SourceByteStream.js +46 -0
  27. package/dist/packages/core/src/application/download/Transfers.js +75 -0
  28. package/dist/packages/core/src/application/download/startFileDownload.js +37 -0
  29. package/dist/packages/core/src/application/playback/Buffering.js +53 -0
  30. package/dist/packages/core/src/application/playback/DecodePipeline.js +11 -7
  31. package/dist/packages/core/src/application/playback/DecodeRun.js +15 -0
  32. package/dist/packages/core/src/application/playback/PlaybackSession.js +18 -27
  33. package/dist/packages/core/src/application/playback/keyframeTimeAt.js +1 -1
  34. package/dist/packages/core/src/application/playback/probeDecoding.js +17 -3
  35. package/dist/packages/core/src/application/recording/readSampleTable.js +27 -0
  36. package/dist/packages/core/src/application/recording/timeRecording.js +3 -3
  37. package/dist/packages/core/src/domain/container/KeyframeRule.js +7 -0
  38. package/dist/packages/core/src/domain/container/SampleTable.js +29 -0
  39. package/dist/packages/core/src/domain/container/TrackSampleTable.js +96 -0
  40. package/dist/packages/core/src/domain/container/presentationOrder.js +10 -0
  41. package/dist/packages/core/src/domain/download/DownloadPolicy.js +41 -0
  42. package/dist/packages/core/src/domain/download/isReadyToResume.js +27 -0
  43. package/dist/packages/core/src/domain/download/planDownloads.js +175 -0
  44. package/dist/packages/core/src/domain/download/trackNeeds.js +32 -0
  45. package/dist/packages/core/src/domain/format/boxes/boxHeader.js +44 -0
  46. package/dist/packages/core/src/domain/format/boxes/scanBoxes.js +7 -34
  47. package/dist/packages/core/src/domain/format/mp4/keyframeRules.js +81 -0
  48. package/dist/packages/core/src/domain/format/mp4/movieBoxes.js +52 -0
  49. package/dist/packages/core/src/domain/format/mp4/mp4BoxTypes.js +46 -0
  50. package/dist/packages/core/src/domain/format/mp4/mp4Layouts.js +183 -0
  51. package/dist/packages/core/src/domain/format/mp4/parseMovie.js +67 -0
  52. package/dist/packages/core/src/domain/format/mp4/readTable.js +31 -0
  53. package/dist/packages/core/src/domain/format/mp4/sampleLocations.js +69 -0
  54. package/dist/packages/core/src/domain/format/mp4/sampleTiming.js +109 -0
  55. package/dist/packages/core/src/domain/format/mp4/trackHeaders.js +75 -0
  56. package/dist/packages/core/src/index.js +10 -2
  57. package/dist/packages/core/src/shared/async/iteration.js +24 -0
  58. package/dist/packages/core/src/shared/binary/ByteRange.js +12 -0
  59. package/dist/packages/core/src/shared/binary/ByteRangeSet.js +85 -0
  60. package/dist/packages/core/src/shared/binary/ByteReader.js +21 -3
  61. package/dist/packages/core/src/shared/binary/concatenated.js +15 -0
  62. package/dist/packages/core/src/shared/errors/GyroViewError.js +56 -3
  63. package/dist/packages/core/src/shared/math/countAtOrBelow.js +17 -0
  64. package/dist/packages/player/src/composition/browserPorts.js +23 -4
  65. package/dist/packages/player/src/composition/buildPipeline.js +15 -3
  66. package/dist/packages/player/src/composition/openInputs.js +51 -107
  67. package/dist/packages/player/src/composition/readInputs.js +111 -0
  68. package/dist/packages/player/src/controls/messages.js +3 -1
  69. package/dist/packages/player/src/inspectRecording.js +1 -1
  70. package/dist/standalone.js +58 -58
  71. package/package.json +1 -1
  72. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioSegments.js +0 -133
  73. package/dist/packages/adapters/mediabunny/src/MediabunnyDemuxer.js +0 -63
  74. package/dist/packages/adapters/mediabunny/src/MediabunnyVideoTrackReader.js +0 -84
@@ -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, little-endian reads over a byte array. Every read is positional, so callers
12
- * express the record layouts they parse as named offsets rather than as a moving cursor.
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 > BigInt(Number.MAX_SAFE_INTEGER)) throw new GyroViewError("binary-unsafe-integer", `uint64 at offset ${offset} (${value.toString()}) exceeds the safe integer range`);
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 categories. Embedders switch on these; messages are for humans.
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 { MediabunnyDemuxer } from "../../../adapters/mediabunny/src/MediabunnyDemuxer.js";
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
- demuxer: new MediabunnyDemuxer(),
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) ? new HttpRangeSource(input.url, requestOptionsFor(http, input), signal) : new BlobRandomAccessSource(input.blob) };
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 { audioTrack } = parts.opened;
98
- if (!audioTrack) return wallClock(NO_AUDIO_WARNING);
99
+ const { soundSegments } = parts.opened;
100
+ if (!soundSegments) return wallClock(NO_AUDIO_WARNING);
99
101
  try {
100
- const segments = await audioTrack.openSegments();
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, hasErrorCode } from "../../../core/src/shared/errors/GyroViewError.js";
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, its metadata from the input that carries the
14
- * trailer. Everything opened is released again when any step fails or the attempt is aborted.
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 demuxed = await demuxInputs(inputs, attempt, disposables);
20
- const layout = detectLensLayout(demuxed.inputs, demuxed.recording.info);
21
- const frameSources = lensFrameOrder(layout).map((key) => trackAt(demuxed.inputs, key));
22
- const calibration = calibrationOf(demuxed.recording);
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(demuxed.recording, frameSources[0]);
30
+ const timing = await timeRecording(read.recording, frameSources[0]);
25
31
  attempt.signal.throwIfAborted();
26
32
  return assemble({
27
- demuxed,
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
- * A file without a trailer may be the _10_ half of an older camera's pair, which writes the
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
- async function refuseAsLoneHalf(opening, ports) {
97
- let input;
98
- try {
99
- input = await ports.demuxer.open(opening.source, inputName(opening.input));
100
- } catch {
101
- return;
102
- }
103
- try {
104
- detectLensLayout([input], NO_HINTS);
105
- } catch (error) {
106
- if (hasErrorCode(error, "missing-second-file")) throw error;
107
- } finally {
108
- input.dispose();
109
- }
110
- }
111
- /**
112
- * The recording read from the input that carries the trailer: the first, or the second when the
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(inputs, key) {
127
- const track = inputs[key.inputIndex]?.videoTracks[key.trackIndex];
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 { demuxed, layout, frameSources, calibration, timing } = parts;
139
- const duration = seconds(Math.min(...demuxed.inputs.map((input) => input.duration)));
140
- const [audioTrack] = demuxed.inputs.flatMap((input) => input.audioTracks);
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: demuxed.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
- audioTrack,
150
- metadata: metadataOf(parts, duration, audioTrack !== void 0),
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.demuxed.recording;
100
+ const { info } = parts.read.recording;
157
101
  const { motion } = parts.timing;
158
102
  return {
159
103
  model: info.model,