polotno-node 2.9.9 → 2.9.11

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/dist/index.html CHANGED
@@ -8,7 +8,7 @@
8
8
  }
9
9
  </style>
10
10
  <meta name="viewport" content="width=device-width, initial-scale=1" />
11
- <script type="module" crossorigin src="./assets/index-DsJ35GHi.js"></script>
11
+ <script type="module" crossorigin src="./assets/index-BwgLDHgn.js"></script>
12
12
  </head>
13
13
  <body>
14
14
  <div id="root"></div>
package/index.js CHANGED
@@ -45,7 +45,10 @@ const minimal_args = [
45
45
  '--no-sandbox',
46
46
  '--no-zygote',
47
47
  '--password-store=basic',
48
- !isMacOs && '--use-gl=swiftshader',
48
+ // this line breaks the rendering on most of OS
49
+ // it doesn't allow creating of pages
50
+ //'--use-gl=swiftshader',
51
+ '--disable-gpu',
49
52
  '--use-mock-keychain',
50
53
  '--font-render-hinting=none',
51
54
  '--disable-font-subpixel-positioning',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polotno-node",
3
- "version": "2.9.9",
3
+ "version": "2.9.11",
4
4
  "description": "Polotno workflow from NodeJS",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -18,7 +18,7 @@
18
18
  ],
19
19
  "browserslist": "> 0.5%, last 2 versions, not dead",
20
20
  "devDependencies": {
21
- "ava": "^6.1.2",
21
+ "ava": "^6.1.3",
22
22
  "axios": "^1.6.8",
23
23
  "canvas": "^2.11.2",
24
24
  "dotenv": "^16.4.5",
@@ -26,17 +26,17 @@
26
26
  "gifencoder": "^2.0.1",
27
27
  "pixelmatch": "^5.3.0",
28
28
  "pngjs": "^7.0.0",
29
- "polotno": "^2.4.31",
30
- "react": "^18.2.0",
31
- "react-dom": "^18.2.0",
32
- "vite": "^5.2.9"
29
+ "polotno": "^2.5.1",
30
+ "react": "^18.3.1",
31
+ "react-dom": "^18.3.1",
32
+ "vite": "^5.2.11"
33
33
  },
34
34
  "author": "Anton Lavrenov",
35
35
  "license": "MIT",
36
36
  "dependencies": {
37
37
  "@sparticuz/chromium": "123.0.1",
38
- "puppeteer": "22.7.0",
39
- "puppeteer-core": "22.7.0",
38
+ "puppeteer": "22.8.0",
39
+ "puppeteer-core": "22.8.0",
40
40
  "tmp": "^0.2.3"
41
41
  },
42
42
  "bugs": {
package/video-parallel.js CHANGED
@@ -4,7 +4,6 @@ const axios = require('axios');
4
4
  const path = require('path');
5
5
 
6
6
  const tmp = require('tmp');
7
-
8
7
  tmp.setGracefulCleanup();
9
8
 
10
9
  const downloadVideo = async (url, destination) => {
@@ -50,24 +49,23 @@ const fileToDataUrl = (filename) => {
50
49
  });
51
50
  };
52
51
 
53
- const videoToDataURL = async (url) => {
54
- console.log('downloading', url);
52
+ const videoToDataURL = async (url, tempFolder) => {
55
53
  const filename = Math.random().toString(36).substring(7);
56
- const tempFolder = tmp.dirSync();
57
54
  const destination = path.join(tempFolder.name, filename + '.mp4');
58
55
  await downloadVideo(url, destination);
59
56
  const webmFile = destination.replace('.mp4', '.webm');
60
- console.log('converting to webm', url);
61
57
  await convertToWebM(destination, webmFile);
62
- const dataUrl = await fileToDataUrl(webmFile);
63
- fs.rmSync(tempFolder.name, { recursive: true });
64
- return dataUrl;
58
+ const dataURL = await fileToDataUrl(webmFile);
59
+ return {
60
+ dataURL,
61
+ file: webmFile,
62
+ };
65
63
  };
66
64
 
67
65
  const urlCache = {};
68
- const cachedVideoToDataURL = async (url) => {
66
+ const cachedVideoToDataURL = async (url, tempFolder) => {
69
67
  if (!urlCache[url]) {
70
- urlCache[url] = videoToDataURL(url);
68
+ urlCache[url] = videoToDataURL(url, tempFolder);
71
69
  }
72
70
  return urlCache[url];
73
71
  };
@@ -76,11 +74,7 @@ function printProgress(progress) {
76
74
  console.log('Rendering frame: ' + progress + '%');
77
75
  }
78
76
 
79
- module.exports.jsonToVideo = async function jsonToVideo(
80
- createInstance,
81
- json,
82
- attrs
83
- ) {
77
+ module.exports.jsonToVideo = async function jsonToVideo(inst, json, attrs) {
84
78
  const tempFolder = tmp.dirSync();
85
79
 
86
80
  const videoEls = [];
@@ -95,7 +89,9 @@ module.exports.jsonToVideo = async function jsonToVideo(
95
89
 
96
90
  await Promise.all(
97
91
  videoEls.map(async (el) => {
98
- el.src = await cachedVideoToDataURL(el.src);
92
+ const { dataURL, file } = await cachedVideoToDataURL(el.src, tempFolder);
93
+ el.src = dataURL;
94
+ el.file = file;
99
95
  })
100
96
  );
101
97
 
@@ -134,7 +130,7 @@ module.exports.jsonToVideo = async function jsonToVideo(
134
130
 
135
131
  await Promise.all(
136
132
  chunks.map(async (chunk, chunkIndex) => {
137
- const instance = await createInstance();
133
+ const instance = typeof inst === 'function' ? await inst() : inst;
138
134
  const page = await instance.createPage();
139
135
  await page.evaluate(async (json) => {
140
136
  store.loadJSON(json);
@@ -201,65 +197,72 @@ module.exports.jsonToVideo = async function jsonToVideo(
201
197
  const progress = ((finishedFramesNumber / framesNumber) * 100).toFixed(
202
198
  1
203
199
  );
204
- printProgress(progress);
200
+ // printProgress(progress);
205
201
  }
206
- // TODO: why can't we just close the one created page?
207
- // const browserPages = await instance.browser.pages();
208
- // for (let i = 0; i < browserPages.length; i++) {
209
- // await browserPages[i].close();
210
- // }
211
202
  await page.close();
212
- if (!attrs.keepInstance) {
203
+ if (typeof inst === 'function') {
213
204
  await instance.close();
214
205
  }
215
206
  })
216
207
  );
217
208
 
218
- // for (let i = 0; i < frames; i++) {
219
- // let currentTime = i * timePerFrame;
220
- // if (i === 0) {
221
- // // offset the very first frame to enable animation start
222
- // currentTime = 1;
223
- // }
224
- // if (i === frames - 1) {
225
- // // offset the very last frame to enable animation end
226
- // currentTime = duration - 1;
227
- // }
228
-
229
- // const dataURL = await page.evaluate(
230
- // async (json, attrs, currentTime) => {
231
- // store.setCurrentTime(currentTime);
232
- // const currentPage = store.pages.find((p) => {
233
- // return (
234
- // store.currentTime >= p.startTime &&
235
- // store.currentTime < p.startTime + p.duration
236
- // );
237
- // });
238
- // const url = await store.toDataURL({
239
- // pixelRatio: 0.5,
240
- // ...attrs,
241
- // pageId: currentPage?.id,
242
- // });
243
- // return url;
244
- // },
245
- // json,
246
- // attrs || {},
247
- // currentTime
248
- // );
249
- // const progress = ((i / (frames - 1)) * 100).toFixed(1);
250
- // printProgress(progress);
251
- // fs.mkdirSync('./tmp', { recursive: true });
252
- // fs.writeFileSync(`./tmp/${i}.png`, dataURL.split(',')[1], 'base64');
253
- // }
209
+ const inputs = [];
210
+ let pageStartTime = 0;
211
+ for (const page of json.pages) {
212
+ for (const el of page.children) {
213
+ if (el.type === 'video') {
214
+ const elStartTime = el.startTime * el.duration;
215
+ const elDuration = Math.min(
216
+ page.duration,
217
+ el.duration * (el.endTime - el.startTime)
218
+ );
219
+ const elEndTime = elStartTime + elDuration;
220
+ inputs.push({
221
+ file: el.file,
222
+ inputStartTime: elStartTime,
223
+ inputEndTime: elEndTime,
224
+ outputStartTime: pageStartTime,
225
+ outputEndTime: pageStartTime + page.duration,
226
+ });
227
+ }
228
+ }
229
+ pageStartTime += page.duration;
230
+ }
254
231
 
255
232
  const format = attrs.out.split('.').pop() || 'mp4';
256
233
 
257
234
  await new Promise((resolve, reject) => {
258
- ffmpeg()
259
- .input(`${tempFolder.name}/%d.png`) // Make sure your images are named with numbers, e.g. 1.jpg, 2.jpg, etc.
235
+ const ffmpegCmd = ffmpeg()
236
+ .input(`${tempFolder.name}/%d.png`)
260
237
  .inputFPS(fps)
261
- // .videoCodec('libx265')
262
- .outputOptions('-pix_fmt yuva420p')
238
+ .videoCodec('libx264')
239
+ .outputOptions('-pix_fmt yuv420p');
240
+
241
+ // Add each audio segment as a separate input
242
+ inputs.forEach((input, index) => {
243
+ const inputStartSec = (input.inputStartTime / 1000).toFixed(3);
244
+ const inputDurationSec = (
245
+ (input.inputEndTime - input.inputStartTime) /
246
+ 1000
247
+ ).toFixed(3);
248
+ const outputOffsetSec = (input.outputStartTime / 1000).toFixed(3);
249
+
250
+ // Apply `-itsoffset` before each input file to shift its audio stream
251
+ ffmpegCmd
252
+ .inputOptions([`-itsoffset ${outputOffsetSec}`]) // Shift the input time
253
+ .input(input.file) // Add the actual input file
254
+ .inputOptions([`-ss ${inputStartSec}`, `-t ${inputDurationSec}`]); // Specify the timing to cut the audio
255
+ });
256
+
257
+ // Map the video stream from the image sequence
258
+ ffmpegCmd.outputOptions(['-map 0:v']);
259
+
260
+ // Map each audio stream
261
+ inputs.forEach((_, index) => {
262
+ ffmpegCmd.outputOptions([`-map ${index + 1}:a`]);
263
+ });
264
+
265
+ ffmpegCmd
263
266
  .format(format)
264
267
  .on('end', () => resolve())
265
268
  .on('error', (err) => reject(err))