@bubo-squared/gyroview 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +16 -5
  3. package/dist/index.d.ts +1 -0
  4. package/dist/packages/adapters/fetch/src/HttpByteStream.js +174 -0
  5. package/dist/packages/adapters/fetch/src/HttpRangeSource.js +14 -170
  6. package/dist/packages/adapters/fetch/src/HttpResource.js +192 -0
  7. package/dist/packages/adapters/fetch/src/contentRange.js +15 -0
  8. package/dist/packages/adapters/fetch/src/httpRequest.js +5 -3
  9. package/dist/packages/adapters/fetch/src/index.js +3 -1
  10. package/dist/packages/adapters/fetch/src/passingFailures.js +24 -0
  11. package/dist/packages/adapters/fetch/src/rangeFailures.js +17 -0
  12. package/dist/packages/adapters/fetch/src/recordingVersion.js +37 -0
  13. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioPackager.js +129 -0
  14. package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +80 -0
  15. package/dist/packages/adapters/mediabunny/src/SegmentChannel.js +58 -6
  16. package/dist/packages/adapters/mediabunny/src/decoderConfigurations.js +28 -0
  17. package/dist/packages/adapters/mediabunny/src/index.js +3 -2
  18. package/dist/packages/core/src/application/download/BlockStore.js +97 -0
  19. package/dist/packages/core/src/application/download/DownloadedAudioSamples.js +50 -0
  20. package/dist/packages/core/src/application/download/DownloadedVideoTrack.js +122 -0
  21. package/dist/packages/core/src/application/download/FileDownload.js +183 -0
  22. package/dist/packages/core/src/application/download/RecordingBuffer.js +22 -0
  23. package/dist/packages/core/src/application/download/SampleCursor.js +88 -0
  24. package/dist/packages/core/src/application/download/SourceByteStream.js +46 -0
  25. package/dist/packages/core/src/application/download/Transfers.js +75 -0
  26. package/dist/packages/core/src/application/download/startFileDownload.js +37 -0
  27. package/dist/packages/core/src/application/playback/Buffering.js +53 -0
  28. package/dist/packages/core/src/application/playback/DecodePipeline.js +11 -7
  29. package/dist/packages/core/src/application/playback/DecodeRun.js +15 -0
  30. package/dist/packages/core/src/application/playback/PlaybackSession.js +18 -27
  31. package/dist/packages/core/src/application/playback/keyframeTimeAt.js +1 -1
  32. package/dist/packages/core/src/application/playback/probeDecoding.js +15 -1
  33. package/dist/packages/core/src/application/recording/readSampleTable.js +27 -0
  34. package/dist/packages/core/src/application/recording/timeRecording.js +3 -3
  35. package/dist/packages/core/src/domain/container/KeyframeRule.js +7 -0
  36. package/dist/packages/core/src/domain/container/SampleTable.js +29 -0
  37. package/dist/packages/core/src/domain/container/TrackSampleTable.js +96 -0
  38. package/dist/packages/core/src/domain/container/presentationOrder.js +10 -0
  39. package/dist/packages/core/src/domain/download/DownloadPolicy.js +41 -0
  40. package/dist/packages/core/src/domain/download/isReadyToResume.js +27 -0
  41. package/dist/packages/core/src/domain/download/planDownloads.js +175 -0
  42. package/dist/packages/core/src/domain/download/trackNeeds.js +32 -0
  43. package/dist/packages/core/src/domain/format/boxes/boxHeader.js +44 -0
  44. package/dist/packages/core/src/domain/format/boxes/scanBoxes.js +7 -34
  45. package/dist/packages/core/src/domain/format/mp4/keyframeRules.js +81 -0
  46. package/dist/packages/core/src/domain/format/mp4/movieBoxes.js +52 -0
  47. package/dist/packages/core/src/domain/format/mp4/mp4BoxTypes.js +46 -0
  48. package/dist/packages/core/src/domain/format/mp4/mp4Layouts.js +183 -0
  49. package/dist/packages/core/src/domain/format/mp4/parseMovie.js +67 -0
  50. package/dist/packages/core/src/domain/format/mp4/readTable.js +31 -0
  51. package/dist/packages/core/src/domain/format/mp4/sampleLocations.js +69 -0
  52. package/dist/packages/core/src/domain/format/mp4/sampleTiming.js +109 -0
  53. package/dist/packages/core/src/domain/format/mp4/trackHeaders.js +75 -0
  54. package/dist/packages/core/src/index.js +9 -1
  55. package/dist/packages/core/src/shared/async/iteration.js +24 -0
  56. package/dist/packages/core/src/shared/binary/ByteRange.js +12 -0
  57. package/dist/packages/core/src/shared/binary/ByteRangeSet.js +85 -0
  58. package/dist/packages/core/src/shared/binary/ByteReader.js +21 -3
  59. package/dist/packages/core/src/shared/binary/concatenated.js +15 -0
  60. package/dist/packages/core/src/shared/errors/GyroViewError.js +1 -0
  61. package/dist/packages/core/src/shared/math/countAtOrBelow.js +17 -0
  62. package/dist/packages/player/src/composition/browserPorts.js +23 -4
  63. package/dist/packages/player/src/composition/buildPipeline.js +15 -3
  64. package/dist/packages/player/src/composition/openInputs.js +51 -107
  65. package/dist/packages/player/src/composition/readInputs.js +111 -0
  66. package/dist/packages/player/src/controls/messages.js +1 -0
  67. package/dist/packages/player/src/inspectRecording.js +1 -1
  68. package/dist/standalone.js +58 -58
  69. package/package.json +1 -1
  70. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioSegments.js +0 -133
  71. package/dist/packages/adapters/mediabunny/src/MediabunnyDemuxer.js +0 -63
  72. package/dist/packages/adapters/mediabunny/src/MediabunnyVideoTrackReader.js +0 -84
@@ -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,
@@ -0,0 +1,111 @@
1
+ import { ensureInvariant, hasErrorCode } from "../../../core/src/shared/errors/GyroViewError.js";
2
+ import { readRecording } from "../../../core/src/application/recording/readRecording.js";
3
+ import { readSampleTable } from "../../../core/src/application/recording/readSampleTable.js";
4
+ import { detectLensLayout } from "../../../core/src/domain/format/layout/detectLensLayout.js";
5
+ import "../../../core/src/index.js";
6
+ import { inputName } from "../PlayerSource.js";
7
+ //#region ../../packages/player/src/composition/readInputs.ts
8
+ /**
9
+ * Reads the given inputs, and the other lens file an info record declares, as one recording: its
10
+ * metadata from the input that carries the trailer, and every input's sample table and codecs.
11
+ */
12
+ async function readInputs(inputs, attempt) {
13
+ const { ports, signal } = attempt;
14
+ const given = inputs.map((input) => ({
15
+ input,
16
+ opened: ports.sources.open(input, signal)
17
+ }));
18
+ const recording = await readRecordingOf(given, ports);
19
+ signal.throwIfAborted();
20
+ const openings = [...given, ...await declaredSecond(given, recording, attempt)];
21
+ const read = await Promise.all(openings.map((opening) => readInput(opening, ports)));
22
+ signal.throwIfAborted();
23
+ return {
24
+ recording,
25
+ inputs: read
26
+ };
27
+ }
28
+ function describedInput(read) {
29
+ const videoTracks = read.codecs.video.map(({ description }) => ({ description }));
30
+ return {
31
+ name: inputName(read.input),
32
+ videoTracks
33
+ };
34
+ }
35
+ async function readInput(opening, ports) {
36
+ const { source } = opening.opened;
37
+ const { table, movieBytes } = await readSampleTable(source);
38
+ const codecs = await ports.codecReader.read(movieBytes);
39
+ return {
40
+ ...opening,
41
+ table,
42
+ codecs,
43
+ size: await source.size()
44
+ };
45
+ }
46
+ /**
47
+ * The other lens file a lone file's info record says the recording is split into, when the
48
+ * server has it: found before the lone file's tracks are read, which finding it only once they
49
+ * fell short would read twice. A file that does not say so still gets its sibling looked for
50
+ * once its tracks fall short (see openRecording).
51
+ */
52
+ async function declaredSecond(given, recording, attempt) {
53
+ if (!(given.length === 1 && recording.info.fileLayout === "split-files") || !attempt.findSecondFile) return [];
54
+ const input = await attempt.findSecondFile();
55
+ attempt.signal.throwIfAborted();
56
+ return input === void 0 ? [] : [{
57
+ input,
58
+ opened: attempt.ports.sources.open(input, attempt.signal)
59
+ }];
60
+ }
61
+ /**
62
+ * The layout hints of a file without an info record: the tracks alone decide.
63
+ */
64
+ var NO_HINTS = {
65
+ fileLayout: void 0,
66
+ trackOrder: void 0
67
+ };
68
+ async function readRecordingOf(openings, ports) {
69
+ try {
70
+ return await readTrailerCarrier(openings.map(({ opened }) => opened.source));
71
+ } catch (error) {
72
+ const [only] = openings;
73
+ if (openings.length === 1 && hasErrorCode(error, "invalid-trailer") && only) await refuseAsLoneHalf(only, ports);
74
+ throw error;
75
+ }
76
+ }
77
+ /**
78
+ * A file without a trailer may be the _10_ half of an older camera's pair, which writes the
79
+ * trailer to _00_ alone: its one square track tells, and the refusal asks for the other file
80
+ * instead of calling it damaged. Anything else leaves the trailer's own refusal standing.
81
+ */
82
+ async function refuseAsLoneHalf(opening, ports) {
83
+ let read;
84
+ try {
85
+ read = await readInput(opening, ports);
86
+ } catch {
87
+ return;
88
+ }
89
+ try {
90
+ detectLensLayout([describedInput(read)], NO_HINTS);
91
+ } catch (error) {
92
+ if (hasErrorCode(error, "missing-second-file")) throw error;
93
+ }
94
+ }
95
+ /**
96
+ * The recording read from the input that carries the trailer: the first, or the second when the
97
+ * first has none, as the _10_ file of an older camera's pair, which writes it to _00_ alone,
98
+ * when the pair is given the other way round.
99
+ */
100
+ async function readTrailerCarrier(sources) {
101
+ const [first, second] = sources;
102
+ ensureInvariant(first !== void 0, "a recording needs at least one input");
103
+ try {
104
+ return await readRecording(first);
105
+ } catch (error) {
106
+ if (second === void 0 || !hasErrorCode(error, "invalid-trailer")) throw error;
107
+ return readRecording(second);
108
+ }
109
+ }
110
+ //#endregion
111
+ export { describedInput, readInputs };
@@ -71,6 +71,7 @@ var DEFAULT_MESSAGES = {
71
71
  "playback-blocked": FAILED,
72
72
  "range-unsupported": UNREACHABLE,
73
73
  "render-unavailable": UNSUPPORTED_BROWSER,
74
+ "source-changed": "The video was replaced while it played.",
74
75
  "source-truncated": UNREACHABLE,
75
76
  "source-unreadable": UNREACHABLE,
76
77
  "unsupported-calibration": UNREADABLE,
@@ -9,7 +9,7 @@ import { browserSources } from "./composition/browserPorts.js";
9
9
  */
10
10
  function inspectRecording(recording, options = {}) {
11
11
  const signal = options.signal ?? new AbortController().signal;
12
- const source = browserSources(options.http ?? {}).open(inputOf(recording), signal);
12
+ const { source } = browserSources(options.http ?? {}).open(inputOf(recording), signal);
13
13
  return inspectRecording$1(source);
14
14
  }
15
15
  function inputOf(recording) {