@scarlett-player/native 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.
package/README.md CHANGED
@@ -25,12 +25,20 @@ await player.load('https://example.com/video.mp4');
25
25
 
26
26
  ## Supported Formats
27
27
 
28
+ Video:
29
+
28
30
  - **MP4** - H.264/AAC (most common)
29
31
  - **WebM** - VP8/VP9/Opus
30
32
  - **MOV** - QuickTime (H.264/AAC)
31
33
  - **MKV** - Matroska (browser support varies)
32
34
  - **OGV/OGG** - Theora/Vorbis
33
35
 
36
+ Audio:
37
+
38
+ - **MP3**, **WAV**, **OGG**, **FLAC**, **AAC**, **M4A**, **Opus** (pairs with
39
+ `@scarlett-player/audio-ui` and `@scarlett-player/media-session` for a full
40
+ audio player)
41
+
34
42
  ## Configuration
35
43
 
36
44
  ```typescript
package/dist/index.cjs CHANGED
@@ -48,9 +48,11 @@ var MIME_TYPES = {
48
48
  };
49
49
  function createNativePlugin(config) {
50
50
  const preload = config?.preload ?? "metadata";
51
+ const load_timeout_ms = config?.loadTimeoutMs ?? 3e4;
51
52
  let api = null;
52
53
  let video = null;
53
54
  let cleanupEvents = null;
55
+ let derived_title = null;
54
56
  const getExtension = (src) => {
55
57
  try {
56
58
  const url = new URL(src, window.location.href);
@@ -202,6 +204,30 @@ function createNativePlugin(config) {
202
204
  timestamp: Date.now()
203
205
  });
204
206
  });
207
+ on("enterpictureinpicture", () => {
208
+ api?.setState("pip", true);
209
+ api?.logger.debug("PiP: entered (standard)");
210
+ });
211
+ on("leavepictureinpicture", () => {
212
+ api?.setState("pip", false);
213
+ api?.logger.debug("PiP: exited (standard)");
214
+ if (!videoEl.paused || api?.getState("playing")) {
215
+ videoEl.play().catch(() => {
216
+ });
217
+ }
218
+ });
219
+ const webkitVideo = videoEl;
220
+ if ("webkitPresentationMode" in videoEl) {
221
+ on("webkitpresentationmodechanged", () => {
222
+ const mode = webkitVideo.webkitPresentationMode;
223
+ api?.setState("pip", mode === "picture-in-picture");
224
+ api?.logger.debug(`PiP: mode changed to ${mode} (webkit)`);
225
+ if (mode === "inline" && videoEl.paused) {
226
+ videoEl.play().catch(() => {
227
+ });
228
+ }
229
+ });
230
+ }
205
231
  return () => {
206
232
  handlers.forEach(([event, handler]) => {
207
233
  videoEl.removeEventListener(event, handler);
@@ -279,6 +305,7 @@ function createNativePlugin(config) {
279
305
  }
280
306
  video = null;
281
307
  api = null;
308
+ derived_title = null;
282
309
  },
283
310
  async loadSource(src) {
284
311
  if (!api) throw new Error("Plugin not initialized");
@@ -291,13 +318,18 @@ function createNativePlugin(config) {
291
318
  api.setState("buffering", true);
292
319
  api.setState("mediaType", isAudio ? "audio" : "video");
293
320
  if (isAudio) {
294
- try {
295
- const url = new URL(src, window.location.href);
296
- const filename = url.pathname.split("/").pop() || "Audio";
297
- const title = decodeURIComponent(filename.replace(/\.[^.]+$/, "").replace(/[-_]/g, " "));
298
- api.setState("title", title);
299
- } catch {
300
- api.setState("title", "Audio");
321
+ const current_title = api.getState("title");
322
+ if (!current_title || current_title === derived_title) {
323
+ try {
324
+ const url = new URL(src, window.location.href);
325
+ const filename = url.pathname.split("/").pop() || "Audio";
326
+ const title = decodeURIComponent(filename.replace(/\.[^.]+$/, "").replace(/[-_]/g, " "));
327
+ derived_title = title;
328
+ api.setState("title", title);
329
+ } catch {
330
+ derived_title = "Audio";
331
+ api.setState("title", "Audio");
332
+ }
301
333
  }
302
334
  }
303
335
  api.setState("qualities", []);
@@ -315,9 +347,17 @@ function createNativePlugin(config) {
315
347
  }
316
348
  cleanupEvents = setupEventListeners(videoEl);
317
349
  return new Promise((resolve, reject) => {
318
- const onLoaded = () => {
350
+ let watchdog = null;
351
+ const settle = () => {
319
352
  videoEl.removeEventListener("loadedmetadata", onLoaded);
320
353
  videoEl.removeEventListener("error", onError);
354
+ if (watchdog !== null) {
355
+ clearTimeout(watchdog);
356
+ watchdog = null;
357
+ }
358
+ };
359
+ const onLoaded = () => {
360
+ settle();
321
361
  const muted = api?.getState("muted");
322
362
  const volume = api?.getState("volume");
323
363
  if (muted !== void 0) videoEl.muted = muted;
@@ -329,11 +369,16 @@ function createNativePlugin(config) {
329
369
  resolve();
330
370
  };
331
371
  const onError = () => {
332
- videoEl.removeEventListener("loadedmetadata", onLoaded);
333
- videoEl.removeEventListener("error", onError);
372
+ settle();
334
373
  const error = videoEl.error;
335
374
  reject(new Error(error?.message || "Failed to load video source"));
336
375
  };
376
+ if (load_timeout_ms > 0) {
377
+ watchdog = setTimeout(() => {
378
+ settle();
379
+ reject(new Error("Video took too long to load (network timeout)"));
380
+ }, load_timeout_ms);
381
+ }
337
382
  videoEl.addEventListener("loadedmetadata", onLoaded);
338
383
  videoEl.addEventListener("error", onError);
339
384
  videoEl.src = src;
package/dist/index.d.cts CHANGED
@@ -24,6 +24,12 @@ import { PluginType, IPluginAPI } from '@scarlett-player/core';
24
24
  interface NativePluginConfig {
25
25
  /** Preload behavior: 'none' | 'metadata' | 'auto' */
26
26
  preload?: 'none' | 'metadata' | 'auto';
27
+ /**
28
+ * Watchdog for source loading in milliseconds (default: 30000, 0 disables).
29
+ * Guarantees a load attempt terminates with an error instead of leaving
30
+ * the viewer on a spinner when the network stalls without erroring.
31
+ */
32
+ loadTimeoutMs?: number;
27
33
  }
28
34
  interface INativePlugin {
29
35
  id: string;
package/dist/index.d.ts CHANGED
@@ -24,6 +24,12 @@ import { PluginType, IPluginAPI } from '@scarlett-player/core';
24
24
  interface NativePluginConfig {
25
25
  /** Preload behavior: 'none' | 'metadata' | 'auto' */
26
26
  preload?: 'none' | 'metadata' | 'auto';
27
+ /**
28
+ * Watchdog for source loading in milliseconds (default: 30000, 0 disables).
29
+ * Guarantees a load attempt terminates with an error instead of leaving
30
+ * the viewer on a spinner when the network stalls without erroring.
31
+ */
32
+ loadTimeoutMs?: number;
27
33
  }
28
34
  interface INativePlugin {
29
35
  id: string;
package/dist/index.js CHANGED
@@ -23,9 +23,11 @@ var MIME_TYPES = {
23
23
  };
24
24
  function createNativePlugin(config) {
25
25
  const preload = config?.preload ?? "metadata";
26
+ const load_timeout_ms = config?.loadTimeoutMs ?? 3e4;
26
27
  let api = null;
27
28
  let video = null;
28
29
  let cleanupEvents = null;
30
+ let derived_title = null;
29
31
  const getExtension = (src) => {
30
32
  try {
31
33
  const url = new URL(src, window.location.href);
@@ -177,6 +179,30 @@ function createNativePlugin(config) {
177
179
  timestamp: Date.now()
178
180
  });
179
181
  });
182
+ on("enterpictureinpicture", () => {
183
+ api?.setState("pip", true);
184
+ api?.logger.debug("PiP: entered (standard)");
185
+ });
186
+ on("leavepictureinpicture", () => {
187
+ api?.setState("pip", false);
188
+ api?.logger.debug("PiP: exited (standard)");
189
+ if (!videoEl.paused || api?.getState("playing")) {
190
+ videoEl.play().catch(() => {
191
+ });
192
+ }
193
+ });
194
+ const webkitVideo = videoEl;
195
+ if ("webkitPresentationMode" in videoEl) {
196
+ on("webkitpresentationmodechanged", () => {
197
+ const mode = webkitVideo.webkitPresentationMode;
198
+ api?.setState("pip", mode === "picture-in-picture");
199
+ api?.logger.debug(`PiP: mode changed to ${mode} (webkit)`);
200
+ if (mode === "inline" && videoEl.paused) {
201
+ videoEl.play().catch(() => {
202
+ });
203
+ }
204
+ });
205
+ }
180
206
  return () => {
181
207
  handlers.forEach(([event, handler]) => {
182
208
  videoEl.removeEventListener(event, handler);
@@ -254,6 +280,7 @@ function createNativePlugin(config) {
254
280
  }
255
281
  video = null;
256
282
  api = null;
283
+ derived_title = null;
257
284
  },
258
285
  async loadSource(src) {
259
286
  if (!api) throw new Error("Plugin not initialized");
@@ -266,13 +293,18 @@ function createNativePlugin(config) {
266
293
  api.setState("buffering", true);
267
294
  api.setState("mediaType", isAudio ? "audio" : "video");
268
295
  if (isAudio) {
269
- try {
270
- const url = new URL(src, window.location.href);
271
- const filename = url.pathname.split("/").pop() || "Audio";
272
- const title = decodeURIComponent(filename.replace(/\.[^.]+$/, "").replace(/[-_]/g, " "));
273
- api.setState("title", title);
274
- } catch {
275
- api.setState("title", "Audio");
296
+ const current_title = api.getState("title");
297
+ if (!current_title || current_title === derived_title) {
298
+ try {
299
+ const url = new URL(src, window.location.href);
300
+ const filename = url.pathname.split("/").pop() || "Audio";
301
+ const title = decodeURIComponent(filename.replace(/\.[^.]+$/, "").replace(/[-_]/g, " "));
302
+ derived_title = title;
303
+ api.setState("title", title);
304
+ } catch {
305
+ derived_title = "Audio";
306
+ api.setState("title", "Audio");
307
+ }
276
308
  }
277
309
  }
278
310
  api.setState("qualities", []);
@@ -290,9 +322,17 @@ function createNativePlugin(config) {
290
322
  }
291
323
  cleanupEvents = setupEventListeners(videoEl);
292
324
  return new Promise((resolve, reject) => {
293
- const onLoaded = () => {
325
+ let watchdog = null;
326
+ const settle = () => {
294
327
  videoEl.removeEventListener("loadedmetadata", onLoaded);
295
328
  videoEl.removeEventListener("error", onError);
329
+ if (watchdog !== null) {
330
+ clearTimeout(watchdog);
331
+ watchdog = null;
332
+ }
333
+ };
334
+ const onLoaded = () => {
335
+ settle();
296
336
  const muted = api?.getState("muted");
297
337
  const volume = api?.getState("volume");
298
338
  if (muted !== void 0) videoEl.muted = muted;
@@ -304,11 +344,16 @@ function createNativePlugin(config) {
304
344
  resolve();
305
345
  };
306
346
  const onError = () => {
307
- videoEl.removeEventListener("loadedmetadata", onLoaded);
308
- videoEl.removeEventListener("error", onError);
347
+ settle();
309
348
  const error = videoEl.error;
310
349
  reject(new Error(error?.message || "Failed to load video source"));
311
350
  };
351
+ if (load_timeout_ms > 0) {
352
+ watchdog = setTimeout(() => {
353
+ settle();
354
+ reject(new Error("Video took too long to load (network timeout)"));
355
+ }, load_timeout_ms);
356
+ }
312
357
  videoEl.addEventListener("loadedmetadata", onLoaded);
313
358
  videoEl.addEventListener("error", onError);
314
359
  videoEl.src = src;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scarlett-player/native",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "Native Video Provider Plugin for Scarlett Player (MP4, WebM, MOV, MKV)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -17,13 +17,13 @@
17
17
  "dist"
18
18
  ],
19
19
  "peerDependencies": {
20
- "@scarlett-player/core": "^1.0.2"
20
+ "@scarlett-player/core": "^1.0.3"
21
21
  },
22
22
  "devDependencies": {
23
23
  "typescript": "^5.3.0",
24
24
  "tsup": "^8.0.0",
25
25
  "vitest": "^1.6.0",
26
- "@scarlett-player/core": "1.0.2"
26
+ "@scarlett-player/core": "1.2.0"
27
27
  },
28
28
  "keywords": [
29
29
  "video",