@bubo-squared/gyroview 0.4.2 → 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 +32 -0
- package/README.md +6 -6
- package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +2 -2
- package/dist/packages/adapters/mediabunny/src/decoderConfigurations.js +20 -4
- 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/format/mp4/hevcCodecString.js +161 -0
- 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 +4 -2
- 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 +4 -0
- 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/minimum.js +17 -0
- package/dist/packages/player/src/composition/buildPipeline.js +9 -5
- package/dist/standalone.js +58 -58
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,38 @@
|
|
|
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
|
+
|
|
6
38
|
## 0.4.2 (2026-10-02)
|
|
7
39
|
|
|
8
40
|
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.
|
|
@@ -45,10 +45,10 @@ A page without a bundler loads the standalone file, which has Three.js and media
|
|
|
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,6 +1,6 @@
|
|
|
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
5
|
import { BufferSource, Input, MP4, QTFF } from "mediabunny";
|
|
6
6
|
//#region ../../packages/adapters/mediabunny/src/MediabunnyCodecReader.ts
|
|
@@ -46,7 +46,7 @@ async function videoCodecOf(track, trackIndex) {
|
|
|
46
46
|
trackIndex,
|
|
47
47
|
codedWidth,
|
|
48
48
|
codedHeight,
|
|
49
|
-
codec: config
|
|
49
|
+
codec: await codecStringOf(track, config),
|
|
50
50
|
colour: trackColourOf(config.colorSpace)
|
|
51
51
|
};
|
|
52
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 };
|
|
@@ -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
|
/**
|
|
@@ -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 };
|
|
@@ -107,6 +107,34 @@ var X6_IMU_FRAME = measuredImuFrame("X6", QUARTER_TURN_ABOUT_LATERAL);
|
|
|
107
107
|
*/
|
|
108
108
|
var X4_AIR_IMU_FRAME = measuredImuFrame("X4 Air", QUARTER_TURN_ABOUT_LATERAL);
|
|
109
109
|
/**
|
|
110
|
+
* Measured on one Antigravity A1 recording (ADR 0009): the drone's lens axis is its vertical, the
|
|
111
|
+
* IMU's y, lens 0 facing up; the ranking puts this quarter turn about that axis first, the X5's
|
|
112
|
+
* arrangement fourth.
|
|
113
|
+
*/
|
|
114
|
+
var A1_IMU_FRAME = measuredImuFrame("A1", [
|
|
115
|
+
"-z",
|
|
116
|
+
"-x",
|
|
117
|
+
"y"
|
|
118
|
+
]);
|
|
119
|
+
/**
|
|
120
|
+
* Measured on one X3 recording (ADR 0009): the X5's arrangement again. The camera stood still on
|
|
121
|
+
* its side, so the levelling ranking decided it rather than the stillness ranking: it alone of
|
|
122
|
+
* the four that put gravity on the body's axis stands the picture as level as Studio's.
|
|
123
|
+
*/
|
|
124
|
+
var X3_IMU_FRAME = measuredImuFrame("X3", QUARTER_TURN_ABOUT_LATERAL);
|
|
125
|
+
/**
|
|
126
|
+
* Assumed for the ONE RS until a recording that turns is ranked (ADR 0009). On one tripod
|
|
127
|
+
* recording of the 1-inch 360 edition, standing upright, gravity lies along the IMU's x, so the
|
|
128
|
+
* body's down is the IMU's x; of the four arrangements that agree, the levelling ranking puts
|
|
129
|
+
* this quarter turn about the lens axis first in both browsers, by too little to decide it. The
|
|
130
|
+
* model name covers the 360 lens module too, assumed alike.
|
|
131
|
+
*/
|
|
132
|
+
var ONE_RS_IMU_FRAME = assumedImuFrame("ONE RS (unverified)", [
|
|
133
|
+
"-y",
|
|
134
|
+
"x",
|
|
135
|
+
"z"
|
|
136
|
+
]);
|
|
137
|
+
/**
|
|
110
138
|
* Until a recording proves otherwise, the IMU is assumed aligned with the body.
|
|
111
139
|
*/
|
|
112
140
|
var ALIGNED_IMU_FRAME = assumedImuFrame("aligned (unverified)", [
|
|
@@ -116,12 +144,16 @@ var ALIGNED_IMU_FRAME = assumedImuFrame("aligned (unverified)", [
|
|
|
116
144
|
]);
|
|
117
145
|
/**
|
|
118
146
|
* 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.
|
|
147
|
+
* Pro") is another camera, unmeasured until a recording of it is ranked. Every frame is measured
|
|
148
|
+
* but the ONE RS's, assumed and reported as such.
|
|
120
149
|
*/
|
|
121
150
|
var IMU_FRAMES_BY_MODEL = /* @__PURE__ */ new Map([
|
|
151
|
+
["Antigravity A1", A1_IMU_FRAME],
|
|
152
|
+
["Insta360 X3", X3_IMU_FRAME],
|
|
122
153
|
["Insta360 X4 Air", X4_AIR_IMU_FRAME],
|
|
123
154
|
["Insta360 X5", X5_IMU_FRAME],
|
|
124
|
-
["Insta360 X6", X6_IMU_FRAME]
|
|
155
|
+
["Insta360 X6", X6_IMU_FRAME],
|
|
156
|
+
["Insta360 OneRS", ONE_RS_IMU_FRAME]
|
|
125
157
|
]);
|
|
126
158
|
/**
|
|
127
159
|
* The IMU frame for a recording, chosen from the camera model the file names. A starting point
|
|
@@ -131,4 +163,4 @@ function imuFrameFor(hints) {
|
|
|
131
163
|
return IMU_FRAMES_BY_MODEL.get(hints.model?.trim() ?? "") ?? ALIGNED_IMU_FRAME;
|
|
132
164
|
}
|
|
133
165
|
//#endregion
|
|
134
|
-
export { ALIGNED_IMU_FRAME, X4_AIR_IMU_FRAME, X5_IMU_FRAME, X6_IMU_FRAME, assumedImuFrame, imuFrameFor, isProperRotation, toBodyFrame };
|
|
166
|
+
export { A1_IMU_FRAME, ALIGNED_IMU_FRAME, ONE_RS_IMU_FRAME, X3_IMU_FRAME, X4_AIR_IMU_FRAME, X5_IMU_FRAME, X6_IMU_FRAME, assumedImuFrame, imuFrameFor, isProperRotation, toBodyFrame };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { HALF_TURN, QUARTER_TURN, degrees, degreesToRadians, radians } from "../../../shared/units/angle.js";
|
|
2
|
+
import { ZERO_VECTOR3, addVectors, dotProduct, magnitudeOf } from "../../../shared/math/Vector3.js";
|
|
3
|
+
import { multiplyMatrices, rotationAboutX, rotationAboutY, rotationAboutZ, transformVector, transposeMatrix } from "../../../shared/math/Matrix3.js";
|
|
4
|
+
import { toBodyFrame } from "../imu/ImuFrame.js";
|
|
5
|
+
import { indexOfLeast } from "../../../shared/math/minimum.js";
|
|
6
|
+
import { measuredGravity } from "../orientation/gravity.js";
|
|
7
|
+
//#region ../../packages/core/src/domain/motion/mounting/Mounting.ts
|
|
8
|
+
var QUARTER_TURN_ANGLE = degreesToRadians(QUARTER_TURN);
|
|
9
|
+
var MINUS_QUARTER_TURN_ANGLE = degreesToRadians(degrees(-QUARTER_TURN));
|
|
10
|
+
var HALF_TURN_ANGLE = degreesToRadians(HALF_TURN);
|
|
11
|
+
/**
|
|
12
|
+
* Also how a recording stands whose gravity cannot be read: one without a gyro record, or whose
|
|
13
|
+
* IMU frame is a guess.
|
|
14
|
+
*/
|
|
15
|
+
var UPRIGHT_MOUNTING = {
|
|
16
|
+
name: "upright",
|
|
17
|
+
toBody: lensLevel(radians(0))
|
|
18
|
+
};
|
|
19
|
+
var DOWN = [
|
|
20
|
+
0,
|
|
21
|
+
1,
|
|
22
|
+
0
|
|
23
|
+
];
|
|
24
|
+
var LENS_AXIS = [
|
|
25
|
+
0,
|
|
26
|
+
0,
|
|
27
|
+
1
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* The four ways a camera can stand with its lens axis level, one for each body axis across it
|
|
31
|
+
* that gravity can lie along.
|
|
32
|
+
*/
|
|
33
|
+
var LENS_LEVEL_MOUNTINGS = [
|
|
34
|
+
UPRIGHT_MOUNTING,
|
|
35
|
+
{
|
|
36
|
+
name: "upside down",
|
|
37
|
+
toBody: lensLevel(HALF_TURN_ANGLE)
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "on its right side",
|
|
41
|
+
toBody: lensLevel(MINUS_QUARTER_TURN_ANGLE)
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "on its left side",
|
|
45
|
+
toBody: lensLevel(QUARTER_TURN_ANGLE)
|
|
46
|
+
}
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* The two ways a camera can stand with its lens axis the vertical.
|
|
50
|
+
*/
|
|
51
|
+
var LENS_VERTICAL_MOUNTINGS = [{
|
|
52
|
+
name: "with lens 0 up",
|
|
53
|
+
toBody: lensVertical(MINUS_QUARTER_TURN_ANGLE)
|
|
54
|
+
}, {
|
|
55
|
+
name: "with lens 0 down",
|
|
56
|
+
toBody: lensVertical(QUARTER_TURN_ANGLE)
|
|
57
|
+
}];
|
|
58
|
+
var LENS_VERTICAL_CONE_COSINE = Math.cos(degreesToRadians(degrees(30)));
|
|
59
|
+
/**
|
|
60
|
+
* A half turn about the vertical faces the upright frame away from lens 0, then a turn about the
|
|
61
|
+
* lens axis stands the body as it stood.
|
|
62
|
+
*/
|
|
63
|
+
function lensLevel(roll) {
|
|
64
|
+
return multiplyMatrices(rotationAboutZ(roll), rotationAboutY(HALF_TURN_ANGLE));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A quarter turn about the vertical faces the upright frame along the body's minus x, then a
|
|
68
|
+
* quarter turn about the lateral axis stands lens 0's axis up or down.
|
|
69
|
+
*/
|
|
70
|
+
function lensVertical(tilt) {
|
|
71
|
+
return multiplyMatrices(rotationAboutX(tilt), rotationAboutY(MINUS_QUARTER_TURN_ANGLE));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The mounting whose down lies nearest the gravity the accelerometer measured whenever the camera
|
|
75
|
+
* rested, read through the IMU frame: lens-vertical within its cone of the lens axis, lens-level
|
|
76
|
+
* outside it. Quarter turns only, so the picture keeps every tilt the camera really took.
|
|
77
|
+
* Upright for a camera that never rests, and for one whose IMU frame is a guess: read through a
|
|
78
|
+
* wrong frame, gravity would stand the picture on its side or on its head.
|
|
79
|
+
*/
|
|
80
|
+
function mountingOf(gyro, frame) {
|
|
81
|
+
if (!frame.isVerified) return UPRIGHT_MOUNTING;
|
|
82
|
+
const gravity = restingGravitySumOf(gyro, frame);
|
|
83
|
+
const mountings = isNearLensAxis(gravity) ? LENS_VERTICAL_MOUNTINGS : LENS_LEVEL_MOUNTINGS;
|
|
84
|
+
const misalignments = mountings.map((mounting) => -dotProduct(transformVector(mounting.toBody, DOWN), gravity));
|
|
85
|
+
return mountings[indexOfLeast(misalignments)] ?? UPRIGHT_MOUNTING;
|
|
86
|
+
}
|
|
87
|
+
function isNearLensAxis(gravity) {
|
|
88
|
+
return Math.abs(dotProduct(gravity, LENS_AXIS)) > LENS_VERTICAL_CONE_COSINE * magnitudeOf(gravity);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The IMU frame as it lies in the mounting's upright frame: what the camera's orientation is
|
|
92
|
+
* integrated in, so that levelling and heading start from the camera as it stood. Its `toBody`
|
|
93
|
+
* reads into the upright frame, which stands for the body throughout the integration.
|
|
94
|
+
*/
|
|
95
|
+
function uprightImuFrame(frame, mounting) {
|
|
96
|
+
return {
|
|
97
|
+
...frame,
|
|
98
|
+
toBody: multiplyMatrices(transposeMatrix(mounting.toBody), frame.toBody)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A rotation into the mounting's upright frame, such as a stabilizer's, carried on into the body
|
|
103
|
+
* the lenses are posed in.
|
|
104
|
+
*/
|
|
105
|
+
function rotationIntoBody(mounting, uprightRotation) {
|
|
106
|
+
return multiplyMatrices(mounting.toBody, uprightRotation);
|
|
107
|
+
}
|
|
108
|
+
function restingGravitySumOf(gyro, frame) {
|
|
109
|
+
let sum = ZERO_VECTOR3;
|
|
110
|
+
for (let index = 0; index < gyro.length; index += 1) {
|
|
111
|
+
const gravity = measuredGravity(toBodyFrame(frame, gyro.sampleAt(index).acceleration));
|
|
112
|
+
if (gravity) sum = addVectors(sum, gravity);
|
|
113
|
+
}
|
|
114
|
+
return sum;
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
export { UPRIGHT_MOUNTING, mountingOf, rotationIntoBody, uprightImuFrame };
|
|
@@ -24,8 +24,10 @@ var QUATERNION_COMPONENTS = 4;
|
|
|
24
24
|
*/
|
|
25
25
|
var MAX_STEP_SECONDS = .05;
|
|
26
26
|
/**
|
|
27
|
-
* The camera's orientation over the whole recording,
|
|
28
|
-
*
|
|
27
|
+
* The camera's orientation over the whole recording, from the frame its IMU frame reads into (the
|
|
28
|
+
* body, or the upright frame of its mounting, as the player integrates it) to the world, one
|
|
29
|
+
* quaternion per gyro sample. World: y down along gravity, z along that frame's forward axis at
|
|
30
|
+
* the start.
|
|
29
31
|
*/
|
|
30
32
|
var OrientationTrack = class OrientationTrack {
|
|
31
33
|
videoTimes;
|
|
@@ -15,7 +15,9 @@ import { clamp } from "./shared/math/clamp.js";
|
|
|
15
15
|
import "./domain/motion/timing/FrameTimes.js";
|
|
16
16
|
import { IDENTITY_MATRIX3 } from "./shared/math/Matrix3.js";
|
|
17
17
|
import "./domain/motion/imu/ImuFrame.js";
|
|
18
|
+
import "./shared/math/minimum.js";
|
|
18
19
|
import "./shared/math/Quaternion.js";
|
|
20
|
+
import { UPRIGHT_MOUNTING } from "./domain/motion/mounting/Mounting.js";
|
|
19
21
|
import "./domain/motion/orientation/OrientationTrack.js";
|
|
20
22
|
import { timeRecording } from "./application/recording/timeRecording.js";
|
|
21
23
|
import "./domain/format/naming/RecordingFileName.js";
|
|
@@ -26,6 +28,7 @@ import "./domain/container/TrackSampleTable.js";
|
|
|
26
28
|
import { readSampleTable } from "./application/recording/readSampleTable.js";
|
|
27
29
|
import "./domain/stitching/LensLayout.js";
|
|
28
30
|
import { detectLensLayout } from "./domain/format/layout/detectLensLayout.js";
|
|
31
|
+
import { hevcCodecStringOf } from "./domain/format/mp4/hevcCodecString.js";
|
|
29
32
|
import { DEFAULT_STABILIZATION_MODE, STABILIZATION_MODES } from "./domain/motion/stabilization/Stabilizer.js";
|
|
30
33
|
import { stabilizerFor } from "./domain/motion/stabilization/stabilizers.js";
|
|
31
34
|
import "./domain/optics/lensPose.js";
|
|
@@ -68,4 +71,4 @@ import { WallClock } from "./application/playback/WallClock.js";
|
|
|
68
71
|
import { isFlowing } from "./domain/playback/PlayerState.js";
|
|
69
72
|
import { TypedEmitter } from "./shared/events/TypedEmitter.js";
|
|
70
73
|
import { PlaybackSession } from "./application/playback/PlaybackSession.js";
|
|
71
|
-
export { BT709_LUMINANCE, ByteRange, COLOUR_PRIMARIES, DEFAULT_FRAMING, DEFAULT_PICTURE_QUALITY, DEFAULT_STABILIZATION_MODE, DEFAULT_VIEW_MODE, Deferred, Ending, GYRO_VIEW_ERROR_CATEGORIES, GYRO_VIEW_ERROR_CODES, GainMatchingFrameSink, GyroViewError, HLG_OETF, IDENTITY_MATRIX3, ITERATION_END, MATRIX_COEFFICIENTS, MEI_TERM_CAPACITY, Outbox, PICTURE_QUALITIES, PlaybackSession, RecordingBuffer, RunStop, SCREEN_CENTRE, STABILIZATION_MODES, STOPPED, SourceByteStream, StabilizingFrameSink, TRANSFER_CHARACTERISTICS, TypedEmitter, VIEW_MODES, WallClock, asGyroViewError, aspectOf, aspectOfArea, buildStitchingSetup, clamp, clampView, degrees, detectLensLayout, displayConversionsOf, downloadPolicyFor, ensureIndexInRange, ensureInvariant, fileNameOfUrl, hasErrorCode, inspectRecording, isAbortError, isFlowing, isGyroViewErrorCode, isSameFraming, isSameView, keysOf, lazy, lensFrameOrder, locateOtherLensFile, lookAt, matrixCorrectionOf, messageOf, microseconds, microsecondsToSeconds, milliseconds, namedOrUnspecified, pixelRatioCapOf, planeHalfExtentOf, probeDecoding, readRecording, readSampleTable, seconds, secondsToMicroseconds, stabilizerFor, startFileDownload, timeRecording, viewModeRulesFor, zoomStepsForPinch };
|
|
74
|
+
export { BT709_LUMINANCE, ByteRange, COLOUR_PRIMARIES, DEFAULT_FRAMING, DEFAULT_PICTURE_QUALITY, DEFAULT_STABILIZATION_MODE, DEFAULT_VIEW_MODE, Deferred, Ending, GYRO_VIEW_ERROR_CATEGORIES, GYRO_VIEW_ERROR_CODES, GainMatchingFrameSink, GyroViewError, HLG_OETF, IDENTITY_MATRIX3, ITERATION_END, MATRIX_COEFFICIENTS, MEI_TERM_CAPACITY, Outbox, PICTURE_QUALITIES, PlaybackSession, RecordingBuffer, RunStop, SCREEN_CENTRE, STABILIZATION_MODES, STOPPED, SourceByteStream, StabilizingFrameSink, TRANSFER_CHARACTERISTICS, TypedEmitter, UPRIGHT_MOUNTING, VIEW_MODES, WallClock, asGyroViewError, aspectOf, aspectOfArea, buildStitchingSetup, clamp, clampView, degrees, detectLensLayout, displayConversionsOf, downloadPolicyFor, ensureIndexInRange, ensureInvariant, fileNameOfUrl, hasErrorCode, hevcCodecStringOf, inspectRecording, isAbortError, isFlowing, isGyroViewErrorCode, isSameFraming, isSameView, keysOf, lazy, lensFrameOrder, locateOtherLensFile, lookAt, matrixCorrectionOf, messageOf, microseconds, microsecondsToSeconds, milliseconds, namedOrUnspecified, pixelRatioCapOf, planeHalfExtentOf, probeDecoding, readRecording, readSampleTable, seconds, secondsToMicroseconds, stabilizerFor, startFileDownload, timeRecording, viewModeRulesFor, zoomStepsForPinch };
|
|
@@ -41,6 +41,10 @@ var ByteReader = class {
|
|
|
41
41
|
this.ensureAvailable(offset, UINT32_SIZE);
|
|
42
42
|
return this.view.getUint32(offset, IS_LITTLE_ENDIAN);
|
|
43
43
|
}
|
|
44
|
+
uint16BeAt(offset) {
|
|
45
|
+
this.ensureAvailable(offset, UINT16_SIZE);
|
|
46
|
+
return this.view.getUint16(offset, IS_BIG_ENDIAN);
|
|
47
|
+
}
|
|
44
48
|
uint32BeAt(offset) {
|
|
45
49
|
this.ensureAvailable(offset, UINT32_SIZE);
|
|
46
50
|
return this.view.getUint32(offset, IS_BIG_ENDIAN);
|
|
@@ -5,6 +5,7 @@ import { microseconds, microsecondsToSeconds, milliseconds, seconds, secondsToMi
|
|
|
5
5
|
import "./math/Vector3.js";
|
|
6
6
|
import { clamp } from "./math/clamp.js";
|
|
7
7
|
import { IDENTITY_MATRIX3 } from "./math/Matrix3.js";
|
|
8
|
+
import "./math/minimum.js";
|
|
8
9
|
import "./math/Quaternion.js";
|
|
9
10
|
import { keysOf } from "./keysOf.js";
|
|
10
11
|
import { Outbox } from "./events/Outbox.js";
|
|
@@ -73,6 +73,23 @@ function multiplyMatrices(a, b) {
|
|
|
73
73
|
a20 * b02 + a21 * b12 + a22 * b22
|
|
74
74
|
];
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* The transpose, which for a rotation is its inverse.
|
|
78
|
+
*/
|
|
79
|
+
function transposeMatrix(m) {
|
|
80
|
+
const [m00, m01, m02, m10, m11, m12, m20, m21, m22] = m;
|
|
81
|
+
return [
|
|
82
|
+
m00,
|
|
83
|
+
m10,
|
|
84
|
+
m20,
|
|
85
|
+
m01,
|
|
86
|
+
m11,
|
|
87
|
+
m21,
|
|
88
|
+
m02,
|
|
89
|
+
m12,
|
|
90
|
+
m22
|
|
91
|
+
];
|
|
92
|
+
}
|
|
76
93
|
function transformVector(m, v) {
|
|
77
94
|
const [m00, m01, m02, m10, m11, m12, m20, m21, m22] = m;
|
|
78
95
|
const [x, y, z] = v;
|
|
@@ -90,4 +107,4 @@ function determinantOf(m) {
|
|
|
90
107
|
return m00 * (m11 * m22 - m12 * m21) - m01 * (m10 * m22 - m12 * m20) + m02 * (m10 * m21 - m11 * m20);
|
|
91
108
|
}
|
|
92
109
|
//#endregion
|
|
93
|
-
export { IDENTITY_MATRIX3, determinantOf, multiplyMatrices, rotationAboutX, rotationAboutY, rotationAboutZ, transformVector };
|
|
110
|
+
export { IDENTITY_MATRIX3, determinantOf, multiplyMatrices, rotationAboutX, rotationAboutY, rotationAboutZ, transformVector, transposeMatrix };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region ../../packages/core/src/shared/math/minimum.ts
|
|
2
|
+
/**
|
|
3
|
+
* The index of the least of the values, the first on a tie; values that are not finite never
|
|
4
|
+
* win, so a list of them all gives index 0.
|
|
5
|
+
*/
|
|
6
|
+
function indexOfLeast(values) {
|
|
7
|
+
let best = 0;
|
|
8
|
+
let least = Infinity;
|
|
9
|
+
for (const [index, value] of values.entries()) {
|
|
10
|
+
if (!Number.isFinite(value) || !(value < least)) continue;
|
|
11
|
+
best = index;
|
|
12
|
+
least = value;
|
|
13
|
+
}
|
|
14
|
+
return best;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { indexOfLeast };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { messageOf } from "../../../core/src/shared/errors/GyroViewError.js";
|
|
2
2
|
import { milliseconds, seconds } from "../../../core/src/shared/units/time.js";
|
|
3
|
+
import { UPRIGHT_MOUNTING } from "../../../core/src/domain/motion/mounting/Mounting.js";
|
|
3
4
|
import { stabilizerFor } from "../../../core/src/domain/motion/stabilization/stabilizers.js";
|
|
4
5
|
import { buildStitchingSetup } from "../../../core/src/domain/stitching/StitchingSetup.js";
|
|
5
6
|
import { StabilizingFrameSink } from "../../../core/src/application/stabilization/StabilizingFrameSink.js";
|
|
@@ -129,13 +130,16 @@ function stitchingSetupOf(opened) {
|
|
|
129
130
|
});
|
|
130
131
|
}
|
|
131
132
|
function sinkOver(renderer, opened) {
|
|
132
|
-
if (!opened.motion)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
if (!opened.motion) {
|
|
134
|
+
renderer.setStabilization(UPRIGHT_MOUNTING.toBody);
|
|
135
|
+
return {
|
|
136
|
+
sink: renderer,
|
|
137
|
+
stabilizing: void 0
|
|
138
|
+
};
|
|
139
|
+
}
|
|
136
140
|
const stabilizing = new StabilizingFrameSink({
|
|
137
141
|
sink: renderer,
|
|
138
|
-
|
|
142
|
+
motion: opened.motion,
|
|
139
143
|
frameTimes: opened.frameTimes
|
|
140
144
|
});
|
|
141
145
|
return {
|