artplayer-plugin-p2p 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.
package/dist/index.mjs ADDED
@@ -0,0 +1,677 @@
1
+ import {
2
+ BANDWIDTH_WINDOW_MS,
3
+ DEFAULT_FATAL_RETRY_MAX,
4
+ DEFAULT_TYPE,
5
+ P2PStatsEngine,
6
+ P2P_EVENT_BRIDGE_MAP,
7
+ STATS_POLLING_MS,
8
+ __privateAdd,
9
+ __privateGet,
10
+ __privateMethod,
11
+ __privateSet,
12
+ applyRuntimeToggle,
13
+ mergeCoreConfig,
14
+ resolveOptions
15
+ } from "./chunk-IX2RPNML.mjs";
16
+
17
+ // src/index.ts
18
+ import Hls2 from "hls.js";
19
+
20
+ // src/controller.ts
21
+ import Hls from "hls.js";
22
+
23
+ // src/bridge.ts
24
+ function attachEventBridge(art, engine, stats) {
25
+ const subscribe = engine.addEventListener.bind(engine);
26
+ for (const [eventName, artEventName] of Object.entries(P2P_EVENT_BRIDGE_MAP)) {
27
+ subscribe(eventName, (...args) => {
28
+ art.emit(artEventName, ...args);
29
+ });
30
+ }
31
+ engine.addEventListener("onChunkDownloaded", (bytesLength, downloadSource) => {
32
+ stats.recordDownload(bytesLength, downloadSource);
33
+ });
34
+ engine.addEventListener("onChunkUploaded", (bytesLength) => {
35
+ stats.recordUpload(bytesLength);
36
+ });
37
+ engine.addEventListener("onPeerConnect", () => {
38
+ stats.recordPeerConnect();
39
+ });
40
+ engine.addEventListener("onPeerClose", () => {
41
+ stats.recordPeerClose();
42
+ });
43
+ }
44
+
45
+ // src/engine.ts
46
+ import { HlsJsP2PEngine } from "p2p-media-loader-hlsjs";
47
+
48
+ // src/recovery.ts
49
+ var _retryCount, _isMediaRecoveryAttempted, _options, _FatalRecoveryPolicy_instances, handleFatal_fn;
50
+ var FatalRecoveryPolicy = class {
51
+ /**
52
+ * @param hls - 目标 hls.js 实例
53
+ * @param hlsConstructor - hls.js 构造器(Events/ErrorTypes 枚举来源,
54
+ * 避免本模块引入 hls.js 运行时值以保持可移植性)
55
+ * @param options - 回调集合
56
+ */
57
+ constructor(hls, hlsConstructor, options) {
58
+ __privateAdd(this, _FatalRecoveryPolicy_instances);
59
+ __privateAdd(this, _retryCount, 0);
60
+ __privateAdd(this, _isMediaRecoveryAttempted, false);
61
+ __privateAdd(this, _options);
62
+ __privateSet(this, _options, options);
63
+ hls.on(hlsConstructor.Events.ERROR, (_event, data) => {
64
+ if (!data.fatal) return;
65
+ __privateMethod(this, _FatalRecoveryPolicy_instances, handleFatal_fn).call(this, hls, hlsConstructor, data);
66
+ });
67
+ }
68
+ };
69
+ _retryCount = new WeakMap();
70
+ _isMediaRecoveryAttempted = new WeakMap();
71
+ _options = new WeakMap();
72
+ _FatalRecoveryPolicy_instances = new WeakSet();
73
+ /**
74
+ * fatal 分级处理
75
+ *
76
+ * 网络级:重新拉起加载;媒体级:软恢复(连续失败时切换音频编解码);
77
+ * 其他:软恢复无效,交由上层重建。超限后停止一切动作,
78
+ * 仅保留通知,由宿主决策后续(如提示用户或换源)
79
+ */
80
+ handleFatal_fn = function(hls, hlsConstructor, data) {
81
+ const { retryMax, onFatalError, onUnrecoverable } = __privateGet(this, _options);
82
+ if (__privateGet(this, _retryCount) >= retryMax) {
83
+ onFatalError(data, __privateGet(this, _retryCount));
84
+ return;
85
+ }
86
+ __privateSet(this, _retryCount, __privateGet(this, _retryCount) + 1);
87
+ onFatalError(data, __privateGet(this, _retryCount));
88
+ const { ErrorTypes } = hlsConstructor;
89
+ if (data.type === ErrorTypes.NETWORK_ERROR) {
90
+ hls.startLoad();
91
+ } else if (data.type === ErrorTypes.MEDIA_ERROR) {
92
+ if (__privateGet(this, _isMediaRecoveryAttempted)) {
93
+ hls.swapAudioCodec();
94
+ }
95
+ hls.recoverMediaError();
96
+ __privateSet(this, _isMediaRecoveryAttempted, true);
97
+ } else {
98
+ onUnrecoverable(data, __privateGet(this, _retryCount));
99
+ }
100
+ };
101
+
102
+ // src/engine.ts
103
+ var hlsWithP2PCache = /* @__PURE__ */ new WeakMap();
104
+ function getHlsWithP2PClass(hlsConstructor) {
105
+ let cached = hlsWithP2PCache.get(hlsConstructor);
106
+ if (!cached) {
107
+ cached = HlsJsP2PEngine.injectMixin(hlsConstructor);
108
+ hlsWithP2PCache.set(hlsConstructor, cached);
109
+ }
110
+ return cached;
111
+ }
112
+ function createHlsWithP2P(options, hlsConstructor, hooks) {
113
+ const HlsWithP2PClass = getHlsWithP2PClass(hlsConstructor);
114
+ const hls = new HlsWithP2PClass({
115
+ ...options.hls,
116
+ p2p: {
117
+ core: applyRuntimeToggle(mergeCoreConfig(options), options.p2pEnabled, options.uploadEnabled),
118
+ onHlsJsCreated(instance) {
119
+ hooks.onEngineCreated(instance.p2pEngine);
120
+ }
121
+ }
122
+ });
123
+ new FatalRecoveryPolicy(hls, hlsConstructor, {
124
+ retryMax: options.fatalRetryMax ?? DEFAULT_FATAL_RETRY_MAX,
125
+ onFatalError: hooks.onFatalError,
126
+ onUnrecoverable: hooks.onUnrecoverable
127
+ });
128
+ return hls;
129
+ }
130
+
131
+ // src/controller.ts
132
+ var _art, _options2, _stats, _state, _p2pEnabled, _uploadEnabled, _recreateCount, _currentUrl, _currentVideo, _instance, _destroyHandler, _P2PController_instances, start_fn, stop_fn, emitStateChange_fn, handleUnrecoverable_fn;
133
+ var P2PController = class {
134
+ /**
135
+ * @param art - ArtPlayer 实例
136
+ * @param options - 解析后的插件选项(p2pEnabled / uploadEnabled 为初始开关状态)
137
+ * @param stats - 统计引擎实例(由入口层创建并共享给句柄)
138
+ */
139
+ constructor(art, options, stats) {
140
+ __privateAdd(this, _P2PController_instances);
141
+ __privateAdd(this, _art);
142
+ __privateAdd(this, _options2);
143
+ __privateAdd(this, _stats);
144
+ __privateAdd(this, _state, "idle");
145
+ __privateAdd(this, _p2pEnabled);
146
+ __privateAdd(this, _uploadEnabled);
147
+ /** 跨实例的 fatal 重建计数(外部显式激活时归零,防止无限重建循环) */
148
+ __privateAdd(this, _recreateCount, 0);
149
+ __privateAdd(this, _currentUrl);
150
+ __privateAdd(this, _currentVideo);
151
+ __privateAdd(this, _instance);
152
+ /** 当前实例在 art 上的 destroy 监听(实例切换时注销,避免累积监听) */
153
+ __privateAdd(this, _destroyHandler);
154
+ __privateSet(this, _art, art);
155
+ __privateSet(this, _options2, options);
156
+ __privateSet(this, _stats, stats);
157
+ __privateSet(this, _p2pEnabled, options.p2pEnabled);
158
+ __privateSet(this, _uploadEnabled, options.uploadEnabled);
159
+ }
160
+ /** 当前控制器状态 */
161
+ get state() {
162
+ return __privateGet(this, _state);
163
+ }
164
+ /** 当前 P2P 开关状态 */
165
+ get p2pEnabled() {
166
+ return __privateGet(this, _p2pEnabled);
167
+ }
168
+ /** 当前上传开关状态 */
169
+ get uploadEnabled() {
170
+ return __privateGet(this, _uploadEnabled);
171
+ }
172
+ /** 当前播放实例(与 art.hls 槽位同步) */
173
+ get hls() {
174
+ return __privateGet(this, _instance);
175
+ }
176
+ /**
177
+ * 外部显式激活(customType 回调:首次加载 / 换源 / 重连共用)
178
+ *
179
+ * 视为新的播放意图:统计与重建计数全部归零,
180
+ * 以控制器当前开关状态创建实例(不接受模式参数)
181
+ *
182
+ * @param url - 播放地址
183
+ * @param video - 视频元素
184
+ */
185
+ activate(url, video) {
186
+ if (__privateGet(this, _state) === "destroyed") return;
187
+ __privateMethod(this, _P2PController_instances, stop_fn).call(this);
188
+ __privateGet(this, _stats).reset();
189
+ __privateSet(this, _recreateCount, 0);
190
+ __privateMethod(this, _P2PController_instances, start_fn).call(this, url, video);
191
+ }
192
+ /** 销毁当前播放实例(注销监听、清空 art.hls 槽位) */
193
+ deactivate() {
194
+ if (__privateGet(this, _state) === "destroyed") return;
195
+ __privateMethod(this, _P2PController_instances, stop_fn).call(this);
196
+ }
197
+ /** 销毁现有实例后按最近地址与当前开关模式重建 */
198
+ reload() {
199
+ if (__privateGet(this, _state) === "destroyed" || __privateGet(this, _currentUrl) === void 0) return;
200
+ this.activate(__privateGet(this, _currentUrl), __privateGet(this, _currentVideo));
201
+ }
202
+ /**
203
+ * P2P 运行时开关:经 applyDynamicConfig 无损切换
204
+ *
205
+ * 不销毁实例、不中断播放;关闭时 HybridLoader 被引擎销毁、
206
+ * peer 连接全断,主动清零 peer 计数防止残留(迟到关闭事件
207
+ * 由统计引擎的非负保护兜底);字节统计冻结保留供模式对照
208
+ *
209
+ * @param enabled - 目标 P2P 状态
210
+ */
211
+ setP2PEnabled(enabled) {
212
+ if (__privateGet(this, _state) === "destroyed" || enabled === __privateGet(this, _p2pEnabled)) return;
213
+ __privateSet(this, _p2pEnabled, enabled);
214
+ const engine = __privateGet(this, _instance)?.p2pEngine;
215
+ if (engine) {
216
+ engine.applyDynamicConfig({ core: { isP2PDisabled: !enabled } });
217
+ if (!enabled) {
218
+ __privateGet(this, _stats).resetPeers();
219
+ }
220
+ }
221
+ __privateMethod(this, _P2PController_instances, emitStateChange_fn).call(this);
222
+ }
223
+ /**
224
+ * 上行运行时开关:仅信令广播(引擎内部处理),完全无缝
225
+ *
226
+ * @param enabled - 目标上传状态
227
+ */
228
+ setUploadEnabled(enabled) {
229
+ if (__privateGet(this, _state) === "destroyed" || enabled === __privateGet(this, _uploadEnabled)) return;
230
+ __privateSet(this, _uploadEnabled, enabled);
231
+ __privateGet(this, _instance)?.p2pEngine.applyDynamicConfig({ core: { isP2PUploadDisabled: !enabled } });
232
+ __privateMethod(this, _P2PController_instances, emitStateChange_fn).call(this);
233
+ }
234
+ /** 播放器销毁:终止一切并进入 destroyed(幂等) */
235
+ destroy() {
236
+ if (__privateGet(this, _state) === "destroyed") return;
237
+ __privateMethod(this, _P2PController_instances, stop_fn).call(this);
238
+ __privateSet(this, _state, "destroyed");
239
+ }
240
+ };
241
+ _art = new WeakMap();
242
+ _options2 = new WeakMap();
243
+ _stats = new WeakMap();
244
+ _state = new WeakMap();
245
+ _p2pEnabled = new WeakMap();
246
+ _uploadEnabled = new WeakMap();
247
+ _recreateCount = new WeakMap();
248
+ _currentUrl = new WeakMap();
249
+ _currentVideo = new WeakMap();
250
+ _instance = new WeakMap();
251
+ _destroyHandler = new WeakMap();
252
+ _P2PController_instances = new WeakSet();
253
+ /**
254
+ * 创建并绑定播放实例(内部路径,不重置任何状态)
255
+ *
256
+ * 构造 EngineOptions 时以控制器当前开关状态覆盖选项初始值:
257
+ * fatal 重建 / 换源 / 重连的实例重建均经此路径,
258
+ * 保证重建后 P2P 与上传模式与用户当前选择一致
259
+ *
260
+ * @param url - 播放地址
261
+ * @param video - 视频元素
262
+ */
263
+ start_fn = function(url, video) {
264
+ const engineOptions = {
265
+ core: __privateGet(this, _options2).core,
266
+ tracker: __privateGet(this, _options2).tracker,
267
+ hls: __privateGet(this, _options2).hls,
268
+ fatalRetryMax: __privateGet(this, _options2).fatalRetryMax,
269
+ p2pEnabled: __privateGet(this, _p2pEnabled),
270
+ uploadEnabled: __privateGet(this, _uploadEnabled)
271
+ };
272
+ const hooks = {
273
+ onEngineCreated: (engine) => {
274
+ attachEventBridge(__privateGet(this, _art), engine, __privateGet(this, _stats));
275
+ },
276
+ onFatalError: (data, retryCount) => {
277
+ __privateGet(this, _art).emit("p2p:fatalError", data, retryCount);
278
+ },
279
+ onUnrecoverable: (data, retryCount) => {
280
+ __privateMethod(this, _P2PController_instances, handleUnrecoverable_fn).call(this, data, retryCount);
281
+ }
282
+ };
283
+ const instance = createHlsWithP2P(engineOptions, Hls, hooks);
284
+ instance.loadSource(url);
285
+ instance.attachMedia(video);
286
+ __privateGet(this, _art).hls = instance;
287
+ __privateSet(this, _instance, instance);
288
+ __privateSet(this, _currentUrl, url);
289
+ __privateSet(this, _currentVideo, video);
290
+ __privateSet(this, _state, "active");
291
+ __privateSet(this, _destroyHandler, () => {
292
+ __privateSet(this, _instance, void 0);
293
+ __privateSet(this, _currentVideo, void 0);
294
+ __privateSet(this, _state, "destroyed");
295
+ instance.destroy();
296
+ });
297
+ __privateGet(this, _art).on("destroy", __privateGet(this, _destroyHandler));
298
+ };
299
+ /** 销毁当前实例并回到 idle(不改变开关模式与重建计数) */
300
+ stop_fn = function() {
301
+ if (__privateGet(this, _destroyHandler)) {
302
+ __privateGet(this, _art).off("destroy", __privateGet(this, _destroyHandler));
303
+ __privateSet(this, _destroyHandler, void 0);
304
+ }
305
+ const instance = __privateGet(this, _instance);
306
+ __privateSet(this, _instance, void 0);
307
+ __privateGet(this, _art).hls = void 0;
308
+ __privateSet(this, _state, "idle");
309
+ __privateGet(this, _stats).resetPeers();
310
+ if (instance) {
311
+ instance.destroy();
312
+ }
313
+ };
314
+ /** 开关状态变化后派发 p2p:stateChange,供宿主同步自定义 UI */
315
+ emitStateChange_fn = function() {
316
+ const details = {
317
+ p2pEnabled: __privateGet(this, _p2pEnabled),
318
+ uploadEnabled: __privateGet(this, _uploadEnabled)
319
+ };
320
+ __privateGet(this, _art).emit("p2p:stateChange", details);
321
+ };
322
+ /**
323
+ * 不可恢复 fatal 的有界重建
324
+ *
325
+ * 重建计数超限后停止动作,发出带原因的 fatalError
326
+ * 事件交由宿主决策(提示用户 / 换源)
327
+ */
328
+ handleUnrecoverable_fn = function(_data, _retryCount2) {
329
+ if (__privateGet(this, _recreateCount) >= __privateGet(this, _options2).fatalRetryMax) {
330
+ __privateGet(this, _art).emit("p2p:fatalError", { reason: "recreate-limit-exceeded", recreateCount: __privateGet(this, _recreateCount) }, __privateGet(this, _recreateCount));
331
+ return;
332
+ }
333
+ __privateSet(this, _recreateCount, __privateGet(this, _recreateCount) + 1);
334
+ const url = __privateGet(this, _currentUrl);
335
+ const video = __privateGet(this, _currentVideo);
336
+ if (url === void 0 || video === void 0) return;
337
+ __privateMethod(this, _P2PController_instances, stop_fn).call(this);
338
+ __privateGet(this, _stats).reset();
339
+ __privateMethod(this, _P2PController_instances, start_fn).call(this, url, video);
340
+ };
341
+
342
+ // src/ui/styles.ts
343
+ var STYLE_TAG = "data-artp2p";
344
+ var P2P_UI_CSS = `
345
+ .art-video-player .art-info.artp2p-info {
346
+ display: none !important;
347
+ }
348
+
349
+ .art-video-player.artp2p-stats-show .art-info.artp2p-info {
350
+ display: flex !important;
351
+ }
352
+ `;
353
+ function injectStyles() {
354
+ const doc = document;
355
+ if (doc.querySelector(`style[${STYLE_TAG}]`)) return;
356
+ const style = doc.createElement("style");
357
+ style.setAttribute(STYLE_TAG, "");
358
+ style.textContent = P2P_UI_CSS;
359
+ doc.head.appendChild(style);
360
+ }
361
+
362
+ // src/ui/format.ts
363
+ function formatBytes(bytes) {
364
+ if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
365
+ if (bytes >= 1024 ** 2) return `${(bytes / 1024 ** 2).toFixed(2)} MB`;
366
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
367
+ return `${Math.round(bytes)} B`;
368
+ }
369
+ function formatSpeed(bytesPerSecond) {
370
+ return `${formatBytes(bytesPerSecond)}/s`;
371
+ }
372
+ function formatPercent(ratio) {
373
+ return `${Math.round(ratio * 100)}%`;
374
+ }
375
+
376
+ // src/ui/stats-menu.ts
377
+ var CONTEXTMENU_NAME = "artp2pStats";
378
+ var SHOW_CLASS = "artp2p-stats-show";
379
+ var PANEL_HTML = `
380
+ <div class="art-info artp2p-info">
381
+ <div class="art-info-panel">
382
+ <div class="art-info-item">
383
+ <div class="art-info-title">P2P \u72B6\u6001:</div>
384
+ <div class="art-info-content" data-field="state"></div>
385
+ </div>
386
+ <div class="art-info-item">
387
+ <div class="art-info-title">\u4E0B\u884C\u901F\u7387:</div>
388
+ <div class="art-info-content" data-field="download"></div>
389
+ </div>
390
+ <div class="art-info-item">
391
+ <div class="art-info-title">P2P \u5360\u6BD4:</div>
392
+ <div class="art-info-content" data-field="ratio"></div>
393
+ </div>
394
+ <div class="art-info-item">
395
+ <div class="art-info-title">\u4E0A\u884C\u901F\u7387:</div>
396
+ <div class="art-info-content" data-field="upload"></div>
397
+ </div>
398
+ <div class="art-info-item">
399
+ <div class="art-info-title">Peers:</div>
400
+ <div class="art-info-content" data-field="peers"></div>
401
+ </div>
402
+ <div class="art-info-item">
403
+ <div class="art-info-title">\u7D2F\u8BA1\u6D41\u91CF:</div>
404
+ <div class="art-info-content" data-field="total"></div>
405
+ </div>
406
+ </div>
407
+ <div class="art-info-close">[x]</div>
408
+ </div>
409
+ `;
410
+ function resolveStateText(controller) {
411
+ if (!controller.p2pEnabled) return "\u5DF2\u5173\u95ED";
412
+ if (!controller.uploadEnabled) return "\u4EC5\u4E0A\u4F20";
413
+ return "\u8FD0\u884C\u4E2D";
414
+ }
415
+ function mountStatsMenu(art, controller, stats) {
416
+ const wrapper = document.createElement("div");
417
+ wrapper.innerHTML = PANEL_HTML;
418
+ const root = wrapper.firstElementChild;
419
+ const fields = {
420
+ state: root.querySelector('[data-field="state"]'),
421
+ download: root.querySelector('[data-field="download"]'),
422
+ ratio: root.querySelector('[data-field="ratio"]'),
423
+ upload: root.querySelector('[data-field="upload"]'),
424
+ peers: root.querySelector('[data-field="peers"]'),
425
+ total: root.querySelector('[data-field="total"]')
426
+ };
427
+ const $close = root.querySelector(".art-info-close");
428
+ art.template.$player.appendChild(root);
429
+ let timer = 0;
430
+ let opened = false;
431
+ const visibilityCallbacks = [];
432
+ function update() {
433
+ const snapshot = stats.snapshot();
434
+ const p2pOn = controller.p2pEnabled;
435
+ fields.state.textContent = resolveStateText(controller);
436
+ fields.download.textContent = p2pOn ? `${formatSpeed(snapshot.downloadSpeed)}\uFF08P2P ${formatSpeed(snapshot.p2pDownloadSpeed)}\uFF09` : formatSpeed(snapshot.downloadSpeed);
437
+ fields.ratio.textContent = p2pOn ? formatPercent(snapshot.p2pDownloadRatio) : "\u2014";
438
+ fields.upload.textContent = formatSpeed(snapshot.uploadSpeed);
439
+ fields.peers.textContent = `${snapshot.peers} / ${snapshot.peakPeers}`;
440
+ fields.total.textContent = `\u2193 ${formatBytes(snapshot.totalDownloadedBytes)} \xB7 \u2191 ${formatBytes(snapshot.uploadedBytes)}`;
441
+ }
442
+ function notifyVisibility() {
443
+ for (const callback of visibilityCallbacks) callback(opened);
444
+ }
445
+ function openPanel() {
446
+ if (opened) return;
447
+ opened = true;
448
+ art.template.$player.classList.add(SHOW_CLASS);
449
+ art.info.show = false;
450
+ update();
451
+ timer = window.setInterval(update, STATS_POLLING_MS);
452
+ notifyVisibility();
453
+ }
454
+ function closePanel() {
455
+ if (!opened) return;
456
+ opened = false;
457
+ art.template.$player.classList.remove(SHOW_CLASS);
458
+ window.clearInterval(timer);
459
+ notifyVisibility();
460
+ }
461
+ function onCloseClick(event) {
462
+ event.stopPropagation();
463
+ closePanel();
464
+ }
465
+ $close.addEventListener("click", onCloseClick);
466
+ const onNativeInfo = (open) => {
467
+ if (open) closePanel();
468
+ };
469
+ art.on("info", onNativeInfo);
470
+ art.contextmenu.add({
471
+ name: CONTEXTMENU_NAME,
472
+ index: 45,
473
+ html: "P2P \u7EDF\u8BA1",
474
+ click: (contextmenu) => {
475
+ contextmenu.show = false;
476
+ openPanel();
477
+ }
478
+ });
479
+ return {
480
+ open: openPanel,
481
+ close: closePanel,
482
+ isOpen: () => opened,
483
+ onVisibilityChange(callback) {
484
+ visibilityCallbacks.push(callback);
485
+ },
486
+ destroy() {
487
+ closePanel();
488
+ art.off("info", onNativeInfo);
489
+ $close.removeEventListener("click", onCloseClick);
490
+ }
491
+ };
492
+ }
493
+
494
+ // src/ui/setting.ts
495
+ var ICON_P2P_ENABLED = '<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M11 21h-1l1-7H7.5c-.58 0-.57-.32-.38-.66l.07-.12C8.48 10.94 10.42 7.54 13 3h1l-1 7h3.5c.49 0 .56.33.47.51l-.07.15C12.96 17.55 11 21 11 21z"/></svg>';
496
+ var ICON_UPLOAD_ONLY = '<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M9 16h6v-6h4l-7-7-7 7h4v6zm-4 2h14v2H5v-2z"/></svg>';
497
+ var ICON_STATS = '<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M5 9.2h3V19H5V9.2zM10.6 5h2.8v14h-2.8V5zm5.6 8H19v6h-2.8v-6z"/></svg>';
498
+ function mountP2PSettings(art, controller, panel, items) {
499
+ if (!art.option.setting) {
500
+ console.info("[artplayer-plugin-p2p] option.setting \u672A\u5F00\u542F\uFF0C\u8DF3\u8FC7 P2P \u8BBE\u7F6E\u5F00\u5173\u6302\u8F7D");
501
+ return false;
502
+ }
503
+ let mounted = 0;
504
+ if (items.p2pEnabled) {
505
+ art.setting.add({
506
+ name: "artp2pSetting",
507
+ html: "P2P \u52A0\u901F",
508
+ tooltip: "P2P \u52A0\u901F",
509
+ icon: ICON_P2P_ENABLED,
510
+ switch: controller.p2pEnabled,
511
+ onSwitch(item) {
512
+ const next = !item.switch;
513
+ controller.setP2PEnabled(next);
514
+ return next;
515
+ }
516
+ });
517
+ mounted += 1;
518
+ }
519
+ if (items.uploadOnly) {
520
+ art.setting.add({
521
+ name: "artp2pUploadSetting",
522
+ html: "\u4EC5\u4E0A\u4F20\u6A21\u5F0F",
523
+ tooltip: "\u4EC5\u4E0A\u4F20\u6A21\u5F0F",
524
+ icon: ICON_UPLOAD_ONLY,
525
+ switch: !controller.uploadEnabled,
526
+ onSwitch(item) {
527
+ const next = !item.switch;
528
+ controller.setUploadEnabled(!next);
529
+ return next;
530
+ }
531
+ });
532
+ mounted += 1;
533
+ }
534
+ if (panel && items.stats) {
535
+ const panelItem = {
536
+ name: "artp2pStatsSetting",
537
+ html: "P2P \u7EDF\u8BA1",
538
+ tooltip: "P2P \u7EDF\u8BA1",
539
+ icon: ICON_STATS,
540
+ switch: panel.isOpen(),
541
+ onSwitch(item) {
542
+ const next = !item.switch;
543
+ if (next) {
544
+ panel.open();
545
+ } else {
546
+ panel.close();
547
+ }
548
+ return next;
549
+ }
550
+ };
551
+ art.setting.add(panelItem);
552
+ panel.onVisibilityChange((open) => {
553
+ panelItem.switch = open;
554
+ });
555
+ mounted += 1;
556
+ }
557
+ return mounted > 0;
558
+ }
559
+
560
+ // src/ui/index.ts
561
+ function mountUI(art, controller, stats, options) {
562
+ if (!options.uiEnabled) return;
563
+ injectStyles();
564
+ let panel = null;
565
+ if (options.statsEnabled) {
566
+ panel = mountStatsMenu(art, controller, stats);
567
+ }
568
+ if (options.settingEnabled) {
569
+ mountP2PSettings(art, controller, panel, options.settingItems);
570
+ }
571
+ art.on("destroy", () => {
572
+ panel?.destroy();
573
+ });
574
+ }
575
+
576
+ // src/index.ts
577
+ function getUrlExtension(url) {
578
+ const withoutHash = url.split("#")[0];
579
+ const withoutQuery = withoutHash.split("?")[0];
580
+ const lastDotIndex = withoutQuery.lastIndexOf(".");
581
+ if (lastDotIndex === -1) return "";
582
+ return withoutQuery.slice(lastDotIndex + 1).toLowerCase();
583
+ }
584
+ function artplayerPluginP2P(options = {}) {
585
+ return (art) => {
586
+ const resolved = resolveOptions(options);
587
+ const stats = new P2PStatsEngine();
588
+ const controller = new P2PController(art, resolved, stats);
589
+ mountUI(art, controller, stats, resolved);
590
+ function typeCallback(video, url) {
591
+ if (!Hls2.isSupported()) {
592
+ if (video.canPlayType("application/vnd.apple.mpegurl")) {
593
+ video.src = url;
594
+ } else {
595
+ art.notice.show = `Unsupported playback format: ${resolved.typeName}`;
596
+ }
597
+ return;
598
+ }
599
+ controller.activate(url, video);
600
+ }
601
+ let registered = false;
602
+ const customTypeMap = art.option.customType ?? (art.option.customType = {});
603
+ if (customTypeMap[resolved.typeName]) {
604
+ console.warn(`[artplayer-plugin-p2p] customType "${resolved.typeName}" already exists, skip registering`);
605
+ } else {
606
+ customTypeMap[resolved.typeName] = typeCallback;
607
+ registered = true;
608
+ }
609
+ if (registered) {
610
+ const optionUrl = art.option.url;
611
+ if (optionUrl) {
612
+ const currentType = art.option.type || getUrlExtension(optionUrl);
613
+ if (currentType === resolved.typeName) {
614
+ const { $video } = art.template;
615
+ const rawSrc = $video.getAttribute("src");
616
+ if (rawSrc && !rawSrc.startsWith("blob:")) {
617
+ $video.removeAttribute("src");
618
+ $video.load();
619
+ }
620
+ art.url = optionUrl;
621
+ }
622
+ }
623
+ }
624
+ return {
625
+ name: "artplayerPluginP2P",
626
+ get engine() {
627
+ return controller.hls?.p2pEngine;
628
+ },
629
+ get hls() {
630
+ return controller.hls;
631
+ },
632
+ destroy() {
633
+ controller.destroy();
634
+ },
635
+ reload() {
636
+ controller.reload();
637
+ },
638
+ getStats() {
639
+ return stats.snapshot();
640
+ },
641
+ setP2PEnabled(enabled) {
642
+ controller.setP2PEnabled(enabled);
643
+ },
644
+ isP2PEnabled() {
645
+ return controller.p2pEnabled;
646
+ },
647
+ setUploadEnabled(enabled) {
648
+ controller.setUploadEnabled(enabled);
649
+ },
650
+ isUploadEnabled() {
651
+ return controller.uploadEnabled;
652
+ },
653
+ /**
654
+ * 运行时动态配置透传:转发到当前引擎的 Core.applyDynamicConfig;
655
+ * 无活跃实例时不生效(即时调参语义,不做延迟补发)
656
+ */
657
+ applyDynamicConfig(patch) {
658
+ controller.hls?.p2pEngine.applyDynamicConfig({ core: patch });
659
+ }
660
+ };
661
+ };
662
+ }
663
+ export {
664
+ BANDWIDTH_WINDOW_MS,
665
+ DEFAULT_FATAL_RETRY_MAX,
666
+ DEFAULT_TYPE,
667
+ FatalRecoveryPolicy,
668
+ P2PController,
669
+ P2PStatsEngine,
670
+ P2P_EVENT_BRIDGE_MAP,
671
+ STATS_POLLING_MS,
672
+ applyRuntimeToggle,
673
+ createHlsWithP2P,
674
+ artplayerPluginP2P as default,
675
+ mergeCoreConfig,
676
+ resolveOptions
677
+ };