polotno-node 2.9.17 → 2.9.19

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