@bubo-squared/gyroview 0.3.0 → 0.3.1

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 CHANGED
@@ -3,6 +3,22 @@
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.3.1 (2026-09-30)
7
+
8
+ New:
9
+
10
+ - Every error has a `category`, derived from its code, that says whose side the failure is on:
11
+ `browser`, `recording`, `source`, `usage` or `internal`; `GYRO_VIEW_ERROR_CATEGORIES` and the
12
+ type `GyroViewErrorCategory` list them. The README's "When a recording cannot play" gives the
13
+ codes of each, and what a `<video>` fallback can and cannot do (ADR 0030).
14
+ - The `webcodecs-unavailable` error: the browser has no WebCodecs, on a page not served over
15
+ HTTPS or in an old browser.
16
+
17
+ What a page may notice:
18
+
19
+ - A page on plain HTTP, or a browser without WebCodecs, hears `webcodecs-unavailable` where it
20
+ heard `codec-unsupported`, whose message blamed the codec.
21
+
6
22
  ## 0.3.0 (2026-09-30)
7
23
 
8
24
  What a page may notice:
package/README.md CHANGED
@@ -5,6 +5,9 @@ camera's dual-fisheye file directly, over HTTP byte ranges or from a local file,
5
5
  lenses in hardware with WebCodecs, and stitches and gyro-stabilizes them on the GPU. No Insta360
6
6
  Studio export step.
7
7
 
8
+ **Try it first:** [insv-player.com](https://insv-player.com/) plays `.insv` files with this
9
+ package. Drop your own recording to check that your camera and browser work before you install.
10
+
8
11
  ## Install
9
12
 
10
13
  ```sh
@@ -188,7 +191,8 @@ loads a recording only for the player in view, removing `src` from the others.
188
191
 
189
192
  ## Requirements
190
193
 
191
- - **A secure page.** WebCodecs exists only on `https://` pages, or `http://localhost`.
194
+ - **A secure page.** WebCodecs exists only on `https://` pages, or `http://localhost`; elsewhere
195
+ the player fails with `webcodecs-unavailable`.
192
196
  - **Recordings served in byte ranges.** The server answers `Range` requests with `206`, and
193
197
  sends CORS headers when the recordings live on another origin than the page:
194
198
 
@@ -205,17 +209,20 @@ loads a recording only for the player in view, removing `src` from the others.
205
209
  `Access-Control-Allow-Credentials: true`.
206
210
 
207
211
  - **A hardware HEVC decoder.** 5.7K plays on recent laptops and phones; 8K needs a Level 6
208
- decoder (Apple Silicon, recent NVIDIA and Intel). A recording the browser cannot decode fails
209
- with the `codec-unsupported` error.
212
+ decoder (Apple Silicon, recent NVIDIA and Intel). On Linux, Chrome reaches the decoder only
213
+ through VA-API: an Intel or AMD GPU whose driver offers HEVC, not NVIDIA's own driver or a
214
+ virtual machine (`chrome://gpu` lists `Decode hevc main` under Video Acceleration
215
+ Information when it can). H.264 recordings do not need it. A recording the browser cannot
216
+ decode fails with the `codec-unsupported` error.
210
217
 
211
218
  The supported browsers, with the oldest versions that have what the player uses (WebCodecs,
212
219
  WebGL 2, container queries, and on iPhone `ManagedMediaSource` for the sound):
213
220
 
214
- | Browser | From | Notes |
215
- | --------------------- | ---- | -------------------------------------------------------- |
216
- | Chrome, Edge desktop | 107 | HEVC is decoded in hardware from this version on. |
217
- | Safari on macOS | 16.4 | |
218
- | Safari on iPhone/iPad | 17.1 | 16.4 to 17.0 play without sound, with a `warning` event. |
221
+ | Browser | From | Notes |
222
+ | --------------------- | ---- | ----------------------------------------------------------------- |
223
+ | Chrome, Edge desktop | 107 | HEVC is decoded in hardware from this version on (Linux: VA-API). |
224
+ | Safari on macOS | 16.4 | |
225
+ | Safari on iPhone/iPad | 17.1 | 16.4 to 17.0 play without sound, with a `warning` event. |
219
226
 
220
227
  Firefox and Chrome on Android are untested: they play what their decoders accept.
221
228
 
@@ -224,8 +231,10 @@ Until 1.0, a minor version may change the API; [CHANGELOG.md](./CHANGELOG.md) sa
224
231
  ## Reference
225
232
 
226
233
  Every attribute, method, event and keyboard shortcut is in the
227
- [project README](https://github.com/bubo-squared/GyroView#using-the-player); hosting and the
228
- error codes are in [docs/DEPLOYMENT.md](https://github.com/bubo-squared/GyroView/blob/main/docs/DEPLOYMENT.md).
234
+ [project README](https://github.com/bubo-squared/GyroView#using-the-player), and what a failure
235
+ tells a page, with what a `<video>` fallback can do, under
236
+ [When a recording cannot play](https://github.com/bubo-squared/GyroView#when-a-recording-cannot-play);
237
+ hosting and the error codes are in [docs/DEPLOYMENT.md](https://github.com/bubo-squared/GyroView/blob/main/docs/DEPLOYMENT.md).
229
238
 
230
239
  ## License
231
240
 
package/dist/index.d.ts CHANGED
@@ -261,7 +261,7 @@ interface DragDelta {
261
261
  export type ViewMode = "raw-lenses" | "equirectangular" | "normal";
262
262
  export declare const VIEW_MODES: readonly ViewMode[];
263
263
  /**
264
- * Stable machine-readable failure categories. Embedders switch on these; messages are for humans.
264
+ * Stable machine-readable failure codes. Embedders switch on these; messages are for humans.
265
265
  */
266
266
  export declare const GYRO_VIEW_ERROR_CODES: readonly [
267
267
  "binary-out-of-bounds",
@@ -291,12 +291,30 @@ export declare const GYRO_VIEW_ERROR_CODES: readonly [
291
291
  "unsupported-container",
292
292
  "unsupported-gyro-record",
293
293
  "unsupported-info-format",
294
- "unsupported-layout"
294
+ "unsupported-layout",
295
+ "webcodecs-unavailable"
295
296
  ];
296
297
  export type GyroViewErrorCode = (typeof GYRO_VIEW_ERROR_CODES)[number];
298
+ /**
299
+ * Whose side a failure is on, for embedders that handle failures by kind: the browser cannot
300
+ * decode or draw the recording, the file is not one the player can play, its bytes could not be
301
+ * read, the page misused the API, or the player failed in a way it did not expect.
302
+ */
303
+ export declare const GYRO_VIEW_ERROR_CATEGORIES: readonly [
304
+ "browser",
305
+ "recording",
306
+ "source",
307
+ "usage",
308
+ "internal"
309
+ ];
310
+ export type GyroViewErrorCategory = (typeof GYRO_VIEW_ERROR_CATEGORIES)[number];
297
311
  export declare function isGyroViewErrorCode(value: unknown): value is GyroViewErrorCode;
298
312
  export declare class GyroViewError extends Error {
299
313
  readonly code: GyroViewErrorCode;
314
+ /**
315
+ * Its own property rather than a getter, so a serialized or logged copy of the error keeps it.
316
+ */
317
+ readonly category: GyroViewErrorCategory;
300
318
  constructor(code: GyroViewErrorCode, message: string, options?: ErrorOptions);
301
319
  }
302
320
  export declare function hasErrorCode(error: unknown, code: GyroViewErrorCode): boolean;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { GYRO_VIEW_ERROR_CODES, GyroViewError, hasErrorCode, isGyroViewErrorCode } from "./packages/core/src/shared/errors/GyroViewError.js";
1
+ import { GYRO_VIEW_ERROR_CATEGORIES, GYRO_VIEW_ERROR_CODES, GyroViewError, hasErrorCode, isGyroViewErrorCode } from "./packages/core/src/shared/errors/GyroViewError.js";
2
2
  import { STABILIZATION_MODES } from "./packages/core/src/domain/motion/stabilization/Stabilizer.js";
3
3
  import { PICTURE_QUALITIES } from "./packages/core/src/domain/view/PictureQuality.js";
4
4
  import { VIEW_MODES } from "./packages/core/src/domain/view/ViewMode.js";
@@ -10,4 +10,4 @@ import { inspectRecording } from "./packages/player/src/inspectRecording.js";
10
10
  import { GyroViewElement } from "./packages/player/src/element/GyroViewElement.js";
11
11
  import { GYRO_VIEW_TAG, defineGyroView } from "./packages/player/src/element/defineGyroView.js";
12
12
  import "./packages/player/src/index.js";
13
- export { GYRO_VIEW_ERROR_CODES, GYRO_VIEW_TAG, GyroViewElement, GyroViewError, PICTURE_QUALITIES, Player, STABILIZATION_MODES, VIEW_MODES, attachKeyboard, attachViewGestures, createBrowserPlayer, defineGyroView, hasErrorCode, inspectRecording, isGyroViewErrorCode };
13
+ export { GYRO_VIEW_ERROR_CATEGORIES, GYRO_VIEW_ERROR_CODES, GYRO_VIEW_TAG, GyroViewElement, GyroViewError, PICTURE_QUALITIES, Player, STABILIZATION_MODES, VIEW_MODES, attachKeyboard, attachViewGestures, createBrowserPlayer, defineGyroView, hasErrorCode, inspectRecording, isGyroViewErrorCode };
@@ -15,10 +15,11 @@ var HARDWARE_ACCELERATION = "no-preference";
15
15
  */
16
16
  var WebCodecsVideoDecoderPort = class {
17
17
  /**
18
- * A configuration WebCodecs finds malformed is one it does not support.
18
+ * A configuration WebCodecs finds malformed is one it does not support. A browser without
19
+ * WebCodecs refuses to answer, since no configuration would do.
19
20
  */
20
21
  async isSupported(configuration) {
21
- if (typeof VideoDecoder === "undefined") return false;
22
+ if (typeof VideoDecoder === "undefined") throw webCodecsUnavailable();
22
23
  try {
23
24
  return (await VideoDecoder.isConfigSupported(this.toWebCodecsConfig(configuration))).supported === true;
24
25
  } catch {
@@ -33,7 +34,7 @@ var WebCodecsVideoDecoderPort = class {
33
34
  }
34
35
  }
35
36
  configureDecoder(configuration, callbacks) {
36
- if (typeof VideoDecoder === "undefined") throw new GyroViewError("codec-unsupported", "this browser has no WebCodecs VideoDecoder");
37
+ if (typeof VideoDecoder === "undefined") throw webCodecsUnavailable();
37
38
  return new WebCodecsDecoderHandle(this.toWebCodecsConfig(configuration), callbacks);
38
39
  }
39
40
  toWebCodecsConfig(configuration) {
@@ -114,6 +115,14 @@ var WebCodecsDecoderHandle = class {
114
115
  if (this.decoder.state !== "closed") this.decoder.close();
115
116
  }
116
117
  };
118
+ /**
119
+ * Browsers expose WebCodecs only in secure contexts, so a page served over plain HTTP is the
120
+ * likelier cause than an old browser, and the one its developer can fix.
121
+ */
122
+ function webCodecsUnavailable() {
123
+ const reason = globalThis.isSecureContext ? "this browser has no WebCodecs VideoDecoder" : "WebCodecs exists only in secure contexts (HTTPS), and this page is not one";
124
+ return new GyroViewError("webcodecs-unavailable", reason);
125
+ }
117
126
  function chunkOf(packet) {
118
127
  return new EncodedVideoChunk({
119
128
  type: packet.isKeyFrame ? "key" : "delta",
@@ -28,8 +28,8 @@ var KEY_FRAME_LATE = {
28
28
  * a deadline. The host resolves `deadline` once the probe has taken too long (the core has no
29
29
  * timers); sources still undecided then report `timed-out`, or `key-frame-late` while their key
30
30
  * frame was still being read, and their decoders are closed. A track that cannot be read rejects
31
- * the probe with its own failure, and the other sources' decoders are closed then, not at the
32
- * deadline.
31
+ * the probe with its own failure, as a platform without decoders does, and the other sources'
32
+ * decoders are closed then, not at the deadline.
33
33
  */
34
34
  async function probeDecoding(frameSources, decoderPort, deadline) {
35
35
  const probes = frameSources.map((track) => new SourceProbe(track, decoderPort));
@@ -1,7 +1,7 @@
1
1
  import { degrees } from "./shared/units/angle.js";
2
2
  import "./domain/optics/EquidistantModel.js";
3
3
  import "./domain/format/calibration/CalibrationVersion.js";
4
- import { GYRO_VIEW_ERROR_CODES, GyroViewError, asGyroViewError, ensureIndexInRange, ensureInvariant, hasErrorCode, isAbortError, isGyroViewErrorCode, messageOf } from "./shared/errors/GyroViewError.js";
4
+ import { GYRO_VIEW_ERROR_CATEGORIES, GYRO_VIEW_ERROR_CODES, GyroViewError, asGyroViewError, ensureIndexInRange, ensureInvariant, hasErrorCode, isAbortError, isGyroViewErrorCode, messageOf } from "./shared/errors/GyroViewError.js";
5
5
  import { lazy } from "./shared/lazy.js";
6
6
  import { microseconds, microsecondsToSeconds, milliseconds, seconds, secondsToMicroseconds } from "./shared/units/time.js";
7
7
  import "./domain/motion/exposure/ExposureRecord.js";
@@ -64,4 +64,4 @@ import { WallClock } from "./application/playback/WallClock.js";
64
64
  import { isFlowing } from "./domain/playback/PlayerState.js";
65
65
  import { TypedEmitter } from "./shared/events/TypedEmitter.js";
66
66
  import { PlaybackSession } from "./application/playback/PlaybackSession.js";
67
- export { ByteRange, DEFAULT_FRAMING, DEFAULT_PICTURE_QUALITY, DEFAULT_STABILIZATION_MODE, DEFAULT_VIEW_MODE, Deferred, Ending, GYRO_VIEW_ERROR_CODES, GainMatchingFrameSink, GyroViewError, IDENTITY_MATRIX3, ITERATION_END, Outbox, PICTURE_QUALITIES, PlaybackSession, RecordingBuffer, RunStop, SCREEN_CENTRE, STABILIZATION_MODES, STOPPED, SourceByteStream, StabilizingFrameSink, TypedEmitter, VIEW_MODES, WallClock, asGyroViewError, aspectOf, aspectOfArea, buildStitchingSetup, clamp, clampView, degrees, detectLensLayout, downloadPolicyFor, ensureIndexInRange, ensureInvariant, fileNameOfUrl, hasErrorCode, inspectRecording, isAbortError, isFlowing, isGyroViewErrorCode, isSameFraming, isSameView, keysOf, lazy, lensFrameOrder, locateOtherLensFile, lookAt, messageOf, microseconds, microsecondsToSeconds, milliseconds, pixelRatioCapOf, planeHalfExtentOf, probeDecoding, readRecording, readSampleTable, seconds, secondsToMicroseconds, stabilizerFor, startFileDownload, timeRecording, viewModeRulesFor, zoomStepsForPinch };
67
+ export { ByteRange, DEFAULT_FRAMING, DEFAULT_PICTURE_QUALITY, DEFAULT_STABILIZATION_MODE, DEFAULT_VIEW_MODE, Deferred, Ending, GYRO_VIEW_ERROR_CATEGORIES, GYRO_VIEW_ERROR_CODES, GainMatchingFrameSink, GyroViewError, IDENTITY_MATRIX3, ITERATION_END, Outbox, PICTURE_QUALITIES, PlaybackSession, RecordingBuffer, RunStop, SCREEN_CENTRE, STABILIZATION_MODES, STOPPED, SourceByteStream, StabilizingFrameSink, TypedEmitter, VIEW_MODES, WallClock, asGyroViewError, aspectOf, aspectOfArea, buildStitchingSetup, clamp, clampView, degrees, detectLensLayout, downloadPolicyFor, ensureIndexInRange, ensureInvariant, fileNameOfUrl, hasErrorCode, inspectRecording, isAbortError, isFlowing, isGyroViewErrorCode, isSameFraming, isSameView, keysOf, lazy, lensFrameOrder, locateOtherLensFile, lookAt, messageOf, microseconds, microsecondsToSeconds, milliseconds, pixelRatioCapOf, planeHalfExtentOf, probeDecoding, readRecording, readSampleTable, seconds, secondsToMicroseconds, stabilizerFor, startFileDownload, timeRecording, viewModeRulesFor, zoomStepsForPinch };
@@ -1,6 +1,6 @@
1
1
  //#region ../../packages/core/src/shared/errors/GyroViewError.ts
2
2
  /**
3
- * Stable machine-readable failure categories. Embedders switch on these; messages are for humans.
3
+ * Stable machine-readable failure codes. Embedders switch on these; messages are for humans.
4
4
  */
5
5
  var GYRO_VIEW_ERROR_CODES = [
6
6
  "binary-out-of-bounds",
@@ -30,17 +30,69 @@ var GYRO_VIEW_ERROR_CODES = [
30
30
  "unsupported-container",
31
31
  "unsupported-gyro-record",
32
32
  "unsupported-info-format",
33
- "unsupported-layout"
33
+ "unsupported-layout",
34
+ "webcodecs-unavailable"
34
35
  ];
36
+ /**
37
+ * Whose side a failure is on, for embedders that handle failures by kind: the browser cannot
38
+ * decode or draw the recording, the file is not one the player can play, its bytes could not be
39
+ * read, the page misused the API, or the player failed in a way it did not expect.
40
+ */
41
+ var GYRO_VIEW_ERROR_CATEGORIES = [
42
+ "browser",
43
+ "recording",
44
+ "source",
45
+ "usage",
46
+ "internal"
47
+ ];
48
+ /**
49
+ * A record, so a new code cannot be added without its category.
50
+ */
51
+ var CATEGORY_OF_CODE = {
52
+ "binary-out-of-bounds": "recording",
53
+ "binary-unsafe-integer": "recording",
54
+ "codec-unsupported": "browser",
55
+ cors: "source",
56
+ decode: "browser",
57
+ "embed-destroyed": "usage",
58
+ "index-out-of-range": "internal",
59
+ "invalid-argument": "usage",
60
+ "invalid-byte-range": "recording",
61
+ "invalid-calibration": "recording",
62
+ "invalid-protobuf": "recording",
63
+ "invalid-trailer": "recording",
64
+ "invariant-violation": "internal",
65
+ "missing-second-file": "recording",
66
+ "no-calibration": "recording",
67
+ "no-info-record": "recording",
68
+ "no-key-frame": "recording",
69
+ "playback-blocked": "browser",
70
+ "range-unsupported": "source",
71
+ "render-unavailable": "browser",
72
+ "source-changed": "source",
73
+ "source-truncated": "source",
74
+ "source-unreadable": "source",
75
+ "unsupported-calibration": "recording",
76
+ "unsupported-container": "recording",
77
+ "unsupported-gyro-record": "recording",
78
+ "unsupported-info-format": "recording",
79
+ "unsupported-layout": "recording",
80
+ "webcodecs-unavailable": "browser"
81
+ };
35
82
  function isGyroViewErrorCode(value) {
36
83
  return typeof value === "string" && GYRO_VIEW_ERROR_CODES.includes(value);
37
84
  }
38
85
  var GyroViewError = class extends Error {
39
86
  code;
87
+ /**
88
+ * Its own property rather than a getter, so a serialized or logged copy of the error keeps it.
89
+ */
90
+ category;
40
91
  constructor(code, message, options) {
41
92
  super(message, options);
42
93
  this.code = code;
43
94
  this.name = "GyroViewError";
95
+ this.category = CATEGORY_OF_CODE[code];
44
96
  }
45
97
  };
46
98
  /**
@@ -79,4 +131,4 @@ function ensureIndexInRange(index, length, subject) {
79
131
  if (!Number.isSafeInteger(index) || index < 0 || index >= length) throw new GyroViewError("index-out-of-range", `${subject} index ${index} is outside 0..${length - 1}`);
80
132
  }
81
133
  //#endregion
82
- export { GYRO_VIEW_ERROR_CODES, GyroViewError, asGyroViewError, ensureIndexInRange, ensureInvariant, hasErrorCode, isAbortError, isGyroViewErrorCode, messageOf };
134
+ export { GYRO_VIEW_ERROR_CATEGORIES, GYRO_VIEW_ERROR_CODES, GyroViewError, asGyroViewError, ensureIndexInRange, ensureInvariant, hasErrorCode, isAbortError, isGyroViewErrorCode, messageOf };
@@ -78,7 +78,8 @@ var DEFAULT_MESSAGES = {
78
78
  "unsupported-container": UNREADABLE,
79
79
  "unsupported-gyro-record": UNREADABLE,
80
80
  "unsupported-info-format": UNREADABLE,
81
- "unsupported-layout": UNREADABLE
81
+ "unsupported-layout": UNREADABLE,
82
+ "webcodecs-unavailable": UNSUPPORTED_BROWSER
82
83
  }
83
84
  };
84
85
  /**