@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.
- package/CHANGELOG.md +60 -0
- package/README.md +21 -5
- package/dist/index.d.ts +24 -2
- package/dist/packages/adapters/fetch/src/HttpByteStream.js +174 -0
- package/dist/packages/adapters/fetch/src/HttpRangeSource.js +14 -170
- package/dist/packages/adapters/fetch/src/HttpResource.js +192 -0
- package/dist/packages/adapters/fetch/src/contentRange.js +15 -0
- package/dist/packages/adapters/fetch/src/httpRequest.js +15 -4
- package/dist/packages/adapters/fetch/src/index.js +3 -1
- package/dist/packages/adapters/fetch/src/passingFailures.js +24 -0
- package/dist/packages/adapters/fetch/src/rangeFailures.js +17 -0
- package/dist/packages/adapters/fetch/src/recordingVersion.js +37 -0
- package/dist/packages/adapters/mediabunny/src/MediabunnyAudioPackager.js +129 -0
- package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +80 -0
- package/dist/packages/adapters/mediabunny/src/SegmentChannel.js +58 -6
- package/dist/packages/adapters/mediabunny/src/decoderConfigurations.js +28 -0
- package/dist/packages/adapters/mediabunny/src/index.js +3 -2
- package/dist/packages/adapters/three/src/shaders/rawLenses.frag.js +1 -1
- package/dist/packages/core/src/application/download/BlockStore.js +97 -0
- package/dist/packages/core/src/application/download/DownloadedAudioSamples.js +50 -0
- package/dist/packages/core/src/application/download/DownloadedVideoTrack.js +122 -0
- package/dist/packages/core/src/application/download/FileDownload.js +183 -0
- package/dist/packages/core/src/application/download/RecordingBuffer.js +22 -0
- package/dist/packages/core/src/application/download/SampleCursor.js +88 -0
- package/dist/packages/core/src/application/download/SourceByteStream.js +46 -0
- package/dist/packages/core/src/application/download/Transfers.js +75 -0
- package/dist/packages/core/src/application/download/startFileDownload.js +37 -0
- package/dist/packages/core/src/application/playback/Buffering.js +53 -0
- package/dist/packages/core/src/application/playback/DecodePipeline.js +11 -7
- package/dist/packages/core/src/application/playback/DecodeRun.js +15 -0
- package/dist/packages/core/src/application/playback/PlaybackSession.js +18 -27
- package/dist/packages/core/src/application/playback/keyframeTimeAt.js +1 -1
- package/dist/packages/core/src/application/playback/probeDecoding.js +15 -1
- package/dist/packages/core/src/application/recording/readSampleTable.js +27 -0
- package/dist/packages/core/src/application/recording/timeRecording.js +3 -3
- package/dist/packages/core/src/domain/container/KeyframeRule.js +7 -0
- package/dist/packages/core/src/domain/container/SampleTable.js +29 -0
- package/dist/packages/core/src/domain/container/TrackSampleTable.js +96 -0
- package/dist/packages/core/src/domain/container/presentationOrder.js +10 -0
- package/dist/packages/core/src/domain/download/DownloadPolicy.js +41 -0
- package/dist/packages/core/src/domain/download/isReadyToResume.js +27 -0
- package/dist/packages/core/src/domain/download/planDownloads.js +175 -0
- package/dist/packages/core/src/domain/download/trackNeeds.js +32 -0
- package/dist/packages/core/src/domain/format/boxes/boxHeader.js +44 -0
- package/dist/packages/core/src/domain/format/boxes/scanBoxes.js +7 -34
- package/dist/packages/core/src/domain/format/mp4/keyframeRules.js +81 -0
- package/dist/packages/core/src/domain/format/mp4/movieBoxes.js +52 -0
- package/dist/packages/core/src/domain/format/mp4/mp4BoxTypes.js +46 -0
- package/dist/packages/core/src/domain/format/mp4/mp4Layouts.js +183 -0
- package/dist/packages/core/src/domain/format/mp4/parseMovie.js +67 -0
- package/dist/packages/core/src/domain/format/mp4/readTable.js +31 -0
- package/dist/packages/core/src/domain/format/mp4/sampleLocations.js +69 -0
- package/dist/packages/core/src/domain/format/mp4/sampleTiming.js +109 -0
- package/dist/packages/core/src/domain/format/mp4/trackHeaders.js +75 -0
- package/dist/packages/core/src/domain/view/lensTilesView.js +2 -2
- package/dist/packages/core/src/domain/view/magnification.js +1 -1
- package/dist/packages/core/src/domain/view/panoramaView.js +1 -1
- package/dist/packages/core/src/index.js +9 -1
- package/dist/packages/core/src/shared/async/iteration.js +24 -0
- package/dist/packages/core/src/shared/binary/ByteRange.js +12 -0
- package/dist/packages/core/src/shared/binary/ByteRangeSet.js +85 -0
- package/dist/packages/core/src/shared/binary/ByteReader.js +21 -3
- package/dist/packages/core/src/shared/binary/concatenated.js +15 -0
- package/dist/packages/core/src/shared/errors/GyroViewError.js +1 -0
- package/dist/packages/core/src/shared/math/countAtOrBelow.js +17 -0
- package/dist/packages/player/src/composition/browserPorts.js +38 -5
- package/dist/packages/player/src/composition/buildPipeline.js +15 -3
- package/dist/packages/player/src/composition/openInputs.js +51 -109
- package/dist/packages/player/src/composition/openRecording.js +13 -2
- package/dist/packages/player/src/composition/readInputs.js +111 -0
- package/dist/packages/player/src/controls/ChoiceMenu.js +16 -4
- package/dist/packages/player/src/controls/PictureMenus.js +22 -8
- package/dist/packages/player/src/controls/SeekBar.js +6 -3
- package/dist/packages/player/src/controls/SoundControls.js +2 -0
- package/dist/packages/player/src/controls/TransportButtons.js +2 -5
- package/dist/packages/player/src/controls/Wording.js +11 -9
- package/dist/packages/player/src/controls/controlParts.js +5 -3
- package/dist/packages/player/src/controls/controls.js +1 -1
- package/dist/packages/player/src/controls/controlsBar.js +4 -1
- package/dist/packages/player/src/controls/controlsMarkup.js +36 -21
- package/dist/packages/player/src/controls/icons.js +32 -8
- package/dist/packages/player/src/controls/messages.js +27 -4
- package/dist/packages/player/src/controls/sliderFill.js +16 -0
- package/dist/packages/player/src/element/ElementLoads.js +7 -0
- package/dist/packages/player/src/element/GyroViewElement.js +8 -3
- package/dist/packages/player/src/element/attributeNames.js +12 -1
- package/dist/packages/player/src/element/attributes.js +29 -7
- package/dist/packages/player/src/element/reflectedProperties.js +19 -1
- package/dist/packages/player/src/element/styles.js +1 -1
- package/dist/packages/player/src/inspectRecording.js +1 -1
- package/dist/standalone.js +385 -126
- package/package.json +1 -1
- package/dist/packages/adapters/mediabunny/src/MediabunnyAudioSegments.js +0 -133
- package/dist/packages/adapters/mediabunny/src/MediabunnyDemuxer.js +0 -63
- 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
|
|
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
|
-
|
|
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-
|
|
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
|
|
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
|
-
|
|
67
|
-
const name =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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,
|
|
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
|
-
|
|
24
|
-
|
|
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
|
|
28
|
-
*
|
|
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"
|
|
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
|
-
<
|
|
36
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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="
|
|
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
|
-
<
|
|
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="
|
|
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="
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
|
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 : {
|
|
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: #
|
|
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) {
|