@scarlett-player/hls 1.0.2 → 1.2.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.
@@ -1,336 +0,0 @@
1
- // src/quality.ts
2
- function formatLevel(level) {
3
- if (level.name) {
4
- return level.name;
5
- }
6
- if (level.height) {
7
- const standardLabels = {
8
- 2160: "4K",
9
- 1440: "1440p",
10
- 1080: "1080p",
11
- 720: "720p",
12
- 480: "480p",
13
- 360: "360p",
14
- 240: "240p",
15
- 144: "144p"
16
- };
17
- const closest = Object.keys(standardLabels).map(Number).sort((a, b) => Math.abs(a - level.height) - Math.abs(b - level.height))[0];
18
- if (Math.abs(closest - level.height) <= 20) {
19
- return standardLabels[closest];
20
- }
21
- return `${level.height}p`;
22
- }
23
- if (level.bitrate) {
24
- return formatBitrate(level.bitrate);
25
- }
26
- return "Unknown";
27
- }
28
- function formatBitrate(bitrate) {
29
- if (bitrate >= 1e6) {
30
- return `${(bitrate / 1e6).toFixed(1)} Mbps`;
31
- }
32
- if (bitrate >= 1e3) {
33
- return `${Math.round(bitrate / 1e3)} Kbps`;
34
- }
35
- return `${bitrate} bps`;
36
- }
37
- function mapLevels(levels, _currentLevel) {
38
- return levels.map((level, index) => ({
39
- index,
40
- width: level.width || 0,
41
- height: level.height || 0,
42
- bitrate: level.bitrate || 0,
43
- label: formatLevel(level),
44
- codec: level.codecSet
45
- }));
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
- }
59
-
60
- // src/event-map.ts
61
- var HLS_ERROR_TYPES = {
62
- NETWORK_ERROR: "networkError",
63
- MEDIA_ERROR: "mediaError",
64
- KEY_SYSTEM_ERROR: "keySystemError",
65
- MUX_ERROR: "muxError",
66
- OTHER_ERROR: "otherError"
67
- };
68
- function mapErrorType(hlsType) {
69
- switch (hlsType) {
70
- case HLS_ERROR_TYPES.NETWORK_ERROR:
71
- return "network";
72
- case HLS_ERROR_TYPES.MEDIA_ERROR:
73
- return "media";
74
- case HLS_ERROR_TYPES.MUX_ERROR:
75
- return "mux";
76
- default:
77
- return "other";
78
- }
79
- }
80
- function parseHlsError(data) {
81
- return {
82
- type: mapErrorType(data.type),
83
- details: data.details || "Unknown error",
84
- fatal: data.fatal || false,
85
- url: data.url,
86
- reason: data.reason,
87
- response: data.response
88
- };
89
- }
90
- function setupHlsEventHandlers(hls, api, callbacks) {
91
- const handlers = [];
92
- const addHandler = (event, handler) => {
93
- hls.on(event, handler);
94
- handlers.push({ event, handler });
95
- };
96
- addHandler("hlsManifestParsed", (_event, data) => {
97
- api.logger.debug("HLS manifest parsed", { levels: data.levels.length });
98
- const levels = data.levels.map((level, index) => ({
99
- id: `level-${index}`,
100
- label: formatLevel(level),
101
- width: level.width,
102
- height: level.height,
103
- bitrate: level.bitrate,
104
- active: index === hls.currentLevel
105
- }));
106
- api.setState("qualities", levels);
107
- api.emit("quality:levels", {
108
- levels: levels.map((l) => ({ id: l.id, label: l.label }))
109
- });
110
- callbacks.onManifestParsed?.(data.levels);
111
- });
112
- addHandler("hlsLevelSwitched", (_event, data) => {
113
- const level = hls.levels[data.level];
114
- const isAuto = callbacks.getIsAutoQuality?.() ?? hls.autoLevelEnabled;
115
- api.logger.debug("HLS level switched", { level: data.level, height: level?.height, auto: isAuto });
116
- if (level) {
117
- const label = isAuto ? `Auto (${formatLevel(level)})` : formatLevel(level);
118
- api.setState("currentQuality", {
119
- id: isAuto ? "auto" : `level-${data.level}`,
120
- label,
121
- width: level.width,
122
- height: level.height,
123
- bitrate: level.bitrate,
124
- active: true
125
- });
126
- }
127
- api.emit("quality:change", {
128
- quality: level ? formatLevel(level) : "auto",
129
- auto: isAuto
130
- });
131
- callbacks.onLevelSwitched?.(data.level);
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
- });
141
- addHandler("hlsFragBuffered", () => {
142
- api.setState("buffering", false);
143
- callbacks.onBufferUpdate?.();
144
- });
145
- addHandler("hlsFragLoading", () => {
146
- api.setState("buffering", true);
147
- });
148
- addHandler("hlsLevelLoaded", (_event, data) => {
149
- if (data.details?.live !== void 0) {
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
- }
164
- callbacks.onLiveUpdate?.();
165
- }
166
- });
167
- addHandler("hlsError", (_event, data) => {
168
- const error = parseHlsError(data);
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
- }
189
- callbacks.onError?.(error);
190
- });
191
- return () => {
192
- for (const { event, handler } of handlers) {
193
- hls.off(event, handler);
194
- }
195
- handlers.length = 0;
196
- };
197
- }
198
- function setupVideoEventHandlers(video, api) {
199
- const handlers = [];
200
- const addHandler = (event, handler) => {
201
- video.addEventListener(event, handler);
202
- handlers.push({ event, handler });
203
- };
204
- addHandler("play", () => {
205
- api.setState("paused", false);
206
- });
207
- addHandler("playing", () => {
208
- api.setState("playing", true);
209
- api.setState("paused", false);
210
- api.setState("waiting", false);
211
- api.setState("buffering", false);
212
- api.setState("playbackState", "playing");
213
- });
214
- addHandler("pause", () => {
215
- api.setState("playing", false);
216
- api.setState("paused", true);
217
- api.setState("playbackState", "paused");
218
- });
219
- addHandler("ended", () => {
220
- api.setState("playing", false);
221
- api.setState("ended", true);
222
- api.setState("playbackState", "ended");
223
- api.emit("playback:ended", void 0);
224
- });
225
- addHandler("timeupdate", () => {
226
- api.setState("currentTime", video.currentTime);
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
- }
237
- });
238
- addHandler("durationchange", () => {
239
- api.setState("duration", video.duration || 0);
240
- api.emit("media:loadedmetadata", { duration: video.duration || 0 });
241
- });
242
- addHandler("waiting", () => {
243
- api.setState("waiting", true);
244
- api.setState("buffering", true);
245
- api.emit("media:waiting", void 0);
246
- });
247
- addHandler("canplay", () => {
248
- api.setState("waiting", false);
249
- api.setState("playbackState", "ready");
250
- api.emit("media:canplay", void 0);
251
- });
252
- addHandler("canplaythrough", () => {
253
- api.setState("buffering", false);
254
- api.emit("media:canplaythrough", void 0);
255
- });
256
- addHandler("progress", () => {
257
- if (video.buffered.length > 0) {
258
- const bufferedEnd = video.buffered.end(video.buffered.length - 1);
259
- const bufferedAmount = video.duration > 0 ? bufferedEnd / video.duration : 0;
260
- api.setState("bufferedAmount", bufferedAmount);
261
- api.setState("buffered", video.buffered);
262
- api.emit("media:progress", { buffered: bufferedAmount });
263
- }
264
- });
265
- addHandler("seeking", () => {
266
- api.setState("seeking", true);
267
- });
268
- addHandler("seeked", () => {
269
- api.setState("seeking", false);
270
- api.emit("playback:seeked", { time: video.currentTime });
271
- });
272
- addHandler("volumechange", () => {
273
- api.setState("volume", video.volume);
274
- api.setState("muted", video.muted);
275
- api.emit("volume:change", { volume: video.volume, muted: video.muted });
276
- });
277
- addHandler("ratechange", () => {
278
- api.setState("playbackRate", video.playbackRate);
279
- api.emit("playback:ratechange", { rate: video.playbackRate });
280
- });
281
- addHandler("loadedmetadata", () => {
282
- api.setState("duration", video.duration);
283
- api.setState("mediaType", video.videoWidth > 0 ? "video" : "audio");
284
- });
285
- addHandler("loadeddata", () => {
286
- if (video.videoWidth > 0) {
287
- api.setState("mediaType", "video");
288
- }
289
- });
290
- addHandler("error", () => {
291
- const error = video.error;
292
- if (error) {
293
- api.logger.error("Video element error", { code: error.code, message: error.message });
294
- api.emit("media:error", { error: new Error(error.message || "Video playback error") });
295
- }
296
- });
297
- addHandler("enterpictureinpicture", () => {
298
- api.setState("pip", true);
299
- api.logger.debug("PiP: entered (standard)");
300
- });
301
- addHandler("leavepictureinpicture", () => {
302
- api.setState("pip", false);
303
- api.logger.debug("PiP: exited (standard)");
304
- if (!video.paused || api.getState("playing")) {
305
- video.play().catch(() => {
306
- });
307
- }
308
- });
309
- const webkitVideo = video;
310
- if ("webkitPresentationMode" in video) {
311
- addHandler("webkitpresentationmodechanged", () => {
312
- const mode = webkitVideo.webkitPresentationMode;
313
- const isInPip = mode === "picture-in-picture";
314
- api.setState("pip", isInPip);
315
- api.logger.debug(`PiP: mode changed to ${mode} (webkit)`);
316
- if (mode === "inline" && video.paused) {
317
- video.play().catch(() => {
318
- });
319
- }
320
- });
321
- }
322
- return () => {
323
- for (const { event, handler } of handlers) {
324
- video.removeEventListener(event, handler);
325
- }
326
- handlers.length = 0;
327
- };
328
- }
329
-
330
- export {
331
- formatLevel,
332
- mapLevels,
333
- getInitialBandwidthEstimate,
334
- setupHlsEventHandlers,
335
- setupVideoEventHandlers
336
- };