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