LaiTool/src/main/Service/ffmpegOptions.ts

373 lines
12 KiB
TypeScript
Raw Normal View History

2024-07-13 15:44:13 +08:00
import path from 'path'
2024-08-03 12:46:12 +08:00
import { errorMessage, successMessage } from '../Public/generalTools'
import { CheckFileOrDirExist, CheckFolderExistsOrCreate, CopyFileOrFolder, DeleteFolderAllFile } from '../../define/Tools/file'
2024-07-13 15:44:13 +08:00
import { MillisecondsToTimeString } from '../../define/Tools/time'
import Ffmpeg from 'fluent-ffmpeg'
import { SetFfmpegPath } from '../setting/ffmpegSetting'
import fs from 'fs'
2024-08-03 12:46:12 +08:00
import { GeneralResponse } from '../../model/generalResponse'
2024-07-13 15:44:13 +08:00
const fspromises = fs.promises
SetFfmpegPath()
/**
* FFmpeg
*/
export class FfmpegOptions {
2024-08-03 12:46:12 +08:00
ecode: string // 编码的方式
dcode: string // 解码的方式
2024-07-13 15:44:13 +08:00
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'
}
2024-08-03 12:46:12 +08:00
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([
2024-09-04 19:49:20 +08:00
`-vf scale=${Math.floor(width)}:${Math.floor(height)}`, // 调整视频尺寸到 1280x720
2024-08-03 12:46:12 +08:00
`-b:v ${crf}` // 设置视频比特率为 1000kbps
])
.on('end', async () => {
// 删除缓存文件
await fspromises.unlink(tempVideo)
resolve('视频压缩处理完成');
})
.on('error', (err) => {
reject('视频压缩处理失败: ' + err.toString());
})
.save(videoPath)
})
2024-07-13 15:44:13 +08:00
}
/**
* 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
*/
2024-08-18 16:22:19 +08:00
async FfmpegExtractAudio(videoPath: string, outAudioPath: string): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
2024-07-13 15:44:13 +08:00
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'
)
}
}
/**
2024-08-18 16:22:19 +08:00
* Ffmpeg提取视频指定时间的帧
2024-07-13 15:44:13 +08:00
* @param {*} frameTime
* @param {*} videoPath
* @param {*} outFramePath
*/
2024-08-18 16:22:19 +08:00
async FfmpegGetFrame(frameTime: number, videoPath: string, outFramePath: string): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
2024-07-13 15:44:13 +08:00
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))}`])
2024-07-13 15:44:13 +08:00
.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}`
2024-08-18 16:22:19 +08:00
return successMessage(res, res_msg, 'BasicReverse_FfmpegGetFrame')
2024-07-13 15:44:13 +08:00
} catch (error) {
return errorMessage(error.message, 'BasicReverse_FfmpegGetFrame')
}
}
/**
*
* @param {*} videoPath
* @returns
*/
2024-08-03 12:46:12 +08:00
async FfmpegGetVideoSize(videoPath: string): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
2024-07-13 15:44:13 +08:00
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 = []
2024-08-18 16:22:19 +08:00
if (await CheckFileOrDirExist(outImagePath) == false) {
2024-08-03 12:46:12 +08:00
return successMessage(
outImagePaths,
'获取指定位置的帧和裁剪成功',
'WatermarkAndSubtitle_FfmpegGetVideoFramdAndClip'
)
}
2024-07-13 15:44:13 +08:00
// 这边可以会裁剪多个,所以需要循环
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'
)
}
}
}