polotno-node 2.9.38 → 2.10.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 (2) hide show
  1. package/package.json +1 -1
  2. package/video-parallel.js +87 -52
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polotno-node",
3
- "version": "2.9.38",
3
+ "version": "2.10.0",
4
4
  "description": "Polotno workflow from NodeJS",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/video-parallel.js CHANGED
@@ -116,6 +116,20 @@ module.exports.jsonToVideo = async function jsonToVideo(
116
116
  })
117
117
  );
118
118
 
119
+ const audios = [];
120
+ // download all audio tracks
121
+ await Promise.all(
122
+ (json.audios || []).map(async (audio) => {
123
+ const filename = Math.random().toString(36).substring(7);
124
+ const destination = path.join(tempFolder.name, filename);
125
+ await downloadVideo(audio.src, destination);
126
+ audios.push({
127
+ ...audio,
128
+ file: destination,
129
+ });
130
+ })
131
+ );
132
+
119
133
  const duration = json.pages.reduce((acc, page) => {
120
134
  return acc + (page.duration ?? 5000);
121
135
  }, 0);
@@ -276,64 +290,85 @@ module.exports.jsonToVideo = async function jsonToVideo(
276
290
  .inputFPS(fps)
277
291
  .videoCodec('libx264')
278
292
  .outputOptions('-pix_fmt yuv420p')
279
- .videoFilters('scale=trunc(iw/2)*2:trunc(ih/2)*2'); // Ensure dimensions are divisible by 2
293
+ .videoFilters('scale=trunc(iw/2)*2:trunc(ih/2)*2');
280
294
 
281
295
  const audioInputs = [];
282
296
 
283
- // Add each audio segment as a separate input
284
- inputs.forEach((input, index) => {
285
- const inputStartSec = (input.inputStartTime / 1000).toFixed(3);
286
- const inputDurationSec = (
287
- (input.inputEndTime - input.inputStartTime) /
288
- 1000
289
- ).toFixed(3);
290
- const outputOffsetMs = Math.round(input.outputStartTime);
291
- console.log({ outputOffsetMs });
292
- const volume = input.volume !== undefined ? input.volume : 1;
297
+ // Use Promise.all to handle async ffprobe calls
298
+ Promise.all(
299
+ inputs.map((input, index) => {
300
+ return new Promise((resolveProbe) => {
301
+ const inputStartSec = (input.inputStartTime / 1000).toFixed(3);
302
+ const inputDurationSec = (
303
+ (input.inputEndTime - input.inputStartTime) /
304
+ 1000
305
+ ).toFixed(3);
306
+ const outputOffsetMs = Math.round(input.outputStartTime);
307
+ const volume = input.volume !== undefined ? input.volume : 1;
308
+
309
+ ffmpegCmd
310
+ .input(input.mp4File)
311
+ .inputOptions([`-ss ${inputStartSec}`, `-t ${inputDurationSec}`]);
312
+
313
+ ffmpeg.ffprobe(input.mp4File, (err, metadata) => {
314
+ if (
315
+ !err &&
316
+ metadata.streams.some((stream) => stream.codec_type === 'audio')
317
+ ) {
318
+ audioInputs.push({
319
+ index: index + 1,
320
+ delay: outputOffsetMs,
321
+ volume: volume,
322
+ });
323
+ }
324
+ resolveProbe();
325
+ });
326
+ });
327
+ })
328
+ ).then(() => {
329
+ // Map the video stream from the image sequence
330
+ ffmpegCmd.outputOptions(['-map 0:v']);
331
+
332
+ // Prepare audio filter complex for both video and pure audio inputs
333
+ const allAudioInputs = [...audioInputs, ...audios];
334
+ if (allAudioInputs.length > 0) {
335
+ const audioFilters = allAudioInputs.map((input, i) => {
336
+ const inputIndex = audioInputs.includes(input)
337
+ ? input.index
338
+ : audioInputs.length + i + 1;
339
+ return `[${inputIndex}:a]adelay=${input.delay}|${input.delay},volume=${input.volume},apad[a${i}]`;
340
+ });
341
+ const audioMerge = allAudioInputs.map((_, i) => `[a${i}]`).join('');
342
+ const filterComplex = `${audioFilters.join(
343
+ ';'
344
+ )};${audioMerge}amix=inputs=${
345
+ allAudioInputs.length
346
+ }:dropout_transition=0,aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo,apad,atrim=0:${
347
+ duration / 1000
348
+ }[aout]`;
349
+
350
+ ffmpegCmd.complexFilter(filterComplex);
351
+ ffmpegCmd.outputOptions(['-map [aout]']);
352
+
353
+ // Add pure audio inputs
354
+ audios.forEach((audio) => {
355
+ ffmpegCmd.input(audio.file);
356
+ });
357
+ }
293
358
 
294
359
  ffmpegCmd
295
- .input(input.mp4File)
296
- .inputOptions([`-ss ${inputStartSec}`, `-t ${inputDurationSec}`]);
297
-
298
- audioInputs.push({
299
- index: index + 1, // +1 because the first input is the image sequence
300
- delay: outputOffsetMs,
301
- volume: volume,
302
- });
360
+ .format(format)
361
+ .audioCodec('aac')
362
+ .on('start', (command) => console.log('FFmpeg command:', command))
363
+ .on('end', () => resolve())
364
+ .on('error', (err, stdout, stderr) => {
365
+ console.error('Error:', err.message);
366
+ console.error('stdout:', stdout);
367
+ console.error('stderr:', stderr);
368
+ reject(err);
369
+ })
370
+ .save(attrs.out);
303
371
  });
304
-
305
- // Map the video stream from the image sequence
306
- ffmpegCmd.outputOptions(['-map 0:v']);
307
-
308
- // Prepare audio filter complex
309
- if (audioInputs.length > 0) {
310
- const audioFilters = audioInputs.map(
311
- (input, i) =>
312
- `[${input.index}:a]adelay=${input.delay}|${input.delay},volume=${input.volume}[a${i}]`
313
- );
314
- const audioMerge = audioInputs.map((_, i) => `[a${i}]`).join('');
315
- const filterComplex = `${audioFilters.join(
316
- ';'
317
- )};${audioMerge}amix=inputs=${
318
- audioInputs.length
319
- }:duration=longest[aout]`;
320
-
321
- ffmpegCmd.complexFilter(filterComplex);
322
- ffmpegCmd.outputOptions(['-map [aout]']);
323
- }
324
-
325
- ffmpegCmd
326
- .format(format)
327
- .audioCodec('aac')
328
- .on('start', (command) => console.log('FFmpeg command:', command))
329
- .on('end', () => resolve())
330
- .on('error', (err, stdout, stderr) => {
331
- console.error('Error:', err.message);
332
- console.error('stdout:', stdout);
333
- console.error('stderr:', stderr);
334
- reject(err);
335
- })
336
- .save(attrs.out);
337
372
  });
338
373
  } catch (e) {
339
374
  throw e;