ffmpeg
使用场景是需要在web端进行视频的裁剪,包括使用 在线视频url
或 本地视频文件
的裁剪,以及对视频内容的截取等功能。
前端进行视频操作可能会导致性能下降,最好通过后端使用java,c++进行处理,本文的案例是备选方案。
注意:
以下所有的使用案例均基于vue3 setup。
同时由于@ffmpeg版本不同会导致使用的api不同,使用案例前需要注意@ffmpeg版本问题
。
如果使用的是0.12+需要使用新的api,详情请看 文档
npm
npm install @ffmpeg/ffmpeg@^0.10.0npm install @ffmpeg/core@^0.10.0
在线视频url剪辑
<script setup>
// "@ffmpeg/core": "^0.10.0",
// "@ffmpeg/ffmpeg": "^0.10.0",import { ref, onMounted, onUnmounted } from 'vue'
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';const ffmpeg = createFFmpeg({ log: true });
const fileType = ref("") // 视频文件类型/*** 根据在线的视频地址截取片段* @param {String} url 在线视频链接* @param {Number|String} startTime 截取开始时间* @param {Number|String} endTime 截取结束时间* @param {Function} callBack 回调函数*/
const videoCut = async (url, startTime, endTime, callBack) => {if (!ffmpeg.isLoaded()) {await ffmpeg.load();}if(!url) return;fileType.value = url.split(".").pop()const inputName = `input.${fileType.value}`;const outputName = `output.${fileType.value}`;// 将输入文件保存到虚拟文件系统await ffmpeg.FS('writeFile', inputName, await fetchFile(url));// 运行 FFmpeg 命令try {await ffmpeg.run('-ss', `${startTime}`,'-t', `${endTime - startTime}`,'-i', inputName,'-vcodec', 'copy','-acodec', 'copy',outputName);// 读取输出文件let arrayBuffer = ffmpeg.FS('readFile', outputName).buffer; // 读取缓存// 创建下载链接并通过回调下载保存到本地const fileUrl = URL.createObjectURL(new Blob([arrayBuffer])); // 转为Blob URLcallBack && callBack(fileUrl)// 释放内存ffmpeg.FS('unlink', inputName);ffmpeg.FS('unlink', outputName);} catch (e) { }
}const downloadFile = (url, fileName = `clip.${fileType.value}`) => {const link = document.createElement('a');link.href = url;link.download = fileName;link.click();
}onMounted(() => {videoCut("https://视频.mp4", 0, 3, downloadFile)
})onUnmounted(() => {ffmpeg.exit();
})
</script>
本地视频文件剪辑
<template><input type="file" @change="fileChange">
</template><script setup>
import { ref, onUnmounted } from 'vue'
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';const ffmpeg = createFFmpeg({ log: true });
const fileType = ref("") // 视频文件类型const fileChange = (e) => {if (!e.target.files[0]) return;const file = e.target.files[0];fileType.value = file.name.split(".").pop()videoCut(file, 0, 3, downloadFile)
}/*** 根据选择的视频文件截取片段* @param {File} file 选择的视频文件* @param {Number|String} startTime 截取开始时间* @param {Number|String} endTime 截取结束时间* @param {Function} callBack 回调函数*/
const videoCut = async (file, startTime, endTime, callBack) => {if (!ffmpeg.isLoaded()) {await ffmpeg.load();}if(!file) return;const inputName = `input.${fileType.value}`;const outputName = `output.${fileType.value}`;const orgFileBuffer = await file.arrayBuffer()// 将输入文件保存到虚拟文件系统await ffmpeg.FS('writeFile', inputName, await fetchFile(new Blob([orgFileBuffer])));try {await ffmpeg.run('-ss', `${startTime}`,'-t', `${endTime - startTime}`,'-i', inputName,'-vcodec', 'copy','-acodec', 'copy',outputName);// 读取输出文件let arrayBuffer = ffmpeg.FS('readFile', outputName).buffer; // 读取缓存// 创建下载链接并通过回调下载保存到本地const fileUrl = URL.createObjectURL(new Blob([arrayBuffer])); // 转为Blob URLcallBack && callBack(fileUrl)// 释放内存ffmpeg.FS('unlink', inputName);ffmpeg.FS('unlink', outputName);} catch (e) {}
}const downloadFile = (url, fileName = `clip.${fileType.value}`) => {const link = document.createElement('a');link.href = url;link.download = fileName;link.click();
}onUnmounted(() => {ffmpeg.exit();
})
</script>
获取视频画面截图
<template><input type="file" @change="fileChange">
</template><script setup>
import { ref, onUnmounted } from 'vue'
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';const ffmpeg = createFFmpeg({ log: true });
const fileType = ref("") // 视频文件类型const fileChange = (e) => {if (!e.target.files[0]) return;const file = e.target.files[0];fileType.value = file.name.split(".").pop()// 由于这里一秒截取一帧 ,截取5次, 所以如果视频不足5秒会导致截取和读取失败// 回调中是base64图片组成的数组,需要在前面拼接 "data:image/png;base64," ,然后在img的src中赋值即可videoFrame(file, 5, 1, (data) => console.log(data))
}/*** 根据选择的视频文件获取视频截图* @param {File} file 选择的视频文件* @param {Number|String} count 截取图片的次数* @param {Number|String} interval 截取图片的间隔* @param {Function} callBack 回调*/
const videoFrame = async (file, count, interval, callBack) => {if (!ffmpeg.isLoaded()) {await ffmpeg.load();}if(!file) return;const inputName = `input.${fileType.value}`;const orgFileBuffer = await file.arrayBuffer()// 将输入文件保存到虚拟文件系统await ffmpeg.FS('writeFile', inputName, await fetchFile(new Blob([orgFileBuffer])));try {await ffmpeg.run("-i",inputName,"-r",`${interval}`,"-ss","0","-vframes",`${count}`,"-f","image2","-s","88*50","image-%02d.png");const baseArr = []for (let i = 0; i < count; i++) {let temp = i + 1;if (temp < 10) {temp = "0" + temp;}baseArr.push(arrayBufferToBase64(ffmpeg.FS("readFile", "image-" + temp + ".png")));}callBack && callBack(baseArr)// 释放内存ffmpeg.FS('unlink', inputName);} catch (e) {}
}const arrayBufferToBase64 = (array) => {array = new Uint8Array(array);var length = array.byteLength;var table = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'];var base64Str = "";for (var i = 0; length - i >= 3; i += 3) {var num1 = array[i];var num2 = array[i + 1];var num3 = array[i + 2];base64Str +=table[num1 >>> 2] +table[((num1 & 0b11) << 4) | (num2 >>> 4)] +table[((num2 & 0b1111) << 2) | (num3 >>> 6)] +table[num3 & 0b111111];}var lastByte = length - i;if (lastByte === 1) {var lastNum1 = array[i];base64Str +=table[lastNum1 >>> 2] + table[(lastNum1 & 0b11) << 4] + "==";} else if (lastByte === 2) {// eslint-disable-next-line no-redeclarevar lastNum1 = array[i];var lastNum2 = array[i + 1];base64Str +=table[lastNum1 >>> 2] +table[((lastNum1 & 0b11) << 4) | (lastNum2 >>> 4)] +table[(lastNum2 & 0b1111) << 2] +"=";}return base64Str
}onUnmounted(() => {ffmpeg.exit();
})
</script>