@give-tech/ec-player 0.0.1-beta.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/decoder/H264Decoder.d.ts +14 -0
- package/dist/decoder/H264Decoder.d.ts.map +1 -0
- package/dist/decoder/HEVCDecoder.d.ts +21 -0
- package/dist/decoder/HEVCDecoder.d.ts.map +1 -0
- package/dist/decoder/WASMLoader.d.ts +10 -0
- package/dist/decoder/WASMLoader.d.ts.map +1 -0
- package/dist/decoder/index.d.ts +4 -0
- package/dist/decoder/index.d.ts.map +1 -0
- package/dist/demuxer/DemuxerFactory.d.ts +24 -0
- package/dist/demuxer/DemuxerFactory.d.ts.map +1 -0
- package/dist/demuxer/FLVDemuxer.d.ts +132 -0
- package/dist/demuxer/FLVDemuxer.d.ts.map +1 -0
- package/dist/demuxer/FormatDetector.d.ts +21 -0
- package/dist/demuxer/FormatDetector.d.ts.map +1 -0
- package/dist/demuxer/PESExtractor.d.ts +11 -0
- package/dist/demuxer/PESExtractor.d.ts.map +1 -0
- package/dist/demuxer/TSDemuxer.d.ts +17 -0
- package/dist/demuxer/TSDemuxer.d.ts.map +1 -0
- package/dist/demuxer/fMP4Demuxer.d.ts +131 -0
- package/dist/demuxer/fMP4Demuxer.d.ts.map +1 -0
- package/dist/demuxer/index.d.ts +7 -0
- package/dist/demuxer/index.d.ts.map +1 -0
- package/dist/env/EnvDetector.d.ts +167 -0
- package/dist/env/EnvDetector.d.ts.map +1 -0
- package/dist/env/index.d.ts +5 -0
- package/dist/env/index.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3869 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/NALParser.d.ts +21 -0
- package/dist/parser/NALParser.d.ts.map +1 -0
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/player/BasePlayer.d.ts +118 -0
- package/dist/player/BasePlayer.d.ts.map +1 -0
- package/dist/player/EcPlayerCore.d.ts +77 -0
- package/dist/player/EcPlayerCore.d.ts.map +1 -0
- package/dist/player/FLVPlayer.d.ts +134 -0
- package/dist/player/FLVPlayer.d.ts.map +1 -0
- package/dist/player/HLSPlayer.d.ts +99 -0
- package/dist/player/HLSPlayer.d.ts.map +1 -0
- package/dist/player/index.d.ts +3 -0
- package/dist/player/index.d.ts.map +1 -0
- package/dist/prefetch/BasePrefetcher.d.ts +99 -0
- package/dist/prefetch/BasePrefetcher.d.ts.map +1 -0
- package/dist/prefetch/SegmentPrefetcher.d.ts +116 -0
- package/dist/prefetch/SegmentPrefetcher.d.ts.map +1 -0
- package/dist/prefetch/StreamPrefetcher.d.ts +113 -0
- package/dist/prefetch/StreamPrefetcher.d.ts.map +1 -0
- package/dist/prefetch/index.d.ts +13 -0
- package/dist/prefetch/index.d.ts.map +1 -0
- package/dist/prefetch/types.d.ts +93 -0
- package/dist/prefetch/types.d.ts.map +1 -0
- package/dist/renderer/Canvas2DRenderer.d.ts +16 -0
- package/dist/renderer/Canvas2DRenderer.d.ts.map +1 -0
- package/dist/renderer/index.d.ts +2 -0
- package/dist/renderer/index.d.ts.map +1 -0
- package/dist/types/index.d.ts +117 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/logger.d.ts +36 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/package.json +48 -0
- package/wasm/decoder-simd.js +19 -0
- package/wasm/decoder-simd.wasm +0 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3869 @@
|
|
|
1
|
+
class WASMLoader {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.module = null;
|
|
4
|
+
}
|
|
5
|
+
async load(wasmPath) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const script = document.createElement("script");
|
|
8
|
+
script.src = wasmPath;
|
|
9
|
+
script.type = "text/javascript";
|
|
10
|
+
script.onload = async () => {
|
|
11
|
+
try {
|
|
12
|
+
const createFFmpegDecoder = window.createFFmpegDecoder;
|
|
13
|
+
if (typeof createFFmpegDecoder !== "function") {
|
|
14
|
+
reject(new Error("createFFmpegDecoder not found"));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
this.module = await createFFmpegDecoder();
|
|
18
|
+
if (typeof this.module?._create_decoder !== "function") {
|
|
19
|
+
reject(new Error("Module initialization failed"));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
resolve(this.module);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
reject(new Error(`Failed to initialize decoder: ${e.message}`));
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
script.onerror = () => {
|
|
28
|
+
reject(new Error(`Failed to load script: ${wasmPath}`));
|
|
29
|
+
};
|
|
30
|
+
document.head.appendChild(script);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
getModule() {
|
|
34
|
+
return this.module;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
class H264Decoder {
|
|
38
|
+
constructor(wasmLoader) {
|
|
39
|
+
this.wasmModule = null;
|
|
40
|
+
this.decoderContext = null;
|
|
41
|
+
this.wasmModule = wasmLoader.getModule();
|
|
42
|
+
}
|
|
43
|
+
async init() {
|
|
44
|
+
if (!this.wasmModule) {
|
|
45
|
+
throw new Error("WASM module not loaded");
|
|
46
|
+
}
|
|
47
|
+
this.decoderContext = this.wasmModule._create_decoder(27);
|
|
48
|
+
if (this.decoderContext === 0 || this.decoderContext === null) {
|
|
49
|
+
throw new Error("Failed to create decoder context");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
decode(nalUnit) {
|
|
53
|
+
if (this.decoderContext === null || !this.wasmModule) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const dataPtr = this.wasmModule._malloc(nalUnit.size);
|
|
57
|
+
this.wasmModule.HEAPU8.set(nalUnit.data, dataPtr);
|
|
58
|
+
const result = this.wasmModule._decode_video(
|
|
59
|
+
this.decoderContext,
|
|
60
|
+
dataPtr,
|
|
61
|
+
nalUnit.size
|
|
62
|
+
);
|
|
63
|
+
this.wasmModule._free(dataPtr);
|
|
64
|
+
if (result === 1) {
|
|
65
|
+
const width = this.wasmModule._get_frame_width(this.decoderContext);
|
|
66
|
+
const height = this.wasmModule._get_frame_height(this.decoderContext);
|
|
67
|
+
if (width <= 0 || height <= 0) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const yPtr = this.wasmModule._get_frame_data(this.decoderContext, 0);
|
|
71
|
+
const uPtr = this.wasmModule._get_frame_data(this.decoderContext, 1);
|
|
72
|
+
const vPtr = this.wasmModule._get_frame_data(this.decoderContext, 2);
|
|
73
|
+
const yLineSize = this.wasmModule._get_frame_linesize(this.decoderContext, 0);
|
|
74
|
+
const uLineSize = this.wasmModule._get_frame_linesize(this.decoderContext, 1);
|
|
75
|
+
const vLineSize = this.wasmModule._get_frame_linesize(this.decoderContext, 2);
|
|
76
|
+
const frameData = this.yuv420pToRgba(
|
|
77
|
+
yPtr,
|
|
78
|
+
uPtr,
|
|
79
|
+
vPtr,
|
|
80
|
+
width,
|
|
81
|
+
height,
|
|
82
|
+
yLineSize,
|
|
83
|
+
uLineSize,
|
|
84
|
+
vLineSize
|
|
85
|
+
);
|
|
86
|
+
return { width, height, data: frameData };
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
yuv420pToRgba(yPtr, uPtr, vPtr, width, height, yLineSize, uLineSize, vLineSize) {
|
|
91
|
+
const rgba = new Uint8Array(width * height * 4);
|
|
92
|
+
for (let y = 0; y < height; y++) {
|
|
93
|
+
for (let x = 0; x < width; x++) {
|
|
94
|
+
const yIndex = y * yLineSize + x;
|
|
95
|
+
const uIndex = (y >> 1) * uLineSize + (x >> 1);
|
|
96
|
+
const vIndex = (y >> 1) * vLineSize + (x >> 1);
|
|
97
|
+
const yValue = this.wasmModule.HEAPU8[yPtr + yIndex];
|
|
98
|
+
const uValue = this.wasmModule.HEAPU8[uPtr + uIndex];
|
|
99
|
+
const vValue = this.wasmModule.HEAPU8[vPtr + vIndex];
|
|
100
|
+
const c = yValue - 16;
|
|
101
|
+
const d = uValue - 128;
|
|
102
|
+
const e = vValue - 128;
|
|
103
|
+
let r = 298 * c + 409 * e + 128 >> 8;
|
|
104
|
+
let g = 298 * c - 100 * d - 208 * e + 128 >> 8;
|
|
105
|
+
let b = 298 * c + 516 * d + 128 >> 8;
|
|
106
|
+
r = r < 0 ? 0 : r > 255 ? 255 : r;
|
|
107
|
+
g = g < 0 ? 0 : g > 255 ? 255 : g;
|
|
108
|
+
b = b < 0 ? 0 : b > 255 ? 255 : b;
|
|
109
|
+
const rgbaIndex = (y * width + x) * 4;
|
|
110
|
+
rgba[rgbaIndex] = r;
|
|
111
|
+
rgba[rgbaIndex + 1] = g;
|
|
112
|
+
rgba[rgbaIndex + 2] = b;
|
|
113
|
+
rgba[rgbaIndex + 3] = 255;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return rgba;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const DEFAULT_PLAYER_CONFIG = {
|
|
120
|
+
wasmPath: "/wasm/decoder-simd.js",
|
|
121
|
+
targetBufferSize: 60,
|
|
122
|
+
decodeBatchSize: 2,
|
|
123
|
+
maxQueueSize: 200,
|
|
124
|
+
isLive: false
|
|
125
|
+
};
|
|
126
|
+
class BasePlayer {
|
|
127
|
+
constructor(config, callbacks, defaults) {
|
|
128
|
+
this.wasmLoader = new WASMLoader();
|
|
129
|
+
this.decoder = null;
|
|
130
|
+
this.renderer = null;
|
|
131
|
+
this.decoderInitialized = false;
|
|
132
|
+
this.isPlaying = false;
|
|
133
|
+
this.frameTimer = null;
|
|
134
|
+
this.frameBuffer = [];
|
|
135
|
+
this.droppedFrames = 0;
|
|
136
|
+
this.decodeLoopAbort = false;
|
|
137
|
+
this._currentTime = 0;
|
|
138
|
+
this._duration = 0;
|
|
139
|
+
this.state = {
|
|
140
|
+
isPlaying: false,
|
|
141
|
+
isLoaded: false,
|
|
142
|
+
isLoading: false,
|
|
143
|
+
fps: 0,
|
|
144
|
+
resolution: "-",
|
|
145
|
+
decoded: 0,
|
|
146
|
+
downloaded: 0,
|
|
147
|
+
droppedFrames: 0,
|
|
148
|
+
isPrefetching: false,
|
|
149
|
+
segmentIndex: 0,
|
|
150
|
+
totalSegments: 0,
|
|
151
|
+
downloadSpeed: 0,
|
|
152
|
+
currentTime: 0,
|
|
153
|
+
duration: 0
|
|
154
|
+
};
|
|
155
|
+
this.renderLoop = () => {
|
|
156
|
+
if (!this.isPlaying) return;
|
|
157
|
+
this.updateState({
|
|
158
|
+
decoded: this.frameBuffer.length,
|
|
159
|
+
droppedFrames: this.droppedFrames
|
|
160
|
+
});
|
|
161
|
+
if (this.frameBuffer.length > 0 && this.renderer) {
|
|
162
|
+
const frame = this.frameBuffer.shift();
|
|
163
|
+
this.renderer.render(frame);
|
|
164
|
+
this.updateState({ resolution: `${frame.width}x${frame.height}` });
|
|
165
|
+
const fps = this.renderer.updateFps();
|
|
166
|
+
this.updateState({ fps });
|
|
167
|
+
this.callbacks.onFrameRender?.(frame);
|
|
168
|
+
}
|
|
169
|
+
this.frameTimer = requestAnimationFrame(this.renderLoop);
|
|
170
|
+
};
|
|
171
|
+
this.config = { ...defaults, ...config };
|
|
172
|
+
this.callbacks = callbacks;
|
|
173
|
+
}
|
|
174
|
+
// ==================== 公共方法 ====================
|
|
175
|
+
/**
|
|
176
|
+
* 设置渲染器
|
|
177
|
+
*/
|
|
178
|
+
setRenderer(renderer) {
|
|
179
|
+
this.renderer = renderer;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* 初始化解码器
|
|
183
|
+
*/
|
|
184
|
+
async initDecoder() {
|
|
185
|
+
await this.wasmLoader.load(this.config.wasmPath);
|
|
186
|
+
this.decoder = new H264Decoder(this.wasmLoader);
|
|
187
|
+
await this.decoder.init();
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 开始播放
|
|
191
|
+
*/
|
|
192
|
+
async play() {
|
|
193
|
+
this.isPlaying = true;
|
|
194
|
+
this.decodeLoopAbort = false;
|
|
195
|
+
this.updateState({ isPlaying: true });
|
|
196
|
+
this.decodeLoop();
|
|
197
|
+
this.frameTimer = requestAnimationFrame(this.renderLoop);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* 暂停播放
|
|
201
|
+
*/
|
|
202
|
+
pause() {
|
|
203
|
+
this.isPlaying = false;
|
|
204
|
+
this.decodeLoopAbort = true;
|
|
205
|
+
this.updateState({ isPlaying: false });
|
|
206
|
+
if (this.frameTimer !== null) {
|
|
207
|
+
cancelAnimationFrame(this.frameTimer);
|
|
208
|
+
this.frameTimer = null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 销毁播放器,释放资源
|
|
213
|
+
*/
|
|
214
|
+
destroy() {
|
|
215
|
+
console.log("[BasePlayer] Destroying player...");
|
|
216
|
+
this.pause();
|
|
217
|
+
this.frameBuffer = [];
|
|
218
|
+
this.resetState();
|
|
219
|
+
this.state = {
|
|
220
|
+
isPlaying: false,
|
|
221
|
+
isLoaded: false,
|
|
222
|
+
isLoading: false,
|
|
223
|
+
fps: 0,
|
|
224
|
+
resolution: "-",
|
|
225
|
+
decoded: 0,
|
|
226
|
+
downloaded: 0,
|
|
227
|
+
droppedFrames: 0,
|
|
228
|
+
isPrefetching: false,
|
|
229
|
+
segmentIndex: 0,
|
|
230
|
+
totalSegments: 0,
|
|
231
|
+
downloadSpeed: 0,
|
|
232
|
+
currentTime: 0,
|
|
233
|
+
duration: 0
|
|
234
|
+
};
|
|
235
|
+
this.decoder = null;
|
|
236
|
+
this.renderer = null;
|
|
237
|
+
console.log("[BasePlayer] Player destroyed");
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* 获取播放状态
|
|
241
|
+
*/
|
|
242
|
+
getState() {
|
|
243
|
+
return { ...this.state };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* 获取当前播放时间(毫秒)
|
|
247
|
+
*/
|
|
248
|
+
getCurrentTime() {
|
|
249
|
+
return this._currentTime;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* 获取总时长(毫秒)
|
|
253
|
+
*/
|
|
254
|
+
getDuration() {
|
|
255
|
+
return this._duration;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* 设置当前时间(供子类调用)
|
|
259
|
+
*/
|
|
260
|
+
setCurrentTime(time) {
|
|
261
|
+
this._currentTime = time;
|
|
262
|
+
this.updateState({ currentTime: time });
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* 设置总时长(供子类调用)
|
|
266
|
+
*/
|
|
267
|
+
setDuration(duration) {
|
|
268
|
+
this._duration = duration;
|
|
269
|
+
this.updateState({ duration });
|
|
270
|
+
}
|
|
271
|
+
// ==================== 受保护的工具方法 ====================
|
|
272
|
+
/**
|
|
273
|
+
* 更新状态
|
|
274
|
+
*/
|
|
275
|
+
updateState(partial) {
|
|
276
|
+
this.state = { ...this.state, ...partial };
|
|
277
|
+
this.callbacks.onStateChange?.(partial);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* 快速让出主线程
|
|
281
|
+
*/
|
|
282
|
+
yieldFast() {
|
|
283
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* 休眠
|
|
287
|
+
*/
|
|
288
|
+
sleep(ms) {
|
|
289
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* 重置播放器状态
|
|
293
|
+
*/
|
|
294
|
+
resetState() {
|
|
295
|
+
this.frameBuffer = [];
|
|
296
|
+
this.droppedFrames = 0;
|
|
297
|
+
this.decoderInitialized = false;
|
|
298
|
+
this.decodeLoopAbort = false;
|
|
299
|
+
this._currentTime = 0;
|
|
300
|
+
this._duration = 0;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
class TSDemuxer {
|
|
304
|
+
constructor() {
|
|
305
|
+
this.videoPID = 258;
|
|
306
|
+
this.audioPID = 257;
|
|
307
|
+
}
|
|
308
|
+
parse(data) {
|
|
309
|
+
let offset = 0;
|
|
310
|
+
const packets = [];
|
|
311
|
+
while (offset < data.length - 188) {
|
|
312
|
+
if (data[offset] !== 71) {
|
|
313
|
+
offset++;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (offset + 188 > data.length) break;
|
|
317
|
+
const adaptationFieldControl = data[offset + 3] >> 4 & 3;
|
|
318
|
+
const hasPayload = adaptationFieldControl === 1 || adaptationFieldControl === 3;
|
|
319
|
+
if (!hasPayload) {
|
|
320
|
+
offset += 188;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const pidValue = (data[offset + 1] & 31) << 8 | data[offset + 2];
|
|
324
|
+
let streamType = null;
|
|
325
|
+
if (pidValue === this.videoPID) {
|
|
326
|
+
streamType = "video";
|
|
327
|
+
} else if (pidValue === this.audioPID) {
|
|
328
|
+
streamType = "audio";
|
|
329
|
+
}
|
|
330
|
+
packets.push({
|
|
331
|
+
data: data.subarray(offset, offset + 188),
|
|
332
|
+
pid: pidValue,
|
|
333
|
+
stream: streamType
|
|
334
|
+
});
|
|
335
|
+
offset += 188;
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
video: packets.filter((p) => p.stream === "video"),
|
|
339
|
+
audio: packets.filter((p) => p.stream === "audio")
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
class PESExtractor {
|
|
344
|
+
/**
|
|
345
|
+
* 从视频 TS 包中提取 payload 数据
|
|
346
|
+
*/
|
|
347
|
+
extractVideoPayload(packets) {
|
|
348
|
+
const payloads = [];
|
|
349
|
+
let pesHeaderProcessed = false;
|
|
350
|
+
for (const packet of packets) {
|
|
351
|
+
const data = packet.data;
|
|
352
|
+
if (data.length < 5) continue;
|
|
353
|
+
const payloadUnitStart = (data[1] & 64) !== 0;
|
|
354
|
+
const adaptationFieldControl = data[3] >> 4 & 3;
|
|
355
|
+
let payloadStart = 4;
|
|
356
|
+
if (adaptationFieldControl === 2 || adaptationFieldControl === 3) {
|
|
357
|
+
const adaptationFieldLength = data[4];
|
|
358
|
+
payloadStart += 1 + adaptationFieldLength;
|
|
359
|
+
}
|
|
360
|
+
if (adaptationFieldControl === 1 || adaptationFieldControl === 3) {
|
|
361
|
+
if (payloadStart < data.length) {
|
|
362
|
+
let payload = data.subarray(payloadStart);
|
|
363
|
+
if (payloadUnitStart && payload.length >= 9) {
|
|
364
|
+
if (payload[0] === 0 && payload[1] === 0 && payload[2] === 1) {
|
|
365
|
+
const pesHeaderDataLength = payload[8];
|
|
366
|
+
const pesHeaderSize = 9 + pesHeaderDataLength;
|
|
367
|
+
if (payload.length > pesHeaderSize) {
|
|
368
|
+
payload = payload.subarray(pesHeaderSize);
|
|
369
|
+
pesHeaderProcessed = true;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (payload.length > 0 && pesHeaderProcessed) {
|
|
374
|
+
payloads.push(payload);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const totalSize = payloads.reduce((sum, p) => sum + p.length, 0);
|
|
380
|
+
const result = new Uint8Array(totalSize);
|
|
381
|
+
let offset = 0;
|
|
382
|
+
for (const payload of payloads) {
|
|
383
|
+
result.set(payload, offset);
|
|
384
|
+
offset += payload.length;
|
|
385
|
+
}
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function readUint32$1(data, offset) {
|
|
390
|
+
return (data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3]) >>> 0;
|
|
391
|
+
}
|
|
392
|
+
function readInt32(data, offset) {
|
|
393
|
+
const value = readUint32$1(data, offset);
|
|
394
|
+
return value > 2147483647 ? value - 4294967296 : value;
|
|
395
|
+
}
|
|
396
|
+
function readUint64(data, offset) {
|
|
397
|
+
const view = new DataView(data.buffer, data.byteOffset, data.length);
|
|
398
|
+
return Number(view.getBigUint64(offset));
|
|
399
|
+
}
|
|
400
|
+
function readFourCC(data, offset) {
|
|
401
|
+
return String.fromCharCode(
|
|
402
|
+
data[offset],
|
|
403
|
+
data[offset + 1],
|
|
404
|
+
data[offset + 2],
|
|
405
|
+
data[offset + 3]
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
function readBoxFlags(data, offset) {
|
|
409
|
+
return (data[offset] << 16 | data[offset + 1] << 8 | data[offset + 2]) >>> 0;
|
|
410
|
+
}
|
|
411
|
+
class fMP4Demuxer {
|
|
412
|
+
constructor() {
|
|
413
|
+
this.timescale = 1e3;
|
|
414
|
+
this.trackId = 1;
|
|
415
|
+
this.avcC = null;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* 解析初始化段 (ftyp + moov)
|
|
419
|
+
*/
|
|
420
|
+
parseInitSegment(data) {
|
|
421
|
+
let offset = 0;
|
|
422
|
+
while (offset < data.length - 8) {
|
|
423
|
+
const boxSize = readUint32$1(data, offset);
|
|
424
|
+
const boxType = readFourCC(data, offset + 4);
|
|
425
|
+
if (boxSize < 8) break;
|
|
426
|
+
if (boxType === "moov") {
|
|
427
|
+
this.parseMoov(data, offset, boxSize);
|
|
428
|
+
}
|
|
429
|
+
offset += boxSize;
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
trackId: this.trackId,
|
|
433
|
+
timescale: this.timescale,
|
|
434
|
+
avcC: this.avcC || void 0
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* 解析 moov box
|
|
439
|
+
*/
|
|
440
|
+
parseMoov(data, moovOffset, moovSize) {
|
|
441
|
+
let offset = moovOffset + 8;
|
|
442
|
+
const endOffset = moovOffset + moovSize;
|
|
443
|
+
while (offset < endOffset - 8) {
|
|
444
|
+
const boxSize = readUint32$1(data, offset);
|
|
445
|
+
const boxType = readFourCC(data, offset + 4);
|
|
446
|
+
if (boxSize < 8) break;
|
|
447
|
+
if (boxType === "trak") {
|
|
448
|
+
this.parseTrak(data, offset, boxSize);
|
|
449
|
+
}
|
|
450
|
+
offset += boxSize;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* 解析 trak box
|
|
455
|
+
*/
|
|
456
|
+
parseTrak(data, trakOffset, trakSize) {
|
|
457
|
+
let offset = trakOffset + 8;
|
|
458
|
+
const endOffset = trakOffset + trakSize;
|
|
459
|
+
while (offset < endOffset - 8) {
|
|
460
|
+
const boxSize = readUint32$1(data, offset);
|
|
461
|
+
const boxType = readFourCC(data, offset + 4);
|
|
462
|
+
if (boxSize < 8) break;
|
|
463
|
+
if (boxType === "mdia") {
|
|
464
|
+
this.parseMdia(data, offset, boxSize);
|
|
465
|
+
}
|
|
466
|
+
offset += boxSize;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* 解析 mdia box
|
|
471
|
+
*/
|
|
472
|
+
parseMdia(data, mdiaOffset, mdiaSize) {
|
|
473
|
+
let offset = mdiaOffset + 8;
|
|
474
|
+
const endOffset = mdiaOffset + mdiaSize;
|
|
475
|
+
while (offset < endOffset - 8) {
|
|
476
|
+
const boxSize = readUint32$1(data, offset);
|
|
477
|
+
const boxType = readFourCC(data, offset + 4);
|
|
478
|
+
if (boxSize < 8) break;
|
|
479
|
+
if (boxType === "mdhd") {
|
|
480
|
+
this.parseMdhd(data, offset + 8);
|
|
481
|
+
} else if (boxType === "minf") {
|
|
482
|
+
this.parseMinf(data, offset, boxSize);
|
|
483
|
+
}
|
|
484
|
+
offset += boxSize;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* 解析 mdhd box 获取 timescale
|
|
489
|
+
*/
|
|
490
|
+
parseMdhd(data, boxDataOffset) {
|
|
491
|
+
const version = data[boxDataOffset];
|
|
492
|
+
if (version === 1) {
|
|
493
|
+
this.timescale = readUint32$1(data, boxDataOffset + 20);
|
|
494
|
+
} else {
|
|
495
|
+
this.timescale = readUint32$1(data, boxDataOffset + 12);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* 解析 minf box
|
|
500
|
+
*/
|
|
501
|
+
parseMinf(data, minfOffset, minfSize) {
|
|
502
|
+
let offset = minfOffset + 8;
|
|
503
|
+
const endOffset = minfOffset + minfSize;
|
|
504
|
+
while (offset < endOffset - 8) {
|
|
505
|
+
const boxSize = readUint32$1(data, offset);
|
|
506
|
+
const boxType = readFourCC(data, offset + 4);
|
|
507
|
+
if (boxSize < 8) break;
|
|
508
|
+
if (boxType === "stbl") {
|
|
509
|
+
this.parseStbl(data, offset, boxSize);
|
|
510
|
+
}
|
|
511
|
+
offset += boxSize;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* 解析 stbl box 获取 avcC
|
|
516
|
+
*/
|
|
517
|
+
parseStbl(data, stblOffset, stblSize) {
|
|
518
|
+
let offset = stblOffset + 8;
|
|
519
|
+
const endOffset = stblOffset + stblSize;
|
|
520
|
+
while (offset < endOffset - 8) {
|
|
521
|
+
const boxSize = readUint32$1(data, offset);
|
|
522
|
+
const boxType = readFourCC(data, offset + 4);
|
|
523
|
+
if (boxSize < 8) break;
|
|
524
|
+
if (boxType === "stsd") {
|
|
525
|
+
this.parseStsd(data, offset, boxSize);
|
|
526
|
+
}
|
|
527
|
+
offset += boxSize;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* 解析 stsd box 获取 avcC
|
|
532
|
+
*/
|
|
533
|
+
parseStsd(data, stsdOffset, stsdSize) {
|
|
534
|
+
const boxDataOffset = stsdOffset + 8;
|
|
535
|
+
const entryCount = readUint32$1(data, boxDataOffset + 4);
|
|
536
|
+
let offset = boxDataOffset + 8;
|
|
537
|
+
const endOffset = stsdOffset + stsdSize;
|
|
538
|
+
for (let i = 0; i < entryCount && offset < endOffset - 8; i++) {
|
|
539
|
+
const entrySize = readUint32$1(data, offset);
|
|
540
|
+
const entryType = readFourCC(data, offset + 4);
|
|
541
|
+
if (entryType === "avc1" || entryType === "avc3") {
|
|
542
|
+
this.parseAvcSampleEntry(data, offset, entrySize);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
offset += entrySize;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* 解析 AVC Sample Entry
|
|
550
|
+
*/
|
|
551
|
+
parseAvcSampleEntry(data, entryOffset, entrySize) {
|
|
552
|
+
let offset = entryOffset + 8 + 78;
|
|
553
|
+
const endOffset = entryOffset + entrySize;
|
|
554
|
+
while (offset < endOffset - 8) {
|
|
555
|
+
const boxSize = readUint32$1(data, offset);
|
|
556
|
+
const boxType = readFourCC(data, offset + 4);
|
|
557
|
+
if (boxSize < 8) break;
|
|
558
|
+
if (boxType === "avcC") {
|
|
559
|
+
this.avcC = data.slice(offset, offset + boxSize);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
offset += boxSize;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* 解析 moof box
|
|
567
|
+
*/
|
|
568
|
+
parseMoof(data, moofOffset) {
|
|
569
|
+
const moofSize = readUint32$1(data, moofOffset);
|
|
570
|
+
const moof = {
|
|
571
|
+
mfhd: void 0,
|
|
572
|
+
trafs: [],
|
|
573
|
+
dataOffset: moofOffset,
|
|
574
|
+
size: moofSize
|
|
575
|
+
};
|
|
576
|
+
let offset = moofOffset + 8;
|
|
577
|
+
const endOffset = moofOffset + moofSize;
|
|
578
|
+
while (offset < endOffset - 8) {
|
|
579
|
+
const boxSize = readUint32$1(data, offset);
|
|
580
|
+
const boxType = readFourCC(data, offset + 4);
|
|
581
|
+
if (boxSize < 8) break;
|
|
582
|
+
if (boxType === "mfhd") {
|
|
583
|
+
moof.mfhd = {
|
|
584
|
+
sequenceNumber: readUint32$1(data, offset + 8 + 4)
|
|
585
|
+
};
|
|
586
|
+
} else if (boxType === "traf") {
|
|
587
|
+
const traf = this.parseTraf(data, offset, boxSize);
|
|
588
|
+
moof.trafs.push(traf);
|
|
589
|
+
}
|
|
590
|
+
offset += boxSize;
|
|
591
|
+
}
|
|
592
|
+
return moof;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* 解析 traf box
|
|
596
|
+
*/
|
|
597
|
+
parseTraf(data, trafOffset, trafSize) {
|
|
598
|
+
const traf = {
|
|
599
|
+
truns: []
|
|
600
|
+
};
|
|
601
|
+
let offset = trafOffset + 8;
|
|
602
|
+
const endOffset = trafOffset + trafSize;
|
|
603
|
+
while (offset < endOffset - 8) {
|
|
604
|
+
const boxSize = readUint32$1(data, offset);
|
|
605
|
+
const boxType = readFourCC(data, offset + 4);
|
|
606
|
+
if (boxSize < 8) break;
|
|
607
|
+
if (boxType === "tfhd") {
|
|
608
|
+
traf.tfhd = this.parseTfhd(data, offset + 8);
|
|
609
|
+
} else if (boxType === "tfdt") {
|
|
610
|
+
traf.tfdt = this.parseTfdt(data, offset + 8);
|
|
611
|
+
} else if (boxType === "trun") {
|
|
612
|
+
if (traf.tfhd) {
|
|
613
|
+
traf.truns.push(this.parseTrun(data, offset + 8, traf.tfhd));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
offset += boxSize;
|
|
617
|
+
}
|
|
618
|
+
return traf;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* 解析 tfhd box
|
|
622
|
+
*/
|
|
623
|
+
parseTfhd(data, boxDataOffset) {
|
|
624
|
+
const flags = readBoxFlags(data, boxDataOffset);
|
|
625
|
+
const tfhd = {
|
|
626
|
+
trackId: readUint32$1(data, boxDataOffset + 4)
|
|
627
|
+
};
|
|
628
|
+
let offset = boxDataOffset + 8;
|
|
629
|
+
if (flags & 1) {
|
|
630
|
+
tfhd.baseDataOffset = readUint64(data, offset);
|
|
631
|
+
offset += 8;
|
|
632
|
+
}
|
|
633
|
+
if (flags & 2) {
|
|
634
|
+
tfhd.sampleDescriptionIndex = readUint32$1(data, offset);
|
|
635
|
+
offset += 4;
|
|
636
|
+
}
|
|
637
|
+
if (flags & 8) {
|
|
638
|
+
tfhd.defaultSampleDuration = readUint32$1(data, offset);
|
|
639
|
+
offset += 4;
|
|
640
|
+
}
|
|
641
|
+
if (flags & 16) {
|
|
642
|
+
tfhd.defaultSampleSize = readUint32$1(data, offset);
|
|
643
|
+
offset += 4;
|
|
644
|
+
}
|
|
645
|
+
if (flags & 32) {
|
|
646
|
+
tfhd.defaultSampleFlags = readUint32$1(data, offset);
|
|
647
|
+
}
|
|
648
|
+
return tfhd;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* 解析 tfdt box
|
|
652
|
+
*/
|
|
653
|
+
parseTfdt(data, boxDataOffset) {
|
|
654
|
+
const version = data[boxDataOffset];
|
|
655
|
+
return {
|
|
656
|
+
baseMediaDecodeTime: version === 1 ? readUint64(data, boxDataOffset + 4) : readUint32$1(data, boxDataOffset + 4)
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* 解析 trun box
|
|
661
|
+
*/
|
|
662
|
+
parseTrun(data, boxDataOffset, tfhd) {
|
|
663
|
+
const version = data[boxDataOffset];
|
|
664
|
+
const flags = readBoxFlags(data, boxDataOffset + 1);
|
|
665
|
+
const sampleCount = readUint32$1(data, boxDataOffset + 4);
|
|
666
|
+
const trun = {
|
|
667
|
+
sampleCount,
|
|
668
|
+
samples: []
|
|
669
|
+
};
|
|
670
|
+
let offset = boxDataOffset + 8;
|
|
671
|
+
if (flags & 1) {
|
|
672
|
+
trun.dataOffset = readInt32(data, offset);
|
|
673
|
+
offset += 4;
|
|
674
|
+
}
|
|
675
|
+
if (flags & 4) {
|
|
676
|
+
trun.firstSampleFlags = readUint32$1(data, offset);
|
|
677
|
+
offset += 4;
|
|
678
|
+
}
|
|
679
|
+
const hasDuration = (flags & 256) !== 0;
|
|
680
|
+
const hasSize = (flags & 512) !== 0;
|
|
681
|
+
const hasFlags = (flags & 1024) !== 0;
|
|
682
|
+
const hasCto = (flags & 2048) !== 0;
|
|
683
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
684
|
+
const sample = {};
|
|
685
|
+
if (hasDuration) {
|
|
686
|
+
sample.duration = readUint32$1(data, offset);
|
|
687
|
+
offset += 4;
|
|
688
|
+
} else {
|
|
689
|
+
sample.duration = tfhd.defaultSampleDuration;
|
|
690
|
+
}
|
|
691
|
+
if (hasSize) {
|
|
692
|
+
sample.size = readUint32$1(data, offset);
|
|
693
|
+
offset += 4;
|
|
694
|
+
} else {
|
|
695
|
+
sample.size = tfhd.defaultSampleSize;
|
|
696
|
+
}
|
|
697
|
+
if (hasFlags) {
|
|
698
|
+
sample.flags = readUint32$1(data, offset);
|
|
699
|
+
offset += 4;
|
|
700
|
+
} else if (i === 0 && trun.firstSampleFlags !== void 0) {
|
|
701
|
+
sample.flags = trun.firstSampleFlags;
|
|
702
|
+
} else {
|
|
703
|
+
sample.flags = tfhd.defaultSampleFlags;
|
|
704
|
+
}
|
|
705
|
+
if (hasCto) {
|
|
706
|
+
sample.compositionTimeOffset = version === 0 ? readUint32$1(data, offset) : readInt32(data, offset);
|
|
707
|
+
offset += 4;
|
|
708
|
+
}
|
|
709
|
+
trun.samples.push(sample);
|
|
710
|
+
}
|
|
711
|
+
return trun;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* 从 moof + mdat 提取视频样本
|
|
715
|
+
*/
|
|
716
|
+
extractSamples(moof, mdatData, mdatOffset) {
|
|
717
|
+
const samples = [];
|
|
718
|
+
for (const traf of moof.trafs) {
|
|
719
|
+
if (!traf.tfhd || !traf.tfdt) continue;
|
|
720
|
+
let baseDts = traf.tfdt.baseMediaDecodeTime;
|
|
721
|
+
for (const trun of traf.truns) {
|
|
722
|
+
let dataOffset = 0;
|
|
723
|
+
if (trun.dataOffset !== void 0) {
|
|
724
|
+
dataOffset = trun.dataOffset - mdatOffset - 8;
|
|
725
|
+
}
|
|
726
|
+
for (const sample of trun.samples) {
|
|
727
|
+
if (sample.size === void 0 || sample.size <= 0) continue;
|
|
728
|
+
const sampleData = mdatData.slice(dataOffset, dataOffset + sample.size);
|
|
729
|
+
const isSync = (sample.flags ?? 0) & 16777216;
|
|
730
|
+
const cto = sample.compositionTimeOffset ?? 0;
|
|
731
|
+
const duration = sample.duration ?? 0;
|
|
732
|
+
samples.push({
|
|
733
|
+
data: sampleData,
|
|
734
|
+
dts: baseDts,
|
|
735
|
+
pts: baseDts + cto,
|
|
736
|
+
duration,
|
|
737
|
+
isSync: isSync !== 0
|
|
738
|
+
});
|
|
739
|
+
dataOffset += sample.size;
|
|
740
|
+
baseDts += duration;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return samples;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* 获取 timescale
|
|
748
|
+
*/
|
|
749
|
+
getTimescale() {
|
|
750
|
+
return this.timescale;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* 获取 avcC 配置
|
|
754
|
+
*/
|
|
755
|
+
getAvcC() {
|
|
756
|
+
return this.avcC;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
class NALParser {
|
|
760
|
+
/**
|
|
761
|
+
* 解析 H.264 NAL 单元
|
|
762
|
+
* 支持 3 字节和 4 字节的 start code
|
|
763
|
+
*/
|
|
764
|
+
parse(data) {
|
|
765
|
+
const units = [];
|
|
766
|
+
let i = 0;
|
|
767
|
+
const len = data.length;
|
|
768
|
+
while (i < len - 3) {
|
|
769
|
+
let startCodeSize = 0;
|
|
770
|
+
let nalStart = 0;
|
|
771
|
+
if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0 && data[i + 3] === 1) {
|
|
772
|
+
startCodeSize = 4;
|
|
773
|
+
nalStart = i + 4;
|
|
774
|
+
} else if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 1) {
|
|
775
|
+
startCodeSize = 3;
|
|
776
|
+
nalStart = i + 3;
|
|
777
|
+
}
|
|
778
|
+
if (startCodeSize > 0) {
|
|
779
|
+
let nalEnd = len;
|
|
780
|
+
for (let j = nalStart; j < len - 3; j++) {
|
|
781
|
+
if (data[j] === 0 && data[j + 1] === 0) {
|
|
782
|
+
if (data[j + 2] === 0 && data[j + 3] === 1) {
|
|
783
|
+
nalEnd = j;
|
|
784
|
+
break;
|
|
785
|
+
} else if (data[j + 2] === 1) {
|
|
786
|
+
nalEnd = j;
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const nalType = data[nalStart] & 31;
|
|
792
|
+
units.push({
|
|
793
|
+
type: nalType,
|
|
794
|
+
data: data.subarray(nalStart, nalEnd),
|
|
795
|
+
size: nalEnd - nalStart
|
|
796
|
+
});
|
|
797
|
+
i = nalEnd;
|
|
798
|
+
} else {
|
|
799
|
+
i++;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return units;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
class BasePrefetcher {
|
|
806
|
+
constructor(config, callbacks = {}) {
|
|
807
|
+
this.isRunning = false;
|
|
808
|
+
this.abortFlag = false;
|
|
809
|
+
this.prefetchPromise = null;
|
|
810
|
+
this.downloadStartTime = 0;
|
|
811
|
+
this.downloadStartBytes = 0;
|
|
812
|
+
this.lastSpeedLogTime = 0;
|
|
813
|
+
this.lastSpeedLogBytes = 0;
|
|
814
|
+
this.currentSpeed = 0;
|
|
815
|
+
this.loopCount = 0;
|
|
816
|
+
this.config = config;
|
|
817
|
+
this.callbacks = callbacks;
|
|
818
|
+
this.status = this.createInitialState();
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* 创建初始状态(子类可覆盖)
|
|
822
|
+
*/
|
|
823
|
+
createInitialState() {
|
|
824
|
+
return {
|
|
825
|
+
isPrefetching: false,
|
|
826
|
+
queueSize: 0,
|
|
827
|
+
downloadSpeed: 0,
|
|
828
|
+
totalBytes: 0
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* 启动预取循环
|
|
833
|
+
*/
|
|
834
|
+
async start() {
|
|
835
|
+
if (this.isRunning) {
|
|
836
|
+
console.log("[BasePrefetcher] Already running");
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
this.isRunning = true;
|
|
840
|
+
this.abortFlag = false;
|
|
841
|
+
this.loopCount = 0;
|
|
842
|
+
console.log("[BasePrefetcher] Starting prefetch loop...");
|
|
843
|
+
this.prefetchPromise = this.prefetchLoop();
|
|
844
|
+
await this.prefetchPromise;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* 停止预取循环
|
|
848
|
+
*/
|
|
849
|
+
stop() {
|
|
850
|
+
console.log("[BasePrefetcher] Stopping prefetch loop...");
|
|
851
|
+
this.abortFlag = true;
|
|
852
|
+
this.isRunning = false;
|
|
853
|
+
this.updateStatus({ isPrefetching: false });
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* 获取当前状态
|
|
857
|
+
*/
|
|
858
|
+
getStatus() {
|
|
859
|
+
return { ...this.status };
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* 获取下载速度 (KB/s)
|
|
863
|
+
*/
|
|
864
|
+
getDownloadSpeed() {
|
|
865
|
+
return this.currentSpeed;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* 预取循环
|
|
869
|
+
*/
|
|
870
|
+
async prefetchLoop() {
|
|
871
|
+
try {
|
|
872
|
+
while (this.isRunning && !this.abortFlag) {
|
|
873
|
+
this.loopCount++;
|
|
874
|
+
const queueSize = this.getQueueSize();
|
|
875
|
+
this.updateStatus({ queueSize });
|
|
876
|
+
if (this.shouldPrefetch()) {
|
|
877
|
+
this.updateStatus({ isPrefetching: true });
|
|
878
|
+
await this.doPrefetch();
|
|
879
|
+
this.updateStatus({ isPrefetching: false });
|
|
880
|
+
}
|
|
881
|
+
const interval = this.calculateInterval(queueSize);
|
|
882
|
+
await this.sleep(interval);
|
|
883
|
+
}
|
|
884
|
+
console.log("[BasePrefetcher] Prefetch loop ended");
|
|
885
|
+
} catch (error) {
|
|
886
|
+
console.error("[BasePrefetcher] Prefetch loop error:", error);
|
|
887
|
+
this.callbacks.onError?.(error);
|
|
888
|
+
this.updateStatus({
|
|
889
|
+
isPrefetching: false,
|
|
890
|
+
lastError: error.message
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* 计算动态等待间隔
|
|
896
|
+
*/
|
|
897
|
+
calculateInterval(queueSize) {
|
|
898
|
+
if (!this.config.dynamicInterval) {
|
|
899
|
+
return this.config.prefetchInterval;
|
|
900
|
+
}
|
|
901
|
+
const lowWater = this.config.lowWaterMark;
|
|
902
|
+
if (queueSize < lowWater / 2) {
|
|
903
|
+
return 1;
|
|
904
|
+
} else if (queueSize < lowWater) {
|
|
905
|
+
return 3;
|
|
906
|
+
} else if (queueSize < this.config.highWaterMark) {
|
|
907
|
+
return this.config.prefetchInterval;
|
|
908
|
+
} else {
|
|
909
|
+
return this.config.prefetchInterval * 2;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* 更新下载速度
|
|
914
|
+
*/
|
|
915
|
+
updateDownloadSpeed(totalBytes) {
|
|
916
|
+
const now = Date.now();
|
|
917
|
+
if (now - this.lastSpeedLogTime >= 1e3) {
|
|
918
|
+
const elapsed = (now - this.lastSpeedLogTime) / 1e3;
|
|
919
|
+
const downloaded = totalBytes - this.lastSpeedLogBytes;
|
|
920
|
+
const speed = downloaded / elapsed / 1024;
|
|
921
|
+
this.currentSpeed = Math.round(speed);
|
|
922
|
+
this.updateStatus({
|
|
923
|
+
downloadSpeed: this.currentSpeed,
|
|
924
|
+
totalBytes
|
|
925
|
+
});
|
|
926
|
+
this.lastSpeedLogTime = now;
|
|
927
|
+
this.lastSpeedLogBytes = totalBytes;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* 初始化速度监控
|
|
932
|
+
*/
|
|
933
|
+
initSpeedMonitor(initialBytes) {
|
|
934
|
+
this.downloadStartTime = Date.now();
|
|
935
|
+
this.downloadStartBytes = initialBytes;
|
|
936
|
+
this.lastSpeedLogTime = this.downloadStartTime;
|
|
937
|
+
this.lastSpeedLogBytes = initialBytes;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* 更新状态(子类可覆盖以支持扩展属性)
|
|
941
|
+
*/
|
|
942
|
+
updateStatus(partial) {
|
|
943
|
+
this.status = { ...this.status, ...partial };
|
|
944
|
+
this.callbacks.onStatusChange?.(this.status);
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* 辅助方法:休眠
|
|
948
|
+
*/
|
|
949
|
+
sleep(ms) {
|
|
950
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* 辅助方法:快速让出(用于不阻塞事件循环)
|
|
954
|
+
*/
|
|
955
|
+
async yieldFast() {
|
|
956
|
+
await this.sleep(0);
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* 重置预取器状态
|
|
960
|
+
*/
|
|
961
|
+
reset() {
|
|
962
|
+
this.stop();
|
|
963
|
+
this.doReset();
|
|
964
|
+
this.status = this.createInitialState();
|
|
965
|
+
this.currentSpeed = 0;
|
|
966
|
+
this.loopCount = 0;
|
|
967
|
+
console.log("[BasePrefetcher] Reset complete");
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
class SegmentPrefetcher extends BasePrefetcher {
|
|
971
|
+
constructor(config, callbacks = {}) {
|
|
972
|
+
super(config, callbacks);
|
|
973
|
+
this.prefetchQueue = [];
|
|
974
|
+
this.segments = [];
|
|
975
|
+
this.currentSegmentIndex = 0;
|
|
976
|
+
this.baseUrl = "";
|
|
977
|
+
this.isPrefetchingSegment = false;
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* 创建初始状态
|
|
981
|
+
*/
|
|
982
|
+
createInitialState() {
|
|
983
|
+
return {
|
|
984
|
+
...super.createInitialState(),
|
|
985
|
+
currentSegmentIndex: 0,
|
|
986
|
+
totalSegments: 0,
|
|
987
|
+
prefetchQueueSize: 0
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* 设置分片列表
|
|
992
|
+
*/
|
|
993
|
+
setSegments(segments, baseUrl) {
|
|
994
|
+
this.segments = segments;
|
|
995
|
+
this.baseUrl = baseUrl;
|
|
996
|
+
this.currentSegmentIndex = 0;
|
|
997
|
+
this.prefetchQueue = [];
|
|
998
|
+
this.updateStatus({
|
|
999
|
+
totalSegments: segments.length,
|
|
1000
|
+
currentSegmentIndex: 0,
|
|
1001
|
+
prefetchQueueSize: 0
|
|
1002
|
+
});
|
|
1003
|
+
console.log(`[SegmentPrefetcher] Set ${segments.length} segments, baseUrl: ${baseUrl}`);
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* 设置当前分片索引(用于 seek)
|
|
1007
|
+
*/
|
|
1008
|
+
setCurrentSegmentIndex(index) {
|
|
1009
|
+
this.currentSegmentIndex = index;
|
|
1010
|
+
this.prefetchQueue = [];
|
|
1011
|
+
this.updateStatus({
|
|
1012
|
+
currentSegmentIndex: index,
|
|
1013
|
+
prefetchQueueSize: 0
|
|
1014
|
+
});
|
|
1015
|
+
console.log(`[SegmentPrefetcher] Current segment index set to ${index}`);
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* 获取当前队列大小(返回预取队列大小)
|
|
1019
|
+
*/
|
|
1020
|
+
getQueueSize() {
|
|
1021
|
+
return this.prefetchQueue.length;
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* 判断是否应该预取
|
|
1025
|
+
*/
|
|
1026
|
+
shouldPrefetch() {
|
|
1027
|
+
if (this.isPrefetchingSegment) return false;
|
|
1028
|
+
if (this.segments.length === 0) return false;
|
|
1029
|
+
const prefetchAhead = this.config.prefetchAhead;
|
|
1030
|
+
if (this.prefetchQueue.length >= prefetchAhead) return false;
|
|
1031
|
+
const nextIndex = this.currentSegmentIndex + this.prefetchQueue.length;
|
|
1032
|
+
return nextIndex < this.segments.length;
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* 执行预取
|
|
1036
|
+
*/
|
|
1037
|
+
async doPrefetch() {
|
|
1038
|
+
if (this.isPrefetchingSegment) return;
|
|
1039
|
+
this.isPrefetchingSegment = true;
|
|
1040
|
+
this.updateStatus({ isPrefetching: true });
|
|
1041
|
+
try {
|
|
1042
|
+
const nextIndex = this.currentSegmentIndex + this.prefetchQueue.length;
|
|
1043
|
+
if (nextIndex >= this.segments.length) {
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const segment = this.segments[nextIndex];
|
|
1047
|
+
const fetchStart = performance.now();
|
|
1048
|
+
const data = await this.fetchSegment(segment, nextIndex);
|
|
1049
|
+
const fetchTime = performance.now() - fetchStart;
|
|
1050
|
+
this.prefetchQueue.push({
|
|
1051
|
+
data,
|
|
1052
|
+
segmentIndex: nextIndex,
|
|
1053
|
+
fetchTime
|
|
1054
|
+
});
|
|
1055
|
+
this.updateStatus({ prefetchQueueSize: this.prefetchQueue.length });
|
|
1056
|
+
const callbacks = this.callbacks;
|
|
1057
|
+
callbacks.onSegmentFetched?.(nextIndex, data);
|
|
1058
|
+
console.log(`[SegmentPrefetcher] Fetched segment #${nextIndex}: ${data.length} bytes, ${fetchTime.toFixed(0)}ms`);
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
console.error(`[SegmentPrefetcher] Fetch failed:`, error.message);
|
|
1061
|
+
this.callbacks.onError?.(error);
|
|
1062
|
+
} finally {
|
|
1063
|
+
this.isPrefetchingSegment = false;
|
|
1064
|
+
this.updateStatus({ isPrefetching: false });
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* 处理预取队列中的数据
|
|
1069
|
+
*
|
|
1070
|
+
* @returns 是否有数据被处理
|
|
1071
|
+
*/
|
|
1072
|
+
processQueue() {
|
|
1073
|
+
let processed = false;
|
|
1074
|
+
while (this.prefetchQueue.length > 0) {
|
|
1075
|
+
const item = this.prefetchQueue[0];
|
|
1076
|
+
if (item.segmentIndex !== this.currentSegmentIndex) {
|
|
1077
|
+
break;
|
|
1078
|
+
}
|
|
1079
|
+
this.prefetchQueue.shift();
|
|
1080
|
+
this.updateStatus({ prefetchQueueSize: this.prefetchQueue.length });
|
|
1081
|
+
const parseStart = performance.now();
|
|
1082
|
+
const itemCount = this.parseSegment(item.data, item.segmentIndex);
|
|
1083
|
+
const parseTime = performance.now() - parseStart;
|
|
1084
|
+
this.currentSegmentIndex++;
|
|
1085
|
+
this.updateStatus({ currentSegmentIndex: this.currentSegmentIndex });
|
|
1086
|
+
const callbacks = this.callbacks;
|
|
1087
|
+
callbacks.onSegmentParsed?.(item.segmentIndex, itemCount);
|
|
1088
|
+
console.log(`[SegmentPrefetcher] Parsed segment #${item.segmentIndex}: ${itemCount} items, ${parseTime.toFixed(0)}ms`);
|
|
1089
|
+
processed = true;
|
|
1090
|
+
}
|
|
1091
|
+
return processed;
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* 重置状态
|
|
1095
|
+
*/
|
|
1096
|
+
doReset() {
|
|
1097
|
+
this.prefetchQueue = [];
|
|
1098
|
+
this.segments = [];
|
|
1099
|
+
this.currentSegmentIndex = 0;
|
|
1100
|
+
this.baseUrl = "";
|
|
1101
|
+
this.isPrefetchingSegment = false;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* 获取当前分片索引
|
|
1105
|
+
*/
|
|
1106
|
+
getCurrentSegmentIndex() {
|
|
1107
|
+
return this.currentSegmentIndex;
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* 获取总分片数
|
|
1111
|
+
*/
|
|
1112
|
+
getTotalSegments() {
|
|
1113
|
+
return this.segments.length;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
class StreamPrefetcher extends BasePrefetcher {
|
|
1117
|
+
constructor(config, callbacks = {}) {
|
|
1118
|
+
super(config, callbacks);
|
|
1119
|
+
this.downloadBuffer = null;
|
|
1120
|
+
this.bufferLength = 0;
|
|
1121
|
+
this.reader = null;
|
|
1122
|
+
this.downloadComplete = false;
|
|
1123
|
+
this.downloadPromise = null;
|
|
1124
|
+
this.totalDownloadedBytes = 0;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* 创建初始状态
|
|
1128
|
+
*/
|
|
1129
|
+
createInitialState() {
|
|
1130
|
+
return {
|
|
1131
|
+
...super.createInitialState(),
|
|
1132
|
+
bufferLength: 0,
|
|
1133
|
+
bufferCapacity: 0,
|
|
1134
|
+
downloadComplete: false
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* 设置流读取器
|
|
1139
|
+
*/
|
|
1140
|
+
setReader(reader, initialData) {
|
|
1141
|
+
this.reader = reader;
|
|
1142
|
+
this.downloadComplete = false;
|
|
1143
|
+
this.totalDownloadedBytes = 0;
|
|
1144
|
+
const initialSize = this.config.initialBufferSize;
|
|
1145
|
+
this.downloadBuffer = new Uint8Array(initialSize);
|
|
1146
|
+
this.bufferLength = 0;
|
|
1147
|
+
if (initialData && initialData.length > 0) {
|
|
1148
|
+
this.ensureCapacity(initialData.length);
|
|
1149
|
+
this.downloadBuffer.set(initialData, 0);
|
|
1150
|
+
this.bufferLength = initialData.length;
|
|
1151
|
+
this.totalDownloadedBytes = initialData.length;
|
|
1152
|
+
}
|
|
1153
|
+
this.updateStatus({
|
|
1154
|
+
bufferLength: this.bufferLength,
|
|
1155
|
+
bufferCapacity: this.downloadBuffer.length,
|
|
1156
|
+
downloadComplete: false,
|
|
1157
|
+
totalBytes: this.totalDownloadedBytes
|
|
1158
|
+
});
|
|
1159
|
+
this.initSpeedMonitor(this.totalDownloadedBytes);
|
|
1160
|
+
console.log(`[StreamPrefetcher] Reader set, initial buffer: ${this.bufferLength} bytes, capacity: ${this.downloadBuffer.length}`);
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* 确保缓冲区有足够的容量
|
|
1164
|
+
*/
|
|
1165
|
+
ensureCapacity(needed) {
|
|
1166
|
+
if (!this.downloadBuffer) {
|
|
1167
|
+
const size = Math.max(this.config.initialBufferSize, needed * 2);
|
|
1168
|
+
this.downloadBuffer = new Uint8Array(size);
|
|
1169
|
+
this.bufferLength = 0;
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const remaining = this.downloadBuffer.length - this.bufferLength;
|
|
1173
|
+
if (remaining < needed) {
|
|
1174
|
+
const newSize = Math.max(
|
|
1175
|
+
this.downloadBuffer.length * 2,
|
|
1176
|
+
this.bufferLength + needed
|
|
1177
|
+
);
|
|
1178
|
+
const newBuffer = new Uint8Array(newSize);
|
|
1179
|
+
newBuffer.set(this.downloadBuffer.slice(0, this.bufferLength), 0);
|
|
1180
|
+
this.downloadBuffer = newBuffer;
|
|
1181
|
+
console.log(`[StreamPrefetcher] Buffer expanded to ${newSize} bytes`);
|
|
1182
|
+
this.updateStatus({ bufferCapacity: newSize });
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* 获取当前队列大小(使用缓冲区长度)
|
|
1187
|
+
*/
|
|
1188
|
+
getQueueSize() {
|
|
1189
|
+
return this.bufferLength;
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* 判断是否应该预取
|
|
1193
|
+
*/
|
|
1194
|
+
shouldPrefetch() {
|
|
1195
|
+
if (!this.reader) return false;
|
|
1196
|
+
if (this.downloadComplete) return false;
|
|
1197
|
+
return this.bufferLength < this.config.highWaterMark * 1e3;
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* 执行预取(从流读取数据)
|
|
1201
|
+
*/
|
|
1202
|
+
async doPrefetch() {
|
|
1203
|
+
if (!this.reader || this.downloadComplete) return;
|
|
1204
|
+
try {
|
|
1205
|
+
const { done, value } = await this.reader.read();
|
|
1206
|
+
if (value) {
|
|
1207
|
+
this.ensureCapacity(value.length);
|
|
1208
|
+
if (this.downloadBuffer) {
|
|
1209
|
+
this.downloadBuffer.set(value, this.bufferLength);
|
|
1210
|
+
this.bufferLength += value.length;
|
|
1211
|
+
this.totalDownloadedBytes += value.length;
|
|
1212
|
+
}
|
|
1213
|
+
this.updateDownloadSpeed(this.totalDownloadedBytes);
|
|
1214
|
+
this.updateStatus({
|
|
1215
|
+
bufferLength: this.bufferLength,
|
|
1216
|
+
totalBytes: this.totalDownloadedBytes
|
|
1217
|
+
});
|
|
1218
|
+
const callbacks = this.callbacks;
|
|
1219
|
+
callbacks.onDataReceived?.(this.totalDownloadedBytes);
|
|
1220
|
+
}
|
|
1221
|
+
if (done) {
|
|
1222
|
+
this.downloadComplete = true;
|
|
1223
|
+
this.updateStatus({ downloadComplete: true });
|
|
1224
|
+
console.log(`[StreamPrefetcher] Download complete: ${this.totalDownloadedBytes} bytes`);
|
|
1225
|
+
}
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
console.error(`[StreamPrefetcher] Read error:`, error.message);
|
|
1228
|
+
this.callbacks.onError?.(error);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* 处理缓冲区数据
|
|
1233
|
+
*
|
|
1234
|
+
* @returns 是否有数据被处理
|
|
1235
|
+
*/
|
|
1236
|
+
processBuffer() {
|
|
1237
|
+
if (!this.downloadBuffer || this.bufferLength === 0) {
|
|
1238
|
+
return false;
|
|
1239
|
+
}
|
|
1240
|
+
const data = this.downloadBuffer.slice(0, this.bufferLength);
|
|
1241
|
+
const result = this.processBufferData(data);
|
|
1242
|
+
this.updateStatus({ bufferLength: this.bufferLength });
|
|
1243
|
+
const callbacks = this.callbacks;
|
|
1244
|
+
callbacks.onBufferProcessed?.(result.processedBytes);
|
|
1245
|
+
return result.hasNewData;
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* 获取缓冲区信息
|
|
1249
|
+
*/
|
|
1250
|
+
getBufferInfo() {
|
|
1251
|
+
return {
|
|
1252
|
+
buffer: this.downloadBuffer,
|
|
1253
|
+
length: this.bufferLength,
|
|
1254
|
+
capacity: this.downloadBuffer?.length || 0
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* 获取缓冲区数据(复制)
|
|
1259
|
+
*/
|
|
1260
|
+
getBufferData() {
|
|
1261
|
+
if (!this.downloadBuffer || this.bufferLength === 0) {
|
|
1262
|
+
return null;
|
|
1263
|
+
}
|
|
1264
|
+
return this.downloadBuffer.slice(0, this.bufferLength);
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* 重置状态
|
|
1268
|
+
*/
|
|
1269
|
+
doReset() {
|
|
1270
|
+
this.downloadBuffer = null;
|
|
1271
|
+
this.bufferLength = 0;
|
|
1272
|
+
this.reader = null;
|
|
1273
|
+
this.downloadComplete = false;
|
|
1274
|
+
this.downloadPromise = null;
|
|
1275
|
+
this.totalDownloadedBytes = 0;
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* 取消下载
|
|
1279
|
+
*/
|
|
1280
|
+
cancelDownload() {
|
|
1281
|
+
if (this.reader) {
|
|
1282
|
+
this.reader.cancel();
|
|
1283
|
+
this.reader = null;
|
|
1284
|
+
}
|
|
1285
|
+
this.downloadComplete = true;
|
|
1286
|
+
this.updateStatus({ downloadComplete: true });
|
|
1287
|
+
console.log(`[StreamPrefetcher] Download cancelled`);
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* 是否下载完成
|
|
1291
|
+
*/
|
|
1292
|
+
isDownloadComplete() {
|
|
1293
|
+
return this.downloadComplete;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
const DEFAULT_PREFETCHER_CONFIG = {
|
|
1297
|
+
lowWaterMark: 30,
|
|
1298
|
+
highWaterMark: 100,
|
|
1299
|
+
prefetchInterval: 10,
|
|
1300
|
+
dynamicInterval: true
|
|
1301
|
+
};
|
|
1302
|
+
const DEFAULT_SEGMENT_PREFETCHER_CONFIG = {
|
|
1303
|
+
...DEFAULT_PREFETCHER_CONFIG,
|
|
1304
|
+
prefetchAhead: 2
|
|
1305
|
+
};
|
|
1306
|
+
const DEFAULT_STREAM_PREFETCHER_CONFIG = {
|
|
1307
|
+
...DEFAULT_PREFETCHER_CONFIG,
|
|
1308
|
+
lowWaterMark: 100,
|
|
1309
|
+
highWaterMark: 500,
|
|
1310
|
+
initialBufferSize: 2 * 1024 * 1024
|
|
1311
|
+
// 2MB
|
|
1312
|
+
};
|
|
1313
|
+
const HLS_PREFETCHER_CONFIG = {
|
|
1314
|
+
...DEFAULT_SEGMENT_PREFETCHER_CONFIG,
|
|
1315
|
+
lowWaterMark: 30,
|
|
1316
|
+
highWaterMark: 100,
|
|
1317
|
+
prefetchAhead: 2,
|
|
1318
|
+
prefetchInterval: 10,
|
|
1319
|
+
dynamicInterval: true
|
|
1320
|
+
};
|
|
1321
|
+
const DEFAULT_HLS_CONFIG = {
|
|
1322
|
+
...DEFAULT_PLAYER_CONFIG
|
|
1323
|
+
};
|
|
1324
|
+
class HLSSegmentPrefetcher extends SegmentPrefetcher {
|
|
1325
|
+
constructor(config, callbacks, player) {
|
|
1326
|
+
super(config, callbacks);
|
|
1327
|
+
this.player = player;
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* 获取分片数据
|
|
1331
|
+
*/
|
|
1332
|
+
async fetchSegment(segment, index) {
|
|
1333
|
+
const baseUrl = this.baseUrl;
|
|
1334
|
+
const url = segment.uri.startsWith("http") ? segment.uri : baseUrl + segment.uri;
|
|
1335
|
+
const headers = {};
|
|
1336
|
+
if (segment.byteRange) {
|
|
1337
|
+
const { start, end } = segment.byteRange;
|
|
1338
|
+
headers["Range"] = `bytes=${start}-${end}`;
|
|
1339
|
+
}
|
|
1340
|
+
const response = await fetch(url, { headers });
|
|
1341
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* 解析分片数据
|
|
1345
|
+
*/
|
|
1346
|
+
parseSegment(data, index) {
|
|
1347
|
+
if (this.player.isFMP4) {
|
|
1348
|
+
return this.player.parseFMP4Data(data);
|
|
1349
|
+
} else {
|
|
1350
|
+
return this.player.parseTSData(data);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* 获取队列大小(使用 player 的解码队列大小)
|
|
1355
|
+
*/
|
|
1356
|
+
getQueueSize() {
|
|
1357
|
+
if (this.player.isFMP4) {
|
|
1358
|
+
return this.player.sampleQueue.length;
|
|
1359
|
+
} else {
|
|
1360
|
+
return this.player.nalQueue.length;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
class HLSPlayer extends BasePlayer {
|
|
1365
|
+
constructor(config = {}, callbacks = {}) {
|
|
1366
|
+
super(config, callbacks, DEFAULT_HLS_CONFIG);
|
|
1367
|
+
this.tsDemuxer = new TSDemuxer();
|
|
1368
|
+
this.pesExtractor = new PESExtractor();
|
|
1369
|
+
this.nalParser = new NALParser();
|
|
1370
|
+
this.fmp4Demuxer = new fMP4Demuxer();
|
|
1371
|
+
this.isPrefetching = false;
|
|
1372
|
+
this.currentSegmentIndex = 0;
|
|
1373
|
+
this.segments = [];
|
|
1374
|
+
this.fmp4Segments = [];
|
|
1375
|
+
this.initSegment = null;
|
|
1376
|
+
this._isFMP4 = false;
|
|
1377
|
+
this.currentPlaylistUrl = "";
|
|
1378
|
+
this._nalQueue = [];
|
|
1379
|
+
this._sampleQueue = [];
|
|
1380
|
+
this.prefetcher = null;
|
|
1381
|
+
this.renderLoop = () => {
|
|
1382
|
+
if (!this.isPlaying) return;
|
|
1383
|
+
const downloaded = this.isFMP4 ? this.sampleQueue.length : this.nalQueue.length;
|
|
1384
|
+
const segments = this.isFMP4 ? this.fmp4Segments : this.segments;
|
|
1385
|
+
const totalSegments = segments.length;
|
|
1386
|
+
let currentTime = 0;
|
|
1387
|
+
for (let i = 0; i < this.currentSegmentIndex && i < segments.length; i++) {
|
|
1388
|
+
currentTime += segments[i].duration * 1e3;
|
|
1389
|
+
}
|
|
1390
|
+
this.setCurrentTime(currentTime);
|
|
1391
|
+
this.updateState({
|
|
1392
|
+
decoded: this.frameBuffer.length,
|
|
1393
|
+
downloaded,
|
|
1394
|
+
droppedFrames: this.droppedFrames,
|
|
1395
|
+
segmentIndex: this.currentSegmentIndex,
|
|
1396
|
+
totalSegments
|
|
1397
|
+
});
|
|
1398
|
+
if (this.frameBuffer.length > 0 && this.renderer) {
|
|
1399
|
+
const frame = this.frameBuffer.shift();
|
|
1400
|
+
this.renderer.render(frame);
|
|
1401
|
+
this.updateState({ resolution: `${frame.width}x${frame.height}` });
|
|
1402
|
+
const fps = this.renderer.updateFps();
|
|
1403
|
+
this.updateState({ fps });
|
|
1404
|
+
this.callbacks.onFrameRender?.(frame);
|
|
1405
|
+
}
|
|
1406
|
+
this.frameTimer = requestAnimationFrame(this.renderLoop);
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
// 公共访问器(供预取器使用)
|
|
1410
|
+
get isFMP4() {
|
|
1411
|
+
return this._isFMP4;
|
|
1412
|
+
}
|
|
1413
|
+
get nalQueue() {
|
|
1414
|
+
return this._nalQueue;
|
|
1415
|
+
}
|
|
1416
|
+
get sampleQueue() {
|
|
1417
|
+
return this._sampleQueue;
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* 加载 HLS 播放列表
|
|
1421
|
+
*/
|
|
1422
|
+
async load(url) {
|
|
1423
|
+
this.currentPlaylistUrl = url;
|
|
1424
|
+
this.segments = [];
|
|
1425
|
+
this.fmp4Segments = [];
|
|
1426
|
+
this.initSegment = null;
|
|
1427
|
+
this._nalQueue.length = 0;
|
|
1428
|
+
this._sampleQueue.length = 0;
|
|
1429
|
+
this.currentSegmentIndex = 0;
|
|
1430
|
+
this.resetState();
|
|
1431
|
+
if (this.prefetcher) {
|
|
1432
|
+
this.prefetcher.reset();
|
|
1433
|
+
this.prefetcher = null;
|
|
1434
|
+
}
|
|
1435
|
+
const playlist = await this.parsePlaylist(url);
|
|
1436
|
+
this._isFMP4 = playlist.isFMP4;
|
|
1437
|
+
console.log("[load] Detected format:", this.isFMP4 ? "fMP4" : "TS");
|
|
1438
|
+
if (playlist.isFMP4) {
|
|
1439
|
+
if (!playlist.fmp4Segments || playlist.fmp4Segments.length === 0) {
|
|
1440
|
+
throw new Error("No fMP4 segments found");
|
|
1441
|
+
}
|
|
1442
|
+
this.fmp4Segments = playlist.fmp4Segments;
|
|
1443
|
+
if (playlist.initSegment) {
|
|
1444
|
+
this.initSegment = playlist.initSegment;
|
|
1445
|
+
}
|
|
1446
|
+
console.log("[HLSPlayer] fMP4 stream parsed:", playlist.fmp4Segments.length, "segments");
|
|
1447
|
+
} else {
|
|
1448
|
+
if (!playlist.segments || playlist.segments.length === 0) {
|
|
1449
|
+
throw new Error("No TS segments found");
|
|
1450
|
+
}
|
|
1451
|
+
this.segments = playlist.segments;
|
|
1452
|
+
console.log("[HLSPlayer] TS stream parsed:", playlist.segments.length, "segments");
|
|
1453
|
+
}
|
|
1454
|
+
this.currentSegmentIndex = 0;
|
|
1455
|
+
const segments = this.isFMP4 ? this.fmp4Segments : this.segments;
|
|
1456
|
+
const totalDuration = segments.reduce((sum, seg) => sum + seg.duration * 1e3, 0);
|
|
1457
|
+
this.setDuration(totalDuration);
|
|
1458
|
+
this.updateState({
|
|
1459
|
+
isLoaded: true,
|
|
1460
|
+
totalSegments: segments.length
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* 初始化解码器(覆盖基类方法,添加 fMP4 支持)
|
|
1465
|
+
*/
|
|
1466
|
+
async initDecoder() {
|
|
1467
|
+
await super.initDecoder();
|
|
1468
|
+
if (this.isFMP4 && this.initSegment) {
|
|
1469
|
+
await this.loadInitSegment();
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* 开始播放(覆盖基类方法,使用自定义渲染循环)
|
|
1474
|
+
*/
|
|
1475
|
+
async play() {
|
|
1476
|
+
this.isPlaying = true;
|
|
1477
|
+
this.decodeLoopAbort = false;
|
|
1478
|
+
this.updateState({ isPlaying: true });
|
|
1479
|
+
this.initPrefetcher();
|
|
1480
|
+
this.prefetcher?.start();
|
|
1481
|
+
this.decodeLoop();
|
|
1482
|
+
this.frameTimer = requestAnimationFrame(this.renderLoop);
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* 跳转到指定时间
|
|
1486
|
+
*/
|
|
1487
|
+
async seek(time) {
|
|
1488
|
+
const segments = this.isFMP4 ? this.fmp4Segments : this.segments;
|
|
1489
|
+
if (segments.length === 0) return;
|
|
1490
|
+
let accumulatedTime = 0;
|
|
1491
|
+
let targetIndex = 0;
|
|
1492
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1493
|
+
const segmentDuration = segments[i].duration * 1e3;
|
|
1494
|
+
if (accumulatedTime + segmentDuration > time) {
|
|
1495
|
+
targetIndex = i;
|
|
1496
|
+
break;
|
|
1497
|
+
}
|
|
1498
|
+
accumulatedTime += segmentDuration;
|
|
1499
|
+
targetIndex = i;
|
|
1500
|
+
}
|
|
1501
|
+
this.frameBuffer = [];
|
|
1502
|
+
this._nalQueue.length = 0;
|
|
1503
|
+
this._sampleQueue.length = 0;
|
|
1504
|
+
this.currentSegmentIndex = targetIndex;
|
|
1505
|
+
if (this.prefetcher) {
|
|
1506
|
+
this.prefetcher.setCurrentSegmentIndex(targetIndex);
|
|
1507
|
+
}
|
|
1508
|
+
this.setCurrentTime(time);
|
|
1509
|
+
console.log("[HLSPlayer] Seek to", time, "ms, segment:", targetIndex);
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* 解码循环
|
|
1513
|
+
*/
|
|
1514
|
+
async decodeLoop() {
|
|
1515
|
+
console.log("[DecodeLoop] START, isFMP4:", this.isFMP4);
|
|
1516
|
+
let batchCount = 0;
|
|
1517
|
+
while (this.isPlaying && !this.decodeLoopAbort) {
|
|
1518
|
+
this.prefetcher?.processQueue();
|
|
1519
|
+
const batchSize = this.config.decodeBatchSize;
|
|
1520
|
+
let decodedInBatch = 0;
|
|
1521
|
+
if (this.isFMP4) {
|
|
1522
|
+
while (this.sampleQueue.length > 0 && decodedInBatch < batchSize) {
|
|
1523
|
+
const queuedSample = this.sampleQueue.shift();
|
|
1524
|
+
const sample = queuedSample.sample;
|
|
1525
|
+
const frame = this.decodeSample(sample);
|
|
1526
|
+
if (frame) {
|
|
1527
|
+
this.frameBuffer.push(frame);
|
|
1528
|
+
decodedInBatch++;
|
|
1529
|
+
while (this.frameBuffer.length > this.config.targetBufferSize) {
|
|
1530
|
+
this.frameBuffer.shift();
|
|
1531
|
+
this.droppedFrames++;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
} else {
|
|
1536
|
+
while (this.nalQueue.length > 0 && decodedInBatch < batchSize) {
|
|
1537
|
+
const queuedNal = this.nalQueue.shift();
|
|
1538
|
+
const nalUnit = queuedNal.nalUnit;
|
|
1539
|
+
if (!this.decoderInitialized && (nalUnit.type === 7 || nalUnit.type === 8)) {
|
|
1540
|
+
this.initDecoderParamsSync([nalUnit]);
|
|
1541
|
+
}
|
|
1542
|
+
if (nalUnit.type === 5 || nalUnit.type === 1) {
|
|
1543
|
+
const frame = this.decodeNAL(nalUnit);
|
|
1544
|
+
if (frame) {
|
|
1545
|
+
this.frameBuffer.push(frame);
|
|
1546
|
+
decodedInBatch++;
|
|
1547
|
+
while (this.frameBuffer.length > this.config.targetBufferSize) {
|
|
1548
|
+
this.frameBuffer.shift();
|
|
1549
|
+
this.droppedFrames++;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
batchCount++;
|
|
1556
|
+
if (decodedInBatch > 0) {
|
|
1557
|
+
await this.yieldFast();
|
|
1558
|
+
} else {
|
|
1559
|
+
await this.sleep(2);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
console.log("[DecodeLoop] END, batches:", batchCount);
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* 初始化预取器
|
|
1566
|
+
*/
|
|
1567
|
+
initPrefetcher() {
|
|
1568
|
+
this.prefetcher = new HLSSegmentPrefetcher(
|
|
1569
|
+
HLS_PREFETCHER_CONFIG,
|
|
1570
|
+
{
|
|
1571
|
+
onStatusChange: (status) => {
|
|
1572
|
+
this.updateState({
|
|
1573
|
+
isPrefetching: status.isPrefetching,
|
|
1574
|
+
downloadSpeed: status.downloadSpeed
|
|
1575
|
+
});
|
|
1576
|
+
},
|
|
1577
|
+
onSegmentParsed: (segmentIndex, itemCount) => {
|
|
1578
|
+
this.currentSegmentIndex = segmentIndex + 1;
|
|
1579
|
+
this.updateState({ segmentIndex: this.currentSegmentIndex });
|
|
1580
|
+
}
|
|
1581
|
+
},
|
|
1582
|
+
this
|
|
1583
|
+
);
|
|
1584
|
+
const baseUrl = this.currentPlaylistUrl.substring(0, this.currentPlaylistUrl.lastIndexOf("/") + 1);
|
|
1585
|
+
if (this.isFMP4) {
|
|
1586
|
+
const segmentInfos = this.fmp4Segments.map((seg) => ({
|
|
1587
|
+
uri: seg.uri,
|
|
1588
|
+
duration: seg.duration,
|
|
1589
|
+
byteRange: seg.byteRange
|
|
1590
|
+
}));
|
|
1591
|
+
this.prefetcher.setSegments(segmentInfos, baseUrl);
|
|
1592
|
+
} else {
|
|
1593
|
+
const segmentInfos = this.segments.map((seg) => ({
|
|
1594
|
+
uri: seg.uri,
|
|
1595
|
+
duration: seg.duration
|
|
1596
|
+
}));
|
|
1597
|
+
this.prefetcher.setSegments(segmentInfos, baseUrl);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
// ==================== 解析方法 ====================
|
|
1601
|
+
/**
|
|
1602
|
+
* 解析 fMP4 数据
|
|
1603
|
+
*/
|
|
1604
|
+
parseFMP4Data(data) {
|
|
1605
|
+
let moofOffset = -1;
|
|
1606
|
+
let mdatOffset = -1;
|
|
1607
|
+
let mdatSize = 0;
|
|
1608
|
+
let offset = 0;
|
|
1609
|
+
while (offset < data.length - 8) {
|
|
1610
|
+
const boxSize = this.readBoxSize(data, offset);
|
|
1611
|
+
const boxType = this.readBoxType(data, offset + 4);
|
|
1612
|
+
if (boxType === "moof") {
|
|
1613
|
+
moofOffset = offset;
|
|
1614
|
+
} else if (boxType === "mdat") {
|
|
1615
|
+
mdatOffset = offset;
|
|
1616
|
+
mdatSize = boxSize;
|
|
1617
|
+
break;
|
|
1618
|
+
}
|
|
1619
|
+
offset += boxSize;
|
|
1620
|
+
}
|
|
1621
|
+
let sampleCount = 0;
|
|
1622
|
+
if (moofOffset >= 0 && mdatOffset >= 0) {
|
|
1623
|
+
const moof = this.fmp4Demuxer.parseMoof(data, moofOffset);
|
|
1624
|
+
const mdatData = data.slice(mdatOffset + 8, mdatOffset + mdatSize);
|
|
1625
|
+
const samples = this.fmp4Demuxer.extractSamples(moof, mdatData, mdatOffset);
|
|
1626
|
+
for (const sample of samples) {
|
|
1627
|
+
this._sampleQueue.push({ sample });
|
|
1628
|
+
}
|
|
1629
|
+
sampleCount = samples.length;
|
|
1630
|
+
}
|
|
1631
|
+
return sampleCount;
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* 解析 TS 数据
|
|
1635
|
+
*/
|
|
1636
|
+
parseTSData(tsData) {
|
|
1637
|
+
const packets = this.tsDemuxer.parse(tsData);
|
|
1638
|
+
let nalCount = 0;
|
|
1639
|
+
if (packets.video.length > 0) {
|
|
1640
|
+
const videoPayload = this.pesExtractor.extractVideoPayload(packets.video);
|
|
1641
|
+
if (videoPayload.length > 0) {
|
|
1642
|
+
const nalUnits = this.nalParser.parse(videoPayload);
|
|
1643
|
+
if (!this.decoderInitialized) {
|
|
1644
|
+
this.initDecoderParamsSync(nalUnits);
|
|
1645
|
+
}
|
|
1646
|
+
for (const nalUnit of nalUnits) {
|
|
1647
|
+
this._nalQueue.push({ nalUnit, pts: 0 });
|
|
1648
|
+
}
|
|
1649
|
+
nalCount = nalUnits.length;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
return nalCount;
|
|
1653
|
+
}
|
|
1654
|
+
// ==================== 私有方法 ====================
|
|
1655
|
+
/**
|
|
1656
|
+
* 加载 fMP4 初始化段
|
|
1657
|
+
*/
|
|
1658
|
+
async loadInitSegment() {
|
|
1659
|
+
if (!this.initSegment) {
|
|
1660
|
+
console.error("[fMP4] No init segment defined!");
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
const baseUrl = this.currentPlaylistUrl.substring(0, this.currentPlaylistUrl.lastIndexOf("/") + 1);
|
|
1664
|
+
const url = this.initSegment.uri.startsWith("http") ? this.initSegment.uri : baseUrl + this.initSegment.uri;
|
|
1665
|
+
console.log("[fMP4] Loading init segment:", url, this.initSegment.byteRange);
|
|
1666
|
+
const headers = {};
|
|
1667
|
+
if (this.initSegment.byteRange) {
|
|
1668
|
+
const { start, end } = this.initSegment.byteRange;
|
|
1669
|
+
headers["Range"] = `bytes=${start}-${end}`;
|
|
1670
|
+
}
|
|
1671
|
+
const response = await fetch(url, { headers });
|
|
1672
|
+
const data = new Uint8Array(await response.arrayBuffer());
|
|
1673
|
+
console.log("[fMP4] Init segment data size:", data.length, "bytes");
|
|
1674
|
+
const initInfo = this.fmp4Demuxer.parseInitSegment(data);
|
|
1675
|
+
console.log("[fMP4] Parse result:", initInfo);
|
|
1676
|
+
if (initInfo?.avcC) {
|
|
1677
|
+
console.log("[fMP4] avcC size:", initInfo.avcC.length);
|
|
1678
|
+
this.initDecoderFromAvcC(initInfo.avcC);
|
|
1679
|
+
console.log("[fMP4] Init segment loaded, timescale:", initInfo.timescale);
|
|
1680
|
+
} else {
|
|
1681
|
+
console.error("[fMP4] Failed to parse init segment or no avcC found!");
|
|
1682
|
+
throw new Error("Failed to parse fMP4 init segment");
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* 从 avcC 初始化解码器
|
|
1687
|
+
*/
|
|
1688
|
+
initDecoderFromAvcC(avcC) {
|
|
1689
|
+
if (this.decoderInitialized || !this.decoder) return;
|
|
1690
|
+
let offset = 8;
|
|
1691
|
+
const configVersion = avcC[offset];
|
|
1692
|
+
const profile = avcC[offset + 1];
|
|
1693
|
+
avcC[offset + 2];
|
|
1694
|
+
const level = avcC[offset + 3];
|
|
1695
|
+
offset += 5;
|
|
1696
|
+
const spsCount = avcC[offset] & 31;
|
|
1697
|
+
offset += 1;
|
|
1698
|
+
console.log(`[fMP4] avcC: version=${configVersion}, profile=${profile}, level=${level}, spsCount=${spsCount}`);
|
|
1699
|
+
if (spsCount > 0) {
|
|
1700
|
+
const spsLength = avcC[offset] << 8 | avcC[offset + 1];
|
|
1701
|
+
offset += 2;
|
|
1702
|
+
const spsData = avcC.slice(offset, offset + spsLength);
|
|
1703
|
+
offset += spsLength;
|
|
1704
|
+
const spsWithStartCode = new Uint8Array(4 + spsLength);
|
|
1705
|
+
spsWithStartCode.set([0, 0, 0, 1], 0);
|
|
1706
|
+
spsWithStartCode.set(spsData, 4);
|
|
1707
|
+
this.decoder.decode({ type: 7, data: spsWithStartCode, size: spsWithStartCode.length });
|
|
1708
|
+
console.log(`[fMP4] SPS decoded, length=${spsLength}`);
|
|
1709
|
+
for (let i = 1; i < spsCount; i++) {
|
|
1710
|
+
const skipLength = avcC[offset] << 8 | avcC[offset + 1];
|
|
1711
|
+
offset += 2 + skipLength;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
const ppsCount = avcC[offset];
|
|
1715
|
+
offset += 1;
|
|
1716
|
+
if (ppsCount > 0) {
|
|
1717
|
+
const ppsLength = avcC[offset] << 8 | avcC[offset + 1];
|
|
1718
|
+
offset += 2;
|
|
1719
|
+
const ppsData = avcC.slice(offset, offset + ppsLength);
|
|
1720
|
+
const ppsWithStartCode = new Uint8Array(4 + ppsLength);
|
|
1721
|
+
ppsWithStartCode.set([0, 0, 0, 1], 0);
|
|
1722
|
+
ppsWithStartCode.set(ppsData, 4);
|
|
1723
|
+
this.decoder.decode({ type: 8, data: ppsWithStartCode, size: ppsWithStartCode.length });
|
|
1724
|
+
console.log(`[fMP4] PPS decoded, length=${ppsLength}`);
|
|
1725
|
+
}
|
|
1726
|
+
this.decoderInitialized = true;
|
|
1727
|
+
console.log("[fMP4] Decoder initialized from avcC");
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* 解析 HLS 播放列表
|
|
1731
|
+
*/
|
|
1732
|
+
async parsePlaylist(url) {
|
|
1733
|
+
const response = await fetch(url);
|
|
1734
|
+
const content = await response.text();
|
|
1735
|
+
const lines = content.split("\n");
|
|
1736
|
+
const segments = [];
|
|
1737
|
+
const fmp4Segments = [];
|
|
1738
|
+
const variants = [];
|
|
1739
|
+
let initSegment;
|
|
1740
|
+
const isMaster = lines.some((line) => line.includes("#EXT-X-STREAM-INF:"));
|
|
1741
|
+
if (isMaster) {
|
|
1742
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1743
|
+
const line = lines[i].trim();
|
|
1744
|
+
if (line.startsWith("#EXT-X-STREAM-INF:")) {
|
|
1745
|
+
const uri = lines[++i]?.trim();
|
|
1746
|
+
if (uri && !uri.startsWith("#")) {
|
|
1747
|
+
variants.push(uri);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
if (variants.length > 0) {
|
|
1752
|
+
const baseUrl = url.substring(0, url.lastIndexOf("/") + 1);
|
|
1753
|
+
const variantUrl = variants[0].startsWith("http") ? variants[0] : baseUrl + variants[0];
|
|
1754
|
+
this.currentPlaylistUrl = variantUrl;
|
|
1755
|
+
return this.parsePlaylist(variantUrl);
|
|
1756
|
+
}
|
|
1757
|
+
return { isMaster: true, isFMP4: false, segments: [], fmp4Segments: [], variants: variants.length };
|
|
1758
|
+
}
|
|
1759
|
+
const isFMP4 = lines.some((line) => line.includes("#EXT-X-MAP:"));
|
|
1760
|
+
console.log("[parsePlaylist] isFMP4:", isFMP4, "url:", url);
|
|
1761
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1762
|
+
const line = lines[i].trim();
|
|
1763
|
+
if (line.startsWith("#EXT-X-MAP:")) {
|
|
1764
|
+
const mapInfo = line.substring("#EXT-X-MAP:".length);
|
|
1765
|
+
const uriMatch = mapInfo.match(/URI="([^"]+)"/);
|
|
1766
|
+
if (uriMatch) {
|
|
1767
|
+
initSegment = { uri: uriMatch[1] };
|
|
1768
|
+
const byteRangeMatch = mapInfo.match(/BYTERANGE="(\d+)@(\d+)"/);
|
|
1769
|
+
if (byteRangeMatch) {
|
|
1770
|
+
initSegment.byteRange = {
|
|
1771
|
+
start: parseInt(byteRangeMatch[2]),
|
|
1772
|
+
end: parseInt(byteRangeMatch[2]) + parseInt(byteRangeMatch[1]) - 1
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1779
|
+
const line = lines[i].trim();
|
|
1780
|
+
if (line.startsWith("#EXTINF:")) {
|
|
1781
|
+
const duration = parseFloat(line.split(":")[1].split(",")[0]);
|
|
1782
|
+
let byteRange;
|
|
1783
|
+
let nextLine = lines[++i]?.trim();
|
|
1784
|
+
if (nextLine?.startsWith("#EXT-X-BYTERANGE:")) {
|
|
1785
|
+
const byteRangeMatch = nextLine.match(/#EXT-X-BYTERANGE:(\d+)@(\d+)/);
|
|
1786
|
+
if (byteRangeMatch) {
|
|
1787
|
+
byteRange = {
|
|
1788
|
+
start: parseInt(byteRangeMatch[2]),
|
|
1789
|
+
end: parseInt(byteRangeMatch[2]) + parseInt(byteRangeMatch[1]) - 1
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
nextLine = lines[++i]?.trim();
|
|
1793
|
+
}
|
|
1794
|
+
const uri = nextLine;
|
|
1795
|
+
if (uri && !uri.startsWith("#")) {
|
|
1796
|
+
if (isFMP4) {
|
|
1797
|
+
fmp4Segments.push({ uri, duration, byteRange });
|
|
1798
|
+
} else {
|
|
1799
|
+
segments.push({ uri, duration });
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
return { isMaster: false, isFMP4, segments, fmp4Segments, initSegment, variants: 1 };
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* 同步初始化解码器参数(SPS/PPS)- TS 流
|
|
1808
|
+
*/
|
|
1809
|
+
initDecoderParamsSync(nalUnits) {
|
|
1810
|
+
if (this.decoderInitialized || !this.decoder) return;
|
|
1811
|
+
const spsUnit = nalUnits.find((u) => u.type === 7);
|
|
1812
|
+
const ppsUnit = nalUnits.find((u) => u.type === 8);
|
|
1813
|
+
if (spsUnit) {
|
|
1814
|
+
const nalWithStartCode = new Uint8Array(spsUnit.size + 4);
|
|
1815
|
+
nalWithStartCode.set([0, 0, 0, 1], 0);
|
|
1816
|
+
nalWithStartCode.set(spsUnit.data, 4);
|
|
1817
|
+
this.decoder.decode({ type: 7, data: nalWithStartCode, size: nalWithStartCode.length });
|
|
1818
|
+
}
|
|
1819
|
+
if (ppsUnit) {
|
|
1820
|
+
const nalWithStartCode = new Uint8Array(ppsUnit.size + 4);
|
|
1821
|
+
nalWithStartCode.set([0, 0, 0, 1], 0);
|
|
1822
|
+
nalWithStartCode.set(ppsUnit.data, 4);
|
|
1823
|
+
this.decoder.decode({ type: 8, data: nalWithStartCode, size: nalWithStartCode.length });
|
|
1824
|
+
}
|
|
1825
|
+
if (spsUnit || ppsUnit) {
|
|
1826
|
+
this.decoderInitialized = true;
|
|
1827
|
+
console.log("[Decoder] Initialized with SPS/PPS");
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
readBoxSize(data, offset) {
|
|
1831
|
+
return data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3];
|
|
1832
|
+
}
|
|
1833
|
+
readBoxType(data, offset) {
|
|
1834
|
+
return String.fromCharCode(data[offset], data[offset + 1], data[offset + 2], data[offset + 3]);
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* 解码 fMP4 sample
|
|
1838
|
+
*/
|
|
1839
|
+
decodeSample(sample) {
|
|
1840
|
+
if (!this.decoder) {
|
|
1841
|
+
console.warn("[fMP4] Decoder not available");
|
|
1842
|
+
return null;
|
|
1843
|
+
}
|
|
1844
|
+
if (!this.decoderInitialized) {
|
|
1845
|
+
console.warn("[fMP4] Decoder not initialized, skipping sample");
|
|
1846
|
+
return null;
|
|
1847
|
+
}
|
|
1848
|
+
const data = sample.data;
|
|
1849
|
+
const hasStartCode = data.length >= 4 && data[0] === 0 && data[1] === 0 && (data[2] === 1 || data[2] === 0 && data[3] === 1);
|
|
1850
|
+
if (hasStartCode) {
|
|
1851
|
+
return this.decoder.decode({
|
|
1852
|
+
type: sample.isSync ? 5 : 1,
|
|
1853
|
+
data,
|
|
1854
|
+
size: data.length
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
let nalCount = 0;
|
|
1858
|
+
let totalSize = 0;
|
|
1859
|
+
let offset = 0;
|
|
1860
|
+
while (offset + 4 <= data.length) {
|
|
1861
|
+
const nalLength = data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3];
|
|
1862
|
+
if (nalLength <= 0 || offset + 4 + nalLength > data.length) {
|
|
1863
|
+
console.warn(`[fMP4] Invalid NAL length: ${nalLength} at offset ${offset}`);
|
|
1864
|
+
break;
|
|
1865
|
+
}
|
|
1866
|
+
totalSize += 4 + nalLength;
|
|
1867
|
+
offset += 4 + nalLength;
|
|
1868
|
+
nalCount++;
|
|
1869
|
+
}
|
|
1870
|
+
if (nalCount === 0) {
|
|
1871
|
+
console.warn("[fMP4] No valid NAL units found in sample");
|
|
1872
|
+
return null;
|
|
1873
|
+
}
|
|
1874
|
+
const annexBData = new Uint8Array(totalSize);
|
|
1875
|
+
let writeOffset = 0;
|
|
1876
|
+
offset = 0;
|
|
1877
|
+
while (offset + 4 <= data.length) {
|
|
1878
|
+
const nalLength = data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3];
|
|
1879
|
+
if (nalLength <= 0 || offset + 4 + nalLength > data.length) {
|
|
1880
|
+
break;
|
|
1881
|
+
}
|
|
1882
|
+
annexBData[writeOffset] = 0;
|
|
1883
|
+
annexBData[writeOffset + 1] = 0;
|
|
1884
|
+
annexBData[writeOffset + 2] = 0;
|
|
1885
|
+
annexBData[writeOffset + 3] = 1;
|
|
1886
|
+
writeOffset += 4;
|
|
1887
|
+
annexBData.set(data.slice(offset + 4, offset + 4 + nalLength), writeOffset);
|
|
1888
|
+
writeOffset += nalLength;
|
|
1889
|
+
offset += 4 + nalLength;
|
|
1890
|
+
}
|
|
1891
|
+
const frame = this.decoder.decode({
|
|
1892
|
+
type: sample.isSync ? 5 : 1,
|
|
1893
|
+
data: annexBData,
|
|
1894
|
+
size: annexBData.length
|
|
1895
|
+
});
|
|
1896
|
+
return frame;
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* 解码单个 NAL 单元
|
|
1900
|
+
*/
|
|
1901
|
+
decodeNAL(nalUnit) {
|
|
1902
|
+
if (!this.decoder) return null;
|
|
1903
|
+
const nalWithStartCode = new Uint8Array(nalUnit.size + 4);
|
|
1904
|
+
nalWithStartCode.set([0, 0, 0, 1], 0);
|
|
1905
|
+
nalWithStartCode.set(nalUnit.data, 4);
|
|
1906
|
+
return this.decoder.decode({
|
|
1907
|
+
type: nalUnit.type,
|
|
1908
|
+
data: nalWithStartCode,
|
|
1909
|
+
size: nalWithStartCode.length
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
const FLV_SIGNATURE = [70, 76, 86];
|
|
1914
|
+
const FLV_VERSION = 1;
|
|
1915
|
+
const TAG_TYPE_VIDEO = 9;
|
|
1916
|
+
const AVC_PACKET_SEQUENCE_HEADER = 0;
|
|
1917
|
+
const AVC_PACKET_NALU = 1;
|
|
1918
|
+
const CODEC_ID_AVC$1 = 7;
|
|
1919
|
+
const CODEC_ID_HEVC$1 = 12;
|
|
1920
|
+
const FRAME_TYPE_KEYFRAME = 1;
|
|
1921
|
+
function readUint24(data, offset) {
|
|
1922
|
+
return data[offset] << 16 | data[offset + 1] << 8 | data[offset + 2];
|
|
1923
|
+
}
|
|
1924
|
+
function readUint32(data, offset) {
|
|
1925
|
+
return (data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3]) >>> 0;
|
|
1926
|
+
}
|
|
1927
|
+
function readInt24(data, offset) {
|
|
1928
|
+
const value = data[offset] << 16 | data[offset + 1] << 8 | data[offset + 2];
|
|
1929
|
+
return value > 8388607 ? value - 16777216 : value;
|
|
1930
|
+
}
|
|
1931
|
+
class FLVDemuxer {
|
|
1932
|
+
constructor() {
|
|
1933
|
+
this.avcConfig = null;
|
|
1934
|
+
this.hevcConfig = null;
|
|
1935
|
+
this.codecId = CODEC_ID_AVC$1;
|
|
1936
|
+
this.parseOffset = 0;
|
|
1937
|
+
this.headerParsed = false;
|
|
1938
|
+
this.headerSize = 9;
|
|
1939
|
+
}
|
|
1940
|
+
// FLV header 大小
|
|
1941
|
+
/**
|
|
1942
|
+
* 检测是否为 FLV 格式
|
|
1943
|
+
*/
|
|
1944
|
+
static isFLV(data) {
|
|
1945
|
+
if (data.length < 13) return false;
|
|
1946
|
+
if (data[0] !== FLV_SIGNATURE[0] || data[1] !== FLV_SIGNATURE[1] || data[2] !== FLV_SIGNATURE[2]) {
|
|
1947
|
+
return false;
|
|
1948
|
+
}
|
|
1949
|
+
if (data[3] !== FLV_VERSION) {
|
|
1950
|
+
return false;
|
|
1951
|
+
}
|
|
1952
|
+
return true;
|
|
1953
|
+
}
|
|
1954
|
+
/**
|
|
1955
|
+
* 解析 FLV 数据
|
|
1956
|
+
*/
|
|
1957
|
+
parse(data) {
|
|
1958
|
+
const videoTags = [];
|
|
1959
|
+
let duration = 0;
|
|
1960
|
+
if (!FLVDemuxer.isFLV(data)) {
|
|
1961
|
+
console.error("[FLVDemuxer] Invalid FLV format");
|
|
1962
|
+
return { videoTags, avcConfig: null, hevcConfig: null, duration: 0 };
|
|
1963
|
+
}
|
|
1964
|
+
const headerSize = readUint32(data, 5);
|
|
1965
|
+
let offset = headerSize + 4;
|
|
1966
|
+
while (offset < data.length - 15) {
|
|
1967
|
+
const tagType = data[offset];
|
|
1968
|
+
const dataSize = readUint24(data, offset + 1);
|
|
1969
|
+
const timestamp = readUint24(data, offset + 4) | data[offset + 7] << 24;
|
|
1970
|
+
offset += 11;
|
|
1971
|
+
if (offset + dataSize > data.length) {
|
|
1972
|
+
console.warn("[FLVDemuxer] Incomplete tag data, stopping");
|
|
1973
|
+
break;
|
|
1974
|
+
}
|
|
1975
|
+
const tagData = data.slice(offset, offset + dataSize);
|
|
1976
|
+
if (tagType === TAG_TYPE_VIDEO) {
|
|
1977
|
+
const videoTag = this.parseVideoTag(tagData, timestamp);
|
|
1978
|
+
if (videoTag) {
|
|
1979
|
+
videoTags.push(videoTag);
|
|
1980
|
+
if (videoTag.timestamp > duration) {
|
|
1981
|
+
duration = videoTag.timestamp;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
offset += dataSize;
|
|
1986
|
+
offset += 4;
|
|
1987
|
+
}
|
|
1988
|
+
return {
|
|
1989
|
+
videoTags,
|
|
1990
|
+
avcConfig: this.avcConfig,
|
|
1991
|
+
hevcConfig: this.hevcConfig,
|
|
1992
|
+
duration
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* 增量解析 FLV 数据(用于流式下载)
|
|
1997
|
+
* 只解析新收到的数据,避免重复解析
|
|
1998
|
+
* @param data 完整的 FLV 数据(包含已解析和未解析的部分)
|
|
1999
|
+
* @returns 新解析的视频标签
|
|
2000
|
+
*/
|
|
2001
|
+
parseIncremental(data) {
|
|
2002
|
+
const videoTags = [];
|
|
2003
|
+
let duration = 0;
|
|
2004
|
+
if (!this.headerParsed) {
|
|
2005
|
+
if (!FLVDemuxer.isFLV(data)) {
|
|
2006
|
+
console.error("[FLVDemuxer] Invalid FLV format");
|
|
2007
|
+
return { videoTags: [], avcConfig: null, hevcConfig: null, duration: 0 };
|
|
2008
|
+
}
|
|
2009
|
+
this.headerSize = readUint32(data, 5);
|
|
2010
|
+
this.parseOffset = this.headerSize + 4;
|
|
2011
|
+
this.headerParsed = true;
|
|
2012
|
+
}
|
|
2013
|
+
let offset = this.parseOffset;
|
|
2014
|
+
while (offset < data.length - 15) {
|
|
2015
|
+
const tagType = data[offset];
|
|
2016
|
+
const dataSize = readUint24(data, offset + 1);
|
|
2017
|
+
const timestamp = readUint24(data, offset + 4) | data[offset + 7] << 24;
|
|
2018
|
+
offset += 11;
|
|
2019
|
+
if (offset + dataSize > data.length) {
|
|
2020
|
+
break;
|
|
2021
|
+
}
|
|
2022
|
+
const tagData = data.slice(offset, offset + dataSize);
|
|
2023
|
+
if (tagType === TAG_TYPE_VIDEO) {
|
|
2024
|
+
const videoTag = this.parseVideoTag(tagData, timestamp);
|
|
2025
|
+
if (videoTag) {
|
|
2026
|
+
videoTags.push(videoTag);
|
|
2027
|
+
if (videoTag.timestamp > duration) {
|
|
2028
|
+
duration = videoTag.timestamp;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
offset += dataSize;
|
|
2033
|
+
offset += 4;
|
|
2034
|
+
this.parseOffset = offset;
|
|
2035
|
+
}
|
|
2036
|
+
return {
|
|
2037
|
+
videoTags,
|
|
2038
|
+
avcConfig: this.avcConfig,
|
|
2039
|
+
hevcConfig: this.hevcConfig,
|
|
2040
|
+
duration
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
/**
|
|
2044
|
+
* 重置增量解析状态(用于重新加载新的 FLV)
|
|
2045
|
+
*/
|
|
2046
|
+
resetIncremental() {
|
|
2047
|
+
this.parseOffset = 0;
|
|
2048
|
+
this.headerParsed = false;
|
|
2049
|
+
this.avcConfig = null;
|
|
2050
|
+
this.hevcConfig = null;
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* 同步增量解析位置(用于从已解析的数据继续)
|
|
2054
|
+
* @param offset 已经解析到的字节位置
|
|
2055
|
+
*/
|
|
2056
|
+
syncParseOffset(offset) {
|
|
2057
|
+
this.headerParsed = true;
|
|
2058
|
+
this.parseOffset = offset;
|
|
2059
|
+
}
|
|
2060
|
+
/**
|
|
2061
|
+
* 解析 Video Tag
|
|
2062
|
+
*
|
|
2063
|
+
* Video Tag Data 格式 (AVC/HEVC):
|
|
2064
|
+
* byte 0: Frame Type (4 bits) + Codec ID (4 bits)
|
|
2065
|
+
* byte 1: AVC/HEVC Packet Type
|
|
2066
|
+
* byte 2-4: Composition Time Offset (24 bits signed)
|
|
2067
|
+
* byte 5+: AVC/HEVC Data
|
|
2068
|
+
*/
|
|
2069
|
+
parseVideoTag(data, timestamp) {
|
|
2070
|
+
if (data.length < 5) {
|
|
2071
|
+
return null;
|
|
2072
|
+
}
|
|
2073
|
+
const frameType = data[0] >> 4 & 15;
|
|
2074
|
+
const codecId = data[0] & 15;
|
|
2075
|
+
const avcPacketType = data[1];
|
|
2076
|
+
const compositionTimeOffset = readInt24(data, 2);
|
|
2077
|
+
if (codecId !== CODEC_ID_AVC$1 && codecId !== CODEC_ID_HEVC$1) {
|
|
2078
|
+
console.warn("[FLVDemuxer] Unsupported codec ID:", codecId);
|
|
2079
|
+
return null;
|
|
2080
|
+
}
|
|
2081
|
+
this.codecId = codecId;
|
|
2082
|
+
const avcData = data.slice(5);
|
|
2083
|
+
if (avcPacketType === AVC_PACKET_SEQUENCE_HEADER) {
|
|
2084
|
+
if (codecId === CODEC_ID_HEVC$1) {
|
|
2085
|
+
this.hevcConfig = this.parseHEVCSequenceHeader(avcData);
|
|
2086
|
+
console.log("[FLVDemuxer] Parsed HEVC Sequence Header:", {
|
|
2087
|
+
vpsCount: this.hevcConfig?.vpsList.length,
|
|
2088
|
+
spsCount: this.hevcConfig?.spsList.length,
|
|
2089
|
+
ppsCount: this.hevcConfig?.ppsList.length
|
|
2090
|
+
});
|
|
2091
|
+
} else {
|
|
2092
|
+
this.avcConfig = this.parseSequenceHeader(avcData);
|
|
2093
|
+
console.log("[FLVDemuxer] Parsed AVC Sequence Header:", {
|
|
2094
|
+
profile: this.avcConfig?.avcProfileIndication,
|
|
2095
|
+
level: this.avcConfig?.avcLevelIndication,
|
|
2096
|
+
spsCount: this.avcConfig?.spsList.length,
|
|
2097
|
+
ppsCount: this.avcConfig?.ppsList.length
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
return null;
|
|
2101
|
+
}
|
|
2102
|
+
if (avcPacketType === AVC_PACKET_NALU) {
|
|
2103
|
+
return {
|
|
2104
|
+
timestamp,
|
|
2105
|
+
frameType,
|
|
2106
|
+
codecId,
|
|
2107
|
+
avcPacketType,
|
|
2108
|
+
compositionTimeOffset,
|
|
2109
|
+
data: avcData
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
return null;
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* 解析 AVC Sequence Header (AVCDecoderConfigurationRecord)
|
|
2116
|
+
*
|
|
2117
|
+
* 结构 (ISO 14496-15):
|
|
2118
|
+
* byte 0: configurationVersion (usually 1)
|
|
2119
|
+
* byte 1: AVCProfileIndication
|
|
2120
|
+
* byte 2: profile_compatibility
|
|
2121
|
+
* byte 3: AVCLevelIndication
|
|
2122
|
+
* byte 4: lengthSizeMinusOne (高6位保留=0x3F, 低2位=NALU长度字节数-1)
|
|
2123
|
+
* byte 5: numOfSequenceParameterSets (高3位保留=0xE0, 低5位=SPS数量)
|
|
2124
|
+
* SPS 列表:
|
|
2125
|
+
* - 2 bytes: SPS length
|
|
2126
|
+
* - n bytes: SPS data
|
|
2127
|
+
* byte: numOfPictureParameterSets
|
|
2128
|
+
* PPS 列表:
|
|
2129
|
+
* - 2 bytes: PPS length
|
|
2130
|
+
* - n bytes: PPS data
|
|
2131
|
+
*/
|
|
2132
|
+
parseSequenceHeader(data) {
|
|
2133
|
+
if (data.length < 7) {
|
|
2134
|
+
console.error("[FLVDemuxer] Sequence Header too short");
|
|
2135
|
+
return null;
|
|
2136
|
+
}
|
|
2137
|
+
let offset = 0;
|
|
2138
|
+
const configurationVersion = data[offset++];
|
|
2139
|
+
const avcProfileIndication = data[offset++];
|
|
2140
|
+
const profileCompatibility = data[offset++];
|
|
2141
|
+
const avcLevelIndication = data[offset++];
|
|
2142
|
+
const lengthSizeMinusOne = data[offset++] & 3;
|
|
2143
|
+
const spsCount = data[offset++] & 31;
|
|
2144
|
+
const spsList = [];
|
|
2145
|
+
for (let i = 0; i < spsCount; i++) {
|
|
2146
|
+
if (offset + 2 > data.length) {
|
|
2147
|
+
console.error("[FLVDemuxer] Incomplete SPS length");
|
|
2148
|
+
return null;
|
|
2149
|
+
}
|
|
2150
|
+
const spsLength = data[offset] << 8 | data[offset + 1];
|
|
2151
|
+
offset += 2;
|
|
2152
|
+
if (offset + spsLength > data.length) {
|
|
2153
|
+
console.error("[FLVDemuxer] Incomplete SPS data");
|
|
2154
|
+
return null;
|
|
2155
|
+
}
|
|
2156
|
+
spsList.push(data.slice(offset, offset + spsLength));
|
|
2157
|
+
offset += spsLength;
|
|
2158
|
+
}
|
|
2159
|
+
if (offset >= data.length) {
|
|
2160
|
+
console.error("[FLVDemuxer] Missing PPS count");
|
|
2161
|
+
return null;
|
|
2162
|
+
}
|
|
2163
|
+
const ppsCount = data[offset++];
|
|
2164
|
+
const ppsList = [];
|
|
2165
|
+
for (let i = 0; i < ppsCount; i++) {
|
|
2166
|
+
if (offset + 2 > data.length) {
|
|
2167
|
+
console.error("[FLVDemuxer] Incomplete PPS length");
|
|
2168
|
+
return null;
|
|
2169
|
+
}
|
|
2170
|
+
const ppsLength = data[offset] << 8 | data[offset + 1];
|
|
2171
|
+
offset += 2;
|
|
2172
|
+
if (offset + ppsLength > data.length) {
|
|
2173
|
+
console.error("[FLVDemuxer] Incomplete PPS data");
|
|
2174
|
+
return null;
|
|
2175
|
+
}
|
|
2176
|
+
ppsList.push(data.slice(offset, offset + ppsLength));
|
|
2177
|
+
offset += ppsLength;
|
|
2178
|
+
}
|
|
2179
|
+
return {
|
|
2180
|
+
configurationVersion,
|
|
2181
|
+
avcProfileIndication,
|
|
2182
|
+
profileCompatibility,
|
|
2183
|
+
avcLevelIndication,
|
|
2184
|
+
lengthSizeMinusOne,
|
|
2185
|
+
spsList,
|
|
2186
|
+
ppsList
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
/**
|
|
2190
|
+
* 解析 HEVC Sequence Header (HEVCDecoderConfigurationRecord)
|
|
2191
|
+
*
|
|
2192
|
+
* 结构 (ISO 14496-15):
|
|
2193
|
+
* byte 0: configurationVersion (usually 1)
|
|
2194
|
+
* byte 1: general_profile_space (2 bits) + general_tier_flag (1 bit) + general_profile_idc (5 bits)
|
|
2195
|
+
* byte 2-5: general_profile_compatibility_flags (32 bits)
|
|
2196
|
+
* byte 6-11: general_constraint_indicator_flags (48 bits)
|
|
2197
|
+
* byte 12: general_level_idc
|
|
2198
|
+
* byte 13-14: min_spatial_segmentation_idc (高4位保留)
|
|
2199
|
+
* byte 15: parallelismType (高6位保留)
|
|
2200
|
+
* byte 16: chromaFormat (高6位保留)
|
|
2201
|
+
* byte 17: bitDepthLumaMinus8 (高5位保留)
|
|
2202
|
+
* byte 18: bitDepthChromaMinus8 (高5位保留)
|
|
2203
|
+
* byte 19-20: avgFrameRate
|
|
2204
|
+
* byte 21: constantFrameRate (2 bits) + numTemporalLayers (3 bits) + temporalIdNested (1 bit) + lengthSizeMinusOne (2 bits)
|
|
2205
|
+
* byte 22: numOfArrays (高3位保留)
|
|
2206
|
+
* Arrays:
|
|
2207
|
+
* byte 0: array_completeness (1 bit) + reserved (1 bit) + NAL_unit_type (6 bits)
|
|
2208
|
+
* byte 1-2: numNalus
|
|
2209
|
+
* NALUs:
|
|
2210
|
+
* byte 0-1: nalUnitLength
|
|
2211
|
+
* byte n: nalUnit data
|
|
2212
|
+
*/
|
|
2213
|
+
parseHEVCSequenceHeader(data) {
|
|
2214
|
+
if (data.length < 23) {
|
|
2215
|
+
console.error("[FLVDemuxer] HEVC Sequence Header too short");
|
|
2216
|
+
return null;
|
|
2217
|
+
}
|
|
2218
|
+
let offset = 0;
|
|
2219
|
+
const configurationVersion = data[offset++];
|
|
2220
|
+
if (configurationVersion !== 1) {
|
|
2221
|
+
console.warn("[FLVDemuxer] Unknown HEVC configuration version:", configurationVersion);
|
|
2222
|
+
}
|
|
2223
|
+
const byte1 = data[offset++];
|
|
2224
|
+
const general_profile_space = byte1 >> 6 & 3;
|
|
2225
|
+
const general_tier_flag = byte1 >> 5 & 1;
|
|
2226
|
+
const general_profile_idc = byte1 & 31;
|
|
2227
|
+
const general_profile_compatibility_flags = data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3];
|
|
2228
|
+
offset += 4;
|
|
2229
|
+
const general_constraint_indicator_flags = BigInt(data[offset]) << 40n | BigInt(data[offset + 1]) << 32n | BigInt(data[offset + 2]) << 24n | BigInt(data[offset + 3]) << 16n | BigInt(data[offset + 4]) << 8n | BigInt(data[offset + 5]);
|
|
2230
|
+
offset += 6;
|
|
2231
|
+
const general_level_idc = data[offset++];
|
|
2232
|
+
offset += 2;
|
|
2233
|
+
offset += 1;
|
|
2234
|
+
offset += 1;
|
|
2235
|
+
offset += 1;
|
|
2236
|
+
offset += 1;
|
|
2237
|
+
offset += 2;
|
|
2238
|
+
const byte21 = data[offset++];
|
|
2239
|
+
const lengthSizeMinusOne = byte21 & 3;
|
|
2240
|
+
const numOfArrays = data[offset++];
|
|
2241
|
+
const vpsList = [];
|
|
2242
|
+
const spsList = [];
|
|
2243
|
+
const ppsList = [];
|
|
2244
|
+
for (let i = 0; i < numOfArrays; i++) {
|
|
2245
|
+
if (offset >= data.length) {
|
|
2246
|
+
console.error("[FLVDemuxer] Incomplete HEVC array header");
|
|
2247
|
+
break;
|
|
2248
|
+
}
|
|
2249
|
+
const nalUnitType = data[offset++] & 63;
|
|
2250
|
+
if (offset + 2 > data.length) {
|
|
2251
|
+
console.error("[FLVDemuxer] Incomplete numNalus");
|
|
2252
|
+
break;
|
|
2253
|
+
}
|
|
2254
|
+
const numNalus = data[offset] << 8 | data[offset + 1];
|
|
2255
|
+
offset += 2;
|
|
2256
|
+
for (let j = 0; j < numNalus; j++) {
|
|
2257
|
+
if (offset + 2 > data.length) {
|
|
2258
|
+
console.error("[FLVDemuxer] Incomplete NAL unit length");
|
|
2259
|
+
break;
|
|
2260
|
+
}
|
|
2261
|
+
const nalUnitLength = data[offset] << 8 | data[offset + 1];
|
|
2262
|
+
offset += 2;
|
|
2263
|
+
if (offset + nalUnitLength > data.length) {
|
|
2264
|
+
console.error("[FLVDemuxer] Incomplete NAL unit data");
|
|
2265
|
+
break;
|
|
2266
|
+
}
|
|
2267
|
+
const nalData = data.slice(offset, offset + nalUnitLength);
|
|
2268
|
+
offset += nalUnitLength;
|
|
2269
|
+
if (nalUnitType === 32) {
|
|
2270
|
+
vpsList.push(nalData);
|
|
2271
|
+
} else if (nalUnitType === 33) {
|
|
2272
|
+
spsList.push(nalData);
|
|
2273
|
+
} else if (nalUnitType === 34) {
|
|
2274
|
+
ppsList.push(nalData);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
return {
|
|
2279
|
+
configurationVersion,
|
|
2280
|
+
general_profile_space,
|
|
2281
|
+
general_tier_flag,
|
|
2282
|
+
general_profile_idc,
|
|
2283
|
+
general_profile_compatibility_flags,
|
|
2284
|
+
general_constraint_indicator_flags,
|
|
2285
|
+
general_level_idc,
|
|
2286
|
+
lengthSizeMinusOne,
|
|
2287
|
+
vpsList,
|
|
2288
|
+
spsList,
|
|
2289
|
+
ppsList
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
/**
|
|
2293
|
+
* 获取 AVC 配置
|
|
2294
|
+
*/
|
|
2295
|
+
getAvcConfig() {
|
|
2296
|
+
return this.avcConfig;
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* 获取 HEVC 配置
|
|
2300
|
+
*/
|
|
2301
|
+
getHevcConfig() {
|
|
2302
|
+
return this.hevcConfig;
|
|
2303
|
+
}
|
|
2304
|
+
/**
|
|
2305
|
+
* 获取当前编码类型
|
|
2306
|
+
*/
|
|
2307
|
+
getCodecId() {
|
|
2308
|
+
return this.codecId;
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* 将 Video Tag 转换为 Annex B 格式的数据
|
|
2312
|
+
*
|
|
2313
|
+
* FLV 中的 AVC 数据使用 AVCC 格式 (长度前缀),
|
|
2314
|
+
* 需要转换为 Annex B 格式 (start code) 才能被解码器处理
|
|
2315
|
+
*
|
|
2316
|
+
* @param tag FLV Video Tag
|
|
2317
|
+
* @param lengthSize NALU 长度字节数 (从 Sequence Header 获取, 通常为 4)
|
|
2318
|
+
* @returns Annex B 格式的数据
|
|
2319
|
+
*/
|
|
2320
|
+
videoTagToAnnexB(tag, lengthSize = 4) {
|
|
2321
|
+
const data = tag.data;
|
|
2322
|
+
if (data.length === 0) return null;
|
|
2323
|
+
let nalCount = 0;
|
|
2324
|
+
let totalSize = 0;
|
|
2325
|
+
let offset = 0;
|
|
2326
|
+
while (offset + lengthSize <= data.length) {
|
|
2327
|
+
let nalLength = 0;
|
|
2328
|
+
for (let i = 0; i < lengthSize; i++) {
|
|
2329
|
+
nalLength = nalLength << 8 | data[offset + i];
|
|
2330
|
+
}
|
|
2331
|
+
offset += lengthSize;
|
|
2332
|
+
if (nalLength <= 0 || offset + nalLength > data.length) {
|
|
2333
|
+
console.warn(`[FLVDemuxer] Invalid NAL length: ${nalLength} at offset ${offset - lengthSize}`);
|
|
2334
|
+
break;
|
|
2335
|
+
}
|
|
2336
|
+
totalSize += 4 + nalLength;
|
|
2337
|
+
offset += nalLength;
|
|
2338
|
+
nalCount++;
|
|
2339
|
+
}
|
|
2340
|
+
if (nalCount === 0) {
|
|
2341
|
+
console.warn("[FLVDemuxer] No valid NAL units found");
|
|
2342
|
+
return null;
|
|
2343
|
+
}
|
|
2344
|
+
const annexBData = new Uint8Array(totalSize);
|
|
2345
|
+
let writeOffset = 0;
|
|
2346
|
+
offset = 0;
|
|
2347
|
+
while (offset + lengthSize <= data.length) {
|
|
2348
|
+
let nalLength = 0;
|
|
2349
|
+
for (let i = 0; i < lengthSize; i++) {
|
|
2350
|
+
nalLength = nalLength << 8 | data[offset + i];
|
|
2351
|
+
}
|
|
2352
|
+
offset += lengthSize;
|
|
2353
|
+
if (nalLength <= 0 || offset + nalLength > data.length) {
|
|
2354
|
+
break;
|
|
2355
|
+
}
|
|
2356
|
+
annexBData[writeOffset++] = 0;
|
|
2357
|
+
annexBData[writeOffset++] = 0;
|
|
2358
|
+
annexBData[writeOffset++] = 0;
|
|
2359
|
+
annexBData[writeOffset++] = 1;
|
|
2360
|
+
annexBData.set(data.slice(offset, offset + nalLength), writeOffset);
|
|
2361
|
+
writeOffset += nalLength;
|
|
2362
|
+
offset += nalLength;
|
|
2363
|
+
}
|
|
2364
|
+
return annexBData;
|
|
2365
|
+
}
|
|
2366
|
+
/**
|
|
2367
|
+
* 判断 Video Tag 是否为关键帧
|
|
2368
|
+
*/
|
|
2369
|
+
isKeyframe(tag) {
|
|
2370
|
+
return tag.frameType === FRAME_TYPE_KEYFRAME;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
class HEVCDecoder {
|
|
2374
|
+
constructor(wasmLoader) {
|
|
2375
|
+
this.wasmModule = null;
|
|
2376
|
+
this.decoderContext = null;
|
|
2377
|
+
this.wasmModule = wasmLoader.getModule();
|
|
2378
|
+
}
|
|
2379
|
+
async init() {
|
|
2380
|
+
if (!this.wasmModule) {
|
|
2381
|
+
throw new Error("WASM module not loaded");
|
|
2382
|
+
}
|
|
2383
|
+
this.decoderContext = this.wasmModule._create_decoder(173);
|
|
2384
|
+
if (this.decoderContext === 0 || this.decoderContext === null) {
|
|
2385
|
+
throw new Error("Failed to create HEVC decoder context");
|
|
2386
|
+
}
|
|
2387
|
+
console.log("[HEVCDecoder] Initialized with codec_id=173");
|
|
2388
|
+
}
|
|
2389
|
+
decode(nalUnit) {
|
|
2390
|
+
if (this.decoderContext === null || !this.wasmModule) {
|
|
2391
|
+
return null;
|
|
2392
|
+
}
|
|
2393
|
+
const dataPtr = this.wasmModule._malloc(nalUnit.size);
|
|
2394
|
+
this.wasmModule.HEAPU8.set(nalUnit.data, dataPtr);
|
|
2395
|
+
const result = this.wasmModule._decode_video(
|
|
2396
|
+
this.decoderContext,
|
|
2397
|
+
dataPtr,
|
|
2398
|
+
nalUnit.size
|
|
2399
|
+
);
|
|
2400
|
+
this.wasmModule._free(dataPtr);
|
|
2401
|
+
if (result === 1) {
|
|
2402
|
+
const width = this.wasmModule._get_frame_width(this.decoderContext);
|
|
2403
|
+
const height = this.wasmModule._get_frame_height(this.decoderContext);
|
|
2404
|
+
if (width <= 0 || height <= 0) {
|
|
2405
|
+
return null;
|
|
2406
|
+
}
|
|
2407
|
+
const yPtr = this.wasmModule._get_frame_data(this.decoderContext, 0);
|
|
2408
|
+
const uPtr = this.wasmModule._get_frame_data(this.decoderContext, 1);
|
|
2409
|
+
const vPtr = this.wasmModule._get_frame_data(this.decoderContext, 2);
|
|
2410
|
+
const yLineSize = this.wasmModule._get_frame_linesize(this.decoderContext, 0);
|
|
2411
|
+
const uLineSize = this.wasmModule._get_frame_linesize(this.decoderContext, 1);
|
|
2412
|
+
const vLineSize = this.wasmModule._get_frame_linesize(this.decoderContext, 2);
|
|
2413
|
+
const frameData = this.yuv420pToRgba(
|
|
2414
|
+
yPtr,
|
|
2415
|
+
uPtr,
|
|
2416
|
+
vPtr,
|
|
2417
|
+
width,
|
|
2418
|
+
height,
|
|
2419
|
+
yLineSize,
|
|
2420
|
+
uLineSize,
|
|
2421
|
+
vLineSize
|
|
2422
|
+
);
|
|
2423
|
+
return { width, height, data: frameData };
|
|
2424
|
+
}
|
|
2425
|
+
return null;
|
|
2426
|
+
}
|
|
2427
|
+
yuv420pToRgba(yPtr, uPtr, vPtr, width, height, yLineSize, uLineSize, vLineSize) {
|
|
2428
|
+
const rgba = new Uint8Array(width * height * 4);
|
|
2429
|
+
for (let y = 0; y < height; y++) {
|
|
2430
|
+
for (let x = 0; x < width; x++) {
|
|
2431
|
+
const yIndex = y * yLineSize + x;
|
|
2432
|
+
const uIndex = (y >> 1) * uLineSize + (x >> 1);
|
|
2433
|
+
const vIndex = (y >> 1) * vLineSize + (x >> 1);
|
|
2434
|
+
const yValue = this.wasmModule.HEAPU8[yPtr + yIndex];
|
|
2435
|
+
const uValue = this.wasmModule.HEAPU8[uPtr + uIndex];
|
|
2436
|
+
const vValue = this.wasmModule.HEAPU8[vPtr + vIndex];
|
|
2437
|
+
const c = yValue - 16;
|
|
2438
|
+
const d = uValue - 128;
|
|
2439
|
+
const e = vValue - 128;
|
|
2440
|
+
let r = 298 * c + 409 * e + 128 >> 8;
|
|
2441
|
+
let g = 298 * c - 100 * d - 208 * e + 128 >> 8;
|
|
2442
|
+
let b = 298 * c + 516 * d + 128 >> 8;
|
|
2443
|
+
r = r < 0 ? 0 : r > 255 ? 255 : r;
|
|
2444
|
+
g = g < 0 ? 0 : g > 255 ? 255 : g;
|
|
2445
|
+
b = b < 0 ? 0 : b > 255 ? 255 : b;
|
|
2446
|
+
const rgbaIndex = (y * width + x) * 4;
|
|
2447
|
+
rgba[rgbaIndex] = r;
|
|
2448
|
+
rgba[rgbaIndex + 1] = g;
|
|
2449
|
+
rgba[rgbaIndex + 2] = b;
|
|
2450
|
+
rgba[rgbaIndex + 3] = 255;
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
return rgba;
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
const CODEC_ID_AVC = 7;
|
|
2457
|
+
const CODEC_ID_HEVC = 12;
|
|
2458
|
+
const FLV_PREFETCHER_CONFIG = {
|
|
2459
|
+
...DEFAULT_STREAM_PREFETCHER_CONFIG,
|
|
2460
|
+
lowWaterMark: 100,
|
|
2461
|
+
highWaterMark: 500,
|
|
2462
|
+
initialBufferSize: 2 * 1024 * 1024,
|
|
2463
|
+
// 2MB
|
|
2464
|
+
prefetchInterval: 10,
|
|
2465
|
+
dynamicInterval: true
|
|
2466
|
+
};
|
|
2467
|
+
const DEFAULT_FLV_CONFIG = {
|
|
2468
|
+
...DEFAULT_PLAYER_CONFIG
|
|
2469
|
+
};
|
|
2470
|
+
class FLVStreamPrefetcher extends StreamPrefetcher {
|
|
2471
|
+
constructor(config, callbacks, player) {
|
|
2472
|
+
super(config, callbacks);
|
|
2473
|
+
this.player = player;
|
|
2474
|
+
}
|
|
2475
|
+
/**
|
|
2476
|
+
* 判断是否应该预取
|
|
2477
|
+
* 对于直播流:只要有 reader 且未完成,就持续下载
|
|
2478
|
+
*/
|
|
2479
|
+
shouldPrefetch() {
|
|
2480
|
+
if (!this.reader) return false;
|
|
2481
|
+
if (this.downloadComplete) return false;
|
|
2482
|
+
return true;
|
|
2483
|
+
}
|
|
2484
|
+
/**
|
|
2485
|
+
* 处理缓冲区数据
|
|
2486
|
+
*/
|
|
2487
|
+
processBufferData(data) {
|
|
2488
|
+
return this.player.processPrefetchBuffer(data);
|
|
2489
|
+
}
|
|
2490
|
+
/**
|
|
2491
|
+
* 获取队列大小(使用 player 的 videoTagQueue 大小)
|
|
2492
|
+
*/
|
|
2493
|
+
getQueueSize() {
|
|
2494
|
+
return this.player.videoTagQueue.length;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
class FLVPlayer extends BasePlayer {
|
|
2498
|
+
constructor(config = {}, callbacks = {}) {
|
|
2499
|
+
super(config, callbacks, DEFAULT_FLV_CONFIG);
|
|
2500
|
+
this.flvDemuxer = new FLVDemuxer();
|
|
2501
|
+
this.h264Decoder = null;
|
|
2502
|
+
this.hevcDecoder = null;
|
|
2503
|
+
this.isPrefetching = false;
|
|
2504
|
+
this.currentUrl = "";
|
|
2505
|
+
this._currentCodecId = CODEC_ID_AVC;
|
|
2506
|
+
this._timedFrameBuffer = [];
|
|
2507
|
+
this._videoTagQueue = [];
|
|
2508
|
+
this.playStartTime = 0;
|
|
2509
|
+
this.firstFrameDts = -1;
|
|
2510
|
+
this.lastRenderTime = 0;
|
|
2511
|
+
this.decodeFailCount = 0;
|
|
2512
|
+
this.dynamicMinBufferSize = 10;
|
|
2513
|
+
this.dynamicTargetBufferSize = 60;
|
|
2514
|
+
this.videoWidth = 0;
|
|
2515
|
+
this.videoHeight = 0;
|
|
2516
|
+
this.prefetcher = null;
|
|
2517
|
+
this.liveReader = null;
|
|
2518
|
+
this.liveDownloadAbort = false;
|
|
2519
|
+
this._lastQueuedTimestamp = -1;
|
|
2520
|
+
this._currentDownloadSpeed = 0;
|
|
2521
|
+
this.renderedFrames = 0;
|
|
2522
|
+
this.lastRenderLogTime = 0;
|
|
2523
|
+
this.consecutiveEmptyBuffer = 0;
|
|
2524
|
+
this.lastRenderedDts = -1;
|
|
2525
|
+
this.pausedTime = 0;
|
|
2526
|
+
this.bufferEmptyStartTime = 0;
|
|
2527
|
+
this.playStartTimeOffset = 0;
|
|
2528
|
+
this.resyncCount = 0;
|
|
2529
|
+
this.renderLoop = () => {
|
|
2530
|
+
if (!this.isPlaying) return;
|
|
2531
|
+
this.updateState({
|
|
2532
|
+
decoded: this._timedFrameBuffer.length,
|
|
2533
|
+
downloaded: this._videoTagQueue.length,
|
|
2534
|
+
droppedFrames: this.droppedFrames
|
|
2535
|
+
});
|
|
2536
|
+
const now = performance.now();
|
|
2537
|
+
const isLive = this.config.isLive;
|
|
2538
|
+
if (this._timedFrameBuffer.length > 0 && this.renderer) {
|
|
2539
|
+
this.consecutiveEmptyBuffer = 0;
|
|
2540
|
+
if (this.firstFrameDts < 0) {
|
|
2541
|
+
this.firstFrameDts = this._timedFrameBuffer[0].dts;
|
|
2542
|
+
this.playStartTime = now;
|
|
2543
|
+
this.playStartTimeOffset = 0;
|
|
2544
|
+
console.log("[FLVPlayer] RenderLoop initialized, firstFrameDts:", this.firstFrameDts, "isLive:", isLive);
|
|
2545
|
+
}
|
|
2546
|
+
if (isLive) {
|
|
2547
|
+
const elapsed = now - this.playStartTime - this.pausedTime;
|
|
2548
|
+
const currentTargetDts = this.firstFrameDts + elapsed;
|
|
2549
|
+
this.setCurrentTime(Math.floor(currentTargetDts));
|
|
2550
|
+
let frameToRender = null;
|
|
2551
|
+
while (this._timedFrameBuffer.length > 0 && this._timedFrameBuffer[0].dts <= currentTargetDts) {
|
|
2552
|
+
if (frameToRender) {
|
|
2553
|
+
this.droppedFrames++;
|
|
2554
|
+
}
|
|
2555
|
+
frameToRender = this._timedFrameBuffer.shift();
|
|
2556
|
+
}
|
|
2557
|
+
if (frameToRender) {
|
|
2558
|
+
this.renderFrame(frameToRender, now);
|
|
2559
|
+
}
|
|
2560
|
+
} else {
|
|
2561
|
+
const nextFrame = this._timedFrameBuffer[0];
|
|
2562
|
+
const elapsed = now - this.playStartTime - this.playStartTimeOffset;
|
|
2563
|
+
const currentTargetPts = this.firstFrameDts + elapsed;
|
|
2564
|
+
if (this.lastRenderedDts >= 0) {
|
|
2565
|
+
this.setCurrentTime(Math.floor(this.lastRenderedDts));
|
|
2566
|
+
}
|
|
2567
|
+
if (nextFrame.dts <= currentTargetPts) {
|
|
2568
|
+
const frameToRender = this._timedFrameBuffer.shift();
|
|
2569
|
+
this.renderFrame(frameToRender, now);
|
|
2570
|
+
this.lastRenderedDts = frameToRender.dts;
|
|
2571
|
+
const lag = currentTargetPts - frameToRender.dts;
|
|
2572
|
+
if (lag > 1e3) {
|
|
2573
|
+
this.playStartTime = now;
|
|
2574
|
+
this.playStartTimeOffset = 0;
|
|
2575
|
+
this.firstFrameDts = frameToRender.dts;
|
|
2576
|
+
this.resyncCount++;
|
|
2577
|
+
if (this.resyncCount <= 3) {
|
|
2578
|
+
console.log("[FLVPlayer] Major resync, lag:", Math.floor(lag), "ms");
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
if (now - this.lastRenderLogTime > 5e3) {
|
|
2584
|
+
console.log("[FLVPlayer] RenderLoop stats:", {
|
|
2585
|
+
renderedFrames: this.renderedFrames,
|
|
2586
|
+
droppedFrames: this.droppedFrames,
|
|
2587
|
+
decoded: this._timedFrameBuffer.length,
|
|
2588
|
+
downloaded: this._videoTagQueue.length,
|
|
2589
|
+
mode: isLive ? "live" : "vod"
|
|
2590
|
+
});
|
|
2591
|
+
this.lastRenderLogTime = now;
|
|
2592
|
+
}
|
|
2593
|
+
} else {
|
|
2594
|
+
if (this.consecutiveEmptyBuffer === 0 && this.lastRenderedDts >= 0) {
|
|
2595
|
+
this.bufferEmptyStartTime = now;
|
|
2596
|
+
}
|
|
2597
|
+
this.consecutiveEmptyBuffer++;
|
|
2598
|
+
if (isLive && this.bufferEmptyStartTime > 0) {
|
|
2599
|
+
this.pausedTime = now - this.bufferEmptyStartTime;
|
|
2600
|
+
} else if (!isLive && this.bufferEmptyStartTime > 0) {
|
|
2601
|
+
this.playStartTimeOffset = now - this.bufferEmptyStartTime;
|
|
2602
|
+
}
|
|
2603
|
+
if (this.consecutiveEmptyBuffer === 1) {
|
|
2604
|
+
console.warn("[FLVPlayer] Buffer empty, waiting for frames... queue:", this._videoTagQueue.length);
|
|
2605
|
+
} else if (this.consecutiveEmptyBuffer % 60 === 0) {
|
|
2606
|
+
console.warn("[FLVPlayer] Buffer still empty after", Math.floor(this.consecutiveEmptyBuffer / 60), "seconds");
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
this.frameTimer = requestAnimationFrame(this.renderLoop);
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2612
|
+
// 公共访问器(供预取器使用)
|
|
2613
|
+
get videoTagQueue() {
|
|
2614
|
+
return this._videoTagQueue;
|
|
2615
|
+
}
|
|
2616
|
+
get currentCodecId() {
|
|
2617
|
+
return this._currentCodecId;
|
|
2618
|
+
}
|
|
2619
|
+
get lastQueuedTimestamp() {
|
|
2620
|
+
return this._lastQueuedTimestamp;
|
|
2621
|
+
}
|
|
2622
|
+
/**
|
|
2623
|
+
* 加载 FLV 文件
|
|
2624
|
+
*/
|
|
2625
|
+
async load(url) {
|
|
2626
|
+
console.log("[FLVPlayer] Loading URL:", url);
|
|
2627
|
+
this.currentUrl = url;
|
|
2628
|
+
this.stopLiveDownload();
|
|
2629
|
+
this._timedFrameBuffer = [];
|
|
2630
|
+
this._videoTagQueue = [];
|
|
2631
|
+
this.liveDownloadAbort = false;
|
|
2632
|
+
this.resetState();
|
|
2633
|
+
this.playStartTime = 0;
|
|
2634
|
+
this.firstFrameDts = -1;
|
|
2635
|
+
this.lastRenderTime = 0;
|
|
2636
|
+
this.decodeFailCount = 0;
|
|
2637
|
+
this.dynamicMinBufferSize = 10;
|
|
2638
|
+
this.dynamicTargetBufferSize = this.config.targetBufferSize;
|
|
2639
|
+
this.videoWidth = 0;
|
|
2640
|
+
this.videoHeight = 0;
|
|
2641
|
+
if (this.prefetcher) {
|
|
2642
|
+
this.prefetcher.reset();
|
|
2643
|
+
this.prefetcher = null;
|
|
2644
|
+
}
|
|
2645
|
+
this._currentDownloadSpeed = 0;
|
|
2646
|
+
this._lastQueuedTimestamp = -1;
|
|
2647
|
+
this.updateState({ downloadSpeed: 0 });
|
|
2648
|
+
this.flvDemuxer = new FLVDemuxer();
|
|
2649
|
+
await this.loadFLV(url);
|
|
2650
|
+
this.updateState({ isLoaded: true });
|
|
2651
|
+
console.log("[FLVPlayer] Loaded, video tags:", this._videoTagQueue.length);
|
|
2652
|
+
}
|
|
2653
|
+
/**
|
|
2654
|
+
* 初始化解码器(覆盖基类方法,支持 AVC 和 HEVC)
|
|
2655
|
+
*/
|
|
2656
|
+
async initDecoder() {
|
|
2657
|
+
await this.wasmLoader.load(this.config.wasmPath);
|
|
2658
|
+
const avcConfig = this.flvDemuxer.getAvcConfig();
|
|
2659
|
+
const hevcConfig = this.flvDemuxer.getHevcConfig();
|
|
2660
|
+
this._currentCodecId = this.flvDemuxer.getCodecId();
|
|
2661
|
+
if (hevcConfig) {
|
|
2662
|
+
console.log("[FLVPlayer] Initializing HEVC decoder...");
|
|
2663
|
+
this.hevcDecoder = new HEVCDecoder(this.wasmLoader);
|
|
2664
|
+
await this.hevcDecoder.init();
|
|
2665
|
+
if (hevcConfig.vpsList.length > 0 || hevcConfig.spsList.length > 0) {
|
|
2666
|
+
this.initDecoderFromHEVCConfig(hevcConfig);
|
|
2667
|
+
this.decoderInitialized = true;
|
|
2668
|
+
} else {
|
|
2669
|
+
console.log("[FLVPlayer] HEVC config has no VPS/SPS/PPS, will init from first NALU");
|
|
2670
|
+
if (this._videoTagQueue.length > 0) {
|
|
2671
|
+
const firstTag = this._videoTagQueue[0];
|
|
2672
|
+
this.tryInitFromData(firstTag.annexBData);
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
} else if (avcConfig) {
|
|
2676
|
+
console.log("[FLVPlayer] Initializing AVC decoder...");
|
|
2677
|
+
this.h264Decoder = new H264Decoder(this.wasmLoader);
|
|
2678
|
+
await this.h264Decoder.init();
|
|
2679
|
+
if (avcConfig.spsList.length > 0) {
|
|
2680
|
+
this.initDecoderFromAVCConfig(avcConfig);
|
|
2681
|
+
this.decoderInitialized = true;
|
|
2682
|
+
} else {
|
|
2683
|
+
console.log("[FLVPlayer] AVC config has no SPS/PPS, will init from first NALU");
|
|
2684
|
+
if (this._videoTagQueue.length > 0) {
|
|
2685
|
+
const firstTag = this._videoTagQueue[0];
|
|
2686
|
+
this.tryInitFromData(firstTag.annexBData);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
} else {
|
|
2690
|
+
console.warn("[FLVPlayer] No config found, will try to init from first frames");
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
/**
|
|
2694
|
+
* 根据视频分辨率动态调整缓冲参数
|
|
2695
|
+
*/
|
|
2696
|
+
adjustBufferForResolution(width, height) {
|
|
2697
|
+
this.videoWidth = width;
|
|
2698
|
+
this.videoHeight = height;
|
|
2699
|
+
const pixels = width * height;
|
|
2700
|
+
const basePixels = 1920 * 1080;
|
|
2701
|
+
const ratio = pixels / basePixels;
|
|
2702
|
+
if (ratio > 2) {
|
|
2703
|
+
this.dynamicMinBufferSize = 30;
|
|
2704
|
+
this.dynamicTargetBufferSize = Math.min(150, this.config.targetBufferSize * 2);
|
|
2705
|
+
console.log(`[FLVPlayer] High resolution (${width}x${height}), using larger buffer`);
|
|
2706
|
+
} else if (ratio > 1) {
|
|
2707
|
+
this.dynamicMinBufferSize = 20;
|
|
2708
|
+
this.dynamicTargetBufferSize = Math.min(100, Math.floor(this.config.targetBufferSize * 1.5));
|
|
2709
|
+
console.log(`[FLVPlayer] Medium-high resolution (${width}x${height}), using medium buffer`);
|
|
2710
|
+
} else {
|
|
2711
|
+
this.dynamicMinBufferSize = 10;
|
|
2712
|
+
this.dynamicTargetBufferSize = this.config.targetBufferSize;
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
/**
|
|
2716
|
+
* 开始播放(覆盖基类方法)
|
|
2717
|
+
*/
|
|
2718
|
+
async play() {
|
|
2719
|
+
const MIN_BUFFER_SIZE = this.dynamicMinBufferSize;
|
|
2720
|
+
const MAX_WAIT_TIME = 1e4;
|
|
2721
|
+
const startTime = Date.now();
|
|
2722
|
+
console.log(`[FLVPlayer] Waiting for buffer (target: ${MIN_BUFFER_SIZE} frames)...`);
|
|
2723
|
+
let aggressiveDecodeCount = 0;
|
|
2724
|
+
const MAX_AGGRESSIVE_DECODE = 100;
|
|
2725
|
+
while (this._timedFrameBuffer.length < MIN_BUFFER_SIZE && this._videoTagQueue.length > 0) {
|
|
2726
|
+
if (Date.now() - startTime > MAX_WAIT_TIME) {
|
|
2727
|
+
console.log("[FLVPlayer] Wait timeout, starting with", this._timedFrameBuffer.length, "frames");
|
|
2728
|
+
break;
|
|
2729
|
+
}
|
|
2730
|
+
const batchSize = Math.min(10, this._videoTagQueue.length);
|
|
2731
|
+
for (let i = 0; i < batchSize && this._videoTagQueue.length > 0; i++) {
|
|
2732
|
+
const queuedTag = this._videoTagQueue.shift();
|
|
2733
|
+
const frame = this.decodeVideoTag(queuedTag);
|
|
2734
|
+
if (frame) {
|
|
2735
|
+
const dts = queuedTag.tag.timestamp;
|
|
2736
|
+
const pts = dts + queuedTag.tag.compositionTimeOffset;
|
|
2737
|
+
this._timedFrameBuffer.push({ frame, dts, pts });
|
|
2738
|
+
if (this.videoWidth === 0 || this.videoHeight === 0) {
|
|
2739
|
+
this.adjustBufferForResolution(frame.width, frame.height);
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
aggressiveDecodeCount++;
|
|
2743
|
+
}
|
|
2744
|
+
if (aggressiveDecodeCount > MAX_AGGRESSIVE_DECODE) {
|
|
2745
|
+
await this.sleep(10);
|
|
2746
|
+
aggressiveDecodeCount = 0;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
console.log("[FLVPlayer] Buffer ready, frames:", this._timedFrameBuffer.length, "queue:", this._videoTagQueue.length);
|
|
2750
|
+
if (this._timedFrameBuffer.length > 0) {
|
|
2751
|
+
this.firstFrameDts = this._timedFrameBuffer[0].dts;
|
|
2752
|
+
this.playStartTime = performance.now();
|
|
2753
|
+
console.log("[FLVPlayer] Play time initialized, firstFrameDts:", this.firstFrameDts);
|
|
2754
|
+
}
|
|
2755
|
+
this.isPlaying = true;
|
|
2756
|
+
this.decodeLoopAbort = false;
|
|
2757
|
+
this.updateState({ isPlaying: true });
|
|
2758
|
+
if (this.prefetcher) {
|
|
2759
|
+
this.prefetcher.start();
|
|
2760
|
+
}
|
|
2761
|
+
this.decodeLoop();
|
|
2762
|
+
this.frameTimer = requestAnimationFrame(this.renderLoop);
|
|
2763
|
+
}
|
|
2764
|
+
/**
|
|
2765
|
+
* 暂停播放
|
|
2766
|
+
*/
|
|
2767
|
+
pause() {
|
|
2768
|
+
super.pause();
|
|
2769
|
+
if (this.prefetcher) {
|
|
2770
|
+
this.prefetcher.stop();
|
|
2771
|
+
}
|
|
2772
|
+
if (this.config.isLive) {
|
|
2773
|
+
console.log("[FLVPlayer] Pausing live stream, stopping download");
|
|
2774
|
+
this.stopLiveDownload();
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
/**
|
|
2778
|
+
* 处理预取缓冲区数据(供预取器调用)
|
|
2779
|
+
*/
|
|
2780
|
+
processPrefetchBuffer(data) {
|
|
2781
|
+
if (!data || data.length === 0) {
|
|
2782
|
+
return { hasNewData: false, processedBytes: 0 };
|
|
2783
|
+
}
|
|
2784
|
+
const result = this.flvDemuxer.parseIncremental(data);
|
|
2785
|
+
let lengthSize = 4;
|
|
2786
|
+
if (result.hevcConfig) {
|
|
2787
|
+
lengthSize = result.hevcConfig.lengthSizeMinusOne + 1;
|
|
2788
|
+
this._currentCodecId = CODEC_ID_HEVC;
|
|
2789
|
+
} else if (result.avcConfig) {
|
|
2790
|
+
lengthSize = result.avcConfig.lengthSizeMinusOne + 1;
|
|
2791
|
+
this._currentCodecId = CODEC_ID_AVC;
|
|
2792
|
+
}
|
|
2793
|
+
let newCount = 0;
|
|
2794
|
+
for (const tag of result.videoTags) {
|
|
2795
|
+
if (tag.timestamp > this._lastQueuedTimestamp) {
|
|
2796
|
+
let annexBData = this.flvDemuxer.videoTagToAnnexB(tag, lengthSize);
|
|
2797
|
+
if (!annexBData) {
|
|
2798
|
+
for (const trySize of [4, 2, 1]) {
|
|
2799
|
+
if (trySize !== lengthSize) {
|
|
2800
|
+
annexBData = this.flvDemuxer.videoTagToAnnexB(tag, trySize);
|
|
2801
|
+
if (annexBData) {
|
|
2802
|
+
lengthSize = trySize;
|
|
2803
|
+
break;
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
if (annexBData) {
|
|
2809
|
+
this._videoTagQueue.push({ tag, annexBData });
|
|
2810
|
+
this._lastQueuedTimestamp = tag.timestamp;
|
|
2811
|
+
newCount++;
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
if (newCount > 0) {
|
|
2816
|
+
console.log(`[FLVPlayer] Prefetch: +${newCount} new tags, queue: ${this._videoTagQueue.length}`);
|
|
2817
|
+
}
|
|
2818
|
+
return { hasNewData: newCount > 0, processedBytes: data.length };
|
|
2819
|
+
}
|
|
2820
|
+
/**
|
|
2821
|
+
* 跳转到指定时间
|
|
2822
|
+
*/
|
|
2823
|
+
async seek(time) {
|
|
2824
|
+
this._timedFrameBuffer = [];
|
|
2825
|
+
this.firstFrameDts = -1;
|
|
2826
|
+
this.playStartTime = 0;
|
|
2827
|
+
this.setCurrentTime(time);
|
|
2828
|
+
console.log("[FLVPlayer] Seek to", time, "ms");
|
|
2829
|
+
}
|
|
2830
|
+
/**
|
|
2831
|
+
* 解码循环
|
|
2832
|
+
*/
|
|
2833
|
+
async decodeLoop() {
|
|
2834
|
+
console.log("[FLVPlayer] DecodeLoop START, queue size:", this._videoTagQueue.length);
|
|
2835
|
+
let emptyQueueCount = 0;
|
|
2836
|
+
const MAX_EMPTY_QUEUE_WAIT = 100;
|
|
2837
|
+
while (this.isPlaying && !this.decodeLoopAbort) {
|
|
2838
|
+
this.prefetcher?.processBuffer();
|
|
2839
|
+
const targetBuffer = this.dynamicTargetBufferSize;
|
|
2840
|
+
const bufferRatio = this._timedFrameBuffer.length / targetBuffer;
|
|
2841
|
+
let batchSize;
|
|
2842
|
+
if (bufferRatio < 0.3) {
|
|
2843
|
+
batchSize = Math.max(1, this.config.decodeBatchSize);
|
|
2844
|
+
} else if (bufferRatio < 0.6) {
|
|
2845
|
+
batchSize = Math.max(3, this.config.decodeBatchSize);
|
|
2846
|
+
} else {
|
|
2847
|
+
batchSize = Math.max(5, this.config.decodeBatchSize);
|
|
2848
|
+
}
|
|
2849
|
+
let decodedInBatch = 0;
|
|
2850
|
+
while (this._videoTagQueue.length > 0 && decodedInBatch < batchSize) {
|
|
2851
|
+
if (this._timedFrameBuffer.length >= targetBuffer) {
|
|
2852
|
+
break;
|
|
2853
|
+
}
|
|
2854
|
+
const queuedTag = this._videoTagQueue.shift();
|
|
2855
|
+
const frame = this.decodeVideoTag(queuedTag);
|
|
2856
|
+
if (frame) {
|
|
2857
|
+
const dts = queuedTag.tag.timestamp;
|
|
2858
|
+
const pts = dts + queuedTag.tag.compositionTimeOffset;
|
|
2859
|
+
this._timedFrameBuffer.push({ frame, dts, pts });
|
|
2860
|
+
decodedInBatch++;
|
|
2861
|
+
if (this.videoWidth === 0 || this.videoHeight === 0) {
|
|
2862
|
+
this.adjustBufferForResolution(frame.width, frame.height);
|
|
2863
|
+
}
|
|
2864
|
+
if (this._timedFrameBuffer.length <= 5) {
|
|
2865
|
+
console.log("[FLVPlayer] Frame decoded, dts:", dts, "pts:", pts, "buffer size:", this._timedFrameBuffer.length);
|
|
2866
|
+
}
|
|
2867
|
+
} else {
|
|
2868
|
+
this.decodeFailCount++;
|
|
2869
|
+
if (this.decodeFailCount <= 10) {
|
|
2870
|
+
console.log("[FLVPlayer] Decode failed, dts:", queuedTag.tag.timestamp, "total fails:", this.decodeFailCount);
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
if (decodedInBatch > 0) {
|
|
2875
|
+
emptyQueueCount = 0;
|
|
2876
|
+
if (this._timedFrameBuffer.length < targetBuffer * 0.3) {
|
|
2877
|
+
await this.yieldFast();
|
|
2878
|
+
} else if (this._timedFrameBuffer.length < targetBuffer * 0.6) {
|
|
2879
|
+
await this.yieldFast();
|
|
2880
|
+
} else {
|
|
2881
|
+
await this.sleep(2);
|
|
2882
|
+
}
|
|
2883
|
+
} else {
|
|
2884
|
+
if (this._videoTagQueue.length === 0 && this._timedFrameBuffer.length === 0) {
|
|
2885
|
+
emptyQueueCount++;
|
|
2886
|
+
if (emptyQueueCount >= MAX_EMPTY_QUEUE_WAIT) {
|
|
2887
|
+
console.log("[FLVPlayer] No more data after waiting, stopping decode loop");
|
|
2888
|
+
break;
|
|
2889
|
+
}
|
|
2890
|
+
if (emptyQueueCount === 1) {
|
|
2891
|
+
console.log("[FLVPlayer] Queue empty, waiting for more data...");
|
|
2892
|
+
}
|
|
2893
|
+
} else {
|
|
2894
|
+
emptyQueueCount = 0;
|
|
2895
|
+
}
|
|
2896
|
+
await this.sleep(50);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
console.log("[FLVPlayer] DecodeLoop END, decoded:", this._timedFrameBuffer.length + this.droppedFrames, "failed:", this.decodeFailCount, "dropped:", this.droppedFrames);
|
|
2900
|
+
}
|
|
2901
|
+
// ==================== 私有方法 ====================
|
|
2902
|
+
/**
|
|
2903
|
+
* 流式加载 FLV 文件
|
|
2904
|
+
*/
|
|
2905
|
+
async loadFLV(url) {
|
|
2906
|
+
console.log("[FLVPlayer] Fetching FLV...");
|
|
2907
|
+
const response = await fetch(url);
|
|
2908
|
+
if (!response.ok) {
|
|
2909
|
+
throw new Error(`Failed to fetch FLV: ${response.status}`);
|
|
2910
|
+
}
|
|
2911
|
+
console.log("[FLVPlayer] Fetch response OK, status:", response.status);
|
|
2912
|
+
this.isPrefetching = true;
|
|
2913
|
+
this.updateState({ isPrefetching: true });
|
|
2914
|
+
const isLive = this.config.isLive || false;
|
|
2915
|
+
console.log("[FLVPlayer] isLive:", isLive);
|
|
2916
|
+
try {
|
|
2917
|
+
const reader = response.body?.getReader();
|
|
2918
|
+
if (!reader) {
|
|
2919
|
+
throw new Error("ReadableStream not supported");
|
|
2920
|
+
}
|
|
2921
|
+
const chunks = [];
|
|
2922
|
+
let totalLength = 0;
|
|
2923
|
+
const MIN_DATA_SIZE = isLive ? 100 * 1024 : 500 * 1024;
|
|
2924
|
+
const TIMEOUT_MS = isLive ? 5e3 : 8e3;
|
|
2925
|
+
const startTime = Date.now();
|
|
2926
|
+
let started = false;
|
|
2927
|
+
let lastLoggedTags = 0;
|
|
2928
|
+
this.liveReader = reader;
|
|
2929
|
+
while (true) {
|
|
2930
|
+
const { done, value } = await reader.read();
|
|
2931
|
+
if (value) {
|
|
2932
|
+
chunks.push(value);
|
|
2933
|
+
totalLength += value.length;
|
|
2934
|
+
}
|
|
2935
|
+
const shouldStart = totalLength >= MIN_DATA_SIZE || Date.now() - startTime > TIMEOUT_MS;
|
|
2936
|
+
if (!started && shouldStart && totalLength > 0) {
|
|
2937
|
+
started = true;
|
|
2938
|
+
console.log("[FLVPlayer] Buffered", totalLength, "bytes, starting...");
|
|
2939
|
+
const data = new Uint8Array(totalLength);
|
|
2940
|
+
let offset = 0;
|
|
2941
|
+
for (const chunk of chunks) {
|
|
2942
|
+
data.set(chunk, offset);
|
|
2943
|
+
offset += chunk.length;
|
|
2944
|
+
}
|
|
2945
|
+
this.parseAndQueueFLV(data);
|
|
2946
|
+
lastLoggedTags = this._videoTagQueue.length;
|
|
2947
|
+
this.updateState({ isLoaded: true });
|
|
2948
|
+
console.log("[FLVPlayer] Stream ready, video tags:", this._videoTagQueue.length);
|
|
2949
|
+
this.initPrefetcher(reader, chunks, totalLength);
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
if (started && totalLength > 0) {
|
|
2953
|
+
const allData = new Uint8Array(totalLength);
|
|
2954
|
+
let offset = 0;
|
|
2955
|
+
for (const chunk of chunks) {
|
|
2956
|
+
allData.set(chunk, offset);
|
|
2957
|
+
offset += chunk.length;
|
|
2958
|
+
}
|
|
2959
|
+
this._videoTagQueue = [];
|
|
2960
|
+
this.parseAndQueueFLV(allData);
|
|
2961
|
+
if (this._videoTagQueue.length - lastLoggedTags >= 50) {
|
|
2962
|
+
console.log("[FLVPlayer] Stream:", this._videoTagQueue.length, "tags,", totalLength, "bytes");
|
|
2963
|
+
lastLoggedTags = this._videoTagQueue.length;
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
if (done) {
|
|
2967
|
+
console.log("[FLVPlayer] Download complete:", totalLength, "bytes");
|
|
2968
|
+
if (!started && totalLength > 0) {
|
|
2969
|
+
const data = new Uint8Array(totalLength);
|
|
2970
|
+
let offset = 0;
|
|
2971
|
+
for (const chunk of chunks) {
|
|
2972
|
+
data.set(chunk, offset);
|
|
2973
|
+
offset += chunk.length;
|
|
2974
|
+
}
|
|
2975
|
+
this.parseAndQueueFLV(data);
|
|
2976
|
+
this.updateState({ isLoaded: true });
|
|
2977
|
+
}
|
|
2978
|
+
break;
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
} catch (error) {
|
|
2982
|
+
console.error("[FLVPlayer] Error loading FLV:", error);
|
|
2983
|
+
throw error;
|
|
2984
|
+
} finally {
|
|
2985
|
+
if (!this.config.isLive) {
|
|
2986
|
+
this.isPrefetching = false;
|
|
2987
|
+
this.updateState({ isPrefetching: false });
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
/**
|
|
2992
|
+
* 初始化预取器
|
|
2993
|
+
*/
|
|
2994
|
+
initPrefetcher(reader, chunks, initialLength) {
|
|
2995
|
+
const initialData = new Uint8Array(initialLength);
|
|
2996
|
+
let offset = 0;
|
|
2997
|
+
for (const chunk of chunks) {
|
|
2998
|
+
initialData.set(chunk, offset);
|
|
2999
|
+
offset += chunk.length;
|
|
3000
|
+
}
|
|
3001
|
+
this.prefetcher = new FLVStreamPrefetcher(
|
|
3002
|
+
FLV_PREFETCHER_CONFIG,
|
|
3003
|
+
{
|
|
3004
|
+
onStatusChange: (status) => {
|
|
3005
|
+
this.updateState({
|
|
3006
|
+
isPrefetching: status.isPrefetching,
|
|
3007
|
+
downloadSpeed: status.downloadSpeed
|
|
3008
|
+
});
|
|
3009
|
+
this._currentDownloadSpeed = status.downloadSpeed;
|
|
3010
|
+
},
|
|
3011
|
+
onDataReceived: (totalBytes) => {
|
|
3012
|
+
console.log(`[FLVPlayer] Data received: ${totalBytes} bytes`);
|
|
3013
|
+
}
|
|
3014
|
+
},
|
|
3015
|
+
this
|
|
3016
|
+
);
|
|
3017
|
+
this.prefetcher.setReader(reader, initialData);
|
|
3018
|
+
this.flvDemuxer.parseIncremental(initialData);
|
|
3019
|
+
console.log(`[FLVPlayer] Prefetcher initialized with ${initialLength} bytes`);
|
|
3020
|
+
}
|
|
3021
|
+
/**
|
|
3022
|
+
* 停止直播流下载
|
|
3023
|
+
*/
|
|
3024
|
+
stopLiveDownload() {
|
|
3025
|
+
this.liveDownloadAbort = true;
|
|
3026
|
+
if (this.liveReader) {
|
|
3027
|
+
this.liveReader.cancel();
|
|
3028
|
+
this.liveReader = null;
|
|
3029
|
+
}
|
|
3030
|
+
if (this.prefetcher) {
|
|
3031
|
+
this.prefetcher.cancelDownload();
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
/**
|
|
3035
|
+
* 解析 FLV 数据并入队
|
|
3036
|
+
*/
|
|
3037
|
+
parseAndQueueFLV(data) {
|
|
3038
|
+
if (data.length < 13) {
|
|
3039
|
+
throw new Error("Invalid FLV: data too short");
|
|
3040
|
+
}
|
|
3041
|
+
const flvHeader = String.fromCharCode(data[0], data[1], data[2]);
|
|
3042
|
+
if (flvHeader !== "FLV") {
|
|
3043
|
+
throw new Error("Invalid FLV: header not found, got: " + flvHeader);
|
|
3044
|
+
}
|
|
3045
|
+
console.log("[FLVPlayer] FLV header valid");
|
|
3046
|
+
const result = this.flvDemuxer.parse(data);
|
|
3047
|
+
console.log("[FLVPlayer] Parsed FLV:", {
|
|
3048
|
+
videoTags: result.videoTags.length,
|
|
3049
|
+
duration: result.duration,
|
|
3050
|
+
hasAvcConfig: !!result.avcConfig,
|
|
3051
|
+
hasHevcConfig: !!result.hevcConfig
|
|
3052
|
+
});
|
|
3053
|
+
this.setDuration(result.duration);
|
|
3054
|
+
let lengthSize = 4;
|
|
3055
|
+
if (result.hevcConfig) {
|
|
3056
|
+
lengthSize = result.hevcConfig.lengthSizeMinusOne + 1;
|
|
3057
|
+
this._currentCodecId = CODEC_ID_HEVC;
|
|
3058
|
+
} else if (result.avcConfig) {
|
|
3059
|
+
lengthSize = result.avcConfig.lengthSizeMinusOne + 1;
|
|
3060
|
+
this._currentCodecId = CODEC_ID_AVC;
|
|
3061
|
+
}
|
|
3062
|
+
let detectedLengthSize = lengthSize;
|
|
3063
|
+
for (const tag of result.videoTags) {
|
|
3064
|
+
let annexBData = this.flvDemuxer.videoTagToAnnexB(tag, detectedLengthSize);
|
|
3065
|
+
if (!annexBData && detectedLengthSize !== 4) {
|
|
3066
|
+
annexBData = this.flvDemuxer.videoTagToAnnexB(tag, 4);
|
|
3067
|
+
if (annexBData) {
|
|
3068
|
+
detectedLengthSize = 4;
|
|
3069
|
+
console.log("[FLVPlayer] Auto-detected length size: 4 bytes");
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
if (annexBData) {
|
|
3073
|
+
this._videoTagQueue.push({ tag, annexBData });
|
|
3074
|
+
if (tag.timestamp > this._lastQueuedTimestamp) {
|
|
3075
|
+
this._lastQueuedTimestamp = tag.timestamp;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
console.log("[FLVPlayer] Queued", this._videoTagQueue.length, "video tags, lengthSize:", detectedLengthSize);
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* 从 AVC 配置初始化解码器
|
|
3083
|
+
*/
|
|
3084
|
+
initDecoderFromAVCConfig(config) {
|
|
3085
|
+
if (!this.h264Decoder) return;
|
|
3086
|
+
if (config.spsList.length > 0) {
|
|
3087
|
+
const sps = config.spsList[0];
|
|
3088
|
+
const spsWithStartCode = new Uint8Array(4 + sps.length);
|
|
3089
|
+
spsWithStartCode.set([0, 0, 0, 1], 0);
|
|
3090
|
+
spsWithStartCode.set(sps, 4);
|
|
3091
|
+
this.h264Decoder.decode({ type: 7, data: spsWithStartCode, size: spsWithStartCode.length });
|
|
3092
|
+
console.log("[FLVPlayer] AVC SPS decoded, length:", sps.length);
|
|
3093
|
+
}
|
|
3094
|
+
if (config.ppsList.length > 0) {
|
|
3095
|
+
const pps = config.ppsList[0];
|
|
3096
|
+
const ppsWithStartCode = new Uint8Array(4 + pps.length);
|
|
3097
|
+
ppsWithStartCode.set([0, 0, 0, 1], 0);
|
|
3098
|
+
ppsWithStartCode.set(pps, 4);
|
|
3099
|
+
this.h264Decoder.decode({ type: 8, data: ppsWithStartCode, size: ppsWithStartCode.length });
|
|
3100
|
+
console.log("[FLVPlayer] AVC PPS decoded, length:", pps.length);
|
|
3101
|
+
}
|
|
3102
|
+
console.log("[FLVPlayer] Decoder initialized from AVC config");
|
|
3103
|
+
}
|
|
3104
|
+
/**
|
|
3105
|
+
* 从 HEVC 配置初始化解码器
|
|
3106
|
+
*/
|
|
3107
|
+
initDecoderFromHEVCConfig(config) {
|
|
3108
|
+
if (!this.hevcDecoder) return;
|
|
3109
|
+
if (config.vpsList.length > 0) {
|
|
3110
|
+
const vps = config.vpsList[0];
|
|
3111
|
+
const vpsWithStartCode = new Uint8Array(4 + vps.length);
|
|
3112
|
+
vpsWithStartCode.set([0, 0, 0, 1], 0);
|
|
3113
|
+
vpsWithStartCode.set(vps, 4);
|
|
3114
|
+
this.hevcDecoder.decode({ type: 32, data: vpsWithStartCode, size: vpsWithStartCode.length });
|
|
3115
|
+
console.log("[FLVPlayer] HEVC VPS decoded, length:", vps.length);
|
|
3116
|
+
}
|
|
3117
|
+
if (config.spsList.length > 0) {
|
|
3118
|
+
const sps = config.spsList[0];
|
|
3119
|
+
const spsWithStartCode = new Uint8Array(4 + sps.length);
|
|
3120
|
+
spsWithStartCode.set([0, 0, 0, 1], 0);
|
|
3121
|
+
spsWithStartCode.set(sps, 4);
|
|
3122
|
+
this.hevcDecoder.decode({ type: 33, data: spsWithStartCode, size: spsWithStartCode.length });
|
|
3123
|
+
console.log("[FLVPlayer] HEVC SPS decoded, length:", sps.length);
|
|
3124
|
+
}
|
|
3125
|
+
if (config.ppsList.length > 0) {
|
|
3126
|
+
const pps = config.ppsList[0];
|
|
3127
|
+
const ppsWithStartCode = new Uint8Array(4 + pps.length);
|
|
3128
|
+
ppsWithStartCode.set([0, 0, 0, 1], 0);
|
|
3129
|
+
ppsWithStartCode.set(pps, 4);
|
|
3130
|
+
this.hevcDecoder.decode({ type: 34, data: ppsWithStartCode, size: ppsWithStartCode.length });
|
|
3131
|
+
console.log("[FLVPlayer] HEVC PPS decoded, length:", pps.length);
|
|
3132
|
+
}
|
|
3133
|
+
console.log("[FLVPlayer] Decoder initialized from HEVC config");
|
|
3134
|
+
}
|
|
3135
|
+
/**
|
|
3136
|
+
* 解码 Video Tag
|
|
3137
|
+
*/
|
|
3138
|
+
decodeVideoTag(queuedTag) {
|
|
3139
|
+
const decoder = this._currentCodecId === CODEC_ID_HEVC ? this.hevcDecoder : this.h264Decoder;
|
|
3140
|
+
if (!decoder) {
|
|
3141
|
+
console.warn("[FLVPlayer] Decoder not available");
|
|
3142
|
+
return null;
|
|
3143
|
+
}
|
|
3144
|
+
if (!this.decoderInitialized) {
|
|
3145
|
+
if (this.flvDemuxer.isKeyframe(queuedTag.tag)) {
|
|
3146
|
+
this.tryInitFromData(queuedTag.annexBData);
|
|
3147
|
+
}
|
|
3148
|
+
if (!this.decoderInitialized) {
|
|
3149
|
+
return null;
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
const isKeyframe = this.flvDemuxer.isKeyframe(queuedTag.tag);
|
|
3153
|
+
const frame = decoder.decode({
|
|
3154
|
+
type: isKeyframe ? 5 : 1,
|
|
3155
|
+
data: queuedTag.annexBData,
|
|
3156
|
+
size: queuedTag.annexBData.length
|
|
3157
|
+
});
|
|
3158
|
+
return frame;
|
|
3159
|
+
}
|
|
3160
|
+
/**
|
|
3161
|
+
* 尝试从 Annex B 数据中初始化解码器
|
|
3162
|
+
*/
|
|
3163
|
+
tryInitFromData(data) {
|
|
3164
|
+
if (this.decoderInitialized) return;
|
|
3165
|
+
const decoder = this._currentCodecId === CODEC_ID_HEVC ? this.hevcDecoder : this.h264Decoder;
|
|
3166
|
+
if (!decoder) return;
|
|
3167
|
+
if (this._currentCodecId === CODEC_ID_HEVC) {
|
|
3168
|
+
this.tryInitHEVCFromData(data, decoder);
|
|
3169
|
+
} else {
|
|
3170
|
+
this.tryInitAVCFromData(data, decoder);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
/**
|
|
3174
|
+
* 从数据中初始化 AVC 解码器
|
|
3175
|
+
*/
|
|
3176
|
+
tryInitAVCFromData(data, decoder) {
|
|
3177
|
+
let offset = 0;
|
|
3178
|
+
let sps = null;
|
|
3179
|
+
let pps = null;
|
|
3180
|
+
while (offset + 4 < data.length) {
|
|
3181
|
+
if (data[offset] === 0 && data[offset + 1] === 0 && (data[offset + 2] === 1 || data[offset + 2] === 0 && data[offset + 3] === 1)) {
|
|
3182
|
+
const startCodeSize = data[offset + 2] === 1 ? 3 : 4;
|
|
3183
|
+
const nalStart = offset + startCodeSize;
|
|
3184
|
+
if (nalStart >= data.length) break;
|
|
3185
|
+
const nalType = data[nalStart] & 31;
|
|
3186
|
+
let nalEnd = data.length;
|
|
3187
|
+
for (let i = nalStart + 1; i < data.length - 3; i++) {
|
|
3188
|
+
if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 1) {
|
|
3189
|
+
nalEnd = i;
|
|
3190
|
+
break;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
if (nalType === 7 && !sps) {
|
|
3194
|
+
sps = data.slice(offset, nalEnd);
|
|
3195
|
+
} else if (nalType === 8 && !pps) {
|
|
3196
|
+
pps = data.slice(offset, nalEnd);
|
|
3197
|
+
}
|
|
3198
|
+
offset = nalEnd;
|
|
3199
|
+
} else {
|
|
3200
|
+
offset++;
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
if (sps) {
|
|
3204
|
+
decoder.decode({ type: 7, data: sps, size: sps.length });
|
|
3205
|
+
console.log("[FLVPlayer] AVC SPS decoded from data");
|
|
3206
|
+
}
|
|
3207
|
+
if (pps) {
|
|
3208
|
+
decoder.decode({ type: 8, data: pps, size: pps.length });
|
|
3209
|
+
console.log("[FLVPlayer] AVC PPS decoded from data");
|
|
3210
|
+
}
|
|
3211
|
+
if (sps || pps) {
|
|
3212
|
+
this.decoderInitialized = true;
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
/**
|
|
3216
|
+
* 从数据中初始化 HEVC 解码器
|
|
3217
|
+
*/
|
|
3218
|
+
tryInitHEVCFromData(data, decoder) {
|
|
3219
|
+
if (!data) return;
|
|
3220
|
+
let offset = 0;
|
|
3221
|
+
let vps = null;
|
|
3222
|
+
let sps = null;
|
|
3223
|
+
let pps = null;
|
|
3224
|
+
while (offset + 5 < data.length) {
|
|
3225
|
+
if (data[offset] === 0 && data[offset + 1] === 0 && (data[offset + 2] === 1 || data[offset + 2] === 0 && data[offset + 3] === 1)) {
|
|
3226
|
+
const startCodeSize = data[offset + 2] === 1 ? 3 : 4;
|
|
3227
|
+
const nalStart = offset + startCodeSize;
|
|
3228
|
+
if (nalStart >= data.length) break;
|
|
3229
|
+
const nalType = data[nalStart] >> 1 & 63;
|
|
3230
|
+
let nalEnd = data.length;
|
|
3231
|
+
for (let i = nalStart + 1; i < data.length - 3; i++) {
|
|
3232
|
+
if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 1) {
|
|
3233
|
+
nalEnd = i;
|
|
3234
|
+
break;
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
if (nalType === 32 && !vps) {
|
|
3238
|
+
vps = data.slice(offset, nalEnd);
|
|
3239
|
+
} else if (nalType === 33 && !sps) {
|
|
3240
|
+
sps = data.slice(offset, nalEnd);
|
|
3241
|
+
} else if (nalType === 34 && !pps) {
|
|
3242
|
+
pps = data.slice(offset, nalEnd);
|
|
3243
|
+
}
|
|
3244
|
+
offset = nalEnd;
|
|
3245
|
+
} else {
|
|
3246
|
+
offset++;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
if (vps) {
|
|
3250
|
+
decoder.decode({ type: 32, data: vps, size: vps.length });
|
|
3251
|
+
console.log("[FLVPlayer] HEVC VPS decoded from data");
|
|
3252
|
+
}
|
|
3253
|
+
if (sps) {
|
|
3254
|
+
decoder.decode({ type: 33, data: sps, size: sps.length });
|
|
3255
|
+
console.log("[FLVPlayer] HEVC SPS decoded from data");
|
|
3256
|
+
}
|
|
3257
|
+
if (pps) {
|
|
3258
|
+
decoder.decode({ type: 34, data: pps, size: pps.length });
|
|
3259
|
+
console.log("[FLVPlayer] HEVC PPS decoded from data");
|
|
3260
|
+
}
|
|
3261
|
+
if (vps || sps || pps) {
|
|
3262
|
+
this.decoderInitialized = true;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
/**
|
|
3266
|
+
* 渲染一帧
|
|
3267
|
+
*/
|
|
3268
|
+
renderFrame(frame, now) {
|
|
3269
|
+
if (!this.renderer) return;
|
|
3270
|
+
this.renderer.render(frame.frame);
|
|
3271
|
+
this.renderedFrames++;
|
|
3272
|
+
this.lastRenderedDts = frame.dts;
|
|
3273
|
+
this.updateState({ resolution: `${frame.frame.width}x${frame.frame.height}` });
|
|
3274
|
+
const fps = this.renderer.updateFps();
|
|
3275
|
+
this.updateState({ fps });
|
|
3276
|
+
this.callbacks.onFrameRender?.(frame.frame);
|
|
3277
|
+
this.lastRenderTime = now;
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
class FormatDetector {
|
|
3281
|
+
/**
|
|
3282
|
+
* 通过 URL 检测格式
|
|
3283
|
+
*/
|
|
3284
|
+
static detectFromUrl(url) {
|
|
3285
|
+
try {
|
|
3286
|
+
const urlObj = new URL(url);
|
|
3287
|
+
const pathname = urlObj.pathname.toLowerCase();
|
|
3288
|
+
if (pathname.endsWith(".m3u8") || pathname.endsWith(".m3u")) {
|
|
3289
|
+
return "hls-ts";
|
|
3290
|
+
}
|
|
3291
|
+
if (pathname.endsWith(".flv")) {
|
|
3292
|
+
return "flv";
|
|
3293
|
+
}
|
|
3294
|
+
if (pathname.endsWith(".mp4")) {
|
|
3295
|
+
return "mp4";
|
|
3296
|
+
}
|
|
3297
|
+
} catch {
|
|
3298
|
+
const lower = url.toLowerCase();
|
|
3299
|
+
if (lower.includes(".m3u8") || lower.includes(".m3u")) return "hls-ts";
|
|
3300
|
+
if (lower.includes(".flv")) return "flv";
|
|
3301
|
+
if (lower.includes(".mp4")) return "mp4";
|
|
3302
|
+
}
|
|
3303
|
+
return "unknown";
|
|
3304
|
+
}
|
|
3305
|
+
/**
|
|
3306
|
+
* 通过数据头检测格式
|
|
3307
|
+
*/
|
|
3308
|
+
static detectFromData(data) {
|
|
3309
|
+
if (data.length < 4) return "unknown";
|
|
3310
|
+
if (data[0] === 70 && data[1] === 76 && data[2] === 86) {
|
|
3311
|
+
return "flv";
|
|
3312
|
+
}
|
|
3313
|
+
if (data.length >= 8 && data[4] === 102 && data[5] === 116 && data[6] === 121 && data[7] === 112) {
|
|
3314
|
+
return "hls-fmp4";
|
|
3315
|
+
}
|
|
3316
|
+
if (data[0] === 71) {
|
|
3317
|
+
return "hls-ts";
|
|
3318
|
+
}
|
|
3319
|
+
return "unknown";
|
|
3320
|
+
}
|
|
3321
|
+
/**
|
|
3322
|
+
* 通过 Content-Type 检测格式
|
|
3323
|
+
*/
|
|
3324
|
+
static detectFromContentType(contentType) {
|
|
3325
|
+
const type = contentType.toLowerCase();
|
|
3326
|
+
if (type.includes("mpegurl") || type.includes("x-mpegurl")) {
|
|
3327
|
+
return "hls-ts";
|
|
3328
|
+
}
|
|
3329
|
+
if (type.includes("x-flv") || type.includes("flv")) {
|
|
3330
|
+
return "flv";
|
|
3331
|
+
}
|
|
3332
|
+
if (type.includes("mp4")) {
|
|
3333
|
+
return "mp4";
|
|
3334
|
+
}
|
|
3335
|
+
return "unknown";
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
class Canvas2DRenderer {
|
|
3339
|
+
constructor(canvas) {
|
|
3340
|
+
this.ctx = null;
|
|
3341
|
+
this.frameCount = 0;
|
|
3342
|
+
this.lastFpsUpdate = performance.now();
|
|
3343
|
+
this.lastFps = 0;
|
|
3344
|
+
this.canvas = canvas;
|
|
3345
|
+
this.ctx = canvas.getContext("2d");
|
|
3346
|
+
if (!this.ctx) {
|
|
3347
|
+
throw new Error("Invalid canvas element");
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
render(frame) {
|
|
3351
|
+
const { width, height, data } = frame;
|
|
3352
|
+
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
3353
|
+
this.canvas.width = width;
|
|
3354
|
+
this.canvas.height = height;
|
|
3355
|
+
}
|
|
3356
|
+
const imageData = new ImageData(new Uint8ClampedArray(data), width, height);
|
|
3357
|
+
this.ctx.putImageData(imageData, 0, 0);
|
|
3358
|
+
}
|
|
3359
|
+
updateFps() {
|
|
3360
|
+
this.frameCount++;
|
|
3361
|
+
const now = performance.now();
|
|
3362
|
+
if (now - this.lastFpsUpdate >= 1e3) {
|
|
3363
|
+
this.lastFps = this.frameCount;
|
|
3364
|
+
this.frameCount = 0;
|
|
3365
|
+
this.lastFpsUpdate = now;
|
|
3366
|
+
}
|
|
3367
|
+
return this.lastFps;
|
|
3368
|
+
}
|
|
3369
|
+
getCanvas() {
|
|
3370
|
+
return this.canvas;
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
const DEFAULT_EC_CONFIG = {
|
|
3374
|
+
...DEFAULT_PLAYER_CONFIG
|
|
3375
|
+
};
|
|
3376
|
+
class EcPlayerCore {
|
|
3377
|
+
constructor(config = {}, callbacks = {}) {
|
|
3378
|
+
this.player = null;
|
|
3379
|
+
this.detectedFormat = "unknown";
|
|
3380
|
+
this.pendingRenderer = null;
|
|
3381
|
+
this.renderer = null;
|
|
3382
|
+
this.config = { ...DEFAULT_EC_CONFIG, ...config };
|
|
3383
|
+
this.callbacks = callbacks;
|
|
3384
|
+
if (this.config.canvas) {
|
|
3385
|
+
this.renderer = new Canvas2DRenderer(this.config.canvas);
|
|
3386
|
+
this.pendingRenderer = this.renderer;
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
/**
|
|
3390
|
+
* 加载视频(自动检测格式、初始化解码器)
|
|
3391
|
+
* @param url 视频 URL
|
|
3392
|
+
* @param isLive 是否为直播流(直播流会持续下载数据)
|
|
3393
|
+
*/
|
|
3394
|
+
async load(url, isLive) {
|
|
3395
|
+
this.callbacks.onStateChange?.({ isLoading: true, isLoaded: false });
|
|
3396
|
+
try {
|
|
3397
|
+
this.detectedFormat = FormatDetector.detectFromUrl(url);
|
|
3398
|
+
console.log("[EcPlayerCore] Detected format:", this.detectedFormat, "from URL:", url, "isLive:", isLive);
|
|
3399
|
+
if (isLive !== void 0) {
|
|
3400
|
+
this.config.isLive = isLive;
|
|
3401
|
+
}
|
|
3402
|
+
this.player = this.createPlayer(this.detectedFormat);
|
|
3403
|
+
if (this.pendingRenderer) {
|
|
3404
|
+
this.player.setRenderer(this.pendingRenderer);
|
|
3405
|
+
this.pendingRenderer = null;
|
|
3406
|
+
}
|
|
3407
|
+
await this.player.load(url);
|
|
3408
|
+
await this.player.initDecoder();
|
|
3409
|
+
this.callbacks.onStateChange?.({ isLoaded: true });
|
|
3410
|
+
} finally {
|
|
3411
|
+
this.callbacks.onStateChange?.({ isLoading: false });
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
/**
|
|
3415
|
+
* 根据格式创建播放器
|
|
3416
|
+
*/
|
|
3417
|
+
createPlayer(format) {
|
|
3418
|
+
switch (format) {
|
|
3419
|
+
case "flv":
|
|
3420
|
+
return new FLVPlayer(this.config, this.callbacks);
|
|
3421
|
+
case "hls-ts":
|
|
3422
|
+
case "hls-fmp4":
|
|
3423
|
+
case "unknown":
|
|
3424
|
+
default:
|
|
3425
|
+
return new HLSPlayer(this.config, this.callbacks);
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
// ==================== 公共 API ====================
|
|
3429
|
+
/**
|
|
3430
|
+
* 播放视频
|
|
3431
|
+
* @param options 播放参数
|
|
3432
|
+
*/
|
|
3433
|
+
async play(options) {
|
|
3434
|
+
if (options?.url) {
|
|
3435
|
+
await this.load(options.url, options.isLive);
|
|
3436
|
+
}
|
|
3437
|
+
return this.player?.play();
|
|
3438
|
+
}
|
|
3439
|
+
/**
|
|
3440
|
+
* 暂停播放
|
|
3441
|
+
*/
|
|
3442
|
+
pause() {
|
|
3443
|
+
this.player?.pause();
|
|
3444
|
+
}
|
|
3445
|
+
/**
|
|
3446
|
+
* 跳转到指定时间
|
|
3447
|
+
*/
|
|
3448
|
+
async seek(time) {
|
|
3449
|
+
return this.player?.seek(time);
|
|
3450
|
+
}
|
|
3451
|
+
/**
|
|
3452
|
+
* 获取当前播放时间(毫秒)
|
|
3453
|
+
*/
|
|
3454
|
+
getCurrentTime() {
|
|
3455
|
+
return this.player?.getCurrentTime() ?? 0;
|
|
3456
|
+
}
|
|
3457
|
+
/**
|
|
3458
|
+
* 获取总时长(毫秒)
|
|
3459
|
+
*/
|
|
3460
|
+
getDuration() {
|
|
3461
|
+
return this.player?.getDuration() ?? 0;
|
|
3462
|
+
}
|
|
3463
|
+
/**
|
|
3464
|
+
* 获取播放状态
|
|
3465
|
+
*/
|
|
3466
|
+
getState() {
|
|
3467
|
+
if (!this.player) {
|
|
3468
|
+
return {
|
|
3469
|
+
isPlaying: false,
|
|
3470
|
+
isLoaded: false,
|
|
3471
|
+
isLoading: false,
|
|
3472
|
+
fps: 0,
|
|
3473
|
+
resolution: "-",
|
|
3474
|
+
decoded: 0,
|
|
3475
|
+
downloaded: 0,
|
|
3476
|
+
droppedFrames: 0,
|
|
3477
|
+
isPrefetching: false,
|
|
3478
|
+
segmentIndex: 0,
|
|
3479
|
+
totalSegments: 0,
|
|
3480
|
+
downloadSpeed: 0,
|
|
3481
|
+
currentTime: 0,
|
|
3482
|
+
duration: 0
|
|
3483
|
+
};
|
|
3484
|
+
}
|
|
3485
|
+
return this.player.getState();
|
|
3486
|
+
}
|
|
3487
|
+
/**
|
|
3488
|
+
* 获取检测到的格式
|
|
3489
|
+
*/
|
|
3490
|
+
getDetectedFormat() {
|
|
3491
|
+
return this.detectedFormat;
|
|
3492
|
+
}
|
|
3493
|
+
/**
|
|
3494
|
+
* 销毁播放器,释放资源
|
|
3495
|
+
*/
|
|
3496
|
+
destroy() {
|
|
3497
|
+
console.log("[EcPlayerCore] Destroying player...");
|
|
3498
|
+
if (this.player) {
|
|
3499
|
+
this.player.destroy();
|
|
3500
|
+
this.player = null;
|
|
3501
|
+
}
|
|
3502
|
+
this.pendingRenderer = null;
|
|
3503
|
+
this.renderer = null;
|
|
3504
|
+
this.detectedFormat = "unknown";
|
|
3505
|
+
console.log("[EcPlayerCore] Player destroyed");
|
|
3506
|
+
}
|
|
3507
|
+
/**
|
|
3508
|
+
* 获取内部播放器实例
|
|
3509
|
+
* 用于需要访问特定播放器功能的场景
|
|
3510
|
+
*/
|
|
3511
|
+
getPlayer() {
|
|
3512
|
+
return this.player;
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
const _EnvDetector = class _EnvDetector {
|
|
3516
|
+
/**
|
|
3517
|
+
* 检测当前环境(带缓存)
|
|
3518
|
+
*/
|
|
3519
|
+
static detect() {
|
|
3520
|
+
if (this.cache) {
|
|
3521
|
+
return this.cache;
|
|
3522
|
+
}
|
|
3523
|
+
const ua = this.getUserAgent();
|
|
3524
|
+
const platform = this.detectPlatform(ua);
|
|
3525
|
+
const isMiniProgram = this.isMiniProgramEnv(ua, platform);
|
|
3526
|
+
const isWeChat = platform === "wechat-miniprogram";
|
|
3527
|
+
const isMobile = this.isMobileDevice(ua);
|
|
3528
|
+
const capabilities = this.detectCapabilities(isMiniProgram);
|
|
3529
|
+
this.cache = {
|
|
3530
|
+
platform,
|
|
3531
|
+
envType: isMiniProgram ? "miniprogram" : "browser",
|
|
3532
|
+
isMiniProgram,
|
|
3533
|
+
isWeChat,
|
|
3534
|
+
isMobile,
|
|
3535
|
+
userAgent: ua,
|
|
3536
|
+
capabilities
|
|
3537
|
+
};
|
|
3538
|
+
return this.cache;
|
|
3539
|
+
}
|
|
3540
|
+
/**
|
|
3541
|
+
* 获取 User Agent
|
|
3542
|
+
*/
|
|
3543
|
+
static getUserAgent() {
|
|
3544
|
+
if (typeof navigator !== "undefined" && navigator.userAgent) {
|
|
3545
|
+
return navigator.userAgent;
|
|
3546
|
+
}
|
|
3547
|
+
return "";
|
|
3548
|
+
}
|
|
3549
|
+
/**
|
|
3550
|
+
* 检测平台类型
|
|
3551
|
+
*/
|
|
3552
|
+
static detectPlatform(ua) {
|
|
3553
|
+
const uaLower = ua.toLowerCase();
|
|
3554
|
+
if (this.checkIsWeChatMiniProgram(uaLower)) {
|
|
3555
|
+
return "wechat-miniprogram";
|
|
3556
|
+
}
|
|
3557
|
+
if (uaLower.includes("alipay") && uaLower.includes("miniprogram")) {
|
|
3558
|
+
return "alipay-miniprogram";
|
|
3559
|
+
}
|
|
3560
|
+
if (uaLower.includes("swan-baiduboxapp")) {
|
|
3561
|
+
return "other-miniprogram";
|
|
3562
|
+
}
|
|
3563
|
+
if (uaLower.includes("toutiaomicroapp")) {
|
|
3564
|
+
return "other-miniprogram";
|
|
3565
|
+
}
|
|
3566
|
+
if (uaLower.includes("qqminiapp")) {
|
|
3567
|
+
return "other-miniprogram";
|
|
3568
|
+
}
|
|
3569
|
+
if (this.isMobileDevice(ua)) {
|
|
3570
|
+
return "mobile-browser";
|
|
3571
|
+
}
|
|
3572
|
+
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
3573
|
+
return "pc-browser";
|
|
3574
|
+
}
|
|
3575
|
+
return "unknown";
|
|
3576
|
+
}
|
|
3577
|
+
/**
|
|
3578
|
+
* 检测微信小程序环境(内部方法)
|
|
3579
|
+
*/
|
|
3580
|
+
static checkIsWeChatMiniProgram(uaLower) {
|
|
3581
|
+
if (typeof globalThis !== "undefined") {
|
|
3582
|
+
const g = globalThis;
|
|
3583
|
+
if (g.__wxjs_environment === "miniprogram") {
|
|
3584
|
+
return true;
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
if (typeof window !== "undefined") {
|
|
3588
|
+
const win = window;
|
|
3589
|
+
if (win.__wxjs_environment === "miniprogram") {
|
|
3590
|
+
return true;
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
if (uaLower.includes("miniprogram")) {
|
|
3594
|
+
if (uaLower.includes("micromessenger") || uaLower.includes("wechat")) {
|
|
3595
|
+
return true;
|
|
3596
|
+
}
|
|
3597
|
+
if (!uaLower.includes("alipay") && !uaLower.includes("baidu") && !uaLower.includes("toutiao")) {
|
|
3598
|
+
return true;
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
if (uaLower.includes("miniprogramwebview")) {
|
|
3602
|
+
return true;
|
|
3603
|
+
}
|
|
3604
|
+
return false;
|
|
3605
|
+
}
|
|
3606
|
+
/**
|
|
3607
|
+
* 判断是否为小程序环境
|
|
3608
|
+
*/
|
|
3609
|
+
static isMiniProgramEnv(ua, platform) {
|
|
3610
|
+
if (platform === "wechat-miniprogram" || platform === "alipay-miniprogram" || platform === "other-miniprogram") {
|
|
3611
|
+
return true;
|
|
3612
|
+
}
|
|
3613
|
+
return false;
|
|
3614
|
+
}
|
|
3615
|
+
/**
|
|
3616
|
+
* 检测是否为移动设备
|
|
3617
|
+
*/
|
|
3618
|
+
static isMobileDevice(ua) {
|
|
3619
|
+
const uaLower = ua.toLowerCase();
|
|
3620
|
+
const mobileKeywords = [
|
|
3621
|
+
"android",
|
|
3622
|
+
"webos",
|
|
3623
|
+
"iphone",
|
|
3624
|
+
"ipad",
|
|
3625
|
+
"ipod",
|
|
3626
|
+
"blackberry",
|
|
3627
|
+
"windows phone",
|
|
3628
|
+
"mobile"
|
|
3629
|
+
];
|
|
3630
|
+
return mobileKeywords.some((keyword) => uaLower.includes(keyword));
|
|
3631
|
+
}
|
|
3632
|
+
/**
|
|
3633
|
+
* 检测环境能力
|
|
3634
|
+
*/
|
|
3635
|
+
static detectCapabilities(isMiniProgram) {
|
|
3636
|
+
const capabilities = {
|
|
3637
|
+
sharedArrayBuffer: false,
|
|
3638
|
+
wasmThreads: false,
|
|
3639
|
+
wasmSimd: false,
|
|
3640
|
+
webGL: false,
|
|
3641
|
+
webGL2: false,
|
|
3642
|
+
videoFrame: false,
|
|
3643
|
+
offscreenCanvas: false,
|
|
3644
|
+
webAudio: false,
|
|
3645
|
+
webWorker: false,
|
|
3646
|
+
fetch: false,
|
|
3647
|
+
webSocket: false,
|
|
3648
|
+
readableStream: false
|
|
3649
|
+
};
|
|
3650
|
+
capabilities.sharedArrayBuffer = this.checkSharedArrayBuffer();
|
|
3651
|
+
if (isMiniProgram) {
|
|
3652
|
+
capabilities.wasmThreads = false;
|
|
3653
|
+
} else {
|
|
3654
|
+
capabilities.wasmThreads = capabilities.sharedArrayBuffer;
|
|
3655
|
+
}
|
|
3656
|
+
capabilities.wasmSimd = this.checkWasmSimd();
|
|
3657
|
+
capabilities.webGL = this.checkWebGL();
|
|
3658
|
+
capabilities.webGL2 = this.checkWebGL2();
|
|
3659
|
+
capabilities.videoFrame = typeof VideoFrame !== "undefined";
|
|
3660
|
+
capabilities.offscreenCanvas = typeof OffscreenCanvas !== "undefined";
|
|
3661
|
+
capabilities.webAudio = typeof AudioContext !== "undefined" || typeof window !== "undefined" && "webkitAudioContext" in window;
|
|
3662
|
+
capabilities.webWorker = typeof Worker !== "undefined";
|
|
3663
|
+
capabilities.fetch = typeof fetch !== "undefined";
|
|
3664
|
+
capabilities.webSocket = typeof WebSocket !== "undefined";
|
|
3665
|
+
capabilities.readableStream = typeof ReadableStream !== "undefined";
|
|
3666
|
+
return capabilities;
|
|
3667
|
+
}
|
|
3668
|
+
/**
|
|
3669
|
+
* 检测 SharedArrayBuffer
|
|
3670
|
+
*/
|
|
3671
|
+
static checkSharedArrayBuffer() {
|
|
3672
|
+
if (typeof window !== "undefined" && window.crossOriginIsolated === false) {
|
|
3673
|
+
return false;
|
|
3674
|
+
}
|
|
3675
|
+
if (typeof SharedArrayBuffer === "undefined") {
|
|
3676
|
+
return false;
|
|
3677
|
+
}
|
|
3678
|
+
try {
|
|
3679
|
+
new SharedArrayBuffer(1);
|
|
3680
|
+
return true;
|
|
3681
|
+
} catch {
|
|
3682
|
+
return false;
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
/**
|
|
3686
|
+
* 检测 WASM SIMD 支持
|
|
3687
|
+
*/
|
|
3688
|
+
static checkWasmSimd() {
|
|
3689
|
+
const simdTestBytes = new Uint8Array([
|
|
3690
|
+
0,
|
|
3691
|
+
97,
|
|
3692
|
+
115,
|
|
3693
|
+
109,
|
|
3694
|
+
// magic: \0asm
|
|
3695
|
+
1,
|
|
3696
|
+
0,
|
|
3697
|
+
0,
|
|
3698
|
+
0,
|
|
3699
|
+
// version: 1
|
|
3700
|
+
// Type section (1)
|
|
3701
|
+
1,
|
|
3702
|
+
5,
|
|
3703
|
+
1,
|
|
3704
|
+
// section id=1, size=5, 1 type
|
|
3705
|
+
96,
|
|
3706
|
+
0,
|
|
3707
|
+
1,
|
|
3708
|
+
123,
|
|
3709
|
+
// func () -> v128
|
|
3710
|
+
// Function section (3)
|
|
3711
|
+
3,
|
|
3712
|
+
2,
|
|
3713
|
+
1,
|
|
3714
|
+
// section id=3, size=2, 1 func
|
|
3715
|
+
0,
|
|
3716
|
+
// function 0 uses type 0
|
|
3717
|
+
// Export section (7)
|
|
3718
|
+
7,
|
|
3719
|
+
8,
|
|
3720
|
+
1,
|
|
3721
|
+
// section id=7, size=8, 1 export
|
|
3722
|
+
4,
|
|
3723
|
+
116,
|
|
3724
|
+
101,
|
|
3725
|
+
115,
|
|
3726
|
+
116,
|
|
3727
|
+
// name: "test"
|
|
3728
|
+
0,
|
|
3729
|
+
// export kind: function
|
|
3730
|
+
0,
|
|
3731
|
+
// function index: 0
|
|
3732
|
+
// Code section (10)
|
|
3733
|
+
10,
|
|
3734
|
+
19,
|
|
3735
|
+
1,
|
|
3736
|
+
// section id=10, size=19, 1 func
|
|
3737
|
+
17,
|
|
3738
|
+
0,
|
|
3739
|
+
// func body size=17, 0 locals
|
|
3740
|
+
// v128.const i32x4 0 0 0 0
|
|
3741
|
+
253,
|
|
3742
|
+
12,
|
|
3743
|
+
// v128.const
|
|
3744
|
+
0,
|
|
3745
|
+
// i32x4 lane format
|
|
3746
|
+
0,
|
|
3747
|
+
0,
|
|
3748
|
+
0,
|
|
3749
|
+
0,
|
|
3750
|
+
// lane 0
|
|
3751
|
+
0,
|
|
3752
|
+
0,
|
|
3753
|
+
0,
|
|
3754
|
+
0,
|
|
3755
|
+
// lane 1
|
|
3756
|
+
0,
|
|
3757
|
+
0,
|
|
3758
|
+
0,
|
|
3759
|
+
0,
|
|
3760
|
+
// lane 2
|
|
3761
|
+
0,
|
|
3762
|
+
0,
|
|
3763
|
+
0,
|
|
3764
|
+
0,
|
|
3765
|
+
// lane 3
|
|
3766
|
+
11
|
|
3767
|
+
// end
|
|
3768
|
+
]);
|
|
3769
|
+
try {
|
|
3770
|
+
return WebAssembly.validate(simdTestBytes);
|
|
3771
|
+
} catch {
|
|
3772
|
+
return false;
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
/**
|
|
3776
|
+
* 检测 WebGL 支持
|
|
3777
|
+
*/
|
|
3778
|
+
static checkWebGL() {
|
|
3779
|
+
if (typeof document === "undefined") {
|
|
3780
|
+
return false;
|
|
3781
|
+
}
|
|
3782
|
+
try {
|
|
3783
|
+
const canvas = document.createElement("canvas");
|
|
3784
|
+
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
3785
|
+
return !!gl;
|
|
3786
|
+
} catch {
|
|
3787
|
+
return false;
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
/**
|
|
3791
|
+
* 检测 WebGL 2 支持
|
|
3792
|
+
*/
|
|
3793
|
+
static checkWebGL2() {
|
|
3794
|
+
if (typeof document === "undefined") {
|
|
3795
|
+
return false;
|
|
3796
|
+
}
|
|
3797
|
+
try {
|
|
3798
|
+
const canvas = document.createElement("canvas");
|
|
3799
|
+
const gl = canvas.getContext("webgl2");
|
|
3800
|
+
return !!gl;
|
|
3801
|
+
} catch {
|
|
3802
|
+
return false;
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
/**
|
|
3806
|
+
* 清除缓存(用于测试)
|
|
3807
|
+
*/
|
|
3808
|
+
static resetCache() {
|
|
3809
|
+
this.cache = null;
|
|
3810
|
+
}
|
|
3811
|
+
/**
|
|
3812
|
+
* 快速判断是否为小程序环境
|
|
3813
|
+
*/
|
|
3814
|
+
static isMiniProgram() {
|
|
3815
|
+
return this.detect().isMiniProgram;
|
|
3816
|
+
}
|
|
3817
|
+
/**
|
|
3818
|
+
* 快速判断是否为微信小程序
|
|
3819
|
+
*/
|
|
3820
|
+
static isWeChatMiniProgram() {
|
|
3821
|
+
return this.detect().isWeChat;
|
|
3822
|
+
}
|
|
3823
|
+
/**
|
|
3824
|
+
* 快速获取平台类型
|
|
3825
|
+
*/
|
|
3826
|
+
static getPlatform() {
|
|
3827
|
+
return this.detect().platform;
|
|
3828
|
+
}
|
|
3829
|
+
/**
|
|
3830
|
+
* 快速获取环境能力
|
|
3831
|
+
*/
|
|
3832
|
+
static getCapabilities() {
|
|
3833
|
+
return this.detect().capabilities;
|
|
3834
|
+
}
|
|
3835
|
+
/**
|
|
3836
|
+
* 打印环境信息(调试用)
|
|
3837
|
+
*/
|
|
3838
|
+
static printEnvInfo() {
|
|
3839
|
+
const env = this.detect();
|
|
3840
|
+
console.log("=== 环境检测结果 ===");
|
|
3841
|
+
console.log("平台:", env.platform);
|
|
3842
|
+
console.log("环境类型:", env.envType);
|
|
3843
|
+
console.log("是否小程序:", env.isMiniProgram);
|
|
3844
|
+
console.log("是否微信:", env.isWeChat);
|
|
3845
|
+
console.log("是否移动端:", env.isMobile);
|
|
3846
|
+
console.log("能力检测:", env.capabilities);
|
|
3847
|
+
console.log("UA:", env.userAgent);
|
|
3848
|
+
console.log("==================");
|
|
3849
|
+
}
|
|
3850
|
+
};
|
|
3851
|
+
_EnvDetector.cache = null;
|
|
3852
|
+
let EnvDetector = _EnvDetector;
|
|
3853
|
+
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
|
3854
|
+
LogLevel2[LogLevel2["NONE"] = 0] = "NONE";
|
|
3855
|
+
LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
|
|
3856
|
+
LogLevel2[LogLevel2["WARN"] = 2] = "WARN";
|
|
3857
|
+
LogLevel2[LogLevel2["INFO"] = 3] = "INFO";
|
|
3858
|
+
LogLevel2[LogLevel2["DEBUG"] = 4] = "DEBUG";
|
|
3859
|
+
return LogLevel2;
|
|
3860
|
+
})(LogLevel || {});
|
|
3861
|
+
function setLogLevel(level) {
|
|
3862
|
+
}
|
|
3863
|
+
export {
|
|
3864
|
+
EcPlayerCore,
|
|
3865
|
+
EnvDetector,
|
|
3866
|
+
LogLevel,
|
|
3867
|
+
setLogLevel
|
|
3868
|
+
};
|
|
3869
|
+
//# sourceMappingURL=index.js.map
|