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.
- package/package.json +1 -1
- package/video-parallel.js +214 -186
package/package.json
CHANGED
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([
|
|
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
|
-
|
|
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
|
|
56
|
-
await downloadVideo(url,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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:
|
|
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
|
-
|
|
92
|
+
try {
|
|
93
|
+
const videoEls = [];
|
|
79
94
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
119
|
+
const fps = attrs.fps || 15;
|
|
120
|
+
const timePerFrame = 1000 / fps;
|
|
103
121
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
145
|
+
let finishedFramesNumber = 0;
|
|
128
146
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
223
|
+
await page.close();
|
|
224
|
+
if (typeof inst === 'function') {
|
|
225
|
+
await instance.close();
|
|
151
226
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
269
|
-
.
|
|
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
|
-
|
|
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
|
};
|