@scarlett-player/hls 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.
@@ -44,6 +44,18 @@ function mapLevels(levels, _currentLevel) {
44
44
  codec: level.codecSet
45
45
  }));
46
46
  }
47
+ function getInitialBandwidthEstimate(overrideBps) {
48
+ const HLS_DEFAULT_ESTIMATE = 5e5;
49
+ if (overrideBps !== void 0 && overrideBps > 0) {
50
+ return overrideBps;
51
+ }
52
+ const connection = navigator.connection;
53
+ if (connection?.downlink && connection.downlink > 0) {
54
+ const bps = connection.downlink * 1e6;
55
+ return Math.round(bps * 0.85);
56
+ }
57
+ return HLS_DEFAULT_ESTIMATE;
58
+ }
47
59
 
48
60
  // src/event-map.ts
49
61
  var HLS_ERROR_TYPES = {
@@ -118,6 +130,14 @@ function setupHlsEventHandlers(hls, api, callbacks) {
118
130
  });
119
131
  callbacks.onLevelSwitched?.(data.level);
120
132
  });
133
+ let lastBandwidthUpdate = 0;
134
+ addHandler("hlsFragLoaded", () => {
135
+ const now = Date.now();
136
+ if (now - lastBandwidthUpdate >= 2e3 && hls.bandwidthEstimate) {
137
+ lastBandwidthUpdate = now;
138
+ api.setState("bandwidth", Math.round(hls.bandwidthEstimate));
139
+ }
140
+ });
121
141
  addHandler("hlsFragBuffered", () => {
122
142
  api.setState("buffering", false);
123
143
  callbacks.onBufferUpdate?.();
@@ -128,12 +148,44 @@ function setupHlsEventHandlers(hls, api, callbacks) {
128
148
  addHandler("hlsLevelLoaded", (_event, data) => {
129
149
  if (data.details?.live !== void 0) {
130
150
  api.setState("live", data.details.live);
151
+ if (data.details.live) {
152
+ const video = hls.media;
153
+ if (video && video.seekable && video.seekable.length > 0) {
154
+ const start = video.seekable.start(0);
155
+ const end = video.seekable.end(video.seekable.length - 1);
156
+ api.setState("seekableRange", { start, end });
157
+ const threshold = (data.details.targetduration ?? 3) * 3;
158
+ const isAtLiveEdge = end - video.currentTime < threshold;
159
+ api.setState("liveEdge", isAtLiveEdge);
160
+ const latency = end - video.currentTime;
161
+ api.setState("liveLatency", Math.max(0, latency));
162
+ }
163
+ }
131
164
  callbacks.onLiveUpdate?.();
132
165
  }
133
166
  });
134
167
  addHandler("hlsError", (_event, data) => {
135
168
  const error = parseHlsError(data);
136
- api.logger.warn("HLS error", { error });
169
+ const isBufferHoleSeek = !error.fatal && (error.details?.includes("bufferStalledError") || data.reason?.includes("buffer holes"));
170
+ if (isBufferHoleSeek) {
171
+ api.logger.debug(`HLS buffer recovery: ${error.reason || error.details}`, {
172
+ details: error.details,
173
+ reason: error.reason
174
+ });
175
+ } else if (error.fatal) {
176
+ api.logger.error(`HLS fatal error: ${error.details} (type=${error.type})`, {
177
+ type: error.type,
178
+ details: error.details,
179
+ url: error.url
180
+ });
181
+ } else {
182
+ api.logger.warn(`HLS error: ${error.details} (type=${error.type}, fatal=${error.fatal})`, {
183
+ type: error.type,
184
+ details: error.details,
185
+ fatal: error.fatal,
186
+ url: error.url
187
+ });
188
+ }
137
189
  callbacks.onError?.(error);
138
190
  });
139
191
  return () => {
@@ -155,6 +207,8 @@ function setupVideoEventHandlers(video, api) {
155
207
  addHandler("playing", () => {
156
208
  api.setState("playing", true);
157
209
  api.setState("paused", false);
210
+ api.setState("waiting", false);
211
+ api.setState("buffering", false);
158
212
  api.setState("playbackState", "playing");
159
213
  });
160
214
  addHandler("pause", () => {
@@ -171,6 +225,15 @@ function setupVideoEventHandlers(video, api) {
171
225
  addHandler("timeupdate", () => {
172
226
  api.setState("currentTime", video.currentTime);
173
227
  api.emit("playback:timeupdate", { currentTime: video.currentTime });
228
+ const isLive = api.getState("live");
229
+ if (isLive && video.seekable && video.seekable.length > 0) {
230
+ const start = video.seekable.start(0);
231
+ const end = video.seekable.end(video.seekable.length - 1);
232
+ api.setState("seekableRange", { start, end });
233
+ const isAtLiveEdge = end - video.currentTime < 10;
234
+ api.setState("liveEdge", isAtLiveEdge);
235
+ api.setState("liveLatency", Math.max(0, end - video.currentTime));
236
+ }
174
237
  });
175
238
  addHandler("durationchange", () => {
176
239
  api.setState("duration", video.duration || 0);
@@ -262,6 +325,7 @@ function setupVideoEventHandlers(video, api) {
262
325
  export {
263
326
  formatLevel,
264
327
  mapLevels,
328
+ getInitialBandwidthEstimate,
265
329
  setupHlsEventHandlers,
266
330
  setupVideoEventHandlers
267
331
  };
package/dist/index.cjs CHANGED
@@ -133,6 +133,18 @@ function mapLevels(levels, _currentLevel) {
133
133
  codec: level.codecSet
134
134
  }));
135
135
  }
136
+ function getInitialBandwidthEstimate(overrideBps) {
137
+ const HLS_DEFAULT_ESTIMATE = 5e5;
138
+ if (overrideBps !== void 0 && overrideBps > 0) {
139
+ return overrideBps;
140
+ }
141
+ const connection = navigator.connection;
142
+ if (connection?.downlink && connection.downlink > 0) {
143
+ const bps = connection.downlink * 1e6;
144
+ return Math.round(bps * 0.85);
145
+ }
146
+ return HLS_DEFAULT_ESTIMATE;
147
+ }
136
148
 
137
149
  // src/event-map.ts
138
150
  var HLS_ERROR_TYPES = {
@@ -207,6 +219,14 @@ function setupHlsEventHandlers(hls, api, callbacks) {
207
219
  });
208
220
  callbacks.onLevelSwitched?.(data.level);
209
221
  });
222
+ let lastBandwidthUpdate = 0;
223
+ addHandler("hlsFragLoaded", () => {
224
+ const now = Date.now();
225
+ if (now - lastBandwidthUpdate >= 2e3 && hls.bandwidthEstimate) {
226
+ lastBandwidthUpdate = now;
227
+ api.setState("bandwidth", Math.round(hls.bandwidthEstimate));
228
+ }
229
+ });
210
230
  addHandler("hlsFragBuffered", () => {
211
231
  api.setState("buffering", false);
212
232
  callbacks.onBufferUpdate?.();
@@ -217,12 +237,44 @@ function setupHlsEventHandlers(hls, api, callbacks) {
217
237
  addHandler("hlsLevelLoaded", (_event, data) => {
218
238
  if (data.details?.live !== void 0) {
219
239
  api.setState("live", data.details.live);
240
+ if (data.details.live) {
241
+ const video = hls.media;
242
+ if (video && video.seekable && video.seekable.length > 0) {
243
+ const start = video.seekable.start(0);
244
+ const end = video.seekable.end(video.seekable.length - 1);
245
+ api.setState("seekableRange", { start, end });
246
+ const threshold = (data.details.targetduration ?? 3) * 3;
247
+ const isAtLiveEdge = end - video.currentTime < threshold;
248
+ api.setState("liveEdge", isAtLiveEdge);
249
+ const latency = end - video.currentTime;
250
+ api.setState("liveLatency", Math.max(0, latency));
251
+ }
252
+ }
220
253
  callbacks.onLiveUpdate?.();
221
254
  }
222
255
  });
223
256
  addHandler("hlsError", (_event, data) => {
224
257
  const error = parseHlsError(data);
225
- api.logger.warn("HLS error", { error });
258
+ const isBufferHoleSeek = !error.fatal && (error.details?.includes("bufferStalledError") || data.reason?.includes("buffer holes"));
259
+ if (isBufferHoleSeek) {
260
+ api.logger.debug(`HLS buffer recovery: ${error.reason || error.details}`, {
261
+ details: error.details,
262
+ reason: error.reason
263
+ });
264
+ } else if (error.fatal) {
265
+ api.logger.error(`HLS fatal error: ${error.details} (type=${error.type})`, {
266
+ type: error.type,
267
+ details: error.details,
268
+ url: error.url
269
+ });
270
+ } else {
271
+ api.logger.warn(`HLS error: ${error.details} (type=${error.type}, fatal=${error.fatal})`, {
272
+ type: error.type,
273
+ details: error.details,
274
+ fatal: error.fatal,
275
+ url: error.url
276
+ });
277
+ }
226
278
  callbacks.onError?.(error);
227
279
  });
228
280
  return () => {
@@ -244,6 +296,8 @@ function setupVideoEventHandlers(video, api) {
244
296
  addHandler("playing", () => {
245
297
  api.setState("playing", true);
246
298
  api.setState("paused", false);
299
+ api.setState("waiting", false);
300
+ api.setState("buffering", false);
247
301
  api.setState("playbackState", "playing");
248
302
  });
249
303
  addHandler("pause", () => {
@@ -260,6 +314,15 @@ function setupVideoEventHandlers(video, api) {
260
314
  addHandler("timeupdate", () => {
261
315
  api.setState("currentTime", video.currentTime);
262
316
  api.emit("playback:timeupdate", { currentTime: video.currentTime });
317
+ const isLive = api.getState("live");
318
+ if (isLive && video.seekable && video.seekable.length > 0) {
319
+ const start = video.seekable.start(0);
320
+ const end = video.seekable.end(video.seekable.length - 1);
321
+ api.setState("seekableRange", { start, end });
322
+ const isAtLiveEdge = end - video.currentTime < 10;
323
+ api.setState("liveEdge", isAtLiveEdge);
324
+ api.setState("liveLatency", Math.max(0, end - video.currentTime));
325
+ }
263
326
  });
264
327
  addHandler("durationchange", () => {
265
328
  api.setState("duration", video.duration || 0);
@@ -358,6 +421,7 @@ var DEFAULT_CONFIG = {
358
421
  maxMaxBufferLength: 600,
359
422
  backBufferLength: 30,
360
423
  enableWorker: true,
424
+ capLevelToPlayerSize: true,
361
425
  // Error recovery settings
362
426
  maxNetworkRetries: 3,
363
427
  maxMediaRetries: 2,
@@ -427,11 +491,13 @@ function createHLSPlugin(config) {
427
491
  startPosition: mergedConfig.startPosition,
428
492
  startLevel: -1,
429
493
  // Auto quality selection (ABR)
494
+ abrEwmaDefaultEstimate: getInitialBandwidthEstimate(mergedConfig.initialBandwidthEstimate),
430
495
  lowLatencyMode: mergedConfig.lowLatencyMode,
431
496
  maxBufferLength: mergedConfig.maxBufferLength,
432
497
  maxMaxBufferLength: mergedConfig.maxMaxBufferLength,
433
498
  backBufferLength: mergedConfig.backBufferLength,
434
499
  enableWorker: mergedConfig.enableWorker,
500
+ capLevelToPlayerSize: mergedConfig.capLevelToPlayerSize,
435
501
  // Minimize hls.js internal retries - we handle retries ourselves
436
502
  fragLoadingMaxRetry: 1,
437
503
  manifestLoadingMaxRetry: 1,
@@ -702,7 +768,10 @@ function createHLSPlugin(config) {
702
768
  currentSrc = src;
703
769
  api.setState("playbackState", "loading");
704
770
  api.setState("buffering", true);
705
- if (isHlsJsSupported()) {
771
+ if (api.getState("airplayActive") && supportsNativeHLS()) {
772
+ api.logger.info("Using native HLS (AirPlay active)");
773
+ await loadNative(src);
774
+ } else if (isHlsJsSupported()) {
706
775
  api.logger.info("Using hls.js for HLS playback");
707
776
  await loadWithHlsJs(src);
708
777
  } else if (supportsNativeHLS()) {
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { H as HLSPluginConfig, I as IHLSPlugin } from './types-Df2TUpPu.cjs';
2
- export { a as HLSError, b as HLSLiveInfo, c as HLSQualityLevel } from './types-Df2TUpPu.cjs';
1
+ import { H as HLSPluginConfig, I as IHLSPlugin } from './types-CRKmNxEW.cjs';
2
+ export { a as HLSError, b as HLSLiveInfo, c as HLSQualityLevel } from './types-CRKmNxEW.cjs';
3
3
  import '@scarlett-player/core';
4
4
 
5
5
  /**
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { H as HLSPluginConfig, I as IHLSPlugin } from './types-Df2TUpPu.js';
2
- export { a as HLSError, b as HLSLiveInfo, c as HLSQualityLevel } from './types-Df2TUpPu.js';
1
+ import { H as HLSPluginConfig, I as IHLSPlugin } from './types-CRKmNxEW.js';
2
+ export { a as HLSError, b as HLSLiveInfo, c as HLSQualityLevel } from './types-CRKmNxEW.js';
3
3
  import '@scarlett-player/core';
4
4
 
5
5
  /**
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import {
2
2
  formatLevel,
3
+ getInitialBandwidthEstimate,
3
4
  mapLevels,
4
5
  setupHlsEventHandlers,
5
6
  setupVideoEventHandlers
6
- } from "./chunk-2BPI5GEH.js";
7
+ } from "./chunk-OEPNMGNQ.js";
7
8
 
8
9
  // src/hls-loader.ts
9
10
  var hlsConstructor = null;
@@ -67,6 +68,7 @@ var DEFAULT_CONFIG = {
67
68
  maxMaxBufferLength: 600,
68
69
  backBufferLength: 30,
69
70
  enableWorker: true,
71
+ capLevelToPlayerSize: true,
70
72
  // Error recovery settings
71
73
  maxNetworkRetries: 3,
72
74
  maxMediaRetries: 2,
@@ -136,11 +138,13 @@ function createHLSPlugin(config) {
136
138
  startPosition: mergedConfig.startPosition,
137
139
  startLevel: -1,
138
140
  // Auto quality selection (ABR)
141
+ abrEwmaDefaultEstimate: getInitialBandwidthEstimate(mergedConfig.initialBandwidthEstimate),
139
142
  lowLatencyMode: mergedConfig.lowLatencyMode,
140
143
  maxBufferLength: mergedConfig.maxBufferLength,
141
144
  maxMaxBufferLength: mergedConfig.maxMaxBufferLength,
142
145
  backBufferLength: mergedConfig.backBufferLength,
143
146
  enableWorker: mergedConfig.enableWorker,
147
+ capLevelToPlayerSize: mergedConfig.capLevelToPlayerSize,
144
148
  // Minimize hls.js internal retries - we handle retries ourselves
145
149
  fragLoadingMaxRetry: 1,
146
150
  manifestLoadingMaxRetry: 1,
@@ -411,7 +415,10 @@ function createHLSPlugin(config) {
411
415
  currentSrc = src;
412
416
  api.setState("playbackState", "loading");
413
417
  api.setState("buffering", true);
414
- if (isHlsJsSupported()) {
418
+ if (api.getState("airplayActive") && supportsNativeHLS()) {
419
+ api.logger.info("Using native HLS (AirPlay active)");
420
+ await loadNative(src);
421
+ } else if (isHlsJsSupported()) {
415
422
  api.logger.info("Using hls.js for HLS playback");
416
423
  await loadWithHlsJs(src);
417
424
  } else if (supportsNativeHLS()) {
package/dist/light.cjs CHANGED
@@ -207,6 +207,14 @@ function setupHlsEventHandlers(hls, api, callbacks) {
207
207
  });
208
208
  callbacks.onLevelSwitched?.(data.level);
209
209
  });
210
+ let lastBandwidthUpdate = 0;
211
+ addHandler("hlsFragLoaded", () => {
212
+ const now = Date.now();
213
+ if (now - lastBandwidthUpdate >= 2e3 && hls.bandwidthEstimate) {
214
+ lastBandwidthUpdate = now;
215
+ api.setState("bandwidth", Math.round(hls.bandwidthEstimate));
216
+ }
217
+ });
210
218
  addHandler("hlsFragBuffered", () => {
211
219
  api.setState("buffering", false);
212
220
  callbacks.onBufferUpdate?.();
@@ -217,12 +225,44 @@ function setupHlsEventHandlers(hls, api, callbacks) {
217
225
  addHandler("hlsLevelLoaded", (_event, data) => {
218
226
  if (data.details?.live !== void 0) {
219
227
  api.setState("live", data.details.live);
228
+ if (data.details.live) {
229
+ const video = hls.media;
230
+ if (video && video.seekable && video.seekable.length > 0) {
231
+ const start = video.seekable.start(0);
232
+ const end = video.seekable.end(video.seekable.length - 1);
233
+ api.setState("seekableRange", { start, end });
234
+ const threshold = (data.details.targetduration ?? 3) * 3;
235
+ const isAtLiveEdge = end - video.currentTime < threshold;
236
+ api.setState("liveEdge", isAtLiveEdge);
237
+ const latency = end - video.currentTime;
238
+ api.setState("liveLatency", Math.max(0, latency));
239
+ }
240
+ }
220
241
  callbacks.onLiveUpdate?.();
221
242
  }
222
243
  });
223
244
  addHandler("hlsError", (_event, data) => {
224
245
  const error = parseHlsError(data);
225
- api.logger.warn("HLS error", { error });
246
+ const isBufferHoleSeek = !error.fatal && (error.details?.includes("bufferStalledError") || data.reason?.includes("buffer holes"));
247
+ if (isBufferHoleSeek) {
248
+ api.logger.debug(`HLS buffer recovery: ${error.reason || error.details}`, {
249
+ details: error.details,
250
+ reason: error.reason
251
+ });
252
+ } else if (error.fatal) {
253
+ api.logger.error(`HLS fatal error: ${error.details} (type=${error.type})`, {
254
+ type: error.type,
255
+ details: error.details,
256
+ url: error.url
257
+ });
258
+ } else {
259
+ api.logger.warn(`HLS error: ${error.details} (type=${error.type}, fatal=${error.fatal})`, {
260
+ type: error.type,
261
+ details: error.details,
262
+ fatal: error.fatal,
263
+ url: error.url
264
+ });
265
+ }
226
266
  callbacks.onError?.(error);
227
267
  });
228
268
  return () => {
@@ -244,6 +284,8 @@ function setupVideoEventHandlers(video, api) {
244
284
  addHandler("playing", () => {
245
285
  api.setState("playing", true);
246
286
  api.setState("paused", false);
287
+ api.setState("waiting", false);
288
+ api.setState("buffering", false);
247
289
  api.setState("playbackState", "playing");
248
290
  });
249
291
  addHandler("pause", () => {
@@ -260,6 +302,15 @@ function setupVideoEventHandlers(video, api) {
260
302
  addHandler("timeupdate", () => {
261
303
  api.setState("currentTime", video.currentTime);
262
304
  api.emit("playback:timeupdate", { currentTime: video.currentTime });
305
+ const isLive = api.getState("live");
306
+ if (isLive && video.seekable && video.seekable.length > 0) {
307
+ const start = video.seekable.start(0);
308
+ const end = video.seekable.end(video.seekable.length - 1);
309
+ api.setState("seekableRange", { start, end });
310
+ const isAtLiveEdge = end - video.currentTime < 10;
311
+ api.setState("liveEdge", isAtLiveEdge);
312
+ api.setState("liveLatency", Math.max(0, end - video.currentTime));
313
+ }
263
314
  });
264
315
  addHandler("durationchange", () => {
265
316
  api.setState("duration", video.duration || 0);
package/dist/light.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { H as HLSPluginConfig, I as IHLSPlugin } from './types-Df2TUpPu.cjs';
2
- export { a as HLSError, b as HLSLiveInfo, c as HLSQualityLevel } from './types-Df2TUpPu.cjs';
1
+ import { H as HLSPluginConfig, I as IHLSPlugin } from './types-CRKmNxEW.cjs';
2
+ export { a as HLSError, b as HLSLiveInfo, c as HLSQualityLevel } from './types-CRKmNxEW.cjs';
3
3
  import '@scarlett-player/core';
4
4
 
5
5
  /**
package/dist/light.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { H as HLSPluginConfig, I as IHLSPlugin } from './types-Df2TUpPu.js';
2
- export { a as HLSError, b as HLSLiveInfo, c as HLSQualityLevel } from './types-Df2TUpPu.js';
1
+ import { H as HLSPluginConfig, I as IHLSPlugin } from './types-CRKmNxEW.js';
2
+ export { a as HLSError, b as HLSLiveInfo, c as HLSQualityLevel } from './types-CRKmNxEW.js';
3
3
  import '@scarlett-player/core';
4
4
 
5
5
  /**
package/dist/light.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  mapLevels,
4
4
  setupHlsEventHandlers,
5
5
  setupVideoEventHandlers
6
- } from "./chunk-2BPI5GEH.js";
6
+ } from "./chunk-OEPNMGNQ.js";
7
7
 
8
8
  // src/hls-loader-light.ts
9
9
  var hlsConstructor = null;
@@ -26,6 +26,10 @@ interface HLSPluginConfig {
26
26
  maxNetworkRetries?: number;
27
27
  /** Max media error retries before giving up (default: 2) */
28
28
  maxMediaRetries?: number;
29
+ /** Cap quality to player element dimensions (default: true) */
30
+ capLevelToPlayerSize?: boolean;
31
+ /** Override initial bandwidth estimate in bits per second */
32
+ initialBandwidthEstimate?: number;
29
33
  /** Base retry delay in milliseconds (default: 1000) */
30
34
  retryDelayMs?: number;
31
35
  /** Exponential backoff multiplier (default: 2) */
@@ -26,6 +26,10 @@ interface HLSPluginConfig {
26
26
  maxNetworkRetries?: number;
27
27
  /** Max media error retries before giving up (default: 2) */
28
28
  maxMediaRetries?: number;
29
+ /** Cap quality to player element dimensions (default: true) */
30
+ capLevelToPlayerSize?: boolean;
31
+ /** Override initial bandwidth estimate in bits per second */
32
+ initialBandwidthEstimate?: number;
29
33
  /** Base retry delay in milliseconds (default: 1000) */
30
34
  retryDelayMs?: number;
31
35
  /** Exponential backoff multiplier (default: 2) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scarlett-player/hls",
3
- "version": "0.5.2",
3
+ "version": "1.0.0",
4
4
  "description": "HLS Provider Plugin for Scarlett Player",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -22,8 +22,8 @@
22
22
  "dist"
23
23
  ],
24
24
  "peerDependencies": {
25
- "@scarlett-player/core": "^0.5.2",
26
- "hls.js": "^1.4.0"
25
+ "@scarlett-player/core": "^1.0.0",
26
+ "hls.js": "^1.5.0"
27
27
  },
28
28
  "peerDependenciesMeta": {
29
29
  "hls.js": {
@@ -31,13 +31,13 @@
31
31
  }
32
32
  },
33
33
  "devDependencies": {
34
- "hls.js": "^1.5.0",
34
+ "hls.js": "^1.6.0",
35
35
  "typescript": "^5.3.0",
36
36
  "tsup": "^8.0.0",
37
37
  "vitest": "^1.6.0",
38
38
  "@vitest/coverage-v8": "^1.6.0",
39
39
  "jsdom": "^24.0.0",
40
- "@scarlett-player/core": "0.5.2"
40
+ "@scarlett-player/core": "1.0.0"
41
41
  },
42
42
  "keywords": [
43
43
  "video",