@scarlett-player/embed 0.5.2 → 1.0.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.
@@ -43,6 +43,18 @@ function mapLevels(levels, _currentLevel) {
43
43
  codec: level.codecSet
44
44
  }));
45
45
  }
46
+ function getInitialBandwidthEstimate(overrideBps) {
47
+ const HLS_DEFAULT_ESTIMATE = 5e5;
48
+ if (overrideBps !== void 0 && overrideBps > 0) {
49
+ return overrideBps;
50
+ }
51
+ const connection = navigator.connection;
52
+ if (connection?.downlink && connection.downlink > 0) {
53
+ const bps = connection.downlink * 1e6;
54
+ return Math.round(bps * 0.85);
55
+ }
56
+ return HLS_DEFAULT_ESTIMATE;
57
+ }
46
58
  var HLS_ERROR_TYPES = {
47
59
  NETWORK_ERROR: "networkError",
48
60
  MEDIA_ERROR: "mediaError",
@@ -113,6 +125,14 @@ function setupHlsEventHandlers(hls, api, callbacks) {
113
125
  });
114
126
  callbacks.onLevelSwitched?.(data.level);
115
127
  });
128
+ let lastBandwidthUpdate = 0;
129
+ addHandler("hlsFragLoaded", () => {
130
+ const now = Date.now();
131
+ if (now - lastBandwidthUpdate >= 2e3 && hls.bandwidthEstimate) {
132
+ lastBandwidthUpdate = now;
133
+ api.setState("bandwidth", Math.round(hls.bandwidthEstimate));
134
+ }
135
+ });
116
136
  addHandler("hlsFragBuffered", () => {
117
137
  api.setState("buffering", false);
118
138
  callbacks.onBufferUpdate?.();
@@ -123,12 +143,44 @@ function setupHlsEventHandlers(hls, api, callbacks) {
123
143
  addHandler("hlsLevelLoaded", (_event, data) => {
124
144
  if (data.details?.live !== void 0) {
125
145
  api.setState("live", data.details.live);
146
+ if (data.details.live) {
147
+ const video = hls.media;
148
+ if (video && video.seekable && video.seekable.length > 0) {
149
+ const start = video.seekable.start(0);
150
+ const end = video.seekable.end(video.seekable.length - 1);
151
+ api.setState("seekableRange", { start, end });
152
+ const threshold = (data.details.targetduration ?? 3) * 3;
153
+ const isAtLiveEdge = end - video.currentTime < threshold;
154
+ api.setState("liveEdge", isAtLiveEdge);
155
+ const latency = end - video.currentTime;
156
+ api.setState("liveLatency", Math.max(0, latency));
157
+ }
158
+ }
126
159
  callbacks.onLiveUpdate?.();
127
160
  }
128
161
  });
129
162
  addHandler("hlsError", (_event, data) => {
130
163
  const error = parseHlsError(data);
131
- api.logger.warn("HLS error", { error });
164
+ const isBufferHoleSeek = !error.fatal && (error.details?.includes("bufferStalledError") || data.reason?.includes("buffer holes"));
165
+ if (isBufferHoleSeek) {
166
+ api.logger.debug(`HLS buffer recovery: ${error.reason || error.details}`, {
167
+ details: error.details,
168
+ reason: error.reason
169
+ });
170
+ } else if (error.fatal) {
171
+ api.logger.error(`HLS fatal error: ${error.details} (type=${error.type})`, {
172
+ type: error.type,
173
+ details: error.details,
174
+ url: error.url
175
+ });
176
+ } else {
177
+ api.logger.warn(`HLS error: ${error.details} (type=${error.type}, fatal=${error.fatal})`, {
178
+ type: error.type,
179
+ details: error.details,
180
+ fatal: error.fatal,
181
+ url: error.url
182
+ });
183
+ }
132
184
  callbacks.onError?.(error);
133
185
  });
134
186
  return () => {
@@ -150,6 +202,8 @@ function setupVideoEventHandlers(video, api) {
150
202
  addHandler("playing", () => {
151
203
  api.setState("playing", true);
152
204
  api.setState("paused", false);
205
+ api.setState("waiting", false);
206
+ api.setState("buffering", false);
153
207
  api.setState("playbackState", "playing");
154
208
  });
155
209
  addHandler("pause", () => {
@@ -166,6 +220,15 @@ function setupVideoEventHandlers(video, api) {
166
220
  addHandler("timeupdate", () => {
167
221
  api.setState("currentTime", video.currentTime);
168
222
  api.emit("playback:timeupdate", { currentTime: video.currentTime });
223
+ const isLive = api.getState("live");
224
+ if (isLive && video.seekable && video.seekable.length > 0) {
225
+ const start = video.seekable.start(0);
226
+ const end = video.seekable.end(video.seekable.length - 1);
227
+ api.setState("seekableRange", { start, end });
228
+ const isAtLiveEdge = end - video.currentTime < 10;
229
+ api.setState("liveEdge", isAtLiveEdge);
230
+ api.setState("liveLatency", Math.max(0, end - video.currentTime));
231
+ }
169
232
  });
170
233
  addHandler("durationchange", () => {
171
234
  api.setState("duration", video.duration || 0);
@@ -312,6 +375,7 @@ var DEFAULT_CONFIG$3 = {
312
375
  maxMaxBufferLength: 600,
313
376
  backBufferLength: 30,
314
377
  enableWorker: true,
378
+ capLevelToPlayerSize: true,
315
379
  // Error recovery settings
316
380
  maxNetworkRetries: 3,
317
381
  maxMediaRetries: 2,
@@ -381,11 +445,13 @@ function createHLSPlugin(config) {
381
445
  startPosition: mergedConfig.startPosition,
382
446
  startLevel: -1,
383
447
  // Auto quality selection (ABR)
448
+ abrEwmaDefaultEstimate: getInitialBandwidthEstimate(mergedConfig.initialBandwidthEstimate),
384
449
  lowLatencyMode: mergedConfig.lowLatencyMode,
385
450
  maxBufferLength: mergedConfig.maxBufferLength,
386
451
  maxMaxBufferLength: mergedConfig.maxMaxBufferLength,
387
452
  backBufferLength: mergedConfig.backBufferLength,
388
453
  enableWorker: mergedConfig.enableWorker,
454
+ capLevelToPlayerSize: mergedConfig.capLevelToPlayerSize,
389
455
  // Minimize hls.js internal retries - we handle retries ourselves
390
456
  fragLoadingMaxRetry: 1,
391
457
  manifestLoadingMaxRetry: 1,
@@ -656,7 +722,10 @@ function createHLSPlugin(config) {
656
722
  currentSrc = src;
657
723
  api.setState("playbackState", "loading");
658
724
  api.setState("buffering", true);
659
- if (isHlsJsSupported()) {
725
+ if (api.getState("airplayActive") && supportsNativeHLS()) {
726
+ api.logger.info("Using native HLS (AirPlay active)");
727
+ await loadNative(src);
728
+ } else if (isHlsJsSupported()) {
660
729
  api.logger.info("Using hls.js for HLS playback");
661
730
  await loadWithHlsJs(src);
662
731
  } else if (supportsNativeHLS()) {
@@ -1518,7 +1587,9 @@ var DEFAULT_CONFIG$1 = {
1518
1587
  persist: false,
1519
1588
  persistKey: "scarlett-playlist",
1520
1589
  shuffle: false,
1521
- repeat: "none"
1590
+ repeat: "none",
1591
+ autoLoad: true,
1592
+ advanceDelay: 0
1522
1593
  };
1523
1594
  function generateId() {
1524
1595
  return `track-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
@@ -1662,6 +1733,9 @@ function createPlaylistPlugin(config) {
1662
1733
  }
1663
1734
  api?.setState("mediaType", track.type || "audio");
1664
1735
  emitChange();
1736
+ if (mergedConfig.autoLoad !== false && track.src) {
1737
+ api?.emit("media:load-request", { src: track.src, autoplay: true });
1738
+ }
1665
1739
  };
1666
1740
  const plugin = {
1667
1741
  id: "playlist",
@@ -1676,12 +1750,20 @@ function createPlaylistPlugin(config) {
1676
1750
  if (shuffle && tracks.length > 0) {
1677
1751
  generateShuffleOrder();
1678
1752
  }
1753
+ let advanceTimeout = null;
1679
1754
  const unsubEnded = api.on("playback:ended", () => {
1680
1755
  if (!mergedConfig.autoAdvance) return;
1681
1756
  const nextIdx = getNextIndex();
1682
1757
  if (nextIdx >= 0) {
1683
- api?.logger.debug("Auto-advancing to next track", { nextIdx });
1684
- setCurrentTrack(nextIdx);
1758
+ const advance = () => {
1759
+ api?.logger.debug("Auto-advancing to next track", { nextIdx });
1760
+ setCurrentTrack(nextIdx);
1761
+ };
1762
+ if (mergedConfig.advanceDelay) {
1763
+ advanceTimeout = setTimeout(advance, mergedConfig.advanceDelay);
1764
+ } else {
1765
+ advance();
1766
+ }
1685
1767
  } else {
1686
1768
  api?.logger.info("Playlist ended");
1687
1769
  api?.emit("playlist:ended", void 0);
@@ -1689,6 +1771,10 @@ function createPlaylistPlugin(config) {
1689
1771
  });
1690
1772
  api.onDestroy(() => {
1691
1773
  unsubEnded();
1774
+ if (advanceTimeout) {
1775
+ clearTimeout(advanceTimeout);
1776
+ advanceTimeout = null;
1777
+ }
1692
1778
  persistPlaylist();
1693
1779
  });
1694
1780
  },
@@ -3673,6 +3759,13 @@ class ScarlettPlayer {
3673
3759
  await this.pluginManager.initPlugin(id);
3674
3760
  }
3675
3761
  }
3762
+ this.eventBus.on("media:load-request", async ({ src, autoplay }) => {
3763
+ if (this.stateManager.getValue("chromecastActive")) return;
3764
+ await this.load(src);
3765
+ if (autoplay !== false) {
3766
+ await this.play();
3767
+ }
3768
+ });
3676
3769
  if (this.initialSrc) {
3677
3770
  await this.load(this.initialSrc);
3678
3771
  }
@@ -3880,8 +3973,9 @@ class ScarlettPlayer {
3880
3973
  */
3881
3974
  setPlaybackRate(rate) {
3882
3975
  this.checkDestroyed();
3883
- this.stateManager.set("playbackRate", rate);
3884
- this.eventBus.emit("playback:ratechange", { rate });
3976
+ const clampedRate = Math.max(0.0625, Math.min(16, rate));
3977
+ this.stateManager.set("playbackRate", clampedRate);
3978
+ this.eventBus.emit("playback:ratechange", { rate: clampedRate });
3885
3979
  }
3886
3980
  /**
3887
3981
  * Set autoplay state.
@@ -4010,6 +4104,13 @@ class ScarlettPlayer {
4010
4104
  }
4011
4105
  const provider = this._currentProvider;
4012
4106
  if (typeof provider.setLevel === "function") {
4107
+ if (index !== -1) {
4108
+ const levels = this.getQualities();
4109
+ if (levels.length > 0 && (index < 0 || index >= levels.length)) {
4110
+ this.logger.warn(`Invalid quality index: ${index} (available: ${levels.length})`);
4111
+ return;
4112
+ }
4113
+ }
4013
4114
  provider.setLevel(index);
4014
4115
  this.eventBus.emit("quality:change", {
4015
4116
  quality: index === -1 ? "auto" : `level-${index}`,
@@ -4250,18 +4351,40 @@ class ScarlettPlayer {
4250
4351
  * @private
4251
4352
  */
4252
4353
  detectMimeType(source) {
4253
- const ext = source.split(".").pop()?.toLowerCase();
4354
+ let path = source;
4355
+ try {
4356
+ path = new URL(source).pathname;
4357
+ } catch {
4358
+ const noQuery = source.split("?")[0] ?? source;
4359
+ path = noQuery.split("#")[0] ?? noQuery;
4360
+ }
4361
+ const ext = path.split(".").pop()?.toLowerCase() ?? "";
4254
4362
  switch (ext) {
4255
4363
  case "m3u8":
4256
4364
  return "application/x-mpegURL";
4257
4365
  case "mpd":
4258
4366
  return "application/dash+xml";
4259
4367
  case "mp4":
4368
+ case "m4v":
4260
4369
  return "video/mp4";
4261
4370
  case "webm":
4262
4371
  return "video/webm";
4263
4372
  case "ogg":
4373
+ case "ogv":
4264
4374
  return "video/ogg";
4375
+ case "mov":
4376
+ return "video/quicktime";
4377
+ case "mkv":
4378
+ return "video/x-matroska";
4379
+ case "mp3":
4380
+ return "audio/mpeg";
4381
+ case "wav":
4382
+ return "audio/wav";
4383
+ case "flac":
4384
+ return "audio/flac";
4385
+ case "aac":
4386
+ case "m4a":
4387
+ return "audio/mp4";
4265
4388
  default:
4266
4389
  return "video/mp4";
4267
4390
  }
@@ -4470,6 +4593,12 @@ async function createEmbedPlayer(container, config, pluginCreators2, availableTy
4470
4593
  artwork: config.artwork || config.poster || config.playlist?.[0]?.artwork
4471
4594
  }));
4472
4595
  }
4596
+ if (pluginCreators2.watermark && config.watermark) {
4597
+ plugins.push(pluginCreators2.watermark(config.watermark));
4598
+ }
4599
+ if (pluginCreators2.captions) {
4600
+ plugins.push(pluginCreators2.captions(config.captions || {}));
4601
+ }
4473
4602
  if (pluginCreators2.analytics && config.analytics?.beaconUrl) {
4474
4603
  plugins.push(pluginCreators2.analytics({
4475
4604
  beaconUrl: config.analytics.beaconUrl,
@@ -4587,7 +4716,7 @@ function setupAutoInit(pluginCreators2, availableTypes) {
4587
4716
  }
4588
4717
  }
4589
4718
  }
4590
- const VERSION = "0.3.0-audio";
4719
+ const VERSION = "0.5.3-audio";
4591
4720
  const AVAILABLE_TYPES = ["audio", "audio-mini"];
4592
4721
  const pluginCreators = {
4593
4722
  hls: createHLSPlugin,