polotno-node 2.4.2 → 2.5.1

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 (3) hide show
  1. package/README.md +22 -0
  2. package/package.json +2 -1
  3. package/video.js +132 -0
package/README.md CHANGED
@@ -231,6 +231,28 @@ const url = await instance.run(async (json) => {
231
231
  }, json);
232
232
  ```
233
233
 
234
+ ## Video export
235
+
236
+ **This part is currently under development and API will change.**
237
+
238
+ ```js
239
+ import { createInstance } from 'polotno-node';
240
+ import { jsonToVideo } from 'polotno/video';
241
+
242
+ // create working instance
243
+ const instance = await createInstance({
244
+ key: '...key...',
245
+ });
246
+
247
+ // load sample json
248
+ const json = JSON.parse(fs.readFileSync('./test-data/video.json'));
249
+ const page = await instance.createPage();
250
+
251
+ await jsonToVideo(page, json, { out: 'out.mp4' });
252
+
253
+ await instance.close();
254
+ ```
255
+
234
256
  ## Your own client
235
257
 
236
258
  By default `polotno-node` ships with the default Polotno Editor with its (hopefully) last version. If you use experimental API such as `unstable_registerShapeModel` and `unstable_registerShapeComponent`, the rendering may fail if you use unknown elements types.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polotno-node",
3
- "version": "2.4.2",
3
+ "version": "2.5.1",
4
4
  "description": "Polotno workflow from NodeJS",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -12,6 +12,7 @@
12
12
  "dist",
13
13
  "index.js",
14
14
  "instance.js",
15
+ "video.js",
15
16
  "README.md"
16
17
  ],
17
18
  "browserslist": "> 0.5%, last 2 versions, not dead",
package/video.js ADDED
@@ -0,0 +1,132 @@
1
+ const fs = require('fs');
2
+ const ffmpeg = require('fluent-ffmpeg');
3
+ const axios = require('axios');
4
+ const path = require('path');
5
+
6
+ const downloadVideo = async (url, destination) => {
7
+ const response = await axios({
8
+ method: 'GET',
9
+ url: url,
10
+ responseType: 'stream',
11
+ });
12
+
13
+ const writer = response.data.pipe(fs.createWriteStream(destination));
14
+
15
+ return new Promise((resolve, reject) => {
16
+ writer.on('finish', resolve);
17
+ writer.on('error', reject);
18
+ });
19
+ };
20
+
21
+ // 2. Convert it into webm format
22
+
23
+ const convertToWebM = (input, output) => {
24
+ console.log(input, output);
25
+ return new Promise((resolve, reject) => {
26
+ ffmpeg(input)
27
+ .outputOptions(['-c:v libvpx', '-crf 30', '-b:v 1000k'])
28
+ .output(output)
29
+ .on('start', () => {})
30
+ .on('end', () => {
31
+ resolve();
32
+ })
33
+ .on('error', (err) => {
34
+ console.log('Error occurred: ' + err.message);
35
+ reject(err);
36
+ })
37
+ .run();
38
+ });
39
+ };
40
+
41
+ // 3. Serialize file into dataurl string
42
+ const fileToDataUrl = (filename) => {
43
+ return new Promise((resolve, reject) => {
44
+ fs.readFile(filename, 'base64', (err, data) => {
45
+ if (err) reject(err);
46
+ resolve(`data:video/webm;base64,${data}`);
47
+ });
48
+ });
49
+ };
50
+
51
+ const videoToDataURL = async (url) => {
52
+ console.log('downloading', url);
53
+ const filename = Math.random().toString(36).substring(7);
54
+ const destination = path.join('./tmp', filename + '.mp4');
55
+ await downloadVideo(url, destination);
56
+ const webmFile = destination.replace('.mp4', '.webm');
57
+ console.log('converting to webm', url);
58
+ await convertToWebM(destination, webmFile);
59
+ const dataUrl = await fileToDataUrl(webmFile);
60
+ return dataUrl;
61
+ };
62
+
63
+ function printProgress(progress) {
64
+ process.stdout.clearLine(0);
65
+ process.stdout.cursorTo(0);
66
+ process.stdout.write(progress + '%');
67
+ }
68
+
69
+ module.exports.jsonToVideo = async function jsonToGifFile(page, json, attrs) {
70
+ try {
71
+ fs.rmSync('./tmp', { recursive: true });
72
+ } catch (e) {}
73
+
74
+ fs.mkdirSync('./tmp', { recursive: true });
75
+
76
+ for (const page of json.pages) {
77
+ for (const el of page.children) {
78
+ if (el.type === 'video') {
79
+ el.src = await videoToDataURL(el.src);
80
+ }
81
+ }
82
+ }
83
+ // first load json
84
+ await page.evaluate(
85
+ async (json, attrs) => {
86
+ store.loadJSON(json);
87
+ await store.waitLoading();
88
+ },
89
+ json,
90
+ attrs || {}
91
+ );
92
+
93
+ const duration = await page.evaluate(async () => {
94
+ return store.duration;
95
+ });
96
+
97
+ const fps = 15;
98
+
99
+ // loop through the images and add each to the animation
100
+ const frames = Math.floor((duration / 1000) * fps);
101
+ for (let i = 0; i < frames; i++) {
102
+ const currentTime = i * 100;
103
+ const dataURL = await page.evaluate(
104
+ async (json, attrs, currentTime) => {
105
+ store.setCurrentTime(currentTime + 1);
106
+ const url = await store.toDataURL({ ...attrs, pixelRatio: 0.5 });
107
+ return url;
108
+ },
109
+ json,
110
+ attrs || {},
111
+ currentTime
112
+ );
113
+ const progress = ((i / frames) * 100).toFixed(1);
114
+ printProgress(progress);
115
+ fs.mkdirSync('./tmp', { recursive: true });
116
+ fs.writeFileSync(`./tmp/${i}.png`, dataURL.split(',')[1], 'base64');
117
+ }
118
+
119
+ await new Promise((resolve, reject) => {
120
+ ffmpeg()
121
+ .input(`./tmp/%d.png`) // Make sure your images are named with numbers, e.g. 1.jpg, 2.jpg, etc.
122
+ .inputFPS(fps)
123
+ .videoCodec('libx264')
124
+ .outputOptions('-pix_fmt yuv420p')
125
+ .format('mp4')
126
+ .on('end', () => resolve())
127
+ .on('error', (err) => reject(err))
128
+ .save(attrs.out);
129
+ });
130
+
131
+ fs.rmSync('./tmp', { recursive: true });
132
+ };