1. 移除软件包自带的本地 whisper(需单独安装) 2. 重构版本底层依赖,移除外部依赖 3. 修复 首页 暗黑模式不兼容的问题 4. 修复 SD 合并提示词报错
373 lines
12 KiB
TypeScript
373 lines
12 KiB
TypeScript
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'
|
||
)
|
||
}
|
||
}
|
||
}
|