box-content-preview 3.82.0 → 3.83.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  [![Mergify Status](https://img.shields.io/endpoint.svg?url=https://gh.mergify.io/badges/box/box-content-preview&style=flat)](https://mergify.io)
3
3
  [![Styled With Prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)
4
4
  [![build status](https://img.shields.io/travis/box/box-content-preview/master.svg?style=flat-square)](https://travis-ci.org/box/box-content-preview)
5
- [![version](https://img.shields.io/badge/version-v3.82.0-blue.svg?style=flat-square)](https://github.com/box/box-content-preview)
5
+ [![version](https://img.shields.io/badge/version-v3.83.0-blue.svg?style=flat-square)](https://github.com/box/box-content-preview)
6
6
  [![npm version](https://img.shields.io/npm/v/box-ui-elements.svg?style=flat-square)](https://www.npmjs.com/package/box-ui-elements)
7
7
 
8
8
  # [Box Content Preview](https://developer.box.com/docs/box-content-preview)
@@ -22,11 +22,11 @@ We encourage all users to update their applications to utilize modern browsers f
22
22
 
23
23
  ## Current Version
24
24
 
25
- - Version: v3.82.0
25
+ - Version: v3.83.0
26
26
  - Locale: en-US
27
27
 
28
- https://cdn01.boxcdn.net/platform/preview/3.82.0/en-US/preview.js
29
- https://cdn01.boxcdn.net/platform/preview/3.82.0/en-US/preview.css
28
+ https://cdn01.boxcdn.net/platform/preview/3.83.0/en-US/preview.js
29
+ https://cdn01.boxcdn.net/platform/preview/3.83.0/en-US/preview.css
30
30
 
31
31
  ## Supported Locales
32
32
 
@@ -52,10 +52,10 @@ You can self-host the Box Content Preview library or reference the versions avai
52
52
  <title>Box Content Preview Demo</title>
53
53
 
54
54
  <!-- Latest version of Preview SDK for your locale -->
55
- <script src="https://cdn01.boxcdn.net/platform/preview/3.82.0/en-US/preview.js"></script>
55
+ <script src="https://cdn01.boxcdn.net/platform/preview/3.83.0/en-US/preview.js"></script>
56
56
  <link
57
57
  rel="stylesheet"
58
- href="https://cdn01.boxcdn.net/platform/preview/3.82.0/en-US/preview.css"
58
+ href="https://cdn01.boxcdn.net/platform/preview/3.83.0/en-US/preview.css"
59
59
  />
60
60
  </head>
61
61
  <body>
@@ -76,7 +76,7 @@ To self-host the Box Content Preview library, follow these steps:
76
76
 
77
77
  1. Either fork the repo and check out the version you want to host or download the specific version as a zip:
78
78
 
79
- - Check out a specific version with `git checkout v3.82.0`
79
+ - Check out a specific version with `git checkout v3.83.0`
80
80
  - Download a specific version as a zip from https://github.com/box/box-content-preview/releases
81
81
 
82
82
  2. Install dependencies and build the library with `yarn install && yarn build:i18n && yarn build:prod`
@@ -57,8 +57,8 @@
57
57
  .bp-TimestampControl{position:relative;display:flex;align-items:center}.bp-TimestampControl-button,.bp-TimestampControl-static{display:flex;gap:4px;align-items:center;padding:6px 12px;color:#fff;font-weight:normal;font-size:14px;font-family:Lato,Arial,sans-serif;line-height:20px;letter-spacing:.3px;white-space:nowrap;-webkit-user-select:none;user-select:none}.bp-TimestampControl-button{opacity:.9;transition:opacity 150ms,background-color 150ms;will-change:opacity;outline:0;margin:0;background:transparent;border:0;border-radius:25px;cursor:pointer}.bp-TimestampControl-button:focus:not(:disabled),.bp-TimestampControl-button:hover:not(:disabled){background-color:rgba(255,255,255,.08);opacity:1}.bp-TimestampControl-button:focus:not(:disabled) svg,.bp-TimestampControl-button:hover:not(:disabled) svg,.bp-TimestampControl-button.bp-is-active svg{transform:scale(1.09);transition:transform 150ms}.bp-TimestampControl-button.bp-is-active,.bp-TimestampControl-button:active:not(:disabled){background-color:rgba(255,255,255,.16)}.bp-TimestampControl-button.bp-is-active:focus:not(:disabled),.bp-TimestampControl-button.bp-is-active:hover:not(:disabled){background-color:rgba(255,255,255,.2)}.bp-TimestampControl-button:focus:not(:disabled){box-shadow:inset 0 0 0 1px rgba(211,211,211,.5),0 1px 2px rgba(0,0,0,.1)}.bp-TimestampControl-button:focus-visible:not(:disabled){box-shadow:inset 0 0 0 1px rgba(211,211,211,.5),0 1px 2px rgba(0,0,0,.1)}.bp-TimestampControl-button:focus:not(:focus-visible){box-shadow:none}.bp-TimestampControl-button:focus:not(:disabled),.bp-TimestampControl-button.bp-is-open{background:transparent}.bp-TimestampControl-button:hover:not(:disabled){background:rgba(255,255,255,.08)}.bp-TimestampControl-separator,.bp-TimestampControl-duration{color:rgba(255,255,255,.8)}.bp-TimestampControl-chevron{width:16px;height:16px;margin-left:2px;fill:rgba(255,255,255,.8)}.bp-TimestampControl-flyout{position:absolute;bottom:calc(100% + 12px);left:50%;z-index:1;min-width:250px;padding:12px 8px;background:#fff;border-radius:16px;box-shadow:0 4px 12px 0 rgba(0,0,0,.15);transform:translateX(-50%)}.bp-TimestampControl-header{padding:6px 14px;color:#767676;font-weight:bold;font-size:14px;font-family:Lato,Arial,sans-serif;line-height:20px;letter-spacing:.3px;-webkit-user-select:none;user-select:none}.bp-TimestampControl-listitem{display:flex;gap:16px;align-items:center;justify-content:space-between;padding:6px 14px;color:#000;font-size:16px;font-family:Lato,Arial,sans-serif;line-height:24px;white-space:nowrap;border-radius:8px;outline:none;cursor:pointer}.bp-TimestampControl-listitem:hover,.bp-TimestampControl-listitem:focus{background:#f4f4f4}.bp-TimestampControl-check{flex:none;fill:#000}
58
58
  .bp-VolumeControls{position:relative;display:flex;align-items:center}.bp-VolumeControls-flyout{position:absolute;top:-40px;left:-5px;display:none;flex:0 0 auto;max-width:0;height:40px;overflow:hidden;transition:max-width 200ms ease-in-out}.bp-VolumeControls-flyout.bp-is-open{display:block;overflow:visible}.bp-media--v2 .bp-VolumeControls-flyout{z-index:10}.bp-VolumeControls-slider{width:15px;height:100%;margin-right:6px;margin-left:6px}.bp-VolumeControls-toggle{opacity:.9;transition:opacity 150ms,background-color 150ms;will-change:opacity;outline:0;display:flex;align-items:center;justify-content:center;margin:0;padding:0;color:#fff;background:transparent;border:0;border-radius:25px;cursor:pointer;-webkit-user-select:none;user-select:none;touch-action:manipulation;zoom:1}.bp-VolumeControls-toggle:focus:not(:disabled),.bp-VolumeControls-toggle:hover:not(:disabled){background-color:rgba(255,255,255,.08);opacity:1}.bp-VolumeControls-toggle:focus:not(:disabled) svg,.bp-VolumeControls-toggle:hover:not(:disabled) svg,.bp-VolumeControls-toggle.bp-is-active svg{transform:scale(1.09);transition:transform 150ms}.bp-VolumeControls-toggle.bp-is-active,.bp-VolumeControls-toggle:active:not(:disabled){background-color:rgba(255,255,255,.16)}.bp-VolumeControls-toggle.bp-is-active:focus:not(:disabled),.bp-VolumeControls-toggle.bp-is-active:hover:not(:disabled){background-color:rgba(255,255,255,.2)}.bp-VolumeControls-toggle:focus:not(:disabled){box-shadow:inset 0 0 0 1px rgba(211,211,211,.5),0 1px 2px rgba(0,0,0,.1)}.bp-VolumeControls-toggle:focus-visible:not(:disabled){box-shadow:inset 0 0 0 1px rgba(211,211,211,.5),0 1px 2px rgba(0,0,0,.1)}.bp-VolumeControls-toggle:focus:not(:focus-visible){box-shadow:none}.bp-VolumeControls-toggle:disabled{cursor:default;opacity:.2;pointer-events:none}.bp-VolumeControls-toggle svg{width:32px;height:32px;padding:5px}
59
59
  .bp-VolumeSliderControl{outline:0;position:relative;cursor:pointer}.bp-VolumeSliderControl:focus:not(:disabled){box-shadow:inset 0 0 0 1px rgba(211,211,211,.5),0 1px 2px rgba(0,0,0,.1)}.bp-VolumeSliderControl:focus-visible:not(:disabled){box-shadow:inset 0 0 0 1px rgba(211,211,211,.5),0 1px 2px rgba(0,0,0,.1)}.bp-VolumeSliderControl:focus:not(:focus-visible){box-shadow:none}.bp-VolumeVerticalSliderControl{outline:0;position:absolute;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;height:140px;margin:auto;padding:16px;background:rgba(34,34,34,.9);border:1px solid rgba(255,255,255,.13);border-radius:16px;box-shadow:0 2px 5px 0 rgba(0,0,0,.25);cursor:pointer}.bp-VolumeVerticalSliderControl:focus:not(:disabled){box-shadow:inset 0 0 0 1px rgba(211,211,211,.5),0 1px 2px rgba(0,0,0,.1)}.bp-VolumeVerticalSliderControl:focus-visible:not(:disabled){box-shadow:inset 0 0 0 1px rgba(211,211,211,.5),0 1px 2px rgba(0,0,0,.1)}.bp-VolumeVerticalSliderControl:focus:not(:focus-visible){box-shadow:none}.bp-VolumeSliderControl-thumb{position:absolute;top:calc(50% - 6px);width:10px;height:12px;margin-left:-6px;background:#0061d5;border:0;border-radius:12px;cursor:pointer}.bp-VolumeSliderControl-track{position:absolute;inset:0;height:3px;margin:auto 3px;background:transparent linear-gradient(#fff, #fff) no-repeat center border-box}.bp-VolumeVerticalSliderControl-track-container{position:relative;width:8px;height:108px}.bp-VolumeVerticalSliderControl-track-background{position:relative;left:0;width:8px;height:108px;background-color:#55554f;border-radius:20px}.bp-VolumeVerticalSliderControl-track{position:absolute;bottom:0;left:0;width:8px;background:#fff;border-radius:20px}
60
- .bp-WaveformView{box-sizing:border-box;width:100%;height:206px;min-height:206px;padding:0 40px;overflow:visible;background:none}.bp-WaveformView-track{position:relative;display:flex;align-items:center;width:100%;height:100%;cursor:pointer}.bp-WaveformView-canvas{z-index:0;width:100%;height:140px}.bp-WaveformView-playhead{position:absolute;top:50%;left:0;z-index:1;width:2px;height:150px;pointer-events:none;background:linear-gradient(#2487ff, #006bed);box-shadow:0 0 0 1px #222,0 0 10px 0 rgba(36,135,255,.3);transform:translate(-50%, -50%)}.bp-WaveformView-hover{position:absolute;top:100%;z-index:2;margin-top:4px;pointer-events:none;transform:translateX(-50%)}.bp-WaveformView-hoverTime{padding:4px 10px;color:#fff;font-weight:normal;font-size:14px;font-family:Lato,Arial,sans-serif;line-height:20px;letter-spacing:.3px;white-space:nowrap;background:rgba(34,34,34,.92);border:1px solid rgba(255,255,255,.18);border-radius:8px}.bp-WaveformView--inert{pointer-events:none}.bp-WaveformView--inert .bp-WaveformView-track{cursor:default}
61
- .bp-MP3ControlsRoot:has(.bp-MP3ControlsV2){position:absolute;inset:0;width:100%;height:100%;transform:none;pointer-events:none}.bp-MP3ControlsV2{position:relative;width:100%;max-width:none;height:100%;background:transparent;pointer-events:none}.bp-MP3ControlsV2-stage{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;min-height:0;pointer-events:none}.bp-MP3ControlsV2-stage .bp-WaveformView{pointer-events:auto}.bp-MP3ControlsV2-playOverlay{position:absolute;top:50%;left:50%;z-index:2;display:flex;align-items:center;justify-content:center;width:80px;height:80px;padding:0;background:rgba(0,0,0,.5);border:0;border-radius:50%;transform:translate(-50%, -50%);cursor:pointer;pointer-events:auto}.bp-MP3ControlsV2-playOverlay svg{width:48px;height:48px;fill:#fff}.bp-MP3ControlsV2-stage .bp-media-buffering-spinner{z-index:2;pointer-events:none}.bp-MP3ControlsV2-bar{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;align-items:center;justify-content:space-between;min-height:32px;padding:30px 48px 20px;background:linear-gradient(to top, black, rgba(0, 0, 0, 0));pointer-events:auto}.bp-MP3ControlsV2-group{display:flex;gap:12px;align-items:center}.bp-MP3ControlsV2-divider{width:1px;height:24px;background-color:rgba(255,255,255,.1);border-radius:1px}.bp-MP3ControlsV2 .bp-MP3Controls-settings{position:relative}.bp-MP3ControlsV2 .bp-MP3Controls-settings .bp-SettingsFlyout{right:15px}.bp-MP3ControlsV2 .bp-MP3Controls-settings .bp-SettingsToggle-button{width:36px;height:40px}
60
+ .bp-WaveformView{box-sizing:border-box;width:100%;height:206px;min-height:206px;padding:0 60px;overflow:visible;background:none}.bp-WaveformView-track{position:relative;display:flex;align-items:center;width:100%;height:100%;cursor:pointer}.bp-WaveformView-canvas{z-index:0;width:100%;height:140px}.bp-WaveformView-playhead{position:absolute;top:50%;left:0;z-index:1;width:2px;height:150px;pointer-events:none;background:linear-gradient(#2487ff, #006bed);border-radius:1px;box-shadow:0 0 0 1px #222,0 0 10px 0 rgba(36,135,255,.3);transform:translate(-50%, -50%)}.bp-WaveformView-hover{position:absolute;top:100%;z-index:2;margin-top:4px;pointer-events:none;transform:translateX(-50%)}.bp-WaveformView-hoverTime{padding:4px 10px;color:#fff;font-weight:normal;font-size:14px;font-family:Lato,Arial,sans-serif;line-height:20px;letter-spacing:.3px;white-space:nowrap;background:rgba(34,34,34,.92);border:1px solid rgba(255,255,255,.18);border-radius:8px}.bp-WaveformView--inert{pointer-events:none}.bp-WaveformView--inert .bp-WaveformView-track{cursor:default}
61
+ .bp-MP3ControlsRoot:has(.bp-MP3ControlsV2){position:absolute;inset:0;width:100%;height:100%;transform:none;pointer-events:none}.bp-MP3ControlsV2{position:relative;width:100%;max-width:none;height:100%;background:transparent;pointer-events:none}.bp-MP3ControlsV2-stage{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;min-height:0;pointer-events:none}.bp-MP3ControlsV2-stage .bp-WaveformView{pointer-events:auto}.bp-MP3ControlsV2-playOverlay{position:absolute;top:50%;left:50%;z-index:2;display:flex;align-items:center;justify-content:center;width:80px;height:80px;padding:0;background:rgba(0,0,0,.5);border:0;border-radius:50%;transform:translate(-50%, -50%);cursor:pointer;pointer-events:auto}.bp-MP3ControlsV2-playOverlay svg{width:48px;height:48px;fill:#fff}.bp-MP3ControlsV2-stage .bp-media-buffering-spinner{z-index:2;pointer-events:none}.bp-MP3ControlsV2-bar{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;align-items:center;justify-content:space-between;min-height:32px;padding:30px 58px 25px;background:linear-gradient(to top, black, rgba(0, 0, 0, 0));pointer-events:auto}.bp-MP3ControlsV2-group{display:flex;gap:12px;align-items:center}.bp-MP3ControlsV2-divider{width:1px;height:24px;background-color:rgba(255,255,255,.1);border-radius:1px}.bp-MP3ControlsV2 .bp-MP3Controls-settings{position:relative}.bp-MP3ControlsV2 .bp-MP3Controls-settings .bp-SettingsFlyout{right:15px}.bp-MP3ControlsV2 .bp-MP3Controls-settings .bp-SettingsToggle-button{width:36px;height:40px}
62
62
  .bp-Filmstrip{position:absolute;bottom:100%;overflow:hidden;background:transparent;box-shadow:0 0 1px #464a4f;visibility:hidden}.bp-Filmstrip.bp-is-shown{visibility:visible}.bp-Filmstrip-frame{display:flex;align-items:center;justify-content:center}.bp-Filmstrip-time{height:20px;color:#fff;font-size:13px;line-height:20px;text-align:center;background-color:#222}
63
63
  .bp-SliderControl{outline:0;position:relative;cursor:pointer}.bp-SliderControl:focus:not(:disabled){box-shadow:inset 0 0 0 1px rgba(211,211,211,.5),0 1px 2px rgba(0,0,0,.1)}.bp-SliderControl:focus-visible:not(:disabled){box-shadow:inset 0 0 0 1px rgba(211,211,211,.5),0 1px 2px rgba(0,0,0,.1)}.bp-SliderControl:focus:not(:focus-visible){box-shadow:none}.bp-SliderControl-thumb{position:absolute;top:calc(50% - 6px);width:12px;height:12px;margin-left:-6px;background:#0061d5;border:0;border-radius:12px;cursor:pointer}.bp-SliderControl-track{position:absolute;inset:0;height:3px;margin:auto 3px;background:transparent linear-gradient(#fff, #fff) no-repeat center border-box}
64
64
  .bp-TimeControls{position:relative;margin-right:10px;margin-left:10px}.bp-TimeControls-slider{height:18px}.bp-TimeControls-slider .bp-SliderControl-thumb{transform:scale(0);transition:transform 100ms ease;will-change:transform}.bp-TimeControls-slider .bp-SliderControl-track{backface-visibility:hidden;transition:transform 100ms ease;will-change:transform}.bp-TimeControls-slider.bp-is-scrubbing .bp-SliderControl-thumb,.bp-TimeControls-slider:focus .bp-SliderControl-thumb,.bp-TimeControls-slider:hover .bp-SliderControl-thumb{transform:scale(1)}.bp-TimeControls-slider.bp-is-scrubbing .bp-SliderControl-track,.bp-TimeControls-slider:focus .bp-SliderControl-track,.bp-TimeControls-slider:hover .bp-SliderControl-track{transform:scaleY(2)}
package/dist/lib/index.js CHANGED
@@ -1009,7 +1009,7 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
1009
1009
  const CLIENT_NAME = "box-content-preview"; // eslint-disable-line no-undef
1010
1010
  const CLIENT_NAME_KEY = 'box_client_name';
1011
1011
  const CLIENT_VERSION_KEY = 'box_client_version';
1012
- const CLIENT_VERSION = "3.82.0"; // eslint-disable-line no-undef
1012
+ const CLIENT_VERSION = "3.83.0"; // eslint-disable-line no-undef
1013
1013
  const HEADER_CLIENT_NAME = 'X-Box-Client-Name';
1014
1014
  const HEADER_CLIENT_VERSION = 'X-Box-Client-Version';
1015
1015
  const PROMISE_MAP = {};
@@ -4667,6 +4667,731 @@ function isFpsAvailable(player) {
4667
4667
 
4668
4668
  /***/ },
4669
4669
 
4670
+ /***/ 9514
4671
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
4672
+
4673
+
4674
+ // EXPORTS
4675
+ __webpack_require__.d(__webpack_exports__, {
4676
+ decodeToPeaks: () => (/* binding */ decodeToPeaks),
4677
+ extractPeaks: () => (/* binding */ extractPeaks),
4678
+ getDecodeDecision: () => (/* binding */ getDecodeDecision),
4679
+ loadPeaks: () => (/* binding */ loadPeaks),
4680
+ runClientDecode: () => (/* binding */ runClientDecode)
4681
+ });
4682
+ // ESM COMPAT FLAG
4683
+ __webpack_require__.r(__webpack_exports__);
4684
+
4685
+ ;// ./src/lib/viewers/media/waveform/constants.ts
4686
+ /** Current waveform payload schema version. Bump only with a migration path. */
4687
+ const WAVEFORM_PAYLOAD_VERSION = 1;
4688
+
4689
+ /** Maximum peaks in a single tier (overview or detail). */
4690
+ const MAX_PEAK_COUNT = 16384;
4691
+
4692
+ /** Hard limit on serialized JSON body size for a waveform payload (bytes). */
4693
+ const MAX_PAYLOAD_BYTES = 512 * 1024;
4694
+
4695
+ /** Allowed delta between payload duration and caller-supplied media duration (seconds). */
4696
+ const DURATION_MISMATCH_TOLERANCE_SEC = 1;
4697
+
4698
+ /** Peak values must live in this closed interval when peakScale is "unit". */
4699
+ const PEAK_UNIT_MIN = 0;
4700
+ const PEAK_UNIT_MAX = 1;
4701
+
4702
+ /** Skip client decode when the compressed file is larger than this. */
4703
+ const CLIENT_DECODE_MAX_COMPRESSED_BYTES = 6 * 1024 * 1024;
4704
+
4705
+ /** Skip client decode when media duration is longer than this. */
4706
+ const CLIENT_DECODE_MAX_DURATION_SEC = 5 * 60;
4707
+
4708
+ /** Default overview resolution for client-generated peaks. */
4709
+ const CLIENT_DECODE_PEAK_COUNT = 16384;
4710
+ ;// ./src/lib/viewers/media/waveform/types.ts
4711
+ /**
4712
+ * Box V1 is the in-viewer form: unsigned mono peaks in [0, 1] (peak envelope, mono_max).
4713
+ * Wire JSON only requires version, durationSec, and peaks. Policy fields default.
4714
+ * Conversion or wavesurfer payloads should be adapted at the edge, not stored as-is.
4715
+ */
4716
+
4717
+ /** Who produced peaks */
4718
+
4719
+ /**
4720
+ * Viewer-normalized waveform JSON (version 1).
4721
+ * Required: version, durationSec, peaks. Everything else is optional and defaulted.
4722
+ */
4723
+
4724
+ /** Validated, runtime-friendly peak data passed to renderers. */
4725
+
4726
+ const WAVEFORM_ERROR_CODES = ['INVALID_PAYLOAD', 'UNSUPPORTED_VERSION', 'INVALID_DURATION', 'DURATION_MISMATCH', 'PEAK_COUNT_EXCEEDED', 'PAYLOAD_TOO_LARGE', 'NON_FINITE_PEAK', 'PEAK_OUT_OF_RANGE', 'EMPTY_PEAKS', 'CAP_EXCEEDED', 'LOAD_FAILED', 'DECODE_FAILED', 'CANCELLED', 'UNAVAILABLE'];
4727
+ const WAVEFORM_ERROR_CODE_SET = new Set(WAVEFORM_ERROR_CODES);
4728
+ function isWaveformErrorCode(value) {
4729
+ return WAVEFORM_ERROR_CODE_SET.has(value);
4730
+ }
4731
+
4732
+ /**
4733
+ * Async boundary for waveform data. Implementations may fetch fixtures, decode client-side,
4734
+ * or load Conversion reps — callers only observe WaveformLoadState.
4735
+ */
4736
+ ;// ./src/lib/viewers/media/waveform/createWaveformLoader.ts
4737
+ /* unused harmony import specifier */ var isRetryableWaveformError;
4738
+ /* unused harmony import specifier */ var validateWaveformPayload;
4739
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
4740
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
4741
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
4742
+
4743
+
4744
+ /** Typed throw from fetchPayload (e.g. client decode). `error.name` fallback still works. */
4745
+ class WaveformLoadError extends Error {
4746
+ constructor(code, message) {
4747
+ super(message);
4748
+ _defineProperty(this, "code", void 0);
4749
+ this.name = code;
4750
+ this.code = code;
4751
+ }
4752
+ }
4753
+ function isAbortError(error) {
4754
+ return typeof DOMException !== 'undefined' && error instanceof DOMException && error.name === 'AbortError' || error instanceof Error && error.name === 'AbortError';
4755
+ }
4756
+ function errorFromUnknown(error, fallbackCode = 'LOAD_FAILED') {
4757
+ if (error instanceof WaveformLoadError) {
4758
+ return {
4759
+ code: error.code,
4760
+ message: error.message
4761
+ };
4762
+ }
4763
+ if (error instanceof SyntaxError) {
4764
+ return {
4765
+ code: 'INVALID_PAYLOAD',
4766
+ message: error.message
4767
+ };
4768
+ }
4769
+ if (error instanceof Error && isWaveformErrorCode(error.name)) {
4770
+ return {
4771
+ code: error.name,
4772
+ message: error.message
4773
+ };
4774
+ }
4775
+ const message = error instanceof Error ? error.message : 'Waveform load failed';
4776
+ return {
4777
+ code: fallbackCode,
4778
+ message
4779
+ };
4780
+ }
4781
+ function failedLoadState(error) {
4782
+ return {
4783
+ status: 'failed',
4784
+ error,
4785
+ retryable: isRetryableWaveformError(error.code)
4786
+ };
4787
+ }
4788
+ function readyLoadState(payload) {
4789
+ return {
4790
+ status: 'ready',
4791
+ payload
4792
+ };
4793
+ }
4794
+
4795
+ /**
4796
+ * Wraps an async payload fetch with abort + stale-result suppression.
4797
+ * Playback must not depend on this completing — callers treat unavailable/failed as degrade paths.
4798
+ * abort() cancels an in-flight load only; a successful ready state is left intact.
4799
+ */
4800
+ function createWaveformLoader(fetchPayload, options = {}) {
4801
+ let state = {
4802
+ status: 'unavailable'
4803
+ };
4804
+ let generation = 0;
4805
+ let abortController = null;
4806
+ const setState = next => {
4807
+ state = next;
4808
+ };
4809
+ const settle = next => {
4810
+ setState(next);
4811
+ return next;
4812
+ };
4813
+ const startLoad = () => {
4814
+ if (abortController) {
4815
+ abortController.abort();
4816
+ }
4817
+ generation += 1;
4818
+ abortController = new AbortController();
4819
+ setState({
4820
+ status: 'pending'
4821
+ });
4822
+ return {
4823
+ id: generation,
4824
+ signal: abortController.signal
4825
+ };
4826
+ };
4827
+ const cancelledIfStale = request => {
4828
+ if (!request.signal.aborted && generation === request.id) {
4829
+ return null;
4830
+ }
4831
+ if (generation === request.id) {
4832
+ setState({
4833
+ status: 'cancelled'
4834
+ });
4835
+ }
4836
+ return {
4837
+ status: 'cancelled'
4838
+ };
4839
+ };
4840
+ const settleValidation = validation => {
4841
+ if (!validation.ok) {
4842
+ return settle(failedLoadState(validation.error));
4843
+ }
4844
+ return settle(readyLoadState(validation.payload));
4845
+ };
4846
+ const settleFetchError = (error, request) => {
4847
+ if (request.signal.aborted || generation !== request.id || isAbortError(error)) {
4848
+ if (generation === request.id) {
4849
+ return settle({
4850
+ status: 'cancelled'
4851
+ });
4852
+ }
4853
+ return {
4854
+ status: 'cancelled'
4855
+ };
4856
+ }
4857
+ return settle(failedLoadState(errorFromUnknown(error)));
4858
+ };
4859
+ const load = async () => {
4860
+ const request = startLoad();
4861
+ try {
4862
+ const raw = await fetchPayload(request.signal);
4863
+ const staleAfterFetch = cancelledIfStale(request);
4864
+ if (staleAfterFetch) {
4865
+ return staleAfterFetch;
4866
+ }
4867
+ const validation = validateWaveformPayload(raw, options);
4868
+ const staleAfterValidate = cancelledIfStale(request);
4869
+ if (staleAfterValidate) {
4870
+ return staleAfterValidate;
4871
+ }
4872
+ return settleValidation(validation);
4873
+ } catch (error) {
4874
+ return settleFetchError(error, request);
4875
+ } finally {
4876
+ if (generation === request.id) {
4877
+ abortController = null;
4878
+ }
4879
+ }
4880
+ };
4881
+ const abort = () => {
4882
+ if (!abortController) {
4883
+ return;
4884
+ }
4885
+ generation += 1;
4886
+ abortController.abort();
4887
+ abortController = null;
4888
+ setState({
4889
+ status: 'cancelled'
4890
+ });
4891
+ };
4892
+ return {
4893
+ load,
4894
+ abort,
4895
+ getState: () => state
4896
+ };
4897
+ }
4898
+
4899
+ /** Returns capped state when media exceeds client decode size or duration limits. */
4900
+ function createCappedWaveformState(message) {
4901
+ return {
4902
+ status: 'capped',
4903
+ error: {
4904
+ code: 'CAP_EXCEEDED',
4905
+ message
4906
+ }
4907
+ };
4908
+ }
4909
+ ;// ./src/lib/viewers/media/waveform/validateWaveformPayload.ts
4910
+
4911
+ const DEFAULT_PEAK_SCALE = 'unit';
4912
+ const DEFAULT_CHANNEL_POLICY = 'mono_max';
4913
+ const DEFAULT_ENVELOPE = 'peak';
4914
+ function fail(code, message, retryable = false) {
4915
+ return {
4916
+ ok: false,
4917
+ error: {
4918
+ code,
4919
+ message
4920
+ },
4921
+ retryable
4922
+ };
4923
+ }
4924
+ function isPlainObject(value) {
4925
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4926
+ }
4927
+ function payloadByteLength(raw) {
4928
+ try {
4929
+ const serialized = JSON.stringify(raw);
4930
+ if (typeof Buffer !== 'undefined') {
4931
+ return Buffer.byteLength(serialized, 'utf8');
4932
+ }
4933
+ if (typeof TextEncoder !== 'undefined') {
4934
+ return new TextEncoder().encode(serialized).length;
4935
+ }
4936
+ return serialized.length;
4937
+ } catch {
4938
+ return Number.MAX_SAFE_INTEGER;
4939
+ }
4940
+ }
4941
+ function readOptionalPolicy(raw, key, allowed) {
4942
+ if (!(key in raw) || raw[key] === undefined) {
4943
+ return undefined;
4944
+ }
4945
+ return raw[key] === allowed ? allowed : null;
4946
+ }
4947
+ function asWaveformPayloadV1(raw) {
4948
+ const {
4949
+ version,
4950
+ durationSec,
4951
+ peaks
4952
+ } = raw;
4953
+ if (version !== WAVEFORM_PAYLOAD_VERSION || typeof durationSec !== 'number' || !Array.isArray(peaks)) {
4954
+ return null;
4955
+ }
4956
+ const peakScale = readOptionalPolicy(raw, 'peakScale', DEFAULT_PEAK_SCALE);
4957
+ const channelPolicy = readOptionalPolicy(raw, 'channelPolicy', DEFAULT_CHANNEL_POLICY);
4958
+ const envelope = readOptionalPolicy(raw, 'envelope', DEFAULT_ENVELOPE);
4959
+ if (peakScale === null || channelPolicy === null || envelope === null) {
4960
+ return null;
4961
+ }
4962
+ if ('channels' in raw && raw.channels !== undefined && raw.channels !== 1) {
4963
+ return null;
4964
+ }
4965
+ return {
4966
+ version: WAVEFORM_PAYLOAD_VERSION,
4967
+ durationSec,
4968
+ peaks: peaks,
4969
+ peakScale: peakScale ?? DEFAULT_PEAK_SCALE,
4970
+ channelPolicy: channelPolicy ?? DEFAULT_CHANNEL_POLICY,
4971
+ envelope: envelope ?? DEFAULT_ENVELOPE,
4972
+ channels: 1
4973
+ };
4974
+ }
4975
+ function readPayloadObject(raw, maxPayloadBytes, isPayloadByteCheckSkipped = false) {
4976
+ if (raw === null || raw === undefined) {
4977
+ return fail('INVALID_PAYLOAD', 'Payload is null or undefined');
4978
+ }
4979
+ if (!isPayloadByteCheckSkipped) {
4980
+ const byteLength = payloadByteLength(raw);
4981
+ if (byteLength > maxPayloadBytes) {
4982
+ return fail('PAYLOAD_TOO_LARGE', `Payload size ${byteLength} exceeds limit ${maxPayloadBytes}`);
4983
+ }
4984
+ }
4985
+ if (!isPlainObject(raw)) {
4986
+ return fail('INVALID_PAYLOAD', 'Payload must be an object');
4987
+ }
4988
+ return {
4989
+ ok: true,
4990
+ value: raw
4991
+ };
4992
+ }
4993
+ function checkVersion(raw) {
4994
+ if (raw.version === WAVEFORM_PAYLOAD_VERSION) {
4995
+ return null;
4996
+ }
4997
+ const {
4998
+ version
4999
+ } = raw;
5000
+ if (typeof version === 'number' && version > WAVEFORM_PAYLOAD_VERSION) {
5001
+ return fail('UNSUPPORTED_VERSION', `Unsupported waveform version ${version}; max supported is ${WAVEFORM_PAYLOAD_VERSION}`);
5002
+ }
5003
+ return fail('INVALID_PAYLOAD', 'Missing or invalid version field');
5004
+ }
5005
+ function checkDuration(payload, expectedDurationSec) {
5006
+ if (!Number.isFinite(payload.durationSec) || payload.durationSec <= 0) {
5007
+ return fail('INVALID_DURATION', 'durationSec must be a positive finite number');
5008
+ }
5009
+ if (expectedDurationSec !== undefined && Number.isFinite(expectedDurationSec) && Math.abs(payload.durationSec - expectedDurationSec) > DURATION_MISMATCH_TOLERANCE_SEC) {
5010
+ return fail('DURATION_MISMATCH', `Payload duration ${payload.durationSec}s differs from media duration ${expectedDurationSec}s`, true);
5011
+ }
5012
+ return null;
5013
+ }
5014
+ function normalizePeaks(peaks, maxPeakCount) {
5015
+ if (peaks.length === 0) {
5016
+ return fail('EMPTY_PEAKS', 'peaks array must not be empty');
5017
+ }
5018
+ if (peaks.length > maxPeakCount) {
5019
+ return fail('PEAK_COUNT_EXCEEDED', `peaks length ${peaks.length} exceeds max ${maxPeakCount}`);
5020
+ }
5021
+ const normalized = new Float32Array(peaks.length);
5022
+ for (let i = 0; i < peaks.length; i += 1) {
5023
+ const value = peaks[i];
5024
+ if (!Number.isFinite(value)) {
5025
+ return fail('NON_FINITE_PEAK', `Peak at index ${i} is not finite`);
5026
+ }
5027
+ if (value < PEAK_UNIT_MIN || value > PEAK_UNIT_MAX) {
5028
+ return fail('PEAK_OUT_OF_RANGE', `Peak at index ${i} is outside [${PEAK_UNIT_MIN}, ${PEAK_UNIT_MAX}]`);
5029
+ }
5030
+ normalized[i] = value;
5031
+ }
5032
+ return {
5033
+ ok: true,
5034
+ peaks: normalized
5035
+ };
5036
+ }
5037
+
5038
+ /**
5039
+ * Validates a version-1 waveform payload and returns normalized peaks for rendering.
5040
+ * Wire JSON requires version, durationSec, and peaks. sampleCount and source are ignored.
5041
+ */
5042
+ function validateWaveformPayload_validateWaveformPayload(raw, options = {}) {
5043
+ const maxPeakCount = options.maxPeakCount ?? MAX_PEAK_COUNT;
5044
+ const maxPayloadBytes = options.maxPayloadBytes ?? MAX_PAYLOAD_BYTES;
5045
+ const objectResult = readPayloadObject(raw, maxPayloadBytes, options.isPayloadByteCheckSkipped === true);
5046
+ if (!objectResult.ok) {
5047
+ return objectResult;
5048
+ }
5049
+ const versionFailure = checkVersion(objectResult.value);
5050
+ if (versionFailure) {
5051
+ return versionFailure;
5052
+ }
5053
+ const payload = asWaveformPayloadV1(objectResult.value);
5054
+ if (!payload) {
5055
+ return fail('INVALID_PAYLOAD', 'Payload failed structural validation');
5056
+ }
5057
+ const durationFailure = checkDuration(payload, options.expectedDurationSec);
5058
+ if (durationFailure) {
5059
+ return durationFailure;
5060
+ }
5061
+ const peaksResult = normalizePeaks(payload.peaks, maxPeakCount);
5062
+ if (!peaksResult.ok) {
5063
+ return peaksResult;
5064
+ }
5065
+ return {
5066
+ ok: true,
5067
+ payload: {
5068
+ version: WAVEFORM_PAYLOAD_VERSION,
5069
+ durationSec: payload.durationSec,
5070
+ peaks: peaksResult.peaks,
5071
+ peakScale: payload.peakScale ?? DEFAULT_PEAK_SCALE,
5072
+ channelPolicy: payload.channelPolicy ?? DEFAULT_CHANNEL_POLICY,
5073
+ envelope: payload.envelope ?? DEFAULT_ENVELOPE
5074
+ }
5075
+ };
5076
+ }
5077
+
5078
+ /** Maps validation / load failures to retry policy for loaders. */
5079
+ function validateWaveformPayload_isRetryableWaveformError(code) {
5080
+ return code === 'DURATION_MISMATCH' || code === 'DECODE_FAILED' || code === 'LOAD_FAILED';
5081
+ }
5082
+ ;// ./src/lib/viewers/media/waveform/decode.ts
5083
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
5084
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { decode_defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
5085
+ function decode_defineProperty(e, r, t) { return (r = decode_toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
5086
+ function decode_toPropertyKey(t) { var i = decode_toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
5087
+ function decode_toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
5088
+
5089
+
5090
+
5091
+ const EMPTY_TIMINGS = {
5092
+ attemptMs: null,
5093
+ extractMs: null
5094
+ };
5095
+ function getCurrentTimeMs() {
5096
+ return typeof performance !== 'undefined' ? performance.now() : Date.now();
5097
+ }
5098
+ function createAbortError() {
5099
+ return new DOMException('Aborted', 'AbortError');
5100
+ }
5101
+ function isPositiveFinite(value) {
5102
+ return typeof value === 'number' && Number.isFinite(value) && value > 0;
5103
+ }
5104
+ function getAudioContextConstructor() {
5105
+ if (typeof window === 'undefined') {
5106
+ return undefined;
5107
+ }
5108
+ const {
5109
+ AudioContext,
5110
+ webkitAudioContext
5111
+ } = window;
5112
+ return AudioContext || webkitAudioContext;
5113
+ }
5114
+ function getDecodeSkipMessage(reason) {
5115
+ if (reason === 'compressed_size') {
5116
+ return 'Compressed size exceeds client decode limit';
5117
+ }
5118
+ if (reason === 'duration') {
5119
+ return 'Duration exceeds client decode limit';
5120
+ }
5121
+ return 'Compressed size or duration is missing; skipping client decode';
5122
+ }
5123
+ function createSkippedDecodeResult(reason) {
5124
+ if (reason === 'missing_metadata') {
5125
+ return {
5126
+ status: 'unavailable',
5127
+ error: {
5128
+ code: 'UNAVAILABLE',
5129
+ message: getDecodeSkipMessage(reason)
5130
+ },
5131
+ isDecodeSkipped: true,
5132
+ timings: EMPTY_TIMINGS,
5133
+ reason
5134
+ };
5135
+ }
5136
+ return _objectSpread(_objectSpread({}, createCappedWaveformState(getDecodeSkipMessage(reason))), {}, {
5137
+ isDecodeSkipped: true,
5138
+ timings: EMPTY_TIMINGS,
5139
+ reason
5140
+ });
5141
+ }
5142
+ function decodeWithContext(context, buffer, signal) {
5143
+ return new Promise((resolve, reject) => {
5144
+ let isSettled = false;
5145
+ let onAbort = () => undefined;
5146
+ const onSuccess = decoded => {
5147
+ if (isSettled) {
5148
+ return;
5149
+ }
5150
+ isSettled = true;
5151
+ signal?.removeEventListener('abort', onAbort);
5152
+ resolve(decoded);
5153
+ };
5154
+ const onFailure = error => {
5155
+ if (isSettled) {
5156
+ return;
5157
+ }
5158
+ isSettled = true;
5159
+ signal?.removeEventListener('abort', onAbort);
5160
+ if (isAbortError(error)) {
5161
+ reject(error instanceof DOMException ? error : createAbortError());
5162
+ return;
5163
+ }
5164
+ reject(error instanceof WaveformLoadError ? error : new WaveformLoadError('DECODE_FAILED', 'decodeAudioData failed'));
5165
+ };
5166
+ onAbort = () => onFailure(createAbortError());
5167
+ if (signal?.aborted) {
5168
+ onFailure(createAbortError());
5169
+ return;
5170
+ }
5171
+ signal?.addEventListener('abort', onAbort, {
5172
+ once: true
5173
+ });
5174
+ try {
5175
+ const maybePromise = context.decodeAudioData(buffer, onSuccess, onFailure);
5176
+ if (maybePromise && typeof maybePromise.then === 'function') {
5177
+ maybePromise.then(onSuccess, onFailure);
5178
+ }
5179
+ } catch (error) {
5180
+ onFailure(error);
5181
+ }
5182
+ });
5183
+ }
5184
+
5185
+ /**
5186
+ * Decide whether to run decodeAudioData. Either cap failing, or unknown size/duration,
5187
+ * skips decode so playback is never blocked by expanding the full audio buffer.
5188
+ */
5189
+ function getDecodeDecision(media, caps = {}) {
5190
+ const maxCompressedBytes = caps.maxCompressedBytes ?? CLIENT_DECODE_MAX_COMPRESSED_BYTES;
5191
+ const maxDurationSec = caps.maxDurationSec ?? CLIENT_DECODE_MAX_DURATION_SEC;
5192
+ const {
5193
+ compressedBytes,
5194
+ durationSec
5195
+ } = media;
5196
+ if (!isPositiveFinite(compressedBytes) || !isPositiveFinite(durationSec)) {
5197
+ return {
5198
+ isAllowed: false,
5199
+ reason: 'missing_metadata'
5200
+ };
5201
+ }
5202
+ if (compressedBytes > maxCompressedBytes) {
5203
+ return {
5204
+ isAllowed: false,
5205
+ reason: 'compressed_size'
5206
+ };
5207
+ }
5208
+ if (durationSec > maxDurationSec) {
5209
+ return {
5210
+ isAllowed: false,
5211
+ reason: 'duration'
5212
+ };
5213
+ }
5214
+ return {
5215
+ isAllowed: true
5216
+ };
5217
+ }
5218
+
5219
+ /**
5220
+ * Collapse audio channels to one unsigned peak per time bucket (max abs, then clamp to unit).
5221
+ * Accepts an AudioBuffer so callers can extract before closing the AudioContext.
5222
+ */
5223
+ function extractPeaks(audio, peakCount = CLIENT_DECODE_PEAK_COUNT) {
5224
+ const channels = Array.isArray(audio) ? audio : Array.from({
5225
+ length: audio.numberOfChannels
5226
+ }, (_, channelIndex) => audio.getChannelData(channelIndex));
5227
+ if (peakCount <= 0 || channels.length === 0) {
5228
+ return [];
5229
+ }
5230
+ const sampleCount = channels[0].length;
5231
+ if (sampleCount === 0) {
5232
+ return [];
5233
+ }
5234
+ const peaks = new Array(peakCount);
5235
+ const bucketWidth = sampleCount / peakCount;
5236
+ for (let i = 0; i < peakCount; i += 1) {
5237
+ const start = Math.floor(i * bucketWidth);
5238
+ const end = Math.max(start + 1, Math.floor((i + 1) * bucketWidth));
5239
+ let maxAbs = 0;
5240
+ for (let sampleIndex = start; sampleIndex < end && sampleIndex < sampleCount; sampleIndex += 1) {
5241
+ for (let channelIndex = 0; channelIndex < channels.length; channelIndex += 1) {
5242
+ const value = Math.abs(channels[channelIndex][sampleIndex]);
5243
+ if (value > maxAbs) {
5244
+ maxAbs = value;
5245
+ }
5246
+ }
5247
+ }
5248
+ peaks[i] = Math.min(PEAK_UNIT_MAX, Math.max(PEAK_UNIT_MIN, maxAbs));
5249
+ }
5250
+ return peaks;
5251
+ }
5252
+
5253
+ /**
5254
+ * Decode compressed audio and extract unit peaks while the AudioBuffer is live.
5255
+ * Does not attach a media element or fetch a URL. Does not copy channel data.
5256
+ */
5257
+ async function decodeToPeaks(arrayBuffer, signal, peakCount = CLIENT_DECODE_PEAK_COUNT) {
5258
+ if (signal?.aborted) {
5259
+ throw createAbortError();
5260
+ }
5261
+ const AudioContextConstructor = getAudioContextConstructor();
5262
+ if (!AudioContextConstructor) {
5263
+ throw new WaveformLoadError('DECODE_FAILED', 'AudioContext is not available');
5264
+ }
5265
+ const context = new AudioContextConstructor();
5266
+ try {
5267
+ const audioBuffer = await decodeWithContext(context, arrayBuffer.slice(0), signal);
5268
+ if (signal?.aborted) {
5269
+ throw createAbortError();
5270
+ }
5271
+ const extractStarted = getCurrentTimeMs();
5272
+ const peaks = extractPeaks(audioBuffer, peakCount);
5273
+ return {
5274
+ durationSec: audioBuffer.duration,
5275
+ peaks,
5276
+ extractMs: getCurrentTimeMs() - extractStarted
5277
+ };
5278
+ } catch (error) {
5279
+ if (signal?.aborted || isAbortError(error)) {
5280
+ throw createAbortError();
5281
+ }
5282
+ throw error;
5283
+ } finally {
5284
+ try {
5285
+ await context.close();
5286
+ } catch {
5287
+ // Context may already be closed.
5288
+ }
5289
+ }
5290
+ }
5291
+
5292
+ /**
5293
+ * Run a client-decode attempt against the V1 waveform contract.
5294
+ * Decode is not invoked when size or duration is over the cap (or unknown).
5295
+ */
5296
+ async function runClientDecode(options) {
5297
+ const decision = getDecodeDecision({
5298
+ compressedBytes: options.compressedBytes,
5299
+ durationSec: options.durationSec
5300
+ }, options.caps);
5301
+ if (!decision.isAllowed) {
5302
+ return createSkippedDecodeResult(decision.reason);
5303
+ }
5304
+ if (options.signal?.aborted) {
5305
+ return {
5306
+ status: 'cancelled',
5307
+ isDecodeSkipped: true,
5308
+ timings: EMPTY_TIMINGS
5309
+ };
5310
+ }
5311
+ const signal = options.signal ?? new AbortController().signal;
5312
+ const decodeStarted = getCurrentTimeMs();
5313
+ let decodeOutput;
5314
+ try {
5315
+ decodeOutput = await options.decode(signal);
5316
+ } catch (error) {
5317
+ if (signal.aborted || isAbortError(error)) {
5318
+ return {
5319
+ status: 'cancelled',
5320
+ isDecodeSkipped: false,
5321
+ timings: EMPTY_TIMINGS
5322
+ };
5323
+ }
5324
+ const waveformError = errorFromUnknown(error, 'DECODE_FAILED');
5325
+ return {
5326
+ status: 'failed',
5327
+ error: waveformError,
5328
+ retryable: validateWaveformPayload_isRetryableWaveformError(waveformError.code),
5329
+ isDecodeSkipped: false,
5330
+ timings: {
5331
+ attemptMs: getCurrentTimeMs() - decodeStarted,
5332
+ extractMs: null
5333
+ }
5334
+ };
5335
+ }
5336
+ const attemptMs = getCurrentTimeMs() - decodeStarted;
5337
+ if (signal.aborted) {
5338
+ return {
5339
+ status: 'cancelled',
5340
+ isDecodeSkipped: false,
5341
+ timings: {
5342
+ attemptMs,
5343
+ extractMs: null
5344
+ }
5345
+ };
5346
+ }
5347
+ const validation = validateWaveformPayload_validateWaveformPayload({
5348
+ version: WAVEFORM_PAYLOAD_VERSION,
5349
+ durationSec: options.durationSec ?? decodeOutput.durationSec,
5350
+ peaks: decodeOutput.peaks
5351
+ }, {
5352
+ isPayloadByteCheckSkipped: true
5353
+ });
5354
+ if (!validation.ok) {
5355
+ return {
5356
+ status: 'failed',
5357
+ error: validation.error,
5358
+ retryable: validateWaveformPayload_isRetryableWaveformError(validation.error.code),
5359
+ isDecodeSkipped: false,
5360
+ timings: {
5361
+ attemptMs,
5362
+ extractMs: decodeOutput.extractMs
5363
+ }
5364
+ };
5365
+ }
5366
+ return {
5367
+ status: 'ready',
5368
+ payload: validation.payload,
5369
+ isDecodeSkipped: false,
5370
+ timings: {
5371
+ attemptMs,
5372
+ extractMs: decodeOutput.extractMs
5373
+ }
5374
+ };
5375
+ }
5376
+
5377
+ /**
5378
+ * Gate, fetch, decode, and extract in-viewer peaks. Skips decode when over cap.
5379
+ * Playback must not await this.
5380
+ */
5381
+ function loadPeaks(request) {
5382
+ return runClientDecode({
5383
+ compressedBytes: request.compressedBytes,
5384
+ durationSec: request.durationSec,
5385
+ signal: request.signal,
5386
+ decode: async signal => {
5387
+ const arrayBuffer = await request.fetchArrayBuffer(signal);
5388
+ return decodeToPeaks(arrayBuffer, signal);
5389
+ }
5390
+ });
5391
+ }
5392
+
5393
+ /***/ },
5394
+
4670
5395
  /***/ 2485
4671
5396
  (module, exports) {
4672
5397
 
@@ -20330,7 +21055,7 @@ class Browser {
20330
21055
  ;// ./src/lib/Logger.js
20331
21056
  /* eslint-disable no-undef */
20332
21057
  const CLIENT_NAME = "box-content-preview";
20333
- const CLIENT_VERSION = "3.82.0";
21058
+ const CLIENT_VERSION = "3.83.0";
20334
21059
  /* eslint-enable no-undef */
20335
21060
 
20336
21061
  class Logger {
@@ -33172,6 +33897,11 @@ function MP3Viewer_toPrimitive(t, r) { if ("object" != typeof t || !t) return t;
33172
33897
 
33173
33898
 
33174
33899
  const CSS_CLASS_MP3 = 'bp-media-mp3';
33900
+ function createLoadFailedError(message) {
33901
+ const error = new Error(message);
33902
+ error.name = 'LOAD_FAILED';
33903
+ return error;
33904
+ }
33175
33905
  class MP3Viewer extends media_MediaBaseViewer {
33176
33906
  constructor(...args) {
33177
33907
  super(...args);
@@ -33181,6 +33911,11 @@ class MP3Viewer extends media_MediaBaseViewer {
33181
33911
  MP3Viewer_defineProperty(this, "handlePlayRequest", () => {
33182
33912
  this.userRequestedPlay = true;
33183
33913
  this.togglePlay();
33914
+ if (this.isWaveformDecodeRetryPending && !this.hasUsedWaveformDecodePlayRetry) {
33915
+ this.hasUsedWaveformDecodePlayRetry = true;
33916
+ this.isWaveformDecodeRetryPending = false;
33917
+ this.startClientWaveformDecode();
33918
+ }
33184
33919
  });
33185
33920
  /**
33186
33921
  * Auto-play was prevented, pause the audio
@@ -33210,6 +33945,7 @@ class MP3Viewer extends media_MediaBaseViewer {
33210
33945
  this.wrapperEl.classList.add('bp-media--v2');
33211
33946
  this.mediaContainerEl.classList.add('bp-media-container--v2');
33212
33947
  this.ensureV2Controls();
33948
+ this.importWaveformDecode();
33213
33949
  }
33214
33950
 
33215
33951
  // Audio element
@@ -33271,6 +34007,29 @@ class MP3Viewer extends media_MediaBaseViewer {
33271
34007
  return Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 7893));
33272
34008
  }
33273
34009
 
34010
+ /**
34011
+ * @return {Promise<{ loadPeaks: Function }>} client-decode helpers
34012
+ */
34013
+ async importWaveformDecode() {
34014
+ if (!this.waveformDecodeImport) {
34015
+ this.waveformDecodeImport = Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 9514));
34016
+ }
34017
+ try {
34018
+ return await this.waveformDecodeImport;
34019
+ } catch (error) {
34020
+ this.waveformDecodeImport = null;
34021
+ throw error;
34022
+ }
34023
+ }
34024
+
34025
+ /**
34026
+ * @inheritdoc
34027
+ */
34028
+ destroy() {
34029
+ this.abortClientWaveformDecode();
34030
+ super.destroy();
34031
+ }
34032
+
33274
34033
  /**
33275
34034
  * @inheritdoc
33276
34035
  */
@@ -33315,10 +34074,126 @@ class MP3Viewer extends media_MediaBaseViewer {
33315
34074
  */
33316
34075
  loadeddataHandler() {
33317
34076
  super.loadeddataHandler();
33318
- if (this.isAudioPlayerV2 && this.userRequestedPlay) {
34077
+ if (!this.isAudioPlayerV2) {
34078
+ return;
34079
+ }
34080
+
34081
+ // Play first so Safari has a user gesture before AudioContext is created.
34082
+ if (this.userRequestedPlay) {
33319
34083
  this.play();
33320
34084
  }
34085
+ this.startClientWaveformDecode();
33321
34086
  }
34087
+ /**
34088
+ * Fetch compressed audio bytes for client decode. Prefers an already-fetched blob URL.
34089
+ *
34090
+ * @param {AbortSignal} signal
34091
+ * @return {Promise<ArrayBuffer>}
34092
+ */
34093
+ async fetchAudioArrayBuffer(signal) {
34094
+ if (this.mediaBlobUrl) {
34095
+ const response = await fetch(this.mediaBlobUrl, {
34096
+ signal
34097
+ });
34098
+ if (!response.ok) {
34099
+ throw createLoadFailedError(`Waveform fetch failed (${response.status})`);
34100
+ }
34101
+ return response.arrayBuffer();
34102
+ }
34103
+ const template = this.options.representation && this.options.representation.content && this.options.representation.content.url_template;
34104
+ if (!template) {
34105
+ throw createLoadFailedError('Waveform fetch URL is missing');
34106
+ }
34107
+ const data = await this.api.get(this.createContentUrlV2(template), {
34108
+ headers: this.appendAuthHeader(),
34109
+ signal,
34110
+ type: 'arraybuffer'
34111
+ });
34112
+ if (data instanceof ArrayBuffer) {
34113
+ return data;
34114
+ }
34115
+ throw createLoadFailedError('Waveform fetch did not return binary data');
34116
+ }
34117
+ abortClientWaveformDecode() {
34118
+ if (!this.waveformDecodeController) {
34119
+ return;
34120
+ }
34121
+ this.waveformDecodeController.abort();
34122
+ this.waveformDecodeController = null;
34123
+ }
34124
+
34125
+ /**
34126
+ * Decode peaks in the background when the file is under size and duration caps.
34127
+ * Does not block playback. Capped or failed decode keeps the placeholder waveform.
34128
+ *
34129
+ * @return {Promise<void>}
34130
+ */
34131
+ async startClientWaveformDecode() {
34132
+ if (!this.isAudioPlayerV2 || this.destroyed) {
34133
+ return;
34134
+ }
34135
+ this.abortClientWaveformDecode();
34136
+ const controller = new AbortController();
34137
+ this.waveformDecodeController = controller;
34138
+ const {
34139
+ signal
34140
+ } = controller;
34141
+ try {
34142
+ const decodeModule = await this.importWaveformDecode();
34143
+ if (this.destroyed || signal.aborted || this.waveformDecodeController !== controller) {
34144
+ return;
34145
+ }
34146
+ const result = await decodeModule.loadPeaks({
34147
+ compressedBytes: this.options.file && this.options.file.size,
34148
+ durationSec: this.mediaEl && this.mediaEl.duration,
34149
+ fetchArrayBuffer: fetchSignal => this.fetchAudioArrayBuffer(fetchSignal),
34150
+ signal
34151
+ });
34152
+ this.handleClientWaveformDecodeResult(result, controller, signal);
34153
+ } catch (error) {
34154
+ if (error && error.name === 'AbortError') {
34155
+ this.handleClientWaveformDecodeResult({
34156
+ status: 'cancelled'
34157
+ }, controller, signal);
34158
+ return;
34159
+ }
34160
+ this.handleClientWaveformDecodeResult({
34161
+ error: {
34162
+ code: 'LOAD_FAILED',
34163
+ message: error instanceof Error ? error.message : 'Waveform decode failed'
34164
+ },
34165
+ retryable: true,
34166
+ status: 'failed'
34167
+ }, controller, signal);
34168
+ }
34169
+ }
34170
+
34171
+ /**
34172
+ * Apply peaks, or record a retryable failure for overlay play.
34173
+ *
34174
+ * @param {import('./waveform/types').ClientDecodeResult} result
34175
+ * @param {AbortController} controller in-flight controller for this attempt
34176
+ * @param {AbortSignal} signal
34177
+ * @return {void}
34178
+ */
34179
+ handleClientWaveformDecodeResult(result, controller, signal) {
34180
+ if (!result || this.destroyed || signal.aborted || this.waveformDecodeController !== controller) {
34181
+ return;
34182
+ }
34183
+ if (result.status === 'ready') {
34184
+ this.waveformPeaks = result.payload.peaks;
34185
+ this.isWaveformDecodeRetryPending = false;
34186
+ this.renderUI();
34187
+ return;
34188
+ }
34189
+ if (result.status === 'cancelled') {
34190
+ return;
34191
+ }
34192
+ if (result.status === 'failed' && result.retryable && !this.hasUsedWaveformDecodePlayRetry) {
34193
+ this.isWaveformDecodeRetryPending = true;
34194
+ }
34195
+ }
34196
+
33322
34197
  /**
33323
34198
  * @inheritdoc
33324
34199
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "box-content-preview",
3
- "version": "3.82.0",
3
+ "version": "3.83.0",
4
4
  "description": "Box Content Preview UI Element",
5
5
  "author": "Box (https://www.box.com/)",
6
6
  "license": "SEE LICENSE IN LICENSE",