308 lines
9.8 KiB
JavaScript
308 lines
9.8 KiB
JavaScript
|
|
import path from 'path'
|
|||
|
|
import { TaskScheduler } from './taskScheduler'
|
|||
|
|
import { errorMessage, successMessage } from '../generalTools'
|
|||
|
|
import { CheckFileOrDirExist, CheckFolderExistsOrCreate } from '../../define/Tools/file'
|
|||
|
|
import { MillisecondsToTimeString } from '../../define/Tools/time'
|
|||
|
|
import Ffmpeg from 'fluent-ffmpeg'
|
|||
|
|
import { SetFfmpegPath } from '../setting/ffmpegSetting'
|
|||
|
|
import fs from 'fs'
|
|||
|
|
const fspromises = fs.promises
|
|||
|
|
|
|||
|
|
SetFfmpegPath()
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* FFmpeg 封装的一些操作
|
|||
|
|
*/
|
|||
|
|
export class FfmpegOptions {
|
|||
|
|
constructor() {
|
|||
|
|
this.taskScheduler = new TaskScheduler()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* FFmpeg裁剪视频,将一个视频将裁剪指定的时间内的片段
|
|||
|
|
* @param {*} book 小说对象类
|
|||
|
|
* @param {*} bookTask 小说批次任务对象类
|
|||
|
|
* @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, outAudioPath) {
|
|||
|
|
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提取视频帧(只提取一帧)
|
|||
|
|
* 根据point判断提取什么位置的帧
|
|||
|
|
* @param {*} frameTime 视频的时间点
|
|||
|
|
* @param {*} videoPath 视频地址
|
|||
|
|
* @param {*} outFramePath 输出帧地址
|
|||
|
|
*/
|
|||
|
|
async FfmpegGetFrame(frameTime, videoPath, outFramePath) {
|
|||
|
|
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(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, '视频抽帧成功', 'BasicReverse_FfmpegGetFrame')
|
|||
|
|
} catch (error) {
|
|||
|
|
return errorMessage(error.message, 'BasicReverse_FfmpegGetFrame')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取视频文件的宽高
|
|||
|
|
* @param {*} videoPath 视频文件地址
|
|||
|
|
* @returns
|
|||
|
|
*/
|
|||
|
|
async FfmpegGetVideoSize(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 { 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 = []
|
|||
|
|
// 这边可以会裁剪多个,所以需要循环
|
|||
|
|
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'
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|