@bubo-squared/gyroview 0.1.0 → 0.3.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 (95) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +21 -5
  3. package/dist/index.d.ts +24 -2
  4. package/dist/packages/adapters/fetch/src/HttpByteStream.js +174 -0
  5. package/dist/packages/adapters/fetch/src/HttpRangeSource.js +14 -170
  6. package/dist/packages/adapters/fetch/src/HttpResource.js +192 -0
  7. package/dist/packages/adapters/fetch/src/contentRange.js +15 -0
  8. package/dist/packages/adapters/fetch/src/httpRequest.js +15 -4
  9. package/dist/packages/adapters/fetch/src/index.js +3 -1
  10. package/dist/packages/adapters/fetch/src/passingFailures.js +24 -0
  11. package/dist/packages/adapters/fetch/src/rangeFailures.js +17 -0
  12. package/dist/packages/adapters/fetch/src/recordingVersion.js +37 -0
  13. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioPackager.js +129 -0
  14. package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +80 -0
  15. package/dist/packages/adapters/mediabunny/src/SegmentChannel.js +58 -6
  16. package/dist/packages/adapters/mediabunny/src/decoderConfigurations.js +28 -0
  17. package/dist/packages/adapters/mediabunny/src/index.js +3 -2
  18. package/dist/packages/adapters/three/src/shaders/rawLenses.frag.js +1 -1
  19. package/dist/packages/core/src/application/download/BlockStore.js +97 -0
  20. package/dist/packages/core/src/application/download/DownloadedAudioSamples.js +50 -0
  21. package/dist/packages/core/src/application/download/DownloadedVideoTrack.js +122 -0
  22. package/dist/packages/core/src/application/download/FileDownload.js +183 -0
  23. package/dist/packages/core/src/application/download/RecordingBuffer.js +22 -0
  24. package/dist/packages/core/src/application/download/SampleCursor.js +88 -0
  25. package/dist/packages/core/src/application/download/SourceByteStream.js +46 -0
  26. package/dist/packages/core/src/application/download/Transfers.js +75 -0
  27. package/dist/packages/core/src/application/download/startFileDownload.js +37 -0
  28. package/dist/packages/core/src/application/playback/Buffering.js +53 -0
  29. package/dist/packages/core/src/application/playback/DecodePipeline.js +11 -7
  30. package/dist/packages/core/src/application/playback/DecodeRun.js +15 -0
  31. package/dist/packages/core/src/application/playback/PlaybackSession.js +18 -27
  32. package/dist/packages/core/src/application/playback/keyframeTimeAt.js +1 -1
  33. package/dist/packages/core/src/application/playback/probeDecoding.js +15 -1
  34. package/dist/packages/core/src/application/recording/readSampleTable.js +27 -0
  35. package/dist/packages/core/src/application/recording/timeRecording.js +3 -3
  36. package/dist/packages/core/src/domain/container/KeyframeRule.js +7 -0
  37. package/dist/packages/core/src/domain/container/SampleTable.js +29 -0
  38. package/dist/packages/core/src/domain/container/TrackSampleTable.js +96 -0
  39. package/dist/packages/core/src/domain/container/presentationOrder.js +10 -0
  40. package/dist/packages/core/src/domain/download/DownloadPolicy.js +41 -0
  41. package/dist/packages/core/src/domain/download/isReadyToResume.js +27 -0
  42. package/dist/packages/core/src/domain/download/planDownloads.js +175 -0
  43. package/dist/packages/core/src/domain/download/trackNeeds.js +32 -0
  44. package/dist/packages/core/src/domain/format/boxes/boxHeader.js +44 -0
  45. package/dist/packages/core/src/domain/format/boxes/scanBoxes.js +7 -34
  46. package/dist/packages/core/src/domain/format/mp4/keyframeRules.js +81 -0
  47. package/dist/packages/core/src/domain/format/mp4/movieBoxes.js +52 -0
  48. package/dist/packages/core/src/domain/format/mp4/mp4BoxTypes.js +46 -0
  49. package/dist/packages/core/src/domain/format/mp4/mp4Layouts.js +183 -0
  50. package/dist/packages/core/src/domain/format/mp4/parseMovie.js +67 -0
  51. package/dist/packages/core/src/domain/format/mp4/readTable.js +31 -0
  52. package/dist/packages/core/src/domain/format/mp4/sampleLocations.js +69 -0
  53. package/dist/packages/core/src/domain/format/mp4/sampleTiming.js +109 -0
  54. package/dist/packages/core/src/domain/format/mp4/trackHeaders.js +75 -0
  55. package/dist/packages/core/src/domain/view/lensTilesView.js +2 -2
  56. package/dist/packages/core/src/domain/view/magnification.js +1 -1
  57. package/dist/packages/core/src/domain/view/panoramaView.js +1 -1
  58. package/dist/packages/core/src/index.js +9 -1
  59. package/dist/packages/core/src/shared/async/iteration.js +24 -0
  60. package/dist/packages/core/src/shared/binary/ByteRange.js +12 -0
  61. package/dist/packages/core/src/shared/binary/ByteRangeSet.js +85 -0
  62. package/dist/packages/core/src/shared/binary/ByteReader.js +21 -3
  63. package/dist/packages/core/src/shared/binary/concatenated.js +15 -0
  64. package/dist/packages/core/src/shared/errors/GyroViewError.js +1 -0
  65. package/dist/packages/core/src/shared/math/countAtOrBelow.js +17 -0
  66. package/dist/packages/player/src/composition/browserPorts.js +38 -5
  67. package/dist/packages/player/src/composition/buildPipeline.js +15 -3
  68. package/dist/packages/player/src/composition/openInputs.js +51 -109
  69. package/dist/packages/player/src/composition/openRecording.js +13 -2
  70. package/dist/packages/player/src/composition/readInputs.js +111 -0
  71. package/dist/packages/player/src/controls/ChoiceMenu.js +16 -4
  72. package/dist/packages/player/src/controls/PictureMenus.js +22 -8
  73. package/dist/packages/player/src/controls/SeekBar.js +6 -3
  74. package/dist/packages/player/src/controls/SoundControls.js +2 -0
  75. package/dist/packages/player/src/controls/TransportButtons.js +2 -5
  76. package/dist/packages/player/src/controls/Wording.js +11 -9
  77. package/dist/packages/player/src/controls/controlParts.js +5 -3
  78. package/dist/packages/player/src/controls/controls.js +1 -1
  79. package/dist/packages/player/src/controls/controlsBar.js +4 -1
  80. package/dist/packages/player/src/controls/controlsMarkup.js +36 -21
  81. package/dist/packages/player/src/controls/icons.js +32 -8
  82. package/dist/packages/player/src/controls/messages.js +27 -4
  83. package/dist/packages/player/src/controls/sliderFill.js +16 -0
  84. package/dist/packages/player/src/element/ElementLoads.js +7 -0
  85. package/dist/packages/player/src/element/GyroViewElement.js +8 -3
  86. package/dist/packages/player/src/element/attributeNames.js +12 -1
  87. package/dist/packages/player/src/element/attributes.js +29 -7
  88. package/dist/packages/player/src/element/reflectedProperties.js +19 -1
  89. package/dist/packages/player/src/element/styles.js +1 -1
  90. package/dist/packages/player/src/inspectRecording.js +1 -1
  91. package/dist/standalone.js +385 -126
  92. package/package.json +1 -1
  93. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioSegments.js +0 -133
  94. package/dist/packages/adapters/mediabunny/src/MediabunnyDemuxer.js +0 -63
  95. package/dist/packages/adapters/mediabunny/src/MediabunnyVideoTrackReader.js +0 -84
@@ -4,12 +4,13 @@ import { DEFAULT_MESSAGES, isChoiceTable, isLabelName, messagesWith } from "./me
4
4
  //#region ../../packages/player/src/controls/Wording.ts
5
5
  var LABEL = "data-label";
6
6
  var TEXT = "data-text";
7
- var CHOICES = "data-choices";
7
+ var CHOICE_TEXT = "data-choice-text";
8
8
  var ERROR = "data-error";
9
9
  var SELECTORS = {
10
10
  label: "[data-label]",
11
11
  text: "[data-text]",
12
- choices: "[data-choices]",
12
+ choiceText: "[data-choice-text]",
13
+ choice: "[data-choice]",
13
14
  error: "[data-error]"
14
15
  };
15
16
  /**
@@ -18,7 +19,8 @@ var SELECTORS = {
18
19
  * of words.
19
20
  * - `data-label`: the label for its `aria-label`;
20
21
  * - `data-text`: the label for its text;
21
- * - `data-choices` on a menu: the table its items' `data-choice` are read from;
22
+ * - `data-choice-text`: for its text, the table's word for the choice the nearest `data-choice`,
23
+ * its own or an ancestor's, names;
22
24
  * - `data-error`: the visitor's words for that failure code.
23
25
  */
24
26
  var Wording = class {
@@ -52,7 +54,7 @@ var Wording = class {
52
54
  write(root) {
53
55
  for (const element of root.querySelectorAll(SELECTORS.label)) this.writeLabel(element);
54
56
  for (const element of root.querySelectorAll(SELECTORS.text)) this.writeText(element);
55
- for (const menu of root.querySelectorAll(SELECTORS.choices)) this.writeChoices(menu);
57
+ for (const element of root.querySelectorAll(SELECTORS.choiceText)) this.writeChoiceText(element);
56
58
  for (const element of root.querySelectorAll(SELECTORS.error)) this.writeError(element);
57
59
  }
58
60
  writeLabel(element) {
@@ -63,11 +65,11 @@ var Wording = class {
63
65
  const name = element.getAttribute(TEXT);
64
66
  if (isLabelName(name)) element.textContent = this.words.labels[name];
65
67
  }
66
- writeChoices(menu) {
67
- const name = menu.getAttribute(CHOICES);
68
- if (!isChoiceTable(name)) return;
69
- const table = this.words[name];
70
- for (const item of menu.querySelectorAll("[data-choice]")) item.textContent = table[item.dataset["choice"] ?? ""] ?? "";
68
+ writeChoiceText(element) {
69
+ const name = element.getAttribute(CHOICE_TEXT);
70
+ const choice = element.closest(SELECTORS.choice)?.dataset["choice"];
71
+ if (choice === void 0 || !isChoiceTable(name)) return;
72
+ element.textContent = this.words[name][choice] ?? "";
71
73
  }
72
74
  writeError(element) {
73
75
  const code = element.getAttribute(ERROR);
@@ -14,7 +14,9 @@ function queryChoiceMenu(root, name) {
14
14
  const classes = choiceMenuClasses(name);
15
15
  return {
16
16
  button: queryShadow(root, `.${classes.button}`, HTMLButtonElement),
17
- popup: queryShadow(root, `.${classes.popup}`, HTMLElement)
17
+ icon: queryShadow(root, `.${classes.button} .setting-icon`, HTMLElement),
18
+ popup: queryShadow(root, `.${classes.popup}`, HTMLElement),
19
+ close: queryShadow(root, `.${classes.popup} .popup-close`, HTMLButtonElement)
18
20
  };
19
21
  }
20
22
  function queryControlParts(root) {
@@ -22,8 +24,8 @@ function queryControlParts(root) {
22
24
  seek: queryShadow(root, ".seek", HTMLInputElement),
23
25
  play: queryShadow(root, ".play", HTMLButtonElement),
24
26
  bigPlay: queryShadow(root, ".big-play", HTMLButtonElement),
25
- stop: queryShadow(root, ".stop", HTMLButtonElement),
26
- time: queryShadow(root, ".time", HTMLElement),
27
+ elapsed: queryShadow(root, ".time .elapsed", HTMLElement),
28
+ total: queryShadow(root, ".time .total", HTMLElement),
27
29
  mute: queryShadow(root, ".mute", HTMLButtonElement),
28
30
  volume: queryShadow(root, ".volume", HTMLInputElement),
29
31
  resetView: queryShadow(root, ".reset-view", HTMLButtonElement),
@@ -1,4 +1,4 @@
1
1
  //#region ../../packages/player/src/controls/controls.css?raw
2
- var controls_default = "/*\n * The controls' own styles: the big play button, the bar, its buttons, sliders and menus, and the\n * grab hand over the picture. The host's custom properties (`--gyro-view-*`) theme them.\n */\n\n.big-play {\n position: absolute;\n top: 50%;\n left: 50%;\n width: 4.5rem;\n height: 4.5rem;\n transform: translate(-50%, -50%);\n border: 0;\n border-radius: 50%;\n background: var(--gyro-view-controls-background);\n color: var(--gyro-view-text);\n cursor: pointer;\n}\n\n.big-play .icon {\n width: 2rem;\n height: 2rem;\n}\n\n:host(:not([data-status='ready']):not([data-status='paused'])) .big-play,\n:host(:not([controls])) .big-play {\n display: none;\n}\n\n.controls {\n position: absolute;\n inset: auto 0 0 0;\n display: flex;\n flex-direction: column;\n padding: 0.25rem 0.5rem 0.5rem;\n background: linear-gradient(to top, var(--gyro-view-controls-background), transparent);\n transition: opacity 0.25s;\n}\n\n/* A failed player has nothing to control: the error says so, and a press would only fail again. */\n:host(:not([controls])) .controls,\n:host([data-status='idle']) .controls,\n:host([data-status='loading']) .controls,\n:host([data-status='error']) .controls {\n display: none;\n}\n\n/* Controls with an open menu stay up: the viewer is reading it. So do controls holding the\n keyboard's focus, which would otherwise fade with its ring out of sight. */\n:host([data-idle]) .controls:not(:has(.popup:not([hidden]))):not(:has(:focus-visible)) {\n opacity: 0;\n pointer-events: none;\n}\n\n.row {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n}\n\n.spacer {\n flex: 1;\n}\n\n.time {\n font-size: 0.8rem;\n font-variant-numeric: tabular-nums;\n padding: 0 0.5rem;\n white-space: nowrap;\n}\n\nbutton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 2.25rem;\n height: 2.25rem;\n border: 0;\n border-radius: var(--gyro-view-radius);\n background: transparent;\n color: var(--gyro-view-text);\n font: inherit;\n cursor: pointer;\n}\n\n.icon {\n width: 1.25rem;\n height: 1.25rem;\n pointer-events: none;\n}\n\nbutton:hover,\nbutton:focus-visible {\n background: rgb(255 255 255 / 15%);\n}\n\nbutton:focus-visible,\ninput:focus-visible {\n outline: 2px solid var(--gyro-view-accent);\n outline-offset: 1px;\n}\n\ninput[type='range'] {\n accent-color: var(--gyro-view-accent);\n margin: 0;\n}\n\n.seek {\n width: 100%;\n height: 1.25rem;\n}\n\n.volume {\n width: 5rem;\n}\n\nbutton[hidden] {\n display: none;\n}\n\n.choice {\n position: relative;\n display: flex;\n}\n\n/* A menu opens upward from the bar and never past the player's top, which would hide its first\n items out of reach: what does not fit scrolls. The bar's row and gaps take the 3.75rem. */\n.popup {\n position: absolute;\n right: 0;\n bottom: calc(100% + 0.5rem);\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n max-height: calc(100cqh - 3.75rem);\n overflow-y: auto;\n min-width: 9rem;\n padding: 0.35rem;\n border-radius: var(--gyro-view-radius);\n background: var(--gyro-view-controls-background);\n}\n\n.popup[hidden] {\n display: none;\n}\n\n.popup-title {\n margin: 0.2rem 0.5rem 0.35rem;\n font-size: 0.75rem;\n opacity: 0.7;\n}\n\n[role='menu'] {\n display: flex;\n flex-direction: column;\n}\n\n[role='menuitemradio'] {\n justify-content: flex-start;\n gap: 0.4rem;\n height: 2rem;\n padding: 0 0.75rem 0 0.4rem;\n font-size: 0.9rem;\n white-space: nowrap;\n}\n\n[role='menuitemradio']::before {\n content: '\\2713';\n width: 1rem;\n color: var(--gyro-view-accent);\n visibility: hidden;\n}\n\n[role='menuitemradio'][aria-checked='true']::before {\n visibility: visible;\n}\n\n/* A narrowing player, on a phone or in a narrow column, gives up in turn the stop button (play\n pauses), the volume slider (mute stays), the time and Reset view (the 0 key stays), so the\n other buttons stay in reach. Each width is where the bar stops fitting with the part, with\n times up to an hour, in rem like the bar itself, so larger page text moves them with it (490,\n 450, 360 and 256 px at 16 px a rem). */\n@container (max-width: 30.625rem) {\n .stop {\n display: none;\n }\n}\n\n@container (max-width: 28.125rem) {\n .volume {\n display: none;\n }\n}\n\n@container (max-width: 22.5rem) {\n .time {\n display: none;\n }\n}\n\n@container (max-width: 16rem) {\n .reset-view {\n display: none;\n }\n}\n\n/* The hand only where a drag moves the picture (`data-draggable`, set by the view gestures). */\ncanvas[data-draggable] {\n cursor: grab;\n}\n\ncanvas[data-draggable]:active {\n cursor: grabbing;\n}\n\n/* A viewer who asked for less motion sees the controls come and go without fading. */\n@media (prefers-reduced-motion: reduce) {\n .controls {\n transition: none;\n }\n}\n";
2
+ var controls_default = "/*\n * The controls' own styles: the big play button, the bar, its buttons, sliders and menus, and the\n * grab hand over the picture. The host's custom properties (`--gyro-view-*`) theme them. Secondary\n * words, fills and tracks are the text colour at lower strengths, so a page that changes the text\n * colour changes them with it.\n *\n * Two things change the bar. A coarse pointer makes every target 44 pixels and leaves the\n * volume to the device's own buttons. A narrow player, whatever the pointer, gives up the volume\n * slider, shows the menus over the whole player, and past that gives up the time and then Reset\n * view (the widths below). Sizes are in rem like the page's text, so larger text moves the widths\n * with it.\n */\n\n.big-play {\n position: absolute;\n top: 50%;\n left: 50%;\n width: 4.5rem;\n height: 4.5rem;\n transform: translate(-50%, -50%);\n border-radius: 50%;\n background: var(--gyro-view-controls-background);\n}\n\n.big-play .icon {\n width: 2rem;\n height: 2rem;\n}\n\n:host(:not([data-status='ready']):not([data-status='paused'])) .big-play,\n:host(:not([controls])) .big-play {\n display: none;\n}\n\n.controls {\n /* The buttons, and the seek bar's hit area, which reaches up over the shade. */\n --target: 2.25rem;\n --seek-height: 1rem;\n\n position: absolute;\n inset: auto 0 0 0;\n isolation: isolate;\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n padding: 0 1rem 0.625rem;\n transition: opacity 0.25s;\n}\n\n/* The shade behind the bar, reaching 3rem above it without taking the picture's presses there. */\n.controls::before {\n content: '';\n position: absolute;\n inset: -3rem 0 0;\n z-index: -1;\n background: linear-gradient(to top, var(--gyro-view-controls-background), transparent);\n pointer-events: none;\n}\n\n/* A failed player has nothing to control: the error says so, and a press would only fail again. */\n:host(:not([controls])) .controls,\n:host([data-status='idle']) .controls,\n:host([data-status='loading']) .controls,\n:host([data-status='error']) .controls {\n display: none;\n}\n\n/* Controls with an open menu stay up: the viewer is reading it. So do controls holding the\n keyboard's focus, which would otherwise fade with its ring out of sight. */\n:host([data-idle]) .controls:not(:has(.popup:not([hidden]))):not(:has(:focus-visible)) {\n opacity: 0;\n pointer-events: none;\n}\n\n.row {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n}\n\n.spacer {\n flex: 1;\n}\n\nbutton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n border: 0;\n border-radius: var(--gyro-view-radius);\n background: transparent;\n color: var(--gyro-view-text);\n font: inherit;\n cursor: pointer;\n}\n\nbutton[hidden] {\n display: none;\n}\n\n.icon {\n flex: none;\n width: 1.25rem;\n height: 1.25rem;\n pointer-events: none;\n}\n\n.icon-button {\n flex: none;\n width: var(--target);\n height: var(--target);\n}\n\n.icon-button:hover,\n.icon-button:focus-visible {\n background: color-mix(in srgb, var(--gyro-view-text) 12%, transparent);\n}\n\n:focus-visible {\n outline: 2px solid var(--gyro-view-accent);\n outline-offset: 2px;\n}\n\n.time {\n padding: 0 0.5rem;\n font-size: 0.8125rem;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n.elapsed {\n opacity: 0.85;\n}\n\n.total {\n opacity: 0.5;\n}\n\n/* The sliders: a 3-pixel track in the text colour, filled and handled in the accent. Chrome and\n Safari draw the filled part from `--fill`, which the controls keep; Firefox draws it itself. */\ninput[type='range'] {\n --track: color-mix(in srgb, var(--gyro-view-text) 28%, transparent);\n\n margin: 0;\n padding: 0;\n background: transparent;\n cursor: pointer;\n appearance: none;\n}\n\ninput[type='range']::-webkit-slider-runnable-track {\n height: 3px;\n border-radius: 1.5px;\n background: linear-gradient(\n to right,\n var(--gyro-view-accent) calc(var(--fill, 0) * 100%),\n var(--track) 0\n );\n}\n\ninput[type='range']::-webkit-slider-thumb {\n width: 0.75rem;\n height: 0.75rem;\n margin-top: calc((3px - 0.75rem) / 2);\n border: 0;\n border-radius: 50%;\n background: var(--gyro-view-accent);\n appearance: none;\n}\n\ninput[type='range']::-moz-range-track {\n height: 3px;\n border-radius: 1.5px;\n background: var(--track);\n}\n\ninput[type='range']::-moz-range-progress {\n height: 3px;\n border-radius: 1.5px;\n background: var(--gyro-view-accent);\n}\n\ninput[type='range']::-moz-range-thumb {\n width: 0.75rem;\n height: 0.75rem;\n border: 0;\n border-radius: 50%;\n background: var(--gyro-view-accent);\n}\n\n/* Taller than it looks: the hit area grows upward over the shade, leaving the bar's layout be. */\n.seek {\n width: 100%;\n height: var(--seek-height);\n margin-top: calc(1rem - var(--seek-height));\n}\n\n.volume {\n flex: none;\n width: 4rem;\n height: var(--target);\n}\n\n/* A setting button shows the icon of the choice in effect, and stays lit while its menu is open. */\n.setting-icon {\n display: inline-flex;\n}\n\n.setting[aria-expanded='true'] {\n background: color-mix(in srgb, var(--gyro-view-text) 20%, transparent);\n}\n\n.choice {\n position: relative;\n display: flex;\n}\n\n/* A menu opens upward, right-aligned with its button, as wide as its longest line within limits,\n so a description fits on one line unless a translation makes it long; and never past the\n player's top, which would put its first choices out of reach: what does not fit scrolls. */\n.popup {\n position: absolute;\n right: 0;\n bottom: calc(100% + 0.625rem);\n z-index: 1;\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n width: max-content;\n min-width: 16.25rem;\n max-width: min(22rem, 100cqw - 2rem);\n max-height: calc(100cqh - var(--target) - 2rem);\n overflow-y: auto;\n padding: 0.375rem;\n border-radius: calc(var(--gyro-view-radius) * 1.5);\n background: var(--gyro-view-menu-background);\n box-shadow: 0 8px 30px rgb(0 0 0 / 35%);\n -webkit-backdrop-filter: blur(16px);\n backdrop-filter: blur(16px);\n}\n\n.popup[hidden] {\n display: none;\n}\n\n.popup-header {\n display: flex;\n flex: none;\n align-items: center;\n justify-content: space-between;\n}\n\n.popup-title {\n margin: 0.375rem 0.5rem 0.25rem;\n font-size: 0.75rem;\n opacity: 0.55;\n}\n\n.popup-close {\n display: none;\n}\n\n[role='menu'] {\n display: flex;\n flex-direction: column;\n}\n\n/* A choice: its icon, its name over a line describing it, and a check on the right while it is in\n effect. */\n[role='menuitemradio'] {\n display: grid;\n grid-template-columns: 1.25rem 1fr 1rem;\n column-gap: 0.75rem;\n align-items: center;\n padding: 0.5rem 0.625rem;\n text-align: start;\n}\n\n[role='menuitemradio']:hover,\n[role='menuitemradio']:focus-visible {\n background: color-mix(in srgb, var(--gyro-view-text) 8%, transparent);\n}\n\n[role='menuitemradio']::after {\n content: '\\2713';\n grid-area: 1 / 3 / span 2;\n font-size: 0.875rem;\n color: var(--gyro-view-accent);\n visibility: hidden;\n}\n\n[role='menuitemradio'][aria-checked='true']::after {\n visibility: visible;\n}\n\n.choice-icon {\n display: inline-flex;\n grid-area: 1 / 1 / span 2;\n}\n\n.choice-name {\n grid-column: 2;\n font-size: 0.875rem;\n}\n\n.choice-description {\n grid-column: 2;\n font-size: 0.75rem;\n opacity: 0.55;\n}\n\n/* Touch: 44-pixel targets, and the volume left to the device. */\n@media (pointer: coarse) {\n .controls {\n --target: 2.75rem;\n --seek-height: 2.75rem;\n }\n\n .volume {\n display: none;\n }\n}\n\n/* A narrow player: no volume slider, and menus over the whole player, each with a close button\n and 44-pixel rows. */\n@container (max-width: 37.5rem) {\n .volume {\n display: none;\n }\n\n .choice {\n position: static;\n }\n\n .popup {\n inset: auto 0 0;\n width: auto;\n max-width: none;\n height: 100cqh;\n max-height: none;\n padding: 0 0.5rem 0.5rem;\n border-radius: 0;\n box-shadow: none;\n -webkit-backdrop-filter: blur(14px);\n backdrop-filter: blur(14px);\n }\n\n .popup-header {\n padding-left: 0.5rem;\n }\n\n .popup-title {\n margin: 0;\n font-size: 0.8125rem;\n }\n\n .popup-close {\n display: inline-flex;\n width: 2.75rem;\n height: 2.75rem;\n }\n\n [role='menuitemradio'] {\n min-height: 2.75rem;\n padding-block: 0;\n }\n\n /* Without the descriptions a choice has one line, which its icon and check keep level with. */\n .choice-icon,\n [role='menuitemradio']::after {\n grid-row: 1;\n }\n\n .choice-name {\n font-size: 0.9375rem;\n }\n\n .choice-description {\n display: none;\n }\n}\n\n/*\n * Narrower still, the bar gives up the time (the seek bar still shows where playback is), then\n * Reset view (the 0 key stays). Each width is where the bar stops fitting with the part, measured\n * with stabilization offered and times up to an hour; touch's larger targets move each width out.\n */\n@container (max-width: 23.5rem) {\n .time {\n display: none;\n }\n}\n\n@container (max-width: 17.5rem) {\n .controls {\n padding-inline: 0.5rem;\n }\n\n .reset-view {\n display: none;\n }\n}\n\n@media (pointer: coarse) {\n @container (max-width: 26.5rem) {\n .time {\n display: none;\n }\n }\n\n @container (max-width: 20.5rem) {\n .controls {\n padding-inline: 0.5rem;\n }\n\n .reset-view {\n display: none;\n }\n }\n}\n\n/* Without a blur behind it, or for a viewer who asked for less transparency, a menu is laid\n twice over itself: its own colour, near enough opaque. */\n@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {\n .popup {\n background:\n linear-gradient(var(--gyro-view-menu-background), var(--gyro-view-menu-background)),\n var(--gyro-view-menu-background);\n }\n}\n\n@media (prefers-reduced-transparency: reduce) {\n .popup {\n background:\n linear-gradient(var(--gyro-view-menu-background), var(--gyro-view-menu-background)),\n var(--gyro-view-menu-background);\n -webkit-backdrop-filter: none;\n backdrop-filter: none;\n }\n}\n\n/* Forced colours: the sliders keep a visible track and handle, and the menus an edge, in the\n system's colours. */\n@media (forced-colors: active) {\n input[type='range'] {\n --track: GrayText;\n --gyro-view-accent: Highlight;\n\n forced-color-adjust: none;\n }\n\n .popup {\n border: 1px solid CanvasText;\n }\n}\n\n/* The hand only where a drag moves the picture (`data-draggable`, set by the view gestures). */\ncanvas[data-draggable] {\n cursor: grab;\n}\n\ncanvas[data-draggable]:active {\n cursor: grabbing;\n}\n\n/* A viewer who asked for less motion sees the controls come and go without fading. */\n@media (prefers-reduced-motion: reduce) {\n .controls {\n transition: none;\n }\n}\n";
3
3
  //#endregion
4
4
  export { controls_default as default };
@@ -17,7 +17,10 @@ function bindControlsBar(root, host) {
17
17
  bindViewButtons(parts, host);
18
18
  new SeekBar(parts, host);
19
19
  new SoundControls(parts, host.player);
20
- new PictureMenus(root, parts, host.player);
20
+ new PictureMenus(root, parts, {
21
+ player: host.player,
22
+ wording: host.wording
23
+ });
21
24
  }
22
25
  //#endregion
23
26
  export { bindControlsBar };
@@ -2,7 +2,8 @@ import { STABILIZATION_MODES } from "../../../core/src/domain/motion/stabilizati
2
2
  import { VIEW_MODES } from "../../../core/src/domain/view/ViewMode.js";
3
3
  import "../../../core/src/index.js";
4
4
  import controls_default from "./controls.js";
5
- import { ICONS } from "./icons.js";
5
+ import { ICONS, STABILIZATION_ICONS, VIEW_MODE_ICONS } from "./icons.js";
6
+ import { STABILIZATION_WORDS, VIEW_MODE_WORDS } from "./messages.js";
6
7
  //#region ../../packages/player/src/controls/controlsMarkup.ts
7
8
  /**
8
9
  * The bar's choice menus, by the name their parts are found under.
@@ -20,20 +21,35 @@ function choiceMenuClasses(name) {
20
21
  popup: `${name}-menu`
21
22
  };
22
23
  }
23
- function menuItemsOf(choices) {
24
- return choices.map((choice) => `<button type="button" role="menuitemradio" aria-checked="false" tabindex="-1" data-choice="${choice}"></button>`).join("");
24
+ /**
25
+ * Each choice names itself in `data-choice` and shows its icon, and the words below it fill in
26
+ * from the menu's tables: its name labels it for assistive technology and its description
27
+ * describes it. Ids need be unique only within one element's shadow tree.
28
+ */
29
+ function menuItemsOf(menu) {
30
+ return menu.choices.map((choice) => {
31
+ const id = `${menu.name}-${choice}`;
32
+ return `<button type="button" role="menuitemradio" aria-checked="false" tabindex="-1" data-choice="${choice}" aria-labelledby="${id}-name" aria-describedby="${id}-description"><span class="choice-icon">${menu.icons[choice] ?? ""}</span><span class="choice-name" id="${id}-name" data-choice-text="${menu.words.names}"></span><span class="choice-description" id="${id}-description" data-choice-text="${menu.words.descriptions}"></span></button>`;
33
+ }).join("");
25
34
  }
26
35
  /**
27
- * A button opening a popup of choices; the popup's title repeats the button's label for sight.
28
- * The words are the wording's to fill in.
36
+ * A setting button opening a popup of choices. The button shows the icon of the choice in effect,
37
+ * which its `data-choice` names, and is named by words it never shows: what it sets and the
38
+ * choice's name, which hidden elements still give through `aria-labelledby`. The popup's title
39
+ * repeats what the button sets for sight; its close button serves the popup shown over the whole
40
+ * player.
29
41
  */
30
42
  function choiceMenuOf(menu) {
31
43
  const classes = choiceMenuClasses(menu.name);
44
+ const [label, value] = [`${menu.name}-label`, `${menu.name}-value`];
32
45
  return `<div class="choice">
33
- <button class="${classes.button}" type="button" data-label="${menu.title}" aria-haspopup="menu" aria-expanded="false">${menu.icon}</button>
46
+ <button class="icon-button setting ${classes.button}" type="button" aria-haspopup="menu" aria-expanded="false" aria-labelledby="${label} ${value}"><span class="setting-icon"></span><span hidden><span id="${label}" data-text="${menu.title}"></span> <span id="${value}" data-choice-text="${menu.words.names}"></span></span></button>
34
47
  <div class="popup ${classes.popup}" hidden>
35
- <p class="popup-title" aria-hidden="true" data-text="${menu.title}"></p>
36
- <div role="menu" data-label="${menu.title}" data-choices="${menu.choices}">${menu.items}</div>
48
+ <div class="popup-header">
49
+ <p class="popup-title" aria-hidden="true" data-text="${menu.title}"></p>
50
+ <button class="icon-button popup-close" type="button" data-label="close">${ICONS.close}</button>
51
+ </div>
52
+ <div role="menu" data-label="${menu.title}">${menuItemsOf(menu)}</div>
37
53
  </div>
38
54
  </div>`;
39
55
  }
@@ -41,15 +57,15 @@ function pictureMenusMarkup() {
41
57
  return choiceMenuOf({
42
58
  name: ChoiceMenuName.Stabilization,
43
59
  title: "stabilization",
44
- icon: ICONS.stabilization,
45
- choices: "stabilizationModes",
46
- items: menuItemsOf(STABILIZATION_MODES)
60
+ choices: STABILIZATION_MODES,
61
+ icons: STABILIZATION_ICONS,
62
+ words: STABILIZATION_WORDS
47
63
  }) + choiceMenuOf({
48
64
  name: ChoiceMenuName.ViewMode,
49
65
  title: "viewMode",
50
- icon: ICONS.viewMode,
51
- choices: "viewModes",
52
- items: menuItemsOf(VIEW_MODES)
66
+ choices: VIEW_MODES,
67
+ icons: VIEW_MODE_ICONS,
68
+ words: VIEW_MODE_WORDS
53
69
  });
54
70
  }
55
71
  /**
@@ -69,15 +85,14 @@ function controlsMarkup() {
69
85
  <div class="controls" part="controls">
70
86
  <input class="seek" type="range" min="0" max="0" step="0.01" value="0" data-label="seek" />
71
87
  <div class="row">
72
- <button class="play" type="button"></button>
73
- <button class="stop" type="button" data-label="stop">${ICONS.stop}</button>
74
- <span class="time">0:00 / 0:00</span>
75
- <span class="spacer"></span>
76
- <button class="mute" type="button" data-label="mute"></button>
88
+ <button class="icon-button play" type="button"></button>
89
+ <button class="icon-button mute" type="button" data-label="mute"></button>
77
90
  <input class="volume" type="range" min="0" max="1" step="0.01" value="1" data-label="volume" />
78
- <button class="reset-view" type="button" data-label="resetView">${ICONS.resetView}</button>
91
+ <span class="time"><span class="elapsed">0:00</span> <span class="total">/ 0:00</span></span>
92
+ <span class="spacer"></span>
79
93
  ${pictureMenusMarkup()}
80
- <button class="fullscreen" type="button" data-label="fullscreen">${ICONS.fullscreen}</button>
94
+ <button class="icon-button reset-view" type="button" data-label="resetView">${ICONS.resetView}</button>
95
+ <button class="icon-button fullscreen" type="button" data-label="fullscreen">${ICONS.fullscreen}</button>
81
96
  </div>
82
97
  </div>`;
83
98
  }
@@ -2,12 +2,20 @@ import { parseMarkup } from "./parseMarkup.js";
2
2
  //#region ../../packages/player/src/controls/icons.ts
3
3
  /**
4
4
  * Wraps an icon's shapes in a 24-unit square. Shapes are stroked in `currentColor` unless they
5
- * say otherwise, so every icon takes the controls' text colour.
5
+ * say otherwise, so every icon takes the controls' text colour. The stroke is 1.8 units: a line
6
+ * 1.5 pixels wide at the 20 pixels the bar draws its icons at.
6
7
  */
7
8
  function icon(shapes) {
8
- return `<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${shapes}</svg>`;
9
+ return `<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${shapes}</svg>`;
9
10
  }
10
- var SPEAKER = "<path d=\"M4 9.5v5h3.5L12 19V5L7.5 9.5z\" fill=\"currentColor\"/>";
11
+ /**
12
+ * Shapes drawn on a 20-unit grid with a 1.5-unit stroke, scaled onto the 24-unit one: the stroke
13
+ * scales with them to the 1.8 units of every other icon.
14
+ */
15
+ function fromTwentyUnitGrid(shapes) {
16
+ return icon(`<g transform="scale(1.2)" stroke-width="1.5">${shapes}</g>`);
17
+ }
18
+ var SPEAKER = "<path d=\"M4 9.5v5h3.5L12 19V5L7.5 9.5z\"/>";
11
19
  /**
12
20
  * The controls' icons, drawn as inline SVG in `currentColor` only: a text glyph is left to the
13
21
  * platform's fonts, some of which draw it as a colour emoji.
@@ -15,13 +23,29 @@ var SPEAKER = "<path d=\"M4 9.5v5h3.5L12 19V5L7.5 9.5z\" fill=\"currentColor\"/>
15
23
  var ICONS = {
16
24
  play: icon("<path d=\"M8 5v14l11-7z\" fill=\"currentColor\"/>"),
17
25
  pause: icon("<rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/><rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/>"),
18
- stop: icon("<rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" rx=\"1.5\" fill=\"currentColor\" stroke=\"none\"/>"),
19
26
  sound: icon(`${SPEAKER}<path d="M15.5 9a4 4 0 0 1 0 6"/><path d="M18.5 6a8 8 0 0 1 0 12"/>`),
20
27
  muted: icon(`${SPEAKER}<path d="M16 9.5l5 5M21 9.5l-5 5"/>`),
21
28
  resetView: icon("<path d=\"M4.5 12a7.5 7.5 0 1 0 2.2-5.3\"/><path d=\"M4.5 4.5v4h4\"/>"),
22
- stabilization: icon("<rect x=\"2.5\" y=\"8.5\" width=\"19\" height=\"7\" rx=\"3.5\"/><path d=\"M8.5 8.5v7M15.5 8.5v7\"/><circle cx=\"12\" cy=\"12\" r=\"1.8\" fill=\"currentColor\" stroke=\"none\"/>"),
23
- viewMode: icon("<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><ellipse cx=\"12\" cy=\"12\" rx=\"3.5\" ry=\"8.5\"/><path d=\"M3.5 12h17\"/>"),
24
- fullscreen: icon("<path d=\"M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5\"/>")
29
+ fullscreen: icon("<path d=\"M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5\"/>"),
30
+ close: icon("<path d=\"M6 6l12 12M18 6L6 18\"/>")
31
+ };
32
+ /**
33
+ * Each stabilization mode's icon, which its menu shows beside it and its button while it is in
34
+ * effect: records, so a mode cannot be left without one.
35
+ */
36
+ var STABILIZATION_ICONS = {
37
+ off: fromTwentyUnitGrid("<path d=\"M2 11l2.5-3 3 4.5 3-6 3 5 2-2.5L18 11\"/>"),
38
+ lock: fromTwentyUnitGrid("<rect x=\"4.5\" y=\"9\" width=\"11\" height=\"8\" rx=\"1.5\"/><path d=\"M7 9V6.5a3 3 0 016 0V9\"/>"),
39
+ horizon: fromTwentyUnitGrid("<circle cx=\"10\" cy=\"10\" r=\"7\"/><path d=\"M3 10h14M8 13h4\"/>"),
40
+ follow: fromTwentyUnitGrid("<path d=\"M3 15c3.5 0 4-9 8.5-9 2 0 3 1 4.5 1\"/><path d=\"M14 4.5L16.5 7 14 9.5\"/>")
41
+ };
42
+ /**
43
+ * Each view mode's icon, as the stabilization modes have theirs.
44
+ */
45
+ var VIEW_MODE_ICONS = {
46
+ "raw-lenses": fromTwentyUnitGrid("<circle cx=\"6\" cy=\"10\" r=\"4.25\"/><circle cx=\"14\" cy=\"10\" r=\"4.25\"/>"),
47
+ equirectangular: fromTwentyUnitGrid("<rect x=\"2\" y=\"5\" width=\"16\" height=\"10\" rx=\"1.5\"/><path d=\"M2 10h16M7.5 5c-1.2 3.3-1.2 6.7 0 10M12.5 5c1.2 3.3 1.2 6.7 0 10\"/>"),
48
+ normal: fromTwentyUnitGrid("<path d=\"M10 16L3.2 7.2a10 10 0 0113.6 0z\"/>")
25
49
  };
26
50
  var parsedIcons = /* @__PURE__ */ new Map();
27
51
  /**
@@ -37,4 +61,4 @@ function iconNode(name) {
37
61
  return parsed.cloneNode(true);
38
62
  }
39
63
  //#endregion
40
- export { ICONS, iconNode };
64
+ export { ICONS, STABILIZATION_ICONS, VIEW_MODE_ICONS, iconNode };
@@ -1,4 +1,13 @@
1
1
  //#region ../../packages/player/src/controls/messages.ts
2
+ var STABILIZATION_WORDS = {
3
+ names: "stabilizationModes",
4
+ descriptions: "stabilizationModeDescriptions"
5
+ };
6
+ var VIEW_MODE_WORDS = {
7
+ names: "viewModes",
8
+ descriptions: "viewModeDescriptions"
9
+ };
10
+ var CHOICE_TABLES = new Set([STABILIZATION_WORDS, VIEW_MODE_WORDS].flatMap((words) => [words.names, words.descriptions]));
2
11
  var UNREACHABLE = "The video could not be loaded.";
3
12
  var UNSUPPORTED_BROWSER = "This browser cannot play this video.";
4
13
  var UNREADABLE = "This file cannot be played.";
@@ -9,7 +18,6 @@ var DEFAULT_MESSAGES = {
9
18
  loading: "Loading",
10
19
  play: "Play",
11
20
  pause: "Pause",
12
- stop: "Stop",
13
21
  seek: "Seek",
14
22
  position: "{time} of {duration}",
15
23
  mute: "Mute",
@@ -17,7 +25,8 @@ var DEFAULT_MESSAGES = {
17
25
  resetView: "Reset view",
18
26
  stabilization: "Stabilization",
19
27
  viewMode: "View",
20
- fullscreen: "Fullscreen"
28
+ fullscreen: "Fullscreen",
29
+ close: "Close"
21
30
  },
22
31
  stabilizationModes: {
23
32
  off: "Off",
@@ -25,11 +34,22 @@ var DEFAULT_MESSAGES = {
25
34
  horizon: "Horizon",
26
35
  follow: "Follow"
27
36
  },
37
+ stabilizationModeDescriptions: {
38
+ off: "Footage as the camera moved",
39
+ lock: "Orientation fixed to the world",
40
+ horizon: "Level horizon, follows heading",
41
+ follow: "Follows turns, smooths out shake"
42
+ },
28
43
  viewModes: {
29
44
  "raw-lenses": "Raw lenses",
30
45
  equirectangular: "Equirectangular",
31
46
  normal: "Normal"
32
47
  },
48
+ viewModeDescriptions: {
49
+ "raw-lenses": "Both fisheye images as recorded",
50
+ equirectangular: "The full 360° frame, unwrapped",
51
+ normal: "Standard view, drag to look around"
52
+ },
33
53
  errors: {
34
54
  "binary-out-of-bounds": UNREADABLE,
35
55
  "binary-unsafe-integer": UNREADABLE,
@@ -51,6 +71,7 @@ var DEFAULT_MESSAGES = {
51
71
  "playback-blocked": FAILED,
52
72
  "range-unsupported": UNREACHABLE,
53
73
  "render-unavailable": UNSUPPORTED_BROWSER,
74
+ "source-changed": "The video was replaced while it played.",
54
75
  "source-truncated": UNREACHABLE,
55
76
  "source-unreadable": UNREACHABLE,
56
77
  "unsupported-calibration": UNREADABLE,
@@ -69,7 +90,9 @@ function messagesWith(overrides) {
69
90
  return {
70
91
  labels: tableWith(DEFAULT_MESSAGES.labels, given["labels"]),
71
92
  stabilizationModes: tableWith(DEFAULT_MESSAGES.stabilizationModes, given["stabilizationModes"]),
93
+ stabilizationModeDescriptions: tableWith(DEFAULT_MESSAGES.stabilizationModeDescriptions, given["stabilizationModeDescriptions"]),
72
94
  viewModes: tableWith(DEFAULT_MESSAGES.viewModes, given["viewModes"]),
95
+ viewModeDescriptions: tableWith(DEFAULT_MESSAGES.viewModeDescriptions, given["viewModeDescriptions"]),
73
96
  errors: tableWith(DEFAULT_MESSAGES.errors, given["errors"])
74
97
  };
75
98
  }
@@ -77,7 +100,7 @@ function isLabelName(name) {
77
100
  return name !== null && Object.hasOwn(DEFAULT_MESSAGES.labels, name);
78
101
  }
79
102
  function isChoiceTable(name) {
80
- return name === "stabilizationModes" || name === "viewModes";
103
+ return name !== null && CHOICE_TABLES.has(name);
81
104
  }
82
105
  function tableWith(defaults, overrides) {
83
106
  if (!isRecord(overrides)) return defaults;
@@ -91,4 +114,4 @@ function isRecord(value) {
91
114
  return typeof value === "object" && value !== null;
92
115
  }
93
116
  //#endregion
94
- export { DEFAULT_MESSAGES, isChoiceTable, isLabelName, messagesWith };
117
+ export { DEFAULT_MESSAGES, STABILIZATION_WORDS, VIEW_MODE_WORDS, isChoiceTable, isLabelName, messagesWith };
@@ -0,0 +1,16 @@
1
+ import { clamp } from "../../../core/src/shared/math/clamp.js";
2
+ import "../../../core/src/index.js";
3
+ //#region ../../packages/player/src/controls/sliderFill.ts
4
+ /**
5
+ * Tells the stylesheet how far a range input is filled, as a fraction in `--fill`: only Firefox
6
+ * styles the filled part of a range itself. Set through the CSSOM, which a Content Security
7
+ * Policy does not govern as it governs style attributes.
8
+ */
9
+ function showSliderFill(slider) {
10
+ const minimum = Number(slider.min);
11
+ const span = Number(slider.max) - minimum;
12
+ const fraction = span > 0 ? (Number(slider.value) - minimum) / span : 0;
13
+ slider.style.setProperty("--fill", String(clamp(fraction, 0, 1)));
14
+ }
15
+ //#endregion
16
+ export { showSliderFill };
@@ -49,6 +49,13 @@ var ElementLoads = class {
49
49
  this.files = void 0;
50
50
  this.schedule();
51
51
  }
52
+ /**
53
+ * How the recording is fetched changed: one named by URL loads again, read the new way; files
54
+ * handed in are not fetched, so they play on.
55
+ */
56
+ requestChanged() {
57
+ if (this.files === void 0) this.schedule();
58
+ }
52
59
  loadFiles(files) {
53
60
  this.files = files;
54
61
  this.schedule();
@@ -1,11 +1,11 @@
1
- import { defineBooleanProperties, defineKeywordProperties, defineStringProperties, propertyNameOf } from "./reflectedProperties.js";
1
+ import { defineBooleanProperties, defineKeywordProperties, defineNullableKeywordProperties, defineStringProperties, propertyNameOf } from "./reflectedProperties.js";
2
2
  import { ensureFinite } from "../player/ensureFinite.js";
3
3
  import { bindKeyboard } from "../controls/keyboard.js";
4
4
  import { ViewGestures } from "../controls/ViewGestures.js";
5
5
  import { togglePlayback } from "../controls/customControls.js";
6
6
  import { createBrowserPlayer } from "../browserPlayer.js";
7
- import { OBSERVED_ATTRIBUTES, PlaybackAttribute, SourceAttribute, ViewAttribute } from "./attributeNames.js";
8
- import { GAIN_MATCH, PRELOAD, unreadableAngleWarning, viewAfterAttribute } from "./attributes.js";
7
+ import { OBSERVED_ATTRIBUTES, PlaybackAttribute, RequestAttribute, SourceAttribute, ViewAttribute } from "./attributeNames.js";
8
+ import { CROSS_ORIGIN, GAIN_MATCH, PRELOAD, unreadableAngleWarning, viewAfterAttribute } from "./attributes.js";
9
9
  import { ElementLoads } from "./ElementLoads.js";
10
10
  import { FullscreenToggle } from "./FullscreenToggle.js";
11
11
  import { IdleWatcher } from "./IdleWatcher.js";
@@ -27,8 +27,10 @@ import { Wording } from "../controls/Wording.js";
27
27
  */
28
28
  var STRING_ATTRIBUTES = [...Object.values(SourceAttribute), PlaybackAttribute.Poster];
29
29
  var KEYWORD_ATTRIBUTES = [PRELOAD, GAIN_MATCH];
30
+ var NULLABLE_KEYWORD_ATTRIBUTES = [CROSS_ORIGIN];
30
31
  var BOOLEAN_ATTRIBUTES = [PlaybackAttribute.Autoplay, PlaybackAttribute.Controls];
31
32
  var SOURCE_ATTRIBUTES = Object.values(SourceAttribute);
33
+ var REQUEST_ATTRIBUTES = Object.values(RequestAttribute);
32
34
  /**
33
35
  * The properties a page may set before the element is defined, all kept for it.
34
36
  */
@@ -38,6 +40,7 @@ var PUBLIC_PROPERTIES = [
38
40
  ...KEYWORD_ATTRIBUTES.map(({ name }) => name),
39
41
  ...BOOLEAN_ATTRIBUTES
40
42
  ].map((name) => propertyNameOf(name)),
43
+ ...NULLABLE_KEYWORD_ATTRIBUTES.map(({ property }) => property),
41
44
  ...LIVE_SETTING_NAMES,
42
45
  "currentTime",
43
46
  "messages"
@@ -66,6 +69,7 @@ var GyroViewElement = class extends TypedEventElement {
66
69
  this.earlyProperties = takeEarlyProperties(this, PUBLIC_PROPERTIES);
67
70
  defineStringProperties(this, STRING_ATTRIBUTES);
68
71
  defineKeywordProperties(this, KEYWORD_ATTRIBUTES);
72
+ defineNullableKeywordProperties(this, NULLABLE_KEYWORD_ATTRIBUTES);
69
73
  defineBooleanProperties(this, BOOLEAN_ATTRIBUTES);
70
74
  const shadow = this.attachShadow({ mode: "open" });
71
75
  renderShadowTree(shadow);
@@ -158,6 +162,7 @@ var GyroViewElement = class extends TypedEventElement {
158
162
  }
159
163
  attributeChangedCallback(name, _previous, value) {
160
164
  if (SOURCE_ATTRIBUTES.includes(name)) this.loads.sourceChanged();
165
+ else if (REQUEST_ATTRIBUTES.includes(name)) this.loads.requestChanged();
161
166
  else if (VIEW_ATTRIBUTES.includes(name)) {
162
167
  this.player.setView(viewAfterAttribute(this.player.view, name, value));
163
168
  const warning = unreadableAngleWarning(name, value);
@@ -11,6 +11,16 @@ var SourceAttribute = {
11
11
  Src: "src",
12
12
  Src2: "src2"
13
13
  };
14
+ /**
15
+ * `Request` ones say how the recording is fetched; changing any of them reloads a recording
16
+ * named by URL. The iframe embed carries none of them (ADR 0027).
17
+ */
18
+ var RequestAttribute = {
19
+ /**
20
+ * `use-credentials` fetches with the visitor's cookies, as a media element's `crossorigin`
21
+ * does; `anonymous`, the default, sends them to the page's own origin only.
22
+ */
23
+ CrossOrigin: "crossorigin" };
14
24
  var ViewAttribute = {
15
25
  FieldOfView: "fov",
16
26
  Yaw: "yaw",
@@ -44,8 +54,9 @@ var PlaybackAttribute = {
44
54
  };
45
55
  var OBSERVED_ATTRIBUTES = [
46
56
  ...Object.values(SourceAttribute),
57
+ ...Object.values(RequestAttribute),
47
58
  ...Object.values(ViewAttribute),
48
59
  ...Object.values(PlaybackAttribute)
49
60
  ];
50
61
  //#endregion
51
- export { OBSERVED_ATTRIBUTES, PlaybackAttribute, SourceAttribute, ViewAttribute };
62
+ export { OBSERVED_ATTRIBUTES, PlaybackAttribute, RequestAttribute, SourceAttribute, ViewAttribute };
@@ -1,20 +1,31 @@
1
1
  import { keywordOf } from "./reflectedProperties.js";
2
- import { PlaybackAttribute, SourceAttribute, ViewAttribute } from "./attributeNames.js";
2
+ import { PlaybackAttribute, RequestAttribute, SourceAttribute, ViewAttribute } from "./attributeNames.js";
3
3
  //#region ../../packages/player/src/element/attributes.ts
4
4
  /**
5
5
  * The source the attributes describe, or nothing when `src` is absent. URLs resolve against
6
- * the document, as an image's would.
6
+ * the document, as an image's would, and both files of a pair are read alike.
7
7
  */
8
8
  function sourceFromAttributes(read, baseUrl) {
9
- const main = urlInputOf(read(SourceAttribute.Src), baseUrl);
9
+ const reading = readingOf(read(RequestAttribute.CrossOrigin));
10
+ const main = urlInputOf(read(SourceAttribute.Src), baseUrl, reading);
10
11
  return main && {
11
12
  main,
12
- second: urlInputOf(read(SourceAttribute.Src2), baseUrl)
13
+ second: urlInputOf(read(SourceAttribute.Src2), baseUrl, reading)
13
14
  };
14
15
  }
15
- function urlInputOf(value, baseUrl) {
16
+ function urlInputOf(value, baseUrl, reading) {
16
17
  const trimmed = value?.trim() ?? "";
17
- return trimmed === "" ? void 0 : { url: resolvedOrAsWritten(trimmed, baseUrl) };
18
+ return trimmed === "" ? void 0 : {
19
+ url: resolvedOrAsWritten(trimmed, baseUrl),
20
+ ...reading
21
+ };
22
+ }
23
+ /**
24
+ * `use-credentials` sends the visitor's cookies to any origin; anything else leaves fetch's own
25
+ * default, which sends them to the page's origin only, as `anonymous` does for a media element.
26
+ */
27
+ function readingOf(crossOrigin) {
28
+ return keywordOf(CROSS_ORIGIN, crossOrigin) === "use-credentials" ? { credentials: "include" } : {};
18
29
  }
19
30
  /**
20
31
  * The URL resolved against the document; one that does not parse goes on as written, so the
@@ -91,6 +102,17 @@ var GAIN_MATCH = {
91
102
  keywords: ["on", "off"],
92
103
  fallback: "on"
93
104
  };
105
+ /**
106
+ * `use-credentials` fetches the recording with the visitor's cookies; `anonymous`, in effect for
107
+ * an empty or unknown value, without them across origins. Absent, its property reads `null`, as
108
+ * a media element's `crossOrigin` does.
109
+ */
110
+ var CROSS_ORIGIN = {
111
+ name: RequestAttribute.CrossOrigin,
112
+ property: "crossOrigin",
113
+ keywords: ["anonymous", "use-credentials"],
114
+ fallback: "anonymous"
115
+ };
94
116
  function shouldPreload(value) {
95
117
  return keywordOf(PRELOAD, value) === "auto";
96
118
  }
@@ -98,4 +120,4 @@ function shouldMatchGains(value) {
98
120
  return keywordOf(GAIN_MATCH, value) === "on";
99
121
  }
100
122
  //#endregion
101
- export { GAIN_MATCH, PRELOAD, ignoredChoiceWarning, isBooleanAttributeSet, parseNumber, shouldMatchGains, shouldPreload, sourceFromAttributes, unreadableAngleWarning, viewAfterAttribute };
123
+ export { CROSS_ORIGIN, GAIN_MATCH, PRELOAD, ignoredChoiceWarning, isBooleanAttributeSet, parseNumber, shouldMatchGains, shouldPreload, sourceFromAttributes, unreadableAngleWarning, viewAfterAttribute };
@@ -48,6 +48,24 @@ function defineKeywordProperties(element, attributes) {
48
48
  });
49
49
  }
50
50
  /**
51
+ * Enumerated attributes whose property reads `null` while the attribute is absent and the keyword
52
+ * in effect otherwise, the fallback for an empty or unknown value, as the DOM reflects
53
+ * `crossOrigin`; it writes the attribute.
54
+ */
55
+ function defineNullableKeywordProperties(element, attributes) {
56
+ for (const attribute of attributes) Object.defineProperty(element, attribute.property, {
57
+ configurable: true,
58
+ enumerable: true,
59
+ get() {
60
+ const value = this.getAttribute(attribute.name);
61
+ return value === null ? null : keywordOf(attribute, value);
62
+ },
63
+ set(value) {
64
+ writeAttribute(this, attribute.name, value);
65
+ }
66
+ });
67
+ }
68
+ /**
51
69
  * The keyword an enumerated attribute's value names, ignoring case and spacing; the fallback for
52
70
  * an absent or unknown one.
53
71
  */
@@ -75,4 +93,4 @@ function propertyNameOf(attribute) {
75
93
  return attribute.replaceAll(HYPHENATED_LETTER, (_match, letter) => letter.toUpperCase());
76
94
  }
77
95
  //#endregion
78
- export { defineBooleanProperties, defineKeywordProperties, defineStringProperties, keywordOf, propertyNameOf, writeAttribute };
96
+ export { defineBooleanProperties, defineKeywordProperties, defineNullableKeywordProperties, defineStringProperties, keywordOf, propertyNameOf, writeAttribute };
@@ -1,4 +1,4 @@
1
1
  //#region ../../packages/player/src/element/styles.css?raw
2
- var styles_default = ":host {\n --gyro-view-accent: #4cc2ff;\n --gyro-view-controls-background: rgb(0 0 0 / 55%);\n --gyro-view-text: #f4f4f4;\n --gyro-view-font: system-ui, -apple-system, 'Segoe UI', sans-serif;\n --gyro-view-radius: 6px;\n\n display: block;\n position: relative;\n width: 100%;\n aspect-ratio: 16 / 9;\n background: #000;\n color: var(--gyro-view-text);\n font-family: var(--gyro-view-font);\n overflow: hidden;\n user-select: none;\n -webkit-user-select: none;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n:host(:focus-visible) {\n outline: 2px solid var(--gyro-view-accent);\n outline-offset: 2px;\n}\n\n/*\n * Pinned over the page where the Fullscreen API is refused (iPhone Safari). Like the browser's\n * own fullscreen rules it overrides the size, margins and place the page gave the element, and\n * the insets size it to the visible viewport, where 100vh would reach under Safari's toolbars.\n */\n:host([data-fill]) {\n position: fixed !important;\n inset: 0 !important;\n box-sizing: border-box !important;\n width: auto !important;\n height: auto !important;\n min-width: 0 !important;\n min-height: 0 !important;\n max-width: none !important;\n max-height: none !important;\n margin: 0 !important;\n transform: none !important;\n aspect-ratio: auto !important;\n z-index: 2147483000 !important;\n}\n\n:host(:fullscreen) {\n width: 100%;\n height: 100%;\n aspect-ratio: auto;\n}\n\n/* The size container the controls fit themselves to: the player's size, not the page's. Its\n size never depends on its content, which size containment requires. */\n.stage {\n position: absolute;\n inset: 0;\n container-type: size;\n}\n\ncanvas {\n display: block;\n width: 100%;\n height: 100%;\n touch-action: none;\n}\n\n.poster {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n pointer-events: none;\n}\n\n:host([data-has-frame]) .poster,\n:host(:not([poster])) .poster {\n display: none;\n}\n\n.overlay {\n position: absolute;\n inset: 0;\n display: none;\n align-items: center;\n justify-content: center;\n flex-direction: column;\n gap: 0.5rem;\n padding: 1rem;\n text-align: center;\n background: rgb(0 0 0 / 45%);\n}\n\n:host([data-status='loading']) .overlay.loading,\n:host([data-status='buffering']) .overlay.loading,\n:host([data-status='error']) .overlay.error {\n display: flex;\n}\n\n:host([data-status='buffering']) .overlay.loading {\n background: transparent;\n pointer-events: none;\n}\n\n.spinner {\n width: 2.5rem;\n height: 2.5rem;\n border: 3px solid rgb(255 255 255 / 25%);\n border-top-color: var(--gyro-view-accent);\n border-radius: 50%;\n animation: gyro-view-spin 0.9s linear infinite;\n}\n\n@keyframes gyro-view-spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n/* A viewer who asked for less motion still sees that it loads, three times slower. */\n@media (prefers-reduced-motion: reduce) {\n .spinner {\n animation-duration: 2.7s;\n }\n}\n\n.error-code {\n font-size: 0.8rem;\n opacity: 0.7;\n}\n";
2
+ var styles_default = ":host {\n --gyro-view-accent: #fff;\n --gyro-view-controls-background: rgb(0 0 0 / 60%);\n --gyro-view-menu-background: rgb(24 24 23 / 92%);\n --gyro-view-text: #fff;\n --gyro-view-font: system-ui, -apple-system, 'Segoe UI', sans-serif;\n --gyro-view-radius: 8px;\n\n display: block;\n position: relative;\n width: 100%;\n aspect-ratio: 16 / 9;\n background: #000;\n color: var(--gyro-view-text);\n font-family: var(--gyro-view-font);\n overflow: hidden;\n user-select: none;\n -webkit-user-select: none;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n:host(:focus-visible) {\n outline: 2px solid var(--gyro-view-accent);\n outline-offset: 2px;\n}\n\n/*\n * Pinned over the page where the Fullscreen API is refused (iPhone Safari). Like the browser's\n * own fullscreen rules it overrides the size, margins and place the page gave the element, and\n * the insets size it to the visible viewport, where 100vh would reach under Safari's toolbars.\n */\n:host([data-fill]) {\n position: fixed !important;\n inset: 0 !important;\n box-sizing: border-box !important;\n width: auto !important;\n height: auto !important;\n min-width: 0 !important;\n min-height: 0 !important;\n max-width: none !important;\n max-height: none !important;\n margin: 0 !important;\n transform: none !important;\n aspect-ratio: auto !important;\n z-index: 2147483000 !important;\n}\n\n:host(:fullscreen) {\n width: 100%;\n height: 100%;\n aspect-ratio: auto;\n}\n\n/* The size container the controls fit themselves to: the player's size, not the page's. Its\n size never depends on its content, which size containment requires. */\n.stage {\n position: absolute;\n inset: 0;\n container-type: size;\n}\n\ncanvas {\n display: block;\n width: 100%;\n height: 100%;\n touch-action: none;\n}\n\n.poster {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n pointer-events: none;\n}\n\n:host([data-has-frame]) .poster,\n:host(:not([poster])) .poster {\n display: none;\n}\n\n.overlay {\n position: absolute;\n inset: 0;\n display: none;\n align-items: center;\n justify-content: center;\n flex-direction: column;\n gap: 0.5rem;\n padding: 1rem;\n text-align: center;\n background: rgb(0 0 0 / 45%);\n}\n\n:host([data-status='loading']) .overlay.loading,\n:host([data-status='buffering']) .overlay.loading,\n:host([data-status='error']) .overlay.error {\n display: flex;\n}\n\n:host([data-status='buffering']) .overlay.loading {\n background: transparent;\n pointer-events: none;\n}\n\n.spinner {\n width: 2.5rem;\n height: 2.5rem;\n border: 3px solid rgb(255 255 255 / 25%);\n border-top-color: var(--gyro-view-accent);\n border-radius: 50%;\n animation: gyro-view-spin 0.9s linear infinite;\n}\n\n@keyframes gyro-view-spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n/* A viewer who asked for less motion still sees that it loads, three times slower. */\n@media (prefers-reduced-motion: reduce) {\n .spinner {\n animation-duration: 2.7s;\n }\n}\n\n.error-code {\n font-size: 0.8rem;\n opacity: 0.7;\n}\n";
3
3
  //#endregion
4
4
  export { styles_default as default };
@@ -9,7 +9,7 @@ import { browserSources } from "./composition/browserPorts.js";
9
9
  */
10
10
  function inspectRecording(recording, options = {}) {
11
11
  const signal = options.signal ?? new AbortController().signal;
12
- const source = browserSources(options.http ?? {}).open(inputOf(recording), signal);
12
+ const { source } = browserSources(options.http ?? {}).open(inputOf(recording), signal);
13
13
  return inspectRecording$1(source);
14
14
  }
15
15
  function inputOf(recording) {