@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 +8 -0
- package/dist/index.cjs +55 -10
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +55 -10
- package/package.json +3 -3
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
26
|
+
"@scarlett-player/core": "1.2.0"
|
|
27
27
|
},
|
|
28
28
|
"keywords": [
|
|
29
29
|
"video",
|