image-video-optimizer 3.0.0 → 3.0.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/bin/cli.js +1 -1
- package/package.json +1 -1
- package/src/videoProcessor.js +116 -110
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('
|
|
10
|
+
.version('3.0.1')
|
|
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,134 +1,140 @@
|
|
|
1
1
|
const ffmpeg = require('fluent-ffmpeg');
|
|
2
|
+
const fs = require('fs');
|
|
2
3
|
const path = require('path');
|
|
3
|
-
const fs = require('fs').promises;
|
|
4
|
-
|
|
5
|
-
// Set ffmpeg and ffprobe paths if using static binaries
|
|
6
|
-
try {
|
|
7
|
-
const ffmpegStatic = require('ffmpeg-static');
|
|
8
|
-
const ffprobeStatic = require('ffprobe-static');
|
|
9
|
-
ffmpeg.setFfmpegPath(ffmpegStatic);
|
|
10
|
-
ffmpeg.setFfprobePath(ffprobeStatic.path);
|
|
11
|
-
} catch (error) {
|
|
12
|
-
// Fallback to system ffmpeg/ffprobe if static binaries not available
|
|
13
|
-
console.warn('Note: ffmpeg-static not found, using system ffmpeg/ffprobe');
|
|
14
|
-
}
|
|
15
4
|
|
|
16
5
|
/**
|
|
17
|
-
*
|
|
18
|
-
* @param {string} videoPath - Path to video
|
|
19
|
-
* @
|
|
6
|
+
* Process a single video file
|
|
7
|
+
* @param {string} videoPath - Path to the video
|
|
8
|
+
* @param {object} config - Configuration object
|
|
9
|
+
* @returns {Promise<{success: boolean, originalSize: number, optimizedSize: number, message: string}>}
|
|
20
10
|
*/
|
|
21
|
-
function
|
|
22
|
-
return new Promise((resolve
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
duration: metadata.format.duration,
|
|
33
|
-
codec: videoStream.codec_name,
|
|
34
|
-
bitrate: metadata.format.bit_rate
|
|
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
|
|
35
22
|
});
|
|
36
|
-
} else {
|
|
37
|
-
reject(new Error('No video stream found in file'));
|
|
38
23
|
}
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Process (resize and encode) a video
|
|
46
|
-
* @param {string} inputPath - Path to input video
|
|
47
|
-
* @param {string} outputPath - Path to output video
|
|
48
|
-
* @param {Object} config - Configuration object
|
|
49
|
-
* @returns {Promise<Object>} Processing result
|
|
50
|
-
*/
|
|
51
|
-
function processVideo(inputPath, outputPath, config) {
|
|
52
|
-
return new Promise(async (resolve) => {
|
|
53
|
-
try {
|
|
54
|
-
const metadata = await getVideoMetadata(inputPath);
|
|
55
|
-
const currentWidth = metadata.width;
|
|
56
|
-
const currentHeight = metadata.height;
|
|
57
|
-
const maxWidth = parseInt(config.video_max_width) || 720;
|
|
58
|
-
const videoEncode = config.video_encode || 'h264';
|
|
59
24
|
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
.
|
|
63
|
-
.
|
|
64
|
-
.
|
|
65
|
-
.
|
|
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');
|
|
66
31
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
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
|
+
};
|
|
71
40
|
|
|
72
|
-
|
|
73
|
-
const finalHeight = newHeight % 2 === 0 ? newHeight : newHeight - 1;
|
|
74
|
-
const finalWidth = maxWidth % 2 === 0 ? maxWidth : maxWidth - 1;
|
|
41
|
+
const outputCodec = codecMap[config.video_encode.toLowerCase()] || config.video_encode || 'libx264';
|
|
75
42
|
|
|
76
|
-
command =
|
|
77
|
-
|
|
43
|
+
const command = ffmpeg(videoPath)
|
|
44
|
+
.videoCodec(outputCodec)
|
|
45
|
+
.size(`${config.video_max_width}x?`)
|
|
46
|
+
.outputOptions([
|
|
47
|
+
'-preset fast',
|
|
48
|
+
'-crf 28',
|
|
49
|
+
'-c:a aac' // Use AAC for audio (widely compatible)
|
|
50
|
+
])
|
|
51
|
+
.output(tmpPath);
|
|
78
52
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
53
|
+
command
|
|
54
|
+
.on('end', async () => {
|
|
55
|
+
try {
|
|
56
|
+
const tmpStat = await new Promise((res, rej) => {
|
|
57
|
+
fs.stat(tmpPath, (err, stat) => {
|
|
58
|
+
if (err) rej(err);
|
|
59
|
+
else res(stat);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
82
62
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
success: true,
|
|
90
|
-
inputPath,
|
|
91
|
-
outputPath,
|
|
92
|
-
originalWidth: currentWidth,
|
|
93
|
-
originalHeight: currentHeight,
|
|
94
|
-
resized: currentWidth > maxWidth,
|
|
95
|
-
duration: metadata.duration
|
|
96
|
-
});
|
|
97
|
-
})
|
|
98
|
-
.on('error', (err) => {
|
|
99
|
-
resolve({
|
|
100
|
-
success: false,
|
|
101
|
-
inputPath,
|
|
102
|
-
error: err.message
|
|
103
|
-
});
|
|
104
|
-
})
|
|
105
|
-
.output(outputPath)
|
|
106
|
-
.run();
|
|
63
|
+
const optimizedSize = tmpStat.size;
|
|
64
|
+
|
|
65
|
+
if (optimizedSize < originalSize) {
|
|
66
|
+
// Remove original and rename temp to final
|
|
67
|
+
await fs.promises.unlink(videoPath);
|
|
68
|
+
await fs.promises.rename(tmpPath, finalPath);
|
|
107
69
|
|
|
70
|
+
resolve({
|
|
71
|
+
success: true,
|
|
72
|
+
originalSize,
|
|
73
|
+
optimizedSize,
|
|
74
|
+
message: `✓ ${path.basename(videoPath)} → ${path.basename(finalPath)} (${(originalSize / 1024 / 1024).toFixed(1)}MB → ${(optimizedSize / 1024 / 1024).toFixed(1)}MB)`,
|
|
75
|
+
filePath: finalPath
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
// Remove temp file if not smaller
|
|
79
|
+
await fs.promises.unlink(tmpPath);
|
|
80
|
+
|
|
81
|
+
resolve({
|
|
82
|
+
success: false,
|
|
83
|
+
originalSize,
|
|
84
|
+
optimizedSize,
|
|
85
|
+
message: `○ ${path.basename(videoPath)} - Already optimized (${(originalSize / 1024 / 1024).toFixed(1)}MB)`,
|
|
86
|
+
filePath: videoPath
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
resolve({
|
|
91
|
+
success: false,
|
|
92
|
+
originalSize,
|
|
93
|
+
optimizedSize: 0,
|
|
94
|
+
message: `✗ ${path.basename(videoPath)} - Post-processing error: ${error.message}`,
|
|
95
|
+
error: true
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
.on('error', (err) => {
|
|
100
|
+
// Clean up temp file on error
|
|
101
|
+
fs.unlink(tmpPath, () => {
|
|
102
|
+
let errorMsg = err.message;
|
|
103
|
+
|
|
104
|
+
// Provide helpful suggestions for common errors
|
|
105
|
+
if (err.message.includes('Unknown encoder') || err.message.includes('Encoder')) {
|
|
106
|
+
errorMsg = `Video codec error. Check available codecs with: ffmpeg -encoders | grep lib`;
|
|
107
|
+
} else if (err.message.includes('is not available')) {
|
|
108
|
+
errorMsg = `Codec not available. Verify FFmpeg installation: ffmpeg -version`;
|
|
109
|
+
} else if (err.message.includes('ENOENT') || err.message.includes('ffmpeg')) {
|
|
110
|
+
errorMsg = `FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt-get install ffmpeg (Linux)`;
|
|
111
|
+
} else if (err.message.includes('Conversion failed') || err.message.includes('exited with code')) {
|
|
112
|
+
errorMsg = `Video encoding failed. Try a different codec in config: video_encode=h265 or video_encode=vp8`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
resolve({
|
|
116
|
+
success: false,
|
|
117
|
+
originalSize,
|
|
118
|
+
optimizedSize: 0,
|
|
119
|
+
message: `✗ ${path.basename(videoPath)} - ${errorMsg}`,
|
|
120
|
+
error: true
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
})
|
|
124
|
+
.run();
|
|
125
|
+
});
|
|
108
126
|
} catch (error) {
|
|
109
127
|
resolve({
|
|
110
128
|
success: false,
|
|
111
|
-
|
|
112
|
-
|
|
129
|
+
originalSize: 0,
|
|
130
|
+
optimizedSize: 0,
|
|
131
|
+
message: `✗ ${path.basename(videoPath)} - Error: ${error.message}`,
|
|
132
|
+
error: true
|
|
113
133
|
});
|
|
114
134
|
}
|
|
115
135
|
});
|
|
116
136
|
}
|
|
117
137
|
|
|
118
|
-
/**
|
|
119
|
-
* Check if ffmpeg is available on the system
|
|
120
|
-
* @returns {Promise<boolean>} True if ffmpeg is available
|
|
121
|
-
*/
|
|
122
|
-
function isFfmpegAvailable() {
|
|
123
|
-
return new Promise((resolve) => {
|
|
124
|
-
ffmpeg.getAvailableFormats((err) => {
|
|
125
|
-
resolve(!err);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
138
|
module.exports = {
|
|
131
|
-
processVideo
|
|
132
|
-
getVideoMetadata,
|
|
133
|
-
isFfmpegAvailable
|
|
139
|
+
processVideo
|
|
134
140
|
};
|