@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);
@@ -3880,8 +3923,9 @@ class ScarlettPlayer {
3880
3923
  */
3881
3924
  setPlaybackRate(rate) {
3882
3925
  this.checkDestroyed();
3883
- this.stateManager.set("playbackRate", rate);
3884
- this.eventBus.emit("playback:ratechange", { rate });
3926
+ const clampedRate = Math.max(0.0625, Math.min(16, rate));
3927
+ this.stateManager.set("playbackRate", clampedRate);
3928
+ this.eventBus.emit("playback:ratechange", { rate: clampedRate });
3885
3929
  }
3886
3930
  /**
3887
3931
  * Set autoplay state.
@@ -4010,6 +4054,13 @@ class ScarlettPlayer {
4010
4054
  }
4011
4055
  const provider = this._currentProvider;
4012
4056
  if (typeof provider.setLevel === "function") {
4057
+ if (index !== -1) {
4058
+ const levels = this.getQualities();
4059
+ if (levels.length > 0 && (index < 0 || index >= levels.length)) {
4060
+ this.logger.warn(`Invalid quality index: ${index} (available: ${levels.length})`);
4061
+ return;
4062
+ }
4063
+ }
4013
4064
  provider.setLevel(index);
4014
4065
  this.eventBus.emit("quality:change", {
4015
4066
  quality: index === -1 ? "auto" : `level-${index}`,
@@ -4250,18 +4301,40 @@ class ScarlettPlayer {
4250
4301
  * @private
4251
4302
  */
4252
4303
  detectMimeType(source) {
4253
- const ext = source.split(".").pop()?.toLowerCase();
4304
+ let path = source;
4305
+ try {
4306
+ path = new URL(source).pathname;
4307
+ } catch {
4308
+ const noQuery = source.split("?")[0] ?? source;
4309
+ path = noQuery.split("#")[0] ?? noQuery;
4310
+ }
4311
+ const ext = path.split(".").pop()?.toLowerCase() ?? "";
4254
4312
  switch (ext) {
4255
4313
  case "m3u8":
4256
4314
  return "application/x-mpegURL";
4257
4315
  case "mpd":
4258
4316
  return "application/dash+xml";
4259
4317
  case "mp4":
4318
+ case "m4v":
4260
4319
  return "video/mp4";
4261
4320
  case "webm":
4262
4321
  return "video/webm";
4263
4322
  case "ogg":
4323
+ case "ogv":
4264
4324
  return "video/ogg";
4325
+ case "mov":
4326
+ return "video/quicktime";
4327
+ case "mkv":
4328
+ return "video/x-matroska";
4329
+ case "mp3":
4330
+ return "audio/mpeg";
4331
+ case "wav":
4332
+ return "audio/wav";
4333
+ case "flac":
4334
+ return "audio/flac";
4335
+ case "aac":
4336
+ case "m4a":
4337
+ return "audio/mp4";
4265
4338
  default:
4266
4339
  return "video/mp4";
4267
4340
  }