polotno-node 2.9.18 → 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.
- package/package.json +1 -1
- package/video-parallel.js +192 -178
package/package.json
CHANGED
package/video-parallel.js
CHANGED
|
@@ -50,12 +50,15 @@ const fileToDataUrl = (filename) => {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
const videoToDataURL = async (url, tempFolder) => {
|
|
53
|
-
|
|
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,208 +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
|
-
|
|
84
|
+
try {
|
|
85
|
+
const videoEls = [];
|
|
79
86
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
111
|
+
const fps = attrs.fps || 15;
|
|
112
|
+
const timePerFrame = 1000 / fps;
|
|
103
113
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
137
|
+
let finishedFramesNumber = 0;
|
|
128
138
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
215
|
+
await page.close();
|
|
216
|
+
if (typeof inst === 'function') {
|
|
217
|
+
await instance.close();
|
|
151
218
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
245
|
+
const format = attrs.out.split('.').pop() || 'mp4';
|
|
235
246
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
|
243
254
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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);
|
|
252
263
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
+
});
|
|
259
270
|
|
|
260
|
-
|
|
261
|
-
|
|
271
|
+
// Map the video stream from the image sequence
|
|
272
|
+
ffmpegCmd.outputOptions(['-map 0:v']);
|
|
262
273
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
});
|
|
274
|
+
// Map each audio stream
|
|
275
|
+
inputs.forEach((_, index) => {
|
|
276
|
+
ffmpegCmd.outputOptions([`-map ${index + 1}:a?`]);
|
|
277
|
+
});
|
|
279
278
|
|
|
280
|
-
|
|
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
|
+
}
|
|
281
295
|
};
|