FFmpeg@0.12.15完全不依賴SharedArrayBuffer!!!強烈推薦使用
本文章主要是在vite+vue3項目中使用FFmpeg,只展示了如何在項目中引入和基礎的使用
更多詳細參數可參照?ffmpeg官網
https://ffmpeg.org/
一、安裝FFmpeg
可通過npm直接安裝
npm install @ffmpeg/core@0.12.10 @ffmpeg/ffmpeg@0.12.15 @ffmpeg/util@0.12.2
二、在代碼中使用
1、初始化FFmpeg
首先找到node_modules/@ffmpeg/core/dist/esm下的這兩個文件,并放到public文件夾下,例如
import { FFmpeg } from '@ffmpeg/ffmpeg'
import { fetchFile, toBlobURL } from '@ffmpeg/util'
import { ref, onMounted } from 'vue'
let ffmpeg = null
const initFFmpegFn = async () => {if (ffmpeg) return// 初始化ffmpeg ffmpeg = new FFmpeg();// 如果需要當前編輯視頻進度let progress = ref(0);ffmpeg.on('log', ({ type, message }) => {if (type === 'stderr') {// 匹配日志中的時間信息(例如:time=00:00:05.12)const timeMatch = message.match(/time=(\d+:\d+:\d+\.\d+)/);if (timeMatch && totalDuration.value) {const currentTime = parseTimeToSeconds(timeMatch[1]);const newProgress = Math.min(Math.round((currentTime / totalDuration.value) * 100), 100);progress.value = newProgress;}}});// 加載FFmpeg核心try {// ffmpeg.loaded 核心是否加載if (!ffmpeg.loaded) {let ffmpegBaseUrl = '/FFmpeg/dist'await ffmpeg.load({coreURL: await toBlobURL(`${ffmpegBaseUrl}/ffmpeg-core.js`, 'text/javascript'),wasmURL: await toBlobURL(`${ffmpegBaseUrl}/ffmpeg-core.wasm`, 'application/wasm'),});console.log('加載完成', ffmpeg);}} catch (err) {console.error('FFmpeg核心加載失敗:', err);}}
// 工具函數:將時間字符串(如00:00:05.12)轉換為秒
const parseTimeToSeconds = (timeStr) => {const [hours, minutes, seconds] = timeStr.split(':').map(Number);return hours * 3600 + minutes * 60 + seconds;
};
onMounted(() => {initFFmpegFn()
})
2、加載出錯處理(如果通過上一步能正常加載完成,可忽略)
由于ffmpeg中會使用vite中的worker,可能會導致控制臺中有一個鏈接為http://localhost:9090/node_modules/.vite/deps/worker.js?worker_file&type=module一直處于pending狀態,無法加載成功
需要在vite.config.js中使用optimizeDeps.exclude
?是指定不需要進行依賴預構建。
3、編輯視頻:是否靜音-調整視頻寬高-裁剪視頻時長
const processVideoFn = async (fileBlob, processOptions = {}) => {// fileBlob 視頻文件blob對象if (!fileBlob || !(fileBlob instanceof Blob)) {console.error('錯誤:請傳入有效的視頻Blob對象');return { success: false, error: '無效的視頻Blob', url: null, blob: null };}if (fileBlob.size === 0) {console.error('錯誤:傳入的Blob為空(大小0字節)');return { success: false, error: '輸入Blob為空', url: null, blob: null };}const {needClearVoice = false,//裁剪后是否靜音resizeInfo = {width:800,height:800},//裁剪后視頻寬高cropInfo = { startTime:0, duration:60 }// startTime:裁剪視頻開始時間 duration:總裁剪時長} = processOptions;const { startTime = 0, duration = 60 } = cropInfo;progress.value = 0;//進度條歸0totalDuration.value = duration;//總裁剪時長,用于計算進度條const timestamp = Date.now();const inputFileName = `input_video.mp4`;const outputFileName = `output_video_${timestamp}.mp4`;let outputData = null; // 存儲輸出數據,避免提前清理try {if (!ffmpeg || !loadSuccess.value) {throw new Error('FFmpeg核心未加載完成');}const fileData = await fetchFile(fileBlob); // fetchFile返回Uint8Arrayawait ffmpeg.writeFile(inputFileName, fileData); // 直接傳文件名+數據const ffmpegCommand = ['-i', inputFileName];// 裁剪處理(校驗參數)if (cropInfo) {if (startTime < 0 || duration <= 0) {throw new Error(`裁剪參數無效:startTime=${startTime}(需≥0),duration=${duration}(需>0)`);}ffmpegCommand.push('-ss', startTime.toFixed(2), '-t', duration.toFixed(2));}// 尺寸調整(校驗參數)if (resizeInfo) {let { width, height } = resizeInfo;if (width <= 0 || height <= 0) {throw new Error(`尺寸參數無效:width=${width}(需>0),height=${height}(需>0)`);}const scaleFilter = `scale=w=${width}:h=${height}:force_original_aspect_ratio=decrease`;const padFilter = `pad=w=${width}:h=${height}:x=(ow-iw)/2:y=(oh-ih)/2:color=white`;//視頻寬高不夠時填充白色邊框ffmpegCommand.push('-vf', `${scaleFilter},${padFilter}`);}// 音頻處理if (needClearVoice) {ffmpegCommand.push('-an'); // 移除音頻} else {ffmpegCommand.push('-c:a', 'aac', '-strict', 'experimental'); // 保留音頻}ffmpegCommand.push('-c:v', 'libx264', // H.264編碼(通用)'-pix_fmt', 'yuv420p', // 兼容所有播放器(避免僅支持yuv444p的問題)'-crf', '30', // 控制質量(值越小質量越高,28為平衡值)'-preset', 'ultrafast', // ultrafast/superfast/veryfast/faster/fast/medium(默認值)/slow/slower/veryslow 從前到后處理速度越來越慢,處理視頻越精致'-b:v', '1M', // 視頻比特率(避免輸出過小/過大)'-y', // 覆蓋已有文件outputFileName);await ffmpeg.exec(ffmpegCommand);outputData = await ffmpeg.readFile(outputFileName);if (!outputData || outputData.length === 0) {throw new Error('FFmpeg讀取輸出文件為空');}const resultBlob = new Blob([outputData], { type: 'video/mp4' });if (resultBlob.size === 0) {throw new Error('生成的Blob為空');}const previewUrl = URL.createObjectURL(resultBlob);return {url: previewUrl,//生成的臨時視頻urlblob: resultBlob,//生成的新視頻blob對象};} catch (err) {console.error('視頻處理失敗:',err);} finally {if (ffmpeg && outputData) {if (await ffmpeg.listDir(inputFileName).catch(() => false)) {await ffmpeg.unlink(inputFileName).catch(err => console.warn('清理輸入文件失敗:', err));}if (await ffmpeg.listDir(outputFileName).catch(() => false) && outputData) { // 確保讀取后再刪除輸出await ffmpeg.unlink(outputFileName).catch(err => console.warn('清理輸出文件失敗:', err));}}}
};