@scarlett-player/native 0.1.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/dist/index.cjs +288 -0
- package/dist/index.d.cts +49 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +263 -0
- package/package.json +53 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
createNativePlugin: () => createNativePlugin,
|
|
24
|
+
default: () => index_default
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
var SUPPORTED_EXTENSIONS = ["mp4", "webm", "mov", "mkv", "ogv", "ogg", "m4v"];
|
|
28
|
+
var MIME_TYPES = {
|
|
29
|
+
mp4: "video/mp4",
|
|
30
|
+
m4v: "video/mp4",
|
|
31
|
+
webm: "video/webm",
|
|
32
|
+
mov: "video/quicktime",
|
|
33
|
+
mkv: "video/x-matroska",
|
|
34
|
+
ogv: "video/ogg",
|
|
35
|
+
ogg: "video/ogg"
|
|
36
|
+
};
|
|
37
|
+
function createNativePlugin(config) {
|
|
38
|
+
const preload = config?.preload ?? "metadata";
|
|
39
|
+
let api = null;
|
|
40
|
+
let video = null;
|
|
41
|
+
let cleanupEvents = null;
|
|
42
|
+
const getExtension = (src) => {
|
|
43
|
+
try {
|
|
44
|
+
const url = new URL(src, window.location.href);
|
|
45
|
+
const pathname = url.pathname;
|
|
46
|
+
const ext = pathname.split(".").pop()?.toLowerCase() ?? "";
|
|
47
|
+
return ext;
|
|
48
|
+
} catch {
|
|
49
|
+
const rawExt = src.split(".").pop()?.toLowerCase() ?? "";
|
|
50
|
+
return rawExt.split("?")[0] ?? "";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const getMimeType = (ext) => {
|
|
54
|
+
return MIME_TYPES[ext] || "video/mp4";
|
|
55
|
+
};
|
|
56
|
+
const canBrowserPlay = (mimeType) => {
|
|
57
|
+
const testVideo = document.createElement("video");
|
|
58
|
+
const canPlay = testVideo.canPlayType(mimeType);
|
|
59
|
+
return canPlay === "probably" || canPlay === "maybe";
|
|
60
|
+
};
|
|
61
|
+
const getOrCreateVideo = () => {
|
|
62
|
+
if (video) return video;
|
|
63
|
+
const existing = api?.container.querySelector("video");
|
|
64
|
+
if (existing) {
|
|
65
|
+
video = existing;
|
|
66
|
+
return video;
|
|
67
|
+
}
|
|
68
|
+
video = document.createElement("video");
|
|
69
|
+
video.style.cssText = "width:100%;height:100%;display:block;object-fit:contain;background:#000";
|
|
70
|
+
video.preload = preload;
|
|
71
|
+
video.controls = false;
|
|
72
|
+
video.playsInline = true;
|
|
73
|
+
api?.container.appendChild(video);
|
|
74
|
+
return video;
|
|
75
|
+
};
|
|
76
|
+
const setupEventListeners = (videoEl) => {
|
|
77
|
+
const handlers = [];
|
|
78
|
+
const on = (event, handler) => {
|
|
79
|
+
videoEl.addEventListener(event, handler);
|
|
80
|
+
handlers.push([event, handler]);
|
|
81
|
+
};
|
|
82
|
+
on("playing", () => {
|
|
83
|
+
api?.setState("playing", true);
|
|
84
|
+
api?.setState("paused", false);
|
|
85
|
+
api?.emit("playback:play", void 0);
|
|
86
|
+
});
|
|
87
|
+
on("pause", () => {
|
|
88
|
+
api?.setState("playing", false);
|
|
89
|
+
api?.setState("paused", true);
|
|
90
|
+
api?.emit("playback:pause", void 0);
|
|
91
|
+
});
|
|
92
|
+
on("ended", () => {
|
|
93
|
+
api?.setState("playing", false);
|
|
94
|
+
api?.setState("ended", true);
|
|
95
|
+
api?.emit("playback:ended", void 0);
|
|
96
|
+
});
|
|
97
|
+
on("timeupdate", () => {
|
|
98
|
+
api?.setState("currentTime", videoEl.currentTime);
|
|
99
|
+
api?.emit("playback:timeupdate", { currentTime: videoEl.currentTime });
|
|
100
|
+
});
|
|
101
|
+
on("durationchange", () => {
|
|
102
|
+
api?.setState("duration", videoEl.duration || 0);
|
|
103
|
+
});
|
|
104
|
+
on("loadedmetadata", () => {
|
|
105
|
+
api?.setState("duration", videoEl.duration || 0);
|
|
106
|
+
api?.emit("media:loadedmetadata", { duration: videoEl.duration || 0 });
|
|
107
|
+
});
|
|
108
|
+
on("canplay", () => {
|
|
109
|
+
api?.setState("buffering", false);
|
|
110
|
+
api?.emit("media:canplay", void 0);
|
|
111
|
+
});
|
|
112
|
+
on("canplaythrough", () => {
|
|
113
|
+
api?.emit("media:canplaythrough", void 0);
|
|
114
|
+
});
|
|
115
|
+
on("waiting", () => {
|
|
116
|
+
api?.setState("buffering", true);
|
|
117
|
+
api?.emit("media:waiting", void 0);
|
|
118
|
+
});
|
|
119
|
+
on("progress", () => {
|
|
120
|
+
if (videoEl.buffered.length > 0) {
|
|
121
|
+
const bufferedEnd = videoEl.buffered.end(videoEl.buffered.length - 1);
|
|
122
|
+
const duration = videoEl.duration || 0;
|
|
123
|
+
const buffered = duration > 0 ? bufferedEnd / duration : 0;
|
|
124
|
+
api?.setState("bufferedAmount", buffered);
|
|
125
|
+
api?.emit("media:progress", { buffered });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
on("seeking", () => {
|
|
129
|
+
api?.setState("seeking", true);
|
|
130
|
+
});
|
|
131
|
+
on("seeked", () => {
|
|
132
|
+
api?.setState("seeking", false);
|
|
133
|
+
api?.emit("playback:seeked", { time: videoEl.currentTime });
|
|
134
|
+
});
|
|
135
|
+
on("volumechange", () => {
|
|
136
|
+
api?.setState("volume", videoEl.volume);
|
|
137
|
+
api?.setState("muted", videoEl.muted);
|
|
138
|
+
api?.emit("volume:change", { volume: videoEl.volume, muted: videoEl.muted });
|
|
139
|
+
});
|
|
140
|
+
on("ratechange", () => {
|
|
141
|
+
api?.setState("playbackRate", videoEl.playbackRate);
|
|
142
|
+
api?.emit("playback:ratechange", { rate: videoEl.playbackRate });
|
|
143
|
+
});
|
|
144
|
+
on("error", () => {
|
|
145
|
+
const error = videoEl.error;
|
|
146
|
+
let message = "Unknown video error";
|
|
147
|
+
if (error) {
|
|
148
|
+
switch (error.code) {
|
|
149
|
+
case MediaError.MEDIA_ERR_ABORTED:
|
|
150
|
+
message = "Playback aborted";
|
|
151
|
+
break;
|
|
152
|
+
case MediaError.MEDIA_ERR_NETWORK:
|
|
153
|
+
message = "Network error";
|
|
154
|
+
break;
|
|
155
|
+
case MediaError.MEDIA_ERR_DECODE:
|
|
156
|
+
message = "Decode error - format may not be supported";
|
|
157
|
+
break;
|
|
158
|
+
case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
|
|
159
|
+
message = "Format not supported";
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
api?.logger.error("Video error", { code: error?.code, message });
|
|
164
|
+
api?.emit("error", {
|
|
165
|
+
code: "MEDIA_ERROR",
|
|
166
|
+
message,
|
|
167
|
+
fatal: true,
|
|
168
|
+
timestamp: Date.now()
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
return () => {
|
|
172
|
+
handlers.forEach(([event, handler]) => {
|
|
173
|
+
videoEl.removeEventListener(event, handler);
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
const cleanup = () => {
|
|
178
|
+
cleanupEvents?.();
|
|
179
|
+
cleanupEvents = null;
|
|
180
|
+
if (video) {
|
|
181
|
+
video.pause();
|
|
182
|
+
video.removeAttribute("src");
|
|
183
|
+
video.load();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const plugin = {
|
|
187
|
+
id: "native-provider",
|
|
188
|
+
name: "Native Video Provider",
|
|
189
|
+
version: "1.0.0",
|
|
190
|
+
type: "provider",
|
|
191
|
+
description: "Native HTML5 video playback for MP4, WebM, MOV, MKV",
|
|
192
|
+
canPlay(src) {
|
|
193
|
+
const ext = getExtension(src);
|
|
194
|
+
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
const mimeType = getMimeType(ext);
|
|
198
|
+
return canBrowserPlay(mimeType);
|
|
199
|
+
},
|
|
200
|
+
async init(pluginApi) {
|
|
201
|
+
api = pluginApi;
|
|
202
|
+
api.logger.info("Native video plugin initialized");
|
|
203
|
+
const unsubPlay = api.on("playback:play", async () => {
|
|
204
|
+
if (!video) return;
|
|
205
|
+
try {
|
|
206
|
+
await video.play();
|
|
207
|
+
} catch (e) {
|
|
208
|
+
api?.logger.error("Play failed", e);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
const unsubPause = api.on("playback:pause", () => {
|
|
212
|
+
video?.pause();
|
|
213
|
+
});
|
|
214
|
+
const unsubSeek = api.on("playback:seeking", ({ time }) => {
|
|
215
|
+
if (!video) return;
|
|
216
|
+
const clampedTime = Math.max(0, Math.min(time, video.duration || 0));
|
|
217
|
+
video.currentTime = clampedTime;
|
|
218
|
+
});
|
|
219
|
+
const unsubVolume = api.on("volume:change", ({ volume, muted }) => {
|
|
220
|
+
if (video) {
|
|
221
|
+
video.volume = volume;
|
|
222
|
+
video.muted = muted;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
const unsubMute = api.on("volume:mute", ({ muted }) => {
|
|
226
|
+
if (video) video.muted = muted;
|
|
227
|
+
});
|
|
228
|
+
const unsubRate = api.on("playback:ratechange", ({ rate }) => {
|
|
229
|
+
if (video) video.playbackRate = rate;
|
|
230
|
+
});
|
|
231
|
+
api.onDestroy(() => {
|
|
232
|
+
unsubPlay();
|
|
233
|
+
unsubPause();
|
|
234
|
+
unsubSeek();
|
|
235
|
+
unsubVolume();
|
|
236
|
+
unsubMute();
|
|
237
|
+
unsubRate();
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
async destroy() {
|
|
241
|
+
api?.logger.info("Native video plugin destroying");
|
|
242
|
+
cleanup();
|
|
243
|
+
if (video?.parentNode) {
|
|
244
|
+
video.parentNode.removeChild(video);
|
|
245
|
+
}
|
|
246
|
+
video = null;
|
|
247
|
+
api = null;
|
|
248
|
+
},
|
|
249
|
+
async loadSource(src) {
|
|
250
|
+
if (!api) throw new Error("Plugin not initialized");
|
|
251
|
+
const ext = getExtension(src);
|
|
252
|
+
const mimeType = getMimeType(ext);
|
|
253
|
+
api.logger.info("Loading native video source", { src, mimeType });
|
|
254
|
+
cleanup();
|
|
255
|
+
api.setState("playbackState", "loading");
|
|
256
|
+
api.setState("buffering", true);
|
|
257
|
+
const videoEl = getOrCreateVideo();
|
|
258
|
+
cleanupEvents = setupEventListeners(videoEl);
|
|
259
|
+
return new Promise((resolve, reject) => {
|
|
260
|
+
const onLoaded = () => {
|
|
261
|
+
videoEl.removeEventListener("loadedmetadata", onLoaded);
|
|
262
|
+
videoEl.removeEventListener("error", onError);
|
|
263
|
+
api?.setState("source", { src, type: mimeType });
|
|
264
|
+
api?.setState("playbackState", "ready");
|
|
265
|
+
api?.setState("buffering", false);
|
|
266
|
+
api?.emit("media:loaded", { src, type: mimeType });
|
|
267
|
+
resolve();
|
|
268
|
+
};
|
|
269
|
+
const onError = () => {
|
|
270
|
+
videoEl.removeEventListener("loadedmetadata", onLoaded);
|
|
271
|
+
videoEl.removeEventListener("error", onError);
|
|
272
|
+
const error = videoEl.error;
|
|
273
|
+
reject(new Error(error?.message || "Failed to load video source"));
|
|
274
|
+
};
|
|
275
|
+
videoEl.addEventListener("loadedmetadata", onLoaded);
|
|
276
|
+
videoEl.addEventListener("error", onError);
|
|
277
|
+
videoEl.src = src;
|
|
278
|
+
videoEl.load();
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
return plugin;
|
|
283
|
+
}
|
|
284
|
+
var index_default = createNativePlugin;
|
|
285
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
286
|
+
0 && (module.exports = {
|
|
287
|
+
createNativePlugin
|
|
288
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { PluginType, IPluginAPI } from '@scarlett-player/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Native Video Provider Plugin for Scarlett Player
|
|
5
|
+
*
|
|
6
|
+
* Provides playback for native browser-supported formats:
|
|
7
|
+
* - MP4 (H.264/AAC)
|
|
8
|
+
* - WebM (VP8/VP9/Opus)
|
|
9
|
+
* - MOV (H.264/AAC)
|
|
10
|
+
* - MKV (varies by browser)
|
|
11
|
+
* - OGG/OGV (Theora/Vorbis)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface NativePluginConfig {
|
|
15
|
+
/** Preload behavior: 'none' | 'metadata' | 'auto' */
|
|
16
|
+
preload?: 'none' | 'metadata' | 'auto';
|
|
17
|
+
}
|
|
18
|
+
interface INativePlugin {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
version: string;
|
|
22
|
+
type: PluginType;
|
|
23
|
+
description: string;
|
|
24
|
+
canPlay(src: string): boolean;
|
|
25
|
+
init(api: IPluginAPI): Promise<void>;
|
|
26
|
+
destroy(): Promise<void>;
|
|
27
|
+
loadSource(src: string): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a Native Video Provider Plugin instance.
|
|
31
|
+
*
|
|
32
|
+
* @param config - Plugin configuration
|
|
33
|
+
* @returns Native Plugin instance
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* import { createNativePlugin } from '@scarlett-player/native';
|
|
38
|
+
*
|
|
39
|
+
* const player = new ScarlettPlayer({
|
|
40
|
+
* container: document.getElementById('player'),
|
|
41
|
+
* plugins: [createNativePlugin()],
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* await player.load('video.mp4');
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
declare function createNativePlugin(config?: NativePluginConfig): INativePlugin;
|
|
48
|
+
|
|
49
|
+
export { type INativePlugin, type NativePluginConfig, createNativePlugin, createNativePlugin as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { PluginType, IPluginAPI } from '@scarlett-player/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Native Video Provider Plugin for Scarlett Player
|
|
5
|
+
*
|
|
6
|
+
* Provides playback for native browser-supported formats:
|
|
7
|
+
* - MP4 (H.264/AAC)
|
|
8
|
+
* - WebM (VP8/VP9/Opus)
|
|
9
|
+
* - MOV (H.264/AAC)
|
|
10
|
+
* - MKV (varies by browser)
|
|
11
|
+
* - OGG/OGV (Theora/Vorbis)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface NativePluginConfig {
|
|
15
|
+
/** Preload behavior: 'none' | 'metadata' | 'auto' */
|
|
16
|
+
preload?: 'none' | 'metadata' | 'auto';
|
|
17
|
+
}
|
|
18
|
+
interface INativePlugin {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
version: string;
|
|
22
|
+
type: PluginType;
|
|
23
|
+
description: string;
|
|
24
|
+
canPlay(src: string): boolean;
|
|
25
|
+
init(api: IPluginAPI): Promise<void>;
|
|
26
|
+
destroy(): Promise<void>;
|
|
27
|
+
loadSource(src: string): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a Native Video Provider Plugin instance.
|
|
31
|
+
*
|
|
32
|
+
* @param config - Plugin configuration
|
|
33
|
+
* @returns Native Plugin instance
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* import { createNativePlugin } from '@scarlett-player/native';
|
|
38
|
+
*
|
|
39
|
+
* const player = new ScarlettPlayer({
|
|
40
|
+
* container: document.getElementById('player'),
|
|
41
|
+
* plugins: [createNativePlugin()],
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* await player.load('video.mp4');
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
declare function createNativePlugin(config?: NativePluginConfig): INativePlugin;
|
|
48
|
+
|
|
49
|
+
export { type INativePlugin, type NativePluginConfig, createNativePlugin, createNativePlugin as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var SUPPORTED_EXTENSIONS = ["mp4", "webm", "mov", "mkv", "ogv", "ogg", "m4v"];
|
|
3
|
+
var MIME_TYPES = {
|
|
4
|
+
mp4: "video/mp4",
|
|
5
|
+
m4v: "video/mp4",
|
|
6
|
+
webm: "video/webm",
|
|
7
|
+
mov: "video/quicktime",
|
|
8
|
+
mkv: "video/x-matroska",
|
|
9
|
+
ogv: "video/ogg",
|
|
10
|
+
ogg: "video/ogg"
|
|
11
|
+
};
|
|
12
|
+
function createNativePlugin(config) {
|
|
13
|
+
const preload = config?.preload ?? "metadata";
|
|
14
|
+
let api = null;
|
|
15
|
+
let video = null;
|
|
16
|
+
let cleanupEvents = null;
|
|
17
|
+
const getExtension = (src) => {
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(src, window.location.href);
|
|
20
|
+
const pathname = url.pathname;
|
|
21
|
+
const ext = pathname.split(".").pop()?.toLowerCase() ?? "";
|
|
22
|
+
return ext;
|
|
23
|
+
} catch {
|
|
24
|
+
const rawExt = src.split(".").pop()?.toLowerCase() ?? "";
|
|
25
|
+
return rawExt.split("?")[0] ?? "";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const getMimeType = (ext) => {
|
|
29
|
+
return MIME_TYPES[ext] || "video/mp4";
|
|
30
|
+
};
|
|
31
|
+
const canBrowserPlay = (mimeType) => {
|
|
32
|
+
const testVideo = document.createElement("video");
|
|
33
|
+
const canPlay = testVideo.canPlayType(mimeType);
|
|
34
|
+
return canPlay === "probably" || canPlay === "maybe";
|
|
35
|
+
};
|
|
36
|
+
const getOrCreateVideo = () => {
|
|
37
|
+
if (video) return video;
|
|
38
|
+
const existing = api?.container.querySelector("video");
|
|
39
|
+
if (existing) {
|
|
40
|
+
video = existing;
|
|
41
|
+
return video;
|
|
42
|
+
}
|
|
43
|
+
video = document.createElement("video");
|
|
44
|
+
video.style.cssText = "width:100%;height:100%;display:block;object-fit:contain;background:#000";
|
|
45
|
+
video.preload = preload;
|
|
46
|
+
video.controls = false;
|
|
47
|
+
video.playsInline = true;
|
|
48
|
+
api?.container.appendChild(video);
|
|
49
|
+
return video;
|
|
50
|
+
};
|
|
51
|
+
const setupEventListeners = (videoEl) => {
|
|
52
|
+
const handlers = [];
|
|
53
|
+
const on = (event, handler) => {
|
|
54
|
+
videoEl.addEventListener(event, handler);
|
|
55
|
+
handlers.push([event, handler]);
|
|
56
|
+
};
|
|
57
|
+
on("playing", () => {
|
|
58
|
+
api?.setState("playing", true);
|
|
59
|
+
api?.setState("paused", false);
|
|
60
|
+
api?.emit("playback:play", void 0);
|
|
61
|
+
});
|
|
62
|
+
on("pause", () => {
|
|
63
|
+
api?.setState("playing", false);
|
|
64
|
+
api?.setState("paused", true);
|
|
65
|
+
api?.emit("playback:pause", void 0);
|
|
66
|
+
});
|
|
67
|
+
on("ended", () => {
|
|
68
|
+
api?.setState("playing", false);
|
|
69
|
+
api?.setState("ended", true);
|
|
70
|
+
api?.emit("playback:ended", void 0);
|
|
71
|
+
});
|
|
72
|
+
on("timeupdate", () => {
|
|
73
|
+
api?.setState("currentTime", videoEl.currentTime);
|
|
74
|
+
api?.emit("playback:timeupdate", { currentTime: videoEl.currentTime });
|
|
75
|
+
});
|
|
76
|
+
on("durationchange", () => {
|
|
77
|
+
api?.setState("duration", videoEl.duration || 0);
|
|
78
|
+
});
|
|
79
|
+
on("loadedmetadata", () => {
|
|
80
|
+
api?.setState("duration", videoEl.duration || 0);
|
|
81
|
+
api?.emit("media:loadedmetadata", { duration: videoEl.duration || 0 });
|
|
82
|
+
});
|
|
83
|
+
on("canplay", () => {
|
|
84
|
+
api?.setState("buffering", false);
|
|
85
|
+
api?.emit("media:canplay", void 0);
|
|
86
|
+
});
|
|
87
|
+
on("canplaythrough", () => {
|
|
88
|
+
api?.emit("media:canplaythrough", void 0);
|
|
89
|
+
});
|
|
90
|
+
on("waiting", () => {
|
|
91
|
+
api?.setState("buffering", true);
|
|
92
|
+
api?.emit("media:waiting", void 0);
|
|
93
|
+
});
|
|
94
|
+
on("progress", () => {
|
|
95
|
+
if (videoEl.buffered.length > 0) {
|
|
96
|
+
const bufferedEnd = videoEl.buffered.end(videoEl.buffered.length - 1);
|
|
97
|
+
const duration = videoEl.duration || 0;
|
|
98
|
+
const buffered = duration > 0 ? bufferedEnd / duration : 0;
|
|
99
|
+
api?.setState("bufferedAmount", buffered);
|
|
100
|
+
api?.emit("media:progress", { buffered });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
on("seeking", () => {
|
|
104
|
+
api?.setState("seeking", true);
|
|
105
|
+
});
|
|
106
|
+
on("seeked", () => {
|
|
107
|
+
api?.setState("seeking", false);
|
|
108
|
+
api?.emit("playback:seeked", { time: videoEl.currentTime });
|
|
109
|
+
});
|
|
110
|
+
on("volumechange", () => {
|
|
111
|
+
api?.setState("volume", videoEl.volume);
|
|
112
|
+
api?.setState("muted", videoEl.muted);
|
|
113
|
+
api?.emit("volume:change", { volume: videoEl.volume, muted: videoEl.muted });
|
|
114
|
+
});
|
|
115
|
+
on("ratechange", () => {
|
|
116
|
+
api?.setState("playbackRate", videoEl.playbackRate);
|
|
117
|
+
api?.emit("playback:ratechange", { rate: videoEl.playbackRate });
|
|
118
|
+
});
|
|
119
|
+
on("error", () => {
|
|
120
|
+
const error = videoEl.error;
|
|
121
|
+
let message = "Unknown video error";
|
|
122
|
+
if (error) {
|
|
123
|
+
switch (error.code) {
|
|
124
|
+
case MediaError.MEDIA_ERR_ABORTED:
|
|
125
|
+
message = "Playback aborted";
|
|
126
|
+
break;
|
|
127
|
+
case MediaError.MEDIA_ERR_NETWORK:
|
|
128
|
+
message = "Network error";
|
|
129
|
+
break;
|
|
130
|
+
case MediaError.MEDIA_ERR_DECODE:
|
|
131
|
+
message = "Decode error - format may not be supported";
|
|
132
|
+
break;
|
|
133
|
+
case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
|
|
134
|
+
message = "Format not supported";
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
api?.logger.error("Video error", { code: error?.code, message });
|
|
139
|
+
api?.emit("error", {
|
|
140
|
+
code: "MEDIA_ERROR",
|
|
141
|
+
message,
|
|
142
|
+
fatal: true,
|
|
143
|
+
timestamp: Date.now()
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
return () => {
|
|
147
|
+
handlers.forEach(([event, handler]) => {
|
|
148
|
+
videoEl.removeEventListener(event, handler);
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
const cleanup = () => {
|
|
153
|
+
cleanupEvents?.();
|
|
154
|
+
cleanupEvents = null;
|
|
155
|
+
if (video) {
|
|
156
|
+
video.pause();
|
|
157
|
+
video.removeAttribute("src");
|
|
158
|
+
video.load();
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const plugin = {
|
|
162
|
+
id: "native-provider",
|
|
163
|
+
name: "Native Video Provider",
|
|
164
|
+
version: "1.0.0",
|
|
165
|
+
type: "provider",
|
|
166
|
+
description: "Native HTML5 video playback for MP4, WebM, MOV, MKV",
|
|
167
|
+
canPlay(src) {
|
|
168
|
+
const ext = getExtension(src);
|
|
169
|
+
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
const mimeType = getMimeType(ext);
|
|
173
|
+
return canBrowserPlay(mimeType);
|
|
174
|
+
},
|
|
175
|
+
async init(pluginApi) {
|
|
176
|
+
api = pluginApi;
|
|
177
|
+
api.logger.info("Native video plugin initialized");
|
|
178
|
+
const unsubPlay = api.on("playback:play", async () => {
|
|
179
|
+
if (!video) return;
|
|
180
|
+
try {
|
|
181
|
+
await video.play();
|
|
182
|
+
} catch (e) {
|
|
183
|
+
api?.logger.error("Play failed", e);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
const unsubPause = api.on("playback:pause", () => {
|
|
187
|
+
video?.pause();
|
|
188
|
+
});
|
|
189
|
+
const unsubSeek = api.on("playback:seeking", ({ time }) => {
|
|
190
|
+
if (!video) return;
|
|
191
|
+
const clampedTime = Math.max(0, Math.min(time, video.duration || 0));
|
|
192
|
+
video.currentTime = clampedTime;
|
|
193
|
+
});
|
|
194
|
+
const unsubVolume = api.on("volume:change", ({ volume, muted }) => {
|
|
195
|
+
if (video) {
|
|
196
|
+
video.volume = volume;
|
|
197
|
+
video.muted = muted;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
const unsubMute = api.on("volume:mute", ({ muted }) => {
|
|
201
|
+
if (video) video.muted = muted;
|
|
202
|
+
});
|
|
203
|
+
const unsubRate = api.on("playback:ratechange", ({ rate }) => {
|
|
204
|
+
if (video) video.playbackRate = rate;
|
|
205
|
+
});
|
|
206
|
+
api.onDestroy(() => {
|
|
207
|
+
unsubPlay();
|
|
208
|
+
unsubPause();
|
|
209
|
+
unsubSeek();
|
|
210
|
+
unsubVolume();
|
|
211
|
+
unsubMute();
|
|
212
|
+
unsubRate();
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
async destroy() {
|
|
216
|
+
api?.logger.info("Native video plugin destroying");
|
|
217
|
+
cleanup();
|
|
218
|
+
if (video?.parentNode) {
|
|
219
|
+
video.parentNode.removeChild(video);
|
|
220
|
+
}
|
|
221
|
+
video = null;
|
|
222
|
+
api = null;
|
|
223
|
+
},
|
|
224
|
+
async loadSource(src) {
|
|
225
|
+
if (!api) throw new Error("Plugin not initialized");
|
|
226
|
+
const ext = getExtension(src);
|
|
227
|
+
const mimeType = getMimeType(ext);
|
|
228
|
+
api.logger.info("Loading native video source", { src, mimeType });
|
|
229
|
+
cleanup();
|
|
230
|
+
api.setState("playbackState", "loading");
|
|
231
|
+
api.setState("buffering", true);
|
|
232
|
+
const videoEl = getOrCreateVideo();
|
|
233
|
+
cleanupEvents = setupEventListeners(videoEl);
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
const onLoaded = () => {
|
|
236
|
+
videoEl.removeEventListener("loadedmetadata", onLoaded);
|
|
237
|
+
videoEl.removeEventListener("error", onError);
|
|
238
|
+
api?.setState("source", { src, type: mimeType });
|
|
239
|
+
api?.setState("playbackState", "ready");
|
|
240
|
+
api?.setState("buffering", false);
|
|
241
|
+
api?.emit("media:loaded", { src, type: mimeType });
|
|
242
|
+
resolve();
|
|
243
|
+
};
|
|
244
|
+
const onError = () => {
|
|
245
|
+
videoEl.removeEventListener("loadedmetadata", onLoaded);
|
|
246
|
+
videoEl.removeEventListener("error", onError);
|
|
247
|
+
const error = videoEl.error;
|
|
248
|
+
reject(new Error(error?.message || "Failed to load video source"));
|
|
249
|
+
};
|
|
250
|
+
videoEl.addEventListener("loadedmetadata", onLoaded);
|
|
251
|
+
videoEl.addEventListener("error", onError);
|
|
252
|
+
videoEl.src = src;
|
|
253
|
+
videoEl.load();
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
return plugin;
|
|
258
|
+
}
|
|
259
|
+
var index_default = createNativePlugin;
|
|
260
|
+
export {
|
|
261
|
+
createNativePlugin,
|
|
262
|
+
index_default as default
|
|
263
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scarlett-player/native",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Native Video Provider Plugin for Scarlett Player (MP4, WebM, MOV, MKV)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
21
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
22
|
+
"test": "vitest --run",
|
|
23
|
+
"test:watch": "vitest"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@scarlett-player/core": "^0.1.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@scarlett-player/core": "file:../../core",
|
|
30
|
+
"typescript": "^5.3.0",
|
|
31
|
+
"tsup": "^8.0.0",
|
|
32
|
+
"vitest": "^1.6.0"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"video",
|
|
36
|
+
"player",
|
|
37
|
+
"mp4",
|
|
38
|
+
"webm",
|
|
39
|
+
"native",
|
|
40
|
+
"scarlett"
|
|
41
|
+
],
|
|
42
|
+
"author": "The Stream Platform",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/Hackney-Enterprises-Inc/scarlett-player.git",
|
|
47
|
+
"directory": "packages/plugins/native"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/Hackney-Enterprises-Inc/scarlett-player/issues"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/Hackney-Enterprises-Inc/scarlett-player#readme"
|
|
53
|
+
}
|