@bubo-squared/gyroview 0.4.1 → 0.5.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 +47 -0
- package/README.md +7 -7
- package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +10 -4
- package/dist/packages/adapters/mediabunny/src/decoderConfigurations.js +20 -4
- package/dist/packages/adapters/three/src/ThreeFrameRenderer.js +32 -13
- package/dist/packages/adapters/three/src/drawSchedules.js +13 -0
- package/dist/packages/adapters/three/src/index.js +1 -0
- package/dist/packages/core/src/application/download/FileDownload.js +17 -6
- package/dist/packages/core/src/application/download/SampleCursor.js +4 -4
- package/dist/packages/core/src/application/download/Transfers.js +7 -0
- package/dist/packages/core/src/application/recording/motionOf.js +5 -1
- package/dist/packages/core/src/application/stabilization/StabilizingFrameSink.js +4 -2
- package/dist/packages/core/src/domain/download/DownloadPolicy.js +10 -1
- package/dist/packages/core/src/domain/format/mp4/hevcCodecString.js +161 -0
- package/dist/packages/core/src/domain/format/records/gyro/SampleColumns.js +9 -2
- package/dist/packages/core/src/domain/motion/gyro/GyroTrack.js +14 -4
- package/dist/packages/core/src/domain/motion/imu/ImuFrame.js +35 -3
- package/dist/packages/core/src/domain/motion/mounting/Mounting.js +117 -0
- package/dist/packages/core/src/domain/motion/orientation/OrientationTrack.js +6 -4
- package/dist/packages/core/src/domain/motion/orientation/estimateGyroBias.js +9 -1
- package/dist/packages/core/src/domain/motion/orientation/gravity.js +1 -1
- package/dist/packages/core/src/index.js +4 -1
- package/dist/packages/core/src/shared/binary/ByteReader.js +10 -1
- package/dist/packages/core/src/shared/index.js +1 -0
- package/dist/packages/core/src/shared/math/Matrix3.js +18 -1
- package/dist/packages/core/src/shared/math/Quaternion.js +1 -1
- package/dist/packages/core/src/shared/math/Vector3.js +5 -1
- package/dist/packages/core/src/shared/math/minimum.js +17 -0
- package/dist/packages/player/src/composition/AnimationFrameDraws.js +38 -0
- package/dist/packages/player/src/composition/animationFrames.js +9 -0
- package/dist/packages/player/src/composition/buildPipeline.js +13 -7
- package/dist/packages/player/src/player/FrameLoop.js +1 -6
- package/dist/standalone.js +58 -59
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,53 @@
|
|
|
3
3
|
What changed for a page using the package, newest first. Until 1.0, a minor version may change
|
|
4
4
|
the API.
|
|
5
5
|
|
|
6
|
+
## 0.5.0 (2026-10-03)
|
|
7
|
+
|
|
8
|
+
New:
|
|
9
|
+
|
|
10
|
+
- Antigravity A1 recordings play. The A1 leaves the profile, tier and level of its HEVC
|
|
11
|
+
configuration blank, so the codec string is read from the stream's SPS instead (ADR 0037), and
|
|
12
|
+
its IMU frame is measured (ADR 0009): the `ready` event's `imuFrame` is
|
|
13
|
+
`{ name: 'A1', isVerified: true }`.
|
|
14
|
+
|
|
15
|
+
What a page may notice:
|
|
16
|
+
|
|
17
|
+
- An HEVC recording's codec in the `ready` event's `tracks[].codec` begins with the track's
|
|
18
|
+
sample entry type, `hvc1` on every camera seen, where it began with `hev1`; its profile, tier
|
|
19
|
+
and level are unchanged (ADR 0037).
|
|
20
|
+
- `off` and `follow` stand a recording upright as its camera stood, where its IMU frame is
|
|
21
|
+
measured (ADR 0038). An X3, X4 Air, X5 or X6 held upright on a stick or a tripod, its usual
|
|
22
|
+
pose, holds its body frame a quarter turn from upright, and these modes drew its recordings
|
|
23
|
+
turned a quarter turn; a camera upside down or with its lens axis vertical stands upright too.
|
|
24
|
+
Under `horizon`, such a camera's view keeps its heading while the camera pitches.
|
|
25
|
+
- An Insta360 X3 recording is stabilized about the right axes: its IMU frame is measured
|
|
26
|
+
(ADR 0009). The `ready` event's `imuFrame` is `{ name: 'X3', isVerified: true }` where it was
|
|
27
|
+
the unverified aligned frame, the load no longer warns that it has not been verified, and an
|
|
28
|
+
X3 on a stick or a tripod stands upright in `off` and `follow` too.
|
|
29
|
+
- An Insta360 ONE RS recording stands upright under `lock` and `horizon`, which turned it on its
|
|
30
|
+
side. Its IMU frame is assumed from one recording of a camera that never turned: `imuFrame` is
|
|
31
|
+
`{ name: 'ONE RS (unverified)', isVerified: false }`, and the load still warns.
|
|
32
|
+
- The view opens where Insta360 Studio centres the recording, in every mode: a half turn from
|
|
33
|
+
lens 0, where it opened, for a camera whose lens axis lies level, and a quarter turn from lens 0
|
|
34
|
+
for one whose lens axis is vertical, as the A1's (ADR 0038, ADR 0039). `yaw` 0 looks there: a
|
|
35
|
+
page that set `yaw` to face a direction faces the opposite one on a level camera until it adds
|
|
36
|
+
180 degrees. The equirectangular panorama is centred as Studio's export is.
|
|
37
|
+
|
|
38
|
+
## 0.4.2 (2026-10-02)
|
|
39
|
+
|
|
40
|
+
What a page may notice:
|
|
41
|
+
|
|
42
|
+
- Dragging, pinching or turning the wheel over a playing recording draws the picture once per
|
|
43
|
+
animation frame: in Safari, a fast mouse no longer slows the recording down. A change of the
|
|
44
|
+
view, the view mode, the quality or the exposure matching shows at the next animation frame
|
|
45
|
+
rather than at the call (ADR 0035).
|
|
46
|
+
- Opening a recording keeps the page busy for less time: its gyro is read and integrated in two
|
|
47
|
+
thirds of the time.
|
|
48
|
+
- Playing takes less of the page's main thread: the download plans what to fetch every few
|
|
49
|
+
mebibytes rather than at every frame (ADR 0036).
|
|
50
|
+
- A bundle that includes the package is smaller: mediabunny's demuxers for formats other than
|
|
51
|
+
MP4 and QuickTime are left out. The standalone module is about 40 KB smaller compressed.
|
|
52
|
+
|
|
6
53
|
## 0.4.1 (2026-10-01)
|
|
7
54
|
|
|
8
55
|
What a page may notice:
|
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# @bubo-squared/gyroview
|
|
2
2
|
|
|
3
|
-
Play raw
|
|
4
|
-
camera's dual-fisheye file directly, over HTTP byte
|
|
5
|
-
lenses in hardware with WebCodecs, and stitches and
|
|
6
|
-
Studio export step.
|
|
3
|
+
Play raw `.insv` recordings of Insta360 cameras (X3, X4, X5, X6, ONE RS) and the Antigravity A1
|
|
4
|
+
in the browser. `<gyro-view>` reads the camera's dual-fisheye file directly, over HTTP byte
|
|
5
|
+
ranges or from a local file, decodes both lenses in hardware with WebCodecs, and stitches and
|
|
6
|
+
gyro-stabilizes them on the GPU. No Insta360 Studio export step.
|
|
7
7
|
|
|
8
8
|
**Try it first:** [insv-player.com](https://insv-player.com/) plays `.insv` files with this
|
|
9
9
|
package. Drop your own recording to check that your camera and browser work before you install.
|
|
@@ -40,15 +40,15 @@ times a second while playing, as a media element's does; `frame` comes with ever
|
|
|
40
40
|
drawn.
|
|
41
41
|
|
|
42
42
|
A page without a bundler loads the standalone file, which has Three.js and mediabunny inside
|
|
43
|
-
(about
|
|
43
|
+
(about 260 KB compressed) and registers the element; it exports what the package does:
|
|
44
44
|
|
|
45
45
|
```html
|
|
46
46
|
<script
|
|
47
47
|
type="module"
|
|
48
|
-
src="https://cdn.jsdelivr.net/npm/@bubo-squared/gyroview@0.
|
|
48
|
+
src="https://cdn.jsdelivr.net/npm/@bubo-squared/gyroview@0.5/dist/standalone.js"
|
|
49
49
|
></script>
|
|
50
50
|
<script type="module">
|
|
51
|
-
import { inspectRecording } from 'https://cdn.jsdelivr.net/npm/@bubo-squared/gyroview@0.
|
|
51
|
+
import { inspectRecording } from 'https://cdn.jsdelivr.net/npm/@bubo-squared/gyroview@0.5/dist/standalone.js';
|
|
52
52
|
</script>
|
|
53
53
|
```
|
|
54
54
|
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { GyroViewError, isAbortError } from "../../../core/src/shared/errors/GyroViewError.js";
|
|
2
2
|
import "../../../core/src/index.js";
|
|
3
|
-
import { audioConfigurationOf, videoConfigurationOf } from "./decoderConfigurations.js";
|
|
3
|
+
import { audioConfigurationOf, codecStringOf, videoConfigurationOf } from "./decoderConfigurations.js";
|
|
4
4
|
import { trackColourOf } from "./trackColour.js";
|
|
5
|
-
import {
|
|
5
|
+
import { BufferSource, Input, MP4, QTFF } from "mediabunny";
|
|
6
6
|
//#region ../../packages/adapters/mediabunny/src/MediabunnyCodecReader.ts
|
|
7
7
|
/**
|
|
8
|
+
* The movie bytes are an ISO BMFF file's type and movie boxes, whatever its brand: with only
|
|
9
|
+
* these two formats named, a page's bundle leaves out mediabunny's other demuxers, a third of
|
|
10
|
+
* its weight.
|
|
11
|
+
*/
|
|
12
|
+
var MOVIE_FORMATS = [MP4, QTFF];
|
|
13
|
+
/**
|
|
8
14
|
* CodecReader over mediabunny: opens the file type and movie boxes as a file of their own, in
|
|
9
15
|
* memory, and tells each track's decoder configuration. mediabunny reads a sample only for a
|
|
10
16
|
* video codec without its configuration box; the samples are not there, so that track is
|
|
@@ -13,7 +19,7 @@ import { ALL_FORMATS, BufferSource, Input } from "mediabunny";
|
|
|
13
19
|
var MediabunnyCodecReader = class {
|
|
14
20
|
async read(movieBytes) {
|
|
15
21
|
const input = new Input({
|
|
16
|
-
formats:
|
|
22
|
+
formats: MOVIE_FORMATS,
|
|
17
23
|
source: new BufferSource(movieBytes)
|
|
18
24
|
});
|
|
19
25
|
try {
|
|
@@ -40,7 +46,7 @@ async function videoCodecOf(track, trackIndex) {
|
|
|
40
46
|
trackIndex,
|
|
41
47
|
codedWidth,
|
|
42
48
|
codedHeight,
|
|
43
|
-
codec: config
|
|
49
|
+
codec: await codecStringOf(track, config),
|
|
44
50
|
colour: trackColourOf(config.colorSpace)
|
|
45
51
|
};
|
|
46
52
|
return {
|
|
@@ -1,12 +1,28 @@
|
|
|
1
|
+
import { hevcCodecStringOf } from "../../../core/src/domain/format/mp4/hevcCodecString.js";
|
|
2
|
+
import "../../../core/src/index.js";
|
|
1
3
|
import { copyOfBytes } from "./bufferSources.js";
|
|
2
4
|
//#region ../../packages/adapters/mediabunny/src/decoderConfigurations.ts
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
+
* The sample entry types an HEVC track's codec string begins with (ISO/IEC 14496-15 §8.4.1,
|
|
7
|
+
* Annex E.3). An encrypted track's entry, `encv`, names its protection instead.
|
|
8
|
+
*/
|
|
9
|
+
var HEVC_SAMPLE_ENTRY_TYPES = /* @__PURE__ */ new Set(["hvc1", "hev1"]);
|
|
10
|
+
/**
|
|
11
|
+
* A video track's codec string: mediabunny's, but for an HEVC track, whose codec mediabunny
|
|
12
|
+
* names from the configuration's header alone, the core's, which reads a blank header's SPS.
|
|
13
|
+
*/
|
|
14
|
+
async function codecStringOf(track, config) {
|
|
15
|
+
const [codec, sampleEntryType] = await Promise.all([track.getCodec(), track.getInternalCodecId()]);
|
|
16
|
+
return codec === "hevc" && typeof sampleEntryType === "string" && HEVC_SAMPLE_ENTRY_TYPES.has(sampleEntryType) && config.description !== void 0 ? hevcCodecStringOf(sampleEntryType, copyOfBytes(config.description)) : config.codec;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A WebCodecs video decoder configuration in the core's terms; the codec string and the colour
|
|
20
|
+
* come from the track's description, the coded size from the track where the configuration
|
|
21
|
+
* leaves it out.
|
|
6
22
|
*/
|
|
7
23
|
function videoConfigurationOf(config, description) {
|
|
8
24
|
return {
|
|
9
|
-
codec:
|
|
25
|
+
codec: description.codec,
|
|
10
26
|
codedWidth: config.codedWidth ?? description.codedWidth,
|
|
11
27
|
codedHeight: config.codedHeight ?? description.codedHeight,
|
|
12
28
|
description: config.description === void 0 ? void 0 : copyOfBytes(config.description),
|
|
@@ -25,4 +41,4 @@ function audioConfigurationOf(config) {
|
|
|
25
41
|
};
|
|
26
42
|
}
|
|
27
43
|
//#endregion
|
|
28
|
-
export { audioConfigurationOf, videoConfigurationOf };
|
|
44
|
+
export { audioConfigurationOf, codecStringOf, videoConfigurationOf };
|
|
@@ -5,6 +5,7 @@ import { aspectOf } from "../../../core/src/domain/view/screenLayout.js";
|
|
|
5
5
|
import { DEFAULT_FRAMING } from "../../../core/src/domain/view/Framing.js";
|
|
6
6
|
import { viewModeRulesFor } from "../../../core/src/domain/view/viewModes.js";
|
|
7
7
|
import "../../../core/src/index.js";
|
|
8
|
+
import { DRAW_AT_ONCE } from "./drawSchedules.js";
|
|
8
9
|
import { MatrixCorrections } from "./matrixCorrections.js";
|
|
9
10
|
import { SAMPLING_STRATEGIES } from "./samplingStrategies.js";
|
|
10
11
|
import { applyLensGain, applyPicture, applyShaderSampling, applyStabilization, createRendererUniforms } from "./rendererUniforms.js";
|
|
@@ -58,7 +59,10 @@ var ThreeFrameRenderer = class {
|
|
|
58
59
|
ensureDrawable(setup);
|
|
59
60
|
const renderer = createRenderer(canvas, opening.options);
|
|
60
61
|
try {
|
|
61
|
-
return
|
|
62
|
+
return {
|
|
63
|
+
...assembleParts(renderer, setup, opening.pictures),
|
|
64
|
+
drawSchedule: opening.options.drawSchedule ?? DRAW_AT_ONCE
|
|
65
|
+
};
|
|
62
66
|
} catch (error) {
|
|
63
67
|
renderer.dispose();
|
|
64
68
|
throw error;
|
|
@@ -71,17 +75,17 @@ var ThreeFrameRenderer = class {
|
|
|
71
75
|
this.ensureLive();
|
|
72
76
|
this.framing = framing;
|
|
73
77
|
this.applyFraming();
|
|
74
|
-
this.
|
|
78
|
+
this.redraw();
|
|
75
79
|
}
|
|
76
80
|
setViewMode(mode) {
|
|
77
81
|
this.ensureLive();
|
|
78
82
|
this.viewMode = mode;
|
|
79
83
|
this.applyFraming();
|
|
80
|
-
this.
|
|
84
|
+
this.redraw();
|
|
81
85
|
}
|
|
82
86
|
/**
|
|
83
|
-
* The frames standing on screen are uploaded again with the new filters and mip chain
|
|
84
|
-
* change shows
|
|
87
|
+
* The frames standing on screen are uploaded again with the new filters and mip chain at the
|
|
88
|
+
* next draw, so the change shows even while paused.
|
|
85
89
|
*/
|
|
86
90
|
setQuality(quality) {
|
|
87
91
|
this.ensureLive();
|
|
@@ -91,12 +95,12 @@ var ThreeFrameRenderer = class {
|
|
|
91
95
|
if (this.hasFrames) texture.needsUpdate = true;
|
|
92
96
|
}
|
|
93
97
|
applyShaderSampling(this.parts.uniforms, strategy);
|
|
94
|
-
this.
|
|
98
|
+
this.redraw();
|
|
95
99
|
}
|
|
96
100
|
setLensGains(gains) {
|
|
97
101
|
this.ensureLive();
|
|
98
102
|
for (const [lensIndex, gain] of gains.entries()) applyLensGain(this.parts.uniforms, lensIndex, gain);
|
|
99
|
-
this.
|
|
103
|
+
this.redraw();
|
|
100
104
|
}
|
|
101
105
|
/**
|
|
102
106
|
* The meter leaves this renderer's list when disposed; the renderer disposes those still live
|
|
@@ -130,7 +134,7 @@ var ThreeFrameRenderer = class {
|
|
|
130
134
|
}
|
|
131
135
|
this.parts.matrixCorrections.follow(frames.map((frame) => frame.handle));
|
|
132
136
|
this.hasFrames = true;
|
|
133
|
-
this.
|
|
137
|
+
this.drawNow();
|
|
134
138
|
}
|
|
135
139
|
/**
|
|
136
140
|
* Matches the drawing buffer to a new element size, in device pixels.
|
|
@@ -139,11 +143,12 @@ var ThreeFrameRenderer = class {
|
|
|
139
143
|
this.ensureLive();
|
|
140
144
|
this.parts.renderer.setSize(size.width, size.height, false);
|
|
141
145
|
this.applyFraming();
|
|
142
|
-
this.
|
|
146
|
+
this.drawNow();
|
|
143
147
|
}
|
|
144
148
|
dispose() {
|
|
145
149
|
if (this.isDisposed) return;
|
|
146
150
|
this.isDisposed = true;
|
|
151
|
+
this.parts.drawSchedule.cancel();
|
|
147
152
|
this.canvas.removeEventListener("webglcontextrestored", this.onContextRestored);
|
|
148
153
|
for (const meter of this.meters) meter.dispose();
|
|
149
154
|
this.parts.seamProof.dispose();
|
|
@@ -176,9 +181,11 @@ var ThreeFrameRenderer = class {
|
|
|
176
181
|
this.meters.add(kept);
|
|
177
182
|
return kept;
|
|
178
183
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Draws the picture again for a changed setting, when the draw schedule says.
|
|
186
|
+
*/
|
|
187
|
+
redraw() {
|
|
188
|
+
this.parts.drawSchedule.request(this.draw);
|
|
182
189
|
}
|
|
183
190
|
ensureLive() {
|
|
184
191
|
ensureInvariant(!this.isDisposed, "the renderer has been disposed");
|
|
@@ -189,7 +196,19 @@ var ThreeFrameRenderer = class {
|
|
|
189
196
|
* uploads the frames on screen again.
|
|
190
197
|
*/
|
|
191
198
|
onContextRestored = () => {
|
|
192
|
-
this.
|
|
199
|
+
this.redraw();
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* Draws now what a draw asked for of the schedule would have drawn, which then has nothing left
|
|
203
|
+
* to draw.
|
|
204
|
+
*/
|
|
205
|
+
drawNow() {
|
|
206
|
+
this.parts.drawSchedule.cancel();
|
|
207
|
+
this.draw();
|
|
208
|
+
}
|
|
209
|
+
draw = () => {
|
|
210
|
+
if (!this.hasFrames) return;
|
|
211
|
+
this.parts.renderer.render(this.parts.scene, this.parts.camera);
|
|
193
212
|
};
|
|
194
213
|
/**
|
|
195
214
|
* Asks the view mode what to draw for the framing on this canvas, and switches to that
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region ../../packages/adapters/three/src/drawSchedules.ts
|
|
2
|
+
/**
|
|
3
|
+
* Draws every change at once, for whoever reads the canvas right after a change, as the tests
|
|
4
|
+
* and the measurements do.
|
|
5
|
+
*/
|
|
6
|
+
var DRAW_AT_ONCE = {
|
|
7
|
+
request: (draw) => {
|
|
8
|
+
draw();
|
|
9
|
+
},
|
|
10
|
+
cancel: () => {}
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { DRAW_AT_ONCE };
|
|
@@ -10,10 +10,12 @@ import { planDownloads } from "../../domain/download/planDownloads.js";
|
|
|
10
10
|
var EVERY_BYTE = ByteRange.of(0, Number.MAX_SAFE_INTEGER);
|
|
11
11
|
/**
|
|
12
12
|
* One file of a recording downloaded while it plays (ADR 0029): the only reader of its bytes.
|
|
13
|
-
* Its cursors say where the readers of its tracks stand
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Its cursors say where the readers of its tracks stand. It plans anew, once per turn, what to
|
|
14
|
+
* ask for, give up and let go of when a cursor opens, closes or waits for bytes no transfer
|
|
15
|
+
* brings, once the cursors have moved on by the policy's replan bytes (ADR 0036), when a range
|
|
16
|
+
* comes whole or fails, and when reading ahead starts; it hands each waiting cursor its sample
|
|
17
|
+
* once the bytes have come. A range that failed is not asked for again until a cursor opens, a
|
|
18
|
+
* seek or a replay, lest a failing server be asked without end.
|
|
17
19
|
*/
|
|
18
20
|
var FileDownload = class {
|
|
19
21
|
parts;
|
|
@@ -26,6 +28,10 @@ var FileDownload = class {
|
|
|
26
28
|
anchor;
|
|
27
29
|
isReadingAhead = false;
|
|
28
30
|
isPlanDue = false;
|
|
31
|
+
/**
|
|
32
|
+
* What the readers have taken since the last plan.
|
|
33
|
+
*/
|
|
34
|
+
takenSincePlan = 0;
|
|
29
35
|
isDisposed = false;
|
|
30
36
|
constructor(parts) {
|
|
31
37
|
this.parts = parts;
|
|
@@ -101,8 +107,12 @@ var FileDownload = class {
|
|
|
101
107
|
return {
|
|
102
108
|
bytesOf: (range) => this.store.bytesOf(range),
|
|
103
109
|
failureOf: (range) => this.failures.find((failed) => failed.range.overlaps(range))?.error,
|
|
104
|
-
|
|
105
|
-
this.
|
|
110
|
+
movedOn: (length) => {
|
|
111
|
+
this.takenSincePlan += length;
|
|
112
|
+
if (this.takenSincePlan >= this.parts.policy.replanBytes) this.schedulePlan();
|
|
113
|
+
},
|
|
114
|
+
waiting: (range) => {
|
|
115
|
+
if (!this.transfers.isBringing(range)) this.schedulePlan();
|
|
106
116
|
},
|
|
107
117
|
closed: (cursor) => {
|
|
108
118
|
this.cursors.delete(cursor);
|
|
@@ -141,6 +151,7 @@ var FileDownload = class {
|
|
|
141
151
|
for (const cursor of this.cursors) cursor.fail(EVERY_BYTE, error);
|
|
142
152
|
}
|
|
143
153
|
plan() {
|
|
154
|
+
this.takenSincePlan = 0;
|
|
144
155
|
const decisions = planDownloads({
|
|
145
156
|
cursors: this.positions(),
|
|
146
157
|
held: this.store.held,
|
|
@@ -37,7 +37,7 @@ var SampleCursor = class {
|
|
|
37
37
|
const bytes = this.host.bytesOf(range);
|
|
38
38
|
if (bytes) return Promise.resolve(this.take(bytes));
|
|
39
39
|
const failure = this.host.failureOf(range);
|
|
40
|
-
return failure === void 0 ? this.waitForBytes() : Promise.reject(failure);
|
|
40
|
+
return failure === void 0 ? this.waitForBytes(range) : Promise.reject(failure);
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
43
|
* Bytes came: hands the awaited sample on if they were its.
|
|
@@ -65,19 +65,19 @@ var SampleCursor = class {
|
|
|
65
65
|
this.waiter = void 0;
|
|
66
66
|
this.host.closed(this);
|
|
67
67
|
}
|
|
68
|
-
waitForBytes() {
|
|
68
|
+
waitForBytes(range) {
|
|
69
69
|
return new Promise((resolve, reject) => {
|
|
70
70
|
this.waiter = {
|
|
71
71
|
resolve,
|
|
72
72
|
reject
|
|
73
73
|
};
|
|
74
|
-
this.host.
|
|
74
|
+
this.host.waiting(range);
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
take(bytes) {
|
|
78
78
|
const sample = this.next;
|
|
79
79
|
this.next += 1;
|
|
80
|
-
this.host.
|
|
80
|
+
this.host.movedOn(bytes.byteLength);
|
|
81
81
|
return {
|
|
82
82
|
sample,
|
|
83
83
|
bytes
|
|
@@ -17,6 +17,13 @@ var Transfers = class {
|
|
|
17
17
|
remaining: remainingOf(transfer)
|
|
18
18
|
}));
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Whether every byte of `range` has come or is coming in one range still streaming.
|
|
22
|
+
*/
|
|
23
|
+
isBringing(range) {
|
|
24
|
+
for (const transfer of this.running.values()) if (transfer.block.range.contains(range)) return true;
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
20
27
|
start(range) {
|
|
21
28
|
const transfer = {
|
|
22
29
|
id: this.nextId,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { imuFrameFor } from "../../domain/motion/imu/ImuFrame.js";
|
|
2
|
+
import { mountingOf, uprightImuFrame } from "../../domain/motion/mounting/Mounting.js";
|
|
2
3
|
import { OrientationTrack } from "../../domain/motion/orientation/OrientationTrack.js";
|
|
3
4
|
//#region ../../packages/core/src/application/recording/motionOf.ts
|
|
4
5
|
var NO_GYRO_WARNING = "the recording has no gyro record; stabilization is unavailable";
|
|
@@ -14,15 +15,18 @@ async function motionOf(recording, clock) {
|
|
|
14
15
|
warnings: [NO_GYRO_WARNING]
|
|
15
16
|
};
|
|
16
17
|
const imuFrame = imuFrameFor(recording.info);
|
|
18
|
+
const mounting = mountingOf(gyro.track, imuFrame);
|
|
19
|
+
const frame = uprightImuFrame(imuFrame, mounting);
|
|
17
20
|
const orientations = OrientationTrack.integrate({
|
|
18
21
|
gyro: gyro.track,
|
|
19
22
|
clock,
|
|
20
|
-
frame
|
|
23
|
+
frame
|
|
21
24
|
});
|
|
22
25
|
const warnings = [...unverifiedWarningsOf(imuFrame, recording), ...damageWarningsOf(gyro)];
|
|
23
26
|
return {
|
|
24
27
|
setup: {
|
|
25
28
|
orientations,
|
|
29
|
+
mounting,
|
|
26
30
|
imuFrame
|
|
27
31
|
},
|
|
28
32
|
warnings
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rotationIntoBody } from "../../domain/motion/mounting/Mounting.js";
|
|
1
2
|
import { OffStabilization } from "../../domain/motion/stabilization/stabilizers.js";
|
|
2
3
|
//#region ../../packages/core/src/application/stabilization/StabilizingFrameSink.ts
|
|
3
4
|
/**
|
|
@@ -15,8 +16,9 @@ var StabilizingFrameSink = class {
|
|
|
15
16
|
}
|
|
16
17
|
present(presentation) {
|
|
17
18
|
const time = this.exposureTimeOf(presentation);
|
|
18
|
-
const
|
|
19
|
-
this.
|
|
19
|
+
const { orientations, mounting } = this.parts.motion;
|
|
20
|
+
const rotation = this.stabilizer.nextRotation(orientations.orientationAt(time), time);
|
|
21
|
+
this.parts.sink.setStabilization(rotationIntoBody(mounting, rotation));
|
|
20
22
|
this.parts.sink.present(presentation);
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
@@ -15,6 +15,13 @@ var REQUESTS_IN_FLIGHT = 2;
|
|
|
15
15
|
var BRIDGED_GAP_MEBIBYTES = 1;
|
|
16
16
|
var REFILL_FRACTION = .25;
|
|
17
17
|
/**
|
|
18
|
+
* Planning walks every sample in the window, a few thousand on a fast recording; made for each
|
|
19
|
+
* sample read, it took more of the main thread than drawing the frames (ADR 0036). Sixteen plans
|
|
20
|
+
* a refill keep a top-up within a sixteenth of a refill of falling due, and make about a dozen
|
|
21
|
+
* plans a second on the X5's recordings where there were 170.
|
|
22
|
+
*/
|
|
23
|
+
var REPLANS_PER_REFILL = 16;
|
|
24
|
+
/**
|
|
18
25
|
* Long enough that a link slower than the recording plays in stretches rather than frame by
|
|
19
26
|
* frame, short enough that the wait does not feel like a stop.
|
|
20
27
|
*/
|
|
@@ -25,6 +32,7 @@ var RESUME_SECONDS = 4;
|
|
|
25
32
|
function downloadPolicyFor(file, fileCount) {
|
|
26
33
|
const bytesPerSecond = file.size / file.duration;
|
|
27
34
|
const aheadBytes = Math.min(Math.round(AHEAD_SECONDS * bytesPerSecond), MOST_AHEAD_MEBIBYTES * MEBIBYTE / fileCount);
|
|
35
|
+
const refillBytes = Math.round(aheadBytes * REFILL_FRACTION);
|
|
28
36
|
return {
|
|
29
37
|
aheadSeconds: seconds(AHEAD_SECONDS),
|
|
30
38
|
aheadBytes,
|
|
@@ -33,7 +41,8 @@ function downloadPolicyFor(file, fileCount) {
|
|
|
33
41
|
requestSize: REQUEST_MEBIBYTES * MEBIBYTE,
|
|
34
42
|
requestsInFlight: REQUESTS_IN_FLIGHT,
|
|
35
43
|
bridgedGap: BRIDGED_GAP_MEBIBYTES * MEBIBYTE,
|
|
36
|
-
refillBytes
|
|
44
|
+
refillBytes,
|
|
45
|
+
replanBytes: Math.round(refillBytes / REPLANS_PER_REFILL),
|
|
37
46
|
resumeSeconds: seconds(RESUME_SECONDS)
|
|
38
47
|
};
|
|
39
48
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { hasErrorCode } from "../../../shared/errors/GyroViewError.js";
|
|
2
|
+
import { ByteReader } from "../../../shared/binary/ByteReader.js";
|
|
3
|
+
//#region ../../packages/core/src/domain/format/mp4/hevcCodecString.ts
|
|
4
|
+
/**
|
|
5
|
+
* The general part of profile_tier_level (ITU-T H.265 §7.3.3), as an SPS carries it and an HEVC
|
|
6
|
+
* configuration's header repeats it: two bits of profile space, the tier flag and five bits of
|
|
7
|
+
* profile; 32 compatibility flags; 48 bits of constraint flags; the level.
|
|
8
|
+
*/
|
|
9
|
+
var PROFILE_SPACE_SHIFT = 6;
|
|
10
|
+
var HIGH_TIER_FLAG = 32;
|
|
11
|
+
var PROFILE_MASK = 31;
|
|
12
|
+
var COMPATIBILITY_FLAGS_OFFSET = 1;
|
|
13
|
+
var CONSTRAINT_FLAGS_OFFSET = 5;
|
|
14
|
+
var CONSTRAINT_FLAGS_LENGTH = 6;
|
|
15
|
+
var LEVEL_OFFSET = 11;
|
|
16
|
+
var COMPATIBILITY_FLAG_COUNT = 32;
|
|
17
|
+
/**
|
|
18
|
+
* No profile has the number zero (ITU-T H.265 Annex A): a header naming it was left blank.
|
|
19
|
+
*/
|
|
20
|
+
var NO_PROFILE = 0;
|
|
21
|
+
/**
|
|
22
|
+
* An SPS (NAL unit type 33, ITU-T H.265 Table 7-1) opens with its two-byte NAL unit header and
|
|
23
|
+
* a byte of parameter-set id, sub-layer count and nesting flag; its profile_tier_level follows
|
|
24
|
+
* (§7.3.2.2.1).
|
|
25
|
+
*/
|
|
26
|
+
var SPS_NAL_UNIT_TYPE = 33;
|
|
27
|
+
var SPS_PROFILE_TIER_LEVEL_OFFSET = 3;
|
|
28
|
+
/**
|
|
29
|
+
* Inside a NAL unit, the encoder writes an emulation_prevention_three_byte after two zero bytes
|
|
30
|
+
* that a byte of 0 to 3 would follow, and the decoder drops it (ITU-T H.265 §7.4.2). The SPS's
|
|
31
|
+
* flags are mostly zeros, so they hold such bytes, and are read with them dropped.
|
|
32
|
+
*/
|
|
33
|
+
var EMULATION_PREVENTION_BYTE = 3;
|
|
34
|
+
var ZEROS_BEFORE_EMULATION_PREVENTION = 2;
|
|
35
|
+
/**
|
|
36
|
+
* ISO/IEC 14496-15 Annex E.3: the profile space as no letter or A to C, the tier as L or H, the
|
|
37
|
+
* flags in hexadecimal.
|
|
38
|
+
*/
|
|
39
|
+
var PROFILE_SPACE_CODES = [
|
|
40
|
+
"",
|
|
41
|
+
"A",
|
|
42
|
+
"B",
|
|
43
|
+
"C"
|
|
44
|
+
];
|
|
45
|
+
var MAIN_TIER_CODE = "L";
|
|
46
|
+
var HIGH_TIER_CODE = "H";
|
|
47
|
+
var HEXADECIMAL = 16;
|
|
48
|
+
/**
|
|
49
|
+
* The WebCodecs codec string of an HEVC track (ISO/IEC 14496-15 Annex E.3), from its sample
|
|
50
|
+
* entry type and its configuration (the `hvcC` box's payload). The configuration's header is
|
|
51
|
+
* meant to repeat the profile, tier and level of the stream's SPS; where it names no profile, as
|
|
52
|
+
* the Antigravity A1 leaves it, the SPS the configuration carries is read instead, since browsers
|
|
53
|
+
* refuse a codec string without a profile although they decode the stream.
|
|
54
|
+
*/
|
|
55
|
+
function hevcCodecStringOf(sampleEntryType, configuration) {
|
|
56
|
+
return codecStringOf(sampleEntryType, declaredProfileTierLevelOf(configuration));
|
|
57
|
+
}
|
|
58
|
+
function declaredProfileTierLevelOf(configuration) {
|
|
59
|
+
const header = profileTierLevelAt(new ByteReader(configuration), 1);
|
|
60
|
+
return header.profile === NO_PROFILE ? spsProfileTierLevelIn(configuration) ?? header : header;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The profile, tier and level of the first SPS the configuration carries; undefined where it
|
|
64
|
+
* carries none, or where its parameter sets run past its end. The header's blank string then
|
|
65
|
+
* stands, which browsers refuse as a codec they do not support.
|
|
66
|
+
*/
|
|
67
|
+
function spsProfileTierLevelIn(configuration) {
|
|
68
|
+
try {
|
|
69
|
+
const sps = spsIn(configuration);
|
|
70
|
+
return sps === void 0 ? void 0 : spsProfileTierLevelOf(sps);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (hasErrorCode(error, "binary-out-of-bounds")) return void 0;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function profileTierLevelAt(reader, offset) {
|
|
77
|
+
const first = reader.uint8At(offset);
|
|
78
|
+
return {
|
|
79
|
+
profileSpace: first >> PROFILE_SPACE_SHIFT,
|
|
80
|
+
isHighTier: (first & HIGH_TIER_FLAG) !== 0,
|
|
81
|
+
profile: first & PROFILE_MASK,
|
|
82
|
+
compatibilityFlags: reader.uint32BeAt(offset + COMPATIBILITY_FLAGS_OFFSET),
|
|
83
|
+
constraintFlags: [...reader.bytesAt(offset + CONSTRAINT_FLAGS_OFFSET, CONSTRAINT_FLAGS_LENGTH)],
|
|
84
|
+
level: reader.uint8At(offset + LEVEL_OFFSET)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function spsProfileTierLevelOf(sps) {
|
|
88
|
+
return profileTierLevelAt(new ByteReader(withoutEmulationPrevention(sps)), SPS_PROFILE_TIER_LEVEL_OFFSET);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The first SPS among the configuration's parameter sets; undefined where it carries none, as an
|
|
92
|
+
* `hev1` track may, keeping its parameter sets in the stream.
|
|
93
|
+
*/
|
|
94
|
+
function spsIn(configuration) {
|
|
95
|
+
const reader = new ByteReader(configuration);
|
|
96
|
+
let offset = 23;
|
|
97
|
+
for (let index = 0; index < reader.uint8At(22); index += 1) {
|
|
98
|
+
const array = parameterSetArrayAt(reader, offset);
|
|
99
|
+
const [sps] = array.nalUnitType === SPS_NAL_UNIT_TYPE ? array.nalUnits : [];
|
|
100
|
+
if (sps !== void 0) return sps;
|
|
101
|
+
offset = array.end;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function parameterSetArrayAt(reader, offset) {
|
|
105
|
+
const nalUnitType = reader.uint8At(offset) & 63;
|
|
106
|
+
const count = reader.uint16BeAt(offset + 1);
|
|
107
|
+
const nalUnits = [];
|
|
108
|
+
let unitOffset = offset + 3;
|
|
109
|
+
for (let index = 0; index < count; index += 1) {
|
|
110
|
+
const length = reader.uint16BeAt(unitOffset);
|
|
111
|
+
nalUnits.push(reader.bytesAt(unitOffset + 2, length));
|
|
112
|
+
unitOffset += 2 + length;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
nalUnitType,
|
|
116
|
+
nalUnits,
|
|
117
|
+
end: unitOffset
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* A NAL unit as the decoder reads it, its emulation prevention bytes dropped.
|
|
122
|
+
*/
|
|
123
|
+
function withoutEmulationPrevention(nalUnit) {
|
|
124
|
+
const kept = [];
|
|
125
|
+
let zeros = 0;
|
|
126
|
+
for (const byte of nalUnit) {
|
|
127
|
+
if (!(zeros >= ZEROS_BEFORE_EMULATION_PREVENTION && byte === EMULATION_PREVENTION_BYTE)) kept.push(byte);
|
|
128
|
+
zeros = byte === 0 ? zeros + 1 : 0;
|
|
129
|
+
}
|
|
130
|
+
return Uint8Array.from(kept);
|
|
131
|
+
}
|
|
132
|
+
function codecStringOf(sampleEntryType, declared) {
|
|
133
|
+
const profile = `${PROFILE_SPACE_CODES[declared.profileSpace] ?? ""}${declared.profile}`;
|
|
134
|
+
const tier = declared.isHighTier ? HIGH_TIER_CODE : MAIN_TIER_CODE;
|
|
135
|
+
const constraints = withoutTrailingZeros(declared.constraintFlags).map((byte) => hexadecimalOf(byte));
|
|
136
|
+
return [
|
|
137
|
+
sampleEntryType,
|
|
138
|
+
profile,
|
|
139
|
+
hexadecimalOf(reversedBitsOf(declared.compatibilityFlags)),
|
|
140
|
+
`${tier}${declared.level}`,
|
|
141
|
+
...constraints
|
|
142
|
+
].join(".");
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The compatibility flags as Annex E.3 writes them: flag 0, the configuration's most significant
|
|
146
|
+
* bit, becomes the least significant.
|
|
147
|
+
*/
|
|
148
|
+
function reversedBitsOf(flags) {
|
|
149
|
+
let reversed = 0;
|
|
150
|
+
for (let bit = 0; bit < COMPATIBILITY_FLAG_COUNT; bit += 1) reversed = reversed << 1 | flags >>> bit & 1;
|
|
151
|
+
return reversed >>> 0;
|
|
152
|
+
}
|
|
153
|
+
function withoutTrailingZeros(bytes) {
|
|
154
|
+
const lastSet = bytes.findLastIndex((byte) => byte !== 0);
|
|
155
|
+
return bytes.slice(0, lastSet + 1);
|
|
156
|
+
}
|
|
157
|
+
function hexadecimalOf(value) {
|
|
158
|
+
return value.toString(HEXADECIMAL).toUpperCase();
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
161
|
+
export { hevcCodecStringOf };
|
|
@@ -19,10 +19,17 @@ var SampleColumns = class {
|
|
|
19
19
|
this.accelerations = new Float32Array(capacity * 3);
|
|
20
20
|
this.angularVelocities = new Float32Array(capacity * 3);
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Component by component: `set` from an array of three takes a generic path several times
|
|
24
|
+
* slower, once per sample of an hour-long record.
|
|
25
|
+
*/
|
|
22
26
|
push(sample) {
|
|
27
|
+
const base = this.length * 3;
|
|
23
28
|
this.recordedTimes[this.length] = sample.captureTime;
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
30
|
+
this.accelerations[base + axis] = sample.acceleration[axis] ?? NaN;
|
|
31
|
+
this.angularVelocities[base + axis] = sample.angularVelocity[axis] ?? NaN;
|
|
32
|
+
}
|
|
26
33
|
this.length += 1;
|
|
27
34
|
}
|
|
28
35
|
/**
|