polotno-node 2.9.18 → 2.9.20

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 +214 -186
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polotno-node",
3
- "version": "2.9.18",
3
+ "version": "2.9.20",
4
4
  "description": "Polotno workflow from NodeJS",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/video-parallel.js CHANGED
@@ -25,7 +25,14 @@ const downloadVideo = async (url, destination) => {
25
25
  const convertToWebM = (input, output) => {
26
26
  return new Promise((resolve, reject) => {
27
27
  ffmpeg(input)
28
- .outputOptions(['-c:v libvpx', '-crf 30', '-b:v 1000k'])
28
+ .outputOptions([
29
+ '-c:v libvpx',
30
+ '-crf 30',
31
+ '-b:v 1000k',
32
+ '-quality realtime',
33
+ '-speed 4',
34
+ '-threads 4',
35
+ ])
29
36
  .output(output)
30
37
  .on('start', () => {})
31
38
  .on('end', () => {
@@ -50,17 +57,21 @@ const fileToDataUrl = (filename) => {
50
57
  };
51
58
 
52
59
  const videoToDataURL = async (url, tempFolder) => {
53
- console.log('Downloading video: ' + url.slice(0, 50) + '...');
60
+ const shortUrl = url.slice(0, 50) + '...';
61
+ console.log('Downloading video: ' + shortUrl);
54
62
  const filename = Math.random().toString(36).substring(7);
55
- const destination = path.join(tempFolder.name, filename + '.mp4');
56
- await downloadVideo(url, destination);
57
- const webmFile = destination.replace('.mp4', '.webm');
58
- await convertToWebM(destination, webmFile);
59
- const dataURL = await fileToDataUrl(webmFile);
60
- console.log('Downloading video finished: ' + url.slice(0, 50) + '...');
63
+ const mp4Destination = path.join(tempFolder.name, filename + '.mp4');
64
+ await downloadVideo(url, mp4Destination);
65
+ console.log('Converting video to webm: ' + mp4Destination);
66
+ const webmDestination = mp4Destination.replace('.mp4', '.webm');
67
+ await convertToWebM(mp4Destination, webmDestination);
68
+ fs.unlinkSync(mp4Destination);
69
+ const dataURL = await fileToDataUrl(webmDestination);
70
+
71
+ console.log('Converting video finished: ' + webmDestination);
61
72
  return {
62
73
  dataURL,
63
- file: webmFile,
74
+ file: webmDestination,
64
75
  };
65
76
  };
66
77
 
@@ -74,208 +85,225 @@ const cachedVideoToDataURL = async (url, tempFolder) => {
74
85
 
75
86
  module.exports.jsonToVideo = async function jsonToVideo(inst, json, attrs) {
76
87
  const tempFolder = tmp.dirSync();
88
+ // const tempFolder = {
89
+ // name: './temp',
90
+ // };
77
91
 
78
- const videoEls = [];
92
+ try {
93
+ const videoEls = [];
79
94
 
80
- for (const page of json.pages) {
81
- for (const el of page.children) {
82
- if (el.type === 'video') {
83
- videoEls.push(el);
95
+ for (const page of json.pages) {
96
+ for (const el of page.children) {
97
+ if (el.type === 'video') {
98
+ videoEls.push(el);
99
+ }
84
100
  }
85
101
  }
86
- }
87
102
 
88
- await Promise.all(
89
- videoEls.map(async (el) => {
90
- const { dataURL, file } = await cachedVideoToDataURL(el.src, tempFolder);
91
- el.src = dataURL;
92
- el.file = file;
93
- })
94
- );
103
+ await Promise.all(
104
+ videoEls.map(async (el) => {
105
+ const { dataURL, file } = await cachedVideoToDataURL(
106
+ el.src,
107
+ tempFolder
108
+ );
109
+ el.src = dataURL;
110
+ el.file = file;
111
+ })
112
+ );
95
113
 
96
- const duration = json.pages.reduce((acc, page) => {
97
- return acc + (page.duration ?? 5000);
98
- }, 0);
99
- const parallel = attrs.parallel || 5;
114
+ const duration = json.pages.reduce((acc, page) => {
115
+ return acc + (page.duration ?? 5000);
116
+ }, 0);
117
+ const parallel = attrs.parallel || 5;
100
118
 
101
- const fps = attrs.fps || 15;
102
- const timePerFrame = 1000 / fps;
119
+ const fps = attrs.fps || 15;
120
+ const timePerFrame = 1000 / fps;
103
121
 
104
- // loop through the images and add each to the animation
105
- const framesNumber = Math.floor((duration / 1000) * fps);
106
- const frames = Array.from(Array(framesNumber).keys());
122
+ // loop through the images and add each to the animation
123
+ const framesNumber = Math.floor((duration / 1000) * fps);
124
+ const frames = Array.from(Array(framesNumber).keys());
107
125
 
108
- // split frames into parallel chunks
109
- const chunkSize = Math.ceil(framesNumber / parallel);
110
- const chunks = [];
111
- for (let i = 0; i < framesNumber; i += chunkSize) {
112
- chunks.push(frames.slice(i, i + chunkSize));
113
- }
126
+ // split frames into parallel chunks
127
+ const chunkSize = Math.ceil(framesNumber / parallel);
128
+ const chunks = [];
129
+ for (let i = 0; i < framesNumber; i += chunkSize) {
130
+ chunks.push(frames.slice(i, i + chunkSize));
131
+ }
114
132
 
115
- const pages = json.pages.map((p, index) => {
116
- const previousPage = json.pages[index - 1];
117
- const startTime = previousPage
118
- ? previousPage.startTime + previousPage.duration
119
- : 0;
120
- return {
121
- id: p.id,
122
- startTime,
123
- duration: p.duration,
124
- };
125
- });
133
+ const pages = json.pages.map((p, index) => {
134
+ const previousPage = json.pages[index - 1];
135
+ const startTime = previousPage
136
+ ? previousPage.startTime + previousPage.duration
137
+ : 0;
138
+ return {
139
+ id: p.id,
140
+ startTime,
141
+ duration: p.duration,
142
+ };
143
+ });
126
144
 
127
- let finishedFramesNumber = 0;
145
+ let finishedFramesNumber = 0;
128
146
 
129
- await Promise.all(
130
- chunks.map(async (chunk, chunkIndex) => {
131
- const instance = typeof inst === 'function' ? await inst() : inst;
132
- const page = await instance.createPage();
133
- await page.evaluate(async (json) => {
134
- store.loadJSON(json);
135
- await store.waitLoading();
136
- await new Promise((resolve) => {
137
- setTimeout(resolve, 100);
138
- });
139
- }, json);
147
+ await Promise.all(
148
+ chunks.map(async (chunk, chunkIndex) => {
149
+ const instance = typeof inst === 'function' ? await inst() : inst;
150
+ const page = await instance.createPage();
151
+ await page.evaluate(async (json) => {
152
+ store.loadJSON(json);
153
+ await store.waitLoading();
154
+ await new Promise((resolve) => {
155
+ setTimeout(resolve, 100);
156
+ });
157
+ }, json);
140
158
 
141
- let lastPageIndex = -1;
142
- for (const frameIndex of chunk) {
143
- let currentTime = frameIndex * timePerFrame;
144
- if (frameIndex === 0) {
145
- // offset the very first frame to enable animation start
146
- currentTime = 1;
159
+ let lastPageIndex = -1;
160
+ for (const frameIndex of chunk) {
161
+ let currentTime = frameIndex * timePerFrame;
162
+ if (frameIndex === 0) {
163
+ // offset the very first frame to enable animation start
164
+ currentTime = 1;
165
+ }
166
+ if (frameIndex === framesNumber - 1) {
167
+ // offset the very last frame to enable animation end
168
+ currentTime = duration - 1;
169
+ }
170
+ const storePageIndex = pages.findIndex((p) => {
171
+ return (
172
+ currentTime >= p.startTime &&
173
+ currentTime < p.startTime + p.duration
174
+ );
175
+ });
176
+ if (storePageIndex !== lastPageIndex) {
177
+ await page.evaluate(async (currentTime) => {
178
+ store.setCurrentTime(currentTime);
179
+ await store.waitLoading();
180
+ if (window.config.unstable_setTextOverflow) {
181
+ window.config.unstable_setTextOverflow('resize');
182
+ } else {
183
+ // console.error(
184
+ // 'Can not set text overflow mode. Define window.config.unstable_setTextOverflow function'
185
+ // );
186
+ }
187
+ });
188
+ }
189
+ const startTime = Date.now();
190
+ const dataURL = await page.evaluate(
191
+ async (json, attrs, currentTime) => {
192
+ store.setCurrentTime(currentTime);
193
+ const currentPage = store.pages.find((p) => {
194
+ return (
195
+ store.currentTime >= p.startTime &&
196
+ store.currentTime < p.startTime + p.duration
197
+ );
198
+ });
199
+ const url = await store.toDataURL({
200
+ ...attrs,
201
+ _skipTimeout: true,
202
+ mimeType: 'image/jpeg',
203
+ pageId: currentPage?.id,
204
+ });
205
+ return url;
206
+ },
207
+ json,
208
+ attrs || {},
209
+ currentTime
210
+ );
211
+ fs.writeFileSync(
212
+ `${tempFolder.name}/${frameIndex}.jpeg`,
213
+ dataURL.split(',')[1],
214
+ 'base64'
215
+ );
216
+ finishedFramesNumber += 1;
217
+ if (attrs.onProgress) {
218
+ const frameTime = Date.now() - startTime;
219
+ const progress = finishedFramesNumber / framesNumber;
220
+ attrs.onProgress(progress, frameTime);
221
+ }
147
222
  }
148
- if (frameIndex === framesNumber - 1) {
149
- // offset the very last frame to enable animation end
150
- currentTime = duration - 1;
223
+ await page.close();
224
+ if (typeof inst === 'function') {
225
+ await instance.close();
151
226
  }
152
- const storePageIndex = pages.findIndex((p) => {
153
- return (
154
- currentTime >= p.startTime && currentTime < p.startTime + p.duration
227
+ })
228
+ );
229
+
230
+ const inputs = [];
231
+ let pageStartTime = 0;
232
+ for (const page of json.pages) {
233
+ for (const el of page.children) {
234
+ if (el.type === 'video') {
235
+ const elStartTime = el.startTime * el.duration;
236
+ const elDuration = Math.min(
237
+ page.duration,
238
+ el.duration * (el.endTime - el.startTime)
155
239
  );
156
- });
157
- if (storePageIndex !== lastPageIndex) {
158
- await page.evaluate(async (currentTime) => {
159
- store.setCurrentTime(currentTime);
160
- await store.waitLoading();
161
- if (window.config.unstable_setTextOverflow) {
162
- window.config.unstable_setTextOverflow('resize');
163
- } else {
164
- // console.error(
165
- // 'Can not set text overflow mode. Define window.config.unstable_setTextOverflow function'
166
- // );
167
- }
240
+ const elEndTime = elStartTime + elDuration;
241
+ inputs.push({
242
+ file: el.file,
243
+ inputStartTime: elStartTime,
244
+ inputEndTime: elEndTime,
245
+ outputStartTime: pageStartTime,
246
+ outputEndTime: pageStartTime + page.duration,
168
247
  });
169
248
  }
170
- const startTime = Date.now();
171
- const dataURL = await page.evaluate(
172
- async (json, attrs, currentTime) => {
173
- store.setCurrentTime(currentTime);
174
- const currentPage = store.pages.find((p) => {
175
- return (
176
- store.currentTime >= p.startTime &&
177
- store.currentTime < p.startTime + p.duration
178
- );
179
- });
180
- const url = await store.toDataURL({
181
- ...attrs,
182
- _skipTimeout: true,
183
- mimeType: 'image/jpeg',
184
- pageId: currentPage?.id,
185
- });
186
- return url;
187
- },
188
- json,
189
- attrs || {},
190
- currentTime
191
- );
192
- fs.writeFileSync(
193
- `${tempFolder.name}/${frameIndex}.jpeg`,
194
- dataURL.split(',')[1],
195
- 'base64'
196
- );
197
- finishedFramesNumber += 1;
198
- if (attrs.onProgress) {
199
- const frameTime = Date.now() - startTime;
200
- const progress = finishedFramesNumber / framesNumber;
201
- attrs.onProgress(progress, frameTime);
202
- }
203
- }
204
- await page.close();
205
- if (typeof inst === 'function') {
206
- await instance.close();
207
- }
208
- })
209
- );
210
-
211
- const inputs = [];
212
- let pageStartTime = 0;
213
- for (const page of json.pages) {
214
- for (const el of page.children) {
215
- if (el.type === 'video') {
216
- const elStartTime = el.startTime * el.duration;
217
- const elDuration = Math.min(
218
- page.duration,
219
- el.duration * (el.endTime - el.startTime)
220
- );
221
- const elEndTime = elStartTime + elDuration;
222
- inputs.push({
223
- file: el.file,
224
- inputStartTime: elStartTime,
225
- inputEndTime: elEndTime,
226
- outputStartTime: pageStartTime,
227
- outputEndTime: pageStartTime + page.duration,
228
- });
229
249
  }
250
+ pageStartTime += page.duration;
230
251
  }
231
- pageStartTime += page.duration;
232
- }
233
-
234
- const format = attrs.out.split('.').pop() || 'mp4';
235
-
236
- await new Promise((resolve, reject) => {
237
- const ffmpegCmd = ffmpeg()
238
- .input(`${tempFolder.name}/%d.jpeg`)
239
- .inputFPS(fps)
240
- .videoCodec('libx264')
241
- .outputOptions('-pix_fmt yuv420p')
242
- .videoFilters('scale=trunc(iw/2)*2:trunc(ih/2)*2'); // Ensure dimensions are divisible by 2
243
252
 
244
- // Add each audio segment as a separate input
245
- inputs.forEach((input, index) => {
246
- const inputStartSec = (input.inputStartTime / 1000).toFixed(3);
247
- const inputDurationSec = (
248
- (input.inputEndTime - input.inputStartTime) /
249
- 1000
250
- ).toFixed(3);
251
- const outputOffsetSec = (input.outputStartTime / 1000).toFixed(3);
253
+ const format = attrs.out.split('.').pop() || 'mp4';
252
254
 
253
- // Apply `-itsoffset` before each input file to shift its audio stream
254
- ffmpegCmd
255
- .inputOptions([`-itsoffset ${outputOffsetSec}`]) // Shift the input time
256
- .input(input.file) // Add the actual input file
257
- .inputOptions([`-ss ${inputStartSec}`, `-t ${inputDurationSec}`]); // Specify the timing to cut the audio
255
+ // print all files in temp folder
256
+ fs.readdirSync(tempFolder.name).forEach((file) => {
257
+ console.log(file);
258
258
  });
259
+ console.time('ffmpeg');
260
+ await new Promise((resolve, reject) => {
261
+ const ffmpegCmd = ffmpeg()
262
+ .input(`${tempFolder.name}/%d.jpeg`)
263
+ .inputFPS(fps)
264
+ .videoCodec('libx264')
265
+ .outputOptions('-pix_fmt yuv420p')
266
+ .videoFilters('scale=trunc(iw/2)*2:trunc(ih/2)*2'); // Ensure dimensions are divisible by 2
259
267
 
260
- // Map the video stream from the image sequence
261
- ffmpegCmd.outputOptions(['-map 0:v']);
268
+ // Add each audio segment as a separate input
269
+ inputs.forEach((input, index) => {
270
+ const inputStartSec = (input.inputStartTime / 1000).toFixed(3);
271
+ const inputDurationSec = (
272
+ (input.inputEndTime - input.inputStartTime) /
273
+ 1000
274
+ ).toFixed(3);
275
+ const outputOffsetSec = (input.outputStartTime / 1000).toFixed(3);
262
276
 
263
- // Map each audio stream
264
- inputs.forEach((_, index) => {
265
- ffmpegCmd.outputOptions([`-map ${index + 1}:a?`]);
266
- });
277
+ // Apply `-itsoffset` before each input file to shift its audio stream
278
+ ffmpegCmd
279
+ .inputOptions([`-itsoffset ${outputOffsetSec}`]) // Shift the input time
280
+ .input(input.file) // Add the actual input file
281
+ .inputOptions([`-ss ${inputStartSec}`, `-t ${inputDurationSec}`]); // Specify the timing to cut the audio
282
+ });
267
283
 
268
- ffmpegCmd
269
- .format(format)
270
- .on('end', () => resolve())
271
- .on('error', (err, stdout, stderr) => {
272
- console.log(err.message);
273
- console.log('stdout:\n' + stdout);
274
- console.log('stderr:\n' + stderr);
275
- reject(err);
276
- })
277
- .save(attrs.out);
278
- });
284
+ // Map the video stream from the image sequence
285
+ ffmpegCmd.outputOptions(['-map 0:v']);
279
286
 
280
- fs.rmSync(tempFolder.name, { recursive: true });
287
+ // Map each audio stream
288
+ inputs.forEach((_, index) => {
289
+ ffmpegCmd.outputOptions([`-map ${index + 1}:a?`]);
290
+ });
291
+
292
+ ffmpegCmd
293
+ .format(format)
294
+ .on('end', () => resolve())
295
+ .on('error', (err, stdout, stderr) => {
296
+ console.log(err.message);
297
+ console.log('stdout:\n' + stdout);
298
+ console.log('stderr:\n' + stderr);
299
+ reject(err);
300
+ })
301
+ .save(attrs.out);
302
+ });
303
+ } catch (e) {
304
+ throw e;
305
+ } finally {
306
+ console.timeEnd('ffmpeg');
307
+ fs.rmSync(tempFolder.name, { recursive: true });
308
+ }
281
309
  };