image-video-optimizer 3.0.4 → 3.0.5
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/bin/cli.js +1 -1
- package/package.json +1 -1
- package/src/videoProcessor.js +121 -93
package/bin/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ const optimizer = require('../src/index');
|
|
|
7
7
|
program
|
|
8
8
|
.name('image-video-optimizer')
|
|
9
9
|
.description('Optimize and compress images and videos in a directory')
|
|
10
|
-
.version('3.0.
|
|
10
|
+
.version('3.0.5')
|
|
11
11
|
.argument('[target-dir]', 'Target directory to optimize (default: current directory)', process.cwd())
|
|
12
12
|
.action(async (targetDir) => {
|
|
13
13
|
try {
|
package/package.json
CHANGED
package/src/videoProcessor.js
CHANGED
|
@@ -1,113 +1,141 @@
|
|
|
1
|
+
const ffmpeg = require('fluent-ffmpeg');
|
|
1
2
|
const fs = require('fs');
|
|
2
|
-
const sharp = require('sharp');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Process a single
|
|
7
|
-
* @param {string}
|
|
6
|
+
* Process a single video file
|
|
7
|
+
* @param {string} videoPath - Path to the video
|
|
8
8
|
* @param {object} config - Configuration object
|
|
9
9
|
* @returns {Promise<{success: boolean, originalSize: number, optimizedSize: number, message: string}>}
|
|
10
10
|
*/
|
|
11
|
-
async function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
async function processVideo(videoPath, config) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
try {
|
|
14
|
+
fs.stat(videoPath, async (err, stat) => {
|
|
15
|
+
if (err) {
|
|
16
|
+
return resolve({
|
|
17
|
+
success: false,
|
|
18
|
+
originalSize: 0,
|
|
19
|
+
optimizedSize: 0,
|
|
20
|
+
message: `✗ ${path.basename(videoPath)} - Cannot access file`,
|
|
21
|
+
error: true
|
|
22
|
+
});
|
|
23
|
+
}
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
const originalSize = stat.size;
|
|
26
|
+
const tmpPath = videoPath + '.tmp.mp4';
|
|
27
|
+
const ext = path.extname(videoPath).toLowerCase();
|
|
28
|
+
const directory = path.dirname(videoPath);
|
|
29
|
+
const filename = path.basename(videoPath, ext);
|
|
30
|
+
const finalPath = path.join(directory, filename + '.mp4');
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
// Create ffmpeg command
|
|
33
|
+
// Map codec names to ffmpeg codec names
|
|
34
|
+
const codecMap = {
|
|
35
|
+
'h264': 'libx264',
|
|
36
|
+
'h265': 'libx265',
|
|
37
|
+
'vp8': 'libvpx',
|
|
38
|
+
'vp9': 'libvpx-vp9'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const outputCodec = codecMap[config.video_encode.toLowerCase()] || config.video_encode || 'libx264';
|
|
42
|
+
|
|
43
|
+
const command = ffmpeg(videoPath)
|
|
44
|
+
.videoCodec(outputCodec)
|
|
45
|
+
.size(`${config.video_max_width}x?`)
|
|
46
|
+
.outputOptions([
|
|
47
|
+
'-preset fast',
|
|
48
|
+
'-crf 28'
|
|
49
|
+
])
|
|
50
|
+
.output(tmpPath);
|
|
27
51
|
|
|
28
|
-
|
|
29
|
-
|
|
52
|
+
command
|
|
53
|
+
.on('end', async () => {
|
|
54
|
+
try {
|
|
55
|
+
const tmpStat = await new Promise((res, rej) => {
|
|
56
|
+
fs.stat(tmpPath, (err, stat) => {
|
|
57
|
+
if (err) rej(err);
|
|
58
|
+
else res(stat);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
30
61
|
|
|
31
|
-
|
|
32
|
-
const metadata = await image.metadata();
|
|
62
|
+
const optimizedSize = tmpStat.size;
|
|
33
63
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
withoutEnlargement: true
|
|
39
|
-
});
|
|
40
|
-
wasResized = true;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Convert to target format with optimization
|
|
44
|
-
if (outputFormat === 'jpg' || outputFormat === 'jpeg') {
|
|
45
|
-
image = image.jpeg({ quality: 80, progressive: true });
|
|
46
|
-
} else if (outputFormat === 'png') {
|
|
47
|
-
image = image.png({ compressionLevel: 9 });
|
|
48
|
-
} else if (outputFormat === 'webp') {
|
|
49
|
-
image = image.webp({ quality: 80 });
|
|
50
|
-
} else if (outputFormat === 'gif') {
|
|
51
|
-
image = image.gif();
|
|
52
|
-
} else if (outputFormat === 'tiff') {
|
|
53
|
-
image = image.tiff();
|
|
54
|
-
} else {
|
|
55
|
-
// Default to jpeg for unknown formats
|
|
56
|
-
image = image.jpeg({ quality: 80, progressive: true });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Write to temporary file
|
|
60
|
-
await image.toFile(tmpPath);
|
|
61
|
-
|
|
62
|
-
// Check if optimized file is smaller
|
|
63
|
-
const tmpStat = await fs.promises.stat(tmpPath);
|
|
64
|
-
const optimizedSize = tmpStat.size;
|
|
64
|
+
if (optimizedSize < originalSize) {
|
|
65
|
+
// Remove original and rename temp to final
|
|
66
|
+
await fs.promises.unlink(videoPath);
|
|
67
|
+
await fs.promises.rename(tmpPath, finalPath);
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
resolve({
|
|
70
|
+
success: true,
|
|
71
|
+
originalSize,
|
|
72
|
+
optimizedSize,
|
|
73
|
+
message: `✓ ${path.basename(videoPath)} → ${path.basename(finalPath)} (${(originalSize / 1024 / 1024).toFixed(1)}MB → ${(optimizedSize / 1024 / 1024).toFixed(1)}MB)`,
|
|
74
|
+
filePath: finalPath
|
|
75
|
+
});
|
|
76
|
+
} else {
|
|
77
|
+
// Remove temp file if not smaller
|
|
78
|
+
await fs.promises.unlink(tmpPath);
|
|
68
79
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
80
|
+
resolve({
|
|
81
|
+
success: false,
|
|
82
|
+
originalSize,
|
|
83
|
+
optimizedSize,
|
|
84
|
+
message: `○ ${path.basename(videoPath)} - Already optimized (${(originalSize / 1024 / 1024).toFixed(1)}MB)`,
|
|
85
|
+
filePath: videoPath
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
resolve({
|
|
90
|
+
success: false,
|
|
91
|
+
originalSize,
|
|
92
|
+
optimizedSize: 0,
|
|
93
|
+
message: `✗ ${path.basename(videoPath)} - Post-processing error: ${error.message}`,
|
|
94
|
+
error: true
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
.on('error', (err) => {
|
|
99
|
+
// Clean up temp file on error
|
|
100
|
+
fs.unlink(tmpPath, () => {
|
|
101
|
+
let errorMsg = err.message;
|
|
102
|
+
|
|
103
|
+
// Provide helpful suggestions for common errors
|
|
104
|
+
if (err.message.includes('Unknown encoder') || err.message.includes('Encoder (')) {
|
|
105
|
+
errorMsg = `Codec "${outputCodec}" not supported. Try: video_encode=h264`;
|
|
106
|
+
} else if (err.message.includes('No such file') || err.message.includes('does not exist')) {
|
|
107
|
+
errorMsg = `Input file not found or cannot be read`;
|
|
108
|
+
} else if (err.message.includes('ENOENT') || err.message.includes('ffmpeg')) {
|
|
109
|
+
errorMsg = `FFmpeg not found. Install with: brew install ffmpeg`;
|
|
110
|
+
} else if (err.message.includes('Conversion failed') || err.message.includes('exited with code')) {
|
|
111
|
+
errorMsg = `Video encoding failed. Check file format and try different codec`;
|
|
112
|
+
} else {
|
|
113
|
+
errorMsg = `Encoding error: ${err.message.substring(0, 80)}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
resolve({
|
|
117
|
+
success: false,
|
|
118
|
+
originalSize,
|
|
119
|
+
optimizedSize: 0,
|
|
120
|
+
message: `✗ ${path.basename(videoPath)} - ${errorMsg}`,
|
|
121
|
+
error: true
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
})
|
|
125
|
+
.run();
|
|
126
|
+
});
|
|
127
|
+
} catch (error) {
|
|
128
|
+
resolve({
|
|
93
129
|
success: false,
|
|
94
|
-
originalSize,
|
|
95
|
-
optimizedSize,
|
|
96
|
-
message:
|
|
97
|
-
|
|
98
|
-
};
|
|
130
|
+
originalSize: 0,
|
|
131
|
+
optimizedSize: 0,
|
|
132
|
+
message: `✗ ${path.basename(videoPath)} - Error: ${error.message}`,
|
|
133
|
+
error: true
|
|
134
|
+
});
|
|
99
135
|
}
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
success: false,
|
|
103
|
-
originalSize: 0,
|
|
104
|
-
optimizedSize: 0,
|
|
105
|
-
message: `✗ ${path.basename(imagePath)} - Error: ${error.message}`,
|
|
106
|
-
error: true
|
|
107
|
-
};
|
|
108
|
-
}
|
|
136
|
+
});
|
|
109
137
|
}
|
|
110
138
|
|
|
111
139
|
module.exports = {
|
|
112
|
-
|
|
140
|
+
processVideo
|
|
113
141
|
};
|