音视频在线处理
使用ffmpeg.wasm实现在浏览器运行ffmpeg,可以处理音视频的剪切,拼接,转换等各种操作
tip:首次加载需要加载ffmpeg.wasm文件,预览网站部署的服务器带宽较低,需要等待一段时间
关键知识点
- ffmpeg.wasm 在浏览器中的引用
- 部署之后 SharedArrayBuffer is not defined的解决
- ffmpeg的命令执行
关键代码
引用的umd的插件,因此需要自己声明,且没有代码提示
好处是可以直接完整迁移到别的框架下面
declare const FFmpeg: any
const { createFFmpeg, fetchFile } = FFmpeg
class CustomVideoHandle {
private ffmpeg: any
constructor() {}
loadFfmpeg() {
// 依赖ffmpeg.wasm WebAssembly
if (!this.ffmpeg) {
this.ffmpeg = createFFmpeg({
corePath: '/libs/ffmpeg-core.js',
log: true
})
}
let promise = this.ffmpeg.isLoaded() ? Promise.resolve() : this.ffmpeg.load()
return promise
}
getVideoVoice({ videoUrl = '' as string, blobUrl = true as boolean } = {}) {
return this.loadFfmpeg()
.then(() => {
return fetchFile(videoUrl)
})
.then((res: any) => {
this.ffmpeg.FS('writeFile', 'input.mp4', res)
return this.ffmpeg.run('-i', 'input.mp4', '-acodec', 'libmp3lame', '-f', 'mp3', 'outputAudio.mp3')
})
.then(() => {
return this.ffmpeg.FS('readFile', 'outputAudio.mp3')
})
.then((res: any) => {
this.ffmpeg.FS('unlink', 'input.mp4')
this.ffmpeg.FS('unlink', 'outputAudio.mp3')
let blob = new Blob([res.buffer], { type: 'audio/mp3' })
return blobUrl ? URL.createObjectURL(blob) : blob
})
.catch((err: Error) => {
console.log(err)
throw err
})
}
addVideoVoice({ videoUrl = '' as string, voiceUrl = '' as string, blobUrl = true as boolean }) {
return this.loadFfmpeg()
.then(() => {
return Promise.all([fetchFile(videoUrl), fetchFile(voiceUrl)])
})
.then((res: any) => {
let [videoData, voiceData] = res
return Promise.all([this.ffmpeg.FS('writeFile', 'input.mp4', videoData), this.ffmpeg.FS('writeFile', 'input.mp3', voiceData)])
})
.then(() => {
return this.ffmpeg.run(
'-i',
'input.mp4',
'-i',
'input.mp3',
'-c:v',
'copy',
'-c:a',
'aac',
'-strict',
'experimental',
'-map',
'0:v:0',
'-map',
'1:a:0',
'output.mp4'
)
})
.then(() => {
return this.ffmpeg.FS('readFile', 'output.mp4')
})
.then((mp4Data: any) => {
this.ffmpeg.FS('unlink', 'input.mp4')
this.ffmpeg.FS('unlink', 'input.mp3')
this.ffmpeg.FS('unlink', 'output.mp4')
let blob = new Blob([mp4Data.buffer], { type: 'video/mp4' })
return blobUrl ? URL.createObjectURL(blob) : blob
})
.catch((err: Error) => {
throw err
})
}
reverseVideoAndAudio({ videoUrl = '' as string, blobUrl = true as boolean }): Promise<string | Blob> {
console.log('反转视频')
return this.loadFfmpeg()
.then(() => {
return fetchFile(videoUrl)
})
.then((videoData: any) => {
return this.ffmpeg.FS('writeFile', 'input.mp4', videoData)
})
.then(() => {
const command: string[] = ['-i', 'input.mp4', '-vf', 'reverse', '-af', 'areverse', 'output.mp4']
return this.ffmpeg.run(...command)
})
.then(() => {
return this.ffmpeg.FS('readFile', 'output.mp4')
})
.then((mp4Data: any) => {
this.ffmpeg.FS('unlink', 'input.mp4')
this.ffmpeg.FS('unlink', 'output.mp4')
let blob = new Blob([mp4Data.buffer], { type: 'video/mp4' })
return blobUrl ? URL.createObjectURL(blob) : blob
})
.catch((err: Error) => {
throw err
})
}
async composeAudioAndImages({ voiceUrls = [], imageUrls = [], imageType = 'png', blobUrl = true }) {
if (voiceUrls.length !== imageUrls.length) {
throw new Error('The number of audio files must match the number of image files.')
}
let index = 0
let videoList = []
for (let voiceUrl of voiceUrls) {
videoList.push(await this.voiceAddImage({ voiceUrl, imageUrl: imageUrls[index], imageType }))
index++
}
try {
const videoDataList = await Promise.all(videoList.map((i: any) => fetchFile(i)))
const writePromises = videoDataList.map((data: any, index: number) => {
return this.ffmpeg.FS('writeFile', `input${index}.mp4`, data)
})
await Promise.all(writePromises)
const inputArgs = videoList.map((_, index) => ['-i', `input${index}.mp4`]).flat()
const command = [
...inputArgs,
'-filter_complex',
`concat=n=${videoList.length}:v=1:a=1 [v] [a]`,
'-map',
'[v]',
'-map',
'[a]',
'-c:v',
'libx264',
'-c:a',
'aac',
'output.mp4'
]
await this.ffmpeg.run(...command)
const mp4Data = await this.ffmpeg.FS('readFile', 'output.mp4')
videoDataList.forEach((_, index) => {
this.ffmpeg.FS('unlink', `input${index}.mp4`)
})
this.ffmpeg.FS('unlink', 'output.mp4')
const blob = new Blob([mp4Data.buffer], { type: 'video/mp4' })
return blobUrl ? URL.createObjectURL(blob) : blob
} catch (err) {
throw err
}
}
voiceAddImage({ voiceUrl = '', imageUrl = '', imageType = 'png', blobUrl = true }): Promise<string | Blob> {
return this.loadFfmpeg()
.then(() => {
return Promise.all([fetchFile(voiceUrl), fetchFile(imageUrl)])
})
.then((res: any) => {
let [imageData, voiceData] = res
return Promise.all([
this.ffmpeg.FS('writeFile', `image.${imageType}`, imageData),
this.ffmpeg.FS('writeFile', 'input.mp3', voiceData)
])
})
.then((res: any) => {
const command = [
'-i',
`image.${imageType}`,
'-i',
'input.mp3',
'-c:v',
'libx264',
'-tune',
'stillimage',
'-c:a',
'aac',
'-strict',
'experimental',
'-pix_fmt',
'yuv420p',
'-q:v',
'1',
'output.mp4'
]
return this.ffmpeg.run(...command)
})
.then(() => {
return this.ffmpeg.FS('readFile', 'output.mp4')
})
.then((mp4Data: any) => {
this.ffmpeg.FS('unlink', `image.${imageType}`)
this.ffmpeg.FS('unlink', 'input.mp3')
this.ffmpeg.FS('unlink', 'output.mp4')
let blob = new Blob([mp4Data.buffer], { type: 'video/mp4' })
return blobUrl ? URL.createObjectURL(blob) : blob
})
.catch((err: Error) => {
throw err
})
}
async trimVideo({ videoUrl = '', startTime = 2, endTime = 6, blobUrl = true } = {}) {
await this.loadFfmpeg()
let videoData = await fetchFile(videoUrl)
await this.ffmpeg.FS('writeFile', `input.mp4`, videoData)
const outputFileName = 'trimmedVideo.mp4'
const ffmpegCommand = [
'-i',
'input.mp4',
'-ss',
String(startTime),
'-to',
String(endTime),
'-c:v',
'libx264',
'-c:a',
'copy',
'-strict',
'experimental',
outputFileName
]
await this.ffmpeg.run(...ffmpegCommand)
const trimmedVideoData = await this.ffmpeg.FS('readFile', outputFileName)
this.ffmpeg.FS('unlink', outputFileName)
this.ffmpeg.FS('unlink', 'input.mp4')
const trimmedVideoBlob = new Blob([trimmedVideoData.buffer], { type: 'video/mp4' })
return blobUrl ? URL.createObjectURL(trimmedVideoBlob) : trimmedVideoBlob
}
async cropVideo({ videoUrl = '', width = 720, height = 720, x = 0, y = 0, blobUrl = true } = {}) {
await this.loadFfmpeg()
let videoData = await fetchFile(videoUrl)
await this.ffmpeg.FS('writeFile', 'input.mp4', videoData)
const outputFileName = 'croppedVideo.mp4'
let ffmpegCommand = ['-i', 'input.mp4', '-vf', `crop=${width}:${height}:${x}:${y}`, '-c:v', 'libx264', outputFileName]
await this.ffmpeg.run(...ffmpegCommand)
const croppedVideoData = await this.ffmpeg.FS('readFile', outputFileName)
this.ffmpeg.FS('unlink', outputFileName)
this.ffmpeg.FS('unlink', 'input.mp4')
const croppedVideoBlob = new Blob([croppedVideoData.buffer], { type: 'video/mp4' })
return blobUrl ? URL.createObjectURL(croppedVideoBlob) : croppedVideoBlob
}
async scaleVideo({ videoUrl = '', width = 720, height = 720, blobUrl = true } = {}) {
await this.loadFfmpeg()
let videoData = await fetchFile(videoUrl)
await this.ffmpeg.FS('writeFile', 'input.mp4', videoData)
const outputFileName = 'croppedVideo.mp4'
let ffmpegCommand = ['-i', 'input.mp4', '-vf', `scale=${width}:${height}`, '-c:v', 'libx264', outputFileName]
await this.ffmpeg.run(...ffmpegCommand)
const croppedVideoData = await this.ffmpeg.FS('readFile', outputFileName)
this.ffmpeg.FS('unlink', outputFileName)
this.ffmpeg.FS('unlink', 'input.mp4')
const croppedVideoBlob = new Blob([croppedVideoData.buffer], { type: 'video/mp4' })
return blobUrl ? URL.createObjectURL(croppedVideoBlob) : croppedVideoBlob
}
async convertVideoToImages({ videoUrl = '', duration = 10, frameRate = 8, imageType = 'png', blobUrl = true } = {}) {
await this.loadFfmpeg()
const videoData = await fetchFile(videoUrl)
await this.ffmpeg.FS('writeFile', 'input.mp4', videoData)
const outputImagePattern = 'output-%04d.' + imageType
const ffmpegCommand = ['-i', 'input.mp4', '-r', `${frameRate}`, outputImagePattern]
await this.ffmpeg.run(...ffmpegCommand)
const imageBlobUrls = []
const numFrames = Math.floor(duration * frameRate)
for (let i = 1; i <= numFrames; i++) {
const frameIndex = i.toString().padStart(4, '0')
const imageFileName = outputImagePattern.replace('%04d', frameIndex)
const imageData = await this.ffmpeg.FS('readFile', imageFileName)
const imageBlob = new Blob([imageData.buffer], { type: `image/${imageType}` })
const imageUrl = blobUrl ? URL.createObjectURL(imageBlob) : imageBlob
imageBlobUrls.push(imageUrl)
this.ffmpeg.FS('unlink', imageFileName)
}
this.ffmpeg.FS('unlink', 'input.mp4')
return imageBlobUrls
}
// 运行复合指令处理视频
async handleVideoCommand({
videoUrl = '',
blobUrl = true,
command = [] as string[],
outputFileName = 'handleVideo.mp4' as string,
outputType = 'video/mp4' as string
} = {}) {
await this.loadFfmpeg()
let videoData = await fetchFile(videoUrl)
await this.ffmpeg.FS('writeFile', 'input.mp4', videoData)
let ffmpegCommand = ['-i', 'input.mp4', ...command, outputFileName]
await this.ffmpeg.run(...ffmpegCommand)
const handleVideoData = await this.ffmpeg.FS('readFile', outputFileName)
this.ffmpeg.FS('unlink', outputFileName)
this.ffmpeg.FS('unlink', 'input.mp4')
const handleVideoBlob = new Blob([handleVideoData.buffer], { type: outputType })
return blobUrl ? URL.createObjectURL(handleVideoBlob) : handleVideoBlob
}
}
export const useFFmpeg = () => {
const ffmpegHandle = new CustomVideoHandle()
const onInit = () => {
return ffmpegHandle.loadFfmpeg()
}
return { onInit, ffmpegHandle }
}