nodejs-audio-visualizer 5.1.0 → 5.2.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 (50) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +3 -2
  3. package/dist/audio.d.ts +13 -13
  4. package/dist/audio.js +128 -128
  5. package/dist/beats.d.ts +39 -39
  6. package/dist/beats.js +220 -220
  7. package/dist/bpmEncoder.d.ts +14 -14
  8. package/dist/bpmEncoder.js +97 -97
  9. package/dist/config.d.ts +78 -77
  10. package/dist/config.js +377 -369
  11. package/dist/config.js.map +1 -1
  12. package/dist/dsp.d.ts +16 -16
  13. package/dist/dsp.js +76 -76
  14. package/dist/highlight.d.ts +34 -34
  15. package/dist/highlight.js +334 -334
  16. package/dist/image.d.ts +97 -92
  17. package/dist/image.js +545 -538
  18. package/dist/image.js.map +1 -1
  19. package/dist/index.d.ts +87 -79
  20. package/dist/index.js +597 -566
  21. package/dist/index.js.map +1 -1
  22. package/dist/subtitleConvert.d.ts +3 -3
  23. package/dist/subtitleConvert.js +61 -61
  24. package/dist/video.d.ts +150 -120
  25. package/dist/video.js +677 -582
  26. package/dist/video.js.map +1 -1
  27. package/dist/waitForEventLoop.d.ts +1 -1
  28. package/dist/waitForEventLoop.js +5 -5
  29. package/package.json +62 -62
  30. package/src/audio.ts +129 -129
  31. package/src/beats.ts +279 -279
  32. package/src/bpmEncoder.ts +97 -97
  33. package/src/config.ts +487 -476
  34. package/src/dsp.ts +95 -95
  35. package/src/highlight.ts +291 -291
  36. package/src/image.ts +707 -700
  37. package/src/index.ts +79 -37
  38. package/src/subtitleConvert.ts +63 -63
  39. package/src/tests/audio.test.ts +102 -102
  40. package/src/tests/beats.test.ts +134 -134
  41. package/src/tests/config.test.ts +534 -496
  42. package/src/tests/dsp.test.ts +24 -24
  43. package/src/tests/highlight.test.ts +178 -178
  44. package/src/tests/image.test.ts +319 -308
  45. package/src/tests/index.test.ts +180 -180
  46. package/src/tests/subtitleConvert.test.ts +23 -23
  47. package/src/tests/video.test.ts +535 -447
  48. package/src/typings.d.ts +28 -28
  49. package/src/video.ts +856 -722
  50. package/src/waitForEventLoop.ts +2 -2
package/dist/video.js CHANGED
@@ -1,583 +1,678 @@
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 beats_1 = require("./beats");
31
- var escapeSubtitleFilterPath = function (subtitlePath) {
32
- return subtitlePath
33
- .replace(/\\/g, '\\\\')
34
- .replace(/:/g, '\\:')
35
- .replace(/'/g, "\\'")
36
- .replace(/,/g, '\\,')
37
- .replace(/\[/g, '\\[')
38
- .replace(/\]/g, '\\]');
39
- };
40
- exports.spawnFfmpegVideoWriter = function (config) {
41
- var _a, _b, _c, _d, _e;
42
- if (!ffmpeg_static_1.default) {
43
- throw new Error('ffmpeg path not found');
44
- }
45
- var crf = config.crf || '23';
46
- var preset = config.preset || 'medium';
47
- var args = ['-y'];
48
- var seek = (_b = (_a = config.audioSegment) === null || _a === void 0 ? void 0 : _a.seekSeconds, (_b !== null && _b !== void 0 ? _b : config.audioSeekSeconds));
49
- var duration = (_d = (_c = config.audioSegment) === null || _c === void 0 ? void 0 : _c.durationSeconds, (_d !== null && _d !== void 0 ? _d : config.audioDurationSeconds));
50
- if (seek !== undefined && duration !== undefined) {
51
- args.push('-ss', String(seek), '-t', String(duration));
52
- }
53
- 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');
54
- if (config.subtitleFilename) {
55
- var subPath = escapeSubtitleFilterPath(config.subtitleFilename);
56
- var alignment = (_e = config.subtitleAlignmentAss, (_e !== null && _e !== void 0 ? _e : 2));
57
- args.push('-vf', "subtitles='" + subPath + "':force_style='Alignment=" + alignment + "'");
58
- }
59
- args.push(config.videoFileName);
60
- var ffmpeg = child_process_1.spawn(ffmpeg_static_1.default, args);
61
- if (config.onStderr) {
62
- ffmpeg.stderr.on('data', config.onStderr);
63
- }
64
- return ffmpeg;
65
- };
66
- exports.detectSceneChanges = function (videoPath, threshold) {
67
- if (threshold === void 0) { threshold = 0.4; }
68
- return new Promise(function (resolve, reject) {
69
- if (!ffmpeg_static_1.default) {
70
- reject(new Error('ffmpeg path not found'));
71
- return;
72
- }
73
- var ffmpeg = child_process_1.spawn(ffmpeg_static_1.default, [
74
- '-i', videoPath,
75
- '-filter:v', "select='gt(scene," + threshold + ")',showinfo",
76
- '-f', 'null',
77
- '-'
78
- ]);
79
- var stderr = '';
80
- ffmpeg.stderr.on('data', function (data) { stderr += data.toString(); });
81
- ffmpeg.on('close', function () {
82
- var sceneChanges = [];
83
- for (var _i = 0, _a = stderr.split('\n'); _i < _a.length; _i++) {
84
- var line = _a[_i];
85
- if (!line.includes('Parsed_showinfo'))
86
- continue;
87
- var nMatch = line.match(/n:\s*(\d+)/);
88
- var ptsMatch = line.match(/pts:\s*(\d+)/);
89
- var ptsTimeMatch = line.match(/pts_time:\s*([\d.]+)/);
90
- if (nMatch && ptsTimeMatch) {
91
- sceneChanges.push({
92
- frameNumber: parseInt(nMatch[1]),
93
- pts: ptsMatch ? parseInt(ptsMatch[1]) : 0,
94
- ptsTime: parseFloat(ptsTimeMatch[1]),
95
- });
96
- }
97
- }
98
- resolve(sceneChanges);
99
- });
100
- ffmpeg.on('error', reject);
101
- });
102
- };
103
- exports.getVideoInfo = function (videoPath) {
104
- return new Promise(function (resolvePromise, reject) {
105
- if (!ffmpeg_static_1.default) {
106
- reject(new Error('ffmpeg path not found'));
107
- return;
108
- }
109
- var ffmpeg = child_process_1.spawn(ffmpeg_static_1.default, ['-i', videoPath, '-hide_banner']);
110
- var stderr = '';
111
- ffmpeg.stderr.on('data', function (data) { stderr += data.toString(); });
112
- ffmpeg.on('close', function () {
113
- var dimMatch = stderr.match(/Stream.*Video:.*?(\d{2,5})x(\d{2,5})/);
114
- if (!dimMatch) {
115
- reject(new Error("Could not determine video dimensions for: " + videoPath));
116
- return;
117
- }
118
- var durMatch = stderr.match(/Duration:\s*(\d+):(\d+):([\d.]+)/);
119
- var duration = durMatch
120
- ? parseInt(durMatch[1]) * 3600 + parseInt(durMatch[2]) * 60 + parseFloat(durMatch[3])
121
- : 0;
122
- resolvePromise({
123
- width: parseInt(dimMatch[1]),
124
- height: parseInt(dimMatch[2]),
125
- duration: duration,
126
- });
127
- });
128
- });
129
- };
130
- var isOrientationMismatch = function (sw, sh, tw, th) {
131
- return (sw > sh && tw < th) || (sw < sh && tw > th);
132
- };
133
- exports.buildVideoFilter = function (sourceWidth, sourceHeight, targetWidth, targetHeight) {
134
- if (!isOrientationMismatch(sourceWidth, sourceHeight, targetWidth, targetHeight)) {
135
- return "scale=" + targetWidth + ":" + targetHeight;
136
- }
137
- var targetAspect = targetWidth / targetHeight;
138
- var makeEven = function (n) { return Math.floor(n / 2) * 2; };
139
- var cropW, cropH, cropX, cropY;
140
- if (sourceWidth / sourceHeight > targetAspect) {
141
- cropW = makeEven(Math.round(sourceHeight * targetAspect));
142
- cropH = makeEven(sourceHeight);
143
- cropX = Math.floor((sourceWidth - cropW) / 2);
144
- cropY = Math.floor((sourceHeight - cropH) / 2);
145
- }
146
- else {
147
- cropW = makeEven(sourceWidth);
148
- cropH = makeEven(Math.round(sourceWidth / targetAspect));
149
- cropX = Math.floor((sourceWidth - cropW) / 2);
150
- cropY = Math.floor((sourceHeight - cropH) / 2);
151
- }
152
- return "crop=" + cropW + ":" + cropH + ":" + cropX + ":" + cropY + ",scale=" + targetWidth + ":" + targetHeight;
153
- };
154
- exports.spawnVideoFrameReader = function (config) {
155
- if (!ffmpeg_static_1.default) {
156
- throw new Error('ffmpeg path not found');
157
- }
158
- var args = [
159
- '-stream_loop', '-1',
160
- '-i', config.videoPath,
161
- '-r', "" + config.fps,
162
- '-frames:v', "" + config.totalFrames,
163
- ];
164
- if (config.width && config.height) {
165
- var filter = config.sourceWidth && config.sourceHeight
166
- ? exports.buildVideoFilter(config.sourceWidth, config.sourceHeight, config.width, config.height)
167
- : "scale=" + config.width + ":" + config.height;
168
- args.push('-vf', filter);
169
- }
170
- args.push('-f', 'rawvideo', '-pix_fmt', 'bgr24', '-v', 'quiet', 'pipe:1');
171
- return child_process_1.spawn(ffmpeg_static_1.default, args);
172
- };
173
- exports.readVideoFrame = function (stream, frameSize, timeoutMs) {
174
- if (timeoutMs === void 0) { timeoutMs = config_1.defaults.timeouts.readVideoFrame; }
175
- return new Promise(function (resolve) {
176
- var timer;
177
- var settled = false;
178
- var finish = function (result) {
179
- if (settled) {
180
- return;
181
- }
182
- settled = true;
183
- if (timer) {
184
- clearTimeout(timer);
185
- timer = undefined;
186
- }
187
- stream.removeListener('readable', onReadable);
188
- stream.removeListener('end', onEnd);
189
- stream.removeListener('error', onErrorOrClose);
190
- stream.removeListener('close', onErrorOrClose);
191
- resolve(result);
192
- };
193
- var onReadable = function () {
194
- tryRead();
195
- };
196
- var onEnd = function () {
197
- finish(null);
198
- };
199
- var onErrorOrClose = function () {
200
- finish(null);
201
- };
202
- var tryRead = function () {
203
- var data = stream.read(frameSize);
204
- if (data !== null) {
205
- finish(data.length === frameSize ? data : null);
206
- }
207
- };
208
- tryRead();
209
- if (settled) {
210
- return;
211
- }
212
- if (timeoutMs > 0) {
213
- timer = setTimeout(function () {
214
- console.error('readVideoFrame timeout:', timeoutMs);
215
- finish(null);
216
- }, timeoutMs);
217
- }
218
- stream.on('readable', onReadable);
219
- stream.once('end', onEnd);
220
- stream.once('error', onErrorOrClose);
221
- stream.once('close', onErrorOrClose);
222
- });
223
- };
224
- exports.getProgress = function (onProgress) {
225
- return function (stderrOutput) {
226
- var matchResult = stderrOutput.toString().match(/frame=[ ]+(\d+)/);
227
- if (!matchResult) {
228
- return;
229
- }
230
- var currentFrame = +matchResult[1];
231
- if (isNaN(currentFrame)) {
232
- return;
233
- }
234
- onProgress(currentFrame);
235
- };
236
- };
237
- exports.calculateProgress = function (framesCount, callback) {
238
- return function (currentFrame) {
239
- return callback(+(currentFrame / framesCount * 100).toFixed(2));
240
- };
241
- };
242
- exports.waitDrain = function (stream, processStream, timeoutMs) {
243
- if (timeoutMs === void 0) { timeoutMs = config_1.defaults.timeouts.waitDrain; }
244
- return new Promise(function (resolve) {
245
- var _a, _b, _c;
246
- var cleanup = function () {
247
- var _a, _b, _c;
248
- if (timer) {
249
- clearTimeout(timer);
250
- timer = undefined;
251
- }
252
- stream.removeListener('drain', onDrain);
253
- stream.removeListener('error', onErrorOrClose);
254
- stream.removeListener('close', onErrorOrClose);
255
- (_a = processStream) === null || _a === void 0 ? void 0 : _a.removeListener('error', onErrorOrClose);
256
- (_b = processStream) === null || _b === void 0 ? void 0 : _b.removeListener('exit', onErrorOrClose);
257
- (_c = processStream) === null || _c === void 0 ? void 0 : _c.removeListener('close', onErrorOrClose);
258
- };
259
- var onDrain = function () {
260
- cleanup();
261
- resolve(true);
262
- };
263
- var onErrorOrClose = function () {
264
- cleanup();
265
- resolve(false);
266
- };
267
- var timer;
268
- if (timeoutMs > 0) {
269
- timer = setTimeout(function () {
270
- console.error('waitDrain timeout:', timeoutMs);
271
- cleanup();
272
- resolve(false);
273
- }, timeoutMs);
274
- }
275
- stream.once('drain', onDrain);
276
- stream.once('error', onErrorOrClose);
277
- stream.once('close', onErrorOrClose);
278
- (_a = processStream) === null || _a === void 0 ? void 0 : _a.once('error', onErrorOrClose);
279
- (_b = processStream) === null || _b === void 0 ? void 0 : _b.once('exit', onErrorOrClose);
280
- (_c = processStream) === null || _c === void 0 ? void 0 : _c.once('close', onErrorOrClose);
281
- });
282
- };
283
- var resolveProcessExit = function (resolve, exitCode, reason) {
284
- resolve(__assign({ exitCode: exitCode }, (reason && { reason: reason })));
285
- };
286
- exports.waitForProcessExit = function (processStream, timeoutMs) {
287
- if (timeoutMs === void 0) { timeoutMs = config_1.defaults.timeouts.waitForProcessExit; }
288
- return new Promise(function (resolve) {
289
- var cleanup = function () {
290
- if (timer) {
291
- clearTimeout(timer);
292
- timer = undefined;
293
- }
294
- processStream.removeListener('exit', onExit);
295
- processStream.removeListener('close', onClose);
296
- processStream.removeListener('error', onError);
297
- };
298
- var onExit = 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 onClose = function (code) {
304
- cleanup();
305
- var exitCode = (code !== null && code !== void 0 ? code : 0);
306
- resolveProcessExit(resolve, exitCode, exitCode !== 0 ? "ffmpeg exited with code " + exitCode : undefined);
307
- };
308
- var onError = function () {
309
- cleanup();
310
- resolveProcessExit(resolve, 1, 'ffmpeg process error');
311
- };
312
- var timer;
313
- if (timeoutMs > 0) {
314
- timer = setTimeout(function () {
315
- console.error('waitForProcessExit timeout:', timeoutMs);
316
- cleanup();
317
- resolveProcessExit(resolve, 1, "waitForProcessExit timeout (" + timeoutMs + "ms)");
318
- }, timeoutMs);
319
- }
320
- processStream.once('exit', onExit);
321
- processStream.once('close', onClose);
322
- processStream.once('error', onError);
323
- });
324
- };
325
- var mergeSmallScenes = function (sceneChanges, videoDuration, minDuration) {
326
- if (minDuration === void 0) { minDuration = 2; }
327
- if (sceneChanges.length === 0)
328
- return [];
329
- var merged = [sceneChanges[0]];
330
- for (var i = 1; i < sceneChanges.length; i++) {
331
- var lastKept = merged[merged.length - 1];
332
- var gap = sceneChanges[i].ptsTime - lastKept.ptsTime;
333
- if (gap >= minDuration) {
334
- merged.push(sceneChanges[i]);
335
- }
336
- }
337
- var lastScene = merged[merged.length - 1];
338
- if (videoDuration - lastScene.ptsTime < minDuration && merged.length > 1) {
339
- merged.pop();
340
- }
341
- return merged;
342
- };
343
- /** Minimum time between auto-edit cuts; cuts still land on beats. */
344
- exports.DEFAULT_AUTO_EDIT_MIN_CUT_INTERVAL_SECONDS = 2;
345
- /** Window after the minimum gap in which the strongest beat is chosen. */
346
- exports.DEFAULT_AUTO_EDIT_MAX_CUT_INTERVAL_SECONDS = 4;
347
- /** Half a bar of 4/4, used as the minimum gap when tempo is known. */
348
- exports.AUTO_EDIT_MIN_CUT_BEATS = 2;
349
- /** One bar of 4/4, used as the maximum gap when tempo is known. */
350
- exports.AUTO_EDIT_MAX_CUT_BEATS = 4;
351
- /** Snap an onset to the tempo grid when it is within this fraction of a beat. */
352
- exports.BEAT_SNAP_TOLERANCE = 0.25;
353
- exports.cutIntervalSecondsFromTempo = function (bpm) { return ({
354
- minCutIntervalSeconds: exports.AUTO_EDIT_MIN_CUT_BEATS * (60 / bpm),
355
- maxCutIntervalSeconds: exports.AUTO_EDIT_MAX_CUT_BEATS * (60 / bpm),
356
- }); };
357
- exports.snapBeatsToTempoGrid = function (beats, tempo, totalFrames) {
358
- var period = tempo.periodFrames;
359
- if (!(period > 0)) {
360
- return beats.slice();
361
- }
362
- var maxSnap = Math.max(1, period * exports.BEAT_SNAP_TOLERANCE);
363
- var merged = [];
364
- var push = function (beat) {
365
- var _a, _b;
366
- for (var i = 0; i < merged.length; i++) {
367
- if (merged[i].frameIndex === beat.frameIndex) {
368
- if ((_a = beat.intensity, (_a !== null && _a !== void 0 ? _a : 0)) > (_b = merged[i].intensity, (_b !== null && _b !== void 0 ? _b : 0))) {
369
- merged[i] = beat;
370
- }
371
- return;
372
- }
373
- }
374
- merged.push(beat);
375
- };
376
- for (var _i = 0, beats_2 = beats; _i < beats_2.length; _i++) {
377
- var beat = beats_2[_i];
378
- var n = Math.round((beat.frameIndex - tempo.phaseFrame) / period);
379
- var grid = Math.round(tempo.phaseFrame + n * period);
380
- var dist = Math.abs(beat.frameIndex - grid);
381
- if (dist <= maxSnap && grid > 0 && grid < totalFrames) {
382
- push({ frameIndex: grid, intensity: beat.intensity });
383
- }
384
- else {
385
- push(beat);
386
- }
387
- }
388
- return merged.sort(function (a, b) { return a.frameIndex - b.frameIndex; });
389
- };
390
- /** Every BPM-grid beat, with onset intensity copied onto the nearest grid frame. */
391
- exports.projectBeatsOntoTempoGrid = function (beats, tempo, totalFrames) {
392
- var _a, _b;
393
- var grid = beats_1.beatGridFrameIndices(tempo, totalFrames);
394
- if (grid.length === 0) {
395
- return [];
396
- }
397
- var period = tempo.periodFrames;
398
- var maxSnap = Math.max(1, period * exports.BEAT_SNAP_TOLERANCE);
399
- var intensityByFrame = new Map();
400
- for (var _i = 0, beats_3 = beats; _i < beats_3.length; _i++) {
401
- var beat = beats_3[_i];
402
- var n = Math.round((beat.frameIndex - tempo.phaseFrame) / period);
403
- var gridFrame = Math.round(tempo.phaseFrame + n * period);
404
- if (Math.abs(beat.frameIndex - gridFrame) > maxSnap) {
405
- continue;
406
- }
407
- if (gridFrame <= 0 || gridFrame >= totalFrames) {
408
- continue;
409
- }
410
- var intensity = (_a = beat.intensity, (_a !== null && _a !== void 0 ? _a : 0));
411
- if (intensity >= (_b = intensityByFrame.get(gridFrame), (_b !== null && _b !== void 0 ? _b : 0))) {
412
- intensityByFrame.set(gridFrame, intensity);
413
- }
414
- }
415
- return grid.map(function (frameIndex) {
416
- var _a;
417
- return ({
418
- frameIndex: frameIndex,
419
- intensity: (_a = intensityByFrame.get(frameIndex), (_a !== null && _a !== void 0 ? _a : 0)),
420
- });
421
- });
422
- };
423
- exports.selectAutoEditCutFrames = function (beats, fps, totalFrames, minIntervalSeconds, maxIntervalSeconds) {
424
- if (minIntervalSeconds === void 0) { minIntervalSeconds = exports.DEFAULT_AUTO_EDIT_MIN_CUT_INTERVAL_SECONDS; }
425
- if (maxIntervalSeconds === void 0) { maxIntervalSeconds = exports.DEFAULT_AUTO_EDIT_MAX_CUT_INTERVAL_SECONDS; }
426
- var _a, _b;
427
- var sorted = beats
428
- .filter(function (beat) { return beat.frameIndex > 0 && beat.frameIndex < totalFrames; })
429
- .sort(function (a, b) { return a.frameIndex - b.frameIndex; });
430
- if (sorted.length === 0 || fps <= 0) {
431
- return [];
432
- }
433
- if (minIntervalSeconds <= 0) {
434
- return sorted.map(function (beat) { return beat.frameIndex; });
435
- }
436
- var minGap = Math.max(1, Math.round(fps * minIntervalSeconds));
437
- var maxGap = Math.max(minGap, Math.round(fps * maxIntervalSeconds));
438
- var selected = [];
439
- var lastCut = 0;
440
- while (lastCut + minGap < totalFrames) {
441
- var windowStart = lastCut + minGap;
442
- var windowEnd = Math.min(lastCut + maxGap, totalFrames);
443
- var best = void 0;
444
- var nextAfterWindow = void 0;
445
- for (var _i = 0, sorted_1 = sorted; _i < sorted_1.length; _i++) {
446
- var beat = sorted_1[_i];
447
- if (beat.frameIndex < windowStart) {
448
- continue;
449
- }
450
- if (beat.frameIndex >= windowEnd) {
451
- nextAfterWindow = beat;
452
- break;
453
- }
454
- var intensity = (_a = beat.intensity, (_a !== null && _a !== void 0 ? _a : 0));
455
- if (!best || intensity > (_b = best.intensity, (_b !== null && _b !== void 0 ? _b : 0))) {
456
- best = beat;
457
- }
458
- }
459
- var chosen = (best !== null && best !== void 0 ? best : nextAfterWindow);
460
- if (!chosen) {
461
- break;
462
- }
463
- selected.push(chosen.frameIndex);
464
- lastCut = chosen.frameIndex;
465
- }
466
- return selected;
467
- };
468
- exports.buildBeatSyncedSegments = function (beatFrameIndices, totalFrames, sceneChanges, videoDuration, fps, options) {
469
- var _a, _b, _c, _d, _e, _f, _g;
470
- if (sceneChanges.length === 0) {
471
- return [
472
- {
473
- outputStartFrame: 0,
474
- videoSeekSeconds: 0,
475
- frameCount: totalFrames,
476
- },
477
- ];
478
- }
479
- var consolidated = mergeSmallScenes(sceneChanges, videoDuration);
480
- var seekPositions;
481
- if (consolidated.length >= 2) {
482
- seekPositions = consolidated.map(function (sc) { return sc.ptsTime; });
483
- }
484
- else {
485
- var count_1 = Math.max(10, Math.ceil(videoDuration / 3));
486
- seekPositions = Array.from({ length: count_1 }, function (_, i) { return (i * videoDuration) / count_1; });
487
- }
488
- var tempo = (_a = options) === null || _a === void 0 ? void 0 : _a.tempo;
489
- var tempoIntervals = tempo && tempo.bpm > 0
490
- ? exports.cutIntervalSecondsFromTempo(tempo.bpm)
491
- : undefined;
492
- var beats = beatFrameIndices.map(function (frameIndex, i) {
493
- var _a, _b;
494
- return ({
495
- frameIndex: frameIndex,
496
- intensity: (_b = (_a = options) === null || _a === void 0 ? void 0 : _a.beatIntensities) === null || _b === void 0 ? void 0 : _b[i],
497
- });
498
- });
499
- if (tempo) {
500
- beats = exports.projectBeatsOntoTempoGrid(beats, tempo, totalFrames);
501
- }
502
- var cutBeats = exports.selectAutoEditCutFrames(beats, fps, totalFrames, (_c = (_b = options) === null || _b === void 0 ? void 0 : _b.minCutIntervalSeconds, (_c !== null && _c !== void 0 ? _c : (_d = tempoIntervals) === null || _d === void 0 ? void 0 : _d.minCutIntervalSeconds)), (_f = (_e = options) === null || _e === void 0 ? void 0 : _e.maxCutIntervalSeconds, (_f !== null && _f !== void 0 ? _f : (_g = tempoIntervals) === null || _g === void 0 ? void 0 : _g.maxCutIntervalSeconds)));
503
- var boundaries = __spreadArrays([0], cutBeats);
504
- if (boundaries[boundaries.length - 1] !== totalFrames) {
505
- boundaries.push(totalFrames);
506
- }
507
- var unique = Array.from(new Set(boundaries)).sort(function (a, b) { return a - b; });
508
- var segments = [];
509
- for (var i = 0; i < unique.length - 1; i++) {
510
- segments.push({
511
- outputStartFrame: unique[i],
512
- videoSeekSeconds: seekPositions[i % seekPositions.length],
513
- frameCount: unique[i + 1] - unique[i],
514
- });
515
- }
516
- return segments;
517
- };
518
- exports.getCutFrameIndices = function (segments) {
519
- return segments
520
- .slice(1)
521
- .map(function (segment) { return segment.outputStartFrame; })
522
- .filter(function (frameIndex) { return frameIndex > 0; });
523
- };
524
- exports.writeConcatFile = function (segments, videoPath, videoDuration, fps) {
525
- var absVideoPath = path_1.resolve(videoPath).replace(/\\/g, '/');
526
- var escaped = absVideoPath.replace(/'/g, "'\\''");
527
- var content = '';
528
- for (var _i = 0, segments_1 = segments; _i < segments_1.length; _i++) {
529
- var seg = segments_1[_i];
530
- var segDuration = seg.frameCount / fps;
531
- var remaining = segDuration;
532
- var pos = seg.videoSeekSeconds % videoDuration;
533
- while (remaining > 0.001) {
534
- var available = videoDuration - pos;
535
- var take = Math.min(remaining, available);
536
- content += "file '" + escaped + "'\n";
537
- content += "inpoint " + pos.toFixed(6) + "\n";
538
- content += "outpoint " + (pos + take).toFixed(6) + "\n";
539
- content += "duration " + take.toFixed(6) + "\n\n";
540
- remaining -= take;
541
- pos = 0;
542
- }
543
- }
544
- var concatPath = path_1.join(os_1.tmpdir(), "av-concat-" + Date.now() + ".txt");
545
- fs_1.writeFileSync(concatPath, content, 'utf-8');
546
- return concatPath;
547
- };
548
- exports.writeSubtitlesFile = function (subtitles) {
549
- var subtitlePath = path_1.join(os_1.tmpdir(), "av-subtitles-" + Date.now() + ".srt");
550
- fs_1.writeFileSync(subtitlePath, subtitles, 'utf-8');
551
- return subtitlePath;
552
- };
553
- exports.spawnConcatVideoFrameReader = function (config) {
554
- if (!ffmpeg_static_1.default) {
555
- throw new Error('ffmpeg path not found');
556
- }
557
- var args = [
558
- '-f', 'concat',
559
- '-safe', '0',
560
- '-fflags', '+genpts',
561
- '-i', config.concatFilePath,
562
- '-an',
563
- '-avoid_negative_ts', 'make_zero',
564
- '-r', "" + config.fps,
565
- '-frames:v', "" + config.totalFrames,
566
- ];
567
- if (config.width && config.height) {
568
- var filter = config.sourceWidth && config.sourceHeight
569
- ? exports.buildVideoFilter(config.sourceWidth, config.sourceHeight, config.width, config.height)
570
- : "scale=" + config.width + ":" + config.height;
571
- args.push('-vf', filter);
572
- }
573
- args.push('-f', 'rawvideo', '-pix_fmt', 'bgr24', '-v', 'quiet', 'pipe:1');
574
- return child_process_1.spawn(ffmpeg_static_1.default, args);
575
- };
576
- exports.cleanupTempFile = function (filePath) {
577
- try {
578
- fs_1.unlinkSync(filePath);
579
- }
580
- catch (_a) { }
581
- };
582
- 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 beats_1 = require("./beats");
31
+ var escapeSubtitleFilterPath = function (subtitlePath) {
32
+ return subtitlePath
33
+ .replace(/\\/g, '\\\\')
34
+ .replace(/:/g, '\\:')
35
+ .replace(/'/g, "\\'")
36
+ .replace(/,/g, '\\,')
37
+ .replace(/\[/g, '\\[')
38
+ .replace(/\]/g, '\\]');
39
+ };
40
+ exports.spawnFfmpegVideoWriter = function (config) {
41
+ var _a, _b, _c, _d, _e;
42
+ if (!ffmpeg_static_1.default) {
43
+ throw new Error('ffmpeg path not found');
44
+ }
45
+ var crf = config.crf || '23';
46
+ var preset = config.preset || 'medium';
47
+ var args = ['-y'];
48
+ var seek = (_b = (_a = config.audioSegment) === null || _a === void 0 ? void 0 : _a.seekSeconds, (_b !== null && _b !== void 0 ? _b : config.audioSeekSeconds));
49
+ var duration = (_d = (_c = config.audioSegment) === null || _c === void 0 ? void 0 : _c.durationSeconds, (_d !== null && _d !== void 0 ? _d : config.audioDurationSeconds));
50
+ if (seek !== undefined && duration !== undefined) {
51
+ args.push('-ss', String(seek), '-t', String(duration));
52
+ }
53
+ 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');
54
+ if (config.subtitleFilename) {
55
+ var subPath = escapeSubtitleFilterPath(config.subtitleFilename);
56
+ var alignment = (_e = config.subtitleAlignmentAss, (_e !== null && _e !== void 0 ? _e : 2));
57
+ args.push('-vf', "subtitles='" + subPath + "':force_style='Alignment=" + alignment + "'");
58
+ }
59
+ args.push(config.videoFileName);
60
+ var ffmpeg = child_process_1.spawn(ffmpeg_static_1.default, args);
61
+ if (config.onStderr) {
62
+ ffmpeg.stderr.on('data', config.onStderr);
63
+ }
64
+ return ffmpeg;
65
+ };
66
+ exports.detectSceneChanges = function (videoPath, threshold) {
67
+ if (threshold === void 0) { threshold = 0.4; }
68
+ return new Promise(function (resolve, reject) {
69
+ if (!ffmpeg_static_1.default) {
70
+ reject(new Error('ffmpeg path not found'));
71
+ return;
72
+ }
73
+ var ffmpeg = child_process_1.spawn(ffmpeg_static_1.default, [
74
+ '-i', videoPath,
75
+ '-filter:v', "select='gt(scene," + threshold + ")',showinfo",
76
+ '-f', 'null',
77
+ '-'
78
+ ]);
79
+ var stderr = '';
80
+ ffmpeg.stderr.on('data', function (data) { stderr += data.toString(); });
81
+ ffmpeg.on('close', function () {
82
+ var sceneChanges = [];
83
+ for (var _i = 0, _a = stderr.split('\n'); _i < _a.length; _i++) {
84
+ var line = _a[_i];
85
+ if (!line.includes('Parsed_showinfo'))
86
+ continue;
87
+ var nMatch = line.match(/n:\s*(\d+)/);
88
+ var ptsMatch = line.match(/pts:\s*(\d+)/);
89
+ var ptsTimeMatch = line.match(/pts_time:\s*([\d.]+)/);
90
+ if (nMatch && ptsTimeMatch) {
91
+ sceneChanges.push({
92
+ frameNumber: parseInt(nMatch[1]),
93
+ pts: ptsMatch ? parseInt(ptsMatch[1]) : 0,
94
+ ptsTime: parseFloat(ptsTimeMatch[1]),
95
+ });
96
+ }
97
+ }
98
+ resolve(sceneChanges);
99
+ });
100
+ ffmpeg.on('error', reject);
101
+ });
102
+ };
103
+ exports.getVideoInfo = function (videoPath) {
104
+ return new Promise(function (resolvePromise, reject) {
105
+ if (!ffmpeg_static_1.default) {
106
+ reject(new Error('ffmpeg path not found'));
107
+ return;
108
+ }
109
+ var ffmpeg = child_process_1.spawn(ffmpeg_static_1.default, ['-i', videoPath, '-hide_banner']);
110
+ var stderr = '';
111
+ ffmpeg.stderr.on('data', function (data) { stderr += data.toString(); });
112
+ ffmpeg.on('close', function () {
113
+ var dimMatch = stderr.match(/Stream.*Video:.*?(\d{2,5})x(\d{2,5})/);
114
+ if (!dimMatch) {
115
+ reject(new Error("Could not determine video dimensions for: " + videoPath));
116
+ return;
117
+ }
118
+ var durMatch = stderr.match(/Duration:\s*(\d+):(\d+):([\d.]+)/);
119
+ var duration = durMatch
120
+ ? parseInt(durMatch[1]) * 3600 + parseInt(durMatch[2]) * 60 + parseFloat(durMatch[3])
121
+ : 0;
122
+ resolvePromise({
123
+ width: parseInt(dimMatch[1]),
124
+ height: parseInt(dimMatch[2]),
125
+ duration: duration,
126
+ });
127
+ });
128
+ });
129
+ };
130
+ var isOrientationMismatch = function (sw, sh, tw, th) {
131
+ return (sw > sh && tw < th) || (sw < sh && tw > th);
132
+ };
133
+ exports.buildVideoFilter = function (sourceWidth, sourceHeight, targetWidth, targetHeight) {
134
+ if (!isOrientationMismatch(sourceWidth, sourceHeight, targetWidth, targetHeight)) {
135
+ return "scale=" + targetWidth + ":" + targetHeight;
136
+ }
137
+ var targetAspect = targetWidth / targetHeight;
138
+ var makeEven = function (n) { return Math.floor(n / 2) * 2; };
139
+ var cropW, cropH, cropX, cropY;
140
+ if (sourceWidth / sourceHeight > targetAspect) {
141
+ cropW = makeEven(Math.round(sourceHeight * targetAspect));
142
+ cropH = makeEven(sourceHeight);
143
+ cropX = Math.floor((sourceWidth - cropW) / 2);
144
+ cropY = Math.floor((sourceHeight - cropH) / 2);
145
+ }
146
+ else {
147
+ cropW = makeEven(sourceWidth);
148
+ cropH = makeEven(Math.round(sourceWidth / targetAspect));
149
+ cropX = Math.floor((sourceWidth - cropW) / 2);
150
+ cropY = Math.floor((sourceHeight - cropH) / 2);
151
+ }
152
+ return "crop=" + cropW + ":" + cropH + ":" + cropX + ":" + cropY + ",scale=" + targetWidth + ":" + targetHeight;
153
+ };
154
+ exports.spawnVideoFrameReader = function (config) {
155
+ if (!ffmpeg_static_1.default) {
156
+ throw new Error('ffmpeg path not found');
157
+ }
158
+ var args = [
159
+ '-stream_loop', '-1',
160
+ '-i', config.videoPath,
161
+ '-r', "" + config.fps,
162
+ '-frames:v', "" + config.totalFrames,
163
+ ];
164
+ if (config.width && config.height) {
165
+ var filter = config.sourceWidth && config.sourceHeight
166
+ ? exports.buildVideoFilter(config.sourceWidth, config.sourceHeight, config.width, config.height)
167
+ : "scale=" + config.width + ":" + config.height;
168
+ args.push('-vf', filter);
169
+ }
170
+ args.push('-f', 'rawvideo', '-pix_fmt', 'bgr24', '-v', 'quiet', 'pipe:1');
171
+ return child_process_1.spawn(ffmpeg_static_1.default, args);
172
+ };
173
+ exports.readVideoFrame = function (stream, frameSize, timeoutMs) {
174
+ if (timeoutMs === void 0) { timeoutMs = config_1.defaults.timeouts.readVideoFrame; }
175
+ return new Promise(function (resolve) {
176
+ var timer;
177
+ var settled = false;
178
+ var finish = function (result) {
179
+ if (settled) {
180
+ return;
181
+ }
182
+ settled = true;
183
+ if (timer) {
184
+ clearTimeout(timer);
185
+ timer = undefined;
186
+ }
187
+ stream.removeListener('readable', onReadable);
188
+ stream.removeListener('end', onEnd);
189
+ stream.removeListener('error', onErrorOrClose);
190
+ stream.removeListener('close', onErrorOrClose);
191
+ resolve(result);
192
+ };
193
+ var onReadable = function () {
194
+ tryRead();
195
+ };
196
+ var onEnd = function () {
197
+ finish(null);
198
+ };
199
+ var onErrorOrClose = function () {
200
+ finish(null);
201
+ };
202
+ var tryRead = function () {
203
+ var data = stream.read(frameSize);
204
+ if (data !== null) {
205
+ finish(data.length === frameSize ? data : null);
206
+ }
207
+ };
208
+ tryRead();
209
+ if (settled) {
210
+ return;
211
+ }
212
+ if (timeoutMs > 0) {
213
+ timer = setTimeout(function () {
214
+ console.error('readVideoFrame timeout:', timeoutMs);
215
+ finish(null);
216
+ }, timeoutMs);
217
+ }
218
+ stream.on('readable', onReadable);
219
+ stream.once('end', onEnd);
220
+ stream.once('error', onErrorOrClose);
221
+ stream.once('close', onErrorOrClose);
222
+ });
223
+ };
224
+ exports.getProgress = function (onProgress) {
225
+ return function (stderrOutput) {
226
+ var matchResult = stderrOutput.toString().match(/frame=[ ]+(\d+)/);
227
+ if (!matchResult) {
228
+ return;
229
+ }
230
+ var currentFrame = +matchResult[1];
231
+ if (isNaN(currentFrame)) {
232
+ return;
233
+ }
234
+ onProgress(currentFrame);
235
+ };
236
+ };
237
+ exports.calculateProgress = function (framesCount, callback) {
238
+ return function (currentFrame) {
239
+ return callback(+(currentFrame / framesCount * 100).toFixed(2));
240
+ };
241
+ };
242
+ exports.waitDrain = function (stream, processStream, timeoutMs) {
243
+ if (timeoutMs === void 0) { timeoutMs = config_1.defaults.timeouts.waitDrain; }
244
+ return new Promise(function (resolve) {
245
+ var _a, _b, _c;
246
+ var cleanup = function () {
247
+ var _a, _b, _c;
248
+ if (timer) {
249
+ clearTimeout(timer);
250
+ timer = undefined;
251
+ }
252
+ stream.removeListener('drain', onDrain);
253
+ stream.removeListener('error', onErrorOrClose);
254
+ stream.removeListener('close', onErrorOrClose);
255
+ (_a = processStream) === null || _a === void 0 ? void 0 : _a.removeListener('error', onErrorOrClose);
256
+ (_b = processStream) === null || _b === void 0 ? void 0 : _b.removeListener('exit', onErrorOrClose);
257
+ (_c = processStream) === null || _c === void 0 ? void 0 : _c.removeListener('close', onErrorOrClose);
258
+ };
259
+ var onDrain = function () {
260
+ cleanup();
261
+ resolve(true);
262
+ };
263
+ var onErrorOrClose = function () {
264
+ cleanup();
265
+ resolve(false);
266
+ };
267
+ var timer;
268
+ if (timeoutMs > 0) {
269
+ timer = setTimeout(function () {
270
+ console.error('waitDrain timeout:', timeoutMs);
271
+ cleanup();
272
+ resolve(false);
273
+ }, timeoutMs);
274
+ }
275
+ stream.once('drain', onDrain);
276
+ stream.once('error', onErrorOrClose);
277
+ stream.once('close', onErrorOrClose);
278
+ (_a = processStream) === null || _a === void 0 ? void 0 : _a.once('error', onErrorOrClose);
279
+ (_b = processStream) === null || _b === void 0 ? void 0 : _b.once('exit', onErrorOrClose);
280
+ (_c = processStream) === null || _c === void 0 ? void 0 : _c.once('close', onErrorOrClose);
281
+ });
282
+ };
283
+ var resolveProcessExit = function (resolve, exitCode, reason) {
284
+ resolve(__assign({ exitCode: exitCode }, (reason && { reason: reason })));
285
+ };
286
+ exports.waitForProcessExit = function (processStream, timeoutMs) {
287
+ if (timeoutMs === void 0) { timeoutMs = config_1.defaults.timeouts.waitForProcessExit; }
288
+ return new Promise(function (resolve) {
289
+ var cleanup = function () {
290
+ if (timer) {
291
+ clearTimeout(timer);
292
+ timer = undefined;
293
+ }
294
+ processStream.removeListener('exit', onExit);
295
+ processStream.removeListener('close', onClose);
296
+ processStream.removeListener('error', onError);
297
+ };
298
+ var onExit = 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 onClose = function (code) {
304
+ cleanup();
305
+ var exitCode = (code !== null && code !== void 0 ? code : 0);
306
+ resolveProcessExit(resolve, exitCode, exitCode !== 0 ? "ffmpeg exited with code " + exitCode : undefined);
307
+ };
308
+ var onError = function () {
309
+ cleanup();
310
+ resolveProcessExit(resolve, 1, 'ffmpeg process error');
311
+ };
312
+ var timer;
313
+ if (timeoutMs > 0) {
314
+ timer = setTimeout(function () {
315
+ console.error('waitForProcessExit timeout:', timeoutMs);
316
+ cleanup();
317
+ resolveProcessExit(resolve, 1, "waitForProcessExit timeout (" + timeoutMs + "ms)");
318
+ }, timeoutMs);
319
+ }
320
+ processStream.once('exit', onExit);
321
+ processStream.once('close', onClose);
322
+ processStream.once('error', onError);
323
+ });
324
+ };
325
+ var mergeSmallScenes = function (sceneChanges, videoDuration, minDuration) {
326
+ if (minDuration === void 0) { minDuration = 2; }
327
+ if (sceneChanges.length === 0)
328
+ return [];
329
+ var merged = [sceneChanges[0]];
330
+ for (var i = 1; i < sceneChanges.length; i++) {
331
+ var lastKept = merged[merged.length - 1];
332
+ var gap = sceneChanges[i].ptsTime - lastKept.ptsTime;
333
+ if (gap >= minDuration) {
334
+ merged.push(sceneChanges[i]);
335
+ }
336
+ }
337
+ var lastScene = merged[merged.length - 1];
338
+ if (videoDuration - lastScene.ptsTime < minDuration && merged.length > 1) {
339
+ merged.pop();
340
+ }
341
+ return merged;
342
+ };
343
+ /** Minimum time between auto-edit cuts; cuts still land on beats. */
344
+ exports.DEFAULT_AUTO_EDIT_MIN_CUT_INTERVAL_SECONDS = 2;
345
+ /** Window after the minimum gap in which the strongest beat is chosen. */
346
+ exports.DEFAULT_AUTO_EDIT_MAX_CUT_INTERVAL_SECONDS = 4;
347
+ /** Half a bar of 4/4, used as the minimum gap when tempo is known. */
348
+ exports.AUTO_EDIT_MIN_CUT_BEATS = 2;
349
+ /** One bar of 4/4, used as the maximum gap when tempo is known. */
350
+ exports.AUTO_EDIT_MAX_CUT_BEATS = 4;
351
+ /** Snap an onset to the tempo grid when it is within this fraction of a beat. */
352
+ exports.BEAT_SNAP_TOLERANCE = 0.25;
353
+ exports.cutIntervalSecondsFromTempo = function (bpm) { return ({
354
+ minCutIntervalSeconds: exports.AUTO_EDIT_MIN_CUT_BEATS * (60 / bpm),
355
+ maxCutIntervalSeconds: exports.AUTO_EDIT_MAX_CUT_BEATS * (60 / bpm),
356
+ }); };
357
+ exports.snapBeatsToTempoGrid = function (beats, tempo, totalFrames) {
358
+ var period = tempo.periodFrames;
359
+ if (!(period > 0)) {
360
+ return beats.slice();
361
+ }
362
+ var maxSnap = Math.max(1, period * exports.BEAT_SNAP_TOLERANCE);
363
+ var merged = [];
364
+ var push = function (beat) {
365
+ var _a, _b;
366
+ for (var i = 0; i < merged.length; i++) {
367
+ if (merged[i].frameIndex === beat.frameIndex) {
368
+ if ((_a = beat.intensity, (_a !== null && _a !== void 0 ? _a : 0)) > (_b = merged[i].intensity, (_b !== null && _b !== void 0 ? _b : 0))) {
369
+ merged[i] = beat;
370
+ }
371
+ return;
372
+ }
373
+ }
374
+ merged.push(beat);
375
+ };
376
+ for (var _i = 0, beats_2 = beats; _i < beats_2.length; _i++) {
377
+ var beat = beats_2[_i];
378
+ var n = Math.round((beat.frameIndex - tempo.phaseFrame) / period);
379
+ var grid = Math.round(tempo.phaseFrame + n * period);
380
+ var dist = Math.abs(beat.frameIndex - grid);
381
+ if (dist <= maxSnap && grid > 0 && grid < totalFrames) {
382
+ push({ frameIndex: grid, intensity: beat.intensity });
383
+ }
384
+ else {
385
+ push(beat);
386
+ }
387
+ }
388
+ return merged.sort(function (a, b) { return a.frameIndex - b.frameIndex; });
389
+ };
390
+ /** Every BPM-grid beat, with onset intensity copied onto the nearest grid frame. */
391
+ exports.projectBeatsOntoTempoGrid = function (beats, tempo, totalFrames) {
392
+ var _a, _b;
393
+ var grid = beats_1.beatGridFrameIndices(tempo, totalFrames);
394
+ if (grid.length === 0) {
395
+ return [];
396
+ }
397
+ var period = tempo.periodFrames;
398
+ var maxSnap = Math.max(1, period * exports.BEAT_SNAP_TOLERANCE);
399
+ var intensityByFrame = new Map();
400
+ for (var _i = 0, beats_3 = beats; _i < beats_3.length; _i++) {
401
+ var beat = beats_3[_i];
402
+ var n = Math.round((beat.frameIndex - tempo.phaseFrame) / period);
403
+ var gridFrame = Math.round(tempo.phaseFrame + n * period);
404
+ if (Math.abs(beat.frameIndex - gridFrame) > maxSnap) {
405
+ continue;
406
+ }
407
+ if (gridFrame <= 0 || gridFrame >= totalFrames) {
408
+ continue;
409
+ }
410
+ var intensity = (_a = beat.intensity, (_a !== null && _a !== void 0 ? _a : 0));
411
+ if (intensity >= (_b = intensityByFrame.get(gridFrame), (_b !== null && _b !== void 0 ? _b : 0))) {
412
+ intensityByFrame.set(gridFrame, intensity);
413
+ }
414
+ }
415
+ return grid.map(function (frameIndex) {
416
+ var _a;
417
+ return ({
418
+ frameIndex: frameIndex,
419
+ intensity: (_a = intensityByFrame.get(frameIndex), (_a !== null && _a !== void 0 ? _a : 0)),
420
+ });
421
+ });
422
+ };
423
+ exports.selectAutoEditCutFrames = function (beats, fps, totalFrames, minIntervalSeconds, maxIntervalSeconds) {
424
+ if (minIntervalSeconds === void 0) { minIntervalSeconds = exports.DEFAULT_AUTO_EDIT_MIN_CUT_INTERVAL_SECONDS; }
425
+ if (maxIntervalSeconds === void 0) { maxIntervalSeconds = exports.DEFAULT_AUTO_EDIT_MAX_CUT_INTERVAL_SECONDS; }
426
+ var _a, _b;
427
+ var sorted = beats
428
+ .filter(function (beat) { return beat.frameIndex > 0 && beat.frameIndex < totalFrames; })
429
+ .sort(function (a, b) { return a.frameIndex - b.frameIndex; });
430
+ if (sorted.length === 0 || fps <= 0) {
431
+ return [];
432
+ }
433
+ if (minIntervalSeconds <= 0) {
434
+ return sorted.map(function (beat) { return beat.frameIndex; });
435
+ }
436
+ var minGap = Math.max(1, Math.round(fps * minIntervalSeconds));
437
+ var maxGap = Math.max(minGap, Math.round(fps * maxIntervalSeconds));
438
+ var selected = [];
439
+ var lastCut = 0;
440
+ while (lastCut + minGap < totalFrames) {
441
+ var windowStart = lastCut + minGap;
442
+ var windowEnd = Math.min(lastCut + maxGap, totalFrames);
443
+ var best = void 0;
444
+ var nextAfterWindow = void 0;
445
+ for (var _i = 0, sorted_1 = sorted; _i < sorted_1.length; _i++) {
446
+ var beat = sorted_1[_i];
447
+ if (beat.frameIndex < windowStart) {
448
+ continue;
449
+ }
450
+ if (beat.frameIndex >= windowEnd) {
451
+ nextAfterWindow = beat;
452
+ break;
453
+ }
454
+ var intensity = (_a = beat.intensity, (_a !== null && _a !== void 0 ? _a : 0));
455
+ if (!best || intensity > (_b = best.intensity, (_b !== null && _b !== void 0 ? _b : 0))) {
456
+ best = beat;
457
+ }
458
+ }
459
+ var chosen = (best !== null && best !== void 0 ? best : nextAfterWindow);
460
+ if (!chosen) {
461
+ break;
462
+ }
463
+ selected.push(chosen.frameIndex);
464
+ lastCut = chosen.frameIndex;
465
+ }
466
+ return selected;
467
+ };
468
+ var computeAutoEditCutBoundaries = function (beatFrameIndices, totalFrames, fps, options) {
469
+ var _a, _b, _c, _d, _e, _f, _g;
470
+ var tempo = (_a = options) === null || _a === void 0 ? void 0 : _a.tempo;
471
+ var tempoIntervals = tempo && tempo.bpm > 0
472
+ ? exports.cutIntervalSecondsFromTempo(tempo.bpm)
473
+ : undefined;
474
+ var beats = beatFrameIndices.map(function (frameIndex, i) {
475
+ var _a, _b;
476
+ return ({
477
+ frameIndex: frameIndex,
478
+ intensity: (_b = (_a = options) === null || _a === void 0 ? void 0 : _a.beatIntensities) === null || _b === void 0 ? void 0 : _b[i],
479
+ });
480
+ });
481
+ if (tempo) {
482
+ beats = exports.projectBeatsOntoTempoGrid(beats, tempo, totalFrames);
483
+ }
484
+ var cutBeats = exports.selectAutoEditCutFrames(beats, fps, totalFrames, (_c = (_b = options) === null || _b === void 0 ? void 0 : _b.minCutIntervalSeconds, (_c !== null && _c !== void 0 ? _c : (_d = tempoIntervals) === null || _d === void 0 ? void 0 : _d.minCutIntervalSeconds)), (_f = (_e = options) === null || _e === void 0 ? void 0 : _e.maxCutIntervalSeconds, (_f !== null && _f !== void 0 ? _f : (_g = tempoIntervals) === null || _g === void 0 ? void 0 : _g.maxCutIntervalSeconds)));
485
+ var boundaries = __spreadArrays([0], cutBeats);
486
+ if (boundaries[boundaries.length - 1] !== totalFrames) {
487
+ boundaries.push(totalFrames);
488
+ }
489
+ return Array.from(new Set(boundaries)).sort(function (a, b) { return a - b; });
490
+ };
491
+ /**
492
+ * Splits `totalFrames` into segments that cycle through `videoDurations` in order, each
493
+ * segment playing one source video from the start. Used for `video.path` arrays with
494
+ * `autoEdit` disabled, so the given videos play one after another (looping the sequence).
495
+ * `startVideoIndex` rotates which video plays first (e.g. so separate auto-highlight outputs
496
+ * each open with a different video).
497
+ */
498
+ exports.buildSequentialVideoSegments = function (totalFrames, videoDurations, fps, startVideoIndex) {
499
+ if (startVideoIndex === void 0) { startVideoIndex = 0; }
500
+ if (videoDurations.length === 0 || totalFrames <= 0 || fps <= 0) {
501
+ return [];
502
+ }
503
+ var segments = [];
504
+ var framesRemaining = totalFrames;
505
+ var outputStartFrame = 0;
506
+ var cycleIndex = 0;
507
+ var maxIterations = videoDurations.length * 100000;
508
+ var iterations = 0;
509
+ while (framesRemaining > 0 && iterations < maxIterations) {
510
+ iterations += 1;
511
+ var videoIndex = (startVideoIndex + cycleIndex) % videoDurations.length;
512
+ var videoFrames = Math.max(1, Math.round(videoDurations[videoIndex] * fps));
513
+ var frameCount = Math.min(videoFrames, framesRemaining);
514
+ segments.push({
515
+ outputStartFrame: outputStartFrame,
516
+ videoSeekSeconds: 0,
517
+ frameCount: frameCount,
518
+ videoIndex: videoIndex,
519
+ });
520
+ outputStartFrame += frameCount;
521
+ framesRemaining -= frameCount;
522
+ cycleIndex += 1;
523
+ }
524
+ return segments;
525
+ };
526
+ /**
527
+ * Fisher-Yates shuffle of `[0, videoCount)`, optionally reshuffling the first slot so it
528
+ * doesn't repeat `avoidFirst` (used to avoid a repeat across bag boundaries).
529
+ */
530
+ var shuffleVideoIndices = function (videoCount, random, avoidFirst) {
531
+ var _a, _b;
532
+ var order = Array.from({ length: videoCount }, function (_, i) { return i; });
533
+ for (var i = order.length - 1; i > 0; i--) {
534
+ var j = Math.floor(random() * (i + 1));
535
+ _a = [order[j], order[i]], order[i] = _a[0], order[j] = _a[1];
536
+ }
537
+ if (avoidFirst !== undefined && order.length > 1 && order[0] === avoidFirst) {
538
+ var swapWith = 1 + Math.floor(random() * (order.length - 1));
539
+ _b = [order[swapWith], order[0]], order[0] = _b[0], order[swapWith] = _b[1];
540
+ }
541
+ return order;
542
+ };
543
+ /**
544
+ * Builds auto-edit cut segments like `buildBeatSyncedSegments`, but instead of switching
545
+ * between detected scenes within a single video, each segment is assigned a video from
546
+ * `videoCount` sources. Videos are picked by shuffling the full set into a "bag" and cycling
547
+ * through it (reshuffling once exhausted), rather than picking uniformly at random each time,
548
+ * so every video plays about as often as the others instead of some clumping together while
549
+ * others go unpicked for long stretches. The same video never plays twice in a row, including
550
+ * across a reshuffle.
551
+ */
552
+ exports.buildShuffledVideoSegments = function (beatFrameIndices, totalFrames, videoCount, fps, options) {
553
+ var _a, _b;
554
+ if (videoCount <= 0 || totalFrames <= 0) {
555
+ return [];
556
+ }
557
+ if (videoCount === 1) {
558
+ return [{ outputStartFrame: 0, videoSeekSeconds: 0, frameCount: totalFrames, videoIndex: 0 }];
559
+ }
560
+ var boundaries = computeAutoEditCutBoundaries(beatFrameIndices, totalFrames, fps, options);
561
+ var random = (_b = (_a = options) === null || _a === void 0 ? void 0 : _a.randomFn, (_b !== null && _b !== void 0 ? _b : Math.random));
562
+ var segments = [];
563
+ var bag = [];
564
+ var previousVideoIndex;
565
+ for (var i = 0; i < boundaries.length - 1; i++) {
566
+ if (bag.length === 0) {
567
+ bag = shuffleVideoIndices(videoCount, random, previousVideoIndex);
568
+ }
569
+ var videoIndex = bag.shift();
570
+ previousVideoIndex = videoIndex;
571
+ segments.push({
572
+ outputStartFrame: boundaries[i],
573
+ videoSeekSeconds: 0,
574
+ frameCount: boundaries[i + 1] - boundaries[i],
575
+ videoIndex: videoIndex,
576
+ });
577
+ }
578
+ return segments;
579
+ };
580
+ exports.buildBeatSyncedSegments = function (beatFrameIndices, totalFrames, sceneChanges, videoDuration, fps, options) {
581
+ if (sceneChanges.length === 0) {
582
+ return [
583
+ {
584
+ outputStartFrame: 0,
585
+ videoSeekSeconds: 0,
586
+ frameCount: totalFrames,
587
+ },
588
+ ];
589
+ }
590
+ var consolidated = mergeSmallScenes(sceneChanges, videoDuration);
591
+ var seekPositions;
592
+ if (consolidated.length >= 2) {
593
+ seekPositions = consolidated.map(function (sc) { return sc.ptsTime; });
594
+ }
595
+ else {
596
+ var count_1 = Math.max(10, Math.ceil(videoDuration / 3));
597
+ seekPositions = Array.from({ length: count_1 }, function (_, i) { return (i * videoDuration) / count_1; });
598
+ }
599
+ var unique = computeAutoEditCutBoundaries(beatFrameIndices, totalFrames, fps, options);
600
+ var segments = [];
601
+ for (var i = 0; i < unique.length - 1; i++) {
602
+ segments.push({
603
+ outputStartFrame: unique[i],
604
+ videoSeekSeconds: seekPositions[i % seekPositions.length],
605
+ frameCount: unique[i + 1] - unique[i],
606
+ });
607
+ }
608
+ return segments;
609
+ };
610
+ exports.getCutFrameIndices = function (segments) {
611
+ return segments
612
+ .slice(1)
613
+ .map(function (segment) { return segment.outputStartFrame; })
614
+ .filter(function (frameIndex) { return frameIndex > 0; });
615
+ };
616
+ exports.writeConcatFile = function (segments, videoSources, fps) {
617
+ var _a;
618
+ var content = '';
619
+ for (var _i = 0, segments_1 = segments; _i < segments_1.length; _i++) {
620
+ var seg = segments_1[_i];
621
+ var source = videoSources[_a = seg.videoIndex, (_a !== null && _a !== void 0 ? _a : 0)];
622
+ var absVideoPath = path_1.resolve(source.path).replace(/\\/g, '/');
623
+ var escaped = absVideoPath.replace(/'/g, "'\\''");
624
+ var videoDuration = source.duration;
625
+ var segDuration = seg.frameCount / fps;
626
+ var remaining = segDuration;
627
+ var pos = videoDuration > 0 ? seg.videoSeekSeconds % videoDuration : 0;
628
+ while (remaining > 0.001) {
629
+ var available = videoDuration > 0 ? videoDuration - pos : remaining;
630
+ var take = Math.min(remaining, available);
631
+ content += "file '" + escaped + "'\n";
632
+ content += "inpoint " + pos.toFixed(6) + "\n";
633
+ content += "outpoint " + (pos + take).toFixed(6) + "\n";
634
+ content += "duration " + take.toFixed(6) + "\n\n";
635
+ remaining -= take;
636
+ pos = 0;
637
+ }
638
+ }
639
+ var concatPath = path_1.join(os_1.tmpdir(), "av-concat-" + Date.now() + ".txt");
640
+ fs_1.writeFileSync(concatPath, content, 'utf-8');
641
+ return concatPath;
642
+ };
643
+ exports.writeSubtitlesFile = function (subtitles) {
644
+ var subtitlePath = path_1.join(os_1.tmpdir(), "av-subtitles-" + Date.now() + ".srt");
645
+ fs_1.writeFileSync(subtitlePath, subtitles, 'utf-8');
646
+ return subtitlePath;
647
+ };
648
+ exports.spawnConcatVideoFrameReader = function (config) {
649
+ if (!ffmpeg_static_1.default) {
650
+ throw new Error('ffmpeg path not found');
651
+ }
652
+ var args = [
653
+ '-f', 'concat',
654
+ '-safe', '0',
655
+ '-fflags', '+genpts',
656
+ '-i', config.concatFilePath,
657
+ '-an',
658
+ '-avoid_negative_ts', 'make_zero',
659
+ '-r', "" + config.fps,
660
+ '-frames:v', "" + config.totalFrames,
661
+ ];
662
+ if (config.width && config.height) {
663
+ var filter = config.sourceWidth && config.sourceHeight
664
+ ? exports.buildVideoFilter(config.sourceWidth, config.sourceHeight, config.width, config.height)
665
+ : "scale=" + config.width + ":" + config.height;
666
+ args.push('-vf', filter);
667
+ }
668
+ args.push('-f', 'rawvideo', '-pix_fmt', 'bgr24', '-v', 'quiet', 'pipe:1');
669
+ return child_process_1.spawn(ffmpeg_static_1.default, args);
670
+ };
671
+ exports.cleanupTempFile = function (filePath) {
672
+ try {
673
+ fs_1.unlinkSync(filePath);
674
+ }
675
+ catch (_a) { }
676
+ };
677
+ exports.cleanupConcatFile = exports.cleanupTempFile;
583
678
  //# sourceMappingURL=video.js.map