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 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('1.0.0')
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-video-optimizer",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "CLI tool to optimize and compress images and videos with configurable settings",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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
- * Get video metadata (width, height, duration, etc.)
18
- * @param {string} videoPath - Path to video file
19
- * @returns {Promise<Object>} Video metadata
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 getVideoMetadata(videoPath) {
22
- return new Promise((resolve, reject) => {
23
- ffmpeg.ffprobe(videoPath, (err, metadata) => {
24
- if (err) {
25
- reject(new Error(`Could not read video metadata: ${err.message}`));
26
- } else {
27
- const videoStream = metadata.streams.find(s => s.codec_type === 'video');
28
- if (videoStream) {
29
- resolve({
30
- width: videoStream.width,
31
- height: videoStream.height,
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
- let command = ffmpeg(inputPath)
61
- .videoCodec(videoEncode)
62
- .audioCodec('aac')
63
- .audioChannels(2)
64
- .audioFrequency(48000)
65
- .format('mp4');
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
- // Only resize if video is wider than max_width
68
- if (currentWidth > maxWidth) {
69
- const scaleFactor = maxWidth / currentWidth;
70
- const newHeight = Math.round(currentHeight * scaleFactor);
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
- // Ensure dimensions are even (required for most codecs)
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 = command.size(`${finalWidth}x${finalHeight}`);
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
- // Set bitrate based on resolution to maintain quality while reducing size
80
- const bitrate = currentWidth > maxWidth ? '2000k' : '3000k';
81
- command = command.videoBitrate(bitrate);
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
- command
84
- .on('start', (cmdline) => {
85
- // Processing started
86
- })
87
- .on('end', () => {
88
- resolve({
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
- inputPath,
112
- error: error.message
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
  };