@bubo-squared/gyroview 0.1.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 (228) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +21 -0
  3. package/README.md +216 -0
  4. package/dist/define.d.ts +3 -0
  5. package/dist/define.js +4 -0
  6. package/dist/index.d.ts +959 -0
  7. package/dist/index.js +13 -0
  8. package/dist/packages/adapters/blob/src/BlobRandomAccessSource.js +37 -0
  9. package/dist/packages/adapters/blob/src/index.js +2 -0
  10. package/dist/packages/adapters/fetch/src/HttpRangeSource.js +194 -0
  11. package/dist/packages/adapters/fetch/src/HttpResourceLocator.js +35 -0
  12. package/dist/packages/adapters/fetch/src/httpRequest.js +126 -0
  13. package/dist/packages/adapters/fetch/src/index.js +3 -0
  14. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioSegments.js +133 -0
  15. package/dist/packages/adapters/mediabunny/src/MediabunnyDemuxer.js +63 -0
  16. package/dist/packages/adapters/mediabunny/src/MediabunnyVideoTrackReader.js +84 -0
  17. package/dist/packages/adapters/mediabunny/src/SegmentChannel.js +84 -0
  18. package/dist/packages/adapters/mediabunny/src/bufferSources.js +12 -0
  19. package/dist/packages/adapters/mediabunny/src/index.js +2 -0
  20. package/dist/packages/adapters/mse-audio/src/MediaSourceAudioClock.js +139 -0
  21. package/dist/packages/adapters/mse-audio/src/SourceBufferFeeder.js +174 -0
  22. package/dist/packages/adapters/mse-audio/src/events.js +26 -0
  23. package/dist/packages/adapters/mse-audio/src/index.js +2 -0
  24. package/dist/packages/adapters/mse-audio/src/mediaSourceSupport.js +82 -0
  25. package/dist/packages/adapters/three/src/ThreeFrameRenderer.js +274 -0
  26. package/dist/packages/adapters/three/src/compileAndProve.js +14 -0
  27. package/dist/packages/adapters/three/src/fullscreenPass.js +52 -0
  28. package/dist/packages/adapters/three/src/index.js +2 -0
  29. package/dist/packages/adapters/three/src/lensTextures.js +25 -0
  30. package/dist/packages/adapters/three/src/pictureMaterials.js +44 -0
  31. package/dist/packages/adapters/three/src/renderInto.js +15 -0
  32. package/dist/packages/adapters/three/src/rendererUniforms.js +157 -0
  33. package/dist/packages/adapters/three/src/samplingStrategies.js +26 -0
  34. package/dist/packages/adapters/three/src/seamMeter/SeamMeterPass.js +79 -0
  35. package/dist/packages/adapters/three/src/seamMeter/rowMeans.js +38 -0
  36. package/dist/packages/adapters/three/src/shaderPrograms.js +66 -0
  37. package/dist/packages/adapters/three/src/shaders/analysis.frag.js +4 -0
  38. package/dist/packages/adapters/three/src/shaders/equirectangularRays.js +4 -0
  39. package/dist/packages/adapters/three/src/shaders/fixedJoin.js +4 -0
  40. package/dist/packages/adapters/three/src/shaders/fullscreen.vert.js +4 -0
  41. package/dist/packages/adapters/three/src/shaders/header.js +4 -0
  42. package/dist/packages/adapters/three/src/shaders/lensModels.js +4 -0
  43. package/dist/packages/adapters/three/src/shaders/lensSampling.js +4 -0
  44. package/dist/packages/adapters/three/src/shaders/lensTextures.js +4 -0
  45. package/dist/packages/adapters/three/src/shaders/rawLenses.frag.js +4 -0
  46. package/dist/packages/adapters/three/src/shaders/rectilinearRays.js +4 -0
  47. package/dist/packages/adapters/three/src/shaders/screenAreas.js +4 -0
  48. package/dist/packages/adapters/three/src/shaders/stitch.frag.js +4 -0
  49. package/dist/packages/adapters/three/src/webglRenderer.js +37 -0
  50. package/dist/packages/adapters/webcodecs/src/WebCodecsVideoDecoderPort.js +135 -0
  51. package/dist/packages/adapters/webcodecs/src/index.js +2 -0
  52. package/dist/packages/core/src/application/gainMatching/GainMatching.js +66 -0
  53. package/dist/packages/core/src/application/gainMatching/GainMatchingFrameSink.js +62 -0
  54. package/dist/packages/core/src/application/playback/ClockWatch.js +76 -0
  55. package/dist/packages/core/src/application/playback/DecodePipeline.js +188 -0
  56. package/dist/packages/core/src/application/playback/DecodeRun.js +93 -0
  57. package/dist/packages/core/src/application/playback/FramePairQueue.js +93 -0
  58. package/dist/packages/core/src/application/playback/FramePairer.js +60 -0
  59. package/dist/packages/core/src/application/playback/PlaybackSession.js +417 -0
  60. package/dist/packages/core/src/application/playback/SeekOrder.js +36 -0
  61. package/dist/packages/core/src/application/playback/SessionLifecycle.js +42 -0
  62. package/dist/packages/core/src/application/playback/StartGate.js +55 -0
  63. package/dist/packages/core/src/application/playback/TimeUpdates.js +31 -0
  64. package/dist/packages/core/src/application/playback/WallClock.js +46 -0
  65. package/dist/packages/core/src/application/playback/keyframeTimeAt.js +15 -0
  66. package/dist/packages/core/src/application/playback/probeDecoding.js +168 -0
  67. package/dist/packages/core/src/application/recording/frameTimesOf.js +22 -0
  68. package/dist/packages/core/src/application/recording/inspectLayout.js +21 -0
  69. package/dist/packages/core/src/application/recording/inspectRecording.js +124 -0
  70. package/dist/packages/core/src/application/recording/locateOtherLensFile.js +18 -0
  71. package/dist/packages/core/src/application/recording/motionOf.js +39 -0
  72. package/dist/packages/core/src/application/recording/readRecording.js +31 -0
  73. package/dist/packages/core/src/application/recording/timeRecording.js +57 -0
  74. package/dist/packages/core/src/application/stabilization/StabilizingFrameSink.js +31 -0
  75. package/dist/packages/core/src/domain/format/boxes/BoxLayout.js +7 -0
  76. package/dist/packages/core/src/domain/format/boxes/boxConstants.js +16 -0
  77. package/dist/packages/core/src/domain/format/boxes/scanBoxes.js +59 -0
  78. package/dist/packages/core/src/domain/format/calibration/CalibrationVersion.js +13 -0
  79. package/dist/packages/core/src/domain/format/calibration/layouts/CalibrationStringLayout.js +34 -0
  80. package/dist/packages/core/src/domain/format/calibration/layouts/LegacyCalibrationLayout.js +44 -0
  81. package/dist/packages/core/src/domain/format/calibration/layouts/MeiCalibrationLayout.js +45 -0
  82. package/dist/packages/core/src/domain/format/calibration/layouts/PolynomialCalibrationLayout.js +44 -0
  83. package/dist/packages/core/src/domain/format/calibration/offsetTokens.js +65 -0
  84. package/dist/packages/core/src/domain/format/calibration/parseOffsetString.js +52 -0
  85. package/dist/packages/core/src/domain/format/calibration/selectCalibration.js +46 -0
  86. package/dist/packages/core/src/domain/format/captureOrigin.js +20 -0
  87. package/dist/packages/core/src/domain/format/constants.js +33 -0
  88. package/dist/packages/core/src/domain/format/info/infoFields.js +110 -0
  89. package/dist/packages/core/src/domain/format/info/parseInfoRecord.js +90 -0
  90. package/dist/packages/core/src/domain/format/info/readInfoRecord.js +14 -0
  91. package/dist/packages/core/src/domain/format/layout/detectLensLayout.js +122 -0
  92. package/dist/packages/core/src/domain/format/naming/RecordingFileName.js +67 -0
  93. package/dist/packages/core/src/domain/format/records/TrailerRecords.js +83 -0
  94. package/dist/packages/core/src/domain/format/records/exposure/parseExposureRecord.js +48 -0
  95. package/dist/packages/core/src/domain/format/records/gyro/FloatGyroSampleLayout.js +39 -0
  96. package/dist/packages/core/src/domain/format/records/gyro/RawGyroSampleLayout.js +55 -0
  97. package/dist/packages/core/src/domain/format/records/gyro/SampleColumns.js +82 -0
  98. package/dist/packages/core/src/domain/format/records/gyro/gyroLayouts.js +12 -0
  99. package/dist/packages/core/src/domain/format/records/gyro/parseGyroRecord.js +129 -0
  100. package/dist/packages/core/src/domain/format/trailer/RecordHeader.js +23 -0
  101. package/dist/packages/core/src/domain/format/trailer/Trailer.js +23 -0
  102. package/dist/packages/core/src/domain/format/trailer/TrailerFooter.js +27 -0
  103. package/dist/packages/core/src/domain/format/trailer/locateRecords.js +80 -0
  104. package/dist/packages/core/src/domain/format/trailer/parseRecordIndex.js +28 -0
  105. package/dist/packages/core/src/domain/format/trailer/readTrailer.js +32 -0
  106. package/dist/packages/core/src/domain/motion/exposure/ExposureRecord.js +53 -0
  107. package/dist/packages/core/src/domain/motion/gyro/GyroTrack.js +60 -0
  108. package/dist/packages/core/src/domain/motion/gyro/medianStep.js +11 -0
  109. package/dist/packages/core/src/domain/motion/gyro/repairedTimeline.js +101 -0
  110. package/dist/packages/core/src/domain/motion/imu/ImuFrame.js +112 -0
  111. package/dist/packages/core/src/domain/motion/orientation/OrientationTrack.js +146 -0
  112. package/dist/packages/core/src/domain/motion/orientation/estimateGyroBias.js +64 -0
  113. package/dist/packages/core/src/domain/motion/orientation/gravity.js +97 -0
  114. package/dist/packages/core/src/domain/motion/stabilization/Stabilizer.js +13 -0
  115. package/dist/packages/core/src/domain/motion/stabilization/stabilizers.js +86 -0
  116. package/dist/packages/core/src/domain/motion/timing/CaptureClock.js +41 -0
  117. package/dist/packages/core/src/domain/motion/timing/FrameTimes.js +92 -0
  118. package/dist/packages/core/src/domain/motion/timing/frameTimeSources.js +61 -0
  119. package/dist/packages/core/src/domain/motion/timing/resolveFrameTimes.js +68 -0
  120. package/dist/packages/core/src/domain/optics/EquidistantModel.js +39 -0
  121. package/dist/packages/core/src/domain/optics/MeiModel.js +45 -0
  122. package/dist/packages/core/src/domain/optics/PolynomialModel.js +44 -0
  123. package/dist/packages/core/src/domain/optics/gainMatch.js +62 -0
  124. package/dist/packages/core/src/domain/optics/lensGeometry.js +18 -0
  125. package/dist/packages/core/src/domain/optics/lensPose.js +35 -0
  126. package/dist/packages/core/src/domain/optics/opticsConstants.js +5 -0
  127. package/dist/packages/core/src/domain/playback/PlayerState.js +83 -0
  128. package/dist/packages/core/src/domain/stitching/LensLayout.js +21 -0
  129. package/dist/packages/core/src/domain/stitching/StitchingSetup.js +78 -0
  130. package/dist/packages/core/src/domain/stitching/seamDisparity.js +4 -0
  131. package/dist/packages/core/src/domain/stitching/seamDisparityField.js +5 -0
  132. package/dist/packages/core/src/domain/stitching/seamJoin.js +16 -0
  133. package/dist/packages/core/src/domain/stitching/seamMismatch.js +1 -0
  134. package/dist/packages/core/src/domain/stitching/seamRing.js +4 -0
  135. package/dist/packages/core/src/domain/stitching/seamStrip.js +26 -0
  136. package/dist/packages/core/src/domain/view/Framing.js +31 -0
  137. package/dist/packages/core/src/domain/view/PictureQuality.js +26 -0
  138. package/dist/packages/core/src/domain/view/ViewMode.js +9 -0
  139. package/dist/packages/core/src/domain/view/ViewState.js +48 -0
  140. package/dist/packages/core/src/domain/view/lensTilesView.js +63 -0
  141. package/dist/packages/core/src/domain/view/magnification.js +103 -0
  142. package/dist/packages/core/src/domain/view/normalView.js +29 -0
  143. package/dist/packages/core/src/domain/view/panoramaView.js +99 -0
  144. package/dist/packages/core/src/domain/view/rectilinear.js +40 -0
  145. package/dist/packages/core/src/domain/view/screenLayout.js +113 -0
  146. package/dist/packages/core/src/domain/view/viewGestures.js +98 -0
  147. package/dist/packages/core/src/domain/view/viewModes.js +14 -0
  148. package/dist/packages/core/src/index.js +59 -0
  149. package/dist/packages/core/src/ports/FramePair.js +6 -0
  150. package/dist/packages/core/src/shared/async/Deferred.js +42 -0
  151. package/dist/packages/core/src/shared/async/RunStop.js +40 -0
  152. package/dist/packages/core/src/shared/binary/ByteRange.js +38 -0
  153. package/dist/packages/core/src/shared/binary/ByteReader.js +77 -0
  154. package/dist/packages/core/src/shared/errors/GyroViewError.js +81 -0
  155. package/dist/packages/core/src/shared/errors/reportLater.js +11 -0
  156. package/dist/packages/core/src/shared/events/Outbox.js +68 -0
  157. package/dist/packages/core/src/shared/events/TypedEmitter.js +55 -0
  158. package/dist/packages/core/src/shared/keysOf.js +10 -0
  159. package/dist/packages/core/src/shared/lazy.js +11 -0
  160. package/dist/packages/core/src/shared/math/Matrix3.js +93 -0
  161. package/dist/packages/core/src/shared/math/Quaternion.js +133 -0
  162. package/dist/packages/core/src/shared/math/Vector3.js +54 -0
  163. package/dist/packages/core/src/shared/math/clamp.js +9 -0
  164. package/dist/packages/core/src/shared/protobuf/ProtobufMessage.js +157 -0
  165. package/dist/packages/core/src/shared/text/urlPath.js +31 -0
  166. package/dist/packages/core/src/shared/text/utf8.js +12 -0
  167. package/dist/packages/core/src/shared/units/angle.js +22 -0
  168. package/dist/packages/core/src/shared/units/time.js +13 -0
  169. package/dist/packages/player/src/PlayerSource.js +15 -0
  170. package/dist/packages/player/src/browserPlayer.js +18 -0
  171. package/dist/packages/player/src/choices.js +25 -0
  172. package/dist/packages/player/src/composition/Disposables.js +31 -0
  173. package/dist/packages/player/src/composition/browserPorts.js +46 -0
  174. package/dist/packages/player/src/composition/buildPipeline.js +153 -0
  175. package/dist/packages/player/src/composition/ensureDecodable.js +63 -0
  176. package/dist/packages/player/src/composition/openInputs.js +179 -0
  177. package/dist/packages/player/src/composition/openRecording.js +42 -0
  178. package/dist/packages/player/src/controls/ChoiceMenu.js +113 -0
  179. package/dist/packages/player/src/controls/PictureMenus.js +47 -0
  180. package/dist/packages/player/src/controls/SeekBar.js +125 -0
  181. package/dist/packages/player/src/controls/SoundControls.js +41 -0
  182. package/dist/packages/player/src/controls/TransportButtons.js +31 -0
  183. package/dist/packages/player/src/controls/ViewGestures.js +136 -0
  184. package/dist/packages/player/src/controls/Wording.js +78 -0
  185. package/dist/packages/player/src/controls/controlParts.js +36 -0
  186. package/dist/packages/player/src/controls/controls.js +4 -0
  187. package/dist/packages/player/src/controls/controlsBar.js +23 -0
  188. package/dist/packages/player/src/controls/controlsMarkup.js +85 -0
  189. package/dist/packages/player/src/controls/customControls.js +59 -0
  190. package/dist/packages/player/src/controls/formatTime.js +19 -0
  191. package/dist/packages/player/src/controls/icons.js +40 -0
  192. package/dist/packages/player/src/controls/keyboard.js +118 -0
  193. package/dist/packages/player/src/controls/messages.js +94 -0
  194. package/dist/packages/player/src/controls/parseMarkup.js +31 -0
  195. package/dist/packages/player/src/controls/viewButtons.js +14 -0
  196. package/dist/packages/player/src/element/ElementLoads.js +119 -0
  197. package/dist/packages/player/src/element/FullscreenToggle.js +39 -0
  198. package/dist/packages/player/src/element/GyroViewElement.js +313 -0
  199. package/dist/packages/player/src/element/IdleWatcher.js +71 -0
  200. package/dist/packages/player/src/element/TypedEventElement.js +22 -0
  201. package/dist/packages/player/src/element/accessibleRegion.js +19 -0
  202. package/dist/packages/player/src/element/attributeNames.js +51 -0
  203. package/dist/packages/player/src/element/attributes.js +101 -0
  204. package/dist/packages/player/src/element/defineGyroView.js +21 -0
  205. package/dist/packages/player/src/element/earlyProperties.js +36 -0
  206. package/dist/packages/player/src/element/elementSource.js +23 -0
  207. package/dist/packages/player/src/element/liveSettings.js +138 -0
  208. package/dist/packages/player/src/element/playbackAttributes.js +53 -0
  209. package/dist/packages/player/src/element/playerEventMirror.js +33 -0
  210. package/dist/packages/player/src/element/reflectedProperties.js +78 -0
  211. package/dist/packages/player/src/element/styles.js +4 -0
  212. package/dist/packages/player/src/element/template.js +47 -0
  213. package/dist/packages/player/src/index.js +10 -0
  214. package/dist/packages/player/src/inspectRecording.js +22 -0
  215. package/dist/packages/player/src/player/DrawingBufferFit.js +77 -0
  216. package/dist/packages/player/src/player/FrameLoop.js +40 -0
  217. package/dist/packages/player/src/player/PictureSettings.js +60 -0
  218. package/dist/packages/player/src/player/Player.js +377 -0
  219. package/dist/packages/player/src/player/PlayerEvents.js +28 -0
  220. package/dist/packages/player/src/player/PlayerPhase.js +21 -0
  221. package/dist/packages/player/src/player/PlayerSound.js +61 -0
  222. package/dist/packages/player/src/player/PlayerView.js +113 -0
  223. package/dist/packages/player/src/player/SessionRelay.js +66 -0
  224. package/dist/packages/player/src/player/ensureFinite.js +26 -0
  225. package/dist/packages/player/src/player/loadRecording.js +65 -0
  226. package/dist/packages/player/src/player/transportEvents.js +29 -0
  227. package/dist/standalone.js +4919 -0
  228. package/package.json +73 -0
@@ -0,0 +1,122 @@
1
+ import { GyroViewError } from "../../../shared/errors/GyroViewError.js";
2
+ import { RecordingFileName } from "../naming/RecordingFileName.js";
3
+ import { FULL_FRAME, LEFT_HALF, RIGHT_HALF } from "../../stitching/LensLayout.js";
4
+ //#region ../../packages/core/src/domain/format/layout/detectLensLayout.ts
5
+ var LENS_COUNT = 2;
6
+ var PACKED_ASPECT_RATIO = 2;
7
+ var ASPECT_TOLERANCE = .02;
8
+ var BACK_LENS_RANK = 0;
9
+ var UNNAMED_RANK = 1;
10
+ var SCREEN_LENS_RANK = 2;
11
+ /**
12
+ * Decides how the lens images are stored from the tracks actually present, using the info
13
+ * record only as a hint. See ADR 0004.
14
+ */
15
+ function detectLensLayout(inputs, hints) {
16
+ const candidates = inputs.flatMap((input, inputIndex) => input.videoTracks.map(({ description }) => ({
17
+ input,
18
+ inputIndex,
19
+ track: description
20
+ })));
21
+ if (inputs.length === 1 && areTwoMatchingSquareTracks(candidates)) return multiTrack(candidates, hints);
22
+ if (inputs.length === LENS_COUNT && isOneSquareTrackPerInput(inputs, candidates)) return splitFiles(inputs, candidates);
23
+ const [only] = candidates;
24
+ if (only && candidates.length === 1 && isPacked(only.track)) return packed(only);
25
+ throw undecidable(inputs, candidates);
26
+ }
27
+ /**
28
+ * Lens 0 is the back lens. Without a track-order hint the first track is assumed to be it, as
29
+ * for the `_00_` file of a pair; when the hint says track 0 is stream 10 the tracks are swapped.
30
+ */
31
+ function multiTrack(candidates, hints) {
32
+ const isSwapped = hints.trackOrder === "stream-10-first";
33
+ const ordered = isSwapped ? candidates.toReversed() : candidates;
34
+ const fileLayoutEvidence = hints.fileLayout === void 0 ? [] : [`info record file layout ${hints.fileLayout}`];
35
+ const trackOrderEvidence = hints.trackOrder === void 0 ? "no track order hint: lens 0 assumed to be track 0" : `info record track order ${hints.trackOrder}: lens 0 is track ${isSwapped ? 1 : 0}`;
36
+ return {
37
+ kind: "multi-track",
38
+ sources: ordered.map((candidate, lensIndex) => fullFrameSource(candidate, lensIndex)),
39
+ evidence: [
40
+ "one input with two video tracks",
41
+ ...fileLayoutEvidence,
42
+ trackOrderEvidence
43
+ ]
44
+ };
45
+ }
46
+ function splitFiles(inputs, candidates) {
47
+ const ordered = candidates.toSorted((left, right) => fileRank(left.input) - fileRank(right.input));
48
+ const names = inputs.map((input) => input.name ?? "?").join(", ");
49
+ return {
50
+ kind: "split-files",
51
+ sources: ordered.map((candidate, lensIndex) => fullFrameSource(candidate, lensIndex)),
52
+ evidence: [`two inputs with one video track each (${names})`, "lens 0 taken from the _00_ file when named"]
53
+ };
54
+ }
55
+ function packed(candidate) {
56
+ const { inputIndex, track } = candidate;
57
+ return {
58
+ kind: "packed",
59
+ sources: [{
60
+ lensIndex: 0,
61
+ inputIndex,
62
+ trackIndex: track.trackIndex,
63
+ region: LEFT_HALF
64
+ }, {
65
+ lensIndex: 1,
66
+ inputIndex,
67
+ trackIndex: track.trackIndex,
68
+ region: RIGHT_HALF
69
+ }],
70
+ evidence: [`single ${track.codedWidth}x${track.codedHeight} track with a 2:1 aspect ratio`]
71
+ };
72
+ }
73
+ function fullFrameSource(candidate, lensIndex) {
74
+ return {
75
+ lensIndex,
76
+ inputIndex: candidate.inputIndex,
77
+ trackIndex: candidate.track.trackIndex,
78
+ region: FULL_FRAME
79
+ };
80
+ }
81
+ /**
82
+ * Lens tracks are square and, in one file, identical in size.
83
+ */
84
+ function areTwoMatchingSquareTracks(candidates) {
85
+ const [first, second] = candidates;
86
+ return candidates.length === LENS_COUNT && first !== void 0 && second !== void 0 && isSquare(first.track) && first.track.codedWidth === second.track.codedWidth && first.track.codedHeight === second.track.codedHeight;
87
+ }
88
+ function isOneSquareTrackPerInput(inputs, candidates) {
89
+ return candidates.length === LENS_COUNT && inputs.every((input) => input.videoTracks.length === 1) && candidates.every((candidate) => isSquare(candidate.track));
90
+ }
91
+ function isSquare(track) {
92
+ return track.codedWidth === track.codedHeight;
93
+ }
94
+ function isPacked(track) {
95
+ return Math.abs(track.codedWidth / track.codedHeight - PACKED_ASPECT_RATIO) <= ASPECT_TOLERANCE;
96
+ }
97
+ /**
98
+ * The back lens file (`_00_`) sorts first, the screen-side lens file (`_10_`) second; unnamed or
99
+ * unconventionally named files keep their given order.
100
+ */
101
+ function fileRank(input) {
102
+ const name = RecordingFileName.parse(input.name ?? "");
103
+ if (name?.isBackLens) return BACK_LENS_RANK;
104
+ return name?.isScreenLens ? SCREEN_LENS_RANK : UNNAMED_RANK;
105
+ }
106
+ function undecidable(inputs, candidates) {
107
+ const [only] = candidates;
108
+ if (inputs.length === 1 && candidates.length === 1 && only !== void 0 && isSquare(only.track)) return missingSecondFile(only.input);
109
+ const shape = candidates.map((candidate) => `${candidate.track.codedWidth}x${candidate.track.codedHeight}`).join(", ");
110
+ return new GyroViewError("unsupported-layout", `cannot map ${candidates.length} video track(s) [${shape}] in ${inputs.length} file(s) onto two lenses`);
111
+ }
112
+ /**
113
+ * What to hand over besides a lone half: the file the name says holds the other lens, when the
114
+ * name follows the camera's pattern.
115
+ */
116
+ function missingSecondFile(input) {
117
+ const name = RecordingFileName.parse(input.name ?? "");
118
+ const other = name ? `the matching ${name.otherLensName()}` : "the other lens's file";
119
+ return new GyroViewError("missing-second-file", `this recording stores one lens per file; provide ${other} as the second source`);
120
+ }
121
+ //#endregion
122
+ export { detectLensLayout };
@@ -0,0 +1,67 @@
1
+ //#region ../../packages/core/src/domain/format/naming/RecordingFileName.ts
2
+ /**
3
+ * Insta360 file names follow `VID_20260814_132640_00_013.insv`: a prefix, the capture date and
4
+ * time, a two-digit stream code and a sequence number. In the stream code the first digit names
5
+ * the lens (0 the back lens, 1 the screen-side lens) and the second marks a proxy (0 the
6
+ * recording itself, 1 its low-resolution LRV). Seen on the X5 samples (`VID_..._00_` with
7
+ * `LRV_..._01_`) and documented by the community for the `_00_`/`_10_` pairs of older cameras.
8
+ * The name is only ever a hint: whatever it suggests is verified against the file's contents.
9
+ */
10
+ var FILE_NAME_PATTERN = /^(?<prefix>[A-Za-z]+(?:_[A-Za-z]+)*)_(?<stamp>\d{8}_\d{6})_(?<lens>\d)(?<proxy>\d)_(?<sequence>\d{3})\.(?<extension>[A-Za-z0-9]+)$/u;
11
+ var BACK_LENS_DIGIT = 0;
12
+ var SCREEN_LENS_DIGIT = 1;
13
+ var RECORDING_DIGIT = 0;
14
+ var PROXY_DIGIT = 1;
15
+ /**
16
+ * Value object over a camera file name; derives the name of the other lens's file the camera
17
+ * writes beside it (see {@link otherLensName}).
18
+ */
19
+ var RecordingFileName = class RecordingFileName {
20
+ parts;
21
+ constructor(parts) {
22
+ this.parts = parts;
23
+ }
24
+ /**
25
+ * Undefined for names that do not follow the camera's convention.
26
+ */
27
+ static parse(fileName) {
28
+ const groups = FILE_NAME_PATTERN.exec(fileName)?.groups;
29
+ return groups ? new RecordingFileName(partsOf(groups)) : void 0;
30
+ }
31
+ get isBackLens() {
32
+ return this.parts.lensDigit === BACK_LENS_DIGIT;
33
+ }
34
+ get isScreenLens() {
35
+ return this.parts.lensDigit === SCREEN_LENS_DIGIT;
36
+ }
37
+ get isProxy() {
38
+ return this.parts.proxyDigit === PROXY_DIGIT;
39
+ }
40
+ /**
41
+ * The file holding the other lens of a split-file recording: `_00_` for `_10_` and back.
42
+ */
43
+ otherLensName() {
44
+ const lensDigit = this.isBackLens ? SCREEN_LENS_DIGIT : BACK_LENS_DIGIT;
45
+ return new RecordingFileName({
46
+ ...this.parts,
47
+ lensDigit,
48
+ proxyDigit: RECORDING_DIGIT
49
+ }).toString();
50
+ }
51
+ toString() {
52
+ const { prefix, captureStamp, lensDigit, proxyDigit, sequence, extension } = this.parts;
53
+ return `${prefix}_${captureStamp}_${lensDigit}${proxyDigit}_${sequence}.${extension}`;
54
+ }
55
+ };
56
+ function partsOf(groups) {
57
+ return {
58
+ prefix: groups["prefix"] ?? "",
59
+ captureStamp: groups["stamp"] ?? "",
60
+ lensDigit: Number(groups["lens"]),
61
+ proxyDigit: Number(groups["proxy"]),
62
+ sequence: groups["sequence"] ?? "",
63
+ extension: groups["extension"] ?? ""
64
+ };
65
+ }
66
+ //#endregion
67
+ export { RecordingFileName };
@@ -0,0 +1,83 @@
1
+ import { ensureInvariant } from "../../../shared/errors/GyroViewError.js";
2
+ import { RecordType } from "../constants.js";
3
+ import { parseExposureRecord } from "./exposure/parseExposureRecord.js";
4
+ import { GYRO_LAYOUT_PROBE_SIZE, parseGyroRecord, selectGyroSampleLayout } from "./gyro/parseGyroRecord.js";
5
+ import { captureTimeOfStamp, firstFrameCaptureTime } from "../captureOrigin.js";
6
+ import { ByteRange } from "../../../shared/binary/ByteRange.js";
7
+ //#region ../../packages/core/src/domain/format/records/TrailerRecords.ts
8
+ /**
9
+ * The large records the trailer lists, read on demand through the source: the gyro record in the
10
+ * sample layout the file actually uses, and the exposure record.
11
+ */
12
+ var TrailerRecords = class {
13
+ source;
14
+ trailer;
15
+ info;
16
+ gyroLayoutPromise;
17
+ constructor(source, trailer, info) {
18
+ this.source = source;
19
+ this.trailer = trailer;
20
+ this.info = info;
21
+ }
22
+ /**
23
+ * Whether the trailer lists an exposure record, parsed or not.
24
+ */
25
+ get listsExposure() {
26
+ return this.trailer.locationOf(RecordType.Exposure) !== void 0;
27
+ }
28
+ /**
29
+ * Undefined when the camera wrote no gyro record.
30
+ */
31
+ async readGyro() {
32
+ const location = this.trailer.locationOf(RecordType.Gyro);
33
+ if (location === void 0) return void 0;
34
+ const [payload, layout] = await Promise.all([this.source.read(location.payload), this.gyroSampleLayout()]);
35
+ ensureInvariant(layout !== void 0, "a gyro record always yields or refuses a layout");
36
+ return parseGyroRecord(payload, layout);
37
+ }
38
+ /**
39
+ * Undefined when the camera wrote no exposure record, or a damaged one. Its stamps are in the
40
+ * gyro layout's unit, so a layout that cannot be told refuses it as it refuses the gyro record.
41
+ */
42
+ async readExposure() {
43
+ const location = this.trailer.locationOf(RecordType.Exposure);
44
+ if (location === void 0) return void 0;
45
+ const [payload, layout] = await Promise.all([this.source.read(location.payload), this.gyroSampleLayout()]);
46
+ return parseExposureRecord(payload, (stamp) => captureTimeOfStamp(stamp, layout));
47
+ }
48
+ /**
49
+ * When the first frame was captured, which the info record stamps in the gyro layout's unit;
50
+ * undefined when it does not say.
51
+ */
52
+ async firstFrameCaptureTime() {
53
+ return firstFrameCaptureTime(this.info, await this.gyroSampleLayout());
54
+ }
55
+ /**
56
+ * The gyro sample layout the file actually uses: from the info record's flag when present,
57
+ * otherwise inferred from the first bytes of the gyro record, which may refuse to tell.
58
+ * Undefined without a gyro record and without the flag.
59
+ */
60
+ gyroSampleLayout() {
61
+ this.gyroLayoutPromise ??= this.selectGyroLayout();
62
+ return this.gyroLayoutPromise;
63
+ }
64
+ async selectGyroLayout() {
65
+ const hints = this.gyroLayoutHints();
66
+ if (hints.isRawGyro !== void 0) return selectGyroSampleLayout(hints, void 0);
67
+ const location = this.trailer.locationOf(RecordType.Gyro);
68
+ const head = location === void 0 ? void 0 : await this.readHeadOf(location);
69
+ return selectGyroSampleLayout(hints, head);
70
+ }
71
+ gyroLayoutHints() {
72
+ return {
73
+ isRawGyro: this.info.isRawGyro,
74
+ ranges: this.info.sensorRanges
75
+ };
76
+ }
77
+ readHeadOf(location) {
78
+ const length = Math.min(GYRO_LAYOUT_PROBE_SIZE, location.payload.length);
79
+ return this.source.read(ByteRange.of(location.payload.offset, length));
80
+ }
81
+ };
82
+ //#endregion
83
+ export { TrailerRecords };
@@ -0,0 +1,48 @@
1
+ import { hasErrorCode } from "../../../../shared/errors/GyroViewError.js";
2
+ import { ByteReader } from "../../../../shared/binary/ByteReader.js";
3
+ import { ExposureRecord } from "../../../motion/exposure/ExposureRecord.js";
4
+ //#region ../../packages/core/src/domain/format/records/exposure/parseExposureRecord.ts
5
+ /**
6
+ * Longer than any exposure a camera takes for one frame, night-lapses included; a flipped bit in
7
+ * a float64 shutter time lands far beyond, or at NaN.
8
+ */
9
+ var MAX_PLAUSIBLE_SHUTTER_SECONDS = 60;
10
+ /**
11
+ * Decodes the exposure record into per-frame capture times, from stamps in the camera's unit
12
+ * through `captureTimeOf`, and shutter times; bytes after the last whole entry are ignored.
13
+ * Undefined when an entry cannot be a reading (a stamp past the safe integers or out of order, a
14
+ * shutter time no camera takes, as a flipped bit leaves): entries belong to frames by their
15
+ * position, so a damaged record is left out whole and frame timing falls back to its next source.
16
+ */
17
+ function parseExposureRecord(payload, captureTimeOf) {
18
+ try {
19
+ return decodeEntries(payload, captureTimeOf);
20
+ } catch (error) {
21
+ if (hasErrorCode(error, "binary-unsafe-integer")) return void 0;
22
+ throw error;
23
+ }
24
+ }
25
+ function decodeEntries(payload, captureTimeOf) {
26
+ const count = Math.floor(payload.byteLength / 16);
27
+ const reader = new ByteReader(payload);
28
+ const captureTimes = new Float64Array(count);
29
+ const shutterTimes = new Float64Array(count);
30
+ for (let index = 0; index < count; index += 1) {
31
+ const offset = index * 16;
32
+ captureTimes[index] = captureTimeOf(reader.uint64LeAt(offset + 0));
33
+ shutterTimes[index] = reader.float64LeAt(offset + 8);
34
+ }
35
+ return isInOrder(captureTimes) && shutterTimes.every((time) => isPlausibleShutterTime(time)) ? new ExposureRecord(captureTimes, shutterTimes) : void 0;
36
+ }
37
+ /**
38
+ * Capture times that go back betray a stray stamp, either way: a stamp moved forward makes the
39
+ * next one go back.
40
+ */
41
+ function isInOrder(captureTimes) {
42
+ return captureTimes.every((time, index) => index === 0 || time >= (captureTimes[index - 1] ?? time));
43
+ }
44
+ function isPlausibleShutterTime(time) {
45
+ return time >= 0 && time <= MAX_PLAUSIBLE_SHUTTER_SECONDS;
46
+ }
47
+ //#endregion
48
+ export { parseExposureRecord };
@@ -0,0 +1,39 @@
1
+ import { milliseconds, millisecondsToMicroseconds } from "../../../../shared/units/time.js";
2
+ import "./gyroLayouts.js";
3
+ //#region ../../packages/core/src/domain/format/records/gyro/FloatGyroSampleLayout.ts
4
+ /**
5
+ * 56-byte samples with float64 components, written by cameras before the raw layout.
6
+ */
7
+ var FloatGyroSampleLayout = class {
8
+ name = "float";
9
+ sampleSize = 56;
10
+ /**
11
+ * A flipped exponent bit leaves a float64 far beyond any reading, or next to zero; a flipped
12
+ * fraction bit changes it by less than half.
13
+ */
14
+ flipSteps = {
15
+ acceleration: [],
16
+ angularVelocity: []
17
+ };
18
+ captureTimeOf(stamp) {
19
+ return millisecondsToMicroseconds(milliseconds(stamp));
20
+ }
21
+ timestampAt(reader, offset) {
22
+ return this.captureTimeOf(reader.uint64LeAt(offset + 0));
23
+ }
24
+ accelerationAt(reader, offset) {
25
+ return this.vectorAt(reader, offset + 8);
26
+ }
27
+ angularVelocityAt(reader, offset) {
28
+ return this.vectorAt(reader, offset + 32);
29
+ }
30
+ vectorAt(reader, offset) {
31
+ return [
32
+ reader.float64LeAt(offset),
33
+ reader.float64LeAt(offset + 8),
34
+ reader.float64LeAt(offset + 16)
35
+ ];
36
+ }
37
+ };
38
+ //#endregion
39
+ export { FloatGyroSampleLayout };
@@ -0,0 +1,55 @@
1
+ import { degrees, degreesToRadians } from "../../../../shared/units/angle.js";
2
+ import { microseconds } from "../../../../shared/units/time.js";
3
+ import { DEFAULT_GYROSCOPE_RANGE_DPS, RAW_FULL_SCALE, RAW_HIGH_BIT_WEIGHTS, RAW_ZERO_POINT } from "./gyroLayouts.js";
4
+ //#region ../../packages/core/src/domain/format/records/gyro/RawGyroSampleLayout.ts
5
+ /**
6
+ * Compact 20-byte samples written by X5-era firmware (`is_raw_gyro` = 1).
7
+ */
8
+ var RawGyroSampleLayout = class {
9
+ name = "raw";
10
+ sampleSize = 20;
11
+ flipSteps;
12
+ accelerationScale;
13
+ angularVelocityScale;
14
+ constructor(ranges = {}) {
15
+ const accelerometerG = statedRange(ranges.accelerometerG, 16);
16
+ const gyroscopeDps = statedRange(ranges.gyroscopeDps, DEFAULT_GYROSCOPE_RANGE_DPS);
17
+ this.accelerationScale = accelerometerG / RAW_FULL_SCALE;
18
+ this.angularVelocityScale = degreesToRadians(degrees(gyroscopeDps)) / RAW_FULL_SCALE;
19
+ this.flipSteps = {
20
+ acceleration: RAW_HIGH_BIT_WEIGHTS.map((weight) => weight * this.accelerationScale),
21
+ angularVelocity: RAW_HIGH_BIT_WEIGHTS.map((weight) => weight * this.angularVelocityScale)
22
+ };
23
+ }
24
+ captureTimeOf(stamp) {
25
+ return microseconds(stamp);
26
+ }
27
+ timestampAt(reader, offset) {
28
+ return this.captureTimeOf(reader.uint64LeAt(offset + 0));
29
+ }
30
+ accelerationAt(reader, offset) {
31
+ return this.vectorAt(reader, offset + 8, this.accelerationScale);
32
+ }
33
+ angularVelocityAt(reader, offset) {
34
+ return this.vectorAt(reader, offset + 14, this.angularVelocityScale);
35
+ }
36
+ vectorAt(reader, offset, scale) {
37
+ return [
38
+ this.componentAt(reader, offset, scale),
39
+ this.componentAt(reader, offset + 2, scale),
40
+ this.componentAt(reader, offset + 4, scale)
41
+ ];
42
+ }
43
+ componentAt(reader, offset, scale) {
44
+ return (reader.uint16LeAt(offset) - RAW_ZERO_POINT) * scale;
45
+ }
46
+ };
47
+ /**
48
+ * The stated range, or the default where none is stated. A range of zero, as a flipped bit or a
49
+ * field left unset writes, would scale every reading to nothing.
50
+ */
51
+ function statedRange(range, fallback) {
52
+ return range !== void 0 && range > 0 ? range : fallback;
53
+ }
54
+ //#endregion
55
+ export { RawGyroSampleLayout };
@@ -0,0 +1,82 @@
1
+ import { GyroTrack } from "../../../motion/gyro/GyroTrack.js";
2
+ //#region ../../packages/core/src/domain/format/records/gyro/SampleColumns.ts
3
+ /**
4
+ * How closely the neighbours must agree, and the reading put back meet them, in parts of the
5
+ * step (a sixteenth): looser than real motion curves over one interval and the sensor's noise,
6
+ * tighter than a shock or a vibration would match a bit's weight by chance.
7
+ */
8
+ var STEP_PARTS_TOLERATED = 16;
9
+ /**
10
+ * The samples of a gyro record kept so far, in the columns a {@link GyroTrack} stores them in.
11
+ */
12
+ var SampleColumns = class {
13
+ length = 0;
14
+ recordedTimes;
15
+ accelerations;
16
+ angularVelocities;
17
+ constructor(capacity) {
18
+ this.recordedTimes = new Float64Array(capacity);
19
+ this.accelerations = new Float32Array(capacity * 3);
20
+ this.angularVelocities = new Float32Array(capacity * 3);
21
+ }
22
+ push(sample) {
23
+ this.recordedTimes[this.length] = sample.captureTime;
24
+ this.accelerations.set(sample.acceleration, this.length * 3);
25
+ this.angularVelocities.set(sample.angularVelocity, this.length * 3);
26
+ this.length += 1;
27
+ }
28
+ /**
29
+ * Leaves out samples a flipped high bit moved: one component a bit's weight (one of `steps`)
30
+ * from where its agreeing neighbours put it. A shock or a vibration leaps too, but between
31
+ * neighbours that disagree, or by another amount. Compacts in place; each check reads only
32
+ * samples not yet moved over.
33
+ */
34
+ dropFlippedBits(steps) {
35
+ let kept = 0;
36
+ for (let index = 0; index < this.length; index += 1) {
37
+ if (this.isFlippedAt(index, steps)) continue;
38
+ this.moveSample(index, kept);
39
+ kept += 1;
40
+ }
41
+ this.length = kept;
42
+ }
43
+ /**
44
+ * The stamps as recorded, to be mended before they make a track.
45
+ */
46
+ captureTimes() {
47
+ return this.recordedTimes.subarray(0, this.length);
48
+ }
49
+ toTrack(captureTimes) {
50
+ const vectors = this.length * 3;
51
+ return new GyroTrack(captureTimes, this.accelerations.subarray(0, vectors), this.angularVelocities.subarray(0, vectors));
52
+ }
53
+ isFlippedAt(index, steps) {
54
+ return index > 0 && index < this.length - 1 && (hasFlippedComponent(this.accelerations, index, steps.acceleration) || hasFlippedComponent(this.angularVelocities, index, steps.angularVelocity));
55
+ }
56
+ moveSample(from, to) {
57
+ if (from === to) return;
58
+ this.recordedTimes[to] = this.recordedTimes[from] ?? 0;
59
+ for (const vectors of [this.accelerations, this.angularVelocities]) vectors.copyWithin(to * 3, from * 3, (from + 1) * 3);
60
+ }
61
+ };
62
+ function hasFlippedComponent(vectors, index, steps) {
63
+ for (let axis = 0; axis < 3; axis += 1) if (isFlippedComponent(vectors, index * 3 + axis, steps)) return true;
64
+ return false;
65
+ }
66
+ /**
67
+ * The component at `offset` lies one of `steps` from where its agreeing neighbours put it. Plain
68
+ * loops, no closures: this runs for every component of every sample of an hour-long record.
69
+ */
70
+ function isFlippedComponent(vectors, offset, steps) {
71
+ const before = vectors[offset - 3] ?? 0;
72
+ const after = vectors[offset + 3] ?? 0;
73
+ const leap = Math.abs((vectors[offset] ?? 0) - (before + after) / 2);
74
+ const disagreement = Math.abs(before - after);
75
+ for (const step of steps) {
76
+ const tolerance = step / STEP_PARTS_TOLERATED;
77
+ if (disagreement <= tolerance && Math.abs(leap - step) <= tolerance) return true;
78
+ }
79
+ return false;
80
+ }
81
+ //#endregion
82
+ export { SampleColumns };
@@ -0,0 +1,12 @@
1
+ //#region ../../packages/core/src/domain/format/records/gyro/gyroLayouts.ts
2
+ var RAW_ZERO_POINT = 32768;
3
+ var RAW_FULL_SCALE = 32768;
4
+ /**
5
+ * The weights of a raw component's two highest bits: a flip there moves a reading by its full
6
+ * range or half of it, still within the plausible bounds. A flip lower down moves it too little
7
+ * to matter.
8
+ */
9
+ var RAW_HIGH_BIT_WEIGHTS = [RAW_FULL_SCALE, RAW_FULL_SCALE / 2];
10
+ var DEFAULT_GYROSCOPE_RANGE_DPS = 2e3;
11
+ //#endregion
12
+ export { DEFAULT_GYROSCOPE_RANGE_DPS, RAW_FULL_SCALE, RAW_HIGH_BIT_WEIGHTS, RAW_ZERO_POINT };
@@ -0,0 +1,129 @@
1
+ import { GyroViewError, hasErrorCode } from "../../../../shared/errors/GyroViewError.js";
2
+ import { ByteReader } from "../../../../shared/binary/ByteReader.js";
3
+ import "./gyroLayouts.js";
4
+ import { FloatGyroSampleLayout } from "./FloatGyroSampleLayout.js";
5
+ import { RawGyroSampleLayout } from "./RawGyroSampleLayout.js";
6
+ import { SampleColumns } from "./SampleColumns.js";
7
+ import { medianStep } from "../../../motion/gyro/medianStep.js";
8
+ import { repairedTimeline } from "../../../motion/gyro/repairedTimeline.js";
9
+ //#region ../../packages/core/src/domain/format/records/gyro/parseGyroRecord.ts
10
+ /**
11
+ * Plausible spacing between consecutive samples when guessing the layout: 100 Hz to 10 kHz.
12
+ */
13
+ var MIN_PLAUSIBLE_INTERVAL_US = 100;
14
+ var MAX_PLAUSIBLE_INTERVAL_US = 1e4;
15
+ /**
16
+ * The samples the layout is told from when the info record does not say: enough that a few
17
+ * uninitialised or glitched stamps at the start are outvoted, in one small read.
18
+ */
19
+ var SAMPLES_TO_GUESS_FROM = 32;
20
+ var MIN_SAMPLES_TO_GUESS_FROM = 2;
21
+ /**
22
+ * Readings past these are no motion a camera makes: the X5 declares a 32 g and a 2000 degrees a
23
+ * second (35 rad/s) range. A flipped bit in a float64 reading lands far beyond them, or at NaN.
24
+ */
25
+ var MAX_PLAUSIBLE_ACCELERATION_G = 64;
26
+ var MAX_PLAUSIBLE_ANGULAR_VELOCITY_RAD_S = 100;
27
+ /**
28
+ * Enough leading bytes of a gyro record to tell its layout, in samples of the larger layout.
29
+ */
30
+ var GYRO_LAYOUT_PROBE_SIZE = 56 * SAMPLES_TO_GUESS_FROM;
31
+ /**
32
+ * The sample layout of the gyro record: from the info record's flag when present, otherwise from
33
+ * the timestamps the candidates decode in the record's first bytes (`head`). Undefined when
34
+ * there is neither a flag nor a record to look at.
35
+ */
36
+ function selectGyroSampleLayout(hints, head) {
37
+ const raw = new RawGyroSampleLayout(hints.ranges);
38
+ const float = new FloatGyroSampleLayout();
39
+ if (hints.isRawGyro !== void 0) return hints.isRawGyro ? raw : float;
40
+ if (head === void 0) return void 0;
41
+ const plausible = [raw, float].filter((layout) => hasPlausibleTimestamps(head, layout));
42
+ const [winner] = plausible;
43
+ if (winner && plausible.length === 1) return winner;
44
+ throw new GyroViewError("unsupported-gyro-record", `cannot tell the gyro sample layout from the first ${head.byteLength} bytes without the info record`);
45
+ }
46
+ /**
47
+ * Decodes the gyro record payload into a {@link GyroTrack} with the given layout. Whole samples
48
+ * only: a partial sample at the end is tolerated and reported, never rejected. So is a sample whose
49
+ * bytes cannot be a reading (a stamp past the safe integers, a value no camera measures, a
50
+ * component a flipped bit moved, a slot left all zero): each sample carries its own time, so the
51
+ * others still count.
52
+ * Stamps that stray from their neighbours are mended (see {@link repairedTimeline}).
53
+ */
54
+ function parseGyroRecord(payload, layout) {
55
+ const count = Math.floor(payload.byteLength / layout.sampleSize);
56
+ const reader = new ByteReader(payload);
57
+ const columns = new SampleColumns(count);
58
+ for (let index = 0; index < count; index += 1) {
59
+ const sample = readableSampleAt(reader, layout, index * layout.sampleSize);
60
+ if (sample) columns.push(sample);
61
+ }
62
+ columns.dropFlippedBits(layout.flipSteps);
63
+ const timeline = repairedTimeline(columns.captureTimes());
64
+ return {
65
+ track: columns.toTrack(timeline.times),
66
+ layout: layout.name,
67
+ strayBytes: payload.byteLength % layout.sampleSize,
68
+ damagedSamples: count - columns.length,
69
+ mendedStamps: timeline.mended
70
+ };
71
+ }
72
+ function readableSampleAt(reader, layout, offset) {
73
+ try {
74
+ const captureTime = layout.timestampAt(reader, offset);
75
+ if (captureTime === 0 && isUnwritten(reader.bytesAt(offset, layout.sampleSize))) return void 0;
76
+ const sample = {
77
+ captureTime,
78
+ acceleration: layout.accelerationAt(reader, offset),
79
+ angularVelocity: layout.angularVelocityAt(reader, offset)
80
+ };
81
+ return isWithin(sample.acceleration, MAX_PLAUSIBLE_ACCELERATION_G) && isWithin(sample.angularVelocity, MAX_PLAUSIBLE_ANGULAR_VELOCITY_RAD_S) ? sample : void 0;
82
+ } catch (error) {
83
+ if (hasErrorCode(error, "binary-unsafe-integer")) return void 0;
84
+ throw error;
85
+ }
86
+ }
87
+ /**
88
+ * A sample left all zero, as a camera leaves the first slots it never wrote: in the raw layout
89
+ * it would read the negative full range on every axis, which no sensor measures.
90
+ */
91
+ function isUnwritten(sample) {
92
+ return sample.every((byte) => byte === 0);
93
+ }
94
+ function isWithin(vector, bound) {
95
+ return vector.every((component) => Math.abs(component) <= bound);
96
+ }
97
+ /**
98
+ * A layout fits when the stamps it decodes step by a plausible IMU interval: the median step,
99
+ * so a few stamps left at zero or glitched at the start do not decide.
100
+ */
101
+ function hasPlausibleTimestamps(payload, layout) {
102
+ const stamps = stampsOf(payload, layout);
103
+ if (stamps.length < MIN_SAMPLES_TO_GUESS_FROM) return false;
104
+ const median = medianStep(stamps);
105
+ return median >= MIN_PLAUSIBLE_INTERVAL_US && median <= MAX_PLAUSIBLE_INTERVAL_US;
106
+ }
107
+ /**
108
+ * The stamps `layout` decodes from the leading samples; one that cannot be a stamp is skipped.
109
+ */
110
+ function stampsOf(payload, layout) {
111
+ const reader = new ByteReader(payload);
112
+ const count = Math.floor(payload.byteLength / layout.sampleSize);
113
+ const stamps = [];
114
+ for (let index = 0; index < count; index += 1) {
115
+ const stamp = stampAt(reader, layout, index * layout.sampleSize);
116
+ if (stamp !== void 0) stamps.push(stamp);
117
+ }
118
+ return Float64Array.from(stamps);
119
+ }
120
+ function stampAt(reader, layout, offset) {
121
+ try {
122
+ return layout.timestampAt(reader, offset);
123
+ } catch (error) {
124
+ if (hasErrorCode(error, "binary-unsafe-integer")) return void 0;
125
+ throw error;
126
+ }
127
+ }
128
+ //#endregion
129
+ export { GYRO_LAYOUT_PROBE_SIZE, parseGyroRecord, selectGyroSampleLayout };
@@ -0,0 +1,23 @@
1
+ import { GyroViewError } from "../../../shared/errors/GyroViewError.js";
2
+ import { ByteReader } from "../../../shared/binary/ByteReader.js";
3
+ //#region ../../packages/core/src/domain/format/trailer/RecordHeader.ts
4
+ /**
5
+ * The 6-byte header that follows each record payload.
6
+ */
7
+ var RecordHeader = class RecordHeader {
8
+ format;
9
+ id;
10
+ payloadSize;
11
+ constructor(format, id, payloadSize) {
12
+ this.format = format;
13
+ this.id = id;
14
+ this.payloadSize = payloadSize;
15
+ }
16
+ static parse(bytes) {
17
+ if (bytes.byteLength !== 6) throw new GyroViewError("invalid-trailer", `record header must be 6 bytes, got ${bytes.byteLength}`);
18
+ const reader = new ByteReader(bytes);
19
+ return new RecordHeader(reader.uint8At(0), reader.uint8At(1), reader.uint32LeAt(2));
20
+ }
21
+ };
22
+ //#endregion
23
+ export { RecordHeader };