box-content-preview 3.89.0 → 3.91.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/dist/lib/index.js CHANGED
@@ -21,6 +21,7 @@
21
21
  * and limitations under the license.
22
22
  */
23
23
  import { decode as __WEBPACK_EXTERNAL_MODULE_box_ui_elements_es_utils_keys_d752524f_decode__ } from "box-ui-elements/es/utils/keys";
24
+ import { createPortal as __WEBPACK_EXTERNAL_MODULE_react_dom_7dac9eee_createPortal__ } from "react-dom";
24
25
  import * as __WEBPACK_EXTERNAL_MODULE_react__ from "react";
25
26
  import { default as __WEBPACK_EXTERNAL_MODULE_axios_default__ } from "axios";
26
27
  import { bdlBoxBlue as __WEBPACK_EXTERNAL_MODULE_box_ui_elements_es_styles_variables_c815ce80_bdlBoxBlue__, bdlGray65 as __WEBPACK_EXTERNAL_MODULE_box_ui_elements_es_styles_variables_c815ce80_bdlGray65__, bdlGreenLight as __WEBPACK_EXTERNAL_MODULE_box_ui_elements_es_styles_variables_c815ce80_bdlGreenLight__, bdlGrimace as __WEBPACK_EXTERNAL_MODULE_box_ui_elements_es_styles_variables_c815ce80_bdlGrimace__, bdlWatermelonRed as __WEBPACK_EXTERNAL_MODULE_box_ui_elements_es_styles_variables_c815ce80_bdlWatermelonRed__, bdlYellorange as __WEBPACK_EXTERNAL_MODULE_box_ui_elements_es_styles_variables_c815ce80_bdlYellorange__, bdlYellow as __WEBPACK_EXTERNAL_MODULE_box_ui_elements_es_styles_variables_c815ce80_bdlYellow__, white as __WEBPACK_EXTERNAL_MODULE_box_ui_elements_es_styles_variables_c815ce80_white__ } from "box-ui-elements/es/styles/variables";
@@ -327,6 +328,7 @@ class PreviewError extends Error {
327
328
  /* harmony export */ Jm: () => (/* binding */ SELECTOR_BOX_PREVIEW_LOGO_CUSTOM),
328
329
  /* harmony export */ Kb: () => (/* binding */ CLASS_BOX_PREVIEW_THUMBNAILS_CONTAINER),
329
330
  /* harmony export */ Kh: () => (/* binding */ SELECTOR_NAVIGATION_RIGHT),
331
+ /* harmony export */ Ku: () => (/* binding */ AUDIO_PLAYER_V2),
330
332
  /* harmony export */ Lq: () => (/* binding */ PREVIEW_SCRIPT_NAME),
331
333
  /* harmony export */ Lz: () => (/* binding */ CLASS_BOX_PREVIEW_THUMBNAILS_CLOSE),
332
334
  /* harmony export */ M$: () => (/* binding */ CLASS_BOX_PREVIEW_PRELOAD_WRAPPER_DOCUMENT),
@@ -379,6 +381,7 @@ class PreviewError extends Error {
379
381
  /* harmony export */ jF: () => (/* binding */ CLASS_IS_VISIBLE),
380
382
  /* harmony export */ jP: () => (/* binding */ CLASS_PREVIEW_LOADED),
381
383
  /* harmony export */ jf: () => (/* binding */ CLASS_FULLSCREEN_UNSUPPORTED),
384
+ /* harmony export */ jr: () => (/* binding */ X_REP_HINT_WAVEFORM),
382
385
  /* harmony export */ lM: () => (/* binding */ CLASS_BOX_PREVIEW_MOBILE),
383
386
  /* harmony export */ lR: () => (/* binding */ SELECTOR_BOX_PREVIEW_HEADER_CONTAINER),
384
387
  /* harmony export */ lX: () => (/* binding */ CLASS_BOX_PREVIEW_PRELOAD_OVERLAY),
@@ -396,6 +399,8 @@ class PreviewError extends Error {
396
399
  /* harmony export */ rR: () => (/* binding */ CLASS_BOX_PREVIEW_THEME_DARK),
397
400
  /* harmony export */ rZ: () => (/* binding */ CLASS_BOX_PREVIEW_OVERLAY),
398
401
  /* harmony export */ rr: () => (/* binding */ CLASS_IS_BUFFERING),
402
+ /* harmony export */ u4: () => (/* binding */ WAVEFORM_REP_NAME),
403
+ /* harmony export */ uD: () => (/* binding */ STATUS_ERROR),
399
404
  /* harmony export */ uN: () => (/* binding */ MEDIA_PLAYBACK_SKIP_DURATION),
400
405
  /* harmony export */ vV: () => (/* binding */ CLASS_BOX_PREVIEW_PRELOAD_WRAPPER_PRESENTATION),
401
406
  /* harmony export */ v_: () => (/* binding */ SELECTOR_BOX_PREVIEW_BTN_DOWNLOAD),
@@ -415,7 +420,7 @@ class PreviewError extends Error {
415
420
  /* harmony export */ zb: () => (/* binding */ CLASS_DOC_FIRST_IMAGE),
416
421
  /* harmony export */ ze: () => (/* binding */ STATUS_PENDING)
417
422
  /* harmony export */ });
418
- /* unused harmony exports CLASS_ACTIVE, CLASS_BOX_PREVIEW, CLASS_BOX_PREVIEW_CONTAINER, CLASS_BOX_PREVIEW_HEADER_CONTAINER, CLASS_BOX_PREVIEW_ICON, CLASS_BOX_PREVIEW_LOGO_CUSTOM, CLASS_BOX_PREVIEW_LOGO_DEFAULT, CLASS_PREFETCHED_IMAGE, SELECTOR_BOX_PREVIEW_HEADER, SELECTOR_BOX_PREVIEW_NOTIFICATION, STATUS_ERROR */
423
+ /* unused harmony exports CLASS_ACTIVE, CLASS_BOX_PREVIEW, CLASS_BOX_PREVIEW_CONTAINER, CLASS_BOX_PREVIEW_HEADER_CONTAINER, CLASS_BOX_PREVIEW_ICON, CLASS_BOX_PREVIEW_LOGO_CUSTOM, CLASS_BOX_PREVIEW_LOGO_DEFAULT, CLASS_PREFETCHED_IMAGE, SELECTOR_BOX_PREVIEW_HEADER, SELECTOR_BOX_PREVIEW_NOTIFICATION */
419
424
  const CLASS_ACTIVE = 'bp-is-active';
420
425
  const CLASS_ANNOTATIONS_CREATE_DRAWING = 'bp-annotations-create--drawing';
421
426
  const CLASS_ANNOTATIONS_CREATE_HIGHLIGHT = 'bp-annotations-create--highlight';
@@ -502,6 +507,7 @@ const APP_HOST = 'https://app.box.com';
502
507
  const ORIGINAL_REP_NAME = 'ORIGINAL';
503
508
  const PRELOAD_REP_NAME = 'jpg';
504
509
  const PRELOAD_PAGED_REP_NAME = 'webp';
510
+ const WAVEFORM_REP_NAME = 'waveform';
505
511
  const VIDEO_VIEWER_NAMES = ['Dash', 'MP4'];
506
512
  const STATUS_ERROR = 'error';
507
513
  const STATUS_NONE = 'none';
@@ -516,7 +522,9 @@ const X_REP_HINT_IMAGE = '[jpg?dimensions=2048x2048,png?dimensions=2048x2048]';
516
522
  const X_REP_HINT_VIDEO_DASH = '[dash,mp4][filmstrip]';
517
523
  const X_REP_HINT_VIDEO_DASH_EXTRACTED_TEXT = '[extracted_text]';
518
524
  const X_REP_HINT_VIDEO_MP4 = '[mp4]';
525
+ const X_REP_HINT_WAVEFORM = '[waveform]';
519
526
  const AI_TRANSCRIPTION_FOR_VIDEO_SUBTITLES = 'aiTranscriptionForVideoSubtitles';
527
+ const AUDIO_PLAYER_V2 = 'audioPlayerV2.enabled';
520
528
  const PDFJS_CSS_UNITS = 96.0 / 72.0; // Should match CSS_UNITS in pdf_viewer.js
521
529
  const PDFJS_MAX_AUTO_SCALE = 1.25; // Should match MAX_AUTO_SCALE in pdf_viewer.js
522
530
  const PDFJS_WIDTH_PADDING_PX = 40; // Should match SCROLLBAR_PADDING in pdf_viewer.js
@@ -1010,7 +1018,7 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
1010
1018
  const CLIENT_NAME = "box-content-preview"; // eslint-disable-line no-undef
1011
1019
  const CLIENT_NAME_KEY = 'box_client_name';
1012
1020
  const CLIENT_VERSION_KEY = 'box_client_version';
1013
- const CLIENT_VERSION = "3.89.0"; // eslint-disable-line no-undef
1021
+ const CLIENT_VERSION = "3.91.0"; // eslint-disable-line no-undef
1014
1022
  const HEADER_CLIENT_NAME = 'X-Box-Client-Name';
1015
1023
  const HEADER_CLIENT_VERSION = 'X-Box-Client-Version';
1016
1024
  const PROMISE_MAP = {};
@@ -4480,7 +4488,7 @@ function getPdfjsWorkerSrc() {
4480
4488
 
4481
4489
  /***/ },
4482
4490
 
4483
- /***/ 6180
4491
+ /***/ 2668
4484
4492
  (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
4485
4493
 
4486
4494
 
@@ -4614,6 +4622,7 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
4614
4622
  /** Visible window: start/end times, pixels-per-second, and the current zoom. */
4615
4623
  function createWaveformViewport({
4616
4624
  durationSec,
4625
+ gutterPx = 0,
4617
4626
  heightPx,
4618
4627
  maxZoom,
4619
4628
  scrollLeftPx,
@@ -4623,11 +4632,16 @@ function createWaveformViewport({
4623
4632
  const zoom = Number.isFinite(zoomLevel) && zoomLevel > 0 ? zoomLevel : constants/* WAVEFORM_ZOOM_MIN */.LW;
4624
4633
  const viewDurationSec = durationSec > 0 ? durationSec / zoom : 0; // seconds visible at this zoom
4625
4634
  const pixelsPerSecond = viewDurationSec > 0 && widthPx > 0 ? widthPx / viewDurationSec : 0;
4626
- const maxStartSec = Math.max(0, durationSec - viewDurationSec); // last start that still fills the window
4627
- const startSec = pixelsPerSecond > 0 ? Math.min(maxStartSec, Math.max(0, scrollLeftPx / pixelsPerSecond)) : 0;
4635
+ const leadingGutterPx = Number.isFinite(gutterPx) && gutterPx > 0 ? gutterPx : 0;
4636
+ const unclampedStartSec = pixelsPerSecond > 0 ? (scrollLeftPx - leadingGutterPx) / pixelsPerSecond : 0;
4637
+ const gutterDurationSec = pixelsPerSecond > 0 ? leadingGutterPx / pixelsPerSecond : 0;
4638
+ const latestWindowStartSec = leadingGutterPx > 0 ? durationSec + gutterDurationSec - viewDurationSec : Math.max(0, durationSec - viewDurationSec);
4639
+ const earliestWindowStartSec = leadingGutterPx > 0 ? -gutterDurationSec : 0;
4640
+ const startSec = pixelsPerSecond > 0 ? Math.min(latestWindowStartSec, Math.max(earliestWindowStartSec, unclampedStartSec)) : 0;
4628
4641
  return {
4629
4642
  durationSec,
4630
4643
  endSec: startSec + viewDurationSec,
4644
+ gutterPx: leadingGutterPx,
4631
4645
  heightPx,
4632
4646
  maxZoom,
4633
4647
  pixelsPerSecond,
@@ -4647,7 +4661,7 @@ function getViewportAtScroll(viewport, scrollLeftPx) {
4647
4661
 
4648
4662
  /** True when the visible window has not moved — skip a React state update. */
4649
4663
  function viewportEquals(prev, next) {
4650
- return !!prev && prev.durationSec === next.durationSec && prev.endSec === next.endSec && prev.maxZoom === next.maxZoom && prev.scrollLeftPx === next.scrollLeftPx && prev.startSec === next.startSec && prev.widthPx === next.widthPx && prev.zoomLevel === next.zoomLevel;
4664
+ return !!prev && prev.durationSec === next.durationSec && prev.endSec === next.endSec && prev.gutterPx === next.gutterPx && prev.maxZoom === next.maxZoom && prev.scrollLeftPx === next.scrollLeftPx && prev.startSec === next.startSec && prev.widthPx === next.widthPx && prev.zoomLevel === next.zoomLevel;
4651
4665
  }
4652
4666
 
4653
4667
  /** How many CSS pixels from the left of the visible window this time sits. */
@@ -4694,17 +4708,34 @@ function clampWaveformZoom(zoomLevel, maxZoom = constants/* WAVEFORM_ZOOM_MIN */
4694
4708
  /** WaveSurfer zoom density. 0 = fit the whole file in the view. */
4695
4709
  function getZoomedPixelsPerSecond({
4696
4710
  durationSec,
4711
+ isTape = false,
4697
4712
  maxZoom = constants/* WAVEFORM_ZOOM_MIN */.LW,
4698
4713
  viewWidthPx,
4699
4714
  zoomLevel
4700
4715
  }) {
4701
4716
  const zoom = clampWaveformZoom(zoomLevel, maxZoom);
4702
- if (zoom <= constants/* WAVEFORM_ZOOM_MIN */.LW || !(durationSec > 0) || !(viewWidthPx > 0)) {
4717
+ if (!(durationSec > 0) || !(viewWidthPx > 0)) {
4718
+ return 0;
4719
+ }
4720
+ if (!isTape && zoom <= constants/* WAVEFORM_ZOOM_MIN */.LW) {
4703
4721
  return 0;
4704
4722
  }
4705
4723
  return viewWidthPx / durationSec * zoom;
4706
4724
  }
4707
4725
 
4726
+ /** Half-view empty space so t=0 and duration can sit under the center pin. */
4727
+ function getTapeGutterPx(widthPx) {
4728
+ return widthPx > 0 ? widthPx / 2 : 0;
4729
+ }
4730
+
4731
+ /** Tape first paint: 10s on screen, or 1× when the file is shorter than that. */
4732
+ function getTapeDefaultZoom(durationSec, maxZoom = constants/* WAVEFORM_ZOOM_MIN */.LW) {
4733
+ if (!(durationSec > 0)) {
4734
+ return constants/* WAVEFORM_ZOOM_MIN */.LW;
4735
+ }
4736
+ return clampWaveformZoom(durationSec / constants/* WAVEFORM_TAPE_DEFAULT_WINDOW_SEC */.Av, maxZoom);
4737
+ }
4738
+
4708
4739
  /** Map zoom (1…max) onto the 0–100 slider. */
4709
4740
  function sliderValueFromZoom(zoomLevel, maxZoom = constants/* WAVEFORM_ZOOM_MIN */.LW) {
4710
4741
  const max = Math.max(constants/* WAVEFORM_ZOOM_MIN */.LW, maxZoom);
@@ -4725,9 +4756,10 @@ function zoomFromSliderValue(value, maxZoom = constants/* WAVEFORM_ZOOM_MIN */.L
4725
4756
  return clampWaveformZoom(constants/* WAVEFORM_ZOOM_MIN */.LW + t * (max - constants/* WAVEFORM_ZOOM_MIN */.LW), max);
4726
4757
  }
4727
4758
 
4728
- /** Max scroll that still shows a full window (no overscroll). */
4759
+ /** Max scroll that still shows a full window (no overscroll). Gutters add empty lead/trail. */
4729
4760
  function maxScrollLeft(viewport) {
4730
- return Math.max(0, viewport.durationSec * viewport.pixelsPerSecond - viewport.widthPx);
4761
+ const gutterPx = viewport.gutterPx || 0;
4762
+ return Math.max(0, viewport.durationSec * viewport.pixelsPerSecond + 2 * gutterPx - viewport.widthPx);
4731
4763
  }
4732
4764
 
4733
4765
  /** Scroll offset that still shows a full window (no overscroll). */
@@ -4740,7 +4772,7 @@ function clampScrollLeft(scrollLeftPx, viewport) {
4740
4772
 
4741
4773
  /** Center this time in the view, clamped so the window still fills the canvas. */
4742
4774
  function getCenteredScrollLeft(timeSec, viewport) {
4743
- return clampScrollLeft(timeSec * viewport.pixelsPerSecond - viewport.widthPx / 2, viewport);
4775
+ return clampScrollLeft(timeSec * viewport.pixelsPerSecond + (viewport.gutterPx || 0) - viewport.widthPx / 2, viewport);
4744
4776
  }
4745
4777
 
4746
4778
  /**
@@ -4748,10 +4780,28 @@ function getCenteredScrollLeft(timeSec, viewport) {
4748
4780
  * No-op at fit-to-width or when that time is already visible.
4749
4781
  */
4750
4782
  function getSeekCameraAction({
4783
+ cameraMode = 'desktop',
4751
4784
  timeSec,
4752
4785
  viewport
4753
4786
  }) {
4754
- if (viewport.zoomLevel <= constants/* WAVEFORM_ZOOM_MIN */.LW || !(viewport.widthPx > 0) || !(viewport.pixelsPerSecond > 0)) {
4787
+ if (!(viewport.widthPx > 0) || !(viewport.pixelsPerSecond > 0)) {
4788
+ return {
4789
+ type: 'none'
4790
+ };
4791
+ }
4792
+ if (cameraMode === 'tape') {
4793
+ const scrollLeftPx = getCenteredScrollLeft(timeSec, viewport);
4794
+ if (Math.abs(scrollLeftPx - viewport.scrollLeftPx) < 1) {
4795
+ return {
4796
+ type: 'none'
4797
+ };
4798
+ }
4799
+ return {
4800
+ type: 'jump',
4801
+ scrollLeftPx
4802
+ };
4803
+ }
4804
+ if (viewport.zoomLevel <= constants/* WAVEFORM_ZOOM_MIN */.LW) {
4755
4805
  return {
4756
4806
  type: 'none'
4757
4807
  };
@@ -4767,6 +4817,23 @@ function getSeekCameraAction({
4767
4817
  };
4768
4818
  }
4769
4819
 
4820
+ /** Always keep this time under the center pin. Reuses followRight so the camera hook can pin. */
4821
+ function getTapeCameraAction({
4822
+ timeSec,
4823
+ viewport
4824
+ }) {
4825
+ if (!(viewport.widthPx > 0) || !(viewport.pixelsPerSecond > 0)) {
4826
+ return {
4827
+ type: 'none'
4828
+ };
4829
+ }
4830
+ return {
4831
+ isPlayheadPinned: true,
4832
+ scrollLeftPx: getCenteredScrollLeft(timeSec, viewport),
4833
+ type: 'followRight'
4834
+ };
4835
+ }
4836
+
4770
4837
  /** Follow inset in CSS px, capped at one third of the view so a narrow player still has room. */
4771
4838
  function getFollowInsetPx(widthPx, insetPx = constants/* WAVEFORM_FOLLOW_INSET_PX */.NM) {
4772
4839
  if (!(widthPx > 0)) {
@@ -4784,6 +4851,11 @@ function getPinnedPlayheadLeft(widthPx, insetPx = constants/* WAVEFORM_FOLLOW_IN
4784
4851
  return `${(widthPx - inset) / widthPx * 100}%`;
4785
4852
  }
4786
4853
 
4854
+ /** CSS left for a playhead locked to the center of the view. */
4855
+ function getTapePinnedPlayheadLeft() {
4856
+ return '50%';
4857
+ }
4858
+
4787
4859
  /** CSS left % of the playhead from the left of the visible window. */
4788
4860
  function timeLeftPercent(timeSec, durationSec, viewport) {
4789
4861
  if (viewport.widthPx > 0 && viewport.pixelsPerSecond > 0) {
@@ -4798,12 +4870,19 @@ function timeLeftPercent(timeSec, durationSec, viewport) {
4798
4870
  * Off-screen at play start jumps in; off-screen right while playing keeps following.
4799
4871
  */
4800
4872
  function getPlayheadCameraAction({
4873
+ cameraMode = 'desktop',
4801
4874
  insetPx = constants/* WAVEFORM_FOLLOW_INSET_PX */.NM,
4802
4875
  isPlaying,
4803
4876
  playJustStarted,
4804
4877
  timeSec,
4805
4878
  viewport
4806
4879
  }) {
4880
+ if (cameraMode === 'tape') {
4881
+ return getTapeCameraAction({
4882
+ timeSec,
4883
+ viewport
4884
+ });
4885
+ }
4807
4886
  if (!isPlaying || viewport.zoomLevel <= constants/* WAVEFORM_ZOOM_MIN */.LW || !(viewport.widthPx > 0) || !(viewport.pixelsPerSecond > 0)) {
4808
4887
  return {
4809
4888
  type: 'none'
@@ -4843,6 +4922,46 @@ function getPlayheadCameraAction({
4843
4922
  type: 'none'
4844
4923
  };
4845
4924
  }
4925
+ ;// ./src/lib/viewers/media/waveform/useTapeWaveform.ts
4926
+
4927
+
4928
+ function isIPadNavigator(tapeNavigator) {
4929
+ return /iPad/i.test(tapeNavigator.userAgent || '') || tapeNavigator.platform === 'MacIntel' && (tapeNavigator.maxTouchPoints || 0) > 1;
4930
+ }
4931
+ function isCoarsePrimaryPointer(tapeWindow) {
4932
+ return typeof tapeWindow.matchMedia === 'function' && tapeWindow.matchMedia(constants/* TAPE_POINTER_MEDIA_QUERY */.Le).matches;
4933
+ }
4934
+
4935
+ /**
4936
+ * Tape vs desktop camera: coarse primary pointer (finger) or iPad
4937
+ * (including iPadOS reporting itself as Mac). Not width, not hasTouch,
4938
+ * not Browser.isMobile().
4939
+ */
4940
+ function isTapeWaveformInput(tapeWindow = window) {
4941
+ return isCoarsePrimaryPointer(tapeWindow) || isIPadNavigator(tapeWindow.navigator);
4942
+ }
4943
+
4944
+ /** Subscribe to the pointer media query; re-read the iPad heuristic on change. */
4945
+ function useTapeWaveform() {
4946
+ const [isTape, setIsTape] = (0,external_react_.useState)(() => typeof window !== 'undefined' && isTapeWaveformInput(window));
4947
+ (0,external_react_.useEffect)(() => {
4948
+ if (typeof window.matchMedia !== 'function') {
4949
+ setIsTape(isTapeWaveformInput(window));
4950
+ return undefined;
4951
+ }
4952
+ const mediaQuery = window.matchMedia(constants/* TAPE_POINTER_MEDIA_QUERY */.Le);
4953
+ const sync = () => {
4954
+ setIsTape(isTapeWaveformInput(window));
4955
+ };
4956
+ sync();
4957
+ mediaQuery.addEventListener('change', sync);
4958
+ return () => mediaQuery.removeEventListener('change', sync);
4959
+ }, []);
4960
+ return isTape;
4961
+ }
4962
+ // EXTERNAL MODULE: ./node_modules/classnames/index.js
4963
+ var classnames = __webpack_require__(2485);
4964
+ var classnames_default = /*#__PURE__*/__webpack_require__.n(classnames);
4846
4965
  // EXTERNAL MODULE: ./src/lib/viewers/controls/media/markers/helpers/useDismissableMarkerSelection.ts
4847
4966
  var useDismissableMarkerSelection = __webpack_require__(2378);
4848
4967
  // EXTERNAL MODULE: ./src/lib/viewers/controls/media/markers/helpers/buildClusters.ts
@@ -4862,6 +4981,7 @@ var utils = __webpack_require__(5346);
4862
4981
 
4863
4982
 
4864
4983
 
4984
+
4865
4985
  const WAVEFORM_MARKER_SIZE_PX = 20;
4866
4986
  function hasMappedWindow(viewport) {
4867
4987
  return !!viewport && Number.isFinite(viewport.startSec) && viewport.endSec > viewport.startSec;
@@ -4900,11 +5020,12 @@ function clustersByExactTime(markers, durationSec) {
4900
5020
  function WaveformCommentMarkers({
4901
5021
  commentMarkers,
4902
5022
  durationSec,
5023
+ isTape = false,
4903
5024
  onCommentMarkerClick,
4904
5025
  selectedId: hostSelectedId = null,
4905
5026
  viewport = null
4906
5027
  }) {
4907
- const trackRef = (0,external_react_.useRef)(null);
5028
+ const trackRef = (0,external_react_.useRef)(null); // width source for clustering overlapping badges
4908
5029
  const {
4909
5030
  containerRef,
4910
5031
  selectMarker,
@@ -4913,7 +5034,7 @@ function WaveformCommentMarkers({
4913
5034
  const [trackWidth, setTrackWidth] = (0,external_react_.useState)(0);
4914
5035
  const canShowTrack = durationSec > 0 && commentMarkers.length > 0;
4915
5036
  const zoomLevel = viewport?.zoomLevel ?? constants/* WAVEFORM_ZOOM_MIN */.LW;
4916
- const isZoomed = hasMappedWindow(viewport) && viewport.zoomLevel > constants/* WAVEFORM_ZOOM_MIN */.LW;
5037
+ const alignsMarkersToVisibleWindow = hasMappedWindow(viewport) && (viewport.zoomLevel > constants/* WAVEFORM_ZOOM_MIN */.LW || isTape);
4917
5038
  (0,external_react_.useLayoutEffect)(() => {
4918
5039
  if (!canShowTrack) {
4919
5040
  setTrackWidth(0);
@@ -4949,7 +5070,10 @@ function WaveformCommentMarkers({
4949
5070
  }
4950
5071
  return /*#__PURE__*/external_react_["default"].createElement("div", {
4951
5072
  ref: containerRef,
4952
- className: `bp-WaveformCommentMarkers${isZoomed ? ' bp-WaveformCommentMarkers--zoomed' : ''}`,
5073
+ className: classnames_default()('bp-WaveformCommentMarkers', {
5074
+ 'bp-WaveformCommentMarkers--tape': isTape,
5075
+ 'bp-WaveformCommentMarkers--zoomed': alignsMarkersToVisibleWindow
5076
+ }),
4953
5077
  "data-testid": "bp-waveform-comment-markers"
4954
5078
  }, /*#__PURE__*/external_react_["default"].createElement("div", {
4955
5079
  ref: trackRef,
@@ -4958,7 +5082,10 @@ function WaveformCommentMarkers({
4958
5082
  const marker = cluster.markers[0];
4959
5083
  const isGroup = cluster.markers.length > 1;
4960
5084
  const isSelected = cluster.markers.some(entry => entry.id === selectedId);
4961
- const className = `bp-WaveformCommentMarkers-marker${isSelected ? ' bp-WaveformCommentMarkers-marker--selected' : ''}${isGroup ? ' bp-WaveformCommentMarkers-marker--group' : ''}`;
5085
+ const className = classnames_default()('bp-WaveformCommentMarkers-marker', {
5086
+ 'bp-WaveformCommentMarkers-marker--group': isGroup,
5087
+ 'bp-WaveformCommentMarkers-marker--selected': isSelected
5088
+ });
4962
5089
  const left = `${markerLeftPercent(marker.time, durationSec, viewport)}%`;
4963
5090
  if (isGroup) {
4964
5091
  return /*#__PURE__*/external_react_["default"].createElement("div", {
@@ -4996,6 +5123,8 @@ function WaveformCommentMarkers({
4996
5123
  }));
4997
5124
  })));
4998
5125
  }
5126
+ ;// external "react-dom"
5127
+
4999
5128
  ;// ./node_modules/wavesurfer.js/dist/wavesurfer.esm.js
5000
5129
  function t(t,e,i,n){return new(i||(i=Promise))((function(s,r){function o(t){try{l(n.next(t))}catch(t){r(t)}}function a(t){try{l(n.throw(t))}catch(t){r(t)}}function l(t){var e;t.done?s(t.value):(e=t.value,e instanceof i?e:new i((function(t){t(e)}))).then(o,a)}l((n=n.apply(t,e||[])).next())}))}"function"==typeof SuppressedError&&SuppressedError;class e{constructor(){this.listeners={}}on(t,e,i){if(this.listeners[t]||(this.listeners[t]=new Set),null==i?void 0:i.once){const i=(...n)=>{this.un(t,i),e(...n)};return this.listeners[t].add(i),()=>this.un(t,i)}return this.listeners[t].add(e),()=>this.un(t,e)}un(t,e){var i;null===(i=this.listeners[t])||void 0===i||i.delete(e)}once(t,e){return this.on(t,e,{once:!0})}unAll(){this.listeners={}}emit(t,...e){this.listeners[t]&&this.listeners[t].forEach((t=>t(...e)))}}const i={decode:function(e,i){return t(this,void 0,void 0,(function*(){const t=new AudioContext({sampleRate:i});try{return yield t.decodeAudioData(e)}finally{"closed"!==t.state&&(yield t.close().catch((()=>{})))}}))},createBuffer:function(t,e){if(!t||0===t.length)throw new Error("channelData must be a non-empty array");if(e<=0)throw new Error("duration must be greater than 0");if("number"==typeof t[0]&&(t=[t]),!t[0]||0===t[0].length)throw new Error("channelData must contain non-empty channel arrays");!function(t){const e=t[0];if(e.some((t=>t>1||t<-1))){const i=e.length;let n=0;for(let t=0;t<i;t++){const i=Math.abs(e[t]);i>n&&(n=i)}for(const e of t)for(let t=0;t<i;t++)e[t]/=n}}(t);const i=t.map((t=>t instanceof Float32Array?t:Float32Array.from(t)));return{duration:e,length:i[0].length,sampleRate:i[0].length/e,numberOfChannels:i.length,getChannelData:t=>{const e=i[t];if(!e)throw new Error(`Channel ${t} not found`);return e},copyFromChannel:AudioBuffer.prototype.copyFromChannel,copyToChannel:AudioBuffer.prototype.copyToChannel}}};function n(t,e){const i=e.xmlns?document.createElementNS(e.xmlns,t):document.createElement(t);for(const[t,s]of Object.entries(e))if("children"===t&&s)for(const[t,e]of Object.entries(s))e instanceof Node?i.appendChild(e):"string"==typeof e?i.appendChild(document.createTextNode(e)):i.appendChild(n(t,e));else"style"===t?Object.assign(i.style,s):"textContent"===t?i.textContent=s:i.setAttribute(t,s.toString());return i}function s(t,e,i){const s=n(t,e||{});return null==i||i.appendChild(s),s}function r(t){return t instanceof HTMLElement||"object"==typeof t&&null!==t&&t.nodeType===Node.ELEMENT_NODE&&"object"==typeof t.style}var o=Object.freeze({__proto__:null,createElement:s,default:s,isHTMLElement:r});const a={fetchBlob:function(e,i,n){return t(this,void 0,void 0,(function*(){var s;const r=yield fetch(e,n);if(r.status>=400)throw new Error(`Failed to fetch ${e}: ${r.status} (${r.statusText})`);return function(e,i,n){t(this,void 0,void 0,(function*(){var t;if(!e.body||!e.headers)return;const s=e.body.getReader(),r=Number(e.headers.get("Content-Length"))||0;let o=0;const a=()=>{s.cancel()};if(n){if(n.aborted)return void s.cancel();n.addEventListener("abort",a,{once:!0})}try{for(;;){const e=yield s.read();if(e.done)break;if(o+=(null===(t=e.value)||void 0===t?void 0:t.length)||0,r>0){const t=Math.round(o/r*100);i(t)}}}catch(t){if(t instanceof DOMException&&"AbortError"===t.name)return;console.warn("Progress tracking error:",t)}finally{n&&n.removeEventListener("abort",a)}}))}(r.clone(),i,null!==(s=null==n?void 0:n.signal)&&void 0!==s?s:void 0),r.blob()}))}};function l(t){let e=t;const i=new Set;return{get value(){return e},set(t){Object.is(e,t)||(e=t,i.forEach((t=>t(e))))},update(t){this.set(t(e))},subscribe:t=>(i.add(t),()=>i.delete(t))}}function h(t,e){const i=l(t());return e.forEach((e=>e.subscribe((()=>{const e=t();Object.is(i.value,e)||i.set(e)})))),{get value(){return i.value},subscribe:t=>i.subscribe(t)}}function c(t,e){let i;const n=()=>{i&&(i(),i=void 0),i=t()},s=e.map((t=>t.subscribe(n)));return n(),()=>{i&&(i(),i=void 0),s.forEach((t=>t()))}}class u extends e{get isPlayingSignal(){return this._isPlaying}get currentTimeSignal(){return this._currentTime}get durationSignal(){return this._duration}get volumeSignal(){return this._volume}get mutedSignal(){return this._muted}get playbackRateSignal(){return this._playbackRate}get seekingSignal(){return this._seeking}constructor(t){super(),this.isExternalMedia=!1,this._ownBlobUrl=null,this.reactiveMediaEventCleanups=[],t.media?(this.media=t.media,this.isExternalMedia=!0):this.media=document.createElement("audio"),this._isPlaying=l(!1),this._currentTime=l(0),this._duration=l(0),this._volume=l(this.media.volume),this._muted=l(this.media.muted),this._playbackRate=l(this.media.playbackRate||1),this._seeking=l(!1),this.setupReactiveMediaEvents(),t.mediaControls&&(this.media.controls=!0),t.autoplay&&(this.media.autoplay=!0),null!=t.playbackRate&&this.onMediaEvent("canplay",(()=>{null!=t.playbackRate&&(this.media.playbackRate=t.playbackRate)}),{once:!0})}setupReactiveMediaEvents(){this.reactiveMediaEventCleanups.push(this.onMediaEvent("play",(()=>{this._isPlaying.set(!0)}))),this.reactiveMediaEventCleanups.push(this.onMediaEvent("pause",(()=>{this._isPlaying.set(!1)}))),this.reactiveMediaEventCleanups.push(this.onMediaEvent("ended",(()=>{this._isPlaying.set(!1)}))),this.reactiveMediaEventCleanups.push(this.onMediaEvent("timeupdate",(()=>{this._currentTime.set(this.media.currentTime)}))),this.reactiveMediaEventCleanups.push(this.onMediaEvent("durationchange",(()=>{this._duration.set(this.media.duration||0)}))),this.reactiveMediaEventCleanups.push(this.onMediaEvent("loadedmetadata",(()=>{this._duration.set(this.media.duration||0)}))),this.reactiveMediaEventCleanups.push(this.onMediaEvent("seeking",(()=>{this._seeking.set(!0)}))),this.reactiveMediaEventCleanups.push(this.onMediaEvent("seeked",(()=>{this._seeking.set(!1)}))),this.reactiveMediaEventCleanups.push(this.onMediaEvent("volumechange",(()=>{this._volume.set(this.media.volume),this._muted.set(this.media.muted)}))),this.reactiveMediaEventCleanups.push(this.onMediaEvent("ratechange",(()=>{this._playbackRate.set(this.media.playbackRate)})))}onMediaEvent(t,e,i){return this.media.addEventListener(t,e,i),()=>this.media.removeEventListener(t,e,i)}getSrc(){return this.media.currentSrc||this.media.src||""}revokeSrc(){this._ownBlobUrl&&(URL.revokeObjectURL(this._ownBlobUrl),this._ownBlobUrl=null)}canPlayType(t){return""!==this.media.canPlayType(t)}setSrc(t,e){const i=this.getSrc();if(t&&i===t)return;this.revokeSrc();const n=e instanceof Blob&&(this.canPlayType(e.type)||!t)?URL.createObjectURL(e):t;if(n!==t&&(this._ownBlobUrl=n),i&&this.media.removeAttribute("src"),n||t)try{this.media.src=n}catch(e){this.media.src=t}}destroy(){this.reactiveMediaEventCleanups.forEach((t=>t())),this.reactiveMediaEventCleanups=[],this.revokeSrc(),this.unAll(),this.isExternalMedia||(this.media.pause(),this.media.removeAttribute("src"),this.media.load(),this.media.remove())}setMediaElement(t){this.reactiveMediaEventCleanups.forEach((t=>t())),this.reactiveMediaEventCleanups=[],this.media=t,this.setupReactiveMediaEvents()}play(){return t(this,void 0,void 0,(function*(){try{return yield this.media.play()}catch(t){if(t instanceof DOMException&&"AbortError"===t.name)return;throw t}}))}pause(){this.media.pause()}isPlaying(){return!this.media.paused&&!this.media.ended}setTime(t){this.media.currentTime=Math.max(0,Math.min(t,this.getDuration()))}getDuration(){return this.media.duration}getCurrentTime(){return this.media.currentTime}getVolume(){return this.media.volume}setVolume(t){this.media.volume=t}getMuted(){return this.media.muted}setMuted(t){this.media.muted=t}getPlaybackRate(){return this.media.playbackRate}isSeeking(){return this.media.seeking}setPlaybackRate(t,e){null!=e&&(this.media.preservesPitch=e),this.media.playbackRate=t}getMediaElement(){return this.media}setSinkId(t){return this.media.setSinkId(t)}}function d({maxTop:t,maxBottom:e,halfHeight:i,vScale:n,barMinHeight:s=0,barAlign:r}){let o=Math.round(t*i*n);let a=o+Math.round(e*i*n)||1;return a<s&&(a=s,r||(o=a/2)),{topHeight:o,totalHeight:a}}function p({barAlign:t,halfHeight:e,topHeight:i,totalHeight:n,canvasHeight:s}){return"top"===t?0:"bottom"===t?s-n:e-i}function m(t,e,i){const n=e-t.left,s=i-t.top;return[n/t.width,s/t.height]}function g(t){return Boolean(t.barWidth||t.barGap||t.barAlign)}function f(t,e){if(!g(e))return t;const i=e.barWidth||.5,n=i+(e.barGap||i/2);return 0===n?t:Math.floor(t/n)*n}function v({scrollLeft:t,totalWidth:e,numCanvases:i}){if(0===e)return[0];const n=t/e,s=Math.floor(n*i);return[s-1,s,s+1]}function b(t){const e=t._cleanup;"function"==typeof e&&e()}function y(t){const e=l({scrollLeft:t.scrollLeft,scrollWidth:t.scrollWidth,clientWidth:t.clientWidth}),i=h((()=>function(t){const{scrollLeft:e,scrollWidth:i,clientWidth:n}=t;if(0===i)return{startX:0,endX:1};const s=e/i,r=(e+n)/i;return{startX:Math.max(0,Math.min(1,s)),endX:Math.max(0,Math.min(1,r))}}(e.value)),[e]),n=h((()=>function(t){return{left:t.scrollLeft,right:t.scrollLeft+t.clientWidth}}(e.value)),[e]),s=()=>{e.set({scrollLeft:t.scrollLeft,scrollWidth:t.scrollWidth,clientWidth:t.clientWidth})};t.addEventListener("scroll",s,{passive:!0});return{scrollData:e,percentages:i,bounds:n,cleanup:()=>{t.removeEventListener("scroll",s),b(e)}}}class C extends e{constructor(t,e){super(),this.timeouts=[],this.isScrollable=!1,this.audioData=null,this.resizeObserver=null,this.lastContainerWidth=0,this.isDragging=!1,this.subscriptions=[],this.unsubscribeOnScroll=[],this.dragStream=null,this.scrollStream=null,this.containerInlinePadding=0,this.onClickWrapper=t=>{const e=this.wrapper.getBoundingClientRect(),[i,n]=m(e,t.clientX,t.clientY);this.emit("click",i,n)},this.onDblClickWrapper=t=>{const e=this.wrapper.getBoundingClientRect(),[i,n]=m(e,t.clientX,t.clientY);this.emit("dblclick",i,n)},this.subscriptions=[],this.options=t;const i=this.parentFromOptionsContainer(t.container);this.parent=i;const[n,s]=this.initHtml();i.appendChild(n),this.container=n,this.scrollContainer=s.querySelector(".scroll"),this.wrapper=s.querySelector(".wrapper"),this.canvasWrapper=s.querySelector(".canvases"),this.progressWrapper=s.querySelector(".progress"),this.cursor=s.querySelector(".cursor"),this.calculateInlinePadding(),e&&s.appendChild(e),this.initEvents()}parentFromOptionsContainer(t){let e;if("string"==typeof t?e=document.querySelector(t):r(t)&&(e=t),!e)throw new Error("Container not found");return e}initEvents(){this.wrapper.addEventListener("click",this.onClickWrapper),this.wrapper.addEventListener("dblclick",this.onDblClickWrapper),!0!==this.options.dragToSeek&&"object"!=typeof this.options.dragToSeek||this.initDrag(),this.scrollStream=y(this.scrollContainer);const t=c((()=>{const{startX:t,endX:e}=this.scrollStream.percentages.value,{left:i,right:n}=this.scrollStream.bounds.value;this.emit("scroll",t,e,i,n)}),[this.scrollStream.percentages,this.scrollStream.bounds]);if(this.subscriptions.push(t),"function"==typeof ResizeObserver){const t=this.createDelay(100);this.resizeObserver=new ResizeObserver((()=>{t().then((()=>this.onContainerResize())).catch((()=>{}))})),this.resizeObserver.observe(this.scrollContainer)}}onContainerResize(){const t=this.parent.clientWidth;this.calculateInlinePadding(),t===this.lastContainerWidth&&"auto"!==this.options.height||(this.lastContainerWidth=t,this.reRender(),this.emit("resize"))}initDrag(){if(this.dragStream)return;this.dragStream=function(t,e={}){const{threshold:i=3,mouseButton:n=0,touchDelay:s=100}=e,r=l(null),o=new Map,a=matchMedia("(pointer: coarse)").matches;let h=()=>{};const c=e=>{if(e.button!==n)return;if(o.has(e.pointerId))return;if(o.set(e.pointerId,e),o.size>1)return;const l=e.pointerId;let c=e.clientX,u=e.clientY,d=!1;const p=Date.now(),m=t.getBoundingClientRect(),{left:g,top:f}=m,v=t=>{if(t.pointerId!==l)return;if(t.defaultPrevented||o.size>1)return;if(a&&Date.now()-p<s)return;const e=t.clientX,n=t.clientY,h=e-c,m=n-u;(d||Math.abs(h)>i||Math.abs(m)>i)&&(t.preventDefault(),t.stopPropagation(),d||(r.set({type:"start",x:c-g,y:u-f}),d=!0),r.set({type:"move",x:e-g,y:n-f,deltaX:h,deltaY:m}),c=e,u=n)},b=t=>{if(o.delete(t.pointerId)){if(t.pointerId===l&&d){const e=t.clientX,i=t.clientY;r.set({type:"end",x:e-g,y:i-f})}0===o.size&&h()}},y=t=>{t.relatedTarget&&t.relatedTarget!==document.documentElement||b(t)},C=t=>{d&&(t.stopPropagation(),t.preventDefault())},S=t=>{t.defaultPrevented||o.size>1||d&&t.preventDefault()};document.addEventListener("pointermove",v),document.addEventListener("pointerup",b),document.addEventListener("pointerout",y),document.addEventListener("pointercancel",y),document.addEventListener("touchmove",S,{passive:!1}),document.addEventListener("click",C,{capture:!0}),h=()=>{document.removeEventListener("pointermove",v),document.removeEventListener("pointerup",b),document.removeEventListener("pointerout",y),document.removeEventListener("pointercancel",y),document.removeEventListener("touchmove",S),setTimeout((()=>{document.removeEventListener("click",C,{capture:!0})}),10)}};return t.addEventListener("pointerdown",c),{signal:r,cleanup:()=>{h(),t.removeEventListener("pointerdown",c),o.clear(),b(r)}}}(this.wrapper);const t=c((()=>{const t=this.dragStream.signal.value;if(!t)return;const e=this.wrapper.getBoundingClientRect().width,i=(n=t.x/e)<0?0:n>1?1:n;var n;"start"===t.type?(this.isDragging=!0,this.emit("dragstart",i)):"move"===t.type?this.emit("drag",i):"end"===t.type&&(this.isDragging=!1,this.emit("dragend",i))}),[this.dragStream.signal]);this.subscriptions.push(t)}calculateInlinePadding(){const{paddingLeft:t,paddingRight:e}=getComputedStyle(this.scrollContainer),i=parseFloat(t)+parseFloat(e);this.containerInlinePadding=Number.isNaN(i)?0:i}initHtml(){const t=document.createElement("div"),e=t.attachShadow({mode:"open"}),i=this.options.cspNonce&&"string"==typeof this.options.cspNonce?this.options.cspNonce.replace(/"/g,""):"";return e.innerHTML=`\n <style${i?` nonce="${i}"`:""}>\n :host {\n user-select: none;\n min-width: 1px;\n }\n :host audio {\n display: block;\n width: 100%;\n }\n :host .scroll {\n overflow-x: auto;\n overflow-y: hidden;\n width: 100%;\n position: relative;\n }\n :host .noScrollbar {\n scrollbar-color: transparent;\n scrollbar-width: none;\n }\n :host .noScrollbar::-webkit-scrollbar {\n display: none;\n -webkit-appearance: none;\n }\n :host .wrapper {\n position: relative;\n overflow: visible;\n z-index: 2;\n }\n :host .canvases {\n min-height: ${this.getHeight(this.options.height,this.options.splitChannels)}px;\n pointer-events: none;\n }\n :host .canvases > div {\n position: relative;\n }\n :host canvas {\n display: block;\n position: absolute;\n top: 0;\n image-rendering: pixelated;\n }\n :host .progress {\n pointer-events: none;\n position: absolute;\n z-index: 2;\n top: 0;\n left: 0;\n width: 0;\n height: 100%;\n overflow: hidden;\n }\n :host .progress > div {\n position: relative;\n }\n :host .cursor {\n pointer-events: none;\n position: absolute;\n z-index: 5;\n top: 0;\n left: 0;\n height: 100%;\n border-radius: 2px;\n }\n </style>\n\n <div class="scroll" part="scroll">\n <div class="wrapper" part="wrapper">\n <div class="canvases" part="canvases"></div>\n <div class="progress" part="progress"></div>\n <div class="cursor" part="cursor"></div>\n </div>\n </div>\n `,[t,e]}setOptions(t){var e;if(this.options.container!==t.container){const e=this.parentFromOptionsContainer(t.container);e.appendChild(this.container),this.parent=e}!0===t.dragToSeek||"object"==typeof this.options.dragToSeek?this.initDrag():(null===(e=this.dragStream)||void 0===e||e.cleanup(),this.dragStream=null),this.options=t,this.reRender()}getWrapper(){return this.wrapper}getWidth(){return this.scrollContainer.clientWidth-this.containerInlinePadding}getScroll(){return this.scrollContainer.scrollLeft}setScroll(t){this.scrollContainer.scrollLeft=t}setScrollPercentage(t){const{scrollWidth:e}=this.scrollContainer,i=e*t;this.setScroll(i)}destroy(){var t;this.wrapper.removeEventListener("click",this.onClickWrapper),this.wrapper.removeEventListener("dblclick",this.onDblClickWrapper),this.timeouts.forEach((t=>t())),this.timeouts=[],this.subscriptions.forEach((t=>t())),this.container.remove(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),null===(t=this.unsubscribeOnScroll)||void 0===t||t.forEach((t=>t())),this.unsubscribeOnScroll=[],this.dragStream&&(this.dragStream.cleanup(),this.dragStream=null),this.scrollStream&&(this.scrollStream.cleanup(),this.scrollStream=null)}createDelay(t=10){let e,i;const n=()=>{e&&(clearTimeout(e),e=void 0),i&&(i(),i=void 0)};return this.timeouts.push(n),()=>new Promise(((s,r)=>{n(),i=r,e=setTimeout((()=>{e=void 0,i=void 0,s()}),t)}))}getHeight(t,e){var i;const n=(null===(i=this.audioData)||void 0===i?void 0:i.numberOfChannels)||1;return function({optionsHeight:t,optionsSplitChannels:e,parentHeight:i,numberOfChannels:n,defaultHeight:s=128}){if(null==t)return s;const r=Number(t);if(!isNaN(r))return r;if("auto"===t){const t=i||s;return(null==e?void 0:e.every((t=>!t.overlay)))?t/n:t}return s}({optionsHeight:t,optionsSplitChannels:e,parentHeight:this.parent.clientHeight,numberOfChannels:n,defaultHeight:128})}convertColorValues(t,e){return function(t,e,i){if(!Array.isArray(t))return t||"";if(0===t.length)return"#999";if(t.length<2)return t[0]||"";const n=document.createElement("canvas"),s=n.getContext("2d");if(!s)return t[0]||"";const r=i||n.height*e,o=s.createLinearGradient(0,0,0,r),a=1/(t.length-1);return t.forEach(((t,e)=>{o.addColorStop(e*a,t)})),o}(t,this.getPixelRatio(),null==e?void 0:e.canvas.height)}getPixelRatio(){return t=window.devicePixelRatio,Math.max(1,t||1);// removed by dead control flow
5001
5130
  var t; }renderBarWaveform(t,e,i,n){const{width:s,height:r}=i.canvas,{halfHeight:o,barWidth:a,barRadius:l,barIndexScale:h,barSpacing:c,barMinHeight:u}=function({width:t,height:e,length:i,options:n,pixelRatio:s}){const r=e/2,o=n.barWidth?n.barWidth*s:1,a=n.barGap?n.barGap*s:n.barWidth?o/2:0,l=o+a||1;return{halfHeight:r,barWidth:o,barGap:a,barRadius:n.barRadius||0,barMinHeight:n.barMinHeight?n.barMinHeight*s:0,barIndexScale:i>0?t/l/i:0,barSpacing:l}}({width:s,height:r,length:(t[0]||[]).length,options:e,pixelRatio:this.getPixelRatio()}),m=function({channelData:t,barIndexScale:e,barSpacing:i,barWidth:n,halfHeight:s,vScale:r,canvasHeight:o,barAlign:a,barMinHeight:l}){const h=t[0]||[],c=t[1]||h,u=h.length,m=[];let g=0,f=0,v=0;for(let t=0;t<=u;t++){const u=Math.round(t*e);if(u>g){const{topHeight:t,totalHeight:e}=d({maxTop:f,maxBottom:v,halfHeight:s,vScale:r,barMinHeight:l,barAlign:a}),h=p({barAlign:a,halfHeight:s,topHeight:t,totalHeight:e,canvasHeight:o});m.push({x:g*i,y:h,width:n,height:e}),g=u,f=0,v=0}const b=Math.abs(h[t]||0),y=Math.abs(c[t]||0);b>f&&(f=b),y>v&&(v=y)}return m}({channelData:t,barIndexScale:h,barSpacing:c,barWidth:a,halfHeight:o,vScale:n,canvasHeight:r,barAlign:e.barAlign,barMinHeight:u});i.beginPath();for(const t of m)l&&"roundRect"in i?i.roundRect(t.x,t.y,t.width,t.height,l):i.rect(t.x,t.y,t.width,t.height);i.fill(),i.closePath()}renderLineWaveform(t,e,i,n){const{width:s,height:r}=i.canvas,o=function({channelData:t,width:e,height:i,vScale:n}){const s=i/2,r=t[0]||[];return[r,t[1]||r].map(((t,i)=>{const r=t.length,o=r?e/r:0,a=s,l=0===i?-1:1,h=[{x:0,y:a}];let c=0,u=0;for(let e=0;e<=r;e++){const i=Math.round(e*o);if(i>c){const t=a+(Math.round(u*s*n)||1)*l;h.push({x:c,y:t}),c=i,u=0}const r=Math.abs(t[e]||0);r>u&&(u=r)}return h.push({x:c,y:a}),h}))}({channelData:t,width:s,height:r,vScale:n});i.beginPath();for(const t of o)if(t.length){i.moveTo(t[0].x,t[0].y);for(let e=1;e<t.length;e++){const n=t[e];i.lineTo(n.x,n.y)}}i.fill(),i.closePath()}renderWaveform(t,e,i){if(i.fillStyle=this.convertColorValues(e.waveColor,i),e.renderFunction)return void e.renderFunction(t,i);const n=function({channelData:t,barHeight:e,normalize:i,maxPeak:n}){var s;const r=e||1;if(!i)return r;const o=t[0];if(!o||0===o.length)return r;let a=null!=n?n:0;if(!n)for(let t=0;t<o.length;t++){const e=null!==(s=o[t])&&void 0!==s?s:0,i=Math.abs(e);i>a&&(a=i)}return a?r/a:r}({channelData:t,barHeight:e.barHeight,normalize:e.normalize,maxPeak:e.maxPeak});g(e)?this.renderBarWaveform(t,e,i,n):this.renderLineWaveform(t,e,i,n)}renderSingleCanvas(t,e,i,n,s,r,o){const a=this.getPixelRatio(),l=document.createElement("canvas");l.width=Math.round(i*a),l.height=Math.round(n*a),l.style.width=`${i}px`,l.style.height=`${n}px`,l.style.left=`${Math.round(s)}px`,r.appendChild(l);const h=l.getContext("2d");if(e.renderFunction?(h.fillStyle=this.convertColorValues(e.waveColor,h),e.renderFunction(t,h)):this.renderWaveform(t,e,h),l.width>0&&l.height>0){const t=l.cloneNode(),i=t.getContext("2d");i.drawImage(l,0,0),i.globalCompositeOperation="source-in",i.fillStyle=this.convertColorValues(e.progressColor,i),i.fillRect(0,0,l.width,l.height),o.appendChild(t)}}renderMultiCanvas(t,e,i,n,s,r){const o=this.getPixelRatio(),{clientWidth:a}=this.scrollContainer,l=i/o,h=function({clientWidth:t,totalWidth:e,options:i}){return f(Math.min(8e3,t,e),i)}({clientWidth:a,totalWidth:l,options:e});let c={};if(0===h)return;const u=i=>{if(i<0||i>=d)return;if(c[i])return;c[i]=!0;const o=i*h;let a=Math.min(l-o,h);if(a=f(a,e),a<=0)return;const u=function({channelData:t,offset:e,clampedWidth:i,totalWidth:n}){return t.map((t=>{const s=Math.floor(e/n*t.length),r=Math.floor((e+i)/n*t.length);return t.slice(s,r)}))}({channelData:t,offset:o,clampedWidth:a,totalWidth:l});this.renderSingleCanvas(u,e,a,n,o,s,r)},d=Math.ceil(l/h);if(!this.isScrollable){for(let t=0;t<d;t++)u(t);return}if(v({scrollLeft:this.scrollContainer.scrollLeft,totalWidth:l,numCanvases:d}).forEach((t=>u(t))),d>1){const t=this.on("scroll",(()=>{const{scrollLeft:t}=this.scrollContainer;Object.keys(c).length>10&&(s.innerHTML="",r.innerHTML="",c={}),v({scrollLeft:t,totalWidth:l,numCanvases:d}).forEach((t=>u(t)))}));this.unsubscribeOnScroll.push(t)}}renderChannel(t,e,i,n){var{overlay:s}=e,r=function(t,e){var i={};for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&e.indexOf(n)<0&&(i[n]=t[n]);if(null!=t&&"function"==typeof Object.getOwnPropertySymbols){var s=0;for(n=Object.getOwnPropertySymbols(t);s<n.length;s++)e.indexOf(n[s])<0&&Object.prototype.propertyIsEnumerable.call(t,n[s])&&(i[n[s]]=t[n[s]])}return i}(e,["overlay"]);const o=document.createElement("div"),a=this.getHeight(r.height,r.splitChannels);o.style.height=`${a}px`,s&&n>0&&(o.style.marginTop=`-${a}px`),this.canvasWrapper.style.minHeight=`${a}px`,this.canvasWrapper.appendChild(o);const l=o.cloneNode();this.progressWrapper.appendChild(l),this.renderMultiCanvas(t,r,i,a,o,l)}render(e){return t(this,void 0,void 0,(function*(){var t;this.timeouts.forEach((t=>t())),this.timeouts=[],this.unsubscribeOnScroll.forEach((t=>t())),this.unsubscribeOnScroll=[],this.canvasWrapper.innerHTML="",this.progressWrapper.innerHTML="",null!=this.options.width&&(this.scrollContainer.style.width="number"==typeof this.options.width?`${this.options.width}px`:this.options.width);const i=this.getPixelRatio(),n=this.scrollContainer.clientWidth-this.containerInlinePadding,{scrollWidth:s,isScrollable:r,useParentWidth:o,width:a}=function({duration:t,minPxPerSec:e=0,parentWidth:i,fillParent:n,pixelRatio:s}){const r=Math.ceil(t*e),o=r>i,a=Boolean(n&&!o);return{scrollWidth:r,isScrollable:o,useParentWidth:a,width:(a?i:r)*s}}({duration:e.duration,minPxPerSec:this.options.minPxPerSec||0,parentWidth:n,fillParent:this.options.fillParent,pixelRatio:i});if(this.isScrollable=r,this.wrapper.style.width=o?"100%":`${s}px`,this.scrollContainer.style.overflowX=this.isScrollable?"auto":"hidden",this.scrollContainer.classList.toggle("noScrollbar",!!this.options.hideScrollbar),this.cursor.style.backgroundColor=`${this.options.cursorColor||this.options.progressColor}`,this.cursor.style.width=`${this.options.cursorWidth}px`,this.audioData=e,this.emit("render"),this.options.splitChannels)for(let i=0;i<e.numberOfChannels;i++){const n=Object.assign(Object.assign({},this.options),null===(t=this.options.splitChannels)||void 0===t?void 0:t[i]);this.renderChannel([e.getChannelData(i)],n,a,i)}else{const t=[e.getChannelData(0)];e.numberOfChannels>1&&t.push(e.getChannelData(1)),this.renderChannel(t,this.options,a,0)}Promise.resolve().then((()=>this.emit("rendered")))}))}reRender(){if(this.unsubscribeOnScroll.forEach((t=>t())),this.unsubscribeOnScroll=[],!this.audioData)return;const{scrollWidth:t}=this.scrollContainer,{right:e}=this.progressWrapper.getBoundingClientRect();if(this.render(this.audioData),!this.isScrollable&&this.scrollContainer.scrollLeft)this.scrollContainer.scrollLeft=0;else if(this.isScrollable&&t!==this.scrollContainer.scrollWidth){const{right:t}=this.progressWrapper.getBoundingClientRect(),i=function(t){const e=2*t;return(e<0?Math.floor(e):Math.ceil(e))/2}(t-e);this.scrollContainer.scrollLeft+=i}}zoom(t){this.options.minPxPerSec=t,this.reRender()}scrollIntoView(t,e=!1){var i;const{scrollLeft:n,scrollWidth:s,clientWidth:r}=this.scrollContainer,o=t*s,a=n,l=n+r,h=r/2;if(this.isDragging){const t=30;o+t>l?this.scrollContainer.scrollLeft+=t:o-t<a&&(this.scrollContainer.scrollLeft-=t)}else{(o<a||o>l)&&(this.scrollContainer.scrollLeft=o-(this.options.autoCenter?h:0));const t=o-n-h;if(e&&this.options.autoCenter&&t>0){const e=null===(i=this.audioData)||void 0===i?void 0:i.duration;if(void 0===e||e<=0)return void(this.scrollContainer.scrollLeft+=t);const n=s/e;this.scrollContainer.scrollLeft+=n<=600?Math.min(t,10):t}}}renderProgress(t,e){if(isNaN(t))return;const i=100*t;this.canvasWrapper.style.clipPath=`polygon(${i}% 0%, 100% 0%, 100% 100%, ${i}% 100%)`,this.progressWrapper.style.width=`${i}%`,this.cursor.style.left=`${i}%`,this.cursor.style.transform=this.options.cursorWidth?`translateX(-${t*this.options.cursorWidth}px)`:"",this.isScrollable&&this.options.autoScroll&&this.audioData&&this.audioData.duration>0&&this.scrollIntoView(t,e)}exportImage(e,i,n){return t(this,void 0,void 0,(function*(){const t=this.canvasWrapper.querySelectorAll("canvas");if(!t.length)throw new Error("No waveform data");if("dataURL"===n){const n=Array.from(t).map((t=>t.toDataURL(e,i)));return Promise.resolve(n)}return Promise.all(Array.from(t).map((t=>new Promise(((n,s)=>{t.toBlob((t=>{t?n(t):s(new Error("Could not export image"))}),e,i)})))))}))}}class S extends e{constructor(){super(...arguments),this.animationFrameId=null,this.isRunning=!1}start(){if(this.isRunning)return;this.isRunning=!0;const t=()=>{this.isRunning&&(this.emit("tick"),this.animationFrameId=requestAnimationFrame(t))};t()}stop(){this.isRunning=!1,null!==this.animationFrameId&&(cancelAnimationFrame(this.animationFrameId),this.animationFrameId=null)}destroy(){this.stop(),this.unAll()}}class E extends e{constructor(t){super(),this.bufferNode=null,this.playStartTime=0,this.playbackPosition=0,this._muted=!1,this._playbackRate=1,this._duration=void 0,this.buffer=null,this.currentSrc="",this.paused=!0,this.crossOrigin=null,this.seeking=!1,this.autoplay=!1,this.addEventListener=this.on,this.removeEventListener=this.un,this._destroyed=!1,function(){const t=globalThis.navigator;if(null==t?void 0:t.audioSession)try{t.audioSession.type="playback"}catch(t){console.warn("Setting navigator.audioSession.type failed:",t)}}(),this.audioContext=t||new AudioContext,this.gainNode=this.audioContext.createGain(),this.gainNode.connect(this.audioContext.destination)}load(){return t(this,void 0,void 0,(function*(){}))}remove(){this.destroy()}destroy(){if(!this._destroyed){if(this._destroyed=!0,this.currentSrc="",this.bufferNode){this.bufferNode.onended=null;try{this.bufferNode.stop()}catch(t){}this.bufferNode.disconnect(),this.bufferNode=null}this.gainNode.disconnect(),"function"==typeof this.audioContext.close&&Promise.resolve(this.audioContext.close.call(this.audioContext)).catch((()=>{})),this.buffer=null,this.unAll()}}get src(){return this.currentSrc}set src(t){if(this.currentSrc=t,this._duration=void 0,!t)return this.buffer=null,void this.emit("emptied");fetch(t).then((e=>{if(e.status>=400)throw new Error(`Failed to fetch ${t}: ${e.status} (${e.statusText})`);return e.arrayBuffer()})).then((e=>this.currentSrc!==t?null:this.audioContext.decodeAudioData(e))).then((e=>{this.currentSrc===t&&(this.buffer=e,this.emit("loadedmetadata"),this.emit("canplay"),this.autoplay&&this.play())})).catch((t=>{console.error("WebAudioPlayer load error:",t)}))}_play(){if(!this.paused)return;this.paused=!1,this.bufferNode&&(this.bufferNode.onended=null,this.bufferNode.disconnect()),this.bufferNode=this.audioContext.createBufferSource(),this.buffer&&(this.bufferNode.buffer=this.buffer),this.bufferNode.playbackRate.value=this._playbackRate,this.bufferNode.connect(this.gainNode);let t=this.playbackPosition;(t>=this.duration||t<0)&&(t=0,this.playbackPosition=0),this.bufferNode.start(this.audioContext.currentTime,t),this.playStartTime=this.audioContext.currentTime,this.bufferNode.onended=()=>{!this.paused&&this.duration-this.currentTime<.01&&(this.pause(),this.emit("ended"))}}_pause(){if(this.paused=!0,this.bufferNode){this.bufferNode.onended=null;try{this.bufferNode.stop()}catch(t){}}this.playbackPosition+=(this.audioContext.currentTime-this.playStartTime)*this._playbackRate}play(){return t(this,void 0,void 0,(function*(){this.paused&&(this._play(),this.emit("play"))}))}pause(){this.paused||(this._pause(),this.emit("pause"))}stopAt(t){const e=(t-this.currentTime)/this._playbackRate,i=this.bufferNode;null==i||i.stop(this.audioContext.currentTime+e),null==i||i.addEventListener("ended",(()=>{i===this.bufferNode&&(this.bufferNode=null,this.pause(),this.playbackPosition=Math.min(t,this.duration),this.emit("timeupdate"))}),{once:!0})}setSinkId(e){return t(this,void 0,void 0,(function*(){return this.audioContext.setSinkId(e)}))}get playbackRate(){return this._playbackRate}set playbackRate(t){const e=!this.paused;e&&this._pause(),this._playbackRate=t,e&&this._play(),this.bufferNode&&(this.bufferNode.playbackRate.value=t)}get currentTime(){return this.paused?this.playbackPosition:this.playbackPosition+(this.audioContext.currentTime-this.playStartTime)*this._playbackRate}set currentTime(t){const e=!this.paused;e&&this._pause(),this.playbackPosition=t,e&&this._play(),this.emit("seeking"),this.emit("timeupdate")}get duration(){var t,e;return null!==(t=this._duration)&&void 0!==t?t:(null===(e=this.buffer)||void 0===e?void 0:e.duration)||0}set duration(t){this._duration=t}get volume(){return this.gainNode.gain.value}set volume(t){this.gainNode.gain.value=t,this.emit("volumechange")}get muted(){return this._muted}set muted(t){this._muted!==t&&(this._muted=t,this._muted?this.gainNode.disconnect():this.gainNode.connect(this.audioContext.destination))}canPlayType(t){return/^(audio|video)\//.test(t)}getGainNode(){return this.gainNode}getChannelData(){const t=[];if(!this.buffer)return t;const e=this.buffer.numberOfChannels;for(let i=0;i<e;i++)t.push(this.buffer.getChannelData(i));return t}removeAttribute(t){switch(t){case"src":this.src="";break;case"playbackRate":this.playbackRate=0;break;case"currentTime":this.currentTime=0;break;case"duration":this.duration=0;break;case"volume":this.volume=0;break;case"muted":this.muted=!1}}}const P={waveColor:"#999",progressColor:"#555",cursorWidth:1,minPxPerSec:0,fillParent:!0,interact:!0,dragToSeek:!1,autoScroll:!0,autoCenter:!0,sampleRate:8e3};class w extends u{static create(t){return new w(t)}getState(){return this.wavesurferState}getRenderer(){return this.renderer}constructor(t){const e=t.media||("WebAudio"===t.backend?new E:void 0);super({media:e,mediaControls:t.mediaControls,autoplay:t.autoplay,playbackRate:t.audioRate}),this.plugins=[],this.decodedData=null,this.stopAtPosition=null,this.subscriptions=[],this.mediaSubscriptions=[],this.abortController=null,this._isDestroyed=!1,this._loadVersion=0,this.reactiveCleanups=[],this.options=Object.assign({},P,t);const{state:i,actions:n}=function(t){var e,i,n,s,r,o;const a=null!==(e=null==t?void 0:t.currentTime)&&void 0!==e?e:l(0),c=null!==(i=null==t?void 0:t.duration)&&void 0!==i?i:l(0),u=null!==(n=null==t?void 0:t.isPlaying)&&void 0!==n?n:l(!1),d=null!==(s=null==t?void 0:t.isSeeking)&&void 0!==s?s:l(!1),p=null!==(r=null==t?void 0:t.volume)&&void 0!==r?r:l(1),m=null!==(o=null==t?void 0:t.playbackRate)&&void 0!==o?o:l(1),g=l(null),f=l(null),v=l(""),b=l(0),y=l(0),C=h((()=>!u.value),[u]),S=h((()=>null!==g.value),[g]),E=h((()=>S.value&&c.value>0),[S,c]),P=h((()=>a.value),[a]),w=h((()=>c.value>0?a.value/c.value:0),[a,c]);return{state:{currentTime:a,duration:c,isPlaying:u,isPaused:C,isSeeking:d,volume:p,playbackRate:m,audioBuffer:g,peaks:f,url:v,zoom:b,scrollPosition:y,canPlay:S,isReady:E,progress:P,progressPercent:w},actions:{setCurrentTime:t=>{const e=Math.max(0,Math.min(c.value||1/0,t));a.set(e)},setDuration:t=>{c.set(Math.max(0,t))},setPlaying:t=>{u.set(t)},setSeeking:t=>{d.set(t)},setVolume:t=>{const e=Math.max(0,Math.min(1,t));p.set(e)},setPlaybackRate:t=>{const e=Math.max(.1,Math.min(16,t));m.set(e)},setAudioBuffer:t=>{g.set(t),t&&c.set(t.duration)},setPeaks:t=>{f.set(t)},setUrl:t=>{v.set(t)},setZoom:t=>{b.set(Math.max(0,t))},setScrollPosition:t=>{y.set(Math.max(0,t))}}}}({isPlaying:this.isPlayingSignal,currentTime:this.currentTimeSignal,duration:this.durationSignal,volume:this.volumeSignal,playbackRate:this.playbackRateSignal,isSeeking:this.seekingSignal});this.wavesurferState=i,this.wavesurferActions=n,this.timer=new S;const s=e?void 0:this.getMediaElement();this.renderer=new C(this.options,s),this.initPlayerEvents(),this.initRendererEvents(),this.initTimerEvents(),this.initReactiveState(),this.initPlugins();const r=this.options.url||this.getSrc()||"";Promise.resolve().then((()=>{this.emit("init");const{peaks:t,duration:e}=this.options;(r||t&&e)&&this.load(r,t,e).catch((()=>{}))}))}updateProgress(t=this.getCurrentTime()){return this.renderer.renderProgress(t/this.getDuration(),this.isPlaying()),t}initTimerEvents(){this.subscriptions.push(this.timer.on("tick",(()=>{if(!this.isSeeking()){const t=this.updateProgress();if(this.emit("timeupdate",t),this.emit("audioprocess",t),null!=this.stopAtPosition&&this.isPlaying()&&t>=this.stopAtPosition){const t=this.stopAtPosition;this.pause(),this.setTime(t)}}})))}initReactiveState(){this.reactiveCleanups.push(function(t,e){const i=[];i.push(c((()=>{const i=t.isPlaying.value;e.emit(i?"play":"pause")}),[t.isPlaying])),i.push(c((()=>{const i=t.currentTime.value;e.emit("timeupdate",i),t.isPlaying.value&&e.emit("audioprocess",i)}),[t.currentTime,t.isPlaying])),i.push(c((()=>{t.isSeeking.value&&e.emit("seeking",t.currentTime.value)}),[t.isSeeking,t.currentTime]));let n=!1;i.push(c((()=>{t.isReady.value&&!n&&(n=!0,e.emit("ready",t.duration.value))}),[t.isReady,t.duration])),i.push(c((()=>{null===t.audioBuffer.value&&(n=!1)}),[t.audioBuffer]));let s=!1;return i.push(c((()=>{const i=t.isPlaying.value,n=t.currentTime.value,r=t.duration.value,o=r>0&&n>=r;s&&!i&&o&&e.emit("finish"),s=i&&o}),[t.isPlaying,t.currentTime,t.duration])),i.push(c((()=>{const i=t.zoom.value;i>0&&e.emit("zoom",i)}),[t.zoom])),()=>{i.forEach((t=>t()))}}(this.wavesurferState,{emit:this.emit.bind(this)}))}initPlayerEvents(){this.isPlaying()&&(this.emit("play"),this.timer.start()),this.mediaSubscriptions.push(this.onMediaEvent("timeupdate",(()=>{const t=this.updateProgress();this.emit("timeupdate",t)})),this.onMediaEvent("play",(()=>{this.emit("play"),this.timer.start()})),this.onMediaEvent("pause",(()=>{this.emit("pause"),this.timer.stop(),this.stopAtPosition=null})),this.onMediaEvent("emptied",(()=>{this.timer.stop(),this.stopAtPosition=null})),this.onMediaEvent("ended",(()=>{this.emit("timeupdate",this.getDuration()),this.emit("finish"),this.stopAtPosition=null})),this.onMediaEvent("seeking",(()=>{this.emit("seeking",this.getCurrentTime())})),this.onMediaEvent("error",(()=>{var t;this.emit("error",null!==(t=this.getMediaElement().error)&&void 0!==t?t:new Error("Media error")),this.stopAtPosition=null})))}initRendererEvents(){this.subscriptions.push(this.renderer.on("click",((t,e)=>{this.options.interact&&(this.seekTo(t),this.emit("interaction",t*this.getDuration()),this.emit("click",t,e))})),this.renderer.on("dblclick",((t,e)=>{this.emit("dblclick",t,e)})),this.renderer.on("scroll",((t,e,i,n)=>{const s=this.getDuration();this.emit("scroll",t*s,e*s,i,n)})),this.renderer.on("render",(()=>{this.emit("redraw")})),this.renderer.on("rendered",(()=>{this.emit("redrawcomplete")})),this.renderer.on("dragstart",(t=>{this.emit("dragstart",t)})),this.renderer.on("dragend",(t=>{this.emit("dragend",t)})),this.renderer.on("resize",(()=>{this.emit("resize")})));{let t;const e=this.renderer.on("drag",(e=>{var i;if(!this.options.interact)return;this.renderer.renderProgress(e),clearTimeout(t);let n=0;const s=this.options.dragToSeek;this.isPlaying()?n=0:!0===s?n=200:s&&"object"==typeof s&&(n=null!==(i=s.debounceTime)&&void 0!==i?i:200),t=setTimeout((()=>{this.seekTo(e)}),n),this.emit("interaction",e*this.getDuration()),this.emit("drag",e)}));this.subscriptions.push((()=>{clearTimeout(t),e()}))}}initPlugins(){var t;(null===(t=this.options.plugins)||void 0===t?void 0:t.length)&&this.options.plugins.forEach((t=>{this.registerPlugin(t)}))}unsubscribePlayerEvents(){this.mediaSubscriptions.forEach((t=>t())),this.mediaSubscriptions=[]}setOptions(t){this.options=Object.assign({},this.options,t),t.duration&&!t.peaks&&(this.decodedData=i.createBuffer(this.exportPeaks(),t.duration)),t.peaks&&t.duration&&(this.decodedData=i.createBuffer(t.peaks,t.duration)),this.renderer.setOptions(this.options),t.audioRate&&this.setPlaybackRate(t.audioRate),null!=t.mediaControls&&(this.getMediaElement().controls=t.mediaControls)}registerPlugin(t){if(this.plugins.includes(t))return t;t._init(this),this.plugins.push(t);const e=t.once("destroy",(()=>{this.plugins=this.plugins.filter((e=>e!==t)),this.subscriptions=this.subscriptions.filter((t=>t!==e))}));return this.subscriptions.push(e),t}unregisterPlugin(t){this.plugins=this.plugins.filter((e=>e!==t)),t.destroy()}getWrapper(){return this.renderer.getWrapper()}getWidth(){return this.renderer.getWidth()}getScroll(){return this.renderer.getScroll()}setScroll(t){return this.renderer.setScroll(t)}setScrollTime(t){const e=t/this.getDuration();this.renderer.setScrollPercentage(e)}getActivePlugins(){return this.plugins}loadAudio(e,n,s,r){return t(this,void 0,void 0,(function*(){var t;const o=++this._loadVersion;if(this._isDestroyed=!1,this.emit("load",e),!this.options.media&&this.isPlaying()&&this.pause(),this.decodedData=null,this.stopAtPosition=null,null===(t=this.abortController)||void 0===t||t.abort(),this.abortController=null,!n&&!s){const t=this.options.fetchParams||{};window.AbortController&&!t.signal&&(this.abortController=new AbortController,t.signal=this.abortController.signal);const i=t=>this.emit("loading",t);if(n=yield a.fetchBlob(e,i,t),this._isDestroyed||o!==this._loadVersion)return;const s=this.options.blobMimeType;s&&(n=new Blob([n],{type:s}))}if(this._isDestroyed||o!==this._loadVersion)return;this.setSrc(e,n);const l=yield new Promise((t=>{const e=r||this.getDuration();e?t(e):this.mediaSubscriptions.push(this.onMediaEvent("loadedmetadata",(()=>t(this.getDuration())),{once:!0}))}));if(!this._isDestroyed&&o===this._loadVersion){if(!e&&!n){const t=this.getMediaElement();t instanceof E&&(t.duration=l)}if(s)this.decodedData=i.createBuffer(s,l||0);else if(n){const t=yield n.arrayBuffer();if(this._isDestroyed||o!==this._loadVersion)return;this.decodedData=yield i.decode(t,this.options.sampleRate)}this._isDestroyed||o!==this._loadVersion||(this.decodedData&&(this.emit("decode",this.getDuration()),this.renderer.render(this.decodedData)),this.emit("ready",this.getDuration()))}}))}load(e,i,n){return t(this,void 0,void 0,(function*(){try{return yield this.loadAudio(e,void 0,i,n)}catch(t){throw this.emit("error",t),t}}))}loadBlob(e,i,n){return t(this,void 0,void 0,(function*(){try{return yield this.loadAudio("",e,i,n)}catch(t){throw this.emit("error",t),t}}))}zoom(t){if(!this.decodedData)throw new Error("No audio loaded");this.renderer.zoom(t),this.emit("zoom",t)}getDecodedData(){return this.decodedData}exportPeaks({channels:t=2,maxLength:e=8e3,precision:i=1e4}={}){if(!this.decodedData)throw new Error("The audio has not been decoded yet");const n=Math.min(t,this.decodedData.numberOfChannels),s=[];for(let t=0;t<n;t++){const n=this.decodedData.getChannelData(t),r=[],o=n.length/e;for(let t=0;t<e;t++){const e=n.slice(Math.floor(t*o),Math.ceil((t+1)*o));let s=0;for(let t=0;t<e.length;t++){const i=e[t];Math.abs(i)>Math.abs(s)&&(s=i)}r.push(Math.round(s*i)/i)}s.push(r)}return s}getDuration(){let t=super.getDuration()||0;return 0!==t&&t!==1/0||!this.decodedData||(t=this.decodedData.duration),t}toggleInteraction(t){this.options.interact=t}setTime(t){this.stopAtPosition=null,super.setTime(t),this.updateProgress(t),this.emit("timeupdate",t)}seekTo(t){const e=this.getDuration()*t;this.setTime(e)}play(e,i){const n=Object.create(null,{play:{get:()=>super.play}});return t(this,void 0,void 0,(function*(){null!=e&&this.setTime(e);const t=yield n.play.call(this);return null!=i&&(this.media instanceof E?this.media.stopAt(i):this.stopAtPosition=i),t}))}playPause(){return t(this,void 0,void 0,(function*(){return this.isPlaying()?this.pause():this.play()}))}stop(){this.pause(),this.setTime(0)}skip(t){this.setTime(this.getCurrentTime()+t)}empty(){this.load("",[[0]],.001)}setMediaElement(t){this.unsubscribePlayerEvents(),super.setMediaElement(t),this.initPlayerEvents()}exportImage(){return t(this,arguments,void 0,(function*(t="image/png",e=1,i="dataURL"){return this.renderer.exportImage(t,e,i)}))}destroy(){var t;this._isDestroyed=!0,this.emit("destroy"),null===(t=this.abortController)||void 0===t||t.abort(),this.plugins.forEach((t=>t.destroy())),this.subscriptions.forEach((t=>t())),this.unsubscribePlayerEvents(),this.reactiveCleanups.forEach((t=>t())),this.reactiveCleanups=[],this.timer.destroy(),this.renderer.destroy(),super.destroy()}}w.BasePlugin=class extends e{constructor(t){super(),this.subscriptions=[],this.isDestroyed=!1,this.options=t}onInit(){}_init(t){this.isDestroyed&&(this.subscriptions=[],this.isDestroyed=!1),this.wavesurfer=t,this.onInit()}destroy(){this.emit("destroy"),this.subscriptions.forEach((t=>t())),this.subscriptions=[],this.isDestroyed=!0,this.wavesurfer=void 0}},w.dom=o;
@@ -5080,18 +5209,31 @@ function getBufferedProgress(bufferedRange, durationSec) {
5080
5209
  return clampTo0And1(bufferedRange.end(bufferedRange.length - 1) / durationSec);
5081
5210
  }
5082
5211
 
5212
+ /** Played bars: rest token on desktop, hover-played (brightest) on tape. Tape never uses hover or buffer fills. */
5213
+ function getPlayedWaveformColor(isTape = false) {
5214
+ return isTape ? WAVEFORM_COLOR_HOVER_PLAYED : WAVEFORM_COLOR_PLAYED;
5215
+ }
5216
+
5083
5217
  /**
5084
5218
  * progressColor paints left of the playhead (clipped by wavesurfer).
5085
5219
  * waveColor paints right of the playhead, including buffered vs not-yet-buffered.
5086
5220
  */
5087
5221
  function getWaveformFills({
5088
5222
  bufferProgress,
5089
- hoverProgress
5223
+ hoverProgress,
5224
+ isTape = false
5090
5225
  }) {
5091
5226
  const buffer = clampTo0And1(bufferProgress);
5227
+ const playedColor = getPlayedWaveformColor(isTape);
5228
+ if (isTape) {
5229
+ return {
5230
+ progressColor: playedColor,
5231
+ waveColor: WAVEFORM_COLOR_UNPLAYED
5232
+ };
5233
+ }
5092
5234
  if (hoverProgress == null) {
5093
5235
  return {
5094
- progressColor: WAVEFORM_COLOR_PLAYED,
5236
+ progressColor: playedColor,
5095
5237
  waveColor: gradientStopsFromColorRanges([{
5096
5238
  color: WAVEFORM_COLOR_UNPLAYED,
5097
5239
  end: buffer,
@@ -5268,6 +5410,7 @@ function prefersReducedMotion() {
5268
5410
  * every frame. WaveformView owns WaveSurfer, zoom, and the media rAF loop.
5269
5411
  */
5270
5412
  function usePlayheadCamera({
5413
+ cameraMode = 'desktop',
5271
5414
  mediaElRef,
5272
5415
  onViewportCommit,
5273
5416
  playheadRef,
@@ -5276,6 +5419,8 @@ function usePlayheadCamera({
5276
5419
  }) {
5277
5420
  const onViewportCommitRef = (0,external_react_.useRef)(onViewportCommit);
5278
5421
  onViewportCommitRef.current = onViewportCommit;
5422
+ const cameraModeRef = (0,external_react_.useRef)(cameraMode);
5423
+ cameraModeRef.current = cameraMode;
5279
5424
  const programmaticScrollRef = (0,external_react_.useRef)(false);
5280
5425
  const jumpAnimationRef = (0,external_react_.useRef)(0);
5281
5426
  const lastCameraScrollRef = (0,external_react_.useRef)(null);
@@ -5286,6 +5431,8 @@ function usePlayheadCamera({
5286
5431
  const scrollSettleTimerRef = (0,external_react_.useRef)(0);
5287
5432
  const applyRef = (0,external_react_.useRef)(null);
5288
5433
  const isFollowPinned = (0,external_react_.useCallback)(() => isFollowPinnedRef.current, []);
5434
+ const isUserPanning = (0,external_react_.useCallback)(() => userIsScrollingRef.current, []);
5435
+ const isTape = (0,external_react_.useCallback)(() => cameraModeRef.current === 'tape', []);
5289
5436
  const cancelJump = (0,external_react_.useCallback)(() => {
5290
5437
  if (!jumpAnimationRef.current) {
5291
5438
  return;
@@ -5314,15 +5461,22 @@ function usePlayheadCamera({
5314
5461
  const onSeek = (0,external_react_.useCallback)(timeSec => {
5315
5462
  mediaTimeRef.current = timeSec;
5316
5463
  cancelJump();
5317
- clearFollowPin();
5318
- }, [cancelJump, clearFollowPin]);
5464
+ if (!isTape()) {
5465
+ clearFollowPin();
5466
+ }
5467
+ }, [cancelJump, clearFollowPin, isTape]);
5319
5468
 
5320
5469
  /** Unpin and hold follow so zoom setScroll (slider center or pinch origin) is not stolen. */
5321
5470
  const onZoom = (0,external_react_.useCallback)(() => {
5322
5471
  cancelJump();
5472
+ if (isTape()) {
5473
+ isFollowPinnedRef.current = true;
5474
+ holdFollowUntilInsetRef.current = false;
5475
+ return;
5476
+ }
5323
5477
  isFollowPinnedRef.current = false;
5324
5478
  holdFollowUntilInsetRef.current = true;
5325
- }, [cancelJump]);
5479
+ }, [cancelJump, isTape]);
5326
5480
  const applyScrollLeft = (0,external_react_.useCallback)((scrollLeftPx, shouldCommitState) => {
5327
5481
  const wavesurfer = wavesurferRef.current;
5328
5482
  if (!wavesurfer || !wavesurfer.setScroll) {
@@ -5350,8 +5504,8 @@ function usePlayheadCamera({
5350
5504
  if (!playhead || !isPinned || !(viewport.widthPx > 0)) {
5351
5505
  return;
5352
5506
  }
5353
- playhead.style.left = getPinnedPlayheadLeft(viewport.widthPx);
5354
- }, []);
5507
+ playhead.style.left = isTape() ? getTapePinnedPlayheadLeft() : getPinnedPlayheadLeft(viewport.widthPx);
5508
+ }, [isTape]);
5355
5509
  const apply = (0,external_react_.useCallback)((timeSec, playJustStarted = false) => {
5356
5510
  mediaTimeRef.current = timeSec;
5357
5511
  const wavesurfer = wavesurferRef.current;
@@ -5359,7 +5513,7 @@ function usePlayheadCamera({
5359
5513
  if (!wavesurfer) {
5360
5514
  return;
5361
5515
  }
5362
- if (viewport.zoomLevel <= constants/* WAVEFORM_ZOOM_MIN */.LW) {
5516
+ if (!isTape() && viewport.zoomLevel <= constants/* WAVEFORM_ZOOM_MIN */.LW) {
5363
5517
  clearFollowPin();
5364
5518
  return;
5365
5519
  }
@@ -5371,12 +5525,13 @@ function usePlayheadCamera({
5371
5525
  }
5372
5526
  const liveViewport = getViewportAtScroll(viewport, getScrollLeft(wavesurfer, viewport.scrollLeftPx));
5373
5527
  const action = getPlayheadCameraAction({
5528
+ cameraMode: cameraModeRef.current,
5374
5529
  isPlaying: true,
5375
5530
  playJustStarted,
5376
5531
  timeSec,
5377
5532
  viewport: liveViewport
5378
5533
  });
5379
- if (holdFollowUntilInsetRef.current && !playJustStarted) {
5534
+ if (!isTape() && holdFollowUntilInsetRef.current && !playJustStarted) {
5380
5535
  if (action.type !== 'none') {
5381
5536
  return;
5382
5537
  }
@@ -5408,6 +5563,7 @@ function usePlayheadCamera({
5408
5563
  const tick = now => {
5409
5564
  const t = Math.min(1, (now - start) / constants/* WAVEFORM_PLAYHEAD_JUMP_MS */.Bq);
5410
5565
  const liveAction = getPlayheadCameraAction({
5566
+ cameraMode: cameraModeRef.current,
5411
5567
  isPlaying: true,
5412
5568
  playJustStarted: true,
5413
5569
  timeSec: mediaTimeRef.current,
@@ -5424,7 +5580,7 @@ function usePlayheadCamera({
5424
5580
  apply(mediaTimeRef.current, false);
5425
5581
  };
5426
5582
  jumpAnimationRef.current = window.requestAnimationFrame(tick);
5427
- }, [applyScrollLeft, cancelJump, clearFollowPin, pinFollowPlayhead, playheadRef, viewportRef, wavesurferRef]);
5583
+ }, [applyScrollLeft, cancelJump, clearFollowPin, isTape, pinFollowPlayhead, playheadRef, viewportRef, wavesurferRef]);
5428
5584
  applyRef.current = apply;
5429
5585
 
5430
5586
  /**
@@ -5447,9 +5603,12 @@ function usePlayheadCamera({
5447
5603
  }
5448
5604
  releaseUserPanHold();
5449
5605
  cancelJump();
5450
- isFollowPinnedRef.current = false;
5606
+ if (!isTape()) {
5607
+ isFollowPinnedRef.current = false;
5608
+ }
5451
5609
  const liveViewport = getViewportAtScroll(viewportRef.current, getScrollLeft(wavesurfer, viewportRef.current.scrollLeftPx));
5452
5610
  const action = getSeekCameraAction({
5611
+ cameraMode: cameraModeRef.current,
5453
5612
  timeSec,
5454
5613
  viewport: liveViewport
5455
5614
  });
@@ -5469,7 +5628,7 @@ function usePlayheadCamera({
5469
5628
  applyScrollLeft(from + (to - from) * (1 - (1 - t) * (1 - t)), false);
5470
5629
  const playhead = playheadRef.current;
5471
5630
  if (playhead) {
5472
- playhead.style.left = timeLeftPercent(mediaTimeRef.current, viewportRef.current.durationSec, viewportRef.current);
5631
+ playhead.style.left = isTape() ? getTapePinnedPlayheadLeft() : timeLeftPercent(mediaTimeRef.current, viewportRef.current.durationSec, viewportRef.current);
5473
5632
  }
5474
5633
  if (t < 1) {
5475
5634
  jumpAnimationRef.current = window.requestAnimationFrame(tick);
@@ -5479,7 +5638,7 @@ function usePlayheadCamera({
5479
5638
  applyScrollLeft(to, true);
5480
5639
  };
5481
5640
  jumpAnimationRef.current = window.requestAnimationFrame(tick);
5482
- }, [applyScrollLeft, cancelJump, playheadRef, releaseUserPanHold, viewportRef, wavesurferRef]);
5641
+ }, [applyScrollLeft, cancelJump, isTape, playheadRef, releaseUserPanHold, viewportRef, wavesurferRef]);
5483
5642
  const handleScroll = (0,external_react_.useCallback)(onUserPan => {
5484
5643
  const wavesurfer = wavesurferRef.current;
5485
5644
  if (!wavesurfer) {
@@ -5491,8 +5650,28 @@ function usePlayheadCamera({
5491
5650
  return;
5492
5651
  }
5493
5652
  viewportRef.current = getViewportAtScroll(viewportRef.current, scrollLeftPx);
5494
- const timeSec = readMediaTime();
5495
5653
  const playhead = playheadRef.current;
5654
+ if (isTape()) {
5655
+ if (playhead) {
5656
+ playhead.style.left = getTapePinnedPlayheadLeft();
5657
+ }
5658
+ const pinTimeSec = Math.min(viewportRef.current.durationSec, Math.max(0, timeFromPositionPx(viewportRef.current.widthPx / 2, viewportRef.current)));
5659
+ userIsScrollingRef.current = true;
5660
+ window.clearTimeout(scrollSettleTimerRef.current);
5661
+ scrollSettleTimerRef.current = window.setTimeout(() => {
5662
+ userIsScrollingRef.current = false;
5663
+ scrollSettleTimerRef.current = 0;
5664
+ if (!mediaElRef.current || mediaElRef.current.paused) {
5665
+ return;
5666
+ }
5667
+ applyRef.current?.(readMediaTime(), false);
5668
+ }, constants/* WAVEFORM_FOLLOW_SCROLL_SETTLE_MS */.Zs);
5669
+ cancelJump();
5670
+ programmaticScrollRef.current = false;
5671
+ onUserPan(pinTimeSec);
5672
+ return;
5673
+ }
5674
+ const timeSec = readMediaTime();
5496
5675
  if (playhead) {
5497
5676
  playhead.style.left = timeLeftPercent(timeSec, viewportRef.current.durationSec, viewportRef.current);
5498
5677
  }
@@ -5511,7 +5690,7 @@ function usePlayheadCamera({
5511
5690
  isFollowPinnedRef.current = false;
5512
5691
  programmaticScrollRef.current = false;
5513
5692
  onUserPan(timeSec);
5514
- }, [cancelJump, isUserPan, mediaElRef, playheadRef, readMediaTime, viewportRef, wavesurferRef]);
5693
+ }, [cancelJump, isTape, isUserPan, mediaElRef, playheadRef, readMediaTime, viewportRef, wavesurferRef]);
5515
5694
  (0,external_react_.useEffect)(() => () => {
5516
5695
  releaseUserPanHold();
5517
5696
  cancelJump();
@@ -5523,6 +5702,7 @@ function usePlayheadCamera({
5523
5702
  clearFollowPin,
5524
5703
  handleScroll,
5525
5704
  isFollowPinned,
5705
+ isUserPanning,
5526
5706
  onSeek,
5527
5707
  onZoom,
5528
5708
  releaseUserPanHold,
@@ -5548,6 +5728,8 @@ function WaveformView_toPrimitive(t, r) { if ("object" != typeof t || !t) return
5548
5728
 
5549
5729
 
5550
5730
 
5731
+
5732
+
5551
5733
  /** Pointer X in the view and the media time under it; zoom keeps this point fixed. */
5552
5734
 
5553
5735
  /** CSS width × devicePixelRatio, for canvas gradient fills. */
@@ -5577,6 +5759,25 @@ function disableScrollOverscroll(container) {
5577
5759
  }
5578
5760
  }
5579
5761
 
5762
+ /** Half-view margins so t=0 and duration can sit under the center pin. */
5763
+ function applyWaveformGutters(wavesurfer, gutterPx) {
5764
+ const wrapper = wavesurfer.getWrapper ? wavesurfer.getWrapper() : null;
5765
+ if (!wrapper || !wrapper.style) {
5766
+ return;
5767
+ }
5768
+ const pad = gutterPx > 0 ? `${gutterPx}px` : '';
5769
+ wrapper.style.marginLeft = pad;
5770
+ wrapper.style.marginRight = pad;
5771
+ // WaveSurfer sets overflow-x:hidden when duration * minPxPerSec <= view width
5772
+ // (tape 1x). Gutters live in these margins, so the scroller must stay pan-able.
5773
+ const scroll = wrapper.parentElement;
5774
+ if (scroll && scroll.style) {
5775
+ const scrollStyle = scroll.style;
5776
+ scrollStyle.overflowX = gutterPx > 0 ? 'auto' : '';
5777
+ scrollStyle.scrollbarWidth = gutterPx > 0 ? 'none' : '';
5778
+ }
5779
+ }
5780
+
5580
5781
  /** Tint WaveSurfer's zoomed tiles with played/unplayed/hover/buffer colors. */
5581
5782
  function tintZoomedWaveform(wavesurfer, fills, replaceSnapshot = false) {
5582
5783
  const wrapper = wavesurfer.getWrapper ? wavesurfer.getWrapper() : null;
@@ -5590,6 +5791,15 @@ function tintZoomedWaveform(wavesurfer, fills, replaceSnapshot = false) {
5590
5791
  totalWidthCss: wrapper.clientWidth
5591
5792
  });
5592
5793
  }
5794
+
5795
+ /** Touch (and the compatibility mouse events Chrome sends after a tap) must not drive hover fills. */
5796
+ function isTouchHoverInput(event) {
5797
+ if (event.pointerType === 'touch') {
5798
+ return true;
5799
+ }
5800
+ const native = event.nativeEvent;
5801
+ return !!native.sourceCapabilities?.firesTouchEvents;
5802
+ }
5593
5803
  function touchDistance(touches) {
5594
5804
  if (touches.length < 2) {
5595
5805
  return 0;
@@ -5620,11 +5830,14 @@ function zoomOriginAtPointer(pointerX, wavesurfer, durationSec, fallbackWidth) {
5620
5830
  */
5621
5831
  function WaveformView({
5622
5832
  bufferedRange,
5833
+ cameraMode = 'desktop',
5623
5834
  currentTime = 0,
5624
5835
  durationSec,
5625
5836
  height = constants/* WAVEFORM_HEIGHT */.oN,
5626
5837
  interactive = true,
5838
+ isPlaying = false,
5627
5839
  mediaEl,
5840
+ onPlayPause,
5628
5841
  onSeek,
5629
5842
  onViewportChange,
5630
5843
  onZoomChange,
@@ -5632,42 +5845,59 @@ function WaveformView({
5632
5845
  zoomLevel: zoomLevelProp
5633
5846
  }) {
5634
5847
  // DOM / WaveSurfer
5635
- const containerRef = (0,external_react_.useRef)(null);
5636
- const trackRef = (0,external_react_.useRef)(null);
5637
- const playheadRef = (0,external_react_.useRef)(null);
5638
- const playheadAnimationRef = (0,external_react_.useRef)(0);
5639
- const wavesurferRef = (0,external_react_.useRef)(null);
5848
+ const containerRef = (0,external_react_.useRef)(null); // WaveSurfer canvas host
5849
+ const playheadAnimationRef = (0,external_react_.useRef)(0); // animation frame id while the playhead follows playback
5850
+ const playheadRef = (0,external_react_.useRef)(null); // playhead element the camera positions
5851
+ const trackRef = (0,external_react_.useRef)(null); // hover / tap target around the canvas
5852
+ const wavesurferRef = (0,external_react_.useRef)(null); // WaveSurfer instance
5640
5853
 
5641
5854
  // Latest props for WaveSurfer + media listeners that must not re-subscribe each render.
5642
- const onSeekRef = (0,external_react_.useRef)(onSeek);
5643
- const interactiveRef = (0,external_react_.useRef)(interactive);
5644
- const hasZoomHandlersRef = (0,external_react_.useRef)(false);
5645
- const currentTimeRef = (0,external_react_.useRef)(currentTime);
5646
- const peaksRef = (0,external_react_.useRef)(peaks);
5647
- const durationSecRef = (0,external_react_.useRef)(durationSec);
5648
- const mediaElRef = (0,external_react_.useRef)(mediaEl);
5649
- const onViewportChangeRef = (0,external_react_.useRef)(onViewportChange);
5650
- const displayedPeaksRef = (0,external_react_.useRef)(null);
5651
- const peakTransitionAnimationRef = (0,external_react_.useRef)(0);
5855
+ const cameraModeRef = (0,external_react_.useRef)(cameraMode); // latest tape/desktop; WaveSurfer create() deps stay empty
5856
+ const currentTimeRef = (0,external_react_.useRef)(currentTime); // latest media time; zoom recenter reads this
5857
+ const durationSecRef = (0,external_react_.useRef)(durationSec); // latest duration; click-to-seek reads this
5858
+ const hasZoomHandlersRef = (0,external_react_.useRef)(false); // parent can zoom; wheel/pinch check this
5859
+ const interactiveRef = (0,external_react_.useRef)(interactive); // latest interactive; WaveSurfer listeners read this
5860
+ const isPlayingRef = (0,external_react_.useRef)(isPlaying); // latest isPlaying; tape tap toggles from this
5861
+ const mediaElRef = (0,external_react_.useRef)(mediaEl); // latest <audio>/<video>; camera + playhead tick read this
5862
+ const onPlayPauseRef = (0,external_react_.useRef)(onPlayPause); // latest play/pause; tape tap must not re-bind
5863
+ const onSeekRef = (0,external_react_.useRef)(onSeek); // latest onSeek; click/scroll handlers must not re-bind
5864
+ const onViewportChangeRef = (0,external_react_.useRef)(onViewportChange); // latest viewport callback; camera commits here
5865
+ const peaksRef = (0,external_react_.useRef)(peaks); // latest peaks; WaveSurfer create() + morph read this
5866
+
5867
+ const displayedPeaksRef = (0,external_react_.useRef)(null); // peaks currently drawn (for morph)
5868
+ const peakTransitionAnimationRef = (0,external_react_.useRef)(0); // animation frame id while peaks morph in
5652
5869
  // Zoom / pinch / tile tint (read from pointer + WaveSurfer handlers)
5653
- const zoomOriginRef = (0,external_react_.useRef)(null);
5654
- const pinchStartRef = (0,external_react_.useRef)(null);
5655
- const pointerZoomRef = (0,external_react_.useRef)(false);
5656
- const pointerZoomClearTimerRef = (0,external_react_.useRef)(0);
5657
- const applyZoomWindowRef = (0,external_react_.useRef)(null);
5658
- // WaveSurfer `setOptions` fires zoom/scroll before we apply the intended scroll.
5659
- const suppressViewportSyncRef = (0,external_react_.useRef)(false);
5660
- const bufferProgressRef = (0,external_react_.useRef)(0);
5661
- const hoverProgressRef = (0,external_react_.useRef)(null);
5662
- onSeekRef.current = onSeek;
5663
- interactiveRef.current = interactive;
5870
+ const applyZoomWindowRef = (0,external_react_.useRef)(null); // latest applyZoomWindow; resize observer calls this
5871
+ const bufferProgressRef = (0,external_react_.useRef)(0); // latest buffer fill; zoomed tile tint reads this
5872
+ const hideScrubTimeChipTimerRef = (0,external_react_.useRef)(0); // hides the tape scrub time chip after scroll settles
5873
+ const hoverProgressRef = (0,external_react_.useRef)(null); // latest hover fill; zoomed tile tint reads this
5874
+ const lastObservedWidthRef = (0,external_react_.useRef)(0); // last ResizeObserver width; skip no-op resizes
5875
+ const liveHeightRef = (0,external_react_.useRef)(height); // canvas height last applied to WaveSurfer
5876
+ const pinchStartRef = (0,external_react_.useRef)(null); // two-finger distance and zoom at pinch start
5877
+ const pointerZoomClearTimerRef = (0,external_react_.useRef)(0); // clears pointerZoomRef after WAVEFORM_ZOOM_DISMISS_MS
5878
+ const pointerZoomRef = (0,external_react_.useRef)(false); // pinch/wheel in progress; skip play/pause and tape recenter
5879
+ const queuedTapeSeekTimeSecRef = (0,external_react_.useRef)(null); // seek time waiting for the next animation frame
5880
+ const suppressNextTapPlayPauseRef = (0,external_react_.useRef)(false); // swallow play/pause after a swipe or pinch
5881
+ const suppressNextTapPlayPauseTimerRef = (0,external_react_.useRef)(0); // clears suppressNextTapPlayPauseRef
5882
+ const suppressViewportSyncRef = (0,external_react_.useRef)(false); // ignore WaveSurfer zoom/scroll while we setOptions
5883
+ const tapeSeekAnimationRef = (0,external_react_.useRef)(0); // coalesces swipe seeks to one seek per animation frame
5884
+ const toggleTapePlaybackRef = (0,external_react_.useRef)(null); // latest tap play/pause; click/pointerup call this
5885
+ const zoomOriginRef = (0,external_react_.useRef)(null); // pointer+time so zoom keeps that point fixed
5886
+
5887
+ cameraModeRef.current = cameraMode;
5664
5888
  currentTimeRef.current = currentTime;
5665
- peaksRef.current = peaks;
5666
5889
  durationSecRef.current = durationSec;
5890
+ interactiveRef.current = interactive;
5891
+ isPlayingRef.current = isPlaying;
5667
5892
  mediaElRef.current = mediaEl;
5893
+ onPlayPauseRef.current = onPlayPause;
5894
+ onSeekRef.current = onSeek;
5668
5895
  onViewportChangeRef.current = onViewportChange;
5896
+ peaksRef.current = peaks;
5669
5897
  const [internalZoom, setInternalZoom] = (0,external_react_.useState)(constants/* WAVEFORM_ZOOM_MIN */.LW);
5670
5898
  const [hoverProgress, setHoverProgress] = (0,external_react_.useState)(null);
5899
+ const [scrubPreviewTimeSec, setScrubPreviewTimeSec] = (0,external_react_.useState)(null);
5900
+ const [overlayPortalHost, setOverlayPortalHost] = (0,external_react_.useState)(null);
5671
5901
  const [canvasWidthPx, setCanvasWidthPx] = (0,external_react_.useState)(0);
5672
5902
  const [scrollLeft, setScrollLeft] = (0,external_react_.useState)(0);
5673
5903
  const isControlled = typeof zoomLevelProp === 'number';
@@ -5680,19 +5910,22 @@ function WaveformView({
5680
5910
  const zoomLevel = clampWaveformZoom(isControlled ? zoomLevelProp : internalZoom, maxZoom);
5681
5911
  const zoomRef = (0,external_react_.useRef)(zoomLevel); // latest zoom for WaveSurfer redraw/zoom handlers
5682
5912
  zoomRef.current = zoomLevel;
5683
- const prevZoomRef = (0,external_react_.useRef)(null);
5913
+ const prevZoomRef = (0,external_react_.useRef)(null); // last applied zoom; tape recenters only when this changes
5914
+ const isTape = cameraMode === 'tape';
5684
5915
  const isZoomed = zoomLevel > constants/* WAVEFORM_ZOOM_MIN */.LW;
5916
+ const isScrollableWindow = isZoomed || isTape;
5685
5917
  const bufferProgress = getBufferedProgress(bufferedRange, durationSec);
5686
5918
  bufferProgressRef.current = bufferProgress;
5687
5919
  hoverProgressRef.current = hoverProgress;
5688
5920
  const viewport = (0,external_react_.useMemo)(() => createWaveformViewport({
5689
5921
  durationSec,
5922
+ gutterPx: isTape ? getTapeGutterPx(canvasWidthPx) : 0,
5690
5923
  heightPx: height,
5691
5924
  maxZoom,
5692
5925
  scrollLeftPx: scrollLeft,
5693
5926
  widthPx: canvasWidthPx,
5694
5927
  zoomLevel
5695
- }), [canvasWidthPx, durationSec, height, maxZoom, scrollLeft, zoomLevel]);
5928
+ }), [canvasWidthPx, durationSec, height, isTape, maxZoom, scrollLeft, zoomLevel]);
5696
5929
  const viewportRef = (0,external_react_.useRef)(viewport); // live scroll window; prefer this over render-state while the camera is moving
5697
5930
  const onViewportCommit = (0,external_react_.useCallback)((scrollLeftPx, nextViewport, commitReactState = true) => {
5698
5931
  onViewportChangeRef.current?.(nextViewport);
@@ -5707,20 +5940,45 @@ function WaveformView({
5707
5940
  clearFollowPin,
5708
5941
  handleScroll: handleCameraScroll,
5709
5942
  isFollowPinned,
5943
+ isUserPanning,
5710
5944
  onSeek: onPlayheadSeek,
5711
5945
  onZoom,
5712
5946
  releaseUserPanHold,
5713
5947
  seekTo
5714
5948
  } = usePlayheadCamera({
5949
+ cameraMode,
5715
5950
  mediaElRef,
5716
5951
  onViewportCommit,
5717
5952
  playheadRef,
5718
5953
  viewportRef,
5719
5954
  wavesurferRef
5720
5955
  });
5956
+ const toggleTapePlayback = (0,external_react_.useCallback)(() => {
5957
+ if (!interactiveRef.current) {
5958
+ return;
5959
+ }
5960
+ if (isUserPanning() || pointerZoomRef.current) {
5961
+ return;
5962
+ }
5963
+ if (suppressNextTapPlayPauseRef.current) {
5964
+ suppressNextTapPlayPauseRef.current = false;
5965
+ window.clearTimeout(suppressNextTapPlayPauseTimerRef.current);
5966
+ suppressNextTapPlayPauseTimerRef.current = 0;
5967
+ return;
5968
+ }
5969
+ onPlayPauseRef.current?.(!isPlayingRef.current);
5970
+ suppressNextTapPlayPauseRef.current = true;
5971
+ window.clearTimeout(suppressNextTapPlayPauseTimerRef.current);
5972
+ suppressNextTapPlayPauseTimerRef.current = window.setTimeout(() => {
5973
+ suppressNextTapPlayPauseRef.current = false;
5974
+ suppressNextTapPlayPauseTimerRef.current = 0;
5975
+ }, constants/* WAVEFORM_TAPE_CLICK_SUPPRESS_MS */.fI);
5976
+ }, [isUserPanning]);
5977
+ toggleTapePlaybackRef.current = toggleTapePlayback;
5721
5978
  (0,external_react_.useLayoutEffect)(() => {
5722
5979
  viewportRef.current = createWaveformViewport({
5723
5980
  durationSec: viewport.durationSec,
5981
+ gutterPx: viewport.gutterPx,
5724
5982
  heightPx: viewport.heightPx,
5725
5983
  maxZoom: viewport.maxZoom,
5726
5984
  scrollLeftPx: viewportRef.current.scrollLeftPx,
@@ -5743,6 +6001,41 @@ function WaveformView({
5743
6001
  pointerZoomClearTimerRef.current = 0;
5744
6002
  }, constants/* WAVEFORM_ZOOM_DISMISS_MS */.m6);
5745
6003
  }, []);
6004
+
6005
+ /**
6006
+ * Seek the time under the pin (one seek per frame). Swallow the delayed iOS click;
6007
+ * show the scrub chip until the swipe settles.
6008
+ */
6009
+ const handleTapeSwipe = (0,external_react_.useCallback)(timeSec => {
6010
+ // Swallow the click WaveSurfer/iOS fires after a swipe or pinch.
6011
+ suppressNextTapPlayPauseRef.current = true;
6012
+ window.clearTimeout(suppressNextTapPlayPauseTimerRef.current);
6013
+ suppressNextTapPlayPauseTimerRef.current = window.setTimeout(() => {
6014
+ suppressNextTapPlayPauseRef.current = false;
6015
+ suppressNextTapPlayPauseTimerRef.current = 0;
6016
+ }, constants/* WAVEFORM_TAPE_CLICK_SUPPRESS_MS */.fI);
6017
+ queuedTapeSeekTimeSecRef.current = timeSec;
6018
+ if (!tapeSeekAnimationRef.current) {
6019
+ // One seek per frame; later scrolls just update the queued time.
6020
+ tapeSeekAnimationRef.current = window.requestAnimationFrame(() => {
6021
+ tapeSeekAnimationRef.current = 0;
6022
+ const next = queuedTapeSeekTimeSecRef.current;
6023
+ queuedTapeSeekTimeSecRef.current = null;
6024
+ if (next != null) {
6025
+ onSeekRef.current?.(next);
6026
+ }
6027
+ });
6028
+ }
6029
+ if (interactiveRef.current) {
6030
+ // Show the time under the pin; hide once scrolling has settled.
6031
+ setScrubPreviewTimeSec(timeSec);
6032
+ window.clearTimeout(hideScrubTimeChipTimerRef.current);
6033
+ hideScrubTimeChipTimerRef.current = window.setTimeout(() => {
6034
+ setScrubPreviewTimeSec(null);
6035
+ hideScrubTimeChipTimerRef.current = 0;
6036
+ }, constants/* WAVEFORM_FOLLOW_SCROLL_SETTLE_MS */.Zs);
6037
+ }
6038
+ }, []);
5746
6039
  const syncViewport = (0,external_react_.useCallback)(() => {
5747
6040
  const wavesurfer = wavesurferRef.current;
5748
6041
  if (!wavesurfer) {
@@ -5758,14 +6051,16 @@ function WaveformView({
5758
6051
  if (!playhead) {
5759
6052
  return;
5760
6053
  }
5761
- if (!isFollowPinned()) {
6054
+ if (isTape) {
6055
+ playhead.style.left = getTapePinnedPlayheadLeft();
6056
+ } else if (!isFollowPinned()) {
5762
6057
  playhead.style.left = timeLeftPercent(timeSec, durationSecRef.current, viewportRef.current);
5763
6058
  }
5764
6059
  const wavesurfer = wavesurferRef.current;
5765
6060
  if (wavesurfer && wavesurfer.setTime) {
5766
6061
  wavesurfer.setTime(timeSec);
5767
6062
  }
5768
- }, [isFollowPinned]);
6063
+ }, [isFollowPinned, isTape]);
5769
6064
 
5770
6065
  // Same zoom-window body as the zoom effect; extracted so resize can call it too.
5771
6066
  const applyZoomWindow = (0,external_react_.useCallback)(() => {
@@ -5777,11 +6072,13 @@ function WaveformView({
5777
6072
  const viewWidthPx = wavesurfer.getWidth ? wavesurfer.getWidth() : container.clientWidth;
5778
6073
  const minPxPerSec = getZoomedPixelsPerSecond({
5779
6074
  durationSec,
6075
+ isTape,
5780
6076
  maxZoom,
5781
6077
  viewWidthPx,
5782
6078
  zoomLevel
5783
6079
  });
5784
- const origin = zoomOriginRef.current;
6080
+ const gutterPx = isTape ? getTapeGutterPx(viewWidthPx) : 0;
6081
+ const origin = isTape ? null : zoomOriginRef.current;
5785
6082
  zoomOriginRef.current = null;
5786
6083
  const didZoomChange = prevZoomRef.current !== zoomLevel;
5787
6084
  if (origin || didZoomChange) {
@@ -5791,24 +6088,27 @@ function WaveformView({
5791
6088
  try {
5792
6089
  wavesurfer.setOptions(WaveformView_objectSpread({
5793
6090
  autoScroll: false,
6091
+ fillParent: !isTape,
5794
6092
  minPxPerSec
5795
- }, zoomLevel > constants/* WAVEFORM_ZOOM_MIN */.LW ? {
5796
- progressColor: WAVEFORM_COLOR_PLAYED,
6093
+ }, isScrollableWindow ? {
6094
+ progressColor: getPlayedWaveformColor(isTape),
5797
6095
  waveColor: WAVEFORM_COLOR_UNPLAYED
5798
6096
  } : {}));
6097
+ applyWaveformGutters(wavesurfer, gutterPx);
5799
6098
  if (minPxPerSec > 0) {
6099
+ const windowViewport = createWaveformViewport({
6100
+ durationSec,
6101
+ gutterPx,
6102
+ heightPx: liveHeightRef.current || height,
6103
+ maxZoom,
6104
+ scrollLeftPx: 0,
6105
+ widthPx: viewWidthPx,
6106
+ zoomLevel
6107
+ });
5800
6108
  if (origin) {
5801
- applyScrollLeft(origin.timeSec * minPxPerSec - origin.pointerX, true);
5802
- } else if (didZoomChange && !pointerZoomRef.current) {
5803
- const zoomedViewport = createWaveformViewport({
5804
- durationSec,
5805
- heightPx: height,
5806
- maxZoom,
5807
- scrollLeftPx: 0,
5808
- widthPx: viewWidthPx,
5809
- zoomLevel
5810
- });
5811
- applyScrollLeft(Math.min(maxScrollLeft(zoomedViewport), Math.max(0, currentTimeRef.current * minPxPerSec - viewWidthPx / 2)), true);
6109
+ applyScrollLeft(origin.timeSec * minPxPerSec + gutterPx - origin.pointerX, true);
6110
+ } else if (didZoomChange && (isTape || !pointerZoomRef.current)) {
6111
+ applyScrollLeft(getCenteredScrollLeft(currentTimeRef.current, windowViewport), true);
5812
6112
  }
5813
6113
  }
5814
6114
  wavesurfer.setTime(currentTimeRef.current);
@@ -5818,13 +6118,17 @@ function WaveformView({
5818
6118
  }
5819
6119
  syncViewport();
5820
6120
  updatePlayheadPosition(currentTimeRef.current);
5821
- }, [applyScrollLeft, durationSec, height, maxZoom, onZoom, syncViewport, updatePlayheadPosition, zoomLevel]);
6121
+ }, [applyScrollLeft, durationSec, height, isScrollableWindow, isTape, maxZoom, onZoom, syncViewport, updatePlayheadPosition, zoomLevel]);
5822
6122
  applyZoomWindowRef.current = applyZoomWindow;
5823
6123
  (0,external_react_.useEffect)(() => {
5824
6124
  const container = containerRef.current;
5825
6125
  if (!container) {
5826
6126
  return undefined;
5827
6127
  }
6128
+ const measuredHeight = cameraModeRef.current === 'tape' && container.clientHeight > 0 ? Math.round(container.clientHeight) : height;
6129
+ if (cameraModeRef.current === 'tape' && measuredHeight > 0) {
6130
+ liveHeightRef.current = measuredHeight;
6131
+ }
5828
6132
  const wavesurfer = w.create({
5829
6133
  autoScroll: false,
5830
6134
  barGap: constants/* WAVEFORM_BAR_GAP */.Lu,
@@ -5835,25 +6139,34 @@ function WaveformView({
5835
6139
  cursorWidth: 0,
5836
6140
  duration: durationSecRef.current,
5837
6141
  fillParent: true,
5838
- height,
6142
+ height: measuredHeight,
5839
6143
  hideScrollbar: true,
5840
- interact: interactiveRef.current,
6144
+ dragToSeek: false,
6145
+ interact: interactiveRef.current && cameraModeRef.current !== 'tape',
5841
6146
  normalize: false,
5842
6147
  peaks: toChannels(peaksRef.current),
5843
- progressColor: WAVEFORM_COLOR_PLAYED,
6148
+ progressColor: getPlayedWaveformColor(cameraModeRef.current === 'tape'),
5844
6149
  waveColor: WAVEFORM_COLOR_UNPLAYED
5845
6150
  });
5846
6151
  const unsubscribeClick = wavesurfer.on('click', relativeX => {
5847
6152
  if (!interactiveRef.current) {
5848
6153
  return;
5849
6154
  }
6155
+ if (cameraModeRef.current === 'tape') {
6156
+ toggleTapePlaybackRef.current?.();
6157
+ return;
6158
+ }
5850
6159
  onSeekRef.current?.(relativeX * durationSecRef.current);
5851
6160
  });
5852
6161
  const unsubscribeScroll = wavesurfer.on('scroll', () => {
5853
6162
  if (suppressViewportSyncRef.current) {
5854
6163
  return;
5855
6164
  }
5856
- handleCameraScroll(() => {
6165
+ // User pan: map scroll to the time under the playhead, then tape seeks it.
6166
+ handleCameraScroll(timeSec => {
6167
+ if (cameraModeRef.current === 'tape' && !pointerZoomRef.current) {
6168
+ handleTapeSwipe(timeSec);
6169
+ }
5857
6170
  syncViewport();
5858
6171
  });
5859
6172
  });
@@ -5864,12 +6177,13 @@ function WaveformView({
5864
6177
  syncViewport();
5865
6178
  });
5866
6179
  const unsubscribeRedraw = wavesurfer.on('redrawcomplete', () => {
5867
- if (!(zoomRef.current > constants/* WAVEFORM_ZOOM_MIN */.LW)) {
6180
+ if (!(zoomRef.current > constants/* WAVEFORM_ZOOM_MIN */.LW) && cameraModeRef.current !== 'tape') {
5868
6181
  return;
5869
6182
  }
5870
6183
  tintZoomedWaveform(wavesurfer, getWaveformFills({
5871
6184
  bufferProgress: bufferProgressRef.current,
5872
- hoverProgress: hoverProgressRef.current
6185
+ hoverProgress: cameraModeRef.current === 'tape' ? null : hoverProgressRef.current,
6186
+ isTape: cameraModeRef.current === 'tape'
5873
6187
  }), true);
5874
6188
  });
5875
6189
  wavesurferRef.current = wavesurfer;
@@ -5880,6 +6194,9 @@ function WaveformView({
5880
6194
  return () => {
5881
6195
  releaseUserPanHold();
5882
6196
  cancelJump();
6197
+ window.clearTimeout(hideScrubTimeChipTimerRef.current);
6198
+ window.clearTimeout(suppressNextTapPlayPauseTimerRef.current);
6199
+ window.cancelAnimationFrame(tapeSeekAnimationRef.current);
5883
6200
  window.cancelAnimationFrame(peakTransitionAnimationRef.current);
5884
6201
  unsubscribeClick();
5885
6202
  unsubscribeScroll();
@@ -5889,18 +6206,39 @@ function WaveformView({
5889
6206
  wavesurferRef.current = null;
5890
6207
  displayedPeaksRef.current = null;
5891
6208
  };
5892
- }, [cancelJump, handleCameraScroll, height, releaseUserPanHold, syncViewport]);
6209
+ }, [cancelJump, handleCameraScroll, handleTapeSwipe, height, isUserPanning, releaseUserPanHold, syncViewport]);
5893
6210
  (0,external_react_.useLayoutEffect)(() => {
5894
6211
  const el = containerRef.current;
5895
6212
  if (!el) {
5896
6213
  return undefined;
5897
6214
  }
5898
6215
  setCanvasWidthPx(el.clientWidth);
6216
+ lastObservedWidthRef.current = el.clientWidth;
6217
+ const mountedHeight = Math.round(el.clientHeight);
6218
+ if (cameraModeRef.current === 'tape' && mountedHeight > 0) {
6219
+ liveHeightRef.current = mountedHeight;
6220
+ }
5899
6221
  const observer = new ResizeObserver(entries => {
5900
6222
  entries.forEach(entry => {
5901
- setCanvasWidthPx(entry.contentRect.width);
6223
+ const nextWidth = entry.contentRect.width;
6224
+ const widthChanged = nextWidth !== lastObservedWidthRef.current;
6225
+ lastObservedWidthRef.current = nextWidth;
6226
+ setCanvasWidthPx(nextWidth);
6227
+ const nextHeight = Math.round(entry.contentRect.height);
6228
+ if (cameraModeRef.current === 'tape' && Number.isFinite(nextHeight) && nextHeight > 0 && nextHeight !== liveHeightRef.current) {
6229
+ liveHeightRef.current = nextHeight;
6230
+ const wavesurfer = wavesurferRef.current;
6231
+ if (wavesurfer && wavesurfer.setOptions) {
6232
+ wavesurfer.setOptions({
6233
+ height: nextHeight
6234
+ });
6235
+ }
6236
+ }
6237
+ syncViewport();
6238
+ if (widthChanged) {
6239
+ applyZoomWindowRef.current?.();
6240
+ }
5902
6241
  });
5903
- syncViewport();
5904
6242
  });
5905
6243
  observer.observe(el);
5906
6244
  return () => observer.disconnect();
@@ -5925,7 +6263,7 @@ function WaveformView({
5925
6263
  return;
5926
6264
  }
5927
6265
  wavesurfer.setOptions({
5928
- interact: interactive
6266
+ interact: interactive && cameraModeRef.current !== 'tape'
5929
6267
  });
5930
6268
  }, [interactive]);
5931
6269
  (0,external_react_.useLayoutEffect)(() => {
@@ -5956,13 +6294,17 @@ function WaveformView({
5956
6294
  window.cancelAnimationFrame(playheadAnimationRef.current);
5957
6295
  cancelJump();
5958
6296
  releaseUserPanHold();
5959
- clearFollowPin();
6297
+ if (cameraModeRef.current !== 'tape') {
6298
+ clearFollowPin();
6299
+ }
5960
6300
  syncViewport();
5961
6301
  updatePlayheadPosition(media.currentTime);
5962
6302
  };
5963
6303
  const handleSeeked = () => {
5964
6304
  onPlayheadSeek(media.currentTime);
5965
- seekTo(media.currentTime);
6305
+ if (!isUserPanning()) {
6306
+ seekTo(media.currentTime);
6307
+ }
5966
6308
  updatePlayheadPosition(media.currentTime);
5967
6309
  };
5968
6310
  if (!media.paused) {
@@ -5981,7 +6323,7 @@ function WaveformView({
5981
6323
  media.removeEventListener('pause', stopLoop);
5982
6324
  media.removeEventListener('seeked', handleSeeked);
5983
6325
  };
5984
- }, [applyPlayheadCamera, cancelJump, clearFollowPin, mediaEl, onPlayheadSeek, releaseUserPanHold, seekTo, syncViewport, updatePlayheadPosition]);
6326
+ }, [applyPlayheadCamera, cancelJump, clearFollowPin, isUserPanning, mediaEl, onPlayheadSeek, releaseUserPanHold, seekTo, syncViewport, updatePlayheadPosition]);
5985
6327
  (0,external_react_.useEffect)(() => {
5986
6328
  const wavesurfer = wavesurferRef.current;
5987
6329
  if (!wavesurfer || !wavesurfer.setOptions || !(durationSec > 0)) {
@@ -6016,9 +6358,10 @@ function WaveformView({
6016
6358
  }
6017
6359
  const fills = getWaveformFills({
6018
6360
  bufferProgress,
6019
- hoverProgress
6361
+ hoverProgress: isTape ? null : hoverProgress,
6362
+ isTape
6020
6363
  });
6021
- if (isZoomed) {
6364
+ if (isScrollableWindow) {
6022
6365
  tintZoomedWaveform(wavesurfer, fills);
6023
6366
  return;
6024
6367
  }
@@ -6027,7 +6370,7 @@ function WaveformView({
6027
6370
  waveColor: toCanvasFill(fills.waveColor, devicePixelWidth(canvasWidthPx))
6028
6371
  });
6029
6372
  wavesurfer.setTime(currentTimeRef.current);
6030
- }, [bufferProgress, canvasWidthPx, hoverProgress, isZoomed]);
6373
+ }, [bufferProgress, canvasWidthPx, hoverProgress, isScrollableWindow, isTape]);
6031
6374
  (0,external_react_.useEffect)(() => {
6032
6375
  const track = trackRef.current;
6033
6376
  if (!track) {
@@ -6108,7 +6451,7 @@ function WaveformView({
6108
6451
  };
6109
6452
  }, [durationSec, markPointerZoom, setZoomLevel]);
6110
6453
  const onHoverMove = (0,external_react_.useCallback)(event => {
6111
- if (!interactive) {
6454
+ if (!interactive || isTape || isTouchHoverInput(event)) {
6112
6455
  return;
6113
6456
  }
6114
6457
  const rect = event.currentTarget.getBoundingClientRect();
@@ -6125,19 +6468,48 @@ function WaveformView({
6125
6468
  return;
6126
6469
  }
6127
6470
  setHoverProgress(Math.min(1, Math.max(0, pointerX / rect.width)));
6128
- }, [durationSec, interactive]);
6471
+ }, [durationSec, interactive, isTape]);
6129
6472
  const onHoverLeave = (0,external_react_.useCallback)(() => {
6130
6473
  setHoverProgress(null);
6131
6474
  }, []);
6475
+ const bindOverlayPortalHost = (0,external_react_.useCallback)(node => {
6476
+ const host = node?.parentElement ?? null;
6477
+ setOverlayPortalHost(prev => prev === host ? prev : host);
6478
+ }, []);
6132
6479
  const hoverLeft = hoverProgress == null ? null : timeLeftPercent(hoverProgress * durationSec, durationSec, viewportRef.current);
6480
+ const scrubTimeChip = isTape && scrubPreviewTimeSec != null ? /*#__PURE__*/external_react_["default"].createElement("div", {
6481
+ className: "bp-WaveformView-hover bp-WaveformView-hover--tape",
6482
+ "data-testid": "bp-waveform-hover"
6483
+ }, /*#__PURE__*/external_react_["default"].createElement("div", {
6484
+ className: "bp-WaveformView-hoverTime",
6485
+ "data-testid": "bp-waveform-hover-time"
6486
+ }, formatTime(scrubPreviewTimeSec))) : null;
6133
6487
  return /*#__PURE__*/external_react_["default"].createElement("div", {
6134
- className: `bp-WaveformView${interactive ? '' : ' bp-WaveformView--inert'}${isZoomed ? ' bp-WaveformView--zoomed' : ''}`,
6488
+ ref: bindOverlayPortalHost,
6489
+ className: classnames_default()('bp-WaveformView', {
6490
+ 'bp-WaveformView--inert': !interactive,
6491
+ 'bp-WaveformView--tape': isTape,
6492
+ 'bp-WaveformView--zoomed': isZoomed
6493
+ }),
6135
6494
  "data-testid": "bp-waveform-view"
6136
6495
  }, /*#__PURE__*/external_react_["default"].createElement("div", {
6137
6496
  ref: trackRef,
6138
6497
  className: "bp-WaveformView-track",
6139
- onMouseLeave: interactive ? onHoverLeave : undefined,
6140
- onMouseMove: interactive ? onHoverMove : undefined
6498
+ onMouseLeave: interactive && !isTape ? onHoverLeave : undefined,
6499
+ onMouseMove: interactive && !isTape ? onHoverMove : undefined,
6500
+ onPointerMove: interactive && !isTape ? event => {
6501
+ if (event.pointerType === 'touch') {
6502
+ onHoverLeave();
6503
+ return;
6504
+ }
6505
+ onHoverMove(event);
6506
+ } : undefined,
6507
+ onPointerUp: isTape ? event => {
6508
+ if (event.button > 0) {
6509
+ return;
6510
+ }
6511
+ toggleTapePlaybackRef.current?.();
6512
+ } : undefined
6141
6513
  }, /*#__PURE__*/external_react_["default"].createElement("div", {
6142
6514
  ref: containerRef,
6143
6515
  className: "bp-WaveformView-canvas"
@@ -6155,12 +6527,9 @@ function WaveformView({
6155
6527
  }, /*#__PURE__*/external_react_["default"].createElement("div", {
6156
6528
  className: "bp-WaveformView-hoverTime",
6157
6529
  "data-testid": "bp-waveform-hover-time"
6158
- }, formatTime(hoverProgress * durationSec)))));
6530
+ }, formatTime(hoverProgress * durationSec)))), scrubTimeChip && overlayPortalHost ? /*#__PURE__*/__WEBPACK_EXTERNAL_MODULE_react_dom_7dac9eee_createPortal__(scrubTimeChip, overlayPortalHost) : null);
6159
6531
  }
6160
6532
  /* harmony default export */ const waveform_WaveformView = (/*#__PURE__*/external_react_["default"].memo(WaveformView));
6161
- // EXTERNAL MODULE: ./node_modules/classnames/index.js
6162
- var classnames = __webpack_require__(2485);
6163
- var classnames_default = /*#__PURE__*/__webpack_require__.n(classnames);
6164
6533
  ;// ./src/lib/viewers/controls/icons/IconZoomIn24.tsx
6165
6534
 
6166
6535
  function IconZoomIn24() {
@@ -6246,11 +6615,14 @@ function WaveformZoomControl({
6246
6615
  }) {
6247
6616
  const [isHovered, setHovered] = (0,external_react_.useState)(false);
6248
6617
  const [isFocused, setFocused] = (0,external_react_.useState)(false);
6249
- const dismissTimerRef = (0,external_react_.useRef)(0);
6618
+ const [isPinned, setPinned] = (0,external_react_.useState)(false);
6619
+ const wasFlyoutOpenOnPointerDownRef = (0,external_react_.useRef)(true); // first tap on + pins the flyout instead of zooming
6620
+ const zoomControlElRef = (0,external_react_.useRef)(null); // root; outside pointerdown unpins the flyout
6621
+ const dismissTimerRef = (0,external_react_.useRef)(0); // delay before hover-close of the flyout
6250
6622
  const sliderId = `bp-waveform-zoom-slider${(0,external_react_.useId)()}`;
6251
6623
  const zoom = clampWaveformZoom(zoomLevel, maxZoom);
6252
6624
  const zoomValue = Math.round(sliderValueFromZoom(zoom, maxZoom));
6253
- const isOpen = isHovered || isFocused || isRevealed;
6625
+ const isOpen = isHovered || isFocused || isRevealed || isPinned;
6254
6626
  const isAtMinZoom = zoomValue <= 0;
6255
6627
  const isAtMaxZoom = zoomValue >= constants/* WAVEFORM_ZOOM_SLIDER_MAX */.nh;
6256
6628
  const clearDismiss = (0,external_react_.useCallback)(() => {
@@ -6258,13 +6630,42 @@ function WaveformZoomControl({
6258
6630
  dismissTimerRef.current = 0;
6259
6631
  }, []);
6260
6632
  (0,external_react_.useEffect)(() => () => window.clearTimeout(dismissTimerRef.current), []);
6633
+ (0,external_react_.useEffect)(() => {
6634
+ if (!isPinned) {
6635
+ return undefined;
6636
+ }
6637
+ const closeIfOutside = event => {
6638
+ const zoomControlEl = zoomControlElRef.current;
6639
+ if (!zoomControlEl || zoomControlEl.contains(event.target)) {
6640
+ return;
6641
+ }
6642
+ setPinned(false);
6643
+ setFocused(false);
6644
+ setHovered(false);
6645
+ };
6646
+ document.addEventListener('pointerdown', closeIfOutside);
6647
+ return () => document.removeEventListener('pointerdown', closeIfOutside);
6648
+ }, [isPinned]);
6261
6649
  const handleSlider = (0,external_react_.useCallback)(newValue => {
6262
6650
  onZoomChange(zoomFromSliderValue(newValue, maxZoom));
6263
6651
  }, [maxZoom, onZoomChange]);
6264
6652
  const handleStep = (0,external_react_.useCallback)(delta => {
6265
6653
  onZoomChange(zoomFromSliderValue(zoomValue + delta, maxZoom));
6266
6654
  }, [maxZoom, onZoomChange, zoomValue]);
6655
+ const handleZoomInPointerDown = (0,external_react_.useCallback)(() => {
6656
+ wasFlyoutOpenOnPointerDownRef.current = isHovered || isFocused || isRevealed || isPinned;
6657
+ }, [isFocused, isHovered, isPinned, isRevealed]);
6658
+ const handleZoomInClick = (0,external_react_.useCallback)(() => {
6659
+ if (!wasFlyoutOpenOnPointerDownRef.current) {
6660
+ setPinned(true);
6661
+ return;
6662
+ }
6663
+ if (!isAtMaxZoom) {
6664
+ handleStep(constants/* WAVEFORM_ZOOM_BUTTON_STEP */.QD);
6665
+ }
6666
+ }, [handleStep, isAtMaxZoom]);
6267
6667
  return /*#__PURE__*/external_react_["default"].createElement("div", {
6668
+ ref: zoomControlElRef,
6268
6669
  "aria-label": "Zoom",
6269
6670
  className: classnames_default()('bp-WaveformZoomControl', {
6270
6671
  'bp-is-open': isOpen
@@ -6275,6 +6676,7 @@ function WaveformZoomControl({
6275
6676
  return;
6276
6677
  }
6277
6678
  setFocused(false);
6679
+ setPinned(false);
6278
6680
  },
6279
6681
  onFocus: () => {
6280
6682
  clearDismiss();
@@ -6292,15 +6694,12 @@ function WaveformZoomControl({
6292
6694
  },
6293
6695
  role: "group"
6294
6696
  }, /*#__PURE__*/external_react_["default"].createElement(MediaToggle/* default */.A, {
6295
- "aria-disabled": isAtMaxZoom,
6697
+ "aria-disabled": isOpen && isAtMaxZoom,
6296
6698
  className: "bp-WaveformZoomControl-button",
6297
6699
  "data-resin-target": "waveformZoomIn",
6298
6700
  "data-testid": "bp-waveform-zoom-in",
6299
- onClick: () => {
6300
- if (!isAtMaxZoom) {
6301
- handleStep(constants/* WAVEFORM_ZOOM_BUTTON_STEP */.QD);
6302
- }
6303
- },
6701
+ onClick: handleZoomInClick,
6702
+ onPointerDown: handleZoomInPointerDown,
6304
6703
  title: "Zoom in"
6305
6704
  }, /*#__PURE__*/external_react_["default"].createElement(icons_IconZoomIn24, null)), /*#__PURE__*/external_react_["default"].createElement("div", {
6306
6705
  "aria-hidden": !isOpen,
@@ -6354,6 +6753,7 @@ function WaveformZoomControl({
6354
6753
 
6355
6754
 
6356
6755
 
6756
+
6357
6757
  const PLACEHOLDER_PEAKS = placeholderPeaks();
6358
6758
  function MP3ControlsV2({
6359
6759
  autoplay,
@@ -6375,16 +6775,21 @@ function MP3ControlsV2({
6375
6775
  volume
6376
6776
  }) {
6377
6777
  const durationValue = typeof durationTime === 'number' && isFinite_default()(durationTime) ? durationTime : 0;
6778
+ const mediaDuration = mediaEl ? mediaEl.duration : NaN;
6779
+ const hasMediaMetadata = typeof mediaDuration === 'number' && Number.isFinite(mediaDuration) && mediaDuration > 0;
6780
+ const hasWaveformDuration = durationValue > 0;
6378
6781
  const [zoomLevel, setZoomLevel] = (0,external_react_.useState)(constants/* WAVEFORM_ZOOM_MIN */.LW);
6379
6782
  const [maxZoom, setMaxZoom] = (0,external_react_.useState)(constants/* WAVEFORM_ZOOM_MIN */.LW);
6380
6783
  const [isZoomRevealed, setIsZoomRevealed] = (0,external_react_.useState)(false);
6381
- const zoomRevealTimerRef = (0,external_react_.useRef)(0);
6784
+ const zoomRevealTimerRef = (0,external_react_.useRef)(0); // hides the zoom flyout after WAVEFORM_ZOOM_DISMISS_MS
6382
6785
  const hasRealPeaks = !!(peaks && peaks.length);
6383
6786
  const waveformPeaks = hasRealPeaks ? peaks : PLACEHOLDER_PEAKS;
6384
- const hasMetadata = durationValue > 0;
6385
- const waveformDurationSec = hasMetadata ? durationValue : PLACEHOLDER_DURATION_SEC;
6787
+ const waveformDurationSec = hasWaveformDuration ? durationValue : PLACEHOLDER_DURATION_SEC;
6386
6788
  const [playRequested, setPlayRequested] = (0,external_react_.useState)(false);
6387
6789
  const [viewport, setViewport] = (0,external_react_.useState)(null);
6790
+ const isTape = useTapeWaveform();
6791
+ const hasAppliedTapeDefaultZoomRef = (0,external_react_.useRef)(false); // ~10s window applied; reset to 1× when leaving tape
6792
+ const userChangedTapeZoomRef = (0,external_react_.useRef)(false); // pinch/wheel zoom; skip re-applying the 10s default
6388
6793
  const handleViewportChange = (0,external_react_.useCallback)(next => {
6389
6794
  setMaxZoom(prev => prev === next.maxZoom ? prev : next.maxZoom);
6390
6795
  setViewport(prev => viewportEquals(prev, next) ? prev : next);
@@ -6398,12 +6803,33 @@ function MP3ControlsV2({
6398
6803
  }, constants/* WAVEFORM_ZOOM_DISMISS_MS */.m6);
6399
6804
  }, []);
6400
6805
  const handleWaveformZoom = (0,external_react_.useCallback)(nextZoom => {
6806
+ userChangedTapeZoomRef.current = true;
6401
6807
  setZoomLevel(nextZoom);
6402
6808
  revealZoomControl();
6403
6809
  }, [revealZoomControl]);
6404
6810
  (0,external_react_.useEffect)(() => {
6405
6811
  setZoomLevel(prev => clampWaveformZoom(prev, maxZoom));
6406
6812
  }, [maxZoom]);
6813
+ (0,external_react_.useEffect)(() => {
6814
+ if (!isTape) {
6815
+ if (hasAppliedTapeDefaultZoomRef.current) {
6816
+ hasAppliedTapeDefaultZoomRef.current = false;
6817
+ userChangedTapeZoomRef.current = false;
6818
+ setZoomLevel(constants/* WAVEFORM_ZOOM_MIN */.LW);
6819
+ }
6820
+ return;
6821
+ }
6822
+ if (!hasWaveformDuration || !hasRealPeaks || !viewport || !(viewport.widthPx > 0)) {
6823
+ return;
6824
+ }
6825
+ if (userChangedTapeZoomRef.current) {
6826
+ hasAppliedTapeDefaultZoomRef.current = true;
6827
+ return;
6828
+ }
6829
+ const nextZoom = getTapeDefaultZoom(durationValue, Math.max(maxZoom, viewport.maxZoom));
6830
+ hasAppliedTapeDefaultZoomRef.current = true;
6831
+ setZoomLevel(prev => prev === nextZoom ? prev : nextZoom);
6832
+ }, [durationValue, hasRealPeaks, hasWaveformDuration, isTape, maxZoom, viewport]);
6407
6833
  (0,external_react_.useEffect)(() => () => window.clearTimeout(zoomRevealTimerRef.current), []);
6408
6834
  (0,external_react_.useEffect)(() => {
6409
6835
  if (isPlaying) {
@@ -6427,11 +6853,12 @@ function MP3ControlsV2({
6427
6853
  }
6428
6854
  onTimeChange(marker.time);
6429
6855
  }, [onCommentMarkerClick, onPlayPause, onTimeChange]);
6430
- const isWaveformInteractive = playRequested && hasMetadata;
6431
- const isWaitingToPlay = playRequested && !hasMetadata;
6856
+ const isWaveformInteractive = playRequested && hasMediaMetadata;
6857
+ const isWaitingToPlay = playRequested && !hasMediaMetadata;
6432
6858
  const showPlayOverlay = !playRequested && !isPlaying;
6433
6859
  const hasZoomHandlers = hasRealPeaks && !showPlayOverlay;
6434
- const hasZoomControl = hasZoomHandlers && hasMetadata && maxZoom > constants/* WAVEFORM_ZOOM_MIN */.LW;
6860
+ const hasZoomControl = hasZoomHandlers && hasMediaMetadata && maxZoom > constants/* WAVEFORM_ZOOM_MIN */.LW;
6861
+ const waveformZoomLevel = isTape || hasZoomHandlers ? zoomLevel : constants/* WAVEFORM_ZOOM_MIN */.LW;
6435
6862
  return /*#__PURE__*/external_react_["default"].createElement("div", {
6436
6863
  className: "bp-MP3ControlsV2",
6437
6864
  "data-testid": "media-controls-wrapper-v2"
@@ -6441,18 +6868,22 @@ function MP3ControlsV2({
6441
6868
  className: "bp-MP3ControlsV2-waveform"
6442
6869
  }, /*#__PURE__*/external_react_["default"].createElement(waveform_WaveformView, {
6443
6870
  bufferedRange: bufferedRange,
6871
+ cameraMode: isTape ? 'tape' : 'desktop',
6444
6872
  currentTime: currentTime,
6445
6873
  durationSec: waveformDurationSec,
6446
6874
  interactive: isWaveformInteractive,
6875
+ isPlaying: isPlaying,
6447
6876
  mediaEl: mediaEl,
6877
+ onPlayPause: isWaveformInteractive ? onPlayPause : undefined,
6448
6878
  onSeek: isWaveformInteractive ? onTimeChange : undefined,
6449
6879
  onViewportChange: hasRealPeaks ? handleViewportChange : undefined,
6450
6880
  onZoomChange: hasZoomHandlers ? handleWaveformZoom : undefined,
6451
6881
  peaks: waveformPeaks,
6452
- zoomLevel: hasZoomHandlers ? zoomLevel : constants/* WAVEFORM_ZOOM_MIN */.LW
6882
+ zoomLevel: waveformZoomLevel
6453
6883
  }), /*#__PURE__*/external_react_["default"].createElement(WaveformCommentMarkers, {
6454
6884
  commentMarkers: waveformMarkers,
6455
- durationSec: hasMetadata ? durationValue : 0,
6885
+ durationSec: hasWaveformDuration ? durationValue : 0,
6886
+ isTape: isTape,
6456
6887
  onCommentMarkerClick: handleCommentMarkerClick,
6457
6888
  selectedId: selectedMarkerId,
6458
6889
  viewport: viewport
@@ -6461,7 +6892,7 @@ function MP3ControlsV2({
6461
6892
  }, /*#__PURE__*/external_react_["default"].createElement(WaveformZoomControl, {
6462
6893
  isRevealed: isZoomRevealed,
6463
6894
  maxZoom: maxZoom,
6464
- onZoomChange: setZoomLevel,
6895
+ onZoomChange: handleWaveformZoom,
6465
6896
  zoomLevel: zoomLevel
6466
6897
  })), showPlayOverlay && /*#__PURE__*/external_react_["default"].createElement("button", {
6467
6898
  className: "bp-MP3ControlsV2-playOverlay"
@@ -6479,7 +6910,7 @@ function MP3ControlsV2({
6479
6910
  }), isWaitingToPlay && /*#__PURE__*/external_react_["default"].createElement("div", {
6480
6911
  className: "bp-media-buffering-spinner",
6481
6912
  "data-testid": "bp-MP3ControlsV2-loading"
6482
- })), hasMetadata && /*#__PURE__*/external_react_["default"].createElement("div", {
6913
+ })), hasMediaMetadata && /*#__PURE__*/external_react_["default"].createElement("div", {
6483
6914
  className: "bp-MP3ControlsV2-bar",
6484
6915
  "data-testid": "bp-MP3ControlsV2-bar"
6485
6916
  }, /*#__PURE__*/external_react_["default"].createElement("div", {
@@ -6581,6 +7012,7 @@ function isFpsAvailable(player) {
6581
7012
  (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
6582
7013
 
6583
7014
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
7015
+ /* harmony export */ Av: () => (/* binding */ WAVEFORM_TAPE_DEFAULT_WINDOW_SEC),
6584
7016
  /* harmony export */ Bq: () => (/* binding */ WAVEFORM_PLAYHEAD_JUMP_MS),
6585
7017
  /* harmony export */ DY: () => (/* binding */ WAVEFORM_BAR_RADIUS),
6586
7018
  /* harmony export */ EB: () => (/* binding */ CLIENT_DECODE_MAX_COMPRESSED_BYTES),
@@ -6588,12 +7020,14 @@ function isFpsAvailable(player) {
6588
7020
  /* harmony export */ GQ: () => (/* binding */ DURATION_MISMATCH_TOLERANCE_SEC),
6589
7021
  /* harmony export */ Kl: () => (/* binding */ WAVEFORM_MIN_VIEW_WINDOW_SEC),
6590
7022
  /* harmony export */ LW: () => (/* binding */ WAVEFORM_ZOOM_MIN),
7023
+ /* harmony export */ Le: () => (/* binding */ TAPE_POINTER_MEDIA_QUERY),
6591
7024
  /* harmony export */ Lu: () => (/* binding */ WAVEFORM_BAR_GAP),
6592
7025
  /* harmony export */ NM: () => (/* binding */ WAVEFORM_FOLLOW_INSET_PX),
6593
7026
  /* harmony export */ QD: () => (/* binding */ WAVEFORM_ZOOM_BUTTON_STEP),
6594
7027
  /* harmony export */ XS: () => (/* binding */ WAVEFORM_BAR_MIN_HEIGHT),
6595
7028
  /* harmony export */ Zs: () => (/* binding */ WAVEFORM_FOLLOW_SCROLL_SETTLE_MS),
6596
7029
  /* harmony export */ f3: () => (/* binding */ CLIENT_DECODE_PEAK_COUNT),
7030
+ /* harmony export */ fI: () => (/* binding */ WAVEFORM_TAPE_CLICK_SUPPRESS_MS),
6597
7031
  /* harmony export */ i8: () => (/* binding */ PEAK_UNIT_MAX),
6598
7032
  /* harmony export */ m6: () => (/* binding */ WAVEFORM_ZOOM_DISMISS_MS),
6599
7033
  /* harmony export */ mJ: () => (/* binding */ PEAK_UNIT_MIN),
@@ -6633,6 +7067,10 @@ const WAVEFORM_ZOOM_MIN = 1;
6633
7067
  const WAVEFORM_ZOOM_MAX = 24;
6634
7068
  /** Visible window never shorter than this. */
6635
7069
  const WAVEFORM_MIN_VIEW_WINDOW_SEC = 4;
7070
+ /** Tape (phone/tablet) default: seconds of audio visible in the viewport. */
7071
+ const WAVEFORM_TAPE_DEFAULT_WINDOW_SEC = 10;
7072
+ /** Primary pointer is a finger, not a mouse/trackpad. */
7073
+ const TAPE_POINTER_MEDIA_QUERY = '(hover: none) and (pointer: coarse)';
6636
7074
  const WAVEFORM_ZOOM_SLIDER_MAX = 100;
6637
7075
  /** One click on zoom in/out moves this many units on the 0–100 slider. */
6638
7076
  const WAVEFORM_ZOOM_BUTTON_STEP = 10;
@@ -6641,6 +7079,8 @@ const WAVEFORM_FOLLOW_INSET_PX = 200;
6641
7079
  const WAVEFORM_PLAYHEAD_JUMP_MS = 400;
6642
7080
  /** After the last user pan, wait this long before the camera may pin again. */
6643
7081
  const WAVEFORM_FOLLOW_SCROLL_SETTLE_MS = 150;
7082
+ /** Ignore a WaveSurfer click after a tape swipe (iOS delayed click). Must outlast the scrub-chip hide. */
7083
+ const WAVEFORM_TAPE_CLICK_SUPPRESS_MS = 200;
6644
7084
  const WAVEFORM_BAR_GAP = 2;
6645
7085
  const WAVEFORM_BAR_WIDTH = 2;
6646
7086
  const WAVEFORM_BAR_RADIUS = WAVEFORM_BAR_WIDTH / 2;
@@ -6650,25 +7090,19 @@ const WAVEFORM_HEIGHT = 140;
6650
7090
 
6651
7091
  /***/ },
6652
7092
 
6653
- /***/ 546
7093
+ /***/ 4971
6654
7094
  (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
6655
7095
 
6656
7096
 
6657
7097
  // EXPORTS
6658
7098
  __webpack_require__.d(__webpack_exports__, {
6659
- decodeToPeaks: () => (/* binding */ decodeToPeaks),
6660
- extractPeaks: () => (/* binding */ extractPeaks),
6661
- getDecodeDecision: () => (/* binding */ getDecodeDecision),
6662
- loadPeaks: () => (/* binding */ loadPeaks),
6663
- runClientDecode: () => (/* binding */ runClientDecode)
7099
+ oz: () => (/* binding */ WaveformLoadError),
7100
+ iY: () => (/* binding */ createCappedWaveformState),
7101
+ nR: () => (/* binding */ createWaveformLoader),
7102
+ Vq: () => (/* binding */ errorFromUnknown),
7103
+ zf: () => (/* binding */ isAbortError)
6664
7104
  });
6665
- // ESM COMPAT FLAG
6666
- __webpack_require__.r(__webpack_exports__);
6667
7105
 
6668
- // EXTERNAL MODULE: ./src/lib/util.js + 1 modules
6669
- var util = __webpack_require__(4410);
6670
- // EXTERNAL MODULE: ./src/lib/viewers/media/waveform/constants.ts
6671
- var constants = __webpack_require__(4929);
6672
7106
  ;// ./src/lib/viewers/media/waveform/types.ts
6673
7107
  /**
6674
7108
  * Box V1 is the in-viewer form: unsigned mono peaks in [0, 1] (peak envelope, mono_max).
@@ -6698,15 +7132,21 @@ function isWaveformErrorCode(value) {
6698
7132
 
6699
7133
  /** Visible slice of the timeline. Emitted whenever zoom, scroll, or width changes. */
6700
7134
 
7135
+ /** Navigator fields used to detect iPad, including iPadOS reporting itself as Mac. */
7136
+
7137
+ /** Window bits needed to choose tape vs desktop (coarse pointer + iPad). */
7138
+
7139
+ /** Desktop walks then pins near the right inset. Tape keeps the playhead at center. */
7140
+
6701
7141
  /** One canvas linear-gradient stop. `offset` is 0–1 along the bar. */
6702
7142
 
6703
7143
  /**
6704
7144
  * Wavesurfer's two paints: left of the playhead (`progressColor`) and right of it (`waveColor`).
6705
7145
  * A string is a solid fill; a stop list is a left-to-right step gradient.
6706
7146
  */
7147
+ // EXTERNAL MODULE: ./src/lib/viewers/media/waveform/validateWaveformPayload.ts
7148
+ var validateWaveformPayload = __webpack_require__(1053);
6707
7149
  ;// ./src/lib/viewers/media/waveform/createWaveformLoader.ts
6708
- /* unused harmony import specifier */ var isRetryableWaveformError;
6709
- /* unused harmony import specifier */ var validateWaveformPayload;
6710
7150
  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; }
6711
7151
  function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
6712
7152
  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); }
@@ -6753,7 +7193,7 @@ function failedLoadState(error) {
6753
7193
  return {
6754
7194
  status: 'failed',
6755
7195
  error,
6756
- retryable: isRetryableWaveformError(error.code)
7196
+ retryable: (0,validateWaveformPayload/* isRetryableWaveformError */.Oe)(error.code)
6757
7197
  };
6758
7198
  }
6759
7199
  function readyLoadState(payload) {
@@ -6835,7 +7275,7 @@ function createWaveformLoader(fetchPayload, options = {}) {
6835
7275
  if (staleAfterFetch) {
6836
7276
  return staleAfterFetch;
6837
7277
  }
6838
- const validation = validateWaveformPayload(raw, options);
7278
+ const validation = (0,validateWaveformPayload/* validateWaveformPayload */.a9)(raw, options);
6839
7279
  const staleAfterValidate = cancelledIfStale(request);
6840
7280
  if (staleAfterValidate) {
6841
7281
  return staleAfterValidate;
@@ -6877,185 +7317,29 @@ function createCappedWaveformState(message) {
6877
7317
  }
6878
7318
  };
6879
7319
  }
6880
- ;// ./src/lib/viewers/media/waveform/validateWaveformPayload.ts
6881
7320
 
6882
- const DEFAULT_PEAK_SCALE = 'unit';
6883
- const DEFAULT_CHANNEL_POLICY = 'mono_max';
6884
- const DEFAULT_ENVELOPE = 'peak';
6885
- function fail(code, message, retryable = false) {
6886
- return {
6887
- ok: false,
6888
- error: {
6889
- code,
6890
- message
6891
- },
6892
- retryable
6893
- };
6894
- }
6895
- function isPlainObject(value) {
6896
- return typeof value === 'object' && value !== null && !Array.isArray(value);
6897
- }
6898
- function payloadByteLength(raw) {
6899
- try {
6900
- const serialized = JSON.stringify(raw);
6901
- if (typeof Buffer !== 'undefined') {
6902
- return Buffer.byteLength(serialized, 'utf8');
6903
- }
6904
- if (typeof TextEncoder !== 'undefined') {
6905
- return new TextEncoder().encode(serialized).length;
6906
- }
6907
- return serialized.length;
6908
- } catch {
6909
- return Number.MAX_SAFE_INTEGER;
6910
- }
6911
- }
6912
- function readOptionalPolicy(raw, key, allowed) {
6913
- if (!(key in raw) || raw[key] === undefined) {
6914
- return undefined;
6915
- }
6916
- return raw[key] === allowed ? allowed : null;
6917
- }
6918
- function asWaveformPayloadV1(raw) {
6919
- const {
6920
- version,
6921
- durationSec,
6922
- peaks
6923
- } = raw;
6924
- if (version !== constants/* WAVEFORM_PAYLOAD_VERSION */.sh || typeof durationSec !== 'number' || !Array.isArray(peaks)) {
6925
- return null;
6926
- }
6927
- const peakScale = readOptionalPolicy(raw, 'peakScale', DEFAULT_PEAK_SCALE);
6928
- const channelPolicy = readOptionalPolicy(raw, 'channelPolicy', DEFAULT_CHANNEL_POLICY);
6929
- const envelope = readOptionalPolicy(raw, 'envelope', DEFAULT_ENVELOPE);
6930
- if (peakScale === null || channelPolicy === null || envelope === null) {
6931
- return null;
6932
- }
6933
- if ('channels' in raw && raw.channels !== undefined && raw.channels !== 1) {
6934
- return null;
6935
- }
6936
- return {
6937
- version: constants/* WAVEFORM_PAYLOAD_VERSION */.sh,
6938
- durationSec,
6939
- peaks: peaks,
6940
- peakScale: peakScale ?? DEFAULT_PEAK_SCALE,
6941
- channelPolicy: channelPolicy ?? DEFAULT_CHANNEL_POLICY,
6942
- envelope: envelope ?? DEFAULT_ENVELOPE,
6943
- channels: 1
6944
- };
6945
- }
6946
- function readPayloadObject(raw, maxPayloadBytes, isPayloadByteCheckSkipped = false) {
6947
- if (raw === null || raw === undefined) {
6948
- return fail('INVALID_PAYLOAD', 'Payload is null or undefined');
6949
- }
6950
- if (!isPayloadByteCheckSkipped) {
6951
- const byteLength = payloadByteLength(raw);
6952
- if (byteLength > maxPayloadBytes) {
6953
- return fail('PAYLOAD_TOO_LARGE', `Payload size ${byteLength} exceeds limit ${maxPayloadBytes}`);
6954
- }
6955
- }
6956
- if (!isPlainObject(raw)) {
6957
- return fail('INVALID_PAYLOAD', 'Payload must be an object');
6958
- }
6959
- return {
6960
- ok: true,
6961
- value: raw
6962
- };
6963
- }
6964
- function checkVersion(raw) {
6965
- if (raw.version === constants/* WAVEFORM_PAYLOAD_VERSION */.sh) {
6966
- return null;
6967
- }
6968
- const {
6969
- version
6970
- } = raw;
6971
- if (typeof version === 'number' && version > constants/* WAVEFORM_PAYLOAD_VERSION */.sh) {
6972
- return fail('UNSUPPORTED_VERSION', `Unsupported waveform version ${version}; max supported is ${constants/* WAVEFORM_PAYLOAD_VERSION */.sh}`);
6973
- }
6974
- return fail('INVALID_PAYLOAD', 'Missing or invalid version field');
6975
- }
6976
- function checkDuration(payload, expectedDurationSec) {
6977
- if (!Number.isFinite(payload.durationSec) || payload.durationSec <= 0) {
6978
- return fail('INVALID_DURATION', 'durationSec must be a positive finite number');
6979
- }
6980
- if (expectedDurationSec !== undefined && Number.isFinite(expectedDurationSec) && Math.abs(payload.durationSec - expectedDurationSec) > constants/* DURATION_MISMATCH_TOLERANCE_SEC */.GQ) {
6981
- return fail('DURATION_MISMATCH', `Payload duration ${payload.durationSec}s differs from media duration ${expectedDurationSec}s`, true);
6982
- }
6983
- return null;
6984
- }
6985
- function normalizePeaks(peaks, maxPeakCount) {
6986
- if (peaks.length === 0) {
6987
- return fail('EMPTY_PEAKS', 'peaks array must not be empty');
6988
- }
6989
- if (peaks.length > maxPeakCount) {
6990
- return fail('PEAK_COUNT_EXCEEDED', `peaks length ${peaks.length} exceeds max ${maxPeakCount}`);
6991
- }
6992
- const normalized = new Float32Array(peaks.length);
6993
- for (let i = 0; i < peaks.length; i += 1) {
6994
- const value = peaks[i];
6995
- if (!Number.isFinite(value)) {
6996
- return fail('NON_FINITE_PEAK', `Peak at index ${i} is not finite`);
6997
- }
6998
- if (value < constants/* PEAK_UNIT_MIN */.mJ || value > constants/* PEAK_UNIT_MAX */.i8) {
6999
- return fail('PEAK_OUT_OF_RANGE', `Peak at index ${i} is outside [${constants/* PEAK_UNIT_MIN */.mJ}, ${constants/* PEAK_UNIT_MAX */.i8}]`);
7000
- }
7001
- normalized[i] = value;
7002
- }
7003
- return {
7004
- ok: true,
7005
- peaks: normalized
7006
- };
7007
- }
7321
+ /***/ },
7008
7322
 
7009
- /**
7010
- * Validates a version-1 waveform payload and returns normalized peaks for rendering.
7011
- * Wire JSON requires version, durationSec, and peaks. sampleCount and source are ignored.
7012
- */
7013
- function validateWaveformPayload_validateWaveformPayload(raw, options = {}) {
7014
- const maxPeakCount = options.maxPeakCount ?? constants/* MAX_PEAK_COUNT */.FY;
7015
- const maxPayloadBytes = options.maxPayloadBytes ?? constants/* MAX_PAYLOAD_BYTES */.qY;
7016
- const objectResult = readPayloadObject(raw, maxPayloadBytes, options.isPayloadByteCheckSkipped === true);
7017
- if (!objectResult.ok) {
7018
- return objectResult;
7019
- }
7020
- const versionFailure = checkVersion(objectResult.value);
7021
- if (versionFailure) {
7022
- return versionFailure;
7023
- }
7024
- const payload = asWaveformPayloadV1(objectResult.value);
7025
- if (!payload) {
7026
- return fail('INVALID_PAYLOAD', 'Payload failed structural validation');
7027
- }
7028
- const durationFailure = checkDuration(payload, options.expectedDurationSec);
7029
- if (durationFailure) {
7030
- return durationFailure;
7031
- }
7032
- const peaksResult = normalizePeaks(payload.peaks, maxPeakCount);
7033
- if (!peaksResult.ok) {
7034
- return peaksResult;
7035
- }
7036
- return {
7037
- ok: true,
7038
- payload: {
7039
- version: constants/* WAVEFORM_PAYLOAD_VERSION */.sh,
7040
- durationSec: payload.durationSec,
7041
- peaks: peaksResult.peaks,
7042
- peakScale: payload.peakScale ?? DEFAULT_PEAK_SCALE,
7043
- channelPolicy: payload.channelPolicy ?? DEFAULT_CHANNEL_POLICY,
7044
- envelope: payload.envelope ?? DEFAULT_ENVELOPE
7045
- }
7046
- };
7047
- }
7323
+ /***/ 8296
7324
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
7048
7325
 
7049
- /** Maps validation / load failures to retry policy for loaders. */
7050
- function validateWaveformPayload_isRetryableWaveformError(code) {
7051
- return code === 'DURATION_MISMATCH' || code === 'DECODE_FAILED' || code === 'LOAD_FAILED';
7052
- }
7053
- ;// ./src/lib/viewers/media/waveform/decode.ts
7326
+ __webpack_require__.r(__webpack_exports__);
7327
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
7328
+ /* harmony export */ decodeToPeaks: () => (/* binding */ decodeToPeaks),
7329
+ /* harmony export */ extractPeaks: () => (/* binding */ extractPeaks),
7330
+ /* harmony export */ getDecodeDecision: () => (/* binding */ getDecodeDecision),
7331
+ /* harmony export */ loadPeaks: () => (/* binding */ loadPeaks),
7332
+ /* harmony export */ runClientDecode: () => (/* binding */ runClientDecode)
7333
+ /* harmony export */ });
7334
+ /* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4410);
7335
+ /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4929);
7336
+ /* harmony import */ var _createWaveformLoader__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4971);
7337
+ /* harmony import */ var _validateWaveformPayload__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(1053);
7054
7338
  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; }
7055
- 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; }
7056
- 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; }
7057
- function decode_toPropertyKey(t) { var i = decode_toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
7058
- 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); }
7339
+ 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) { _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; }
7340
+ 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; }
7341
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
7342
+ 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); }
7059
7343
 
7060
7344
 
7061
7345
 
@@ -7067,9 +7351,6 @@ const EMPTY_TIMINGS = {
7067
7351
  function createAbortError() {
7068
7352
  return new DOMException('Aborted', 'AbortError');
7069
7353
  }
7070
- function isPositiveFinite(value) {
7071
- return typeof value === 'number' && Number.isFinite(value) && value > 0;
7072
- }
7073
7354
  function getAudioContextConstructor() {
7074
7355
  if (typeof window === 'undefined') {
7075
7356
  return undefined;
@@ -7102,7 +7383,7 @@ function createSkippedDecodeResult(reason) {
7102
7383
  reason
7103
7384
  };
7104
7385
  }
7105
- return _objectSpread(_objectSpread({}, createCappedWaveformState(getDecodeSkipMessage(reason))), {}, {
7386
+ return _objectSpread(_objectSpread({}, (0,_createWaveformLoader__WEBPACK_IMPORTED_MODULE_2__/* .createCappedWaveformState */ .iY)(getDecodeSkipMessage(reason))), {}, {
7106
7387
  isDecodeSkipped: true,
7107
7388
  timings: EMPTY_TIMINGS,
7108
7389
  reason
@@ -7126,11 +7407,11 @@ function decodeWithContext(context, buffer, signal) {
7126
7407
  }
7127
7408
  isSettled = true;
7128
7409
  signal?.removeEventListener('abort', onAbort);
7129
- if (isAbortError(error)) {
7410
+ if ((0,_createWaveformLoader__WEBPACK_IMPORTED_MODULE_2__/* .isAbortError */ .zf)(error)) {
7130
7411
  reject(error instanceof DOMException ? error : createAbortError());
7131
7412
  return;
7132
7413
  }
7133
- reject(error instanceof WaveformLoadError ? error : new WaveformLoadError('DECODE_FAILED', 'decodeAudioData failed'));
7414
+ reject(error instanceof _createWaveformLoader__WEBPACK_IMPORTED_MODULE_2__/* .WaveformLoadError */ .oz ? error : new _createWaveformLoader__WEBPACK_IMPORTED_MODULE_2__/* .WaveformLoadError */ .oz('DECODE_FAILED', 'decodeAudioData failed'));
7134
7415
  };
7135
7416
  onAbort = () => onFailure(createAbortError());
7136
7417
  if (signal?.aborted) {
@@ -7156,13 +7437,13 @@ function decodeWithContext(context, buffer, signal) {
7156
7437
  * skips decode so playback is never blocked by expanding the full audio buffer.
7157
7438
  */
7158
7439
  function getDecodeDecision(media, caps = {}) {
7159
- const maxCompressedBytes = caps.maxCompressedBytes ?? constants/* CLIENT_DECODE_MAX_COMPRESSED_BYTES */.EB;
7160
- const maxDurationSec = caps.maxDurationSec ?? constants/* CLIENT_DECODE_MAX_DURATION_SEC */.nG;
7440
+ const maxCompressedBytes = caps.maxCompressedBytes ?? _constants__WEBPACK_IMPORTED_MODULE_1__/* .CLIENT_DECODE_MAX_COMPRESSED_BYTES */ .EB;
7441
+ const maxDurationSec = caps.maxDurationSec ?? _constants__WEBPACK_IMPORTED_MODULE_1__/* .CLIENT_DECODE_MAX_DURATION_SEC */ .nG;
7161
7442
  const {
7162
7443
  compressedBytes,
7163
7444
  durationSec
7164
7445
  } = media;
7165
- if (!isPositiveFinite(compressedBytes) || !isPositiveFinite(durationSec)) {
7446
+ if (!(0,_validateWaveformPayload__WEBPACK_IMPORTED_MODULE_3__/* .isPositiveFinite */ .Ze)(compressedBytes) || !(0,_validateWaveformPayload__WEBPACK_IMPORTED_MODULE_3__/* .isPositiveFinite */ .Ze)(durationSec)) {
7166
7447
  return {
7167
7448
  isAllowed: false,
7168
7449
  reason: 'missing_metadata'
@@ -7189,7 +7470,7 @@ function getDecodeDecision(media, caps = {}) {
7189
7470
  * Collapse audio channels to one unsigned peak per time bucket (max abs, then clamp to unit).
7190
7471
  * Accepts an AudioBuffer so callers can extract before closing the AudioContext.
7191
7472
  */
7192
- function extractPeaks(audio, peakCount = constants/* CLIENT_DECODE_PEAK_COUNT */.f3) {
7473
+ function extractPeaks(audio, peakCount = _constants__WEBPACK_IMPORTED_MODULE_1__/* .CLIENT_DECODE_PEAK_COUNT */ .f3) {
7193
7474
  const channels = Array.isArray(audio) ? audio : Array.from({
7194
7475
  length: audio.numberOfChannels
7195
7476
  }, (_, channelIndex) => audio.getChannelData(channelIndex));
@@ -7214,7 +7495,7 @@ function extractPeaks(audio, peakCount = constants/* CLIENT_DECODE_PEAK_COUNT */
7214
7495
  }
7215
7496
  }
7216
7497
  }
7217
- peaks[i] = Math.min(constants/* PEAK_UNIT_MAX */.i8, Math.max(constants/* PEAK_UNIT_MIN */.mJ, maxAbs));
7498
+ peaks[i] = Math.min(_constants__WEBPACK_IMPORTED_MODULE_1__/* .PEAK_UNIT_MAX */ .i8, Math.max(_constants__WEBPACK_IMPORTED_MODULE_1__/* .PEAK_UNIT_MIN */ .mJ, maxAbs));
7218
7499
  }
7219
7500
  return peaks;
7220
7501
  }
@@ -7223,13 +7504,13 @@ function extractPeaks(audio, peakCount = constants/* CLIENT_DECODE_PEAK_COUNT */
7223
7504
  * Decode compressed audio and extract unit peaks while the AudioBuffer is live.
7224
7505
  * Does not attach a media element or fetch a URL. Does not copy channel data.
7225
7506
  */
7226
- async function decodeToPeaks(arrayBuffer, signal, peakCount = constants/* CLIENT_DECODE_PEAK_COUNT */.f3) {
7507
+ async function decodeToPeaks(arrayBuffer, signal, peakCount = _constants__WEBPACK_IMPORTED_MODULE_1__/* .CLIENT_DECODE_PEAK_COUNT */ .f3) {
7227
7508
  if (signal?.aborted) {
7228
7509
  throw createAbortError();
7229
7510
  }
7230
7511
  const AudioContextConstructor = getAudioContextConstructor();
7231
7512
  if (!AudioContextConstructor) {
7232
- throw new WaveformLoadError('DECODE_FAILED', 'AudioContext is not available');
7513
+ throw new _createWaveformLoader__WEBPACK_IMPORTED_MODULE_2__/* .WaveformLoadError */ .oz('DECODE_FAILED', 'AudioContext is not available');
7233
7514
  }
7234
7515
  const context = new AudioContextConstructor();
7235
7516
  try {
@@ -7237,15 +7518,15 @@ async function decodeToPeaks(arrayBuffer, signal, peakCount = constants/* CLIENT
7237
7518
  if (signal?.aborted) {
7238
7519
  throw createAbortError();
7239
7520
  }
7240
- const extractStarted = (0,util/* getCurrentTimeMs */.RU)();
7521
+ const extractStarted = (0,_util__WEBPACK_IMPORTED_MODULE_0__/* .getCurrentTimeMs */ .RU)();
7241
7522
  const peaks = extractPeaks(audioBuffer, peakCount);
7242
7523
  return {
7243
7524
  durationSec: audioBuffer.duration,
7244
7525
  peaks,
7245
- extractMs: (0,util/* getCurrentTimeMs */.RU)() - extractStarted
7526
+ extractMs: (0,_util__WEBPACK_IMPORTED_MODULE_0__/* .getCurrentTimeMs */ .RU)() - extractStarted
7246
7527
  };
7247
7528
  } catch (error) {
7248
- if (signal?.aborted || isAbortError(error)) {
7529
+ if (signal?.aborted || (0,_createWaveformLoader__WEBPACK_IMPORTED_MODULE_2__/* .isAbortError */ .zf)(error)) {
7249
7530
  throw createAbortError();
7250
7531
  }
7251
7532
  throw error;
@@ -7278,31 +7559,31 @@ async function runClientDecode(options) {
7278
7559
  };
7279
7560
  }
7280
7561
  const signal = options.signal ?? new AbortController().signal;
7281
- const decodeStarted = (0,util/* getCurrentTimeMs */.RU)();
7562
+ const decodeStarted = (0,_util__WEBPACK_IMPORTED_MODULE_0__/* .getCurrentTimeMs */ .RU)();
7282
7563
  let decodeOutput;
7283
7564
  try {
7284
7565
  decodeOutput = await options.decode(signal);
7285
7566
  } catch (error) {
7286
- if (signal.aborted || isAbortError(error)) {
7567
+ if (signal.aborted || (0,_createWaveformLoader__WEBPACK_IMPORTED_MODULE_2__/* .isAbortError */ .zf)(error)) {
7287
7568
  return {
7288
7569
  status: 'cancelled',
7289
7570
  isDecodeSkipped: false,
7290
7571
  timings: EMPTY_TIMINGS
7291
7572
  };
7292
7573
  }
7293
- const waveformError = errorFromUnknown(error, 'DECODE_FAILED');
7574
+ const waveformError = (0,_createWaveformLoader__WEBPACK_IMPORTED_MODULE_2__/* .errorFromUnknown */ .Vq)(error, 'DECODE_FAILED');
7294
7575
  return {
7295
7576
  status: 'failed',
7296
7577
  error: waveformError,
7297
- retryable: validateWaveformPayload_isRetryableWaveformError(waveformError.code),
7578
+ retryable: (0,_validateWaveformPayload__WEBPACK_IMPORTED_MODULE_3__/* .isRetryableWaveformError */ .Oe)(waveformError.code),
7298
7579
  isDecodeSkipped: false,
7299
7580
  timings: {
7300
- attemptMs: (0,util/* getCurrentTimeMs */.RU)() - decodeStarted,
7581
+ attemptMs: (0,_util__WEBPACK_IMPORTED_MODULE_0__/* .getCurrentTimeMs */ .RU)() - decodeStarted,
7301
7582
  extractMs: null
7302
7583
  }
7303
7584
  };
7304
7585
  }
7305
- const attemptMs = (0,util/* getCurrentTimeMs */.RU)() - decodeStarted;
7586
+ const attemptMs = (0,_util__WEBPACK_IMPORTED_MODULE_0__/* .getCurrentTimeMs */ .RU)() - decodeStarted;
7306
7587
  if (signal.aborted) {
7307
7588
  return {
7308
7589
  status: 'cancelled',
@@ -7313,8 +7594,8 @@ async function runClientDecode(options) {
7313
7594
  }
7314
7595
  };
7315
7596
  }
7316
- const validation = validateWaveformPayload_validateWaveformPayload({
7317
- version: constants/* WAVEFORM_PAYLOAD_VERSION */.sh,
7597
+ const validation = (0,_validateWaveformPayload__WEBPACK_IMPORTED_MODULE_3__/* .validateWaveformPayload */ .a9)({
7598
+ version: _constants__WEBPACK_IMPORTED_MODULE_1__/* .WAVEFORM_PAYLOAD_VERSION */ .sh,
7318
7599
  durationSec: options.durationSec ?? decodeOutput.durationSec,
7319
7600
  peaks: decodeOutput.peaks
7320
7601
  }, {
@@ -7324,7 +7605,7 @@ async function runClientDecode(options) {
7324
7605
  return {
7325
7606
  status: 'failed',
7326
7607
  error: validation.error,
7327
- retryable: validateWaveformPayload_isRetryableWaveformError(validation.error.code),
7608
+ retryable: (0,_validateWaveformPayload__WEBPACK_IMPORTED_MODULE_3__/* .isRetryableWaveformError */ .Oe)(validation.error.code),
7328
7609
  isDecodeSkipped: false,
7329
7610
  timings: {
7330
7611
  attemptMs,
@@ -7361,6 +7642,193 @@ function loadPeaks(request) {
7361
7642
 
7362
7643
  /***/ },
7363
7644
 
7645
+ /***/ 1053
7646
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
7647
+
7648
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
7649
+ /* harmony export */ Oe: () => (/* binding */ isRetryableWaveformError),
7650
+ /* harmony export */ Ze: () => (/* binding */ isPositiveFinite),
7651
+ /* harmony export */ a9: () => (/* binding */ validateWaveformPayload)
7652
+ /* harmony export */ });
7653
+ /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4929);
7654
+
7655
+ const DEFAULT_PEAK_SCALE = 'unit';
7656
+ const DEFAULT_CHANNEL_POLICY = 'mono_max';
7657
+ const DEFAULT_ENVELOPE = 'peak';
7658
+ function fail(code, message, retryable = false) {
7659
+ return {
7660
+ ok: false,
7661
+ error: {
7662
+ code,
7663
+ message
7664
+ },
7665
+ retryable
7666
+ };
7667
+ }
7668
+ function isPositiveFinite(value) {
7669
+ return typeof value === 'number' && Number.isFinite(value) && value > 0;
7670
+ }
7671
+ function isPlainObject(value) {
7672
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
7673
+ }
7674
+ function payloadByteLength(raw) {
7675
+ try {
7676
+ const serialized = JSON.stringify(raw);
7677
+ if (typeof Buffer !== 'undefined') {
7678
+ return Buffer.byteLength(serialized, 'utf8');
7679
+ }
7680
+ if (typeof TextEncoder !== 'undefined') {
7681
+ return new TextEncoder().encode(serialized).length;
7682
+ }
7683
+ return serialized.length;
7684
+ } catch {
7685
+ return Number.MAX_SAFE_INTEGER;
7686
+ }
7687
+ }
7688
+ function readOptionalPolicy(raw, key, allowed) {
7689
+ if (!(key in raw) || raw[key] === undefined) {
7690
+ return undefined;
7691
+ }
7692
+ return raw[key] === allowed ? allowed : null;
7693
+ }
7694
+ function asWaveformPayloadV1(raw) {
7695
+ const {
7696
+ version,
7697
+ durationSec,
7698
+ peaks
7699
+ } = raw;
7700
+ if (version !== _constants__WEBPACK_IMPORTED_MODULE_0__/* .WAVEFORM_PAYLOAD_VERSION */ .sh || typeof durationSec !== 'number' || !Array.isArray(peaks)) {
7701
+ return null;
7702
+ }
7703
+ const peakScale = readOptionalPolicy(raw, 'peakScale', DEFAULT_PEAK_SCALE);
7704
+ const channelPolicy = readOptionalPolicy(raw, 'channelPolicy', DEFAULT_CHANNEL_POLICY);
7705
+ const envelope = readOptionalPolicy(raw, 'envelope', DEFAULT_ENVELOPE);
7706
+ if (peakScale === null || channelPolicy === null || envelope === null) {
7707
+ return null;
7708
+ }
7709
+ if ('channels' in raw && raw.channels !== undefined && raw.channels !== 1) {
7710
+ return null;
7711
+ }
7712
+ return {
7713
+ version: _constants__WEBPACK_IMPORTED_MODULE_0__/* .WAVEFORM_PAYLOAD_VERSION */ .sh,
7714
+ durationSec,
7715
+ peaks: peaks,
7716
+ peakScale: peakScale ?? DEFAULT_PEAK_SCALE,
7717
+ channelPolicy: channelPolicy ?? DEFAULT_CHANNEL_POLICY,
7718
+ envelope: envelope ?? DEFAULT_ENVELOPE,
7719
+ channels: 1
7720
+ };
7721
+ }
7722
+ function readPayloadObject(raw, maxPayloadBytes, isPayloadByteCheckSkipped = false) {
7723
+ if (raw === null || raw === undefined) {
7724
+ return fail('INVALID_PAYLOAD', 'Payload is null or undefined');
7725
+ }
7726
+ if (!isPayloadByteCheckSkipped) {
7727
+ const byteLength = payloadByteLength(raw);
7728
+ if (byteLength > maxPayloadBytes) {
7729
+ return fail('PAYLOAD_TOO_LARGE', `Payload size ${byteLength} exceeds limit ${maxPayloadBytes}`);
7730
+ }
7731
+ }
7732
+ if (!isPlainObject(raw)) {
7733
+ return fail('INVALID_PAYLOAD', 'Payload must be an object');
7734
+ }
7735
+ return {
7736
+ ok: true,
7737
+ value: raw
7738
+ };
7739
+ }
7740
+ function checkVersion(raw) {
7741
+ if (raw.version === _constants__WEBPACK_IMPORTED_MODULE_0__/* .WAVEFORM_PAYLOAD_VERSION */ .sh) {
7742
+ return null;
7743
+ }
7744
+ const {
7745
+ version
7746
+ } = raw;
7747
+ if (typeof version === 'number' && version > _constants__WEBPACK_IMPORTED_MODULE_0__/* .WAVEFORM_PAYLOAD_VERSION */ .sh) {
7748
+ return fail('UNSUPPORTED_VERSION', `Unsupported waveform version ${version}; max supported is ${_constants__WEBPACK_IMPORTED_MODULE_0__/* .WAVEFORM_PAYLOAD_VERSION */ .sh}`);
7749
+ }
7750
+ return fail('INVALID_PAYLOAD', 'Missing or invalid version field');
7751
+ }
7752
+ function checkDuration(payload, expectedDurationSec) {
7753
+ if (!isPositiveFinite(payload.durationSec)) {
7754
+ return fail('INVALID_DURATION', 'durationSec must be a positive finite number');
7755
+ }
7756
+ if (expectedDurationSec !== undefined && Number.isFinite(expectedDurationSec) && Math.abs(payload.durationSec - expectedDurationSec) > _constants__WEBPACK_IMPORTED_MODULE_0__/* .DURATION_MISMATCH_TOLERANCE_SEC */ .GQ) {
7757
+ return fail('DURATION_MISMATCH', `Payload duration ${payload.durationSec}s differs from media duration ${expectedDurationSec}s`, true);
7758
+ }
7759
+ return null;
7760
+ }
7761
+ function normalizePeaks(peaks, maxPeakCount) {
7762
+ if (peaks.length === 0) {
7763
+ return fail('EMPTY_PEAKS', 'peaks array must not be empty');
7764
+ }
7765
+ if (peaks.length > maxPeakCount) {
7766
+ return fail('PEAK_COUNT_EXCEEDED', `peaks length ${peaks.length} exceeds max ${maxPeakCount}`);
7767
+ }
7768
+ const normalized = new Float32Array(peaks.length);
7769
+ for (let i = 0; i < peaks.length; i += 1) {
7770
+ const value = peaks[i];
7771
+ if (!Number.isFinite(value)) {
7772
+ return fail('NON_FINITE_PEAK', `Peak at index ${i} is not finite`);
7773
+ }
7774
+ if (value < _constants__WEBPACK_IMPORTED_MODULE_0__/* .PEAK_UNIT_MIN */ .mJ || value > _constants__WEBPACK_IMPORTED_MODULE_0__/* .PEAK_UNIT_MAX */ .i8) {
7775
+ return fail('PEAK_OUT_OF_RANGE', `Peak at index ${i} is outside [${_constants__WEBPACK_IMPORTED_MODULE_0__/* .PEAK_UNIT_MIN */ .mJ}, ${_constants__WEBPACK_IMPORTED_MODULE_0__/* .PEAK_UNIT_MAX */ .i8}]`);
7776
+ }
7777
+ normalized[i] = value;
7778
+ }
7779
+ return {
7780
+ ok: true,
7781
+ peaks: normalized
7782
+ };
7783
+ }
7784
+
7785
+ /**
7786
+ * Validates a version-1 waveform payload and returns normalized peaks for rendering.
7787
+ * Wire JSON requires version, durationSec, and peaks. sampleCount and source are ignored.
7788
+ */
7789
+ function validateWaveformPayload(raw, options = {}) {
7790
+ const maxPeakCount = options.maxPeakCount ?? _constants__WEBPACK_IMPORTED_MODULE_0__/* .MAX_PEAK_COUNT */ .FY;
7791
+ const maxPayloadBytes = options.maxPayloadBytes ?? _constants__WEBPACK_IMPORTED_MODULE_0__/* .MAX_PAYLOAD_BYTES */ .qY;
7792
+ const objectResult = readPayloadObject(raw, maxPayloadBytes, options.isPayloadByteCheckSkipped === true);
7793
+ if (!objectResult.ok) {
7794
+ return objectResult;
7795
+ }
7796
+ const versionFailure = checkVersion(objectResult.value);
7797
+ if (versionFailure) {
7798
+ return versionFailure;
7799
+ }
7800
+ const payload = asWaveformPayloadV1(objectResult.value);
7801
+ if (!payload) {
7802
+ return fail('INVALID_PAYLOAD', 'Payload failed structural validation');
7803
+ }
7804
+ const durationFailure = checkDuration(payload, options.expectedDurationSec);
7805
+ if (durationFailure) {
7806
+ return durationFailure;
7807
+ }
7808
+ const peaksResult = normalizePeaks(payload.peaks, maxPeakCount);
7809
+ if (!peaksResult.ok) {
7810
+ return peaksResult;
7811
+ }
7812
+ return {
7813
+ ok: true,
7814
+ payload: {
7815
+ version: _constants__WEBPACK_IMPORTED_MODULE_0__/* .WAVEFORM_PAYLOAD_VERSION */ .sh,
7816
+ durationSec: payload.durationSec,
7817
+ peaks: peaksResult.peaks,
7818
+ peakScale: payload.peakScale ?? DEFAULT_PEAK_SCALE,
7819
+ channelPolicy: payload.channelPolicy ?? DEFAULT_CHANNEL_POLICY,
7820
+ envelope: payload.envelope ?? DEFAULT_ENVELOPE
7821
+ }
7822
+ };
7823
+ }
7824
+
7825
+ /** Maps validation / load failures to retry policy for loaders. */
7826
+ function isRetryableWaveformError(code) {
7827
+ return code === 'DURATION_MISMATCH' || code === 'DECODE_FAILED' || code === 'LOAD_FAILED';
7828
+ }
7829
+
7830
+ /***/ },
7831
+
7364
7832
  /***/ 2485
7365
7833
  (module, exports) {
7366
7834
 
@@ -23024,7 +23492,7 @@ class Browser {
23024
23492
  ;// ./src/lib/Logger.js
23025
23493
  /* eslint-disable no-undef */
23026
23494
  const CLIENT_NAME = "box-content-preview";
23027
- const CLIENT_VERSION = "3.89.0";
23495
+ const CLIENT_VERSION = "3.91.0";
23028
23496
  /* eslint-enable no-undef */
23029
23497
 
23030
23498
  class Logger {
@@ -34403,10 +34871,9 @@ class MediaBaseViewer extends viewers_BaseViewer {
34403
34871
  const template = this.options.representation.content.url_template;
34404
34872
  this.mediaEl.addEventListener('error', this.errorHandler);
34405
34873
  this.mediaEl.setAttribute('title', this.options.file.name);
34406
- if (lib_Browser.isIOS()) {
34407
- // iOS doesn't fire loadeddata event until some data loads
34408
- // Adding autoplay prevents this but won't actually autoplay the video.
34409
- // https://webkit.org/blog/6784/new-video-policies-for-ios/
34874
+ if (lib_Browser.isIOS() && this.mediaEl.tagName === 'VIDEO') {
34875
+ // Unblocks loadeddata on iOS <video>. Do not set this on <audio>:
34876
+ // after tap-to-open, Safari will start playback.
34410
34877
  this.mediaEl.autoplay = true;
34411
34878
  }
34412
34879
  const contentUrl = this.createContentUrlV2(template);
@@ -35709,6 +36176,12 @@ class MP3ControlsRoot {
35709
36176
  this.root.render(controls);
35710
36177
  }
35711
36178
  }
36179
+ // EXTERNAL MODULE: ./src/lib/viewers/media/waveform/constants.ts
36180
+ var waveform_constants = __webpack_require__(4929);
36181
+ // EXTERNAL MODULE: ./src/lib/viewers/media/waveform/createWaveformLoader.ts + 1 modules
36182
+ var createWaveformLoader = __webpack_require__(4971);
36183
+ // EXTERNAL MODULE: ./src/lib/viewers/media/waveform/validateWaveformPayload.ts
36184
+ var validateWaveformPayload = __webpack_require__(1053);
35712
36185
  ;// ./src/lib/viewers/media/MP3.scss
35713
36186
  // extracted by mini-css-extract-plugin
35714
36187
 
@@ -35723,6 +36196,11 @@ function MP3Viewer_toPrimitive(t, r) { if ("object" != typeof t || !t) return t;
35723
36196
 
35724
36197
 
35725
36198
 
36199
+
36200
+
36201
+
36202
+
36203
+
35726
36204
  const CSS_CLASS_MP3 = 'bp-media-mp3';
35727
36205
  function createLoadFailedError(message) {
35728
36206
  const error = new Error(message);
@@ -35738,7 +36216,7 @@ class MP3Viewer extends media_MediaBaseViewer {
35738
36216
  MP3Viewer_defineProperty(this, "handlePlayRequest", () => {
35739
36217
  this.userRequestedPlay = true;
35740
36218
  this.togglePlay();
35741
- if (this.isWaveformDecodeRetryPending && !this.hasUsedWaveformDecodePlayRetry) {
36219
+ if (this.shouldRaceClientWaveformDecode() && this.isWaveformDecodeRetryPending && !this.hasUsedWaveformDecodePlayRetry) {
35742
36220
  this.hasUsedWaveformDecodePlayRetry = true;
35743
36221
  this.isWaveformDecodeRetryPending = false;
35744
36222
  this.startClientWaveformDecode();
@@ -35775,11 +36253,11 @@ class MP3Viewer extends media_MediaBaseViewer {
35775
36253
  });
35776
36254
  MP3Viewer_defineProperty(this, "handleCommentMarkerClick", marker => {
35777
36255
  this.hostSelectedMarkerId = marker.id;
35778
- this.pendingHostSelectedSeek = null;
36256
+ this.pendingHostSelectedSeek = marker;
35779
36257
  if (this.mediaEl) {
35780
36258
  this.mediaEl.pause();
35781
- this.mediaEl.currentTime = marker.time;
35782
36259
  }
36260
+ this.applyPendingHostSelectedSeek();
35783
36261
  // Overlay paints the ring from click optimism until the host re-emits selected.
35784
36262
  this.emit('comment_marker_select', {
35785
36263
  id: marker.id,
@@ -35811,11 +36289,16 @@ class MP3Viewer extends media_MediaBaseViewer {
35811
36289
  this.wrapperEl.classList.add(CSS_CLASS_MP3);
35812
36290
  this.isAudioPlayerV2 = this.getIsAudioPlayerV2();
35813
36291
  this.waveformPeaks = [];
36292
+ this.waveformPeaksSource = null;
36293
+ this.waveformDurationSec = 0;
35814
36294
  if (this.isAudioPlayerV2) {
35815
36295
  this.wrapperEl.classList.add('bp-media--v2');
35816
36296
  this.mediaContainerEl.classList.add('bp-media-container--v2');
35817
36297
  this.ensureV2Controls();
35818
36298
  this.importWaveformDecode();
36299
+ // Listen on the loading shell. The waveform is on
36300
+ // screen before the audio blob is playable; getViewer() is still null.
36301
+ this.bindCommentMarkersListener();
35819
36302
  }
35820
36303
 
35821
36304
  // Audio element
@@ -35823,6 +36306,13 @@ class MP3Viewer extends media_MediaBaseViewer {
35823
36306
  this.mediaEl.setAttribute('preload', 'auto');
35824
36307
  this.commentMarkers = [];
35825
36308
  }
36309
+ bindCommentMarkersListener() {
36310
+ if (!this.isAudioPlayerV2) {
36311
+ return;
36312
+ }
36313
+ this.removeListener('comment_markers', this.handleCommentMarkersUpdated);
36314
+ this.addListener('comment_markers', this.handleCommentMarkersUpdated);
36315
+ }
35826
36316
 
35827
36317
  /**
35828
36318
  * Audio player v2 (waveform). Off unless the host enables `audioPlayerV2`
@@ -35831,7 +36321,7 @@ class MP3Viewer extends media_MediaBaseViewer {
35831
36321
  * @return {boolean} whether v2 treatment is on
35832
36322
  */
35833
36323
  getIsAudioPlayerV2() {
35834
- return this.featureEnabled('audioPlayerV2.enabled') && this.useReactControls();
36324
+ return this.featureEnabled(constants/* AUDIO_PLAYER_V2 */.Ku) && this.useReactControls();
35835
36325
  }
35836
36326
 
35837
36327
  /**
@@ -35863,6 +36353,7 @@ class MP3Viewer extends media_MediaBaseViewer {
35863
36353
  */
35864
36354
  fallbackToV1Controls() {
35865
36355
  this.isAudioPlayerV2 = false;
36356
+ this.abortWaveformLoads();
35866
36357
  if (this.wrapperEl) {
35867
36358
  this.wrapperEl.classList.remove('bp-media--v2');
35868
36359
  }
@@ -35875,7 +36366,7 @@ class MP3Viewer extends media_MediaBaseViewer {
35875
36366
  * @return {Promise<{ default: Function }>} MP3ControlsV2 module
35876
36367
  */
35877
36368
  importV2Controls() {
35878
- return Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 6180));
36369
+ return Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 2668));
35879
36370
  }
35880
36371
 
35881
36372
  /**
@@ -35883,7 +36374,7 @@ class MP3Viewer extends media_MediaBaseViewer {
35883
36374
  */
35884
36375
  async importWaveformDecode() {
35885
36376
  if (!this.waveformDecodeImport) {
35886
- this.waveformDecodeImport = Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 546));
36377
+ this.waveformDecodeImport = Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 8296));
35887
36378
  }
35888
36379
  try {
35889
36380
  return await this.waveformDecodeImport;
@@ -35898,7 +36389,7 @@ class MP3Viewer extends media_MediaBaseViewer {
35898
36389
  */
35899
36390
  destroy() {
35900
36391
  this.removeListener('comment_markers', this.handleCommentMarkersUpdated);
35901
- this.abortClientWaveformDecode();
36392
+ this.abortWaveformLoads();
35902
36393
  super.destroy();
35903
36394
  }
35904
36395
 
@@ -35908,6 +36399,7 @@ class MP3Viewer extends media_MediaBaseViewer {
35908
36399
  load() {
35909
36400
  if (this.isAudioPlayerV2) {
35910
36401
  this.showAudioLoadingShell();
36402
+ this.startConversionWaveformLoad();
35911
36403
  }
35912
36404
  return super.load();
35913
36405
  }
@@ -35945,6 +36437,9 @@ class MP3Viewer extends media_MediaBaseViewer {
35945
36437
  * @inheritdoc
35946
36438
  */
35947
36439
  loadeddataHandler() {
36440
+ if (this.isAudioPlayerV2) {
36441
+ this.dropConversionPeaksOnDurationMismatch();
36442
+ }
35948
36443
  super.loadeddataHandler();
35949
36444
  if (!this.isAudioPlayerV2) {
35950
36445
  return;
@@ -35955,7 +36450,173 @@ class MP3Viewer extends media_MediaBaseViewer {
35955
36450
  if (this.userRequestedPlay) {
35956
36451
  this.play();
35957
36452
  }
35958
- this.startClientWaveformDecode();
36453
+ if (this.shouldRaceClientWaveformDecode()) {
36454
+ this.startClientWaveformDecode();
36455
+ }
36456
+ }
36457
+
36458
+ /**
36459
+ * Client decode races conversion until some source applies peaks. Size and
36460
+ * duration caps still apply. First source to apply peaks wins; later
36461
+ * results are ignored.
36462
+ *
36463
+ * @return {boolean}
36464
+ */
36465
+ shouldRaceClientWaveformDecode() {
36466
+ if (!this.isAudioPlayerV2 || this.destroyed || this.waveformPeaksSource) {
36467
+ return false;
36468
+ }
36469
+ const compressedBytes = this.options.file?.size;
36470
+ const durationSec = this.mediaEl?.duration;
36471
+ return (0,validateWaveformPayload/* isPositiveFinite */.Ze)(compressedBytes) && compressedBytes <= waveform_constants/* CLIENT_DECODE_MAX_COMPRESSED_BYTES */.EB && (0,validateWaveformPayload/* isPositiveFinite */.Ze)(durationSec) && durationSec <= waveform_constants/* CLIENT_DECODE_MAX_DURATION_SEC */.nG;
36472
+ }
36473
+
36474
+ /**
36475
+ * @param {number} durationSec
36476
+ * @return {boolean}
36477
+ */
36478
+ isConversionDurationMismatch(durationSec) {
36479
+ const mediaDuration = this.mediaEl?.duration;
36480
+ return (0,validateWaveformPayload/* isPositiveFinite */.Ze)(mediaDuration) && (0,validateWaveformPayload/* isPositiveFinite */.Ze)(durationSec) && Math.abs(durationSec - mediaDuration) > waveform_constants/* DURATION_MISMATCH_TOLERANCE_SEC */.GQ;
36481
+ }
36482
+
36483
+ /**
36484
+ * Conversion JSON fetched before metadata skipped duration matching. Drop
36485
+ * those peaks once media duration is known if they disagree.
36486
+ *
36487
+ * @return {void}
36488
+ */
36489
+ dropConversionPeaksOnDurationMismatch() {
36490
+ if (this.waveformPeaksSource !== 'conversion' || !this.isConversionDurationMismatch(this.waveformDurationSec)) {
36491
+ return;
36492
+ }
36493
+ this.waveformPeaks = [];
36494
+ this.waveformPeaksSource = null;
36495
+ this.waveformDurationSec = 0;
36496
+ this.renderUI();
36497
+ }
36498
+
36499
+ /**
36500
+ * Poll the conversion waveform representation and fetch its JSON.
36501
+ * Missing, failed, or unloadable conversion falls back to client decode
36502
+ * when the file is under size and duration caps.
36503
+ *
36504
+ * @return {Promise<void>}
36505
+ */
36506
+ async startConversionWaveformLoad() {
36507
+ if (!this.isAudioPlayerV2 || this.destroyed) {
36508
+ return;
36509
+ }
36510
+ const {
36511
+ file
36512
+ } = this.options;
36513
+ const waveform = file?.representations?.entries ? getRepresentation(file, constants/* WAVEFORM_REP_NAME */.u4) : null;
36514
+ const template = waveform?.content?.url_template;
36515
+ const conversionStatus = waveform?.status;
36516
+ const statusState = conversionStatus && typeof conversionStatus === 'object' ? conversionStatus.state : null;
36517
+ if (!waveform || !template || !statusState || statusState === constants/* STATUS_ERROR */.uD) {
36518
+ this.startClientWaveformDecode();
36519
+ return;
36520
+ }
36521
+ this.abortConversionWaveformLoad();
36522
+ this.waveformStatus = this.getRepStatus(waveform);
36523
+ this.waveformStatus.removeListener('conversionpending', this.resetLoadTimeout);
36524
+ const status = this.waveformStatus;
36525
+ try {
36526
+ await status.getPromise();
36527
+ if (this.destroyed || this.waveformStatus !== status || this.waveformPeaksSource) {
36528
+ return;
36529
+ }
36530
+ const result = await this.loadConversionWaveformPayload(template);
36531
+ this.handleConversionWaveformResult(result, status);
36532
+ } catch {
36533
+ this.startClientWaveformDecode();
36534
+ }
36535
+ }
36536
+
36537
+ /**
36538
+ * Fetch and validate conversion waveform JSON after the rep is ready.
36539
+ * `{+asset_path}` is empty. Auth is a header, not a query token.
36540
+ *
36541
+ * @param {string} template - content URL template
36542
+ * @return {Promise<import('./waveform/types').WaveformLoadState>}
36543
+ */
36544
+ loadConversionWaveformPayload(template) {
36545
+ const url = this.createContentUrlV2(template);
36546
+ const durationSec = this.mediaEl?.duration;
36547
+ const source = (0,createWaveformLoader/* createWaveformLoader */.nR)(signal => this.api.get(url, {
36548
+ headers: this.appendAuthHeader(),
36549
+ signal
36550
+ }), Number.isFinite(durationSec) ? {
36551
+ expectedDurationSec: durationSec
36552
+ } : {});
36553
+ this.waveformSource = source;
36554
+ return source.load();
36555
+ }
36556
+ abortWaveformLoads() {
36557
+ this.abortConversionWaveformLoad();
36558
+ this.abortClientWaveformDecode();
36559
+ }
36560
+ abortConversionWaveformLoad() {
36561
+ if (this.waveformSource) {
36562
+ this.waveformSource.abort();
36563
+ this.waveformSource = null;
36564
+ }
36565
+ if (this.waveformStatus) {
36566
+ this.waveformStatus.destroy();
36567
+ this.waveformStatus = null;
36568
+ }
36569
+ }
36570
+
36571
+ /**
36572
+ * First source to apply peaks wins. Later results are ignored.
36573
+ *
36574
+ * @param {'conversion'|'client'} source
36575
+ * @param {{ peaks: ArrayLike<number>, durationSec?: number }} payload
36576
+ * @return {boolean} true when this source claimed peaks
36577
+ */
36578
+ applyWaveformPeaks(source, payload) {
36579
+ if (this.waveformPeaksSource) {
36580
+ return false;
36581
+ }
36582
+ this.waveformPeaks = payload.peaks;
36583
+ this.waveformPeaksSource = source;
36584
+ this.isWaveformDecodeRetryPending = false;
36585
+ if (source === 'conversion') {
36586
+ this.waveformDurationSec = payload.durationSec;
36587
+ this.abortClientWaveformDecode();
36588
+ } else if (this.waveformSource) {
36589
+ // RepStatus.destroy() does not reject getPromise().
36590
+ this.waveformSource.abort();
36591
+ this.waveformSource = null;
36592
+ }
36593
+ this.renderUI();
36594
+ return true;
36595
+ }
36596
+
36597
+ /**
36598
+ * Apply conversion peaks when the load is still current. Ignored if client
36599
+ * decode already applied peaks.
36600
+ *
36601
+ * @param {import('./waveform/types').WaveformLoadState} result
36602
+ * @param {Object} status - RepStatus instance that started this load
36603
+ * @return {void}
36604
+ */
36605
+ handleConversionWaveformResult(result, status) {
36606
+ if (!result || this.destroyed || !this.isAudioPlayerV2 || this.waveformStatus !== status) {
36607
+ return;
36608
+ }
36609
+ if (result.status !== 'ready') {
36610
+ if (result.status !== 'cancelled') {
36611
+ this.startClientWaveformDecode();
36612
+ }
36613
+ return;
36614
+ }
36615
+ if (this.isConversionDurationMismatch(result.payload.durationSec)) {
36616
+ this.startClientWaveformDecode();
36617
+ return;
36618
+ }
36619
+ this.applyWaveformPeaks('conversion', result.payload);
35959
36620
  }
35960
36621
  /**
35961
36622
  * Fetch compressed audio bytes for client decode. Prefers an already-fetched blob URL.
@@ -35996,16 +36657,19 @@ class MP3Viewer extends media_MediaBaseViewer {
35996
36657
  }
35997
36658
 
35998
36659
  /**
35999
- * Decode peaks in the background when the file is under size and duration caps.
36000
- * Does not block playback. Capped or failed decode keeps the placeholder waveform.
36660
+ * Decode peaks in the background until conversion or client decode applies
36661
+ * them, when the file is under size and duration caps. Does not block
36662
+ * playback. First source to apply peaks wins.
36001
36663
  *
36002
36664
  * @return {Promise<void>}
36003
36665
  */
36004
36666
  async startClientWaveformDecode() {
36005
- if (!this.isAudioPlayerV2 || this.destroyed) {
36667
+ if (!this.shouldRaceClientWaveformDecode()) {
36668
+ return;
36669
+ }
36670
+ if (this.waveformDecodeController && !this.waveformDecodeController.signal.aborted) {
36006
36671
  return;
36007
36672
  }
36008
- this.abortClientWaveformDecode();
36009
36673
  const controller = new AbortController();
36010
36674
  this.waveformDecodeController = controller;
36011
36675
  const {
@@ -36054,9 +36718,7 @@ class MP3Viewer extends media_MediaBaseViewer {
36054
36718
  return;
36055
36719
  }
36056
36720
  if (result.status === 'ready') {
36057
- this.waveformPeaks = result.payload.peaks;
36058
- this.isWaveformDecodeRetryPending = false;
36059
- this.renderUI();
36721
+ this.applyWaveformPeaks('client', result.payload);
36060
36722
  return;
36061
36723
  }
36062
36724
  if (result.status === 'cancelled') {
@@ -36086,10 +36748,7 @@ class MP3Viewer extends media_MediaBaseViewer {
36086
36748
  containerEl: this.mediaContainerEl
36087
36749
  });
36088
36750
  }
36089
- if (this.isAudioPlayerV2) {
36090
- this.removeListener('comment_markers', this.handleCommentMarkersUpdated);
36091
- this.addListener('comment_markers', this.handleCommentMarkersUpdated);
36092
- }
36751
+ this.bindCommentMarkersListener();
36093
36752
  this.renderUI();
36094
36753
  }
36095
36754
  /**
@@ -36117,11 +36776,13 @@ class MP3Viewer extends media_MediaBaseViewer {
36117
36776
  if (!this.controls) {
36118
36777
  return;
36119
36778
  }
36779
+ const mediaDuration = this.mediaEl.duration;
36780
+ const durationTime = typeof mediaDuration === 'number' && mediaDuration > 0 ? mediaDuration : this.waveformDurationSec;
36120
36781
  const sharedProps = {
36121
36782
  autoplay: this.isAutoplayEnabled(),
36122
36783
  bufferedRange: this.mediaEl.buffered,
36123
36784
  currentTime: this.mediaEl.currentTime,
36124
- durationTime: this.mediaEl.duration,
36785
+ durationTime,
36125
36786
  isPlaying: !this.mediaEl.paused,
36126
36787
  movePlayback: this.movePlayback,
36127
36788
  onAutoplayChange: this.setAutoplay,
@@ -54353,8 +55014,12 @@ class Preview extends (events_default()) {
54353
55014
  if (isDash && isFeatureEnabled(this.options.features, constants/* AI_TRANSCRIPTION_FOR_VIDEO_SUBTITLES */.Iu)) {
54354
55015
  videoHint += constants/* X_REP_HINT_VIDEO_DASH_EXTRACTED_TEXT */.xU;
54355
55016
  }
55017
+ let hints = `${constants/* X_REP_HINT_BASE */.pl}${constants/* X_REP_HINT_DOC_THUMBNAIL */.nC}${constants/* X_REP_HINT_IMAGE */.h3}${videoHint}`;
55018
+ if (isFeatureEnabled(this.options.features, constants/* AUDIO_PLAYER_V2 */.Ku)) {
55019
+ hints += constants/* X_REP_HINT_WAVEFORM */.jr;
55020
+ }
54356
55021
  const headers = {
54357
- 'X-Rep-Hints': `${constants/* X_REP_HINT_BASE */.pl}${constants/* X_REP_HINT_DOC_THUMBNAIL */.nC}${constants/* X_REP_HINT_IMAGE */.h3}${videoHint}`
55022
+ 'X-Rep-Hints': hints
54358
55023
  };
54359
55024
  return (0,util/* getHeaders */.dJ)(headers, token || this.options.token, this.options.sharedLink, this.options.sharedLinkPassword);
54360
55025
  }