@file-ud.js/plugins 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,773 @@
1
+ import { FileUDError, ErrorCode, formatFileSize, validator } from '@file-ud.js/core';
2
+
3
+ // src/uploader/compress/compress-image-plugin.ts
4
+ var BasePlugin = class {
5
+ constructor() {
6
+ /** 插件版本 */
7
+ this.version = "1.0.0";
8
+ /** 插件优先级 */
9
+ this.priority = 0;
10
+ /** 是否关键插件 */
11
+ this.critical = false;
12
+ /** 插件描述 */
13
+ this.desc = "";
14
+ }
15
+ /**
16
+ * 安装插件 - 统一日志输出
17
+ */
18
+ install(transfer) {
19
+ console.log(`${this.name} \u2705 \u63D2\u4EF6\u5DF2\u5B89\u88C5`);
20
+ }
21
+ /**
22
+ * 创建基础错误对象
23
+ * @param file - 当前文件
24
+ * @param uploader - 上传器实例
25
+ * @returns 基础错误对象
26
+ */
27
+ createBaseError(file, uploader) {
28
+ return new FileUDError(
29
+ ErrorCode.UNKNOWN,
30
+ "",
31
+ {
32
+ uploader,
33
+ plugin: this.name,
34
+ fileName: file.fileName,
35
+ fileSize: file.File.size,
36
+ fileType: file.File.type
37
+ }
38
+ );
39
+ }
40
+ /**
41
+ * 抛出验证错误
42
+ * @param error - 错误对象
43
+ * @returns Promise.reject
44
+ */
45
+ throwError(error) {
46
+ return Promise.reject(error);
47
+ }
48
+ /**
49
+ * 销毁插件
50
+ */
51
+ destroy() {
52
+ console.log(`${this.name} \u{1F512} \u63D2\u4EF6\u5DF2\u9500\u6BC1`);
53
+ }
54
+ };
55
+
56
+ // src/uploader/compress/compress-image-plugin.ts
57
+ var CompressImagePlugin = class extends BasePlugin {
58
+ constructor(options = {}) {
59
+ super();
60
+ this.name = "compress-image-plugin";
61
+ this.priority = 10;
62
+ this.desc = "\u56FE\u7247\u538B\u7F29\u63D2\u4EF6";
63
+ this.defaultOptions = {
64
+ quality: 0.8,
65
+ maxWidth: 1920,
66
+ maxHeight: 1080,
67
+ format: "jpeg",
68
+ showInfo: true,
69
+ onCompressStart: function(file) {
70
+ throw new Error("Function not implemented.");
71
+ },
72
+ onCompressComplete: function(file, compressedFile) {
73
+ throw new Error("Function not implemented.");
74
+ }
75
+ };
76
+ this.options = { ...this.defaultOptions, ...options };
77
+ }
78
+ async onFileSelect(uploadFile, context) {
79
+ if (!uploadFile.File.type.startsWith("image/")) {
80
+ return uploadFile;
81
+ }
82
+ const originalSize = uploadFile.File.size;
83
+ try {
84
+ this.options.onCompressStart?.(uploadFile);
85
+ const compressedFile = await this.compressImage(uploadFile.File);
86
+ const compressedSize = compressedFile.size;
87
+ const ratio = ((originalSize - compressedSize) / originalSize * 100).toFixed(1);
88
+ if (this.options.showInfo) {
89
+ console.log(
90
+ `\u{1F4F8} \u538B\u7F29\u5B8C\u6210: ${uploadFile.fileName} | ${formatFileSize(originalSize)} \u2192 ${formatFileSize(compressedSize)} (\u51CF\u5C11 ${ratio}%)`
91
+ );
92
+ }
93
+ uploadFile.proxy.File = compressedFile;
94
+ this.options.onCompressComplete?.(uploadFile, compressedFile);
95
+ uploadFile.proxy.formatSize = formatFileSize(compressedSize);
96
+ return uploadFile;
97
+ } catch (error) {
98
+ console.error(`\u538B\u7F29\u5931\u8D25: ${uploadFile.fileName}`, error);
99
+ return uploadFile;
100
+ }
101
+ }
102
+ compressImage(file) {
103
+ return new Promise((resolve, reject) => {
104
+ const img = new Image();
105
+ const url = URL.createObjectURL(file);
106
+ img.onload = () => {
107
+ URL.revokeObjectURL(url);
108
+ let { width, height } = img;
109
+ if (width > this.options.maxWidth) {
110
+ height = Math.floor(height * (this.options.maxWidth / width));
111
+ width = this.options.maxWidth;
112
+ }
113
+ if (height > this.options.maxHeight) {
114
+ width = Math.floor(width * (this.options.maxHeight / height));
115
+ height = this.options.maxHeight;
116
+ }
117
+ const canvas = document.createElement("canvas");
118
+ canvas.width = width;
119
+ canvas.height = height;
120
+ const ctx = canvas.getContext("2d");
121
+ if (!ctx) {
122
+ reject(new Error("\u65E0\u6CD5\u521B\u5EFA Canvas \u4E0A\u4E0B\u6587"));
123
+ return;
124
+ }
125
+ ctx.drawImage(img, 0, 0, width, height);
126
+ canvas.toBlob(
127
+ (blob) => {
128
+ if (blob) {
129
+ const compressedFile = new File(
130
+ [blob],
131
+ file.name.replace(/\.\w+$/, `.${this.options.format}`),
132
+ { type: `image/${this.options.format}` }
133
+ );
134
+ resolve(compressedFile);
135
+ } else {
136
+ reject(new Error("\u538B\u7F29\u5931\u8D25"));
137
+ }
138
+ },
139
+ `image/${this.options.format}`,
140
+ this.options.quality
141
+ );
142
+ };
143
+ img.onerror = () => reject(new Error("\u56FE\u7247\u52A0\u8F7D\u5931\u8D25"));
144
+ img.src = url;
145
+ });
146
+ }
147
+ };
148
+ var WatermarkPlugin = class extends BasePlugin {
149
+ constructor(options = {}) {
150
+ super();
151
+ this.name = "watermark-plugin";
152
+ this.priority = 20;
153
+ this.desc = "\u6C34\u5370\u63D2\u4EF6\uFF08\u652F\u6301\u6587\u5B57/\u56FE\u7247\u6C34\u5370\uFF09";
154
+ this.defaultOptions = {
155
+ text: "\xA9 FileUD",
156
+ imageUrl: "",
157
+ position: "bottom-right",
158
+ opacity: 0.6,
159
+ fontSize: 24,
160
+ color: "#ffffff",
161
+ padding: 20,
162
+ imageWidth: 100,
163
+ imageHeight: 100
164
+ };
165
+ this.options = { ...this.defaultOptions, ...options };
166
+ }
167
+ async onFileSelect(file, context) {
168
+ if (!file.File.type.startsWith("image/")) {
169
+ return file;
170
+ }
171
+ try {
172
+ const watermarkedFile = await this.addWatermark(file.File);
173
+ file.proxy.File = watermarkedFile;
174
+ file.proxy.formatSize = formatFileSize(watermarkedFile.size);
175
+ console.log(`\u{1F4A7} \u6C34\u5370\u5DF2\u6DFB\u52A0: ${file.fileName}`);
176
+ return file;
177
+ } catch (error) {
178
+ console.error(`\u6C34\u5370\u6DFB\u52A0\u5931\u8D25: ${file.fileName}`, error);
179
+ return file;
180
+ }
181
+ }
182
+ addWatermark(file) {
183
+ return new Promise((resolve, reject) => {
184
+ const img = new Image();
185
+ const url = URL.createObjectURL(file);
186
+ img.onload = () => {
187
+ URL.revokeObjectURL(url);
188
+ const canvas = document.createElement("canvas");
189
+ canvas.width = img.width;
190
+ canvas.height = img.height;
191
+ const ctx = canvas.getContext("2d");
192
+ if (!ctx) {
193
+ reject(new Error("\u65E0\u6CD5\u521B\u5EFA Canvas \u4E0A\u4E0B\u6587"));
194
+ return;
195
+ }
196
+ ctx.drawImage(img, 0, 0);
197
+ ctx.globalAlpha = this.options.opacity;
198
+ if (this.options.imageUrl) {
199
+ this.addImageWatermark(ctx, canvas, resolve, reject, file);
200
+ } else if (this.options.text) {
201
+ this.addTextWatermark(ctx, canvas, resolve, reject, file);
202
+ }
203
+ };
204
+ img.onerror = () => reject(new Error("\u56FE\u7247\u52A0\u8F7D\u5931\u8D25"));
205
+ img.src = url;
206
+ });
207
+ }
208
+ /**
209
+ * 添加文字水印
210
+ */
211
+ addTextWatermark(ctx, canvas, resolve, reject, originalFile) {
212
+ ctx.font = `${this.options.fontSize}px Arial`;
213
+ ctx.fillStyle = this.options.color;
214
+ ctx.textAlign = "center";
215
+ ctx.textBaseline = "middle";
216
+ const text = this.options.text;
217
+ const textWidth = ctx.measureText(text).width;
218
+ const padding = this.options.padding;
219
+ const { x, y } = this.calculatePosition(
220
+ ctx,
221
+ canvas,
222
+ textWidth,
223
+ this.options.fontSize,
224
+ padding
225
+ );
226
+ ctx.fillText(text, x, y);
227
+ canvas.toBlob((blob) => {
228
+ if (blob) {
229
+ const watermarkedFile = new File([blob], originalFile.name, {
230
+ type: originalFile.type
231
+ });
232
+ resolve(watermarkedFile);
233
+ } else {
234
+ reject(new Error("\u6C34\u5370\u6DFB\u52A0\u5931\u8D25"));
235
+ }
236
+ }, originalFile.type);
237
+ }
238
+ /**
239
+ * 添加图片水印
240
+ */
241
+ addImageWatermark(ctx, canvas, resolve, reject, originalFile) {
242
+ const watermarkImg = new Image();
243
+ watermarkImg.crossOrigin = "Anonymous";
244
+ watermarkImg.onload = () => {
245
+ let drawWidth = this.options.imageWidth;
246
+ let drawHeight = this.options.imageHeight;
247
+ if (!drawWidth && !drawHeight) {
248
+ drawWidth = watermarkImg.width;
249
+ drawHeight = watermarkImg.height;
250
+ } else if (drawWidth && !drawHeight) {
251
+ drawHeight = watermarkImg.height / watermarkImg.width * drawWidth;
252
+ } else if (!drawWidth && drawHeight) {
253
+ drawWidth = watermarkImg.width / watermarkImg.height * drawHeight;
254
+ }
255
+ const padding = this.options.padding;
256
+ const { x, y } = this.calculatePosition(
257
+ ctx,
258
+ canvas,
259
+ drawWidth,
260
+ drawHeight,
261
+ padding
262
+ );
263
+ ctx.drawImage(watermarkImg, x, y, drawWidth, drawHeight);
264
+ canvas.toBlob((blob) => {
265
+ if (blob) {
266
+ const watermarkedFile = new File([blob], originalFile.name, {
267
+ type: originalFile.type
268
+ });
269
+ resolve(watermarkedFile);
270
+ } else {
271
+ reject(new Error("\u6C34\u5370\u6DFB\u52A0\u5931\u8D25"));
272
+ }
273
+ }, originalFile.type);
274
+ };
275
+ watermarkImg.onerror = () => {
276
+ reject(new Error("\u6C34\u5370\u56FE\u7247\u52A0\u8F7D\u5931\u8D25"));
277
+ };
278
+ watermarkImg.src = this.options.imageUrl;
279
+ }
280
+ /**
281
+ * 计算水印位置
282
+ */
283
+ calculatePosition(ctx, canvas, width, height, padding) {
284
+ const pad = padding ?? this.options.padding;
285
+ let x, y;
286
+ switch (this.options.position) {
287
+ case "top-left":
288
+ x = pad;
289
+ y = pad;
290
+ break;
291
+ case "top-right":
292
+ x = canvas.width - width - pad;
293
+ y = pad;
294
+ break;
295
+ case "bottom-left":
296
+ x = pad;
297
+ y = canvas.height - height - pad;
298
+ break;
299
+ case "bottom-right":
300
+ x = canvas.width - width - pad;
301
+ y = canvas.height - height - pad;
302
+ break;
303
+ default:
304
+ x = (canvas.width - width) / 2;
305
+ y = (canvas.height - height) / 2;
306
+ }
307
+ return { x, y };
308
+ }
309
+ };
310
+ var FileValidatorPlugin = class extends BasePlugin {
311
+ constructor(options = {}) {
312
+ super();
313
+ this.name = "file-validator-plugin";
314
+ this.priority = 0;
315
+ // 最先执行
316
+ this.critical = true;
317
+ this.desc = "\u57FA\u7840\u9A8C\u8BC1\u63D2\u4EF6\uFF08\u5927\u5C0F\u3001\u7C7B\u578B\u3001\u6570\u91CF\uFF09";
318
+ this.defaultOptions = {
319
+ maxSize: Infinity,
320
+ minSize: 0,
321
+ accept: [],
322
+ allowEmpty: false,
323
+ customValidate: async () => true,
324
+ messages: {}
325
+ };
326
+ this.options = { ...this.defaultOptions, ...options };
327
+ }
328
+ async created(uploader) {
329
+ uploader.inputHTML?.setAttribute("accept", this.options.accept.join(","));
330
+ }
331
+ async onFileSelect(file, context) {
332
+ const uploader = context.transfer;
333
+ const baseError = this.createBaseError(file, uploader);
334
+ if (!this.options.allowEmpty && file.File.size === 0) {
335
+ const msg = this.options.messages.empty?.() || `\u6587\u4EF6 ${file.fileName} \u662F\u7A7A\u6587\u4EF6`;
336
+ return this.throwError(
337
+ baseError.setCode(ErrorCode.FILE_EMPTY).setMessage(msg)
338
+ );
339
+ }
340
+ if (file.File.size < this.options.minSize) {
341
+ const msg = this.options.messages.minSize?.(this.options.minSize, file.File.size) || `\u6587\u4EF6 ${file.fileName} \u592A\u5C0F\uFF0C\u6700\u5C0F ${formatFileSize(this.options.minSize)}`;
342
+ return this.throwError(
343
+ baseError.setCode(ErrorCode.FILE_TOO_SMALL).setMessage(msg).setContext({
344
+ minSize: this.options.minSize,
345
+ currentSize: file.File.size
346
+ })
347
+ );
348
+ }
349
+ if (file.File.size > this.options.maxSize) {
350
+ const msg = this.options.messages.maxSize?.(this.options.maxSize, file.File.size) || `\u6587\u4EF6 ${file.fileName} \u592A\u5927\uFF0C\u6700\u5927 ${formatFileSize(this.options.maxSize)}`;
351
+ return this.throwError(
352
+ baseError.setCode(ErrorCode.FILE_TOO_LARGE).setMessage(msg).setContext({
353
+ maxSize: this.options.maxSize,
354
+ currentSize: file.File.size
355
+ })
356
+ );
357
+ }
358
+ if (!validator.type(this.options.accept, file)) {
359
+ const msg = this.options.messages.accept?.(this.options.accept, file.fileName) || `\u6587\u4EF6 ${file.fileName} \u7C7B\u578B\u4E0D\u652F\u6301\uFF0C\u5141\u8BB8\uFF1A${this.options.accept.join(", ")}`;
360
+ return this.throwError(
361
+ baseError.setCode(ErrorCode.INVALID_TYPE).setMessage(msg)
362
+ );
363
+ }
364
+ try {
365
+ const customResult = await this.options.customValidate(
366
+ file.File,
367
+ FileUDError
368
+ );
369
+ if (!customResult) {
370
+ const msg = this.options.messages.custom?.(file.fileName) || `\u6587\u4EF6 ${file.fileName} \u9A8C\u8BC1\u5931\u8D25`;
371
+ return this.throwError(
372
+ baseError.setCode(ErrorCode.FILE_CORRUPTED).setMessage(msg)
373
+ );
374
+ }
375
+ } catch (error) {
376
+ console.log("\u{1F680} ~ FileValidatorPlugin ~ onFileSelect ~ error:", error);
377
+ return this.throwError(
378
+ baseError.setCode(ErrorCode.PLUGIN_ERROR).setMessage("\u6587\u4EF6\u9A8C\u8BC1\u8FC7\u7A0B\u51FA\u9519").setContext({
379
+ originalError: error
380
+ })
381
+ );
382
+ }
383
+ return file;
384
+ }
385
+ };
386
+ var ImageValidatorPlugin = class extends BasePlugin {
387
+ constructor(options = {}) {
388
+ super();
389
+ this.options = options;
390
+ this.name = "image-validator-plugin";
391
+ this.priority = 0;
392
+ this.desc = "\u56FE\u7247\u9A8C\u8BC1\u63D2\u4EF6\uFF08\u5C3A\u5BF8\u3001\u6BD4\u4F8B\u3001\u5206\u8FA8\u7387\uFF09";
393
+ }
394
+ async onFileSelect(file, context) {
395
+ if (!file.File.type.startsWith("image/")) {
396
+ return file;
397
+ }
398
+ const dimensions = await this.getImageDimensions(file.File);
399
+ const baseError = this.createBaseError(file, context.transfer);
400
+ if (this.options.exactWidth && dimensions.width !== this.options.exactWidth) {
401
+ return this.throwError(
402
+ baseError.setCode(ErrorCode.IMAGE_WIDTH_INVALID).setMessage(
403
+ `\u5BBD\u5EA6\u5FC5\u987B\u4E3A ${this.options.exactWidth}px\uFF0C\u5F53\u524D ${dimensions.width}px`
404
+ ).setContext({ width: dimensions.width })
405
+ );
406
+ }
407
+ if (this.options.minWidth && dimensions.width < this.options.minWidth) {
408
+ return this.throwError(
409
+ baseError.setCode(ErrorCode.IMAGE_WIDTH_INVALID).setMessage(
410
+ `\u5BBD\u5EA6\u4E0D\u80FD\u5C0F\u4E8E ${this.options.minWidth}px\uFF0C\u5F53\u524D ${dimensions.width}px`
411
+ ).setContext({ width: dimensions.width })
412
+ );
413
+ }
414
+ if (this.options.maxWidth && dimensions.width > this.options.maxWidth) {
415
+ return this.throwError(
416
+ baseError.setCode(ErrorCode.IMAGE_WIDTH_INVALID).setMessage(
417
+ `\u5BBD\u5EA6\u4E0D\u80FD\u5927\u4E8E ${this.options.maxWidth}px\uFF0C\u5F53\u524D ${dimensions.width}px`
418
+ ).setContext({ width: dimensions.width })
419
+ );
420
+ }
421
+ if (this.options.exactHeight && dimensions.height !== this.options.exactHeight) {
422
+ return this.throwError(
423
+ baseError.setCode(ErrorCode.IMAGE_HEIGHT_INVALID).setMessage(
424
+ `\u9AD8\u5EA6\u5FC5\u987B\u4E3A ${this.options.exactHeight}px\uFF0C\u5F53\u524D ${dimensions.height}px`
425
+ ).setContext({ height: dimensions.height })
426
+ );
427
+ }
428
+ if (this.options.minHeight && dimensions.height < this.options.minHeight) {
429
+ return this.throwError(
430
+ baseError.setCode(ErrorCode.IMAGE_HEIGHT_INVALID).setMessage(
431
+ `\u9AD8\u5EA6\u4E0D\u80FD\u5C0F\u4E8E ${this.options.minHeight}px\uFF0C\u5F53\u524D ${dimensions.height}px`
432
+ ).setContext({ height: dimensions.height })
433
+ );
434
+ }
435
+ if (this.options.maxHeight && dimensions.height > this.options.maxHeight) {
436
+ return this.throwError(
437
+ baseError.setCode(ErrorCode.IMAGE_HEIGHT_INVALID).setMessage(
438
+ `\u9AD8\u5EA6\u4E0D\u80FD\u5927\u4E8E ${this.options.maxHeight}px\uFF0C\u5F53\u524D ${dimensions.height}px`
439
+ ).setContext({ height: dimensions.height })
440
+ );
441
+ }
442
+ if (this.options.square && dimensions.width !== dimensions.height) {
443
+ return this.throwError(
444
+ baseError.setCode(ErrorCode.IMAGE_NOT_SQUARE).setMessage(
445
+ `\u56FE\u7247\u5FC5\u987B\u662F\u6B63\u65B9\u5F62\uFF0C\u5F53\u524D ${dimensions.width}x${dimensions.height}`
446
+ ).setContext({
447
+ width: dimensions.width,
448
+ height: dimensions.height
449
+ })
450
+ );
451
+ }
452
+ const aspectRatio = dimensions.width / dimensions.height;
453
+ if (this.options.minAspectRatio && aspectRatio < this.options.minAspectRatio) {
454
+ return this.throwError(
455
+ baseError.setCode(ErrorCode.IMAGE_ASPECT_RATIO_INVALID).setMessage(
456
+ `\u5BBD\u9AD8\u6BD4\u4E0D\u80FD\u5C0F\u4E8E ${this.options.minAspectRatio}\uFF0C\u5F53\u524D ${aspectRatio.toFixed(2)}`
457
+ ).setContext({ aspectRatio })
458
+ );
459
+ }
460
+ if (this.options.maxAspectRatio && aspectRatio > this.options.maxAspectRatio) {
461
+ return this.throwError(
462
+ baseError.setCode(ErrorCode.IMAGE_ASPECT_RATIO_INVALID).setMessage(
463
+ `\u5BBD\u9AD8\u6BD4\u4E0D\u80FD\u5927\u4E8E ${this.options.maxAspectRatio}\uFF0C\u5F53\u524D ${aspectRatio.toFixed(2)}`
464
+ ).setContext({ aspectRatio })
465
+ );
466
+ }
467
+ if (this.options.aspectRatios?.length) {
468
+ const isValid = this.options.aspectRatios.some(
469
+ (ratio) => Math.abs(aspectRatio - ratio) < 0.01
470
+ );
471
+ if (!isValid) {
472
+ return this.throwError(
473
+ baseError.setCode(ErrorCode.IMAGE_ASPECT_RATIO_INVALID).setMessage(
474
+ `\u5BBD\u9AD8\u6BD4\u5FC5\u987B\u4E3A ${this.options.aspectRatios.join(", ")}\uFF0C\u5F53\u524D ${aspectRatio.toFixed(2)}`
475
+ ).setContext({ aspectRatio })
476
+ );
477
+ }
478
+ }
479
+ const resolution = dimensions.width * dimensions.height;
480
+ if (this.options.minResolution && resolution < this.options.minResolution) {
481
+ return this.throwError(
482
+ baseError.setCode(ErrorCode.IMAGE_RESOLUTION_INVALID).setMessage(
483
+ `\u5206\u8FA8\u7387\u4E0D\u80FD\u5C0F\u4E8E ${this.options.minResolution}px\xB2\uFF0C\u5F53\u524D ${resolution}px\xB2`
484
+ ).setContext({ resolution })
485
+ );
486
+ }
487
+ if (this.options.maxResolution && resolution > this.options.maxResolution) {
488
+ return this.throwError(
489
+ baseError.setCode(ErrorCode.IMAGE_RESOLUTION_INVALID).setMessage(
490
+ `\u5206\u8FA8\u7387\u4E0D\u80FD\u5927\u4E8E ${this.options.maxResolution}px\xB2\uFF0C\u5F53\u524D ${resolution}px\xB2`
491
+ ).setContext({ resolution })
492
+ );
493
+ }
494
+ if (this.options.allowAnimated === false && file.File.type === "image/gif") {
495
+ const isAnimated = await this.isAnimatedGif(file.File);
496
+ if (isAnimated) {
497
+ return this.throwError(
498
+ baseError.setCode(ErrorCode.IMAGE_ANIMATED).setMessage("\u4E0D\u652F\u6301\u52A8\u6001\u56FE\u7247")
499
+ );
500
+ }
501
+ }
502
+ file.metadata = {
503
+ ...file.metadata,
504
+ width: dimensions.width,
505
+ height: dimensions.height,
506
+ aspectRatio,
507
+ resolution
508
+ };
509
+ return file;
510
+ }
511
+ /**
512
+ * 获取图片尺寸
513
+ */
514
+ async getImageDimensions(file) {
515
+ return new Promise((resolve, reject) => {
516
+ const img = new Image();
517
+ img.onload = () => resolve({ width: img.width, height: img.height });
518
+ img.onerror = reject;
519
+ img.src = URL.createObjectURL(file);
520
+ });
521
+ }
522
+ /**
523
+ * 检查 GIF 是否为动态图片
524
+ */
525
+ async isAnimatedGif(file) {
526
+ const buffer = await file.arrayBuffer();
527
+ const view = new DataView(buffer);
528
+ if (view.getUint8(0) !== 71 || view.getUint8(1) !== 73 || view.getUint8(2) !== 70 || view.getUint8(3) !== 56) {
529
+ return false;
530
+ }
531
+ let frames = 0;
532
+ const maxFrames = 2;
533
+ let offset = 6;
534
+ while (offset < view.byteLength && frames < maxFrames) {
535
+ const blockId = view.getUint8(offset);
536
+ if (blockId === 33) {
537
+ offset += 2;
538
+ const blockSize = view.getUint8(offset);
539
+ offset += blockSize + 1;
540
+ } else if (blockId === 44) {
541
+ frames++;
542
+ offset += 10;
543
+ } else if (blockId === 59) {
544
+ break;
545
+ } else {
546
+ offset++;
547
+ }
548
+ }
549
+ return frames > 1;
550
+ }
551
+ };
552
+ var VideoValidatorPlugin = class extends BasePlugin {
553
+ constructor(options = {}) {
554
+ super();
555
+ this.options = options;
556
+ this.name = "video-validator-plugin";
557
+ this.version = "1.0.0";
558
+ this.priority = 0;
559
+ }
560
+ async onFileSelect(file, context) {
561
+ if (!file.File.type.startsWith("video/")) {
562
+ return file;
563
+ }
564
+ const metadata = await this.getVideoMetadata(file.File);
565
+ const baseError = this.createBaseError(file, context.transfer);
566
+ if (this.options.minDuration && metadata.duration < this.options.minDuration) {
567
+ return this.throwError(
568
+ baseError.setCode(ErrorCode.VIDEO_DURATION_INVALID).setMessage(
569
+ `\u89C6\u9891\u65F6\u957F\u4E0D\u80FD\u5C0F\u4E8E ${this.options.minDuration}\u79D2\uFF0C\u5F53\u524D ${metadata.duration.toFixed(1)}\u79D2`
570
+ ).setContext({ duration: metadata.duration })
571
+ );
572
+ }
573
+ if (this.options.maxDuration && metadata.duration > this.options.maxDuration) {
574
+ return this.throwError(
575
+ baseError.setCode(ErrorCode.VIDEO_DURATION_INVALID).setMessage(
576
+ `\u89C6\u9891\u65F6\u957F\u4E0D\u80FD\u5927\u4E8E ${this.options.maxDuration}\u79D2\uFF0C\u5F53\u524D ${metadata.duration.toFixed(1)}\u79D2`
577
+ ).setContext({ duration: metadata.duration })
578
+ );
579
+ }
580
+ if (this.options.minWidth && metadata.width < this.options.minWidth) {
581
+ return this.throwError(
582
+ baseError.setCode(ErrorCode.VIDEO_WIDTH_INVALID).setMessage(
583
+ `\u89C6\u9891\u5BBD\u5EA6\u4E0D\u80FD\u5C0F\u4E8E ${this.options.minWidth}px\uFF0C\u5F53\u524D ${metadata.width}px`
584
+ ).setContext({ width: metadata.width })
585
+ );
586
+ }
587
+ if (this.options.minHeight && metadata.height < this.options.minHeight) {
588
+ return this.throwError(
589
+ baseError.setCode(ErrorCode.VIDEO_HEIGHT_INVALID).setMessage(
590
+ `\u89C6\u9891\u9AD8\u5EA6\u4E0D\u80FD\u5C0F\u4E8E ${this.options.minHeight}px\uFF0C\u5F53\u524D ${metadata.height}px`
591
+ ).setContext({ height: metadata.height })
592
+ );
593
+ }
594
+ if (this.options.minBitrate && metadata.bitrate < this.options.minBitrate) {
595
+ return this.throwError(
596
+ baseError.setCode(ErrorCode.VIDEO_BITRATE_INVALID).setMessage(
597
+ `\u89C6\u9891\u6BD4\u7279\u7387\u4E0D\u80FD\u5C0F\u4E8E ${this.options.minBitrate}kbps\uFF0C\u5F53\u524D ${metadata.bitrate}kbps`
598
+ ).setContext({ bitrate: metadata.bitrate })
599
+ );
600
+ }
601
+ if (this.options.maxBitrate && metadata.bitrate > this.options.maxBitrate) {
602
+ return this.throwError(
603
+ baseError.setCode(ErrorCode.VIDEO_BITRATE_INVALID).setMessage(
604
+ `\u89C6\u9891\u6BD4\u7279\u7387\u4E0D\u80FD\u5927\u4E8E ${this.options.maxBitrate}kbps\uFF0C\u5F53\u524D ${metadata.bitrate}kbps`
605
+ ).setContext({ bitrate: metadata.bitrate })
606
+ );
607
+ }
608
+ if (this.options.allowedCodecs?.length) {
609
+ const codecValid = this.options.allowedCodecs.some(
610
+ (codec) => metadata.codec?.includes(codec)
611
+ );
612
+ if (!codecValid) {
613
+ return this.throwError(
614
+ baseError.setCode(ErrorCode.VIDEO_CODEC_INVALID).setMessage(
615
+ `\u89C6\u9891\u7F16\u7801\u683C\u5F0F\u4E0D\u652F\u6301\uFF0C\u5F53\u524D ${metadata.codec || "\u672A\u77E5"}\uFF0C\u5141\u8BB8\uFF1A${this.options.allowedCodecs.join(", ")}`
616
+ ).setContext({ codec: metadata.codec })
617
+ );
618
+ }
619
+ }
620
+ file.metadata = { ...file.metadata, video: metadata };
621
+ return file;
622
+ }
623
+ /**
624
+ * 获取视频元数据
625
+ */
626
+ async getVideoMetadata(file) {
627
+ return new Promise((resolve, reject) => {
628
+ const video = document.createElement("video");
629
+ video.preload = "metadata";
630
+ video.onloadedmetadata = () => {
631
+ URL.revokeObjectURL(video.src);
632
+ resolve({
633
+ duration: video.duration,
634
+ width: video.videoWidth,
635
+ height: video.videoHeight,
636
+ bitrate: 0,
637
+ // 浏览器无法直接获取比特率,需要后端提供或估算
638
+ codec: video.currentSrc.split(",").pop()?.split('"')[1]
639
+ });
640
+ };
641
+ video.onerror = () => {
642
+ URL.revokeObjectURL(video.src);
643
+ reject(new Error("Failed to load video metadata"));
644
+ };
645
+ video.src = URL.createObjectURL(file);
646
+ });
647
+ }
648
+ };
649
+
650
+ // src/retry/smart-retry-plugin.ts
651
+ var SmartRetryPlugin = class {
652
+ // 文件ID -> 重试定时器
653
+ constructor(config = {}) {
654
+ this.name = "SmartRetryPlugin";
655
+ this.version = "1.0.0";
656
+ this.desc = "\u667A\u80FD\u91CD\u8BD5\u7B56\u7565\u63D2\u4EF6\uFF0C\u540C\u65F6\u652F\u6301\u4E0A\u4F20/\u4E0B\u8F7D\uFF0C\u652F\u6301\u6307\u6570\u9000\u907F\u3001\u7EBF\u6027\u589E\u957F\u7B49\u591A\u79CD\u91CD\u8BD5\u7B56\u7565";
657
+ this.priority = 10;
658
+ this.retryCountMap = /* @__PURE__ */ new Map();
659
+ // 文件ID -> 重试次数
660
+ this.retryTimerMap = /* @__PURE__ */ new Map();
661
+ this.config = {
662
+ maxRetries: config.maxRetries ?? 3,
663
+ strategy: config.strategy ?? "exponential",
664
+ initialDelay: config.initialDelay ?? 1e3,
665
+ maxDelay: config.maxDelay ?? 3e4,
666
+ retryableErrors: config.retryableErrors ?? [],
667
+ showRetryNotification: config.showRetryNotification ?? true
668
+ };
669
+ }
670
+ /**
671
+ * 插件初始化
672
+ */
673
+ install(transfer, options) {
674
+ console.log(`\u2705 [${this.name}] \u63D2\u4EF6\u5DF2\u5B89\u88C5`, this.config);
675
+ }
676
+ /**
677
+ * 传输失败时触发(上传/下载通用)
678
+ */
679
+ onError(error, file, context) {
680
+ const fileId = file.fileId;
681
+ const currentRetries = this.retryCountMap.get(fileId) || 0;
682
+ if (currentRetries >= this.config.maxRetries) {
683
+ console.warn(
684
+ `[${this.name}] \u6587\u4EF6 ${file.fileName} \u5DF2\u8FBE\u5230\u6700\u5927\u91CD\u8BD5\u6B21\u6570 (${this.config.maxRetries})\uFF0C\u505C\u6B62\u91CD\u8BD5`
685
+ );
686
+ this.cleanup(fileId);
687
+ return;
688
+ }
689
+ if (!this.isRetryableError(error)) {
690
+ console.log(
691
+ `[${this.name}] \u6587\u4EF6 ${file.fileName} \u7684\u9519\u8BEF\u4E0D\u53EF\u91CD\u8BD5\uFF0C\u8DF3\u8FC7\u91CD\u8BD5`
692
+ );
693
+ return;
694
+ }
695
+ const delay = this.calculateDelay(currentRetries);
696
+ const timer = setTimeout(() => {
697
+ console.log(
698
+ `[${this.name}] \u6587\u4EF6 ${file.fileName} \u7B2C ${currentRetries + 1}/${this.config.maxRetries} \u6B21\u91CD\u8BD5\uFF08\u5EF6\u8FDF ${delay}ms\uFF09`
699
+ );
700
+ this.retryCountMap.set(fileId, currentRetries + 1);
701
+ file.retry();
702
+ this.retryTimerMap.delete(fileId);
703
+ }, delay);
704
+ this.retryTimerMap.set(fileId, timer);
705
+ if (this.config.showRetryNotification) {
706
+ console.info(
707
+ `[${this.name}] \u23F3 \u6587\u4EF6 ${file.fileName} \u5C06\u5728 ${delay}ms \u540E\u81EA\u52A8\u91CD\u8BD5...`
708
+ );
709
+ }
710
+ }
711
+ /**
712
+ * 传输成功时清理重试状态
713
+ */
714
+ onSuccess(response, file, context) {
715
+ this.cleanup(file.fileId);
716
+ }
717
+ /**
718
+ * 文件移除时清理重试状态
719
+ */
720
+ destroy() {
721
+ this.retryTimerMap.forEach((timer) => clearTimeout(timer));
722
+ this.retryTimerMap.clear();
723
+ this.retryCountMap.clear();
724
+ console.log(`\u{1F5D1}\uFE0F [${this.name}] \u63D2\u4EF6\u5DF2\u9500\u6BC1`);
725
+ }
726
+ /**
727
+ * 判断错误是否可重试
728
+ */
729
+ isRetryableError(error) {
730
+ if (this.config.retryableErrors.length === 0) {
731
+ return true;
732
+ }
733
+ const errorCode = error.code || error.message || "";
734
+ return this.config.retryableErrors.some(
735
+ (code) => errorCode.includes(code)
736
+ );
737
+ }
738
+ /**
739
+ * 计算延迟时间
740
+ */
741
+ calculateDelay(retryCount) {
742
+ const { strategy, initialDelay, maxDelay } = this.config;
743
+ let delay;
744
+ switch (strategy) {
745
+ case "fixed":
746
+ delay = initialDelay;
747
+ break;
748
+ case "linear":
749
+ delay = initialDelay * (retryCount + 1);
750
+ break;
751
+ case "exponential":
752
+ default:
753
+ delay = initialDelay * Math.pow(2, retryCount);
754
+ break;
755
+ }
756
+ return Math.min(delay, maxDelay);
757
+ }
758
+ /**
759
+ * 清理文件的重试状态
760
+ */
761
+ cleanup(fileId) {
762
+ const timer = this.retryTimerMap.get(fileId);
763
+ if (timer) {
764
+ clearTimeout(timer);
765
+ this.retryTimerMap.delete(fileId);
766
+ }
767
+ this.retryCountMap.delete(fileId);
768
+ }
769
+ };
770
+
771
+ export { BasePlugin, CompressImagePlugin, FileValidatorPlugin, ImageValidatorPlugin, SmartRetryPlugin, VideoValidatorPlugin, WatermarkPlugin };
772
+ //# sourceMappingURL=out.js.map
773
+ //# sourceMappingURL=index.mjs.map