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)
|
2024-10-20 23:19:22 +08:00
|
|
|
|
.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'
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|