@bowenqt/qiniu-ai-sdk 0.19.0 → 0.20.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/ai/memory/index.d.ts.map +1 -1
- package/dist/ai/memory/index.js +9 -2
- package/dist/ai/memory/index.js.map +1 -1
- package/dist/ai/memory/index.mjs +9 -2
- package/dist/browser/index.d.ts +6 -2
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +29 -20
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/index.mjs +15 -6
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6 -0
- package/dist/lib/asset-cost.d.ts +56 -0
- package/dist/lib/asset-cost.d.ts.map +1 -0
- package/dist/lib/asset-cost.js +138 -0
- package/dist/lib/asset-cost.js.map +1 -0
- package/dist/lib/asset-cost.mjs +134 -0
- package/dist/lib/asset-resolver.d.ts +70 -0
- package/dist/lib/asset-resolver.d.ts.map +1 -0
- package/dist/lib/asset-resolver.js +145 -0
- package/dist/lib/asset-resolver.js.map +1 -0
- package/dist/lib/asset-resolver.mjs +138 -0
- package/dist/lib/signer.d.ts +0 -6
- package/dist/lib/signer.d.ts.map +1 -1
- package/dist/lib/signer.js +14 -8
- package/dist/lib/signer.js.map +1 -1
- package/dist/lib/signer.mjs +14 -8
- package/dist/lib/vframe.d.ts +94 -0
- package/dist/lib/vframe.d.ts.map +1 -0
- package/dist/lib/vframe.js +189 -0
- package/dist/lib/vframe.js.map +1 -0
- package/dist/lib/vframe.mjs +181 -0
- package/package.json +1 -1
package/dist/lib/signer.js
CHANGED
|
@@ -12,12 +12,6 @@
|
|
|
12
12
|
* }
|
|
13
13
|
* };
|
|
14
14
|
* ```
|
|
15
|
-
*
|
|
16
|
-
* @example Node.js usage (direct signing):
|
|
17
|
-
* ```typescript
|
|
18
|
-
* import { createSigner } from '@bowenqt/qiniu-ai-sdk/node';
|
|
19
|
-
* const signer = createSigner({ accessKey, secretKey });
|
|
20
|
-
* ```
|
|
21
15
|
*/
|
|
22
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
17
|
exports.CachedSigner = exports.UrlCache = void 0;
|
|
@@ -56,7 +50,15 @@ class UrlCache {
|
|
|
56
50
|
/** Cache a signed URL */
|
|
57
51
|
set(bucket, key, signedUrl, fop) {
|
|
58
52
|
const cacheKey = this.getKey(bucket, key, fop);
|
|
59
|
-
//
|
|
53
|
+
// Check if key already exists (update, not new entry)
|
|
54
|
+
if (this.cache.has(cacheKey)) {
|
|
55
|
+
this.cache.set(cacheKey, {
|
|
56
|
+
signedUrl,
|
|
57
|
+
accessTime: Date.now(),
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Evict if at capacity (only for new entries)
|
|
60
62
|
if (this.cache.size >= this.config.maxSize) {
|
|
61
63
|
this.evictLRU();
|
|
62
64
|
}
|
|
@@ -113,7 +115,11 @@ class CachedSigner {
|
|
|
113
115
|
this.cache = new UrlCache(cacheConfig);
|
|
114
116
|
}
|
|
115
117
|
async sign(bucket, key, options) {
|
|
116
|
-
//
|
|
118
|
+
// Skip cache if custom expiry is specified (different TTLs = different URLs)
|
|
119
|
+
if (options?.expiry) {
|
|
120
|
+
return this.baseSigner.sign(bucket, key, options);
|
|
121
|
+
}
|
|
122
|
+
// Check cache first (only for default expiry)
|
|
117
123
|
const cached = this.cache.get(bucket, key, options?.fop);
|
|
118
124
|
if (cached)
|
|
119
125
|
return cached;
|
package/dist/lib/signer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signer.js","sourceRoot":"","sources":["../../src/lib/signer.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"signer.js","sourceRoot":"","sources":["../../src/lib/signer.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/signer.mjs
CHANGED
|
@@ -11,12 +11,6 @@
|
|
|
11
11
|
* }
|
|
12
12
|
* };
|
|
13
13
|
* ```
|
|
14
|
-
*
|
|
15
|
-
* @example Node.js usage (direct signing):
|
|
16
|
-
* ```typescript
|
|
17
|
-
* import { createSigner } from '@bowenqt/qiniu-ai-sdk/node';
|
|
18
|
-
* const signer = createSigner({ accessKey, secretKey });
|
|
19
|
-
* ```
|
|
20
14
|
*/
|
|
21
15
|
/**
|
|
22
16
|
* LRU cache for signed URLs.
|
|
@@ -53,7 +47,15 @@ export class UrlCache {
|
|
|
53
47
|
/** Cache a signed URL */
|
|
54
48
|
set(bucket, key, signedUrl, fop) {
|
|
55
49
|
const cacheKey = this.getKey(bucket, key, fop);
|
|
56
|
-
//
|
|
50
|
+
// Check if key already exists (update, not new entry)
|
|
51
|
+
if (this.cache.has(cacheKey)) {
|
|
52
|
+
this.cache.set(cacheKey, {
|
|
53
|
+
signedUrl,
|
|
54
|
+
accessTime: Date.now(),
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Evict if at capacity (only for new entries)
|
|
57
59
|
if (this.cache.size >= this.config.maxSize) {
|
|
58
60
|
this.evictLRU();
|
|
59
61
|
}
|
|
@@ -109,7 +111,11 @@ export class CachedSigner {
|
|
|
109
111
|
this.cache = new UrlCache(cacheConfig);
|
|
110
112
|
}
|
|
111
113
|
async sign(bucket, key, options) {
|
|
112
|
-
//
|
|
114
|
+
// Skip cache if custom expiry is specified (different TTLs = different URLs)
|
|
115
|
+
if (options?.expiry) {
|
|
116
|
+
return this.baseSigner.sign(bucket, key, options);
|
|
117
|
+
}
|
|
118
|
+
// Check cache first (only for default expiry)
|
|
113
119
|
const cached = this.cache.get(bucket, key, options?.fop);
|
|
114
120
|
if (cached)
|
|
115
121
|
return cached;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Frame Extraction using Qiniu vframe API.
|
|
3
|
+
*
|
|
4
|
+
* @see https://developer.qiniu.com/dora/1313/video-frame-thumbnails-vframe
|
|
5
|
+
*/
|
|
6
|
+
import type { QiniuSigner } from './signer';
|
|
7
|
+
import type { QiniuAsset, ResolveOptions } from './asset-resolver';
|
|
8
|
+
/** Vframe extraction options */
|
|
9
|
+
export interface VframeOptions {
|
|
10
|
+
/** Specific second offsets to extract frames */
|
|
11
|
+
offsets?: number[];
|
|
12
|
+
/** Number of frames to extract (uniformly distributed) */
|
|
13
|
+
count?: number;
|
|
14
|
+
/** Video duration in seconds (required for count mode) */
|
|
15
|
+
duration?: number;
|
|
16
|
+
/** Frame width (default: 640, range: 20-3840) */
|
|
17
|
+
width?: number;
|
|
18
|
+
/** Frame height (default: auto, range: 20-2160) */
|
|
19
|
+
height?: number;
|
|
20
|
+
/** Output format (default: 'jpg') */
|
|
21
|
+
format?: 'jpg' | 'png';
|
|
22
|
+
/** Rotation in degrees */
|
|
23
|
+
rotate?: 90 | 180 | 270 | 'auto';
|
|
24
|
+
}
|
|
25
|
+
/** Extracted video frame */
|
|
26
|
+
export interface VideoFrame {
|
|
27
|
+
/** Signed frame URL */
|
|
28
|
+
url: string;
|
|
29
|
+
/** URL expiration timestamp (ms) */
|
|
30
|
+
expiresAt: number;
|
|
31
|
+
/** Frame offset in seconds */
|
|
32
|
+
offset: number;
|
|
33
|
+
/** Frame index (0-based) */
|
|
34
|
+
index: number;
|
|
35
|
+
}
|
|
36
|
+
/** Frame extraction result */
|
|
37
|
+
export interface VframeResult {
|
|
38
|
+
/** Extracted frames */
|
|
39
|
+
frames: VideoFrame[];
|
|
40
|
+
/** Total frame count */
|
|
41
|
+
count: number;
|
|
42
|
+
/** Video duration (if provided) */
|
|
43
|
+
duration?: number;
|
|
44
|
+
}
|
|
45
|
+
export declare class VframeError extends Error {
|
|
46
|
+
constructor(message: string);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build a vframe fop command.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* buildVframeFop(5, { width: 320, format: 'jpg' });
|
|
54
|
+
* // 'vframe/jpg/offset/5/w/320'
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare function buildVframeFop(offset: number, options?: Omit<VframeOptions, 'offsets' | 'count' | 'duration'>): string;
|
|
58
|
+
/**
|
|
59
|
+
* Build a complete vframe URL from a base URL.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* buildVframeUrl('https://cdn.example.com/video.mp4', 5, { width: 320 });
|
|
64
|
+
* // 'https://cdn.example.com/video.mp4?vframe/jpg/offset/5/w/320'
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildVframeUrl(baseUrl: string, offset: number, options?: Omit<VframeOptions, 'offsets' | 'count' | 'duration'>): string;
|
|
68
|
+
/**
|
|
69
|
+
* Extract frames from a video.
|
|
70
|
+
*
|
|
71
|
+
* @example Uniform extraction
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const result = await extractFrames(asset, signer, {
|
|
74
|
+
* count: 5,
|
|
75
|
+
* duration: 120, // 2 minute video
|
|
76
|
+
* width: 640,
|
|
77
|
+
* });
|
|
78
|
+
* // Frames at: 20s, 40s, 60s, 80s, 100s
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* @example Explicit offsets
|
|
82
|
+
* ```typescript
|
|
83
|
+
* const result = await extractFrames(asset, signer, {
|
|
84
|
+
* offsets: [0, 30, 60, 90],
|
|
85
|
+
* width: 640,
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export declare function extractFrames(asset: QiniuAsset, signer: QiniuSigner, options: VframeOptions, resolveOptions?: ResolveOptions): Promise<VframeResult>;
|
|
90
|
+
/**
|
|
91
|
+
* Extract a single frame at a specific offset.
|
|
92
|
+
*/
|
|
93
|
+
export declare function extractFrame(asset: QiniuAsset, signer: QiniuSigner, offset: number, options?: Omit<VframeOptions, 'offsets' | 'count' | 'duration'>, resolveOptions?: ResolveOptions): Promise<VideoFrame>;
|
|
94
|
+
//# sourceMappingURL=vframe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vframe.d.ts","sourceRoot":"","sources":["../../src/lib/vframe.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAa,MAAM,UAAU,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAOnE,gCAAgC;AAChC,MAAM,WAAW,aAAa;IAE1B,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAGnB,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,iDAAiD;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACvB,0BAA0B;IAC1B,MAAM,CAAC,EAAE,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,MAAM,CAAC;CACpC;AAED,4BAA4B;AAC5B,MAAM,WAAW,UAAU;IACvB,uBAAuB;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,8BAA8B;AAC9B,MAAM,WAAW,YAAY;IACzB,uBAAuB;IACvB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,wBAAwB;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAiBD,qBAAa,WAAY,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM;CAI9B;AAMD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,IAAI,CAAC,aAAa,EAAE,SAAS,GAAG,OAAO,GAAG,UAAU,CAAM,GAAG,MAAM,CA8B1H;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC1B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,IAAI,CAAC,aAAa,EAAE,SAAS,GAAG,OAAO,GAAG,UAAU,CAAM,GACpE,MAAM,CAIR;AA6BD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,aAAa,CAC/B,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,aAAa,EACtB,cAAc,CAAC,EAAE,cAAc,GAChC,OAAO,CAAC,YAAY,CAAC,CAuDvB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAC9B,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,IAAI,CAAC,aAAa,EAAE,SAAS,GAAG,OAAO,GAAG,UAAU,CAAM,EACnE,cAAc,CAAC,EAAE,cAAc,GAChC,OAAO,CAAC,UAAU,CAAC,CAMrB"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Video Frame Extraction using Qiniu vframe API.
|
|
4
|
+
*
|
|
5
|
+
* @see https://developer.qiniu.com/dora/1313/video-frame-thumbnails-vframe
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.VframeError = void 0;
|
|
9
|
+
exports.buildVframeFop = buildVframeFop;
|
|
10
|
+
exports.buildVframeUrl = buildVframeUrl;
|
|
11
|
+
exports.extractFrames = extractFrames;
|
|
12
|
+
exports.extractFrame = extractFrame;
|
|
13
|
+
const asset_resolver_1 = require("./asset-resolver");
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Constants
|
|
16
|
+
// ============================================================================
|
|
17
|
+
const DEFAULT_COUNT = 5;
|
|
18
|
+
const DEFAULT_WIDTH = 640;
|
|
19
|
+
const DEFAULT_FORMAT = 'jpg';
|
|
20
|
+
const MIN_DIMENSION = 20;
|
|
21
|
+
const MAX_LONG_EDGE = 3840;
|
|
22
|
+
const MAX_SHORT_EDGE = 2160;
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Errors
|
|
25
|
+
// ============================================================================
|
|
26
|
+
class VframeError extends Error {
|
|
27
|
+
constructor(message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'VframeError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.VframeError = VframeError;
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// URL Building
|
|
35
|
+
// ============================================================================
|
|
36
|
+
/**
|
|
37
|
+
* Build a vframe fop command.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* buildVframeFop(5, { width: 320, format: 'jpg' });
|
|
42
|
+
* // 'vframe/jpg/offset/5/w/320'
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
function buildVframeFop(offset, options = {}) {
|
|
46
|
+
if (offset < 0) {
|
|
47
|
+
throw new VframeError('Offset must be non-negative');
|
|
48
|
+
}
|
|
49
|
+
const format = options.format ?? DEFAULT_FORMAT;
|
|
50
|
+
const parts = [`vframe/${format}/offset/${offset}`];
|
|
51
|
+
// Width
|
|
52
|
+
if (options.width !== undefined) {
|
|
53
|
+
if (options.width < MIN_DIMENSION || options.width > MAX_LONG_EDGE) {
|
|
54
|
+
throw new VframeError(`Width must be between ${MIN_DIMENSION} and ${MAX_LONG_EDGE}`);
|
|
55
|
+
}
|
|
56
|
+
parts.push(`w/${options.width}`);
|
|
57
|
+
}
|
|
58
|
+
// Height
|
|
59
|
+
if (options.height !== undefined) {
|
|
60
|
+
if (options.height < MIN_DIMENSION || options.height > MAX_SHORT_EDGE) {
|
|
61
|
+
throw new VframeError(`Height must be between ${MIN_DIMENSION} and ${MAX_SHORT_EDGE}`);
|
|
62
|
+
}
|
|
63
|
+
parts.push(`h/${options.height}`);
|
|
64
|
+
}
|
|
65
|
+
// Rotate
|
|
66
|
+
if (options.rotate !== undefined) {
|
|
67
|
+
parts.push(`rotate/${options.rotate}`);
|
|
68
|
+
}
|
|
69
|
+
return parts.join('/');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Build a complete vframe URL from a base URL.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* buildVframeUrl('https://cdn.example.com/video.mp4', 5, { width: 320 });
|
|
77
|
+
* // 'https://cdn.example.com/video.mp4?vframe/jpg/offset/5/w/320'
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
function buildVframeUrl(baseUrl, offset, options = {}) {
|
|
81
|
+
const fop = buildVframeFop(offset, options);
|
|
82
|
+
const separator = baseUrl.includes('?') ? '|' : '?';
|
|
83
|
+
return `${baseUrl}${separator}${fop}`;
|
|
84
|
+
}
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Frame Extraction
|
|
87
|
+
// ============================================================================
|
|
88
|
+
/**
|
|
89
|
+
* Calculate uniform offsets for frame extraction.
|
|
90
|
+
*/
|
|
91
|
+
function calculateUniformOffsets(count, duration) {
|
|
92
|
+
if (count <= 0) {
|
|
93
|
+
throw new VframeError('Count must be positive');
|
|
94
|
+
}
|
|
95
|
+
if (duration <= 0) {
|
|
96
|
+
throw new VframeError('Duration must be positive');
|
|
97
|
+
}
|
|
98
|
+
if (count === 1) {
|
|
99
|
+
return [duration / 2];
|
|
100
|
+
}
|
|
101
|
+
const offsets = [];
|
|
102
|
+
const step = duration / (count + 1);
|
|
103
|
+
for (let i = 1; i <= count; i++) {
|
|
104
|
+
offsets.push(Math.round(step * i * 100) / 100);
|
|
105
|
+
}
|
|
106
|
+
return offsets;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Extract frames from a video.
|
|
110
|
+
*
|
|
111
|
+
* @example Uniform extraction
|
|
112
|
+
* ```typescript
|
|
113
|
+
* const result = await extractFrames(asset, signer, {
|
|
114
|
+
* count: 5,
|
|
115
|
+
* duration: 120, // 2 minute video
|
|
116
|
+
* width: 640,
|
|
117
|
+
* });
|
|
118
|
+
* // Frames at: 20s, 40s, 60s, 80s, 100s
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* @example Explicit offsets
|
|
122
|
+
* ```typescript
|
|
123
|
+
* const result = await extractFrames(asset, signer, {
|
|
124
|
+
* offsets: [0, 30, 60, 90],
|
|
125
|
+
* width: 640,
|
|
126
|
+
* });
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
async function extractFrames(asset, signer, options, resolveOptions) {
|
|
130
|
+
// Validate options
|
|
131
|
+
if (options.offsets && options.count) {
|
|
132
|
+
throw new VframeError('Cannot specify both offsets and count');
|
|
133
|
+
}
|
|
134
|
+
let offsets;
|
|
135
|
+
if (options.offsets) {
|
|
136
|
+
// Explicit offsets mode
|
|
137
|
+
offsets = options.offsets;
|
|
138
|
+
for (const offset of offsets) {
|
|
139
|
+
if (offset < 0) {
|
|
140
|
+
throw new VframeError('All offsets must be non-negative');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// Uniform extraction mode
|
|
146
|
+
const count = options.count ?? DEFAULT_COUNT;
|
|
147
|
+
if (!options.duration) {
|
|
148
|
+
throw new VframeError('Duration is required for uniform extraction');
|
|
149
|
+
}
|
|
150
|
+
offsets = calculateUniformOffsets(count, options.duration);
|
|
151
|
+
}
|
|
152
|
+
// Build frame extraction options
|
|
153
|
+
const frameOpts = {
|
|
154
|
+
width: options.width ?? DEFAULT_WIDTH,
|
|
155
|
+
height: options.height,
|
|
156
|
+
format: options.format ?? DEFAULT_FORMAT,
|
|
157
|
+
rotate: options.rotate,
|
|
158
|
+
};
|
|
159
|
+
// Extract frames in parallel
|
|
160
|
+
const frames = await Promise.all(offsets.map(async (offset, index) => {
|
|
161
|
+
const fop = buildVframeFop(offset, frameOpts);
|
|
162
|
+
const resolved = await (0, asset_resolver_1.resolveAsset)(asset, signer, {
|
|
163
|
+
...resolveOptions,
|
|
164
|
+
fop,
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
url: resolved.url,
|
|
168
|
+
expiresAt: resolved.expiresAt,
|
|
169
|
+
offset,
|
|
170
|
+
index,
|
|
171
|
+
};
|
|
172
|
+
}));
|
|
173
|
+
return {
|
|
174
|
+
frames,
|
|
175
|
+
count: frames.length,
|
|
176
|
+
duration: options.duration,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Extract a single frame at a specific offset.
|
|
181
|
+
*/
|
|
182
|
+
async function extractFrame(asset, signer, offset, options = {}, resolveOptions) {
|
|
183
|
+
const result = await extractFrames(asset, signer, {
|
|
184
|
+
offsets: [offset],
|
|
185
|
+
...options,
|
|
186
|
+
}, resolveOptions);
|
|
187
|
+
return result.frames[0];
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=vframe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vframe.js","sourceRoot":"","sources":["../../src/lib/vframe.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AA0FH,wCA8BC;AAWD,wCAQC;AAkDD,sCA4DC;AAKD,oCAYC;AAtQD,qDAAsE;AAmDtE,+EAA+E;AAC/E,YAAY;AACZ,+EAA+E;AAE/E,MAAM,aAAa,GAAG,CAAC,CAAC;AACxB,MAAM,aAAa,GAAG,GAAG,CAAC;AAC1B,MAAM,cAAc,GAAG,KAAK,CAAC;AAC7B,MAAM,aAAa,GAAG,EAAE,CAAC;AACzB,MAAM,aAAa,GAAG,IAAI,CAAC;AAC3B,MAAM,cAAc,GAAG,IAAI,CAAC;AAE5B,+EAA+E;AAC/E,SAAS;AACT,+EAA+E;AAE/E,MAAa,WAAY,SAAQ,KAAK;IAClC,YAAY,OAAe;QACvB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;IAC9B,CAAC;CACJ;AALD,kCAKC;AAED,+EAA+E;AAC/E,eAAe;AACf,+EAA+E;AAE/E;;;;;;;;GAQG;AACH,SAAgB,cAAc,CAAC,MAAc,EAAE,UAAiE,EAAE;IAC9G,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;QACb,MAAM,IAAI,WAAW,CAAC,6BAA6B,CAAC,CAAC;IACzD,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,cAAc,CAAC;IAChD,MAAM,KAAK,GAAG,CAAC,UAAU,MAAM,WAAW,MAAM,EAAE,CAAC,CAAC;IAEpD,QAAQ;IACR,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC9B,IAAI,OAAO,CAAC,KAAK,GAAG,aAAa,IAAI,OAAO,CAAC,KAAK,GAAG,aAAa,EAAE,CAAC;YACjE,MAAM,IAAI,WAAW,CAAC,yBAAyB,aAAa,QAAQ,aAAa,EAAE,CAAC,CAAC;QACzF,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;IACrC,CAAC;IAED,SAAS;IACT,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,MAAM,GAAG,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,cAAc,EAAE,CAAC;YACpE,MAAM,IAAI,WAAW,CAAC,0BAA0B,aAAa,QAAQ,cAAc,EAAE,CAAC,CAAC;QAC3F,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACtC,CAAC;IAED,SAAS;IACT,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC/B,KAAK,CAAC,IAAI,CAAC,UAAU,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,cAAc,CAC1B,OAAe,EACf,MAAc,EACd,UAAiE,EAAE;IAEnE,MAAM,GAAG,GAAG,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;IACpD,OAAO,GAAG,OAAO,GAAG,SAAS,GAAG,GAAG,EAAE,CAAC;AAC1C,CAAC;AAED,+EAA+E;AAC/E,mBAAmB;AACnB,+EAA+E;AAE/E;;GAEG;AACH,SAAS,uBAAuB,CAAC,KAAa,EAAE,QAAgB;IAC5D,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;QACb,MAAM,IAAI,WAAW,CAAC,wBAAwB,CAAC,CAAC;IACpD,CAAC;IACD,IAAI,QAAQ,IAAI,CAAC,EAAE,CAAC;QAChB,MAAM,IAAI,WAAW,CAAC,2BAA2B,CAAC,CAAC;IACvD,CAAC;IAED,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;QACd,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC;IAC1B,CAAC;IAED,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,IAAI,GAAG,QAAQ,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;QAC9B,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,OAAO,CAAC;AACnB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACI,KAAK,UAAU,aAAa,CAC/B,KAAiB,EACjB,MAAmB,EACnB,OAAsB,EACtB,cAA+B;IAE/B,mBAAmB;IACnB,IAAI,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QACnC,MAAM,IAAI,WAAW,CAAC,uCAAuC,CAAC,CAAC;IACnE,CAAC;IAED,IAAI,OAAiB,CAAC;IAEtB,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QAClB,wBAAwB;QACxB,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAC1B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC3B,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;gBACb,MAAM,IAAI,WAAW,CAAC,kCAAkC,CAAC,CAAC;YAC9D,CAAC;QACL,CAAC;IACL,CAAC;SAAM,CAAC;QACJ,0BAA0B;QAC1B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,aAAa,CAAC;QAC7C,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;YACpB,MAAM,IAAI,WAAW,CAAC,6CAA6C,CAAC,CAAC;QACzE,CAAC;QACD,OAAO,GAAG,uBAAuB,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/D,CAAC;IAED,iCAAiC;IACjC,MAAM,SAAS,GAAG;QACd,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,aAAa;QACrC,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,cAAc;QACxC,MAAM,EAAE,OAAO,CAAC,MAAM;KACzB,CAAC;IAEF,6BAA6B;IAC7B,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAC5B,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;QAChC,MAAM,GAAG,GAAG,cAAc,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAC9C,MAAM,QAAQ,GAAG,MAAM,IAAA,6BAAY,EAAC,KAAK,EAAE,MAAM,EAAE;YAC/C,GAAG,cAAc;YACjB,GAAG;SACN,CAAC,CAAC;QACH,OAAO;YACH,GAAG,EAAE,QAAQ,CAAC,GAAG;YACjB,SAAS,EAAE,QAAQ,CAAC,SAAS;YAC7B,MAAM;YACN,KAAK;SACR,CAAC;IACN,CAAC,CAAC,CACL,CAAC;IAEF,OAAO;QACH,MAAM;QACN,KAAK,EAAE,MAAM,CAAC,MAAM;QACpB,QAAQ,EAAE,OAAO,CAAC,QAAQ;KAC7B,CAAC;AACN,CAAC;AAED;;GAEG;AACI,KAAK,UAAU,YAAY,CAC9B,KAAiB,EACjB,MAAmB,EACnB,MAAc,EACd,UAAiE,EAAE,EACnE,cAA+B;IAE/B,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE;QAC9C,OAAO,EAAE,CAAC,MAAM,CAAC;QACjB,GAAG,OAAO;KACb,EAAE,cAAc,CAAC,CAAC;IACnB,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AAC5B,CAAC"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Frame Extraction using Qiniu vframe API.
|
|
3
|
+
*
|
|
4
|
+
* @see https://developer.qiniu.com/dora/1313/video-frame-thumbnails-vframe
|
|
5
|
+
*/
|
|
6
|
+
import { resolveAsset } from './asset-resolver.mjs';
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Constants
|
|
9
|
+
// ============================================================================
|
|
10
|
+
const DEFAULT_COUNT = 5;
|
|
11
|
+
const DEFAULT_WIDTH = 640;
|
|
12
|
+
const DEFAULT_FORMAT = 'jpg';
|
|
13
|
+
const MIN_DIMENSION = 20;
|
|
14
|
+
const MAX_LONG_EDGE = 3840;
|
|
15
|
+
const MAX_SHORT_EDGE = 2160;
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Errors
|
|
18
|
+
// ============================================================================
|
|
19
|
+
export class VframeError extends Error {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'VframeError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// URL Building
|
|
27
|
+
// ============================================================================
|
|
28
|
+
/**
|
|
29
|
+
* Build a vframe fop command.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* buildVframeFop(5, { width: 320, format: 'jpg' });
|
|
34
|
+
* // 'vframe/jpg/offset/5/w/320'
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function buildVframeFop(offset, options = {}) {
|
|
38
|
+
if (offset < 0) {
|
|
39
|
+
throw new VframeError('Offset must be non-negative');
|
|
40
|
+
}
|
|
41
|
+
const format = options.format ?? DEFAULT_FORMAT;
|
|
42
|
+
const parts = [`vframe/${format}/offset/${offset}`];
|
|
43
|
+
// Width
|
|
44
|
+
if (options.width !== undefined) {
|
|
45
|
+
if (options.width < MIN_DIMENSION || options.width > MAX_LONG_EDGE) {
|
|
46
|
+
throw new VframeError(`Width must be between ${MIN_DIMENSION} and ${MAX_LONG_EDGE}`);
|
|
47
|
+
}
|
|
48
|
+
parts.push(`w/${options.width}`);
|
|
49
|
+
}
|
|
50
|
+
// Height
|
|
51
|
+
if (options.height !== undefined) {
|
|
52
|
+
if (options.height < MIN_DIMENSION || options.height > MAX_SHORT_EDGE) {
|
|
53
|
+
throw new VframeError(`Height must be between ${MIN_DIMENSION} and ${MAX_SHORT_EDGE}`);
|
|
54
|
+
}
|
|
55
|
+
parts.push(`h/${options.height}`);
|
|
56
|
+
}
|
|
57
|
+
// Rotate
|
|
58
|
+
if (options.rotate !== undefined) {
|
|
59
|
+
parts.push(`rotate/${options.rotate}`);
|
|
60
|
+
}
|
|
61
|
+
return parts.join('/');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build a complete vframe URL from a base URL.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* buildVframeUrl('https://cdn.example.com/video.mp4', 5, { width: 320 });
|
|
69
|
+
* // 'https://cdn.example.com/video.mp4?vframe/jpg/offset/5/w/320'
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function buildVframeUrl(baseUrl, offset, options = {}) {
|
|
73
|
+
const fop = buildVframeFop(offset, options);
|
|
74
|
+
const separator = baseUrl.includes('?') ? '|' : '?';
|
|
75
|
+
return `${baseUrl}${separator}${fop}`;
|
|
76
|
+
}
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// Frame Extraction
|
|
79
|
+
// ============================================================================
|
|
80
|
+
/**
|
|
81
|
+
* Calculate uniform offsets for frame extraction.
|
|
82
|
+
*/
|
|
83
|
+
function calculateUniformOffsets(count, duration) {
|
|
84
|
+
if (count <= 0) {
|
|
85
|
+
throw new VframeError('Count must be positive');
|
|
86
|
+
}
|
|
87
|
+
if (duration <= 0) {
|
|
88
|
+
throw new VframeError('Duration must be positive');
|
|
89
|
+
}
|
|
90
|
+
if (count === 1) {
|
|
91
|
+
return [duration / 2];
|
|
92
|
+
}
|
|
93
|
+
const offsets = [];
|
|
94
|
+
const step = duration / (count + 1);
|
|
95
|
+
for (let i = 1; i <= count; i++) {
|
|
96
|
+
offsets.push(Math.round(step * i * 100) / 100);
|
|
97
|
+
}
|
|
98
|
+
return offsets;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Extract frames from a video.
|
|
102
|
+
*
|
|
103
|
+
* @example Uniform extraction
|
|
104
|
+
* ```typescript
|
|
105
|
+
* const result = await extractFrames(asset, signer, {
|
|
106
|
+
* count: 5,
|
|
107
|
+
* duration: 120, // 2 minute video
|
|
108
|
+
* width: 640,
|
|
109
|
+
* });
|
|
110
|
+
* // Frames at: 20s, 40s, 60s, 80s, 100s
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
113
|
+
* @example Explicit offsets
|
|
114
|
+
* ```typescript
|
|
115
|
+
* const result = await extractFrames(asset, signer, {
|
|
116
|
+
* offsets: [0, 30, 60, 90],
|
|
117
|
+
* width: 640,
|
|
118
|
+
* });
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export async function extractFrames(asset, signer, options, resolveOptions) {
|
|
122
|
+
// Validate options
|
|
123
|
+
if (options.offsets && options.count) {
|
|
124
|
+
throw new VframeError('Cannot specify both offsets and count');
|
|
125
|
+
}
|
|
126
|
+
let offsets;
|
|
127
|
+
if (options.offsets) {
|
|
128
|
+
// Explicit offsets mode
|
|
129
|
+
offsets = options.offsets;
|
|
130
|
+
for (const offset of offsets) {
|
|
131
|
+
if (offset < 0) {
|
|
132
|
+
throw new VframeError('All offsets must be non-negative');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// Uniform extraction mode
|
|
138
|
+
const count = options.count ?? DEFAULT_COUNT;
|
|
139
|
+
if (!options.duration) {
|
|
140
|
+
throw new VframeError('Duration is required for uniform extraction');
|
|
141
|
+
}
|
|
142
|
+
offsets = calculateUniformOffsets(count, options.duration);
|
|
143
|
+
}
|
|
144
|
+
// Build frame extraction options
|
|
145
|
+
const frameOpts = {
|
|
146
|
+
width: options.width ?? DEFAULT_WIDTH,
|
|
147
|
+
height: options.height,
|
|
148
|
+
format: options.format ?? DEFAULT_FORMAT,
|
|
149
|
+
rotate: options.rotate,
|
|
150
|
+
};
|
|
151
|
+
// Extract frames in parallel
|
|
152
|
+
const frames = await Promise.all(offsets.map(async (offset, index) => {
|
|
153
|
+
const fop = buildVframeFop(offset, frameOpts);
|
|
154
|
+
const resolved = await resolveAsset(asset, signer, {
|
|
155
|
+
...resolveOptions,
|
|
156
|
+
fop,
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
url: resolved.url,
|
|
160
|
+
expiresAt: resolved.expiresAt,
|
|
161
|
+
offset,
|
|
162
|
+
index,
|
|
163
|
+
};
|
|
164
|
+
}));
|
|
165
|
+
return {
|
|
166
|
+
frames,
|
|
167
|
+
count: frames.length,
|
|
168
|
+
duration: options.duration,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Extract a single frame at a specific offset.
|
|
173
|
+
*/
|
|
174
|
+
export async function extractFrame(asset, signer, offset, options = {}, resolveOptions) {
|
|
175
|
+
const result = await extractFrames(asset, signer, {
|
|
176
|
+
offsets: [offset],
|
|
177
|
+
...options,
|
|
178
|
+
}, resolveOptions);
|
|
179
|
+
return result.frames[0];
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=vframe.js.map
|
package/package.json
CHANGED