@scarlett-player/embed 0.5.2 → 0.5.3

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.
@@ -123,12 +123,44 @@ function setupHlsEventHandlers(hls, api, callbacks) {
123
123
  addHandler("hlsLevelLoaded", (_event, data) => {
124
124
  if (data.details?.live !== void 0) {
125
125
  api.setState("live", data.details.live);
126
+ if (data.details.live) {
127
+ const video = hls.media;
128
+ if (video && video.seekable && video.seekable.length > 0) {
129
+ const start = video.seekable.start(0);
130
+ const end = video.seekable.end(video.seekable.length - 1);
131
+ api.setState("seekableRange", { start, end });
132
+ const threshold = (data.details.targetduration ?? 3) * 3;
133
+ const isAtLiveEdge = end - video.currentTime < threshold;
134
+ api.setState("liveEdge", isAtLiveEdge);
135
+ const latency = end - video.currentTime;
136
+ api.setState("liveLatency", Math.max(0, latency));
137
+ }
138
+ }
126
139
  callbacks.onLiveUpdate?.();
127
140
  }
128
141
  });
129
142
  addHandler("hlsError", (_event, data) => {
130
143
  const error = parseHlsError(data);
131
- api.logger.warn("HLS error", { error });
144
+ const isBufferHoleSeek = !error.fatal && (error.details?.includes("bufferStalledError") || data.reason?.includes("buffer holes"));
145
+ if (isBufferHoleSeek) {
146
+ api.logger.debug(`HLS buffer recovery: ${error.reason || error.details}`, {
147
+ details: error.details,
148
+ reason: error.reason
149
+ });
150
+ } else if (error.fatal) {
151
+ api.logger.error(`HLS fatal error: ${error.details} (type=${error.type})`, {
152
+ type: error.type,
153
+ details: error.details,
154
+ url: error.url
155
+ });
156
+ } else {
157
+ api.logger.warn(`HLS error: ${error.details} (type=${error.type}, fatal=${error.fatal})`, {
158
+ type: error.type,
159
+ details: error.details,
160
+ fatal: error.fatal,
161
+ url: error.url
162
+ });
163
+ }
132
164
  callbacks.onError?.(error);
133
165
  });
134
166
  return () => {
@@ -150,6 +182,8 @@ function setupVideoEventHandlers(video, api) {
150
182
  addHandler("playing", () => {
151
183
  api.setState("playing", true);
152
184
  api.setState("paused", false);
185
+ api.setState("waiting", false);
186
+ api.setState("buffering", false);
153
187
  api.setState("playbackState", "playing");
154
188
  });
155
189
  addHandler("pause", () => {
@@ -166,6 +200,15 @@ function setupVideoEventHandlers(video, api) {
166
200
  addHandler("timeupdate", () => {
167
201
  api.setState("currentTime", video.currentTime);
168
202
  api.emit("playback:timeupdate", { currentTime: video.currentTime });
203
+ const isLive = api.getState("live");
204
+ if (isLive && video.seekable && video.seekable.length > 0) {
205
+ const start = video.seekable.start(0);
206
+ const end = video.seekable.end(video.seekable.length - 1);
207
+ api.setState("seekableRange", { start, end });
208
+ const isAtLiveEdge = end - video.currentTime < 10;
209
+ api.setState("liveEdge", isAtLiveEdge);
210
+ api.setState("liveLatency", Math.max(0, end - video.currentTime));
211
+ }
169
212
  });
170
213
  addHandler("durationchange", () => {
171
214
  api.setState("duration", video.duration || 0);
@@ -5259,8 +5302,9 @@ class ScarlettPlayer {
5259
5302
  */
5260
5303
  setPlaybackRate(rate) {
5261
5304
  this.checkDestroyed();
5262
- this.stateManager.set("playbackRate", rate);
5263
- this.eventBus.emit("playback:ratechange", { rate });
5305
+ const clampedRate = Math.max(0.0625, Math.min(16, rate));
5306
+ this.stateManager.set("playbackRate", clampedRate);
5307
+ this.eventBus.emit("playback:ratechange", { rate: clampedRate });
5264
5308
  }
5265
5309
  /**
5266
5310
  * Set autoplay state.
@@ -5389,6 +5433,13 @@ class ScarlettPlayer {
5389
5433
  }
5390
5434
  const provider = this._currentProvider;
5391
5435
  if (typeof provider.setLevel === "function") {
5436
+ if (index !== -1) {
5437
+ const levels = this.getQualities();
5438
+ if (levels.length > 0 && (index < 0 || index >= levels.length)) {
5439
+ this.logger.warn(`Invalid quality index: ${index} (available: ${levels.length})`);
5440
+ return;
5441
+ }
5442
+ }
5392
5443
  provider.setLevel(index);
5393
5444
  this.eventBus.emit("quality:change", {
5394
5445
  quality: index === -1 ? "auto" : `level-${index}`,
@@ -5629,18 +5680,40 @@ class ScarlettPlayer {
5629
5680
  * @private
5630
5681
  */
5631
5682
  detectMimeType(source) {
5632
- const ext = source.split(".").pop()?.toLowerCase();
5683
+ let path = source;
5684
+ try {
5685
+ path = new URL(source).pathname;
5686
+ } catch {
5687
+ const noQuery = source.split("?")[0] ?? source;
5688
+ path = noQuery.split("#")[0] ?? noQuery;
5689
+ }
5690
+ const ext = path.split(".").pop()?.toLowerCase() ?? "";
5633
5691
  switch (ext) {
5634
5692
  case "m3u8":
5635
5693
  return "application/x-mpegURL";
5636
5694
  case "mpd":
5637
5695
  return "application/dash+xml";
5638
5696
  case "mp4":
5697
+ case "m4v":
5639
5698
  return "video/mp4";
5640
5699
  case "webm":
5641
5700
  return "video/webm";
5642
5701
  case "ogg":
5702
+ case "ogv":
5643
5703
  return "video/ogg";
5704
+ case "mov":
5705
+ return "video/quicktime";
5706
+ case "mkv":
5707
+ return "video/x-matroska";
5708
+ case "mp3":
5709
+ return "audio/mpeg";
5710
+ case "wav":
5711
+ return "audio/wav";
5712
+ case "flac":
5713
+ return "audio/flac";
5714
+ case "aac":
5715
+ case "m4a":
5716
+ return "audio/mp4";
5644
5717
  default:
5645
5718
  return "video/mp4";
5646
5719
  }