-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathffmpeg-utils.cjs
More file actions
127 lines (112 loc) · 4.31 KB
/
ffmpeg-utils.cjs
File metadata and controls
127 lines (112 loc) · 4.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// ffmpeg-utils.cjs - FFmpeg関連ユーティリティ
const path = require('path');
let ffmpeg;
/**
* FFmpegモジュールを初期化する
* ffmpeg-staticのパスを設定(asar環境での補正含む)
*/
function initialize() {
ffmpeg = require('fluent-ffmpeg');
const ffmpegStatic = require('ffmpeg-static');
if (ffmpegStatic) {
const ffmpegPath = ffmpegStatic.replace('app.asar', 'app.asar.unpacked');
ffmpeg.setFfmpegPath(ffmpegPath);
}
}
/**
* フレームレート文字列を安全にパースする
* 例: "30000/1001" → "29.97", "30" → "30.00"
* @param {string} rFrameRate - ffprobeから取得したr_frame_rate
* @returns {string} 小数点2桁のFPS文字列
*/
function parseFrameRate(rFrameRate) {
if (!rFrameRate) return '0.00';
const parts = rFrameRate.split('/');
if (parts.length === 2) {
const numerator = parseFloat(parts[0]);
const denominator = parseFloat(parts[1]);
if (denominator !== 0 && !isNaN(numerator) && !isNaN(denominator)) {
return (numerator / denominator).toFixed(2);
}
}
const num = parseFloat(rFrameRate);
return isNaN(num) ? '0.00' : num.toFixed(2);
}
/**
* 動画を軽量解析してサムネイルを生成する
* @param {string} videoPath - 動画ファイルパス
* @param {string} thumbnailDir - サムネイル保存ディレクトリ
* @returns {Promise<Object>} 動画メタデータ
*/
function analyzeLightweight(videoPath, thumbnailDir) {
return new Promise((resolve, reject) => {
if (!ffmpeg) {
return reject(new Error('FFmpegが初期化されていません'));
}
const fileId = Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
const thumbPath = path.join(thumbnailDir, `${fileId}.jpg`);
ffmpeg(videoPath).ffprobe((err, metadata) => {
if (err) {
console.error('FFprobeエラー:', err);
return reject(err);
}
const videoStream = metadata.streams.find(s => s.codec_type === 'video');
if (!videoStream) {
return reject(new Error('動画ストリームが見つかりません'));
}
// サムネイル生成
ffmpeg(videoPath)
.inputOptions('-threads 1')
.screenshots({
timestamps: [1],
filename: path.basename(thumbPath),
folder: thumbnailDir,
size: '320x?'
})
.on('end', () => {
resolve({
path: videoPath,
name: path.basename(videoPath),
thumbnail: thumbPath,
preview: '',
codec: videoStream.codec_name || 'unknown',
width: videoStream.width || 0,
height: videoStream.height || 0,
fps: parseFrameRate(videoStream.r_frame_rate),
tags: ''
});
})
.on('error', (err) => {
console.error('サムネイル生成エラー:', err);
reject(err);
});
});
});
}
/**
* プレビュー動画を生成する
* @param {string} videoPath - 元動画ファイルパス
* @param {string} previewDir - プレビュー保存ディレクトリ
* @returns {Promise<string>} 生成されたプレビューファイルパス
*/
function generatePreview(videoPath, previewDir) {
if (!ffmpeg) {
return Promise.reject(new Error('FFmpegが初期化されていません'));
}
const previewFilePath = path.join(previewDir, `temp_${Date.now().toString(36)}.mp4`);
return new Promise((resolve, reject) => {
ffmpeg(videoPath)
.inputOptions('-threads 1')
.setStartTime(1)
.setDuration(3)
.size('480x?')
.noAudio()
.videoCodec('libx264')
.outputOptions(['-preset ultrafast', '-crf 28'])
.output(previewFilePath)
.on('end', () => resolve(previewFilePath))
.on('error', reject)
.run();
});
}
module.exports = { initialize, parseFrameRate, analyzeLightweight, generatePreview };