LaiTool/src/main/Service/ffmpegOptions.ts
lq1405 f4d042f699 V 3.1.7
1. 移除软件包自带的本地 whisper(需单独安装)
2. 重构版本底层依赖,移除外部依赖
3. 修复 首页 暗黑模式不兼容的问题
4. 修复 SD 合并提示词报错
2024-10-20 23:19:22 +08:00

373 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import path from 'path'
import { errorMessage, successMessage } from '../Public/generalTools'
import { CheckFileOrDirExist, CheckFolderExistsOrCreate, CopyFileOrFolder, DeleteFolderAllFile } from '../../define/Tools/file'
import { MillisecondsToTimeString } from '../../define/Tools/time'
import Ffmpeg from 'fluent-ffmpeg'
import { SetFfmpegPath } from '../setting/ffmpegSetting'
import fs from 'fs'
import { GeneralResponse } from '../../model/generalResponse'
const fspromises = fs.promises
SetFfmpegPath()
/**
* FFmpeg 封装的一些操作
*/
export class FfmpegOptions {
ecode: string // 编码的方式
dcode: string // 解码的方式
constructor() {
}
InitCodec() {
let videoCodec = 'libx264' // 默认编码器
if (global.gpu.type === 'NVIDIA') {
videoCodec = 'h264_nvenc'
} else if (global.gpu.type === 'AMD') {
videoCodec = 'h264_amf'
}
this.ecode = videoCodec
let videoDcodec = 'libx264' // 默认解码器
if (global.gpu.type === 'NVIDIA') {
videoDcodec = 'h264_cuvid '
} else if (global.gpu.type === 'AMD') {
videoDcodec = 'h264_amf'
}
this.dcode = videoDcodec
}
/**
* 压缩视频文件
* @param videoPath 文件地址
* @param wh 宽高比
* @param crf crf值
* @returns
*/
async FfmpegCompressVideo(videoPath: string, maxV: number, crf: string): Promise<string> {
// 判断视频地址是不是存在
let videoIsExist = await CheckFileOrDirExist(videoPath)
if (!videoIsExist) {
throw new Error('视频地址对应的文件不存在')
}
if (!videoPath.toLowerCase().endsWith('.mp4')) {
throw new Error("只支持MP4文件");
}
let tempVideo = path.join(path.dirname(videoPath), "tmp.mp4");
await CopyFileOrFolder(videoPath, tempVideo);
await fspromises.unlink(videoPath)
let wh = await this.FfmpegGetVideoSize(tempVideo)
if (wh.code != 1) {
throw new Error(wh.message)
}
let rate = undefined
let width = undefined
let height = undefined
// 计算尺寸
if (wh.data.width > wh.data.height) {
rate = maxV / wh.data.width
width = maxV
height = wh.data.height * rate
} else {
height = maxV
rate = maxV / wh.data.height
width = wh.data.width * rate
}
return new Promise((resolve, reject) => {
Ffmpeg(tempVideo)
.outputOptions([
`-vf scale=${Math.floor(width)}:${Math.floor(height)}`, // 调整视频尺寸到 1280x720
`-b:v ${crf}` // 设置视频比特率为 1000kbps
])
.on('end', async () => {
// 删除缓存文件
await fspromises.unlink(tempVideo)
resolve('视频压缩处理完成');
})
.on('error', (err) => {
reject('视频压缩处理失败: ' + err.toString());
})
.save(videoPath)
})
}
/**
* FFmpeg裁剪视频将一个视频将裁剪指定的时间内的片段
* @param {*} startTime 开始时间
* @param {*} endTime 结束时间
* @param {*} videoPath 视频地址
* @param {*} outVideoFile 输出地址
* @returns
*/
async FfmpegCutVideo(startTime, endTime, videoPath, outVideoFile) {
try {
// 判断视频地址是不是存在
let videoIsExist = await CheckFileOrDirExist(videoPath)
if (!videoIsExist) {
throw new Error('视频地址对应的文件不存在')
}
// 判断开始时间和结束时间是不是合法
if (startTime == null || endTime == null) {
throw new Error('开始时间和结束时间不能为空')
}
// 判断输出文件夹是不是存在
let outputFolder = path.dirname(outVideoFile)
await CheckFolderExistsOrCreate(outputFolder)
// 将时间转换为字符串
let startTimeString = MillisecondsToTimeString(startTime)
let endTimeString = MillisecondsToTimeString(endTime)
// 设置视频编码器
let videoCodec = 'libx264' // 默认编码器
if (global.gpu.type === 'NVIDIA') {
videoCodec = 'h264_nvenc'
} else if (global.gpu.type === 'AMD') {
videoCodec = 'h264_amf'
}
// 判断分镜是不是和数据库中的数据匹配的上
let res = await new Promise((resolve, reject) => {
Ffmpeg(videoPath)
.outputOptions([
`-ss ${startTimeString}`,
`-to ${endTimeString}`,
'-preset fast',
'-c:v ' + videoCodec,
'-c:a copy'
])
.output(outVideoFile)
.on('end', async function () {
resolve(outVideoFile)
})
.on('error', async function (err) {
reject(new Error(`视频裁剪失败,错误信息如下:${err.toString()}`))
})
.run()
})
let res_msg = `视频裁剪完成,输出地址:${outVideoFile}`
return successMessage(res_msg, '视频裁剪成功', 'BasicReverse_FfmpegCutVideo')
// 开始裁剪视频
} catch (error) {
return errorMessage(
'裁剪视频失败,错误信息如下: ' + error.message,
'BasicReverse_FfmpegCutVideo'
)
}
}
/**
* Ffmpeg提取音频
* @param {*} videoPath 视频地址
* @param {*} outAudioPath 输出音频地址
* @returns
*/
async FfmpegExtractAudio(videoPath: string, outAudioPath: string): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
try {
// 判断视频地址是不是存在
let videoIsExist = await CheckFileOrDirExist(videoPath)
if (!videoIsExist) {
throw new Error('视频地址对应的文件不存在')
}
// 开始提取音频
let res = await new Promise((resolve, reject) => {
Ffmpeg(videoPath)
.output(outAudioPath)
.audioCodec('libmp3lame')
.audioBitrate('128k')
.on('end', async function () {
resolve(outAudioPath)
})
.on('error', async function (err) {
let res_msg = `音频提取失败,错误信息如下:${err.toString()}`
reject(new Error(res_msg))
})
.run()
})
let res_msg = `音频提取完成,输出地址:${res}`
return successMessage(res, res_msg, 'BasicReverse_FfmpegExtractAudio')
} catch (error) {
return errorMessage(
'提取音频失败,错误信息如下: ' + error.message,
'BasicReverse_FfmpegExtractAudio'
)
}
}
/**
* Ffmpeg提取视频指定时间的帧只提取一帧
* @param {*} frameTime 视频的时间点
* @param {*} videoPath 视频地址
* @param {*} outFramePath 输出帧地址
*/
async FfmpegGetFrame(frameTime: number, videoPath: string, outFramePath: string): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
try {
let videoIsExist = await CheckFileOrDirExist(videoPath)
if (videoIsExist == false) {
throw new Error('视频地址对应的文件不存在')
}
// 判断输出文件夹是不是存在
let outputFolder = path.dirname(outFramePath)
await CheckFolderExistsOrCreate(outputFolder)
// 开始抽帧
// 判断分镜是不是和数据库中的数据匹配的上
let res = await new Promise((resolve, reject) => {
Ffmpeg(videoPath)
.inputOptions([`-ss ${MillisecondsToTimeString(Math.ceil(frameTime))}`])
.output(outFramePath)
.frames(1)
.on('end', async function () {
resolve(outFramePath)
})
.on('error', async function (err) {
reject(new Error(err.toString()))
})
.run()
})
let res_msg = `视频抽帧完成,输出地址:${res}`
return successMessage(res, res_msg, 'BasicReverse_FfmpegGetFrame')
} catch (error) {
return errorMessage(error.message, 'BasicReverse_FfmpegGetFrame')
}
}
/**
* 获取视频文件的宽高
* @param {*} videoPath 视频文件地址
* @returns
*/
async FfmpegGetVideoSize(videoPath: string): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
try {
let videoIsExist = await CheckFileOrDirExist(videoPath)
if (videoIsExist == false) {
throw new Error('视频地址对应的文件不存在')
}
let res = await new Promise((resolve, reject) => {
Ffmpeg.ffprobe(videoPath, function (err, metadata) {
if (err) {
reject(new Error(err.toString()))
}
const { width, height } = metadata.streams.find((s) => s.codec_type === 'video')
resolve({ width, height })
})
})
return successMessage(res, '获取视频的宽高成功', 'BasicReverse_GetVideoSize')
} catch (error) {
return errorMessage(
'获取视频的宽高失败,失败信息如下:' + error.message,
'BasicReverse_GetVideoSize'
)
}
}
/**
* 通过FFmpeg获取指定视频的时长
* @param {*} videoPath 视频地址
* @returns 返回视频的时长
*/
async FfmpegGetVideoDuration(videoPath) {
try {
let videoIsExist = await CheckFileOrDirExist(videoPath)
if (videoIsExist == false) {
throw new Error('视频地址对应的文件不存在')
}
let res = await new Promise((resolve, reject) => {
Ffmpeg.ffprobe(videoPath, function (err, metadata) {
if (err) {
reject(new Error(err.toString()))
}
const duration = metadata.format.duration
resolve(duration * 1000)
})
})
return successMessage(res, '获取视频的时长成功', 'BasicReverse_GetVideoDuration')
} catch (error) {
return errorMessage(
'获取视频的时长,失败信息如下:' + error.message,
'BasicReverse_GetVideoDuration'
)
}
}
/**
* 获取视频的指定时间点的一帧,然后再裁剪
* @param {*} videoPath
* @param {*} currentTime
* @param {*} outImagePath
* @param {*} clipRanges
*/
async FfmpegGetVideoFramdAndClip(videoPath, currentTime, outImagePath, clipRanges) {
try {
let videoIsExist = await CheckFileOrDirExist(videoPath)
if (videoIsExist == false) {
throw new Error('视频地址对应的文件不存在')
}
// 判断输出文件夹是不是存在
let outputFolder = path.dirname(outImagePath)
await CheckFolderExistsOrCreate(outputFolder)
let frameRes = await this.FfmpegGetFrame(currentTime, videoPath, outImagePath)
if (frameRes.code == 0) {
throw new Error(frameRes.message)
}
let outImagePaths = []
if (await CheckFileOrDirExist(outImagePath) == false) {
return successMessage(
outImagePaths,
'获取指定位置的帧和裁剪成功',
'WatermarkAndSubtitle_FfmpegGetVideoFramdAndClip'
)
}
// 这边可以会裁剪多个,所以需要循环
for (let i = 0; i < clipRanges.length; i++) {
const element = clipRanges[i]
let outCropImagePath = outImagePath.replace('.png', `_${i}.png`)
outImagePaths.push(outCropImagePath)
// 开始裁剪
let res = await new Promise((resolve, reject) => {
Ffmpeg(outImagePath)
.outputOptions([
`-vf crop=${element.width}:${element.height}:${element.startX}:${element.startY}`
])
.output(outCropImagePath)
.on('end', async function () {
resolve(outCropImagePath)
})
.on('error', async function (err) {
reject(new Error(err.toString()))
})
.run()
})
}
// 删除文件
await fspromises.unlink(outImagePath)
return successMessage(
outImagePaths,
'获取指定位置的帧和裁剪成功',
'WatermarkAndSubtitle_FfmpegGetVideoFramdAndClip'
)
} catch (error) {
return errorMessage(
'获取指定位置的帧失败和裁剪失败,失败信息如下:' + error.toString(),
'WatermarkAndSubtitle_FfmpegGetVideoFramdAndClip'
)
}
}
}