forked from xiangsx/gpt4free-ts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
file.ts
329 lines (300 loc) · 9.14 KB
/
file.ts
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
import * as fs from 'fs';
import { PathOrFileDescriptor, WriteFileOptions } from 'fs';
import ffmpeg from 'fluent-ffmpeg';
import ffmpegInstaller from '@ffmpeg-installer/ffmpeg';
import ffprobeInstaller from '@ffprobe-installer/ffprobe';
import path from 'path';
import { CreateNewAxios } from './proxyAgent';
import {
downloadAndUploadCDN,
downloadFile,
replaceLocalUrl,
uploadFile,
} from './index';
import { v4 } from 'uuid';
import { exec } from 'node:child_process';
class SyncFileDebouncer {
private static instance: SyncFileDebouncer;
private readonly debounceTime: number;
private timers: { [path: string]: NodeJS.Timeout } = {};
private constructor(debounceTime: number = 300) {
this.debounceTime = debounceTime;
}
public static getInstance(debounceTime?: number): SyncFileDebouncer {
if (!SyncFileDebouncer.instance) {
SyncFileDebouncer.instance = new SyncFileDebouncer(debounceTime);
}
return SyncFileDebouncer.instance;
}
public writeFileSync(
file: string,
data: string | NodeJS.ArrayBufferView,
options?: WriteFileOptions,
) {
if (this.timers[file]) {
clearTimeout(this.timers[file]);
}
this.timers[file] = setTimeout(() => {
fs.writeFileSync(file, data);
delete this.timers[file];
}, this.debounceTime);
}
}
// 使用示例
export const fileDebouncer = SyncFileDebouncer.getInstance(500); // 100ms 的 debounce 时间
export function getImageExtension(contentType: string): string {
switch (contentType) {
case 'image/jpeg':
return 'jpeg';
case 'image/jpg':
return 'jpg';
case 'image/png':
return 'png';
case 'image/gif':
return 'gif';
case 'image/bmp':
return 'bmp';
case 'image/webp':
return 'webp';
// Add other cases as needed
default:
throw new Error(`Unsupported content type: ${contentType}`);
}
}
export function IsImageMineType(mimeType: string): boolean {
return (
mimeType === 'image/jpeg' ||
mimeType === 'image/jpg' ||
mimeType === 'image/png' ||
mimeType === 'image/gif' ||
mimeType === 'image/bmp' ||
mimeType === 'image/webp'
);
}
ffmpeg.setFfmpegPath(ffmpegInstaller.path);
ffmpeg.setFfprobePath(ffprobeInstaller.path);
/**
* 提取视频的最后一帧并保存为图像文件
* @param videoUrl 视频链接
* @param outputImagePath 输出图像文件路径
*/
export async function extractVideoLastFrame(videoUrl: string): Promise<string> {
const outputImagePath = `run/${v4()}.png`;
const videoPath = `run/${v4()}.mp4`;
// 下载视频到本地临时文件
const localURL = await downloadAndUploadCDN(videoUrl);
const url = replaceLocalUrl(localURL);
const response = await CreateNewAxios({}, { proxy: false }).get(url, {
responseType: 'stream',
});
const writer = fs.createWriteStream(videoPath);
response.data.pipe(writer);
await new Promise<void>((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
});
// 使用 ffmpeg 提取最后一帧
await new Promise<void>((resolve, reject) => {
ffmpeg(videoPath)
.inputOptions('-sseof', '-1')
.outputOptions('-vframes', '1')
.output(outputImagePath)
.on('end', () => {
console.log(`提取最后一帧完成:${outputImagePath}`);
resolve();
})
.on('error', (err) => {
console.error('提取最后一帧时出错:', err);
reject('extract last frame failed');
})
.run();
});
// 删除临时视频文件
fs.unlinkSync(videoPath);
const imageURL = await uploadFile(outputImagePath);
fs.unlinkSync(outputImagePath);
return imageURL;
}
/**
* 获取视频的时长和帧率
* @param videoPath 视频文件路径
*/
async function getVideoInfo(videoPath: string): Promise<{
duration: number;
frameRate: number;
codec: string;
width: number;
height: number;
}> {
return new Promise((resolve, reject) => {
ffmpeg.ffprobe(videoPath, (err, metadata) => {
if (err) {
return reject('get video info failed');
}
const duration = metadata.format.duration!;
const frameRate = eval(metadata.streams[0].r_frame_rate || '') || 0; // 计算帧率
const codec = metadata.streams[0].codec_name!;
const width = metadata.streams[0].width!;
const height = metadata.streams[0].height!;
resolve({ duration, frameRate, codec, width, height });
});
});
}
/**
* 转换第一个视频为第二个视频的格式
* @param inputPath 输入视频文件路径
* @param outputPath 输出视频文件路径
* @param codec 编码格式
* @param width 视频宽度
* @param height 视频高度
* @param frameRate 视频帧率
*/
async function convertVideoFormat(
inputPath: string,
codec: string,
width: number,
height: number,
frameRate: number,
): Promise<void> {
const tempOutputPath = `run/${v4()}.mp4`;
// 检测格式是不是已经是目标格式
const {
codec: inputCodec,
width: inputWidth,
height: inputHeight,
} = await getVideoInfo(inputPath);
if (inputCodec === codec && inputWidth === width && inputHeight === height) {
return;
}
await new Promise<void>((resolve, reject) => {
ffmpeg(inputPath)
.size(`${width}x${height}`)
.fps(frameRate)
.aspect(`${width}:${height}`)
.autopad(true, 'black')
.output(tempOutputPath)
.noAudio()
.on('end', () => {
fs.renameSync(tempOutputPath, inputPath); // 用转换后的文件覆盖原始文件
console.log(`转换视频格式完成:${inputPath}`);
resolve();
})
.on('error', (err) => {
console.error(`转换视频格式时出错: ${err.message}`);
reject('video format convert failed');
})
.run();
});
}
/**
* 合并两个视频并剔除第一个视频的最后一帧
* @param videoPath1 第一个视频文件路径
* @param videoPath2 第二个视频文件路径
* @param outputVideoPath 输出合并视频文件路径
*/
export async function mergeVideosExcludeLastFrame(
video_url1: string,
video_url2: string,
): Promise<string> {
const [video1, video2] = await Promise.all([
downloadFile(video_url1),
downloadFile(video_url2),
]);
const outputVideoPath = `run/file/${v4()}.mp4`;
const { duration, codec, width, height, frameRate } = await getVideoInfo(
video2.outputFilePath,
);
await convertVideoFormat(
video1.outputFilePath,
codec,
width,
height,
frameRate,
);
// 合并两个视频
await new Promise<void>((resolve, reject) => {
ffmpeg()
.input(video1.outputFilePath)
.input(video2.outputFilePath)
.on('end', () => {
console.log(`视频合并完成:${outputVideoPath}`);
resolve();
})
.on('error', (err) => {
console.error('合并视频时出错:', err);
reject('video merge failed');
})
.mergeToFile(outputVideoPath, path.join(__dirname, 'temp'));
});
// 删除临时视频文件
const videoURL = await uploadFile(outputVideoPath);
fs.unlinkSync(outputVideoPath);
return videoURL;
}
export async function removeWatermarkFromVideo(
video_url: string,
watermarkX: number,
watermarkY: number,
watermarkWidth: number,
watermarkHeight: number,
): Promise<string> {
const video = await downloadFile(video_url);
const outputVideoPath = `run/file/${v4()}.mp4`;
// 获取视频信息
const { codec, width, height, frameRate } = await getVideoInfo(
video.outputFilePath,
);
// 确保水印坐标和尺寸在视频范围内
const safeWatermarkX = Math.max(
0,
Math.min(watermarkX, width - watermarkWidth),
);
const safeWatermarkY = Math.max(
0,
Math.min(watermarkY, height - watermarkHeight),
);
const safeWatermarkWidth = Math.min(watermarkWidth, width - safeWatermarkX);
const safeWatermarkHeight = Math.min(
watermarkHeight,
height - safeWatermarkY,
);
// 构建 FFmpeg 命令
const ffmpegCommand = `${ffmpegInstaller.path} -i "${video.outputFilePath}" -vf "delogo=x=${safeWatermarkX}:y=${safeWatermarkY}:w=${safeWatermarkWidth}:h=${safeWatermarkHeight}:show=0" -c:v libx264 -preset fast -crf 22 -c:a copy "${outputVideoPath}"`;
// 运行 FFmpeg 命令
await new Promise<void>((resolve, reject) => {
exec(ffmpegCommand, (error, stdout, stderr) => {
if (error) {
console.error('Error during watermark removal:', stderr);
reject(error);
} else {
console.log('FFmpeg process completed:', stdout);
resolve();
}
});
});
// 上传处理后的视频并删除本地文件
const videoURL = await uploadFile(outputVideoPath);
fs.unlinkSync(outputVideoPath);
return videoURL;
}
/**
* 获取音频文件的时长
* @param filePath 音频文件的路径
* @returns 一个 Promise,返回音频的时长(秒)
*/
export async function getAudioDuration(filePath: string): Promise<number> {
return new Promise((resolve, reject) => {
ffmpeg.ffprobe(filePath, (err, metadata) => {
if (err) {
reject(err);
} else {
const duration = metadata.format.duration;
if (!duration) {
reject('get audio duration failed');
return;
}
resolve(duration);
}
});
});
}