@bubo-squared/gyroview 0.4.2 → 0.6.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 +54 -0
- package/README.md +6 -6
- package/dist/index.d.ts +56 -15
- 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/domain/view/Framing.js +8 -2
- package/dist/packages/core/src/domain/view/ViewState.js +13 -8
- package/dist/packages/core/src/domain/view/lensTilesView.js +2 -1
- package/dist/packages/core/src/domain/view/motionLookView.js +103 -0
- package/dist/packages/core/src/domain/view/normalView.js +12 -5
- package/dist/packages/core/src/domain/view/panoramaView.js +2 -1
- package/dist/packages/core/src/domain/view/screenLook.js +74 -0
- package/dist/packages/core/src/domain/view/viewModes.js +18 -1
- package/dist/packages/core/src/index.js +8 -3
- 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 +29 -1
- package/dist/packages/core/src/shared/math/minimum.js +17 -0
- package/dist/packages/player/src/browserPlayer.js +6 -4
- package/dist/packages/player/src/composition/BrowserAttitudeSensor.js +192 -0
- package/dist/packages/player/src/composition/attitudeSensor.js +18 -0
- package/dist/packages/player/src/composition/buildPipeline.js +9 -5
- package/dist/packages/player/src/controls/MotionLookButton.js +35 -0
- package/dist/packages/player/src/controls/controlParts.js +1 -0
- package/dist/packages/player/src/controls/controls.js +1 -1
- package/dist/packages/player/src/controls/controlsBar.js +5 -3
- package/dist/packages/player/src/controls/controlsMarkup.js +1 -0
- package/dist/packages/player/src/controls/icons.js +1 -0
- package/dist/packages/player/src/controls/messages.js +1 -0
- package/dist/packages/player/src/element/GyroViewElement.js +21 -0
- package/dist/packages/player/src/player/AutomaticStarts.js +54 -0
- package/dist/packages/player/src/player/MotionLook.js +150 -0
- package/dist/packages/player/src/player/Player.js +46 -39
- package/dist/packages/player/src/player/PlayerEvents.js +1 -0
- package/dist/packages/player/src/player/PlayerView.js +44 -13
- package/dist/packages/player/src/player/ensureFinite.js +2 -1
- package/dist/packages/player/src/player/viewAngles.js +17 -0
- package/dist/standalone.js +115 -62
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,60 @@
|
|
|
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.6.0 (2026-10-05)
|
|
7
|
+
|
|
8
|
+
New:
|
|
9
|
+
|
|
10
|
+
- Motion look: on a phone or tablet, a toggle in the normal view lets the device turn the view, the
|
|
11
|
+
screen a window into the recording with its horizon level with the real one (ADR 0040). The
|
|
12
|
+
element and the player have `motionLook` (`on`, `off` or `unavailable`), `startMotionLook()`,
|
|
13
|
+
which iOS honours only from a tap's handler, and `stopMotionLook()`; `motionlookchange` reports
|
|
14
|
+
it, and the warnings `motion-look-refused` and `motion-look-needs-gesture` say why it did not
|
|
15
|
+
start.
|
|
16
|
+
|
|
17
|
+
What a page may notice:
|
|
18
|
+
|
|
19
|
+
- `PlayerEvents`, `GyroViewElementEventMap` and `WarningCode` have the new event and codes; a
|
|
20
|
+
record keyed on every event or code needs them.
|
|
21
|
+
- `view` and `viewchange` give a fresh object of `yaw`, `pitch` and `fieldOfView` each time.
|
|
22
|
+
- The iframe the snippet creates allows `accelerometer; gyroscope; magnetometer`; a hand-written
|
|
23
|
+
iframe needs them for motion look. Without them, Chromium offers no toggle; iOS offers it and
|
|
24
|
+
refuses at the first press (`motion-look-refused`).
|
|
25
|
+
- On the narrowest players with the motion look toggle shown, the Stabilization menu gives way
|
|
26
|
+
to it.
|
|
27
|
+
|
|
28
|
+
## 0.5.0 (2026-10-03)
|
|
29
|
+
|
|
30
|
+
New:
|
|
31
|
+
|
|
32
|
+
- Antigravity A1 recordings play. The A1 leaves the profile, tier and level of its HEVC
|
|
33
|
+
configuration blank, so the codec string is read from the stream's SPS instead (ADR 0037), and
|
|
34
|
+
its IMU frame is measured (ADR 0009): the `ready` event's `imuFrame` is
|
|
35
|
+
`{ name: 'A1', isVerified: true }`.
|
|
36
|
+
|
|
37
|
+
What a page may notice:
|
|
38
|
+
|
|
39
|
+
- An HEVC recording's codec in the `ready` event's `tracks[].codec` begins with the track's
|
|
40
|
+
sample entry type, `hvc1` on every camera seen, where it began with `hev1`; its profile, tier
|
|
41
|
+
and level are unchanged (ADR 0037).
|
|
42
|
+
- `off` and `follow` stand a recording upright as its camera stood, where its IMU frame is
|
|
43
|
+
measured (ADR 0038). An X3, X4 Air, X5 or X6 held upright on a stick or a tripod, its usual
|
|
44
|
+
pose, holds its body frame a quarter turn from upright, and these modes drew its recordings
|
|
45
|
+
turned a quarter turn; a camera upside down or with its lens axis vertical stands upright too.
|
|
46
|
+
Under `horizon`, such a camera's view keeps its heading while the camera pitches.
|
|
47
|
+
- An Insta360 X3 recording is stabilized about the right axes: its IMU frame is measured
|
|
48
|
+
(ADR 0009). The `ready` event's `imuFrame` is `{ name: 'X3', isVerified: true }` where it was
|
|
49
|
+
the unverified aligned frame, the load no longer warns that it has not been verified, and an
|
|
50
|
+
X3 on a stick or a tripod stands upright in `off` and `follow` too.
|
|
51
|
+
- An Insta360 ONE RS recording stands upright under `lock` and `horizon`, which turned it on its
|
|
52
|
+
side. Its IMU frame is assumed from one recording of a camera that never turned: `imuFrame` is
|
|
53
|
+
`{ name: 'ONE RS (unverified)', isVerified: false }`, and the load still warns.
|
|
54
|
+
- The view opens where Insta360 Studio centres the recording, in every mode: a half turn from
|
|
55
|
+
lens 0, where it opened, for a camera whose lens axis lies level, and a quarter turn from lens 0
|
|
56
|
+
for one whose lens axis is vertical, as the A1's (ADR 0038, ADR 0039). `yaw` 0 looks there: a
|
|
57
|
+
page that set `yaw` to face a direction faces the opposite one on a level camera until it adds
|
|
58
|
+
180 degrees. The equirectangular panorama is centred as Studio's export is.
|
|
59
|
+
|
|
6
60
|
## 0.4.2 (2026-10-02)
|
|
7
61
|
|
|
8
62
|
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.6/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.6/dist/standalone.js';
|
|
52
52
|
</script>
|
|
53
53
|
```
|
|
54
54
|
|
package/dist/index.d.ts
CHANGED
|
@@ -418,8 +418,9 @@ export interface ViewAngles {
|
|
|
418
418
|
readonly fieldOfView: number;
|
|
419
419
|
}
|
|
420
420
|
/**
|
|
421
|
-
* What concerns one load only. The settings (view, view mode, stabilization, gain
|
|
422
|
-
* sound, loop) belong to the player and carry over from load to load; set them on the
|
|
421
|
+
* What concerns one load only. The settings (view, view mode, motion look, stabilization, gain
|
|
422
|
+
* matching, sound, loop) belong to the player and carry over from load to load; set them on the
|
|
423
|
+
* player.
|
|
423
424
|
*/
|
|
424
425
|
export interface LoadOptions {
|
|
425
426
|
/**
|
|
@@ -436,6 +437,11 @@ export interface LoadOptions {
|
|
|
436
437
|
* The session's states plus the two only the player knows: nothing loaded, and loading.
|
|
437
438
|
*/
|
|
438
439
|
export type PlayerStatus = "idle" | "loading" | PlayerState;
|
|
440
|
+
/**
|
|
441
|
+
* Motion look as it stands: `unavailable` where the device reports no attitude or access was
|
|
442
|
+
* refused, `off`, or `on`, the device turning the normal view (ADR 0040).
|
|
443
|
+
*/
|
|
444
|
+
export type MotionLookState = "unavailable" | "off" | "on";
|
|
439
445
|
/**
|
|
440
446
|
* The sound as it is now: volume from 0 to 1, and whether it is muted.
|
|
441
447
|
*/
|
|
@@ -453,8 +459,12 @@ export interface SoundLevel {
|
|
|
453
459
|
* - `playback-failed`: a start (a press, autoplay, the loop) or the seek bar's preview failed.
|
|
454
460
|
* - `ignored-attribute`: an attribute's value names nothing the element knows; the setting stays.
|
|
455
461
|
* - `refused-property`: a property set before the element was defined was refused.
|
|
462
|
+
* - `motion-look-refused`: the viewer, or an iframe's `allow`, refused access to the device's
|
|
463
|
+
* attitude; motion look is unavailable.
|
|
464
|
+
* - `motion-look-needs-gesture`: motion look was started outside a user gesture, where iOS does
|
|
465
|
+
* not ask; it stays off.
|
|
456
466
|
*/
|
|
457
|
-
export type WarningCode = "recording-degraded" | "no-sound" | "autoplay-blocked" | "playback-failed" | "ignored-attribute" | "refused-property";
|
|
467
|
+
export type WarningCode = "recording-degraded" | "no-sound" | "autoplay-blocked" | "playback-failed" | "ignored-attribute" | "refused-property" | "motion-look-refused" | "motion-look-needs-gesture";
|
|
458
468
|
export interface PlayerWarning {
|
|
459
469
|
readonly code: WarningCode;
|
|
460
470
|
/**
|
|
@@ -488,6 +498,7 @@ export interface PlayerEvents {
|
|
|
488
498
|
readonly frame: number;
|
|
489
499
|
readonly viewchange: ViewAngles;
|
|
490
500
|
readonly viewmodechange: ViewMode;
|
|
501
|
+
readonly motionlookchange: MotionLookState;
|
|
491
502
|
readonly stabilizationchange: StabilizationMode;
|
|
492
503
|
/**
|
|
493
504
|
* The picture quality changed, to the one named.
|
|
@@ -506,8 +517,8 @@ export interface PlayerEvents {
|
|
|
506
517
|
/**
|
|
507
518
|
* The player without the element, which `createBrowserPlayer` gives: it plays one recording at a
|
|
508
519
|
* time on the canvas and audio element it was handed, reports in media-element events, and keeps
|
|
509
|
-
* its settings (view, view mode, stabilization, gain matching, sound, loop) from
|
|
510
|
-
* `<gyro-view>` is a facade over it.
|
|
520
|
+
* its settings (view, view mode, motion look, stabilization, gain matching, sound, loop) from
|
|
521
|
+
* load to load. `<gyro-view>` is a facade over it.
|
|
511
522
|
*/
|
|
512
523
|
export declare class Player {
|
|
513
524
|
private readonly parts;
|
|
@@ -519,6 +530,7 @@ export declare class Player {
|
|
|
519
530
|
private readonly loop;
|
|
520
531
|
private phase;
|
|
521
532
|
private readonly viewing;
|
|
533
|
+
private readonly motion;
|
|
522
534
|
private readonly picture;
|
|
523
535
|
/**
|
|
524
536
|
* A seek asked for before a recording was ready, where the next one starts.
|
|
@@ -526,7 +538,7 @@ export declare class Player {
|
|
|
526
538
|
private pendingStartTime;
|
|
527
539
|
private readonly sound;
|
|
528
540
|
private readonly relay;
|
|
529
|
-
private
|
|
541
|
+
private readonly starts;
|
|
530
542
|
/**
|
|
531
543
|
* Past `dispose`: a listener of the idle it announces must not load again.
|
|
532
544
|
*/
|
|
@@ -549,6 +561,11 @@ export declare class Player {
|
|
|
549
561
|
* Whether a drag moves the picture as it is framed now; the view mode decides.
|
|
550
562
|
*/
|
|
551
563
|
get canPan(): boolean;
|
|
564
|
+
/**
|
|
565
|
+
* Whether turning the device turns the normal view: `unavailable` where the device reports no
|
|
566
|
+
* attitude or access was refused.
|
|
567
|
+
*/
|
|
568
|
+
get motionLook(): MotionLookState;
|
|
552
569
|
get stabilization(): StabilizationMode;
|
|
553
570
|
get quality(): PictureQuality;
|
|
554
571
|
get isLooping(): boolean;
|
|
@@ -604,6 +621,17 @@ export declare class Player {
|
|
|
604
621
|
zoom(steps: number, focus?: ScreenPoint): void;
|
|
605
622
|
resetView(): void;
|
|
606
623
|
setViewMode(mode: ViewMode): void;
|
|
624
|
+
/**
|
|
625
|
+
* Lets the device turn the normal view, as a window into the recording: its tilt and roll
|
|
626
|
+
* hold the pitch and keep the horizon level, drags turn the heading alone. Call it from a tap's
|
|
627
|
+
* handler: iOS asks the viewer for access there and refuses it anywhere else. Resolves with the
|
|
628
|
+
* state it ends in; a refusal is a warning, never a rejection.
|
|
629
|
+
*/
|
|
630
|
+
startMotionLook(): Promise<MotionLookState>;
|
|
631
|
+
/**
|
|
632
|
+
* Gives the view back to the pointer, level, looking where it looked.
|
|
633
|
+
*/
|
|
634
|
+
stopMotionLook(): void;
|
|
607
635
|
setStabilization(mode: StabilizationMode): void;
|
|
608
636
|
/**
|
|
609
637
|
* How finely the lens images are read and how many device pixels are drawn; kept across loads.
|
|
@@ -626,12 +654,6 @@ export declare class Player {
|
|
|
626
654
|
private complete;
|
|
627
655
|
private startAtPendingTime;
|
|
628
656
|
private attach;
|
|
629
|
-
/**
|
|
630
|
-
* A refused start leaves the recording loaded and paused: a warning, never a failed load.
|
|
631
|
-
*/
|
|
632
|
-
private autoplay;
|
|
633
|
-
private onEnded;
|
|
634
|
-
private replay;
|
|
635
657
|
/**
|
|
636
658
|
* Lets go of whatever is loading or loaded, announcing nothing.
|
|
637
659
|
*/
|
|
@@ -664,6 +686,10 @@ export interface GyroViewLabels {
|
|
|
664
686
|
readonly mute: string;
|
|
665
687
|
readonly volume: string;
|
|
666
688
|
readonly resetView: string;
|
|
689
|
+
/**
|
|
690
|
+
* The toggle that lets the device's own turns turn the normal view.
|
|
691
|
+
*/
|
|
692
|
+
readonly motionLook: string;
|
|
667
693
|
readonly stabilization: string;
|
|
668
694
|
readonly viewMode: string;
|
|
669
695
|
readonly fullscreen: string;
|
|
@@ -792,9 +818,9 @@ export interface BrowserPortsOptions {
|
|
|
792
818
|
readonly probeTimeoutMs?: number;
|
|
793
819
|
}
|
|
794
820
|
/**
|
|
795
|
-
* A player composed for a browser: HTTP, blob and WebCodecs ports
|
|
796
|
-
* on the host's canvas and sounding through its audio element. What
|
|
797
|
-
* and where a page that wants the player without the element starts.
|
|
821
|
+
* A player composed for a browser: HTTP, blob and WebCodecs ports, the GPU pipeline and the
|
|
822
|
+
* device's attitude, drawing on the host's canvas and sounding through its audio element. What
|
|
823
|
+
* `<gyro-view>` plays with, and where a page that wants the player without the element starts.
|
|
798
824
|
*/
|
|
799
825
|
export declare function createBrowserPlayer(host: PipelineHost, options?: BrowserPortsOptions): Player;
|
|
800
826
|
export interface InspectOptions extends Pick<BrowserPortsOptions, "http"> {
|
|
@@ -903,6 +929,11 @@ export declare class GyroViewElement extends TypedEventElement implements LiveSe
|
|
|
903
929
|
* Where the normal view looks: yaw and pitch in degrees, and the horizontal field of view.
|
|
904
930
|
*/
|
|
905
931
|
get view(): ViewAngles;
|
|
932
|
+
/**
|
|
933
|
+
* Whether turning the device turns the normal view: `on`, `off`, or `unavailable` where the
|
|
934
|
+
* device reports no attitude or access was refused.
|
|
935
|
+
*/
|
|
936
|
+
get motionLook(): MotionLookState;
|
|
906
937
|
/**
|
|
907
938
|
* Every word the element shows or says to assistive technology. Setting it replaces any of
|
|
908
939
|
* them, table by table (`labels`, `stabilizationModes`, `viewModes`, `errors`); the others keep
|
|
@@ -949,6 +980,16 @@ export declare class GyroViewElement extends TypedEventElement implements LiveSe
|
|
|
949
980
|
* when none is given.
|
|
950
981
|
*/
|
|
951
982
|
zoom(steps: number, focus?: ScreenPoint): void;
|
|
983
|
+
/**
|
|
984
|
+
* Lets the device turn the normal view, as a window into the recording. Call it from a tap's
|
|
985
|
+
* handler: iOS asks the viewer for access there and refuses it anywhere else. Resolves with the
|
|
986
|
+
* state it ends in (`motionLook`); a refusal is a `warning`, never a rejection.
|
|
987
|
+
*/
|
|
988
|
+
startMotionLook(): Promise<MotionLookState>;
|
|
989
|
+
/**
|
|
990
|
+
* Gives the view back to the pointer, level, looking where it looked.
|
|
991
|
+
*/
|
|
992
|
+
stopMotionLook(): void;
|
|
952
993
|
/**
|
|
953
994
|
* Stabilizes the picture in `mode` (`off`, `lock`, `horizon`, `follow`); any other value,
|
|
954
995
|
* an unset one included, is refused.
|
|
@@ -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 };
|