nodejs-audio-visualizer 4.5.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +80 -80
  3. package/dist/audio.d.ts +12 -12
  4. package/dist/audio.js +120 -120
  5. package/dist/beats.d.ts +19 -19
  6. package/dist/beats.js +43 -43
  7. package/dist/bpmEncoder.d.ts +14 -14
  8. package/dist/bpmEncoder.js +97 -97
  9. package/dist/config.d.ts +77 -77
  10. package/dist/config.js +369 -369
  11. package/dist/dsp.d.ts +16 -16
  12. package/dist/dsp.js +76 -76
  13. package/dist/highlight.d.ts +32 -32
  14. package/dist/highlight.js +325 -325
  15. package/dist/image.d.ts +79 -79
  16. package/dist/image.js +458 -458
  17. package/dist/index.d.ts +79 -79
  18. package/dist/index.js +534 -531
  19. package/dist/index.js.map +1 -1
  20. package/dist/subtitleConvert.d.ts +3 -3
  21. package/dist/subtitleConvert.js +61 -61
  22. package/dist/video.d.ts +80 -80
  23. package/dist/video.js +434 -426
  24. package/dist/video.js.map +1 -1
  25. package/dist/waitForEventLoop.d.ts +1 -1
  26. package/dist/waitForEventLoop.js +5 -5
  27. package/package.json +61 -61
  28. package/src/audio.ts +120 -120
  29. package/src/beats.ts +68 -68
  30. package/src/bpmEncoder.ts +97 -97
  31. package/src/config.ts +476 -476
  32. package/src/dsp.ts +95 -95
  33. package/src/highlight.ts +284 -284
  34. package/src/image.ts +602 -602
  35. package/src/index.ts +718 -713
  36. package/src/subtitleConvert.ts +63 -63
  37. package/src/tests/audio.test.ts +96 -96
  38. package/src/tests/config.test.ts +496 -496
  39. package/src/tests/dsp.test.ts +24 -24
  40. package/src/tests/highlight.test.ts +178 -178
  41. package/src/tests/image.test.ts +222 -222
  42. package/src/tests/index.test.ts +180 -112
  43. package/src/tests/subtitleConvert.test.ts +23 -23
  44. package/src/tests/video.test.ts +285 -216
  45. package/src/typings.d.ts +6 -6
  46. package/src/video.ts +550 -542
  47. package/src/waitForEventLoop.ts +2 -2
package/dist/video.js CHANGED
@@ -1,427 +1,435 @@
1
- "use strict";
2
- var __assign = (this && this.__assign) || function () {
3
- __assign = Object.assign || function(t) {
4
- for (var s, i = 1, n = arguments.length; i < n; i++) {
5
- s = arguments[i];
6
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
- t[p] = s[p];
8
- }
9
- return t;
10
- };
11
- return __assign.apply(this, arguments);
12
- };
13
- var __spreadArrays = (this && this.__spreadArrays) || function () {
14
- for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
15
- for (var r = Array(s), k = 0, i = 0; i < il; i++)
16
- for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)
17
- r[k] = a[j];
18
- return r;
19
- };
20
- var __importDefault = (this && this.__importDefault) || function (mod) {
21
- return (mod && mod.__esModule) ? mod : { "default": mod };
22
- };
23
- Object.defineProperty(exports, "__esModule", { value: true });
24
- var child_process_1 = require("child_process");
25
- var fs_1 = require("fs");
26
- var path_1 = require("path");
27
- var os_1 = require("os");
28
- var ffmpeg_static_1 = __importDefault(require("ffmpeg-static"));
29
- var config_1 = require("./config");
30
- var escapeSubtitleFilterPath = function (subtitlePath) {
31
- return subtitlePath
32
- .replace(/\\/g, '\\\\')
33
- .replace(/:/g, '\\:')
34
- .replace(/'/g, "\\'")
35
- .replace(/,/g, '\\,')
36
- .replace(/\[/g, '\\[')
37
- .replace(/\]/g, '\\]');
38
- };
39
- exports.spawnFfmpegVideoWriter = function (config) {
40
- var _a, _b, _c, _d, _e;
41
- if (!ffmpeg_static_1.default) {
42
- throw new Error('ffmpeg path not found');
43
- }
44
- var crf = config.crf || '23';
45
- var preset = config.preset || 'medium';
46
- var args = ['-y'];
47
- var seek = (_b = (_a = config.audioSegment) === null || _a === void 0 ? void 0 : _a.seekSeconds, (_b !== null && _b !== void 0 ? _b : config.audioSeekSeconds));
48
- var duration = (_d = (_c = config.audioSegment) === null || _c === void 0 ? void 0 : _c.durationSeconds, (_d !== null && _d !== void 0 ? _d : config.audioDurationSeconds));
49
- if (seek !== undefined && duration !== undefined) {
50
- args.push('-ss', String(seek), '-t', String(duration));
51
- }
52
- args.push('-i', config.audioFilename, '-f', 'image2pipe', '-vcodec', 'bmp', '-framerate', "" + config.fps, '-i', '-', '-crf', crf, '-c:a', 'aac', '-b:a', '384k', '-profile:a', 'aac_low', '-c:v', 'libx264', '-r', "" + config.fps, '-pix_fmt', 'yuv420p', '-preset', preset, '-shortest');
53
- if (config.subtitleFilename) {
54
- var subPath = escapeSubtitleFilterPath(config.subtitleFilename);
55
- var alignment = (_e = config.subtitleAlignmentAss, (_e !== null && _e !== void 0 ? _e : 2));
56
- args.push('-vf', "subtitles='" + subPath + "':force_style='Alignment=" + alignment + "'");
57
- }
58
- args.push(config.videoFileName);
59
- var ffmpeg = child_process_1.spawn(ffmpeg_static_1.default, args);
60
- if (config.onStderr) {
61
- ffmpeg.stderr.on('data', config.onStderr);
62
- }
63
- return ffmpeg;
64
- };
65
- exports.detectSceneChanges = function (videoPath, threshold) {
66
- if (threshold === void 0) { threshold = 0.4; }
67
- return new Promise(function (resolve, reject) {
68
- if (!ffmpeg_static_1.default) {
69
- reject(new Error('ffmpeg path not found'));
70
- return;
71
- }
72
- var ffmpeg = child_process_1.spawn(ffmpeg_static_1.default, [
73
- '-i', videoPath,
74
- '-filter:v', "select='gt(scene," + threshold + ")',showinfo",
75
- '-f', 'null',
76
- '-'
77
- ]);
78
- var stderr = '';
79
- ffmpeg.stderr.on('data', function (data) { stderr += data.toString(); });
80
- ffmpeg.on('close', function () {
81
- var sceneChanges = [];
82
- for (var _i = 0, _a = stderr.split('\n'); _i < _a.length; _i++) {
83
- var line = _a[_i];
84
- if (!line.includes('Parsed_showinfo'))
85
- continue;
86
- var nMatch = line.match(/n:\s*(\d+)/);
87
- var ptsMatch = line.match(/pts:\s*(\d+)/);
88
- var ptsTimeMatch = line.match(/pts_time:\s*([\d.]+)/);
89
- if (nMatch && ptsTimeMatch) {
90
- sceneChanges.push({
91
- frameNumber: parseInt(nMatch[1]),
92
- pts: ptsMatch ? parseInt(ptsMatch[1]) : 0,
93
- ptsTime: parseFloat(ptsTimeMatch[1]),
94
- });
95
- }
96
- }
97
- resolve(sceneChanges);
98
- });
99
- ffmpeg.on('error', reject);
100
- });
101
- };
102
- exports.getVideoInfo = function (videoPath) {
103
- return new Promise(function (resolvePromise, reject) {
104
- if (!ffmpeg_static_1.default) {
105
- reject(new Error('ffmpeg path not found'));
106
- return;
107
- }
108
- var ffmpeg = child_process_1.spawn(ffmpeg_static_1.default, ['-i', videoPath, '-hide_banner']);
109
- var stderr = '';
110
- ffmpeg.stderr.on('data', function (data) { stderr += data.toString(); });
111
- ffmpeg.on('close', function () {
112
- var dimMatch = stderr.match(/Stream.*Video:.*?(\d{2,5})x(\d{2,5})/);
113
- if (!dimMatch) {
114
- reject(new Error("Could not determine video dimensions for: " + videoPath));
115
- return;
116
- }
117
- var durMatch = stderr.match(/Duration:\s*(\d+):(\d+):([\d.]+)/);
118
- var duration = durMatch
119
- ? parseInt(durMatch[1]) * 3600 + parseInt(durMatch[2]) * 60 + parseFloat(durMatch[3])
120
- : 0;
121
- resolvePromise({
122
- width: parseInt(dimMatch[1]),
123
- height: parseInt(dimMatch[2]),
124
- duration: duration,
125
- });
126
- });
127
- });
128
- };
129
- var isOrientationMismatch = function (sw, sh, tw, th) {
130
- return (sw > sh && tw < th) || (sw < sh && tw > th);
131
- };
132
- exports.buildVideoFilter = function (sourceWidth, sourceHeight, targetWidth, targetHeight) {
133
- if (!isOrientationMismatch(sourceWidth, sourceHeight, targetWidth, targetHeight)) {
134
- return "scale=" + targetWidth + ":" + targetHeight;
135
- }
136
- var targetAspect = targetWidth / targetHeight;
137
- var makeEven = function (n) { return Math.floor(n / 2) * 2; };
138
- var cropW, cropH, cropX, cropY;
139
- if (sourceWidth / sourceHeight > targetAspect) {
140
- cropW = makeEven(Math.round(sourceHeight * targetAspect));
141
- cropH = makeEven(sourceHeight);
142
- cropX = Math.floor((sourceWidth - cropW) / 2);
143
- cropY = Math.floor((sourceHeight - cropH) / 2);
144
- }
145
- else {
146
- cropW = makeEven(sourceWidth);
147
- cropH = makeEven(Math.round(sourceWidth / targetAspect));
148
- cropX = Math.floor((sourceWidth - cropW) / 2);
149
- cropY = Math.floor((sourceHeight - cropH) / 2);
150
- }
151
- return "crop=" + cropW + ":" + cropH + ":" + cropX + ":" + cropY + ",scale=" + targetWidth + ":" + targetHeight;
152
- };
153
- exports.spawnVideoFrameReader = function (config) {
154
- if (!ffmpeg_static_1.default) {
155
- throw new Error('ffmpeg path not found');
156
- }
157
- var args = [
158
- '-stream_loop', '-1',
159
- '-i', config.videoPath,
160
- '-r', "" + config.fps,
161
- '-frames:v', "" + config.totalFrames,
162
- ];
163
- if (config.width && config.height) {
164
- var filter = config.sourceWidth && config.sourceHeight
165
- ? exports.buildVideoFilter(config.sourceWidth, config.sourceHeight, config.width, config.height)
166
- : "scale=" + config.width + ":" + config.height;
167
- args.push('-vf', filter);
168
- }
169
- args.push('-f', 'rawvideo', '-pix_fmt', 'bgr24', '-v', 'quiet', 'pipe:1');
170
- return child_process_1.spawn(ffmpeg_static_1.default, args);
171
- };
172
- exports.readVideoFrame = function (stream, frameSize, timeoutMs) {
173
- if (timeoutMs === void 0) { timeoutMs = config_1.defaults.timeouts.readVideoFrame; }
174
- return new Promise(function (resolve) {
175
- var timer;
176
- var cleanup = function () {
177
- if (timer) {
178
- clearTimeout(timer);
179
- timer = undefined;
180
- }
181
- stream.removeListener('readable', onReadable);
182
- stream.removeListener('end', onEnd);
183
- stream.removeListener('error', onErrorOrClose);
184
- stream.removeListener('close', onErrorOrClose);
185
- };
186
- var onReadable = function () {
187
- tryRead();
188
- };
189
- var onEnd = function () {
190
- cleanup();
191
- resolve(null);
192
- };
193
- var onErrorOrClose = function () {
194
- cleanup();
195
- resolve(null);
196
- };
197
- var tryRead = function () {
198
- var data = stream.read(frameSize);
199
- if (data !== null) {
200
- cleanup();
201
- resolve(data.length === frameSize ? data : null);
202
- return;
203
- }
204
- if (timeoutMs > 0) {
205
- timer = setTimeout(function () {
206
- console.error('readVideoFrame timeout:', timeoutMs);
207
- cleanup();
208
- resolve(null);
209
- }, timeoutMs);
210
- }
211
- stream.once('readable', onReadable);
212
- stream.once('end', onEnd);
213
- stream.once('error', onErrorOrClose);
214
- stream.once('close', onErrorOrClose);
215
- };
216
- tryRead();
217
- });
218
- };
219
- exports.getProgress = function (onProgress) {
220
- return function (stderrOutput) {
221
- var matchResult = stderrOutput.toString().match(/frame=[ ]+(\d+)/);
222
- if (!matchResult) {
223
- return;
224
- }
225
- var currentFrame = +matchResult[1];
226
- if (isNaN(currentFrame)) {
227
- return;
228
- }
229
- onProgress(currentFrame);
230
- };
231
- };
232
- exports.calculateProgress = function (framesCount, callback) {
233
- return function (currentFrame) {
234
- return callback(+(currentFrame / framesCount * 100).toFixed(2));
235
- };
236
- };
237
- exports.waitDrain = function (stream, processStream, timeoutMs) {
238
- if (timeoutMs === void 0) { timeoutMs = config_1.defaults.timeouts.waitDrain; }
239
- return new Promise(function (resolve) {
240
- var _a, _b, _c;
241
- var cleanup = function () {
242
- var _a, _b, _c;
243
- if (timer) {
244
- clearTimeout(timer);
245
- timer = undefined;
246
- }
247
- stream.removeListener('drain', onDrain);
248
- stream.removeListener('error', onErrorOrClose);
249
- stream.removeListener('close', onErrorOrClose);
250
- (_a = processStream) === null || _a === void 0 ? void 0 : _a.removeListener('error', onErrorOrClose);
251
- (_b = processStream) === null || _b === void 0 ? void 0 : _b.removeListener('exit', onErrorOrClose);
252
- (_c = processStream) === null || _c === void 0 ? void 0 : _c.removeListener('close', onErrorOrClose);
253
- };
254
- var onDrain = function () {
255
- cleanup();
256
- resolve(true);
257
- };
258
- var onErrorOrClose = function () {
259
- cleanup();
260
- resolve(false);
261
- };
262
- var timer;
263
- if (timeoutMs > 0) {
264
- timer = setTimeout(function () {
265
- console.error('waitDrain timeout:', timeoutMs);
266
- cleanup();
267
- resolve(false);
268
- }, timeoutMs);
269
- }
270
- stream.once('drain', onDrain);
271
- stream.once('error', onErrorOrClose);
272
- stream.once('close', onErrorOrClose);
273
- (_a = processStream) === null || _a === void 0 ? void 0 : _a.once('error', onErrorOrClose);
274
- (_b = processStream) === null || _b === void 0 ? void 0 : _b.once('exit', onErrorOrClose);
275
- (_c = processStream) === null || _c === void 0 ? void 0 : _c.once('close', onErrorOrClose);
276
- });
277
- };
278
- var resolveProcessExit = function (resolve, exitCode, reason) {
279
- resolve(__assign({ exitCode: exitCode }, (reason && { reason: reason })));
280
- };
281
- exports.waitForProcessExit = function (processStream, timeoutMs) {
282
- if (timeoutMs === void 0) { timeoutMs = config_1.defaults.timeouts.waitForProcessExit; }
283
- return new Promise(function (resolve) {
284
- var cleanup = function () {
285
- if (timer) {
286
- clearTimeout(timer);
287
- timer = undefined;
288
- }
289
- processStream.removeListener('exit', onExit);
290
- processStream.removeListener('close', onClose);
291
- processStream.removeListener('error', onError);
292
- };
293
- var onExit = function (code) {
294
- cleanup();
295
- var exitCode = (code !== null && code !== void 0 ? code : 0);
296
- resolveProcessExit(resolve, exitCode, exitCode !== 0 ? "ffmpeg exited with code " + exitCode : undefined);
297
- };
298
- var onClose = function (code) {
299
- cleanup();
300
- var exitCode = (code !== null && code !== void 0 ? code : 0);
301
- resolveProcessExit(resolve, exitCode, exitCode !== 0 ? "ffmpeg exited with code " + exitCode : undefined);
302
- };
303
- var onError = function () {
304
- cleanup();
305
- resolveProcessExit(resolve, 1, 'ffmpeg process error');
306
- };
307
- var timer;
308
- if (timeoutMs > 0) {
309
- timer = setTimeout(function () {
310
- console.error('waitForProcessExit timeout:', timeoutMs);
311
- cleanup();
312
- resolveProcessExit(resolve, 1, "waitForProcessExit timeout (" + timeoutMs + "ms)");
313
- }, timeoutMs);
314
- }
315
- processStream.once('exit', onExit);
316
- processStream.once('close', onClose);
317
- processStream.once('error', onError);
318
- });
319
- };
320
- var mergeSmallScenes = function (sceneChanges, videoDuration, minDuration) {
321
- if (minDuration === void 0) { minDuration = 2; }
322
- if (sceneChanges.length === 0)
323
- return [];
324
- var merged = [sceneChanges[0]];
325
- for (var i = 1; i < sceneChanges.length; i++) {
326
- var lastKept = merged[merged.length - 1];
327
- var gap = sceneChanges[i].ptsTime - lastKept.ptsTime;
328
- if (gap >= minDuration) {
329
- merged.push(sceneChanges[i]);
330
- }
331
- }
332
- var lastScene = merged[merged.length - 1];
333
- if (videoDuration - lastScene.ptsTime < minDuration && merged.length > 1) {
334
- merged.pop();
335
- }
336
- return merged;
337
- };
338
- exports.buildBeatSyncedSegments = function (beatFrameIndices, totalFrames, sceneChanges, videoDuration) {
339
- if (sceneChanges.length === 0) {
340
- return [
341
- {
342
- outputStartFrame: 0,
343
- videoSeekSeconds: 0,
344
- frameCount: totalFrames,
345
- },
346
- ];
347
- }
348
- var consolidated = mergeSmallScenes(sceneChanges, videoDuration);
349
- var seekPositions;
350
- if (consolidated.length >= 2) {
351
- seekPositions = consolidated.map(function (sc) { return sc.ptsTime; });
352
- }
353
- else {
354
- var count_1 = Math.max(10, Math.ceil(videoDuration / 3));
355
- seekPositions = Array.from({ length: count_1 }, function (_, i) { return (i * videoDuration) / count_1; });
356
- }
357
- var boundaries = __spreadArrays([0], beatFrameIndices);
358
- if (boundaries[boundaries.length - 1] !== totalFrames) {
359
- boundaries.push(totalFrames);
360
- }
361
- var unique = Array.from(new Set(boundaries)).sort(function (a, b) { return a - b; });
362
- var segments = [];
363
- for (var i = 0; i < unique.length - 1; i++) {
364
- segments.push({
365
- outputStartFrame: unique[i],
366
- videoSeekSeconds: seekPositions[i % seekPositions.length],
367
- frameCount: unique[i + 1] - unique[i],
368
- });
369
- }
370
- return segments;
371
- };
372
- exports.writeConcatFile = function (segments, videoPath, videoDuration, fps) {
373
- var absVideoPath = path_1.resolve(videoPath).replace(/\\/g, '/');
374
- var escaped = absVideoPath.replace(/'/g, "'\\''");
375
- var content = '';
376
- for (var _i = 0, segments_1 = segments; _i < segments_1.length; _i++) {
377
- var seg = segments_1[_i];
378
- var segDuration = seg.frameCount / fps;
379
- var remaining = segDuration;
380
- var pos = seg.videoSeekSeconds % videoDuration;
381
- while (remaining > 0.001) {
382
- var available = videoDuration - pos;
383
- var take = Math.min(remaining, available);
384
- content += "file '" + escaped + "'\n";
385
- content += "inpoint " + pos.toFixed(6) + "\n";
386
- content += "outpoint " + (pos + take).toFixed(6) + "\n\n";
387
- remaining -= take;
388
- pos = 0;
389
- }
390
- }
391
- var concatPath = path_1.join(os_1.tmpdir(), "av-concat-" + Date.now() + ".txt");
392
- fs_1.writeFileSync(concatPath, content, 'utf-8');
393
- return concatPath;
394
- };
395
- exports.writeSubtitlesFile = function (subtitles) {
396
- var subtitlePath = path_1.join(os_1.tmpdir(), "av-subtitles-" + Date.now() + ".srt");
397
- fs_1.writeFileSync(subtitlePath, subtitles, 'utf-8');
398
- return subtitlePath;
399
- };
400
- exports.spawnConcatVideoFrameReader = function (config) {
401
- if (!ffmpeg_static_1.default) {
402
- throw new Error('ffmpeg path not found');
403
- }
404
- var args = [
405
- '-f', 'concat',
406
- '-safe', '0',
407
- '-i', config.concatFilePath,
408
- '-r', "" + config.fps,
409
- '-frames:v', "" + config.totalFrames,
410
- ];
411
- if (config.width && config.height) {
412
- var filter = config.sourceWidth && config.sourceHeight
413
- ? exports.buildVideoFilter(config.sourceWidth, config.sourceHeight, config.width, config.height)
414
- : "scale=" + config.width + ":" + config.height;
415
- args.push('-vf', filter);
416
- }
417
- args.push('-f', 'rawvideo', '-pix_fmt', 'bgr24', '-v', 'quiet', 'pipe:1');
418
- return child_process_1.spawn(ffmpeg_static_1.default, args);
419
- };
420
- exports.cleanupTempFile = function (filePath) {
421
- try {
422
- fs_1.unlinkSync(filePath);
423
- }
424
- catch (_a) { }
425
- };
426
- exports.cleanupConcatFile = exports.cleanupTempFile;
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __spreadArrays = (this && this.__spreadArrays) || function () {
14
+ for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
15
+ for (var r = Array(s), k = 0, i = 0; i < il; i++)
16
+ for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)
17
+ r[k] = a[j];
18
+ return r;
19
+ };
20
+ var __importDefault = (this && this.__importDefault) || function (mod) {
21
+ return (mod && mod.__esModule) ? mod : { "default": mod };
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ var child_process_1 = require("child_process");
25
+ var fs_1 = require("fs");
26
+ var path_1 = require("path");
27
+ var os_1 = require("os");
28
+ var ffmpeg_static_1 = __importDefault(require("ffmpeg-static"));
29
+ var config_1 = require("./config");
30
+ var escapeSubtitleFilterPath = function (subtitlePath) {
31
+ return subtitlePath
32
+ .replace(/\\/g, '\\\\')
33
+ .replace(/:/g, '\\:')
34
+ .replace(/'/g, "\\'")
35
+ .replace(/,/g, '\\,')
36
+ .replace(/\[/g, '\\[')
37
+ .replace(/\]/g, '\\]');
38
+ };
39
+ exports.spawnFfmpegVideoWriter = function (config) {
40
+ var _a, _b, _c, _d, _e;
41
+ if (!ffmpeg_static_1.default) {
42
+ throw new Error('ffmpeg path not found');
43
+ }
44
+ var crf = config.crf || '23';
45
+ var preset = config.preset || 'medium';
46
+ var args = ['-y'];
47
+ var seek = (_b = (_a = config.audioSegment) === null || _a === void 0 ? void 0 : _a.seekSeconds, (_b !== null && _b !== void 0 ? _b : config.audioSeekSeconds));
48
+ var duration = (_d = (_c = config.audioSegment) === null || _c === void 0 ? void 0 : _c.durationSeconds, (_d !== null && _d !== void 0 ? _d : config.audioDurationSeconds));
49
+ if (seek !== undefined && duration !== undefined) {
50
+ args.push('-ss', String(seek), '-t', String(duration));
51
+ }
52
+ args.push('-i', config.audioFilename, '-f', 'image2pipe', '-vcodec', 'bmp', '-framerate', "" + config.fps, '-i', '-', '-crf', crf, '-c:a', 'aac', '-b:a', '384k', '-profile:a', 'aac_low', '-c:v', 'libx264', '-r', "" + config.fps, '-pix_fmt', 'yuv420p', '-preset', preset, '-shortest');
53
+ if (config.subtitleFilename) {
54
+ var subPath = escapeSubtitleFilterPath(config.subtitleFilename);
55
+ var alignment = (_e = config.subtitleAlignmentAss, (_e !== null && _e !== void 0 ? _e : 2));
56
+ args.push('-vf', "subtitles='" + subPath + "':force_style='Alignment=" + alignment + "'");
57
+ }
58
+ args.push(config.videoFileName);
59
+ var ffmpeg = child_process_1.spawn(ffmpeg_static_1.default, args);
60
+ if (config.onStderr) {
61
+ ffmpeg.stderr.on('data', config.onStderr);
62
+ }
63
+ return ffmpeg;
64
+ };
65
+ exports.detectSceneChanges = function (videoPath, threshold) {
66
+ if (threshold === void 0) { threshold = 0.4; }
67
+ return new Promise(function (resolve, reject) {
68
+ if (!ffmpeg_static_1.default) {
69
+ reject(new Error('ffmpeg path not found'));
70
+ return;
71
+ }
72
+ var ffmpeg = child_process_1.spawn(ffmpeg_static_1.default, [
73
+ '-i', videoPath,
74
+ '-filter:v', "select='gt(scene," + threshold + ")',showinfo",
75
+ '-f', 'null',
76
+ '-'
77
+ ]);
78
+ var stderr = '';
79
+ ffmpeg.stderr.on('data', function (data) { stderr += data.toString(); });
80
+ ffmpeg.on('close', function () {
81
+ var sceneChanges = [];
82
+ for (var _i = 0, _a = stderr.split('\n'); _i < _a.length; _i++) {
83
+ var line = _a[_i];
84
+ if (!line.includes('Parsed_showinfo'))
85
+ continue;
86
+ var nMatch = line.match(/n:\s*(\d+)/);
87
+ var ptsMatch = line.match(/pts:\s*(\d+)/);
88
+ var ptsTimeMatch = line.match(/pts_time:\s*([\d.]+)/);
89
+ if (nMatch && ptsTimeMatch) {
90
+ sceneChanges.push({
91
+ frameNumber: parseInt(nMatch[1]),
92
+ pts: ptsMatch ? parseInt(ptsMatch[1]) : 0,
93
+ ptsTime: parseFloat(ptsTimeMatch[1]),
94
+ });
95
+ }
96
+ }
97
+ resolve(sceneChanges);
98
+ });
99
+ ffmpeg.on('error', reject);
100
+ });
101
+ };
102
+ exports.getVideoInfo = function (videoPath) {
103
+ return new Promise(function (resolvePromise, reject) {
104
+ if (!ffmpeg_static_1.default) {
105
+ reject(new Error('ffmpeg path not found'));
106
+ return;
107
+ }
108
+ var ffmpeg = child_process_1.spawn(ffmpeg_static_1.default, ['-i', videoPath, '-hide_banner']);
109
+ var stderr = '';
110
+ ffmpeg.stderr.on('data', function (data) { stderr += data.toString(); });
111
+ ffmpeg.on('close', function () {
112
+ var dimMatch = stderr.match(/Stream.*Video:.*?(\d{2,5})x(\d{2,5})/);
113
+ if (!dimMatch) {
114
+ reject(new Error("Could not determine video dimensions for: " + videoPath));
115
+ return;
116
+ }
117
+ var durMatch = stderr.match(/Duration:\s*(\d+):(\d+):([\d.]+)/);
118
+ var duration = durMatch
119
+ ? parseInt(durMatch[1]) * 3600 + parseInt(durMatch[2]) * 60 + parseFloat(durMatch[3])
120
+ : 0;
121
+ resolvePromise({
122
+ width: parseInt(dimMatch[1]),
123
+ height: parseInt(dimMatch[2]),
124
+ duration: duration,
125
+ });
126
+ });
127
+ });
128
+ };
129
+ var isOrientationMismatch = function (sw, sh, tw, th) {
130
+ return (sw > sh && tw < th) || (sw < sh && tw > th);
131
+ };
132
+ exports.buildVideoFilter = function (sourceWidth, sourceHeight, targetWidth, targetHeight) {
133
+ if (!isOrientationMismatch(sourceWidth, sourceHeight, targetWidth, targetHeight)) {
134
+ return "scale=" + targetWidth + ":" + targetHeight;
135
+ }
136
+ var targetAspect = targetWidth / targetHeight;
137
+ var makeEven = function (n) { return Math.floor(n / 2) * 2; };
138
+ var cropW, cropH, cropX, cropY;
139
+ if (sourceWidth / sourceHeight > targetAspect) {
140
+ cropW = makeEven(Math.round(sourceHeight * targetAspect));
141
+ cropH = makeEven(sourceHeight);
142
+ cropX = Math.floor((sourceWidth - cropW) / 2);
143
+ cropY = Math.floor((sourceHeight - cropH) / 2);
144
+ }
145
+ else {
146
+ cropW = makeEven(sourceWidth);
147
+ cropH = makeEven(Math.round(sourceWidth / targetAspect));
148
+ cropX = Math.floor((sourceWidth - cropW) / 2);
149
+ cropY = Math.floor((sourceHeight - cropH) / 2);
150
+ }
151
+ return "crop=" + cropW + ":" + cropH + ":" + cropX + ":" + cropY + ",scale=" + targetWidth + ":" + targetHeight;
152
+ };
153
+ exports.spawnVideoFrameReader = function (config) {
154
+ if (!ffmpeg_static_1.default) {
155
+ throw new Error('ffmpeg path not found');
156
+ }
157
+ var args = [
158
+ '-stream_loop', '-1',
159
+ '-i', config.videoPath,
160
+ '-r', "" + config.fps,
161
+ '-frames:v', "" + config.totalFrames,
162
+ ];
163
+ if (config.width && config.height) {
164
+ var filter = config.sourceWidth && config.sourceHeight
165
+ ? exports.buildVideoFilter(config.sourceWidth, config.sourceHeight, config.width, config.height)
166
+ : "scale=" + config.width + ":" + config.height;
167
+ args.push('-vf', filter);
168
+ }
169
+ args.push('-f', 'rawvideo', '-pix_fmt', 'bgr24', '-v', 'quiet', 'pipe:1');
170
+ return child_process_1.spawn(ffmpeg_static_1.default, args);
171
+ };
172
+ exports.readVideoFrame = function (stream, frameSize, timeoutMs) {
173
+ if (timeoutMs === void 0) { timeoutMs = config_1.defaults.timeouts.readVideoFrame; }
174
+ return new Promise(function (resolve) {
175
+ var timer;
176
+ var settled = false;
177
+ var finish = function (result) {
178
+ if (settled) {
179
+ return;
180
+ }
181
+ settled = true;
182
+ if (timer) {
183
+ clearTimeout(timer);
184
+ timer = undefined;
185
+ }
186
+ stream.removeListener('readable', onReadable);
187
+ stream.removeListener('end', onEnd);
188
+ stream.removeListener('error', onErrorOrClose);
189
+ stream.removeListener('close', onErrorOrClose);
190
+ resolve(result);
191
+ };
192
+ var onReadable = function () {
193
+ tryRead();
194
+ };
195
+ var onEnd = function () {
196
+ finish(null);
197
+ };
198
+ var onErrorOrClose = function () {
199
+ finish(null);
200
+ };
201
+ var tryRead = function () {
202
+ var data = stream.read(frameSize);
203
+ if (data !== null) {
204
+ finish(data.length === frameSize ? data : null);
205
+ }
206
+ };
207
+ tryRead();
208
+ if (settled) {
209
+ return;
210
+ }
211
+ if (timeoutMs > 0) {
212
+ timer = setTimeout(function () {
213
+ console.error('readVideoFrame timeout:', timeoutMs);
214
+ finish(null);
215
+ }, timeoutMs);
216
+ }
217
+ stream.on('readable', onReadable);
218
+ stream.once('end', onEnd);
219
+ stream.once('error', onErrorOrClose);
220
+ stream.once('close', onErrorOrClose);
221
+ });
222
+ };
223
+ exports.getProgress = function (onProgress) {
224
+ return function (stderrOutput) {
225
+ var matchResult = stderrOutput.toString().match(/frame=[ ]+(\d+)/);
226
+ if (!matchResult) {
227
+ return;
228
+ }
229
+ var currentFrame = +matchResult[1];
230
+ if (isNaN(currentFrame)) {
231
+ return;
232
+ }
233
+ onProgress(currentFrame);
234
+ };
235
+ };
236
+ exports.calculateProgress = function (framesCount, callback) {
237
+ return function (currentFrame) {
238
+ return callback(+(currentFrame / framesCount * 100).toFixed(2));
239
+ };
240
+ };
241
+ exports.waitDrain = function (stream, processStream, timeoutMs) {
242
+ if (timeoutMs === void 0) { timeoutMs = config_1.defaults.timeouts.waitDrain; }
243
+ return new Promise(function (resolve) {
244
+ var _a, _b, _c;
245
+ var cleanup = function () {
246
+ var _a, _b, _c;
247
+ if (timer) {
248
+ clearTimeout(timer);
249
+ timer = undefined;
250
+ }
251
+ stream.removeListener('drain', onDrain);
252
+ stream.removeListener('error', onErrorOrClose);
253
+ stream.removeListener('close', onErrorOrClose);
254
+ (_a = processStream) === null || _a === void 0 ? void 0 : _a.removeListener('error', onErrorOrClose);
255
+ (_b = processStream) === null || _b === void 0 ? void 0 : _b.removeListener('exit', onErrorOrClose);
256
+ (_c = processStream) === null || _c === void 0 ? void 0 : _c.removeListener('close', onErrorOrClose);
257
+ };
258
+ var onDrain = function () {
259
+ cleanup();
260
+ resolve(true);
261
+ };
262
+ var onErrorOrClose = function () {
263
+ cleanup();
264
+ resolve(false);
265
+ };
266
+ var timer;
267
+ if (timeoutMs > 0) {
268
+ timer = setTimeout(function () {
269
+ console.error('waitDrain timeout:', timeoutMs);
270
+ cleanup();
271
+ resolve(false);
272
+ }, timeoutMs);
273
+ }
274
+ stream.once('drain', onDrain);
275
+ stream.once('error', onErrorOrClose);
276
+ stream.once('close', onErrorOrClose);
277
+ (_a = processStream) === null || _a === void 0 ? void 0 : _a.once('error', onErrorOrClose);
278
+ (_b = processStream) === null || _b === void 0 ? void 0 : _b.once('exit', onErrorOrClose);
279
+ (_c = processStream) === null || _c === void 0 ? void 0 : _c.once('close', onErrorOrClose);
280
+ });
281
+ };
282
+ var resolveProcessExit = function (resolve, exitCode, reason) {
283
+ resolve(__assign({ exitCode: exitCode }, (reason && { reason: reason })));
284
+ };
285
+ exports.waitForProcessExit = function (processStream, timeoutMs) {
286
+ if (timeoutMs === void 0) { timeoutMs = config_1.defaults.timeouts.waitForProcessExit; }
287
+ return new Promise(function (resolve) {
288
+ var cleanup = function () {
289
+ if (timer) {
290
+ clearTimeout(timer);
291
+ timer = undefined;
292
+ }
293
+ processStream.removeListener('exit', onExit);
294
+ processStream.removeListener('close', onClose);
295
+ processStream.removeListener('error', onError);
296
+ };
297
+ var onExit = function (code) {
298
+ cleanup();
299
+ var exitCode = (code !== null && code !== void 0 ? code : 0);
300
+ resolveProcessExit(resolve, exitCode, exitCode !== 0 ? "ffmpeg exited with code " + exitCode : undefined);
301
+ };
302
+ var onClose = function (code) {
303
+ cleanup();
304
+ var exitCode = (code !== null && code !== void 0 ? code : 0);
305
+ resolveProcessExit(resolve, exitCode, exitCode !== 0 ? "ffmpeg exited with code " + exitCode : undefined);
306
+ };
307
+ var onError = function () {
308
+ cleanup();
309
+ resolveProcessExit(resolve, 1, 'ffmpeg process error');
310
+ };
311
+ var timer;
312
+ if (timeoutMs > 0) {
313
+ timer = setTimeout(function () {
314
+ console.error('waitForProcessExit timeout:', timeoutMs);
315
+ cleanup();
316
+ resolveProcessExit(resolve, 1, "waitForProcessExit timeout (" + timeoutMs + "ms)");
317
+ }, timeoutMs);
318
+ }
319
+ processStream.once('exit', onExit);
320
+ processStream.once('close', onClose);
321
+ processStream.once('error', onError);
322
+ });
323
+ };
324
+ var mergeSmallScenes = function (sceneChanges, videoDuration, minDuration) {
325
+ if (minDuration === void 0) { minDuration = 2; }
326
+ if (sceneChanges.length === 0)
327
+ return [];
328
+ var merged = [sceneChanges[0]];
329
+ for (var i = 1; i < sceneChanges.length; i++) {
330
+ var lastKept = merged[merged.length - 1];
331
+ var gap = sceneChanges[i].ptsTime - lastKept.ptsTime;
332
+ if (gap >= minDuration) {
333
+ merged.push(sceneChanges[i]);
334
+ }
335
+ }
336
+ var lastScene = merged[merged.length - 1];
337
+ if (videoDuration - lastScene.ptsTime < minDuration && merged.length > 1) {
338
+ merged.pop();
339
+ }
340
+ return merged;
341
+ };
342
+ exports.buildBeatSyncedSegments = function (beatFrameIndices, totalFrames, sceneChanges, videoDuration) {
343
+ if (sceneChanges.length === 0) {
344
+ return [
345
+ {
346
+ outputStartFrame: 0,
347
+ videoSeekSeconds: 0,
348
+ frameCount: totalFrames,
349
+ },
350
+ ];
351
+ }
352
+ var consolidated = mergeSmallScenes(sceneChanges, videoDuration);
353
+ var seekPositions;
354
+ if (consolidated.length >= 2) {
355
+ seekPositions = consolidated.map(function (sc) { return sc.ptsTime; });
356
+ }
357
+ else {
358
+ var count_1 = Math.max(10, Math.ceil(videoDuration / 3));
359
+ seekPositions = Array.from({ length: count_1 }, function (_, i) { return (i * videoDuration) / count_1; });
360
+ }
361
+ var boundaries = __spreadArrays([0], beatFrameIndices);
362
+ if (boundaries[boundaries.length - 1] !== totalFrames) {
363
+ boundaries.push(totalFrames);
364
+ }
365
+ var unique = Array.from(new Set(boundaries)).sort(function (a, b) { return a - b; });
366
+ var segments = [];
367
+ for (var i = 0; i < unique.length - 1; i++) {
368
+ segments.push({
369
+ outputStartFrame: unique[i],
370
+ videoSeekSeconds: seekPositions[i % seekPositions.length],
371
+ frameCount: unique[i + 1] - unique[i],
372
+ });
373
+ }
374
+ return segments;
375
+ };
376
+ exports.writeConcatFile = function (segments, videoPath, videoDuration, fps) {
377
+ var absVideoPath = path_1.resolve(videoPath).replace(/\\/g, '/');
378
+ var escaped = absVideoPath.replace(/'/g, "'\\''");
379
+ var content = '';
380
+ for (var _i = 0, segments_1 = segments; _i < segments_1.length; _i++) {
381
+ var seg = segments_1[_i];
382
+ var segDuration = seg.frameCount / fps;
383
+ var remaining = segDuration;
384
+ var pos = seg.videoSeekSeconds % videoDuration;
385
+ while (remaining > 0.001) {
386
+ var available = videoDuration - pos;
387
+ var take = Math.min(remaining, available);
388
+ content += "file '" + escaped + "'\n";
389
+ content += "inpoint " + pos.toFixed(6) + "\n";
390
+ content += "outpoint " + (pos + take).toFixed(6) + "\n";
391
+ content += "duration " + take.toFixed(6) + "\n\n";
392
+ remaining -= take;
393
+ pos = 0;
394
+ }
395
+ }
396
+ var concatPath = path_1.join(os_1.tmpdir(), "av-concat-" + Date.now() + ".txt");
397
+ fs_1.writeFileSync(concatPath, content, 'utf-8');
398
+ return concatPath;
399
+ };
400
+ exports.writeSubtitlesFile = function (subtitles) {
401
+ var subtitlePath = path_1.join(os_1.tmpdir(), "av-subtitles-" + Date.now() + ".srt");
402
+ fs_1.writeFileSync(subtitlePath, subtitles, 'utf-8');
403
+ return subtitlePath;
404
+ };
405
+ exports.spawnConcatVideoFrameReader = function (config) {
406
+ if (!ffmpeg_static_1.default) {
407
+ throw new Error('ffmpeg path not found');
408
+ }
409
+ var args = [
410
+ '-f', 'concat',
411
+ '-safe', '0',
412
+ '-fflags', '+genpts',
413
+ '-i', config.concatFilePath,
414
+ '-an',
415
+ '-avoid_negative_ts', 'make_zero',
416
+ '-r', "" + config.fps,
417
+ '-frames:v', "" + config.totalFrames,
418
+ ];
419
+ if (config.width && config.height) {
420
+ var filter = config.sourceWidth && config.sourceHeight
421
+ ? exports.buildVideoFilter(config.sourceWidth, config.sourceHeight, config.width, config.height)
422
+ : "scale=" + config.width + ":" + config.height;
423
+ args.push('-vf', filter);
424
+ }
425
+ args.push('-f', 'rawvideo', '-pix_fmt', 'bgr24', '-v', 'quiet', 'pipe:1');
426
+ return child_process_1.spawn(ffmpeg_static_1.default, args);
427
+ };
428
+ exports.cleanupTempFile = function (filePath) {
429
+ try {
430
+ fs_1.unlinkSync(filePath);
431
+ }
432
+ catch (_a) { }
433
+ };
434
+ exports.cleanupConcatFile = exports.cleanupTempFile;
427
435
  //# sourceMappingURL=video.js.map