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/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/artplayer-plugin-p2p.iife.js +80 -0
- package/dist/chunk-IX2RPNML.mjs +279 -0
- package/dist/index.d.mts +202 -0
- package/dist/index.d.ts +202 -0
- package/dist/index.js +969 -0
- package/dist/index.mjs +677 -0
- package/dist/pure.d.mts +48 -0
- package/dist/pure.d.ts +48 -0
- package/dist/pure.js +309 -0
- package/dist/pure.mjs +24 -0
- package/dist/stats-CFbDXEsi.d.mts +294 -0
- package/dist/stats-CFbDXEsi.d.ts +294 -0
- package/package.json +84 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __typeError = (msg) => {
|
|
9
|
+
throw TypeError(msg);
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
33
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
34
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
35
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
36
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
37
|
+
|
|
38
|
+
// src/index.ts
|
|
39
|
+
var index_exports = {};
|
|
40
|
+
__export(index_exports, {
|
|
41
|
+
BANDWIDTH_WINDOW_MS: () => BANDWIDTH_WINDOW_MS,
|
|
42
|
+
DEFAULT_FATAL_RETRY_MAX: () => DEFAULT_FATAL_RETRY_MAX,
|
|
43
|
+
DEFAULT_TYPE: () => DEFAULT_TYPE,
|
|
44
|
+
FatalRecoveryPolicy: () => FatalRecoveryPolicy,
|
|
45
|
+
P2PController: () => P2PController,
|
|
46
|
+
P2PStatsEngine: () => P2PStatsEngine,
|
|
47
|
+
P2P_EVENT_BRIDGE_MAP: () => P2P_EVENT_BRIDGE_MAP,
|
|
48
|
+
STATS_POLLING_MS: () => STATS_POLLING_MS,
|
|
49
|
+
applyRuntimeToggle: () => applyRuntimeToggle,
|
|
50
|
+
createHlsWithP2P: () => createHlsWithP2P,
|
|
51
|
+
default: () => artplayerPluginP2P,
|
|
52
|
+
mergeCoreConfig: () => mergeCoreConfig,
|
|
53
|
+
resolveOptions: () => resolveOptions
|
|
54
|
+
});
|
|
55
|
+
module.exports = __toCommonJS(index_exports);
|
|
56
|
+
var import_hls2 = __toESM(require("hls.js"));
|
|
57
|
+
|
|
58
|
+
// src/bandwidth.ts
|
|
59
|
+
var _windowMs, _samples, _BandwidthCalculator_instances, removeExpired_fn;
|
|
60
|
+
var BandwidthCalculator = class {
|
|
61
|
+
/**
|
|
62
|
+
* @param windowMs - 滑动时间窗长度(毫秒),窗口越长速率越平滑
|
|
63
|
+
*/
|
|
64
|
+
constructor(windowMs) {
|
|
65
|
+
__privateAdd(this, _BandwidthCalculator_instances);
|
|
66
|
+
__privateAdd(this, _windowMs);
|
|
67
|
+
__privateAdd(this, _samples, []);
|
|
68
|
+
__privateSet(this, _windowMs, windowMs);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 记录一笔字节
|
|
72
|
+
*
|
|
73
|
+
* @param bytes - 字节数(非正值直接忽略,防御脏数据)
|
|
74
|
+
* @param timestampMs - 样本时间戳;缺省取 performance.now(),测试可注入固定时间
|
|
75
|
+
*/
|
|
76
|
+
record(bytes, timestampMs = performance.now()) {
|
|
77
|
+
if (bytes <= 0) return;
|
|
78
|
+
__privateGet(this, _samples).push({ timestampMs, bytes });
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 计算窗口内平均速率
|
|
82
|
+
*
|
|
83
|
+
* 读取时惰性清理过期样本;速率按整个窗口长度折算而非
|
|
84
|
+
* 首尾样本间隔,保证窗口未填满时(刚启动)读数平滑趋近真实值
|
|
85
|
+
*
|
|
86
|
+
* @param nowMs - 当前时间戳;缺省取 performance.now(),测试可注入固定时间
|
|
87
|
+
* @returns 速率 B/s
|
|
88
|
+
*/
|
|
89
|
+
getSpeed(nowMs = performance.now()) {
|
|
90
|
+
__privateMethod(this, _BandwidthCalculator_instances, removeExpired_fn).call(this, nowMs);
|
|
91
|
+
let totalBytes = 0;
|
|
92
|
+
for (const sample of __privateGet(this, _samples)) {
|
|
93
|
+
totalBytes += sample.bytes;
|
|
94
|
+
}
|
|
95
|
+
return totalBytes / (__privateGet(this, _windowMs) / 1e3);
|
|
96
|
+
}
|
|
97
|
+
/** 清空全部样本 */
|
|
98
|
+
reset() {
|
|
99
|
+
__privateGet(this, _samples).length = 0;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
_windowMs = new WeakMap();
|
|
103
|
+
_samples = new WeakMap();
|
|
104
|
+
_BandwidthCalculator_instances = new WeakSet();
|
|
105
|
+
/**
|
|
106
|
+
* 移除窗口外的过期样本
|
|
107
|
+
*
|
|
108
|
+
* 样本时间戳单调递增(缺省时间源 performance.now 单调;
|
|
109
|
+
* 注入时间戳同样须单调),从头扫描至首个窗口内样本即可。
|
|
110
|
+
* 边界样本(age 恰等于窗口长)保留在窗口内,与 p2pml
|
|
111
|
+
* 官方带宽计算的过期判定语义保持一致
|
|
112
|
+
*/
|
|
113
|
+
removeExpired_fn = function(nowMs) {
|
|
114
|
+
const cutoff = nowMs - __privateGet(this, _windowMs);
|
|
115
|
+
let firstValid = __privateGet(this, _samples).length;
|
|
116
|
+
for (let i = 0; i < __privateGet(this, _samples).length; i++) {
|
|
117
|
+
if (__privateGet(this, _samples)[i].timestampMs >= cutoff) {
|
|
118
|
+
firstValid = i;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
__privateGet(this, _samples).splice(0, firstValid);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// src/constants.ts
|
|
126
|
+
var DEFAULT_TYPE = "m3u8";
|
|
127
|
+
var DEFAULT_FATAL_RETRY_MAX = 2;
|
|
128
|
+
var BANDWIDTH_WINDOW_MS = 1e4;
|
|
129
|
+
var STATS_POLLING_MS = 1e3;
|
|
130
|
+
var P2P_EVENT_BRIDGE_MAP = {
|
|
131
|
+
onStreamAdded: "p2p:streamAdded",
|
|
132
|
+
onStreamRegistrationError: "p2p:streamRegistrationError",
|
|
133
|
+
onSegmentLoaded: "p2p:segmentLoaded",
|
|
134
|
+
onSegmentError: "p2p:segmentError",
|
|
135
|
+
onSegmentAbort: "p2p:segmentAbort",
|
|
136
|
+
onSegmentStart: "p2p:segmentStart",
|
|
137
|
+
onPeerConnect: "p2p:peerConnect",
|
|
138
|
+
onPeerConnectError: "p2p:peerConnectError",
|
|
139
|
+
onPeerClose: "p2p:peerClose",
|
|
140
|
+
onPeerError: "p2p:peerError",
|
|
141
|
+
onPeerWarning: "p2p:peerWarning",
|
|
142
|
+
onChunkDownloaded: "p2p:chunkDownloaded",
|
|
143
|
+
onChunkUploaded: "p2p:chunkUploaded",
|
|
144
|
+
onTrackerError: "p2p:trackerError",
|
|
145
|
+
onTrackerWarning: "p2p:trackerWarning"
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// src/stats.ts
|
|
149
|
+
var _peers, _peakPeers, _p2pDownloadedBytes, _httpDownloadedBytes, _uploadedBytes, _downloadBandwidths, _uploadBandwidth;
|
|
150
|
+
var P2PStatsEngine = class {
|
|
151
|
+
constructor() {
|
|
152
|
+
__privateAdd(this, _peers, 0);
|
|
153
|
+
__privateAdd(this, _peakPeers, 0);
|
|
154
|
+
__privateAdd(this, _p2pDownloadedBytes, 0);
|
|
155
|
+
__privateAdd(this, _httpDownloadedBytes, 0);
|
|
156
|
+
__privateAdd(this, _uploadedBytes, 0);
|
|
157
|
+
__privateAdd(this, _downloadBandwidths, {
|
|
158
|
+
p2p: new BandwidthCalculator(BANDWIDTH_WINDOW_MS),
|
|
159
|
+
http: new BandwidthCalculator(BANDWIDTH_WINDOW_MS)
|
|
160
|
+
});
|
|
161
|
+
__privateAdd(this, _uploadBandwidth, new BandwidthCalculator(BANDWIDTH_WINDOW_MS));
|
|
162
|
+
}
|
|
163
|
+
/** 记录一个 peer 连接建立(同时维护峰值) */
|
|
164
|
+
recordPeerConnect() {
|
|
165
|
+
__privateSet(this, _peers, __privateGet(this, _peers) + 1);
|
|
166
|
+
if (__privateGet(this, _peers) > __privateGet(this, _peakPeers)) {
|
|
167
|
+
__privateSet(this, _peakPeers, __privateGet(this, _peers));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** 记录一个 peer 连接断开(计数不小于零) */
|
|
171
|
+
recordPeerClose() {
|
|
172
|
+
if (__privateGet(this, _peers) > 0) {
|
|
173
|
+
__privateSet(this, _peers, __privateGet(this, _peers) - 1);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* 记录一笔下行字节
|
|
178
|
+
*
|
|
179
|
+
* @param bytes - 字节数
|
|
180
|
+
* @param channel - 下载通道(http / p2p)
|
|
181
|
+
* @param timestampMs - 样本时间戳;缺省取 performance.now(),测试可注入固定时间
|
|
182
|
+
*/
|
|
183
|
+
recordDownload(bytes, channel, timestampMs) {
|
|
184
|
+
if (bytes <= 0) return;
|
|
185
|
+
if (channel === "p2p") {
|
|
186
|
+
__privateSet(this, _p2pDownloadedBytes, __privateGet(this, _p2pDownloadedBytes) + bytes);
|
|
187
|
+
} else {
|
|
188
|
+
__privateSet(this, _httpDownloadedBytes, __privateGet(this, _httpDownloadedBytes) + bytes);
|
|
189
|
+
}
|
|
190
|
+
__privateGet(this, _downloadBandwidths)[channel].record(bytes, timestampMs);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* 记录一笔 P2P 上行字节
|
|
194
|
+
*
|
|
195
|
+
* @param bytes - 字节数
|
|
196
|
+
* @param timestampMs - 样本时间戳;缺省取 performance.now(),测试可注入固定时间
|
|
197
|
+
*/
|
|
198
|
+
recordUpload(bytes, timestampMs) {
|
|
199
|
+
if (bytes <= 0) return;
|
|
200
|
+
__privateSet(this, _uploadedBytes, __privateGet(this, _uploadedBytes) + bytes);
|
|
201
|
+
__privateGet(this, _uploadBandwidth).record(bytes, timestampMs);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 输出当前统计快照(累计量 + 派生指标一次算清)
|
|
205
|
+
*
|
|
206
|
+
* 带宽基准时间只取一次、每通道只计算一次,
|
|
207
|
+
* 保证快照内部指标自洽(downloadSpeed 恒等于两通道之和)
|
|
208
|
+
*
|
|
209
|
+
* @param nowMs - 带宽计算基准时间戳;缺省取 performance.now(),测试可注入固定时间
|
|
210
|
+
*/
|
|
211
|
+
snapshot(nowMs) {
|
|
212
|
+
const totalDownloadedBytes = __privateGet(this, _p2pDownloadedBytes) + __privateGet(this, _httpDownloadedBytes);
|
|
213
|
+
const now = nowMs ?? performance.now();
|
|
214
|
+
const p2pDownloadSpeed = __privateGet(this, _downloadBandwidths).p2p.getSpeed(now);
|
|
215
|
+
const httpDownloadSpeed = __privateGet(this, _downloadBandwidths).http.getSpeed(now);
|
|
216
|
+
return {
|
|
217
|
+
peers: __privateGet(this, _peers),
|
|
218
|
+
peakPeers: __privateGet(this, _peakPeers),
|
|
219
|
+
p2pDownloadedBytes: __privateGet(this, _p2pDownloadedBytes),
|
|
220
|
+
httpDownloadedBytes: __privateGet(this, _httpDownloadedBytes),
|
|
221
|
+
uploadedBytes: __privateGet(this, _uploadedBytes),
|
|
222
|
+
totalDownloadedBytes,
|
|
223
|
+
p2pDownloadRatio: totalDownloadedBytes > 0 ? __privateGet(this, _p2pDownloadedBytes) / totalDownloadedBytes : 0,
|
|
224
|
+
downloadSpeed: p2pDownloadSpeed + httpDownloadSpeed,
|
|
225
|
+
p2pDownloadSpeed,
|
|
226
|
+
uploadSpeed: __privateGet(this, _uploadBandwidth).getSpeed(now)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* 清零当前 peer 计数
|
|
231
|
+
*
|
|
232
|
+
* 动态关闭 P2P 或销毁播放实例后连接已全部断开(引擎销毁不保证
|
|
233
|
+
* 逐个发出 onPeerClose),主动清零防止残留计数污染下一次快照;
|
|
234
|
+
* 峰值与字节累计保留
|
|
235
|
+
*/
|
|
236
|
+
resetPeers() {
|
|
237
|
+
__privateSet(this, _peers, 0);
|
|
238
|
+
}
|
|
239
|
+
/** 重置全部计数与样本(换源时调用) */
|
|
240
|
+
reset() {
|
|
241
|
+
__privateSet(this, _peers, 0);
|
|
242
|
+
__privateSet(this, _peakPeers, 0);
|
|
243
|
+
__privateSet(this, _p2pDownloadedBytes, 0);
|
|
244
|
+
__privateSet(this, _httpDownloadedBytes, 0);
|
|
245
|
+
__privateSet(this, _uploadedBytes, 0);
|
|
246
|
+
__privateGet(this, _downloadBandwidths).p2p.reset();
|
|
247
|
+
__privateGet(this, _downloadBandwidths).http.reset();
|
|
248
|
+
__privateGet(this, _uploadBandwidth).reset();
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
_peers = new WeakMap();
|
|
252
|
+
_peakPeers = new WeakMap();
|
|
253
|
+
_p2pDownloadedBytes = new WeakMap();
|
|
254
|
+
_httpDownloadedBytes = new WeakMap();
|
|
255
|
+
_uploadedBytes = new WeakMap();
|
|
256
|
+
_downloadBandwidths = new WeakMap();
|
|
257
|
+
_uploadBandwidth = new WeakMap();
|
|
258
|
+
|
|
259
|
+
// src/controller.ts
|
|
260
|
+
var import_hls = __toESM(require("hls.js"));
|
|
261
|
+
|
|
262
|
+
// src/bridge.ts
|
|
263
|
+
function attachEventBridge(art, engine, stats) {
|
|
264
|
+
const subscribe = engine.addEventListener.bind(engine);
|
|
265
|
+
for (const [eventName, artEventName] of Object.entries(P2P_EVENT_BRIDGE_MAP)) {
|
|
266
|
+
subscribe(eventName, (...args) => {
|
|
267
|
+
art.emit(artEventName, ...args);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
engine.addEventListener("onChunkDownloaded", (bytesLength, downloadSource) => {
|
|
271
|
+
stats.recordDownload(bytesLength, downloadSource);
|
|
272
|
+
});
|
|
273
|
+
engine.addEventListener("onChunkUploaded", (bytesLength) => {
|
|
274
|
+
stats.recordUpload(bytesLength);
|
|
275
|
+
});
|
|
276
|
+
engine.addEventListener("onPeerConnect", () => {
|
|
277
|
+
stats.recordPeerConnect();
|
|
278
|
+
});
|
|
279
|
+
engine.addEventListener("onPeerClose", () => {
|
|
280
|
+
stats.recordPeerClose();
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/engine.ts
|
|
285
|
+
var import_p2p_media_loader_hlsjs = require("p2p-media-loader-hlsjs");
|
|
286
|
+
|
|
287
|
+
// src/config.ts
|
|
288
|
+
function resolveUIOptions(ui) {
|
|
289
|
+
return ui.setting !== false;
|
|
290
|
+
}
|
|
291
|
+
function resolveSettingItems(setting) {
|
|
292
|
+
if (setting === void 0 || setting === true) {
|
|
293
|
+
return { p2pEnabled: true, uploadOnly: true, stats: true };
|
|
294
|
+
}
|
|
295
|
+
if (setting === false) {
|
|
296
|
+
return { p2pEnabled: false, uploadOnly: false, stats: false };
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
p2pEnabled: setting.p2pEnabled ?? true,
|
|
300
|
+
uploadOnly: setting.uploadOnly ?? true,
|
|
301
|
+
stats: setting.stats ?? true
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function resolveOptions(options) {
|
|
305
|
+
const uiEnabled = options.ui !== false;
|
|
306
|
+
let settingEnabled;
|
|
307
|
+
let settingItems;
|
|
308
|
+
if (typeof options.ui === "object" && options.ui !== null) {
|
|
309
|
+
settingEnabled = resolveUIOptions(options.ui);
|
|
310
|
+
settingItems = resolveSettingItems(options.ui.setting);
|
|
311
|
+
} else {
|
|
312
|
+
settingEnabled = uiEnabled;
|
|
313
|
+
settingItems = resolveSettingItems(uiEnabled ? void 0 : false);
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
typeName: options.type ?? DEFAULT_TYPE,
|
|
317
|
+
fatalRetryMax: options.fatalRetryMax ?? DEFAULT_FATAL_RETRY_MAX,
|
|
318
|
+
p2pEnabled: options.enabled ?? true,
|
|
319
|
+
uploadEnabled: options.uploadEnabled ?? true,
|
|
320
|
+
uiEnabled,
|
|
321
|
+
statsEnabled: uiEnabled && options.stats !== false,
|
|
322
|
+
settingEnabled,
|
|
323
|
+
settingItems,
|
|
324
|
+
core: options.core,
|
|
325
|
+
tracker: options.tracker,
|
|
326
|
+
hls: options.hls
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function mergeCoreConfig(options) {
|
|
330
|
+
return { ...options.core, ...options.tracker };
|
|
331
|
+
}
|
|
332
|
+
function applyRuntimeToggle(config, p2pEnabled, uploadEnabled) {
|
|
333
|
+
return {
|
|
334
|
+
...config,
|
|
335
|
+
isP2PDisabled: !p2pEnabled,
|
|
336
|
+
isP2PUploadDisabled: !uploadEnabled
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/recovery.ts
|
|
341
|
+
var _retryCount, _isMediaRecoveryAttempted, _options, _FatalRecoveryPolicy_instances, handleFatal_fn;
|
|
342
|
+
var FatalRecoveryPolicy = class {
|
|
343
|
+
/**
|
|
344
|
+
* @param hls - 目标 hls.js 实例
|
|
345
|
+
* @param hlsConstructor - hls.js 构造器(Events/ErrorTypes 枚举来源,
|
|
346
|
+
* 避免本模块引入 hls.js 运行时值以保持可移植性)
|
|
347
|
+
* @param options - 回调集合
|
|
348
|
+
*/
|
|
349
|
+
constructor(hls, hlsConstructor, options) {
|
|
350
|
+
__privateAdd(this, _FatalRecoveryPolicy_instances);
|
|
351
|
+
__privateAdd(this, _retryCount, 0);
|
|
352
|
+
__privateAdd(this, _isMediaRecoveryAttempted, false);
|
|
353
|
+
__privateAdd(this, _options);
|
|
354
|
+
__privateSet(this, _options, options);
|
|
355
|
+
hls.on(hlsConstructor.Events.ERROR, (_event, data) => {
|
|
356
|
+
if (!data.fatal) return;
|
|
357
|
+
__privateMethod(this, _FatalRecoveryPolicy_instances, handleFatal_fn).call(this, hls, hlsConstructor, data);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
_retryCount = new WeakMap();
|
|
362
|
+
_isMediaRecoveryAttempted = new WeakMap();
|
|
363
|
+
_options = new WeakMap();
|
|
364
|
+
_FatalRecoveryPolicy_instances = new WeakSet();
|
|
365
|
+
/**
|
|
366
|
+
* fatal 分级处理
|
|
367
|
+
*
|
|
368
|
+
* 网络级:重新拉起加载;媒体级:软恢复(连续失败时切换音频编解码);
|
|
369
|
+
* 其他:软恢复无效,交由上层重建。超限后停止一切动作,
|
|
370
|
+
* 仅保留通知,由宿主决策后续(如提示用户或换源)
|
|
371
|
+
*/
|
|
372
|
+
handleFatal_fn = function(hls, hlsConstructor, data) {
|
|
373
|
+
const { retryMax, onFatalError, onUnrecoverable } = __privateGet(this, _options);
|
|
374
|
+
if (__privateGet(this, _retryCount) >= retryMax) {
|
|
375
|
+
onFatalError(data, __privateGet(this, _retryCount));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
__privateSet(this, _retryCount, __privateGet(this, _retryCount) + 1);
|
|
379
|
+
onFatalError(data, __privateGet(this, _retryCount));
|
|
380
|
+
const { ErrorTypes } = hlsConstructor;
|
|
381
|
+
if (data.type === ErrorTypes.NETWORK_ERROR) {
|
|
382
|
+
hls.startLoad();
|
|
383
|
+
} else if (data.type === ErrorTypes.MEDIA_ERROR) {
|
|
384
|
+
if (__privateGet(this, _isMediaRecoveryAttempted)) {
|
|
385
|
+
hls.swapAudioCodec();
|
|
386
|
+
}
|
|
387
|
+
hls.recoverMediaError();
|
|
388
|
+
__privateSet(this, _isMediaRecoveryAttempted, true);
|
|
389
|
+
} else {
|
|
390
|
+
onUnrecoverable(data, __privateGet(this, _retryCount));
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// src/engine.ts
|
|
395
|
+
var hlsWithP2PCache = /* @__PURE__ */ new WeakMap();
|
|
396
|
+
function getHlsWithP2PClass(hlsConstructor) {
|
|
397
|
+
let cached = hlsWithP2PCache.get(hlsConstructor);
|
|
398
|
+
if (!cached) {
|
|
399
|
+
cached = import_p2p_media_loader_hlsjs.HlsJsP2PEngine.injectMixin(hlsConstructor);
|
|
400
|
+
hlsWithP2PCache.set(hlsConstructor, cached);
|
|
401
|
+
}
|
|
402
|
+
return cached;
|
|
403
|
+
}
|
|
404
|
+
function createHlsWithP2P(options, hlsConstructor, hooks) {
|
|
405
|
+
const HlsWithP2PClass = getHlsWithP2PClass(hlsConstructor);
|
|
406
|
+
const hls = new HlsWithP2PClass({
|
|
407
|
+
...options.hls,
|
|
408
|
+
p2p: {
|
|
409
|
+
core: applyRuntimeToggle(mergeCoreConfig(options), options.p2pEnabled, options.uploadEnabled),
|
|
410
|
+
onHlsJsCreated(instance) {
|
|
411
|
+
hooks.onEngineCreated(instance.p2pEngine);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
new FatalRecoveryPolicy(hls, hlsConstructor, {
|
|
416
|
+
retryMax: options.fatalRetryMax ?? DEFAULT_FATAL_RETRY_MAX,
|
|
417
|
+
onFatalError: hooks.onFatalError,
|
|
418
|
+
onUnrecoverable: hooks.onUnrecoverable
|
|
419
|
+
});
|
|
420
|
+
return hls;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/controller.ts
|
|
424
|
+
var _art, _options2, _stats, _state, _p2pEnabled, _uploadEnabled, _recreateCount, _currentUrl, _currentVideo, _instance, _destroyHandler, _P2PController_instances, start_fn, stop_fn, emitStateChange_fn, handleUnrecoverable_fn;
|
|
425
|
+
var P2PController = class {
|
|
426
|
+
/**
|
|
427
|
+
* @param art - ArtPlayer 实例
|
|
428
|
+
* @param options - 解析后的插件选项(p2pEnabled / uploadEnabled 为初始开关状态)
|
|
429
|
+
* @param stats - 统计引擎实例(由入口层创建并共享给句柄)
|
|
430
|
+
*/
|
|
431
|
+
constructor(art, options, stats) {
|
|
432
|
+
__privateAdd(this, _P2PController_instances);
|
|
433
|
+
__privateAdd(this, _art);
|
|
434
|
+
__privateAdd(this, _options2);
|
|
435
|
+
__privateAdd(this, _stats);
|
|
436
|
+
__privateAdd(this, _state, "idle");
|
|
437
|
+
__privateAdd(this, _p2pEnabled);
|
|
438
|
+
__privateAdd(this, _uploadEnabled);
|
|
439
|
+
/** 跨实例的 fatal 重建计数(外部显式激活时归零,防止无限重建循环) */
|
|
440
|
+
__privateAdd(this, _recreateCount, 0);
|
|
441
|
+
__privateAdd(this, _currentUrl);
|
|
442
|
+
__privateAdd(this, _currentVideo);
|
|
443
|
+
__privateAdd(this, _instance);
|
|
444
|
+
/** 当前实例在 art 上的 destroy 监听(实例切换时注销,避免累积监听) */
|
|
445
|
+
__privateAdd(this, _destroyHandler);
|
|
446
|
+
__privateSet(this, _art, art);
|
|
447
|
+
__privateSet(this, _options2, options);
|
|
448
|
+
__privateSet(this, _stats, stats);
|
|
449
|
+
__privateSet(this, _p2pEnabled, options.p2pEnabled);
|
|
450
|
+
__privateSet(this, _uploadEnabled, options.uploadEnabled);
|
|
451
|
+
}
|
|
452
|
+
/** 当前控制器状态 */
|
|
453
|
+
get state() {
|
|
454
|
+
return __privateGet(this, _state);
|
|
455
|
+
}
|
|
456
|
+
/** 当前 P2P 开关状态 */
|
|
457
|
+
get p2pEnabled() {
|
|
458
|
+
return __privateGet(this, _p2pEnabled);
|
|
459
|
+
}
|
|
460
|
+
/** 当前上传开关状态 */
|
|
461
|
+
get uploadEnabled() {
|
|
462
|
+
return __privateGet(this, _uploadEnabled);
|
|
463
|
+
}
|
|
464
|
+
/** 当前播放实例(与 art.hls 槽位同步) */
|
|
465
|
+
get hls() {
|
|
466
|
+
return __privateGet(this, _instance);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* 外部显式激活(customType 回调:首次加载 / 换源 / 重连共用)
|
|
470
|
+
*
|
|
471
|
+
* 视为新的播放意图:统计与重建计数全部归零,
|
|
472
|
+
* 以控制器当前开关状态创建实例(不接受模式参数)
|
|
473
|
+
*
|
|
474
|
+
* @param url - 播放地址
|
|
475
|
+
* @param video - 视频元素
|
|
476
|
+
*/
|
|
477
|
+
activate(url, video) {
|
|
478
|
+
if (__privateGet(this, _state) === "destroyed") return;
|
|
479
|
+
__privateMethod(this, _P2PController_instances, stop_fn).call(this);
|
|
480
|
+
__privateGet(this, _stats).reset();
|
|
481
|
+
__privateSet(this, _recreateCount, 0);
|
|
482
|
+
__privateMethod(this, _P2PController_instances, start_fn).call(this, url, video);
|
|
483
|
+
}
|
|
484
|
+
/** 销毁当前播放实例(注销监听、清空 art.hls 槽位) */
|
|
485
|
+
deactivate() {
|
|
486
|
+
if (__privateGet(this, _state) === "destroyed") return;
|
|
487
|
+
__privateMethod(this, _P2PController_instances, stop_fn).call(this);
|
|
488
|
+
}
|
|
489
|
+
/** 销毁现有实例后按最近地址与当前开关模式重建 */
|
|
490
|
+
reload() {
|
|
491
|
+
if (__privateGet(this, _state) === "destroyed" || __privateGet(this, _currentUrl) === void 0) return;
|
|
492
|
+
this.activate(__privateGet(this, _currentUrl), __privateGet(this, _currentVideo));
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* P2P 运行时开关:经 applyDynamicConfig 无损切换
|
|
496
|
+
*
|
|
497
|
+
* 不销毁实例、不中断播放;关闭时 HybridLoader 被引擎销毁、
|
|
498
|
+
* peer 连接全断,主动清零 peer 计数防止残留(迟到关闭事件
|
|
499
|
+
* 由统计引擎的非负保护兜底);字节统计冻结保留供模式对照
|
|
500
|
+
*
|
|
501
|
+
* @param enabled - 目标 P2P 状态
|
|
502
|
+
*/
|
|
503
|
+
setP2PEnabled(enabled) {
|
|
504
|
+
if (__privateGet(this, _state) === "destroyed" || enabled === __privateGet(this, _p2pEnabled)) return;
|
|
505
|
+
__privateSet(this, _p2pEnabled, enabled);
|
|
506
|
+
const engine = __privateGet(this, _instance)?.p2pEngine;
|
|
507
|
+
if (engine) {
|
|
508
|
+
engine.applyDynamicConfig({ core: { isP2PDisabled: !enabled } });
|
|
509
|
+
if (!enabled) {
|
|
510
|
+
__privateGet(this, _stats).resetPeers();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
__privateMethod(this, _P2PController_instances, emitStateChange_fn).call(this);
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* 上行运行时开关:仅信令广播(引擎内部处理),完全无缝
|
|
517
|
+
*
|
|
518
|
+
* @param enabled - 目标上传状态
|
|
519
|
+
*/
|
|
520
|
+
setUploadEnabled(enabled) {
|
|
521
|
+
if (__privateGet(this, _state) === "destroyed" || enabled === __privateGet(this, _uploadEnabled)) return;
|
|
522
|
+
__privateSet(this, _uploadEnabled, enabled);
|
|
523
|
+
__privateGet(this, _instance)?.p2pEngine.applyDynamicConfig({ core: { isP2PUploadDisabled: !enabled } });
|
|
524
|
+
__privateMethod(this, _P2PController_instances, emitStateChange_fn).call(this);
|
|
525
|
+
}
|
|
526
|
+
/** 播放器销毁:终止一切并进入 destroyed(幂等) */
|
|
527
|
+
destroy() {
|
|
528
|
+
if (__privateGet(this, _state) === "destroyed") return;
|
|
529
|
+
__privateMethod(this, _P2PController_instances, stop_fn).call(this);
|
|
530
|
+
__privateSet(this, _state, "destroyed");
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
_art = new WeakMap();
|
|
534
|
+
_options2 = new WeakMap();
|
|
535
|
+
_stats = new WeakMap();
|
|
536
|
+
_state = new WeakMap();
|
|
537
|
+
_p2pEnabled = new WeakMap();
|
|
538
|
+
_uploadEnabled = new WeakMap();
|
|
539
|
+
_recreateCount = new WeakMap();
|
|
540
|
+
_currentUrl = new WeakMap();
|
|
541
|
+
_currentVideo = new WeakMap();
|
|
542
|
+
_instance = new WeakMap();
|
|
543
|
+
_destroyHandler = new WeakMap();
|
|
544
|
+
_P2PController_instances = new WeakSet();
|
|
545
|
+
/**
|
|
546
|
+
* 创建并绑定播放实例(内部路径,不重置任何状态)
|
|
547
|
+
*
|
|
548
|
+
* 构造 EngineOptions 时以控制器当前开关状态覆盖选项初始值:
|
|
549
|
+
* fatal 重建 / 换源 / 重连的实例重建均经此路径,
|
|
550
|
+
* 保证重建后 P2P 与上传模式与用户当前选择一致
|
|
551
|
+
*
|
|
552
|
+
* @param url - 播放地址
|
|
553
|
+
* @param video - 视频元素
|
|
554
|
+
*/
|
|
555
|
+
start_fn = function(url, video) {
|
|
556
|
+
const engineOptions = {
|
|
557
|
+
core: __privateGet(this, _options2).core,
|
|
558
|
+
tracker: __privateGet(this, _options2).tracker,
|
|
559
|
+
hls: __privateGet(this, _options2).hls,
|
|
560
|
+
fatalRetryMax: __privateGet(this, _options2).fatalRetryMax,
|
|
561
|
+
p2pEnabled: __privateGet(this, _p2pEnabled),
|
|
562
|
+
uploadEnabled: __privateGet(this, _uploadEnabled)
|
|
563
|
+
};
|
|
564
|
+
const hooks = {
|
|
565
|
+
onEngineCreated: (engine) => {
|
|
566
|
+
attachEventBridge(__privateGet(this, _art), engine, __privateGet(this, _stats));
|
|
567
|
+
},
|
|
568
|
+
onFatalError: (data, retryCount) => {
|
|
569
|
+
__privateGet(this, _art).emit("p2p:fatalError", data, retryCount);
|
|
570
|
+
},
|
|
571
|
+
onUnrecoverable: (data, retryCount) => {
|
|
572
|
+
__privateMethod(this, _P2PController_instances, handleUnrecoverable_fn).call(this, data, retryCount);
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
const instance = createHlsWithP2P(engineOptions, import_hls.default, hooks);
|
|
576
|
+
instance.loadSource(url);
|
|
577
|
+
instance.attachMedia(video);
|
|
578
|
+
__privateGet(this, _art).hls = instance;
|
|
579
|
+
__privateSet(this, _instance, instance);
|
|
580
|
+
__privateSet(this, _currentUrl, url);
|
|
581
|
+
__privateSet(this, _currentVideo, video);
|
|
582
|
+
__privateSet(this, _state, "active");
|
|
583
|
+
__privateSet(this, _destroyHandler, () => {
|
|
584
|
+
__privateSet(this, _instance, void 0);
|
|
585
|
+
__privateSet(this, _currentVideo, void 0);
|
|
586
|
+
__privateSet(this, _state, "destroyed");
|
|
587
|
+
instance.destroy();
|
|
588
|
+
});
|
|
589
|
+
__privateGet(this, _art).on("destroy", __privateGet(this, _destroyHandler));
|
|
590
|
+
};
|
|
591
|
+
/** 销毁当前实例并回到 idle(不改变开关模式与重建计数) */
|
|
592
|
+
stop_fn = function() {
|
|
593
|
+
if (__privateGet(this, _destroyHandler)) {
|
|
594
|
+
__privateGet(this, _art).off("destroy", __privateGet(this, _destroyHandler));
|
|
595
|
+
__privateSet(this, _destroyHandler, void 0);
|
|
596
|
+
}
|
|
597
|
+
const instance = __privateGet(this, _instance);
|
|
598
|
+
__privateSet(this, _instance, void 0);
|
|
599
|
+
__privateGet(this, _art).hls = void 0;
|
|
600
|
+
__privateSet(this, _state, "idle");
|
|
601
|
+
__privateGet(this, _stats).resetPeers();
|
|
602
|
+
if (instance) {
|
|
603
|
+
instance.destroy();
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
/** 开关状态变化后派发 p2p:stateChange,供宿主同步自定义 UI */
|
|
607
|
+
emitStateChange_fn = function() {
|
|
608
|
+
const details = {
|
|
609
|
+
p2pEnabled: __privateGet(this, _p2pEnabled),
|
|
610
|
+
uploadEnabled: __privateGet(this, _uploadEnabled)
|
|
611
|
+
};
|
|
612
|
+
__privateGet(this, _art).emit("p2p:stateChange", details);
|
|
613
|
+
};
|
|
614
|
+
/**
|
|
615
|
+
* 不可恢复 fatal 的有界重建
|
|
616
|
+
*
|
|
617
|
+
* 重建计数超限后停止动作,发出带原因的 fatalError
|
|
618
|
+
* 事件交由宿主决策(提示用户 / 换源)
|
|
619
|
+
*/
|
|
620
|
+
handleUnrecoverable_fn = function(_data, _retryCount2) {
|
|
621
|
+
if (__privateGet(this, _recreateCount) >= __privateGet(this, _options2).fatalRetryMax) {
|
|
622
|
+
__privateGet(this, _art).emit("p2p:fatalError", { reason: "recreate-limit-exceeded", recreateCount: __privateGet(this, _recreateCount) }, __privateGet(this, _recreateCount));
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
__privateSet(this, _recreateCount, __privateGet(this, _recreateCount) + 1);
|
|
626
|
+
const url = __privateGet(this, _currentUrl);
|
|
627
|
+
const video = __privateGet(this, _currentVideo);
|
|
628
|
+
if (url === void 0 || video === void 0) return;
|
|
629
|
+
__privateMethod(this, _P2PController_instances, stop_fn).call(this);
|
|
630
|
+
__privateGet(this, _stats).reset();
|
|
631
|
+
__privateMethod(this, _P2PController_instances, start_fn).call(this, url, video);
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
// src/ui/styles.ts
|
|
635
|
+
var STYLE_TAG = "data-artp2p";
|
|
636
|
+
var P2P_UI_CSS = `
|
|
637
|
+
.art-video-player .art-info.artp2p-info {
|
|
638
|
+
display: none !important;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.art-video-player.artp2p-stats-show .art-info.artp2p-info {
|
|
642
|
+
display: flex !important;
|
|
643
|
+
}
|
|
644
|
+
`;
|
|
645
|
+
function injectStyles() {
|
|
646
|
+
const doc = document;
|
|
647
|
+
if (doc.querySelector(`style[${STYLE_TAG}]`)) return;
|
|
648
|
+
const style = doc.createElement("style");
|
|
649
|
+
style.setAttribute(STYLE_TAG, "");
|
|
650
|
+
style.textContent = P2P_UI_CSS;
|
|
651
|
+
doc.head.appendChild(style);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/ui/format.ts
|
|
655
|
+
function formatBytes(bytes) {
|
|
656
|
+
if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
|
|
657
|
+
if (bytes >= 1024 ** 2) return `${(bytes / 1024 ** 2).toFixed(2)} MB`;
|
|
658
|
+
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
659
|
+
return `${Math.round(bytes)} B`;
|
|
660
|
+
}
|
|
661
|
+
function formatSpeed(bytesPerSecond) {
|
|
662
|
+
return `${formatBytes(bytesPerSecond)}/s`;
|
|
663
|
+
}
|
|
664
|
+
function formatPercent(ratio) {
|
|
665
|
+
return `${Math.round(ratio * 100)}%`;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/ui/stats-menu.ts
|
|
669
|
+
var CONTEXTMENU_NAME = "artp2pStats";
|
|
670
|
+
var SHOW_CLASS = "artp2p-stats-show";
|
|
671
|
+
var PANEL_HTML = `
|
|
672
|
+
<div class="art-info artp2p-info">
|
|
673
|
+
<div class="art-info-panel">
|
|
674
|
+
<div class="art-info-item">
|
|
675
|
+
<div class="art-info-title">P2P \u72B6\u6001:</div>
|
|
676
|
+
<div class="art-info-content" data-field="state"></div>
|
|
677
|
+
</div>
|
|
678
|
+
<div class="art-info-item">
|
|
679
|
+
<div class="art-info-title">\u4E0B\u884C\u901F\u7387:</div>
|
|
680
|
+
<div class="art-info-content" data-field="download"></div>
|
|
681
|
+
</div>
|
|
682
|
+
<div class="art-info-item">
|
|
683
|
+
<div class="art-info-title">P2P \u5360\u6BD4:</div>
|
|
684
|
+
<div class="art-info-content" data-field="ratio"></div>
|
|
685
|
+
</div>
|
|
686
|
+
<div class="art-info-item">
|
|
687
|
+
<div class="art-info-title">\u4E0A\u884C\u901F\u7387:</div>
|
|
688
|
+
<div class="art-info-content" data-field="upload"></div>
|
|
689
|
+
</div>
|
|
690
|
+
<div class="art-info-item">
|
|
691
|
+
<div class="art-info-title">Peers:</div>
|
|
692
|
+
<div class="art-info-content" data-field="peers"></div>
|
|
693
|
+
</div>
|
|
694
|
+
<div class="art-info-item">
|
|
695
|
+
<div class="art-info-title">\u7D2F\u8BA1\u6D41\u91CF:</div>
|
|
696
|
+
<div class="art-info-content" data-field="total"></div>
|
|
697
|
+
</div>
|
|
698
|
+
</div>
|
|
699
|
+
<div class="art-info-close">[x]</div>
|
|
700
|
+
</div>
|
|
701
|
+
`;
|
|
702
|
+
function resolveStateText(controller) {
|
|
703
|
+
if (!controller.p2pEnabled) return "\u5DF2\u5173\u95ED";
|
|
704
|
+
if (!controller.uploadEnabled) return "\u4EC5\u4E0A\u4F20";
|
|
705
|
+
return "\u8FD0\u884C\u4E2D";
|
|
706
|
+
}
|
|
707
|
+
function mountStatsMenu(art, controller, stats) {
|
|
708
|
+
const wrapper = document.createElement("div");
|
|
709
|
+
wrapper.innerHTML = PANEL_HTML;
|
|
710
|
+
const root = wrapper.firstElementChild;
|
|
711
|
+
const fields = {
|
|
712
|
+
state: root.querySelector('[data-field="state"]'),
|
|
713
|
+
download: root.querySelector('[data-field="download"]'),
|
|
714
|
+
ratio: root.querySelector('[data-field="ratio"]'),
|
|
715
|
+
upload: root.querySelector('[data-field="upload"]'),
|
|
716
|
+
peers: root.querySelector('[data-field="peers"]'),
|
|
717
|
+
total: root.querySelector('[data-field="total"]')
|
|
718
|
+
};
|
|
719
|
+
const $close = root.querySelector(".art-info-close");
|
|
720
|
+
art.template.$player.appendChild(root);
|
|
721
|
+
let timer = 0;
|
|
722
|
+
let opened = false;
|
|
723
|
+
const visibilityCallbacks = [];
|
|
724
|
+
function update() {
|
|
725
|
+
const snapshot = stats.snapshot();
|
|
726
|
+
const p2pOn = controller.p2pEnabled;
|
|
727
|
+
fields.state.textContent = resolveStateText(controller);
|
|
728
|
+
fields.download.textContent = p2pOn ? `${formatSpeed(snapshot.downloadSpeed)}\uFF08P2P ${formatSpeed(snapshot.p2pDownloadSpeed)}\uFF09` : formatSpeed(snapshot.downloadSpeed);
|
|
729
|
+
fields.ratio.textContent = p2pOn ? formatPercent(snapshot.p2pDownloadRatio) : "\u2014";
|
|
730
|
+
fields.upload.textContent = formatSpeed(snapshot.uploadSpeed);
|
|
731
|
+
fields.peers.textContent = `${snapshot.peers} / ${snapshot.peakPeers}`;
|
|
732
|
+
fields.total.textContent = `\u2193 ${formatBytes(snapshot.totalDownloadedBytes)} \xB7 \u2191 ${formatBytes(snapshot.uploadedBytes)}`;
|
|
733
|
+
}
|
|
734
|
+
function notifyVisibility() {
|
|
735
|
+
for (const callback of visibilityCallbacks) callback(opened);
|
|
736
|
+
}
|
|
737
|
+
function openPanel() {
|
|
738
|
+
if (opened) return;
|
|
739
|
+
opened = true;
|
|
740
|
+
art.template.$player.classList.add(SHOW_CLASS);
|
|
741
|
+
art.info.show = false;
|
|
742
|
+
update();
|
|
743
|
+
timer = window.setInterval(update, STATS_POLLING_MS);
|
|
744
|
+
notifyVisibility();
|
|
745
|
+
}
|
|
746
|
+
function closePanel() {
|
|
747
|
+
if (!opened) return;
|
|
748
|
+
opened = false;
|
|
749
|
+
art.template.$player.classList.remove(SHOW_CLASS);
|
|
750
|
+
window.clearInterval(timer);
|
|
751
|
+
notifyVisibility();
|
|
752
|
+
}
|
|
753
|
+
function onCloseClick(event) {
|
|
754
|
+
event.stopPropagation();
|
|
755
|
+
closePanel();
|
|
756
|
+
}
|
|
757
|
+
$close.addEventListener("click", onCloseClick);
|
|
758
|
+
const onNativeInfo = (open) => {
|
|
759
|
+
if (open) closePanel();
|
|
760
|
+
};
|
|
761
|
+
art.on("info", onNativeInfo);
|
|
762
|
+
art.contextmenu.add({
|
|
763
|
+
name: CONTEXTMENU_NAME,
|
|
764
|
+
index: 45,
|
|
765
|
+
html: "P2P \u7EDF\u8BA1",
|
|
766
|
+
click: (contextmenu) => {
|
|
767
|
+
contextmenu.show = false;
|
|
768
|
+
openPanel();
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
return {
|
|
772
|
+
open: openPanel,
|
|
773
|
+
close: closePanel,
|
|
774
|
+
isOpen: () => opened,
|
|
775
|
+
onVisibilityChange(callback) {
|
|
776
|
+
visibilityCallbacks.push(callback);
|
|
777
|
+
},
|
|
778
|
+
destroy() {
|
|
779
|
+
closePanel();
|
|
780
|
+
art.off("info", onNativeInfo);
|
|
781
|
+
$close.removeEventListener("click", onCloseClick);
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/ui/setting.ts
|
|
787
|
+
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>';
|
|
788
|
+
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>';
|
|
789
|
+
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>';
|
|
790
|
+
function mountP2PSettings(art, controller, panel, items) {
|
|
791
|
+
if (!art.option.setting) {
|
|
792
|
+
console.info("[artplayer-plugin-p2p] option.setting \u672A\u5F00\u542F\uFF0C\u8DF3\u8FC7 P2P \u8BBE\u7F6E\u5F00\u5173\u6302\u8F7D");
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
let mounted = 0;
|
|
796
|
+
if (items.p2pEnabled) {
|
|
797
|
+
art.setting.add({
|
|
798
|
+
name: "artp2pSetting",
|
|
799
|
+
html: "P2P \u52A0\u901F",
|
|
800
|
+
tooltip: "P2P \u52A0\u901F",
|
|
801
|
+
icon: ICON_P2P_ENABLED,
|
|
802
|
+
switch: controller.p2pEnabled,
|
|
803
|
+
onSwitch(item) {
|
|
804
|
+
const next = !item.switch;
|
|
805
|
+
controller.setP2PEnabled(next);
|
|
806
|
+
return next;
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
mounted += 1;
|
|
810
|
+
}
|
|
811
|
+
if (items.uploadOnly) {
|
|
812
|
+
art.setting.add({
|
|
813
|
+
name: "artp2pUploadSetting",
|
|
814
|
+
html: "\u4EC5\u4E0A\u4F20\u6A21\u5F0F",
|
|
815
|
+
tooltip: "\u4EC5\u4E0A\u4F20\u6A21\u5F0F",
|
|
816
|
+
icon: ICON_UPLOAD_ONLY,
|
|
817
|
+
switch: !controller.uploadEnabled,
|
|
818
|
+
onSwitch(item) {
|
|
819
|
+
const next = !item.switch;
|
|
820
|
+
controller.setUploadEnabled(!next);
|
|
821
|
+
return next;
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
mounted += 1;
|
|
825
|
+
}
|
|
826
|
+
if (panel && items.stats) {
|
|
827
|
+
const panelItem = {
|
|
828
|
+
name: "artp2pStatsSetting",
|
|
829
|
+
html: "P2P \u7EDF\u8BA1",
|
|
830
|
+
tooltip: "P2P \u7EDF\u8BA1",
|
|
831
|
+
icon: ICON_STATS,
|
|
832
|
+
switch: panel.isOpen(),
|
|
833
|
+
onSwitch(item) {
|
|
834
|
+
const next = !item.switch;
|
|
835
|
+
if (next) {
|
|
836
|
+
panel.open();
|
|
837
|
+
} else {
|
|
838
|
+
panel.close();
|
|
839
|
+
}
|
|
840
|
+
return next;
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
art.setting.add(panelItem);
|
|
844
|
+
panel.onVisibilityChange((open) => {
|
|
845
|
+
panelItem.switch = open;
|
|
846
|
+
});
|
|
847
|
+
mounted += 1;
|
|
848
|
+
}
|
|
849
|
+
return mounted > 0;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// src/ui/index.ts
|
|
853
|
+
function mountUI(art, controller, stats, options) {
|
|
854
|
+
if (!options.uiEnabled) return;
|
|
855
|
+
injectStyles();
|
|
856
|
+
let panel = null;
|
|
857
|
+
if (options.statsEnabled) {
|
|
858
|
+
panel = mountStatsMenu(art, controller, stats);
|
|
859
|
+
}
|
|
860
|
+
if (options.settingEnabled) {
|
|
861
|
+
mountP2PSettings(art, controller, panel, options.settingItems);
|
|
862
|
+
}
|
|
863
|
+
art.on("destroy", () => {
|
|
864
|
+
panel?.destroy();
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/index.ts
|
|
869
|
+
function getUrlExtension(url) {
|
|
870
|
+
const withoutHash = url.split("#")[0];
|
|
871
|
+
const withoutQuery = withoutHash.split("?")[0];
|
|
872
|
+
const lastDotIndex = withoutQuery.lastIndexOf(".");
|
|
873
|
+
if (lastDotIndex === -1) return "";
|
|
874
|
+
return withoutQuery.slice(lastDotIndex + 1).toLowerCase();
|
|
875
|
+
}
|
|
876
|
+
function artplayerPluginP2P(options = {}) {
|
|
877
|
+
return (art) => {
|
|
878
|
+
const resolved = resolveOptions(options);
|
|
879
|
+
const stats = new P2PStatsEngine();
|
|
880
|
+
const controller = new P2PController(art, resolved, stats);
|
|
881
|
+
mountUI(art, controller, stats, resolved);
|
|
882
|
+
function typeCallback(video, url) {
|
|
883
|
+
if (!import_hls2.default.isSupported()) {
|
|
884
|
+
if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
|
885
|
+
video.src = url;
|
|
886
|
+
} else {
|
|
887
|
+
art.notice.show = `Unsupported playback format: ${resolved.typeName}`;
|
|
888
|
+
}
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
controller.activate(url, video);
|
|
892
|
+
}
|
|
893
|
+
let registered = false;
|
|
894
|
+
const customTypeMap = art.option.customType ?? (art.option.customType = {});
|
|
895
|
+
if (customTypeMap[resolved.typeName]) {
|
|
896
|
+
console.warn(`[artplayer-plugin-p2p] customType "${resolved.typeName}" already exists, skip registering`);
|
|
897
|
+
} else {
|
|
898
|
+
customTypeMap[resolved.typeName] = typeCallback;
|
|
899
|
+
registered = true;
|
|
900
|
+
}
|
|
901
|
+
if (registered) {
|
|
902
|
+
const optionUrl = art.option.url;
|
|
903
|
+
if (optionUrl) {
|
|
904
|
+
const currentType = art.option.type || getUrlExtension(optionUrl);
|
|
905
|
+
if (currentType === resolved.typeName) {
|
|
906
|
+
const { $video } = art.template;
|
|
907
|
+
const rawSrc = $video.getAttribute("src");
|
|
908
|
+
if (rawSrc && !rawSrc.startsWith("blob:")) {
|
|
909
|
+
$video.removeAttribute("src");
|
|
910
|
+
$video.load();
|
|
911
|
+
}
|
|
912
|
+
art.url = optionUrl;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return {
|
|
917
|
+
name: "artplayerPluginP2P",
|
|
918
|
+
get engine() {
|
|
919
|
+
return controller.hls?.p2pEngine;
|
|
920
|
+
},
|
|
921
|
+
get hls() {
|
|
922
|
+
return controller.hls;
|
|
923
|
+
},
|
|
924
|
+
destroy() {
|
|
925
|
+
controller.destroy();
|
|
926
|
+
},
|
|
927
|
+
reload() {
|
|
928
|
+
controller.reload();
|
|
929
|
+
},
|
|
930
|
+
getStats() {
|
|
931
|
+
return stats.snapshot();
|
|
932
|
+
},
|
|
933
|
+
setP2PEnabled(enabled) {
|
|
934
|
+
controller.setP2PEnabled(enabled);
|
|
935
|
+
},
|
|
936
|
+
isP2PEnabled() {
|
|
937
|
+
return controller.p2pEnabled;
|
|
938
|
+
},
|
|
939
|
+
setUploadEnabled(enabled) {
|
|
940
|
+
controller.setUploadEnabled(enabled);
|
|
941
|
+
},
|
|
942
|
+
isUploadEnabled() {
|
|
943
|
+
return controller.uploadEnabled;
|
|
944
|
+
},
|
|
945
|
+
/**
|
|
946
|
+
* 运行时动态配置透传:转发到当前引擎的 Core.applyDynamicConfig;
|
|
947
|
+
* 无活跃实例时不生效(即时调参语义,不做延迟补发)
|
|
948
|
+
*/
|
|
949
|
+
applyDynamicConfig(patch) {
|
|
950
|
+
controller.hls?.p2pEngine.applyDynamicConfig({ core: patch });
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
956
|
+
0 && (module.exports = {
|
|
957
|
+
BANDWIDTH_WINDOW_MS,
|
|
958
|
+
DEFAULT_FATAL_RETRY_MAX,
|
|
959
|
+
DEFAULT_TYPE,
|
|
960
|
+
FatalRecoveryPolicy,
|
|
961
|
+
P2PController,
|
|
962
|
+
P2PStatsEngine,
|
|
963
|
+
P2P_EVENT_BRIDGE_MAP,
|
|
964
|
+
STATS_POLLING_MS,
|
|
965
|
+
applyRuntimeToggle,
|
|
966
|
+
createHlsWithP2P,
|
|
967
|
+
mergeCoreConfig,
|
|
968
|
+
resolveOptions
|
|
969
|
+
});
|