Created
June 25, 2025 05:48
-
-
Save milksense/eb74e42029ea4fff572b5d0cf88a55df to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { FFmpeg } from '@ffmpeg/ffmpeg'; | |
import { toBlobURL } from '@ffmpeg/util'; | |
let ffmpeg: FFmpeg | null = null; | |
export const initFFmpeg = async (): Promise<FFmpeg> => { | |
if (ffmpeg) return ffmpeg; | |
ffmpeg = new FFmpeg(); | |
// Use locally hosted files instead of CDN | |
const baseURL = '/ffmpeg'; | |
await ffmpeg.load({ | |
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'), | |
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'), | |
}); | |
return ffmpeg; | |
}; | |
export const generateThumbnail = async ( | |
videoFile: File, | |
timeInSeconds: number = 1 | |
): Promise<string> => { | |
const ffmpeg = await initFFmpeg(); | |
const inputName = 'input.mp4'; | |
const outputName = 'thumbnail.jpg'; | |
// Write input file | |
await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); | |
// Generate thumbnail at specific time | |
await ffmpeg.exec([ | |
'-i', inputName, | |
'-ss', timeInSeconds.toString(), | |
'-vframes', '1', | |
'-vf', 'scale=320:240', | |
'-q:v', '2', | |
outputName | |
]); | |
// Read output file | |
const data = await ffmpeg.readFile(outputName); | |
const blob = new Blob([data], { type: 'image/jpeg' }); | |
// Cleanup | |
await ffmpeg.deleteFile(inputName); | |
await ffmpeg.deleteFile(outputName); | |
return URL.createObjectURL(blob); | |
}; | |
export const trimVideo = async ( | |
videoFile: File, | |
startTime: number, | |
endTime: number, | |
onProgress?: (progress: number) => void | |
): Promise<Blob> => { | |
const ffmpeg = await initFFmpeg(); | |
const inputName = 'input.mp4'; | |
const outputName = 'output.mp4'; | |
// Set up progress callback | |
if (onProgress) { | |
ffmpeg.on('progress', ({ progress }) => { | |
onProgress(progress * 100); | |
}); | |
} | |
// Write input file | |
await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); | |
const duration = endTime - startTime; | |
// Trim video | |
await ffmpeg.exec([ | |
'-i', inputName, | |
'-ss', startTime.toString(), | |
'-t', duration.toString(), | |
'-c', 'copy', // Use stream copy for faster processing | |
outputName | |
]); | |
// Read output file | |
const data = await ffmpeg.readFile(outputName); | |
const blob = new Blob([data], { type: 'video/mp4' }); | |
// Cleanup | |
await ffmpeg.deleteFile(inputName); | |
await ffmpeg.deleteFile(outputName); | |
return blob; | |
}; | |
export const getVideoInfo = async (videoFile: File): Promise<{ | |
duration: number; | |
width: number; | |
height: number; | |
fps: number; | |
}> => { | |
const ffmpeg = await initFFmpeg(); | |
const inputName = 'input.mp4'; | |
// Write input file | |
await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); | |
// Capture FFmpeg stderr output with a one-time listener pattern | |
let ffmpegOutput = ''; | |
let listening = true; | |
const listener = (data: string) => { | |
if (listening) ffmpegOutput += data; | |
}; | |
ffmpeg.on('log', ({ message }) => listener(message)); | |
// Run ffmpeg to get info (stderr will contain the info) | |
try { | |
await ffmpeg.exec(['-i', inputName, '-f', 'null', '-']); | |
} catch (error) { | |
listening = false; | |
await ffmpeg.deleteFile(inputName); | |
console.error('FFmpeg execution failed:', error); | |
throw new Error('Failed to extract video info. The file may be corrupted or in an unsupported format.'); | |
} | |
// Disable listener after exec completes | |
listening = false; | |
// Cleanup | |
await ffmpeg.deleteFile(inputName); | |
// Parse output for duration, resolution, and fps | |
// Example: Duration: 00:00:10.00, start: 0.000000, bitrate: 1234 kb/s | |
// Example: Stream #0:0: Video: h264 (High), yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 30 fps, 30 tbr, 90k tbn, 60 tbc | |
const durationMatch = ffmpegOutput.match(/Duration: (\d+):(\d+):([\d.]+)/); | |
let duration = 0; | |
if (durationMatch) { | |
const [, h, m, s] = durationMatch; | |
duration = parseInt(h) * 3600 + parseInt(m) * 60 + parseFloat(s); | |
} | |
const videoStreamMatch = ffmpegOutput.match(/Video:.* (\d+)x(\d+)[^,]*, ([\d.]+) fps/); | |
let width = 0, height = 0, fps = 0; | |
if (videoStreamMatch) { | |
width = parseInt(videoStreamMatch[1]); | |
height = parseInt(videoStreamMatch[2]); | |
fps = parseFloat(videoStreamMatch[3]); | |
} | |
return { | |
duration, | |
width, | |
height, | |
fps | |
}; | |
}; | |
export const convertToWebM = async ( | |
videoFile: File, | |
onProgress?: (progress: number) => void | |
): Promise<Blob> => { | |
const ffmpeg = await initFFmpeg(); | |
const inputName = 'input.mp4'; | |
const outputName = 'output.webm'; | |
// Set up progress callback | |
if (onProgress) { | |
ffmpeg.on('progress', ({ progress }) => { | |
onProgress(progress * 100); | |
}); | |
} | |
// Write input file | |
await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); | |
// Convert to WebM | |
await ffmpeg.exec([ | |
'-i', inputName, | |
'-c:v', 'libvpx-vp9', | |
'-crf', '30', | |
'-b:v', '0', | |
'-c:a', 'libopus', | |
outputName | |
]); | |
// Read output file | |
const data = await ffmpeg.readFile(outputName); | |
const blob = new Blob([data], { type: 'video/webm' }); | |
// Cleanup | |
await ffmpeg.deleteFile(inputName); | |
await ffmpeg.deleteFile(outputName); | |
return blob; | |
}; | |
export const extractAudio = async ( | |
videoFile: File, | |
format: 'mp3' | 'wav' = 'mp3' | |
): Promise<Blob> => { | |
const ffmpeg = await initFFmpeg(); | |
const inputName = 'input.mp4'; | |
const outputName = `output.${format}`; | |
// Write input file | |
await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); | |
// Extract audio | |
await ffmpeg.exec([ | |
'-i', inputName, | |
'-vn', // Disable video | |
'-acodec', format === 'mp3' ? 'libmp3lame' : 'pcm_s16le', | |
outputName | |
]); | |
// Read output file | |
const data = await ffmpeg.readFile(outputName); | |
const blob = new Blob([data], { type: `audio/${format}` }); | |
// Cleanup | |
await ffmpeg.deleteFile(inputName); | |
await ffmpeg.deleteFile(outputName); | |
return blob; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment