@gentomiyano/optimized-web-audio-player 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -563,11 +563,14 @@ var BrowserMediaSessionController = class {
563
563
  }
564
564
  return;
565
565
  }
566
+ const intrinsicContext = item.contentContext;
567
+ const creatorName = item.creatorName ?? (intrinsicContext?.sourceType === "release-track" ? intrinsicContext.artistName : void 0);
568
+ const collectionTitle = item.collectionTitle ?? (intrinsicContext?.sourceType === "release-track" ? intrinsicContext.releaseTitle : intrinsicContext?.sourceType === "podcast-episode" ? intrinsicContext.programTitle : void 0);
566
569
  const identity = [
567
570
  item.id,
568
571
  item.title,
569
- item.creatorName ?? "",
570
- item.collectionTitle ?? "",
572
+ creatorName ?? "",
573
+ collectionTitle ?? "",
571
574
  item.artwork?.url ?? ""
572
575
  ].join("\0");
573
576
  if (identity === this.#metadataIdentity || this.#createMetadata === void 0) {
@@ -576,8 +579,8 @@ var BrowserMediaSessionController = class {
576
579
  try {
577
580
  session.metadata = this.#createMetadata({
578
581
  title: item.title,
579
- artist: item.creatorName ?? "",
580
- album: item.collectionTitle ?? "",
582
+ artist: creatorName ?? "",
583
+ album: collectionTitle ?? "",
581
584
  artwork: item.artwork === void 0 ? [] : [{ src: item.artwork.url }]
582
585
  });
583
586
  this.#metadataIdentity = identity;
@@ -1108,6 +1111,7 @@ var CorePlayerStore = class {
1108
1111
  #handlingEnded = false;
1109
1112
  #destroyed = false;
1110
1113
  #lastSavedPodcastPositions = /* @__PURE__ */ new Map();
1114
+ #pendingSeekRetrySec = null;
1111
1115
  #progressSaveChain = Promise.resolve();
1112
1116
  constructor(options) {
1113
1117
  this.#media = options.media;
@@ -1436,6 +1440,7 @@ var CorePlayerStore = class {
1436
1440
  return failed(INVALID_QUEUE_ERROR);
1437
1441
  }
1438
1442
  const config = this.#state.experienceConfig;
1443
+ this.#pendingSeekRetrySec = null;
1439
1444
  const replacingCurrent = this.#state.currentItem !== null;
1440
1445
  let transitionError = null;
1441
1446
  if (replacingCurrent) {
@@ -1662,13 +1667,24 @@ var CorePlayerStore = class {
1662
1667
  curve: fadeCurve
1663
1668
  });
1664
1669
  if (!this.#isCurrentCommand(version)) {
1665
- return;
1670
+ return OK_RESULT;
1666
1671
  }
1667
1672
  if (fadeOut.error !== null) {
1668
1673
  this.#setState({ error: fadeOut.error });
1669
1674
  }
1670
1675
  this.#setState({ status: "seeking" });
1671
- this.#media.seekTo(target);
1676
+ try {
1677
+ this.#media.seekTo(target);
1678
+ this.#pendingSeekRetrySec = null;
1679
+ } catch {
1680
+ const error = createPlayerError("seek", true);
1681
+ this.#pendingSeekRetrySec = target;
1682
+ this.#setState({
1683
+ status: this.#media.isPaused() ? "paused" : "playing",
1684
+ error
1685
+ });
1686
+ return failed(error);
1687
+ }
1672
1688
  this.#setState({ currentTimeSec: target });
1673
1689
  const fadeIn = await runFade(this.#gain, {
1674
1690
  target: 1,
@@ -1676,7 +1692,7 @@ var CorePlayerStore = class {
1676
1692
  curve: fadeCurve
1677
1693
  });
1678
1694
  if (!this.#isCurrentCommand(version)) {
1679
- return;
1695
+ return OK_RESULT;
1680
1696
  }
1681
1697
  if (fadeIn.error !== null) {
1682
1698
  this.#setState({ error: fadeIn.error });
@@ -1684,6 +1700,7 @@ var CorePlayerStore = class {
1684
1700
  this.#setState({
1685
1701
  status: this.#media.isPaused() ? "paused" : "playing"
1686
1702
  });
1703
+ return OK_RESULT;
1687
1704
  }
1688
1705
  #setVolume(volume) {
1689
1706
  if (this.#destroyed) {
@@ -1794,6 +1811,18 @@ var CorePlayerStore = class {
1794
1811
  if (this.#state.currentQueueIndex === null || this.#state.error === null || !this.#state.error.recoverable) {
1795
1812
  return failed(this.#state.error ?? NO_ITEM_ERROR);
1796
1813
  }
1814
+ if (this.#state.error.code === "seek" && this.#pendingSeekRetrySec !== null) {
1815
+ const target = this.#pendingSeekRetrySec;
1816
+ const version2 = this.#beginCommand(this.#desiredPlayback);
1817
+ const config = this.#state.experienceConfig;
1818
+ this.#setState({ error: null });
1819
+ return this.#performSeek(
1820
+ version2,
1821
+ target,
1822
+ config.seekFadeMs,
1823
+ config.fadeCurve
1824
+ );
1825
+ }
1797
1826
  const version = this.#beginCommand("playing");
1798
1827
  this.#setState({ error: null, needsUserGesture: false });
1799
1828
  return this.#selectQueueIndex(
@@ -1807,6 +1836,7 @@ var CorePlayerStore = class {
1807
1836
  return;
1808
1837
  }
1809
1838
  const version = this.#beginCommand("paused");
1839
+ this.#pendingSeekRetrySec = null;
1810
1840
  const config = this.#state.experienceConfig;
1811
1841
  const halted = await this.#halt(version, {
1812
1842
  fadeDurationMs: config.pauseFadeOutMs,
@@ -2891,6 +2921,9 @@ function TrackMetadata({
2891
2921
  scrollTitle = false,
2892
2922
  status
2893
2923
  }) {
2924
+ const intrinsicContext = item?.contentContext;
2925
+ const creatorName = item?.creatorName ?? (intrinsicContext?.sourceType === "release-track" ? intrinsicContext.artistName : void 0);
2926
+ const collectionTitle = item?.collectionTitle ?? (intrinsicContext?.sourceType === "release-track" ? intrinsicContext.releaseTitle : intrinsicContext?.sourceType === "podcast-episode" ? intrinsicContext.programTitle : void 0);
2894
2927
  return /* @__PURE__ */ jsxs(
2895
2928
  "div",
2896
2929
  {
@@ -2905,8 +2938,8 @@ function TrackMetadata({
2905
2938
  },
2906
2939
  item?.title ?? "No media selected"
2907
2940
  ),
2908
- /* @__PURE__ */ jsx("span", { className: "player-creator", children: item === null ? "Select media to begin" : item.creatorName ?? "Artist unavailable" }),
2909
- item?.collectionTitle === void 0 ? null : /* @__PURE__ */ jsx("span", { className: "player-collection", children: item.collectionTitle })
2941
+ /* @__PURE__ */ jsx("span", { className: "player-creator", children: item === null ? "Select media to begin" : creatorName ?? "Artist unavailable" }),
2942
+ collectionTitle === void 0 ? null : /* @__PURE__ */ jsx("span", { className: "player-collection", children: collectionTitle })
2910
2943
  ]
2911
2944
  }
2912
2945
  );
@@ -2951,100 +2984,6 @@ function IconButton({
2951
2984
  }
2952
2985
  );
2953
2986
  }
2954
- function SeekBar({
2955
- canSeek,
2956
- className,
2957
- commitMode,
2958
- currentTimeSec,
2959
- durationSec,
2960
- label = "Seek",
2961
- onSeek,
2962
- status
2963
- }) {
2964
- const duration = durationSec !== null && Number.isFinite(durationSec) && durationSec > 0 ? durationSec : 0;
2965
- const disabled = !canSeek || duration === 0 || status === "loading";
2966
- const safeCurrentTime = clamp(currentTimeSec, 0, duration);
2967
- const [previewTime, setPreviewTime] = useState(safeCurrentTime);
2968
- const previewProgress = duration === 0 ? 0 : clamp(previewTime / duration, 0, 1) * 100;
2969
- const isAdjusting = useRef(false);
2970
- const lastCommitted = useRef(null);
2971
- useEffect(() => {
2972
- if (!isAdjusting.current) {
2973
- setPreviewTime(safeCurrentTime);
2974
- }
2975
- }, [safeCurrentTime]);
2976
- function updatePreview(value) {
2977
- const nextValue = clamp(value, 0, duration);
2978
- isAdjusting.current = true;
2979
- setPreviewTime(nextValue);
2980
- if (commitMode === "live") {
2981
- onSeek(nextValue);
2982
- lastCommitted.current = nextValue;
2983
- }
2984
- }
2985
- function commitPreview(value = previewTime) {
2986
- const nextValue = clamp(value, 0, duration);
2987
- if (disabled || !isAdjusting.current || commitMode === "live" && lastCommitted.current === nextValue) {
2988
- isAdjusting.current = false;
2989
- return;
2990
- }
2991
- onSeek(nextValue);
2992
- lastCommitted.current = nextValue;
2993
- isAdjusting.current = false;
2994
- }
2995
- function cancelPreview() {
2996
- isAdjusting.current = false;
2997
- lastCommitted.current = null;
2998
- setPreviewTime(safeCurrentTime);
2999
- }
3000
- return /* @__PURE__ */ jsxs(
3001
- "label",
3002
- {
3003
- className: ["player-seek", className].filter(Boolean).join(" "),
3004
- "data-disabled": disabled ? "true" : "false",
3005
- children: [
3006
- /* @__PURE__ */ jsx("span", { className: "player-visually-hidden", children: label }),
3007
- /* @__PURE__ */ jsx(
3008
- "span",
3009
- {
3010
- "aria-hidden": "true",
3011
- className: "player-seek__progress",
3012
- style: { width: `${String(disabled ? 0 : previewProgress)}%` }
3013
- }
3014
- ),
3015
- /* @__PURE__ */ jsx(
3016
- "input",
3017
- {
3018
- "aria-label": label,
3019
- "aria-valuetext": formatTimeAria(previewTime),
3020
- disabled,
3021
- max: duration || 1,
3022
- min: 0,
3023
- onBlur: (event) => {
3024
- commitPreview(Number(event.currentTarget.value));
3025
- },
3026
- onChange: (event) => {
3027
- updatePreview(Number(event.currentTarget.value));
3028
- },
3029
- onKeyUp: (event) => {
3030
- commitPreview(Number(event.currentTarget.value));
3031
- },
3032
- onPointerDown: () => {
3033
- isAdjusting.current = true;
3034
- },
3035
- onPointerCancel: cancelPreview,
3036
- onPointerUp: (event) => {
3037
- commitPreview(Number(event.currentTarget.value));
3038
- },
3039
- step: 0.1,
3040
- type: "range",
3041
- value: disabled ? 0 : previewTime
3042
- }
3043
- )
3044
- ]
3045
- }
3046
- );
3047
- }
3048
2987
  var FALLBACK_PEAKS = [
3049
2988
  0.16,
3050
2989
  0.24,
@@ -3141,6 +3080,182 @@ function Waveform({
3141
3080
  }
3142
3081
  );
3143
3082
  }
3083
+ function CompactPlayerLayout({
3084
+ appearance,
3085
+ className,
3086
+ contextId,
3087
+ contextMode,
3088
+ currentTimeSec,
3089
+ durationSec,
3090
+ errorNotice,
3091
+ isActive,
3092
+ isPlaying,
3093
+ item,
3094
+ label,
3095
+ primaryControl,
3096
+ queueItemId,
3097
+ runtimeStatus,
3098
+ seekControl,
3099
+ status,
3100
+ trailingControl,
3101
+ waveformOpacity,
3102
+ waveformPlayedStateEnabled
3103
+ }) {
3104
+ const progress = durationSec === null || durationSec <= 0 ? 0 : currentTimeSec / durationSec;
3105
+ return /* @__PURE__ */ jsxs(
3106
+ PlayerSurface,
3107
+ {
3108
+ appearance,
3109
+ className: ["player-compact", className].filter(Boolean).join(" "),
3110
+ label,
3111
+ children: [
3112
+ /* @__PURE__ */ jsxs(
3113
+ "div",
3114
+ {
3115
+ className: "player-compact__main",
3116
+ ...isActive === void 0 ? {} : { "data-active": isActive ? "true" : "false" },
3117
+ "data-has-status": status === void 0 ? "false" : "true",
3118
+ "data-has-trailing-control": trailingControl === void 0 ? "false" : "true",
3119
+ "data-playback-context-id": contextId,
3120
+ "data-playback-context-mode": contextMode,
3121
+ "data-player-runtime-status": runtimeStatus,
3122
+ "data-queue-item-id": queueItemId,
3123
+ children: [
3124
+ primaryControl,
3125
+ /* @__PURE__ */ jsx(Artwork, { className: "player-compact__artwork", item }),
3126
+ /* @__PURE__ */ jsx(TrackMetadata, { item, scrollTitle: isPlaying }),
3127
+ /* @__PURE__ */ jsxs("div", { className: "player-compact__signal", children: [
3128
+ status,
3129
+ /* @__PURE__ */ jsxs(
3130
+ "div",
3131
+ {
3132
+ className: "player-compact__scrubber",
3133
+ "data-seek-enabled": seekControl === void 0 ? "false" : "true",
3134
+ children: [
3135
+ /* @__PURE__ */ jsx(
3136
+ Waveform,
3137
+ {
3138
+ opacity: waveformOpacity,
3139
+ playedStateEnabled: waveformPlayedStateEnabled,
3140
+ progress,
3141
+ waveform: item?.waveform
3142
+ }
3143
+ ),
3144
+ seekControl
3145
+ ]
3146
+ }
3147
+ ),
3148
+ /* @__PURE__ */ jsx(
3149
+ TimeReadout,
3150
+ {
3151
+ currentTimeSec,
3152
+ durationSec
3153
+ }
3154
+ )
3155
+ ] }),
3156
+ trailingControl
3157
+ ]
3158
+ }
3159
+ ),
3160
+ errorNotice
3161
+ ]
3162
+ }
3163
+ );
3164
+ }
3165
+ function SeekBar({
3166
+ canSeek,
3167
+ className,
3168
+ commitMode,
3169
+ currentTimeSec,
3170
+ durationSec,
3171
+ label = "Seek",
3172
+ onSeek,
3173
+ status
3174
+ }) {
3175
+ const duration = durationSec !== null && Number.isFinite(durationSec) && durationSec > 0 ? durationSec : 0;
3176
+ const disabled = !canSeek || duration === 0 || status === "loading";
3177
+ const safeCurrentTime = clamp(currentTimeSec, 0, duration);
3178
+ const [previewTime, setPreviewTime] = useState(safeCurrentTime);
3179
+ const previewProgress = duration === 0 ? 0 : clamp(previewTime / duration, 0, 1) * 100;
3180
+ const isAdjusting = useRef(false);
3181
+ const lastCommitted = useRef(null);
3182
+ useEffect(() => {
3183
+ if (!isAdjusting.current) {
3184
+ setPreviewTime(safeCurrentTime);
3185
+ }
3186
+ }, [safeCurrentTime]);
3187
+ function updatePreview(value) {
3188
+ const nextValue = clamp(value, 0, duration);
3189
+ isAdjusting.current = true;
3190
+ setPreviewTime(nextValue);
3191
+ if (commitMode === "live") {
3192
+ onSeek(nextValue);
3193
+ lastCommitted.current = nextValue;
3194
+ }
3195
+ }
3196
+ function commitPreview(value = previewTime) {
3197
+ const nextValue = clamp(value, 0, duration);
3198
+ if (disabled || !isAdjusting.current || commitMode === "live" && lastCommitted.current === nextValue) {
3199
+ isAdjusting.current = false;
3200
+ return;
3201
+ }
3202
+ onSeek(nextValue);
3203
+ lastCommitted.current = nextValue;
3204
+ isAdjusting.current = false;
3205
+ }
3206
+ function cancelPreview() {
3207
+ isAdjusting.current = false;
3208
+ lastCommitted.current = null;
3209
+ setPreviewTime(safeCurrentTime);
3210
+ }
3211
+ return /* @__PURE__ */ jsxs(
3212
+ "label",
3213
+ {
3214
+ className: ["player-seek", className].filter(Boolean).join(" "),
3215
+ "data-disabled": disabled ? "true" : "false",
3216
+ children: [
3217
+ /* @__PURE__ */ jsx("span", { className: "player-visually-hidden", children: label }),
3218
+ /* @__PURE__ */ jsx(
3219
+ "span",
3220
+ {
3221
+ "aria-hidden": "true",
3222
+ className: "player-seek__progress",
3223
+ style: { width: `${String(disabled ? 0 : previewProgress)}%` }
3224
+ }
3225
+ ),
3226
+ /* @__PURE__ */ jsx(
3227
+ "input",
3228
+ {
3229
+ "aria-label": label,
3230
+ "aria-valuetext": formatTimeAria(previewTime),
3231
+ disabled,
3232
+ max: duration || 1,
3233
+ min: 0,
3234
+ onBlur: (event) => {
3235
+ commitPreview(Number(event.currentTarget.value));
3236
+ },
3237
+ onChange: (event) => {
3238
+ updatePreview(Number(event.currentTarget.value));
3239
+ },
3240
+ onKeyUp: (event) => {
3241
+ commitPreview(Number(event.currentTarget.value));
3242
+ },
3243
+ onPointerDown: () => {
3244
+ isAdjusting.current = true;
3245
+ },
3246
+ onPointerCancel: cancelPreview,
3247
+ onPointerUp: (event) => {
3248
+ commitPreview(Number(event.currentTarget.value));
3249
+ },
3250
+ step: 0.1,
3251
+ type: "range",
3252
+ value: disabled ? 0 : previewTime
3253
+ }
3254
+ )
3255
+ ]
3256
+ }
3257
+ );
3258
+ }
3144
3259
  function BoundCompactPlayerView({
3145
3260
  appearance,
3146
3261
  className,
@@ -3151,77 +3266,48 @@ function BoundCompactPlayerView({
3151
3266
  runtimeStatus,
3152
3267
  state
3153
3268
  }) {
3154
- const progress = state.durationSec === null || state.durationSec <= 0 ? 0 : state.currentTimeSec / state.durationSec;
3155
3269
  return /* @__PURE__ */ jsx(
3156
- PlayerSurface,
3270
+ CompactPlayerLayout,
3157
3271
  {
3158
3272
  appearance,
3159
- className: ["player-bound-compact", className].filter(Boolean).join(" "),
3273
+ ...className === void 0 ? {} : { className },
3274
+ ...contextId === void 0 ? {} : { contextId },
3275
+ ...contextMode === void 0 ? {} : { contextMode },
3276
+ currentTimeSec: state.currentTimeSec,
3277
+ durationSec: state.durationSec,
3278
+ isActive: state.isActive,
3279
+ isPlaying: state.isPlaying,
3280
+ item: state.item,
3160
3281
  label: `Inline player: ${state.item.title}`,
3161
- children: /* @__PURE__ */ jsxs(
3162
- "div",
3282
+ primaryControl: /* @__PURE__ */ jsx(
3283
+ IconButton,
3163
3284
  {
3164
- className: "player-bound-compact__main",
3165
- "data-active": state.isActive ? "true" : "false",
3166
- "data-playback-context-id": contextId,
3167
- "data-playback-context-mode": contextMode,
3168
- "data-player-runtime-status": runtimeStatus,
3169
- "data-queue-item-id": state.queueItemId,
3170
- children: [
3171
- /* @__PURE__ */ jsx(
3172
- IconButton,
3173
- {
3174
- className: "player-play-button",
3175
- label: state.isPlaying ? "Pause" : "Play",
3176
- onClick: () => {
3177
- void commands.togglePlayback();
3178
- },
3179
- children: state.isPlaying ? /* @__PURE__ */ jsx(PauseIcon, { size: 22 }) : /* @__PURE__ */ jsx(PlayIcon, { size: 22 })
3180
- }
3181
- ),
3182
- /* @__PURE__ */ jsx(
3183
- Artwork,
3184
- {
3185
- className: "player-bound-compact__artwork",
3186
- item: state.item
3187
- }
3188
- ),
3189
- /* @__PURE__ */ jsx(TrackMetadata, { item: state.item, scrollTitle: state.isPlaying }),
3190
- /* @__PURE__ */ jsxs("div", { className: "player-bound-compact__signal", children: [
3191
- /* @__PURE__ */ jsx(
3192
- Waveform,
3193
- {
3194
- opacity: options.waveformOpacity,
3195
- playedStateEnabled: options.waveformPlayedStateEnabled,
3196
- progress,
3197
- waveform: state.item.waveform
3198
- }
3199
- ),
3200
- /* @__PURE__ */ jsx(
3201
- TimeReadout,
3202
- {
3203
- currentTimeSec: state.currentTimeSec,
3204
- durationSec: state.durationSec
3205
- }
3206
- )
3207
- ] }),
3208
- /* @__PURE__ */ jsx(
3209
- SeekBar,
3210
- {
3211
- canSeek: state.canSeek,
3212
- commitMode: options.seekCommitMode,
3213
- currentTimeSec: state.currentTimeSec,
3214
- durationSec: state.durationSec,
3215
- label: `Seek in ${state.item.title}`,
3216
- onSeek: (seconds) => {
3217
- commands.seekTo(seconds);
3218
- },
3219
- status: state.status
3220
- }
3221
- )
3222
- ]
3285
+ className: "player-play-button",
3286
+ label: state.isPlaying ? "Pause" : "Play",
3287
+ onClick: () => {
3288
+ void commands.togglePlayback();
3289
+ },
3290
+ children: state.isPlaying ? /* @__PURE__ */ jsx(PauseIcon, { size: 22 }) : /* @__PURE__ */ jsx(PlayIcon, { size: 22 })
3223
3291
  }
3224
- )
3292
+ ),
3293
+ queueItemId: state.queueItemId,
3294
+ ...runtimeStatus === void 0 ? {} : { runtimeStatus },
3295
+ seekControl: /* @__PURE__ */ jsx(
3296
+ SeekBar,
3297
+ {
3298
+ canSeek: state.canSeek,
3299
+ commitMode: options.seekCommitMode,
3300
+ currentTimeSec: state.currentTimeSec,
3301
+ durationSec: state.durationSec,
3302
+ label: `Seek in ${state.item.title}`,
3303
+ onSeek: (seconds) => {
3304
+ commands.seekTo(seconds);
3305
+ },
3306
+ status: state.status
3307
+ }
3308
+ ),
3309
+ waveformOpacity: options.waveformOpacity,
3310
+ waveformPlayedStateEnabled: options.waveformPlayedStateEnabled
3225
3311
  }
3226
3312
  );
3227
3313
  }