@bubo-squared/gyroview 0.4.0 → 0.4.2
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 +27 -0
- package/README.md +3 -3
- package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +8 -2
- 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/domain/download/DownloadPolicy.js +10 -1
- 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 +16 -3
- package/dist/packages/core/src/domain/motion/orientation/OrientationTrack.js +2 -2
- package/dist/packages/core/src/domain/motion/orientation/estimateGyroBias.js +9 -1
- package/dist/packages/core/src/shared/binary/ByteReader.js +6 -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/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 +4 -2
- 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,33 @@
|
|
|
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.4.2 (2026-10-02)
|
|
7
|
+
|
|
8
|
+
What a page may notice:
|
|
9
|
+
|
|
10
|
+
- Dragging, pinching or turning the wheel over a playing recording draws the picture once per
|
|
11
|
+
animation frame: in Safari, a fast mouse no longer slows the recording down. A change of the
|
|
12
|
+
view, the view mode, the quality or the exposure matching shows at the next animation frame
|
|
13
|
+
rather than at the call (ADR 0035).
|
|
14
|
+
- Opening a recording keeps the page busy for less time: its gyro is read and integrated in two
|
|
15
|
+
thirds of the time.
|
|
16
|
+
- Playing takes less of the page's main thread: the download plans what to fetch every few
|
|
17
|
+
mebibytes rather than at every frame (ADR 0036).
|
|
18
|
+
- A bundle that includes the package is smaller: mediabunny's demuxers for formats other than
|
|
19
|
+
MP4 and QuickTime are left out. The standalone module is about 40 KB smaller compressed.
|
|
20
|
+
|
|
21
|
+
## 0.4.1 (2026-10-01)
|
|
22
|
+
|
|
23
|
+
What a page may notice:
|
|
24
|
+
|
|
25
|
+
- An Insta360 X4 Air recording is stabilized about the right axes: its IMU frame is measured
|
|
26
|
+
(ADR 0009) where it was assumed. The `ready` event's `imuFrame` is
|
|
27
|
+
`{ name: 'X4 Air', isVerified: true }` where it was the unverified aligned frame, and the load
|
|
28
|
+
no longer warns (`recording-degraded`) that the IMU frame has not been verified on a recording.
|
|
29
|
+
- A camera is given a measured IMU frame only by its whole model name: a model whose name merely
|
|
30
|
+
begins with a measured camera's, such as a later "X5 Pro", gets the unverified frame and its
|
|
31
|
+
warning until a recording of it is measured.
|
|
32
|
+
|
|
6
33
|
## 0.4.0 (2026-10-01)
|
|
7
34
|
|
|
8
35
|
New:
|
package/README.md
CHANGED
|
@@ -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.4/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.4/dist/standalone.js';
|
|
52
52
|
</script>
|
|
53
53
|
```
|
|
54
54
|
|
|
@@ -2,9 +2,15 @@ import { GyroViewError, isAbortError } from "../../../core/src/shared/errors/Gyr
|
|
|
2
2
|
import "../../../core/src/index.js";
|
|
3
3
|
import { audioConfigurationOf, 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 {
|
|
@@ -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,
|
|
@@ -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
|
}
|
|
@@ -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
|
/**
|
|
@@ -38,14 +38,24 @@ var GyroTrack = class {
|
|
|
38
38
|
}
|
|
39
39
|
sampleAt(index) {
|
|
40
40
|
ensureIndexInRange(index, this.length, "gyro sample");
|
|
41
|
-
const base = index * 3;
|
|
42
41
|
return {
|
|
43
42
|
captureTime: microseconds(this.captureTimeStore[index] ?? 0),
|
|
44
|
-
acceleration: this.vectorAt(this.accelerationStore,
|
|
45
|
-
angularVelocity: this.vectorAt(this.angularVelocityStore,
|
|
43
|
+
acceleration: this.vectorAt(this.accelerationStore, index),
|
|
44
|
+
angularVelocity: this.vectorAt(this.angularVelocityStore, index)
|
|
46
45
|
};
|
|
47
46
|
}
|
|
48
|
-
|
|
47
|
+
/**
|
|
48
|
+
* A sample's angular velocity alone, for the loops over every sample that need nothing else.
|
|
49
|
+
*/
|
|
50
|
+
angularVelocityAt(index) {
|
|
51
|
+
ensureIndexInRange(index, this.length, "gyro sample");
|
|
52
|
+
return this.vectorAt(this.angularVelocityStore, index);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The vector at an index already checked.
|
|
56
|
+
*/
|
|
57
|
+
vectorAt(store, index) {
|
|
58
|
+
const base = index * 3;
|
|
49
59
|
return [
|
|
50
60
|
store[base] ?? NaN,
|
|
51
61
|
store[base + 1] ?? NaN,
|
|
@@ -102,6 +102,11 @@ var X5_IMU_FRAME = measuredImuFrame("X5", QUARTER_TURN_ABOUT_LATERAL);
|
|
|
102
102
|
*/
|
|
103
103
|
var X6_IMU_FRAME = measuredImuFrame("X6", QUARTER_TURN_ABOUT_LATERAL);
|
|
104
104
|
/**
|
|
105
|
+
* Measured on one X4 Air recording (ADR 0009): the X5's arrangement again, which the ranking puts
|
|
106
|
+
* first, the aligned frame third.
|
|
107
|
+
*/
|
|
108
|
+
var X4_AIR_IMU_FRAME = measuredImuFrame("X4 Air", QUARTER_TURN_ABOUT_LATERAL);
|
|
109
|
+
/**
|
|
105
110
|
* Until a recording proves otherwise, the IMU is assumed aligned with the body.
|
|
106
111
|
*/
|
|
107
112
|
var ALIGNED_IMU_FRAME = assumedImuFrame("aligned (unverified)", [
|
|
@@ -109,13 +114,21 @@ var ALIGNED_IMU_FRAME = assumedImuFrame("aligned (unverified)", [
|
|
|
109
114
|
"y",
|
|
110
115
|
"z"
|
|
111
116
|
]);
|
|
112
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Keyed by the whole model name: a name that extends a measured one ("X4 Air" of "X4", an "X5
|
|
119
|
+
* Pro") is another camera, unmeasured until a recording of it is ranked.
|
|
120
|
+
*/
|
|
121
|
+
var IMU_FRAMES_BY_MODEL = /* @__PURE__ */ new Map([
|
|
122
|
+
["Insta360 X4 Air", X4_AIR_IMU_FRAME],
|
|
123
|
+
["Insta360 X5", X5_IMU_FRAME],
|
|
124
|
+
["Insta360 X6", X6_IMU_FRAME]
|
|
125
|
+
]);
|
|
113
126
|
/**
|
|
114
127
|
* The IMU frame for a recording, chosen from the camera model the file names. A starting point
|
|
115
128
|
* the recording's own data can be checked against with the ranking test, never the last word.
|
|
116
129
|
*/
|
|
117
130
|
function imuFrameFor(hints) {
|
|
118
|
-
return IMU_FRAMES_BY_MODEL.
|
|
131
|
+
return IMU_FRAMES_BY_MODEL.get(hints.model?.trim() ?? "") ?? ALIGNED_IMU_FRAME;
|
|
119
132
|
}
|
|
120
133
|
//#endregion
|
|
121
|
-
export { ALIGNED_IMU_FRAME, X5_IMU_FRAME, X6_IMU_FRAME, assumedImuFrame, imuFrameFor, isProperRotation, toBodyFrame };
|
|
134
|
+
export { ALIGNED_IMU_FRAME, X4_AIR_IMU_FRAME, X5_IMU_FRAME, X6_IMU_FRAME, assumedImuFrame, imuFrameFor, isProperRotation, toBodyFrame };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ensureInvariant } from "../../../shared/errors/GyroViewError.js";
|
|
2
|
-
import { seconds } from "../../../shared/units/time.js";
|
|
2
|
+
import { microseconds, seconds } from "../../../shared/units/time.js";
|
|
3
3
|
import { ZERO_VECTOR3, addVectors, isFiniteVector, scaleVector, subtractVectors } from "../../../shared/math/Vector3.js";
|
|
4
4
|
import { clamp } from "../../../shared/math/clamp.js";
|
|
5
5
|
import { toBodyFrame } from "../imu/ImuFrame.js";
|
|
@@ -107,7 +107,7 @@ var OrientationTrack = class OrientationTrack {
|
|
|
107
107
|
* lookup's binary search holds.
|
|
108
108
|
*/
|
|
109
109
|
function sampleVideoTimes(gyro, clock) {
|
|
110
|
-
return
|
|
110
|
+
return gyro.captureTimes.map((captureTime) => clock.gyroVideoTimeOf(microseconds(captureTime)));
|
|
111
111
|
}
|
|
112
112
|
function finiteSample(gyro, index) {
|
|
113
113
|
const sample = gyro.sampleAt(index);
|
|
@@ -18,7 +18,7 @@ var FALLBACK_INTERVAL = secondsToMicroseconds(seconds(1));
|
|
|
18
18
|
function stillestWindow(gyro, frame, length) {
|
|
19
19
|
const interval = microsecondsToSeconds(gyro.meanSampleInterval ?? FALLBACK_INTERVAL);
|
|
20
20
|
const size = Math.max(1, Math.min(gyro.length, Math.round(length / interval)));
|
|
21
|
-
const magnitudes =
|
|
21
|
+
const magnitudes = rateMagnitudesOf(gyro, frame);
|
|
22
22
|
let windowSum = 0;
|
|
23
23
|
for (let index = 0; index < size; index += 1) windowSum += magnitudes[index] ?? 0;
|
|
24
24
|
let best = {
|
|
@@ -38,6 +38,14 @@ function stillestWindow(gyro, frame, length) {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
|
+
* How fast the body turns at each sample, in radians per second.
|
|
42
|
+
*/
|
|
43
|
+
function rateMagnitudesOf(gyro, frame) {
|
|
44
|
+
const magnitudes = new Float64Array(gyro.length);
|
|
45
|
+
for (let index = 0; index < gyro.length; index += 1) magnitudes[index] = magnitudeOf(toBodyFrame(frame, gyro.angularVelocityAt(index)));
|
|
46
|
+
return magnitudes;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
41
49
|
* The mean of one vector quantity over a window of samples, in the body frame.
|
|
42
50
|
*/
|
|
43
51
|
function meanOverWindow({ gyro, frame }, window, quantity) {
|
|
@@ -10,6 +10,11 @@ var FLOAT64_SIZE = 8;
|
|
|
10
10
|
var IS_LITTLE_ENDIAN = true;
|
|
11
11
|
var IS_BIG_ENDIAN = false;
|
|
12
12
|
/**
|
|
13
|
+
* The safe integer range as big integers, made once: a gyro record reads a stamp per sample.
|
|
14
|
+
*/
|
|
15
|
+
var MIN_SAFE_BIGINT = BigInt(Number.MIN_SAFE_INTEGER);
|
|
16
|
+
var MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
|
|
17
|
+
/**
|
|
13
18
|
* Bounds-checked reads over a byte array: little-endian for Insta360's records, big-endian for
|
|
14
19
|
* ISO BMFF boxes. Every read is positional, so callers express the layouts they parse as named
|
|
15
20
|
* offsets rather than as a moving cursor.
|
|
@@ -84,7 +89,7 @@ var ByteReader = class {
|
|
|
84
89
|
return Array.from(this.bytesAt(offset, length), (byte) => String.fromCodePoint(byte)).join("");
|
|
85
90
|
}
|
|
86
91
|
toSafeNumber(value, offset) {
|
|
87
|
-
if (!(value >=
|
|
92
|
+
if (!(value >= MIN_SAFE_BIGINT && value <= MAX_SAFE_BIGINT)) throw new GyroViewError("binary-unsafe-integer", `64-bit integer at offset ${offset} (${value.toString()}) exceeds the safe integer range`);
|
|
88
93
|
return Number(value);
|
|
89
94
|
}
|
|
90
95
|
ensureAvailable(offset, length) {
|
|
@@ -62,7 +62,7 @@ function conjugateQuaternion(q) {
|
|
|
62
62
|
}
|
|
63
63
|
function normalizeQuaternion(q) {
|
|
64
64
|
const [x, y, z, w] = q;
|
|
65
|
-
const length = Math.
|
|
65
|
+
const length = Math.sqrt(x * x + y * y + z * z + w * w);
|
|
66
66
|
return length === 0 ? IDENTITY_QUATERNION : [
|
|
67
67
|
x / length,
|
|
68
68
|
y / length,
|
|
@@ -4,8 +4,12 @@ var ZERO_VECTOR3 = [
|
|
|
4
4
|
0,
|
|
5
5
|
0
|
|
6
6
|
];
|
|
7
|
+
/**
|
|
8
|
+
* The square root of the squares, not `Math.hypot`: several times slower in V8, it guards against
|
|
9
|
+
* overflow no reading or direction here comes near, and gyro integration takes a million of these.
|
|
10
|
+
*/
|
|
7
11
|
function magnitudeOf(vector) {
|
|
8
|
-
return Math.
|
|
12
|
+
return Math.sqrt(dotProduct(vector, vector));
|
|
9
13
|
}
|
|
10
14
|
function dotProduct(a, b) {
|
|
11
15
|
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ANIMATION_FRAMES } from "./animationFrames.js";
|
|
2
|
+
//#region ../../packages/player/src/composition/AnimationFrameDraws.ts
|
|
3
|
+
/**
|
|
4
|
+
* Draws a renderer's changed settings once at the next animation frame, however many changes
|
|
5
|
+
* come before it (ADR 0035): the screen shows one picture a frame, and WebKit hands the page
|
|
6
|
+
* every pointer move of a drag, thousands a second from a fast mouse, which drawn one by one
|
|
7
|
+
* leave no time for the frames of the recording. One schedule serves one renderer.
|
|
8
|
+
*/
|
|
9
|
+
var AnimationFrameDraws = class {
|
|
10
|
+
frames;
|
|
11
|
+
pendingDraw;
|
|
12
|
+
handle;
|
|
13
|
+
constructor(frames = ANIMATION_FRAMES) {
|
|
14
|
+
this.frames = frames;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The latest draw asked for is the one drawn.
|
|
18
|
+
*/
|
|
19
|
+
request(draw) {
|
|
20
|
+
const isFrameRequested = this.pendingDraw !== void 0;
|
|
21
|
+
this.pendingDraw = draw;
|
|
22
|
+
if (!isFrameRequested) this.handle = this.frames.request(this.drawPending);
|
|
23
|
+
}
|
|
24
|
+
cancel() {
|
|
25
|
+
this.pendingDraw = void 0;
|
|
26
|
+
if (this.handle === void 0) return;
|
|
27
|
+
this.frames.cancel(this.handle);
|
|
28
|
+
this.handle = void 0;
|
|
29
|
+
}
|
|
30
|
+
drawPending = () => {
|
|
31
|
+
const draw = this.pendingDraw;
|
|
32
|
+
this.pendingDraw = void 0;
|
|
33
|
+
this.handle = void 0;
|
|
34
|
+
draw?.();
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
export { AnimationFrameDraws };
|
|
@@ -12,6 +12,7 @@ import { MediaSourceAudioClock } from "../../../adapters/mse-audio/src/MediaSour
|
|
|
12
12
|
import "../../../adapters/mse-audio/src/index.js";
|
|
13
13
|
import { ThreeFrameRenderer } from "../../../adapters/three/src/ThreeFrameRenderer.js";
|
|
14
14
|
import "../../../adapters/three/src/index.js";
|
|
15
|
+
import { AnimationFrameDraws } from "./AnimationFrameDraws.js";
|
|
15
16
|
import { Disposables } from "./Disposables.js";
|
|
16
17
|
var DECODE_PIPELINE_OPTIONS = {
|
|
17
18
|
maxPendingPackets: 4,
|
|
@@ -53,10 +54,11 @@ async function buildPipeline(parts) {
|
|
|
53
54
|
}
|
|
54
55
|
/**
|
|
55
56
|
* The GPU stitcher on the host canvas, behind the stabilizing sink when there is a gyro, behind
|
|
56
|
-
* gain matching, which measures what the stitcher drew.
|
|
57
|
+
* gain matching, which measures what the stitcher drew. A change of the view or the gains is
|
|
58
|
+
* drawn once at the next animation frame, where a frame presented in it draws it anyway.
|
|
57
59
|
*/
|
|
58
60
|
function drawingFor(parts, disposables) {
|
|
59
|
-
const renderer = ThreeFrameRenderer.create(parts.host.canvas, stitchingSetupOf(parts.opened));
|
|
61
|
+
const renderer = ThreeFrameRenderer.create(parts.host.canvas, stitchingSetupOf(parts.opened), { drawSchedule: new AnimationFrameDraws() });
|
|
60
62
|
disposables.add(() => {
|
|
61
63
|
renderer.dispose();
|
|
62
64
|
});
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
+
import { ANIMATION_FRAMES } from "../composition/animationFrames.js";
|
|
1
2
|
//#region ../../packages/player/src/player/FrameLoop.ts
|
|
2
|
-
var ANIMATION_FRAMES = {
|
|
3
|
-
request: (callback) => requestAnimationFrame(callback),
|
|
4
|
-
cancel: (handle) => {
|
|
5
|
-
cancelAnimationFrame(handle);
|
|
6
|
-
}
|
|
7
|
-
};
|
|
8
3
|
/**
|
|
9
4
|
* Calls `tick` once per animation frame between {@link start} and {@link stop}.
|
|
10
5
|
*/
|