@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +6 -6
  3. package/dist/index.d.ts +56 -15
  4. package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +2 -2
  5. package/dist/packages/adapters/mediabunny/src/decoderConfigurations.js +20 -4
  6. package/dist/packages/core/src/application/recording/motionOf.js +5 -1
  7. package/dist/packages/core/src/application/stabilization/StabilizingFrameSink.js +4 -2
  8. package/dist/packages/core/src/domain/format/mp4/hevcCodecString.js +161 -0
  9. package/dist/packages/core/src/domain/motion/imu/ImuFrame.js +35 -3
  10. package/dist/packages/core/src/domain/motion/mounting/Mounting.js +117 -0
  11. package/dist/packages/core/src/domain/motion/orientation/OrientationTrack.js +4 -2
  12. package/dist/packages/core/src/domain/motion/orientation/gravity.js +1 -1
  13. package/dist/packages/core/src/domain/view/Framing.js +8 -2
  14. package/dist/packages/core/src/domain/view/ViewState.js +13 -8
  15. package/dist/packages/core/src/domain/view/lensTilesView.js +2 -1
  16. package/dist/packages/core/src/domain/view/motionLookView.js +103 -0
  17. package/dist/packages/core/src/domain/view/normalView.js +12 -5
  18. package/dist/packages/core/src/domain/view/panoramaView.js +2 -1
  19. package/dist/packages/core/src/domain/view/screenLook.js +74 -0
  20. package/dist/packages/core/src/domain/view/viewModes.js +18 -1
  21. package/dist/packages/core/src/index.js +8 -3
  22. package/dist/packages/core/src/shared/binary/ByteReader.js +4 -0
  23. package/dist/packages/core/src/shared/index.js +1 -0
  24. package/dist/packages/core/src/shared/math/Matrix3.js +29 -1
  25. package/dist/packages/core/src/shared/math/minimum.js +17 -0
  26. package/dist/packages/player/src/browserPlayer.js +6 -4
  27. package/dist/packages/player/src/composition/BrowserAttitudeSensor.js +192 -0
  28. package/dist/packages/player/src/composition/attitudeSensor.js +18 -0
  29. package/dist/packages/player/src/composition/buildPipeline.js +9 -5
  30. package/dist/packages/player/src/controls/MotionLookButton.js +35 -0
  31. package/dist/packages/player/src/controls/controlParts.js +1 -0
  32. package/dist/packages/player/src/controls/controls.js +1 -1
  33. package/dist/packages/player/src/controls/controlsBar.js +5 -3
  34. package/dist/packages/player/src/controls/controlsMarkup.js +1 -0
  35. package/dist/packages/player/src/controls/icons.js +1 -0
  36. package/dist/packages/player/src/controls/messages.js +1 -0
  37. package/dist/packages/player/src/element/GyroViewElement.js +21 -0
  38. package/dist/packages/player/src/player/AutomaticStarts.js +54 -0
  39. package/dist/packages/player/src/player/MotionLook.js +150 -0
  40. package/dist/packages/player/src/player/Player.js +46 -39
  41. package/dist/packages/player/src/player/PlayerEvents.js +1 -0
  42. package/dist/packages/player/src/player/PlayerView.js +44 -13
  43. package/dist/packages/player/src/player/ensureFinite.js +2 -1
  44. package/dist/packages/player/src/player/viewAngles.js +17 -0
  45. package/dist/standalone.js +115 -62
  46. 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 Insta360 `.insv` recordings (X3, X4, X5, X6) in the browser. `<gyro-view>` reads the
4
- camera's dual-fisheye file directly, over HTTP byte ranges or from a local file, decodes both
5
- lenses in hardware with WebCodecs, and stitches and gyro-stabilizes them on the GPU. No Insta360
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.4/dist/standalone.js"
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.4/dist/standalone.js';
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 matching,
422
- * sound, loop) belong to the player and carry over from load to load; set them on the player.
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 load to load.
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 isLoopingValue;
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 and the GPU pipeline, drawing
796
- * on the host's canvas and sounding through its audio element. What `<gyro-view>` plays with,
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.codec,
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
- * A WebCodecs video decoder configuration in the core's terms; the coded size comes from the
5
- * track where the configuration leaves it out, the colour from the track's description.
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: config.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: imuFrame
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 orientation = this.parts.orientations.orientationAt(time);
19
- this.parts.sink.setStabilization(this.stabilizer.nextRotation(orientation, time));
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 };