@bowenqt/qiniu-ai-sdk 0.19.1 → 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/browser/index.d.ts +6 -0
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +19 -2
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/index.mjs +6 -0
- 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/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
|
@@ -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