polotno-node 2.15.19 → 3.0.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.
package/video-parallel.js DELETED
@@ -1,414 +0,0 @@
1
- const fs = require('fs');
2
- const ffmpeg = require('fluent-ffmpeg');
3
- const axios = require('axios');
4
- const path = require('path');
5
-
6
- const tmp = require('tmp');
7
- tmp.setGracefulCleanup();
8
-
9
- const downloadVideo = async (url, destination) => {
10
- const response = await axios({
11
- method: 'GET',
12
- url: url,
13
- responseType: 'stream',
14
- });
15
-
16
- const writer = response.data.pipe(fs.createWriteStream(destination));
17
-
18
- return new Promise((resolve, reject) => {
19
- writer.on('finish', resolve);
20
- writer.on('error', reject);
21
- });
22
- };
23
-
24
- // 2. Convert it into webm format
25
- const convertToWebM = (input, output) => {
26
- return new Promise((resolve, reject) => {
27
- ffmpeg(input)
28
- .outputOptions([
29
- '-c:v libvpx-vp9',
30
- '-crf 30',
31
- '-b:v 0',
32
- '-b:a 128k',
33
- '-c:a libopus',
34
- '-cpu-used 4', // Faster encoding with slight quality loss
35
- '-deadline realtime', // Fastest encoding mode
36
- '-threads 0', // Use all available CPU cores
37
- ])
38
- .output(output)
39
- .on('end', () => {
40
- resolve();
41
- })
42
- .on('error', (err) => {
43
- console.error('Error occurred: ' + err.message);
44
- reject(err);
45
- })
46
- .run();
47
- });
48
- };
49
-
50
- // 3. Serialize file into dataurl string
51
- const fileToDataUrl = (filename) => {
52
- return new Promise((resolve, reject) => {
53
- fs.readFile(filename, 'base64', (err, data) => {
54
- if (err) reject(err);
55
- resolve(`data:video/webm;base64,${data}`);
56
- });
57
- });
58
- };
59
-
60
- const convertVideo = async (url, tempFolder, skipWebm) => {
61
- const shortUrl = url.slice(0, 50) + '...';
62
- console.log('Downloading video: ' + shortUrl);
63
- const filename = Math.random().toString(36).substring(7);
64
- const mp4Destination = path.join(tempFolder.name, filename + '.mp4');
65
- await downloadVideo(url, mp4Destination);
66
- let dataURL = '';
67
- if (!skipWebm) {
68
- console.log('Converting video to webm: ' + filename);
69
- const webmDestination = mp4Destination.replace('.mp4', '.webm');
70
- await convertToWebM(mp4Destination, webmDestination);
71
- dataURL = await fileToDataUrl(webmDestination);
72
- console.log('Converting video finished: ' + filename);
73
- }
74
-
75
- return {
76
- dataURL,
77
- mp4File: mp4Destination,
78
- };
79
- };
80
-
81
- module.exports.jsonToVideo = async function jsonToVideo(
82
- inst,
83
- json,
84
- attrs = {}
85
- ) {
86
- const tempFolder = tmp.dirSync({
87
- tmpdir: attrs.tmpdir,
88
- });
89
- // const tempFolder = {
90
- // name: './temp',
91
- // };
92
-
93
- try {
94
- const videoEls = [];
95
-
96
- for (const page of json.pages) {
97
- for (const el of page.children) {
98
- if (el.type === 'video') {
99
- videoEls.push(el);
100
- }
101
- }
102
- }
103
-
104
- await Promise.all(
105
- videoEls.map(async (el) => {
106
- const { dataURL, file, mp4File } = await convertVideo(
107
- el.src,
108
- tempFolder,
109
- attrs.skipWebm
110
- );
111
- if (!attrs.skipWebm) {
112
- el.src = dataURL;
113
- }
114
- el.file = file;
115
- el.mp4File = mp4File;
116
- })
117
- );
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
-
133
- const duration = json.pages.reduce((acc, page) => {
134
- return acc + (page.duration ?? 5000);
135
- }, 0);
136
-
137
- if (duration === 0) {
138
- throw new Error('Video duration is 0');
139
- }
140
- const parallel = attrs.parallel || 5;
141
-
142
- const fps = attrs.fps || 30;
143
- const timePerFrame = 1000 / fps;
144
-
145
- // loop through the images and add each to the animation
146
- const framesNumber = Math.floor((duration / 1000) * fps) || 1;
147
- const frames = Array.from(Array(framesNumber).keys());
148
-
149
- // split frames into parallel chunks
150
- const chunkSize = Math.ceil(framesNumber / parallel);
151
- const chunks = [];
152
- for (let i = 0; i < framesNumber; i += chunkSize) {
153
- chunks.push(frames.slice(i, i + chunkSize));
154
- }
155
-
156
- let lastTime = 0;
157
- const pages = json.pages.map((p, index) => {
158
- const startTime = lastTime;
159
- lastTime = startTime + p.duration;
160
- return {
161
- id: p.id,
162
- startTime,
163
- duration: p.duration,
164
- };
165
- });
166
-
167
- let finishedFramesNumber = 0;
168
-
169
- await Promise.all(
170
- chunks.map(async (chunk, chunkIndex) => {
171
- const instance = typeof inst === 'function' ? await inst() : inst;
172
- const page = await instance.createPage();
173
- await page.evaluate(() => {
174
- if (window.config?.onLoadError) {
175
- window.config.onLoadError((error) => {
176
- window._polotnoError = error;
177
- });
178
- } else {
179
- console.error(
180
- 'onLoadError function is not defined in the client. Error handling will not work.'
181
- );
182
- }
183
- });
184
- page.setDefaultNavigationTimeout(60000); // Increase timeout to 60 seconds
185
-
186
- let lastPageIndex = -1;
187
- for (const frameIndex of chunk) {
188
- let currentTime = frameIndex * timePerFrame;
189
- if (frameIndex === 0) {
190
- // offset the very first frame to enable animation start
191
- currentTime = 1;
192
- }
193
- if (frameIndex === framesNumber - 1) {
194
- // offset the very last frame to enable animation end
195
- currentTime = duration - 1;
196
- }
197
- const storePageIndex = pages.findIndex((p) => {
198
- return (
199
- currentTime >= p.startTime &&
200
- currentTime < p.startTime + p.duration
201
- );
202
- });
203
- if (storePageIndex !== lastPageIndex) {
204
- await page.evaluate(async (json) => {
205
- store.loadJSON(json);
206
- await store.waitLoading();
207
- await new Promise((resolve) => {
208
- setTimeout(resolve, 100);
209
- });
210
- }, json);
211
- await page.evaluate(async (currentTime) => {
212
- store.setCurrentTime(currentTime);
213
- store.checkActivePage();
214
- store.activePage.set({
215
- _rendering: true,
216
- });
217
- await store.waitLoading();
218
- await new Promise((resolve) => {
219
- setTimeout(resolve, 100);
220
- });
221
- if (window.config.unstable_setTextOverflow) {
222
- window.config.unstable_setTextOverflow('resize');
223
- } else {
224
- // console.error(
225
- // 'Can not set text overflow mode. Define window.config.unstable_setTextOverflow function'
226
- // );
227
- }
228
- }, currentTime);
229
- lastPageIndex = storePageIndex;
230
- }
231
- const startTime = Date.now();
232
- const dataURL = await page.evaluate(
233
- async (json, attrs, currentTime) => {
234
- store.setCurrentTime(currentTime);
235
- const currentPage = store.pages.find((p) => {
236
- return (
237
- store.currentTime >= p.startTime &&
238
- store.currentTime < p.startTime + p.duration
239
- );
240
- });
241
- const url = await store.toDataURL({
242
- ...attrs,
243
- _skipTimeout: true,
244
- mimeType: 'image/jpeg',
245
- pageId: currentPage?.id,
246
- });
247
- return url;
248
- },
249
- json,
250
- attrs || {},
251
- currentTime
252
- );
253
- const error = await page.evaluate(() => window._polotnoError);
254
- if (error) {
255
- // clear error
256
- await page.evaluate(() => {
257
- window._polotnoError = null;
258
- });
259
- const message = error.toString();
260
- const isFontError =
261
- message.indexOf('Timeout for loading font') >= 0;
262
- const skipError = isFontError && attrs.skipFontError;
263
-
264
- const isImageError = message.indexOf('image ') >= 0;
265
- const skipImageError = isImageError && attrs.skipImageError;
266
-
267
- if (!skipError && !skipImageError) {
268
- throw new Error('Asset loading error: ' + error);
269
- }
270
- }
271
- // TODO: add error handling!!!!
272
- fs.writeFileSync(
273
- `${tempFolder.name}/${frameIndex}.jpeg`,
274
- dataURL.split(',')[1],
275
- 'base64'
276
- );
277
- finishedFramesNumber += 1;
278
- if (attrs.onProgress) {
279
- const frameTime = Date.now() - startTime;
280
- const progress = (finishedFramesNumber / framesNumber) * 0.95;
281
- attrs.onProgress(progress, frameTime);
282
- }
283
- }
284
- await page.close();
285
- if (typeof inst === 'function') {
286
- await instance.close();
287
- }
288
- })
289
- );
290
-
291
- const inputs = [];
292
- let pageStartTime = 0;
293
- for (const page of json.pages) {
294
- const pageDuration = page.duration || 5000;
295
- for (const el of page.children) {
296
- if (el.type === 'video') {
297
- const startTime = el.startTime || 0;
298
- const endTime = el.endTime || 1;
299
- const dur = el.duration || 5000;
300
- const elStartTime = startTime * dur;
301
-
302
- const elDuration = Math.min(
303
- pageDuration,
304
- dur * (endTime - startTime)
305
- );
306
- const elEndTime = elStartTime + elDuration;
307
- inputs.push({
308
- file: el.file,
309
- mp4File: el.mp4File,
310
- inputStartTime: elStartTime,
311
- inputEndTime: elEndTime,
312
- outputStartTime: pageStartTime,
313
- outputEndTime: pageStartTime + pageDuration,
314
- volume: el.volume,
315
- });
316
- }
317
- }
318
- pageStartTime += pageDuration;
319
- }
320
-
321
- const format = attrs.out.split('.').pop() || 'mp4';
322
-
323
- await new Promise((resolve, reject) => {
324
- const ffmpegCmd = ffmpeg()
325
- .input(`${tempFolder.name}/%d.jpeg`)
326
- .inputFPS(fps)
327
- .videoCodec('libx264')
328
- .outputOptions('-pix_fmt yuv420p')
329
- .videoFilters('scale=trunc(iw/2)*2:trunc(ih/2)*2');
330
-
331
- const audioInputs = [];
332
-
333
- // Use Promise.all to handle async ffprobe calls
334
- Promise.all(
335
- inputs.map((input, index) => {
336
- return new Promise((resolveProbe) => {
337
- const inputStartSec = (input.inputStartTime / 1000).toFixed(3);
338
- const inputDurationSec = (
339
- (input.inputEndTime - input.inputStartTime) /
340
- 1000
341
- ).toFixed(3);
342
- const outputOffsetMs = Math.round(input.outputStartTime);
343
- const volume = input.volume !== undefined ? input.volume : 1;
344
-
345
- ffmpegCmd
346
- .input(input.mp4File)
347
- .inputOptions([`-ss ${inputStartSec}`, `-t ${inputDurationSec}`]);
348
-
349
- ffmpeg.ffprobe(input.mp4File, (err, metadata) => {
350
- if (
351
- !err &&
352
- metadata.streams.some((stream) => stream.codec_type === 'audio')
353
- ) {
354
- audioInputs.push({
355
- index: index + 1,
356
- delay: outputOffsetMs,
357
- volume: volume,
358
- });
359
- }
360
- resolveProbe();
361
- });
362
- });
363
- })
364
- ).then(() => {
365
- // Map the video stream from the image sequence
366
- ffmpegCmd.outputOptions(['-map 0:v']);
367
-
368
- // Prepare audio filter complex for both video and pure audio inputs
369
- const allAudioInputs = [...audioInputs, ...audios];
370
- if (allAudioInputs.length > 0) {
371
- const audioFilters = allAudioInputs.map((input, i) => {
372
- const inputIndex = audioInputs.includes(input)
373
- ? input.index
374
- : audioInputs.length + i + 1;
375
- return `[${inputIndex}:a]adelay=${input.delay}|${input.delay},volume=${input.volume},apad[a${i}]`;
376
- });
377
- const audioMerge = allAudioInputs.map((_, i) => `[a${i}]`).join('');
378
- const filterComplex = `${audioFilters.join(
379
- ';'
380
- )};${audioMerge}amix=inputs=${
381
- allAudioInputs.length
382
- }:dropout_transition=0,aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo,apad,atrim=0:${
383
- duration / 1000
384
- }[aout]`;
385
-
386
- ffmpegCmd.complexFilter(filterComplex);
387
- ffmpegCmd.outputOptions(['-map [aout]']);
388
-
389
- // Add pure audio inputs
390
- audios.forEach((audio) => {
391
- ffmpegCmd.input(audio.file);
392
- });
393
- }
394
-
395
- ffmpegCmd
396
- .format(format)
397
- .audioCodec('aac')
398
- .on('start', (command) => console.log('FFmpeg command:', command))
399
- .on('end', () => resolve())
400
- .on('error', (err, stdout, stderr) => {
401
- console.error('Error:', err.message);
402
- console.error('stdout:', stdout);
403
- console.error('stderr:', stderr);
404
- reject(err);
405
- })
406
- .save(attrs.out);
407
- });
408
- });
409
- } catch (e) {
410
- throw e;
411
- } finally {
412
- fs.rmSync(tempFolder.name, { recursive: true });
413
- }
414
- };