polotno-node 2.5.0 → 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.
- package/package.json +2 -1
- package/video.js +132 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polotno-node",
|
|
3
|
-
"version": "2.5.
|
|
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
|
+
};
|