Files
OpenIsle/frontend_nuxt/utils/webcodecVideoCompressor.js
2025-09-11 16:32:15 +08:00

99 lines
2.7 KiB
JavaScript

import MP4Box from 'mp4box'
// 检查 WebCodecs 支持
export function isWebCodecSupported() {
return typeof window !== 'undefined' && typeof window.VideoEncoder !== 'undefined'
}
// 使用 WebCodecs + MP4Box.js 压缩视频
export async function compressVideoWithWebCodecs(file, opts = {}) {
const { onProgress = () => {}, width = 720, bitrate = 1_000_000 } = opts
if (!isWebCodecSupported()) {
throw new Error('当前浏览器不支持 WebCodecs')
}
onProgress({ stage: 'initializing', progress: 0 })
// 加载原始视频
const url = URL.createObjectURL(file)
const video = document.createElement('video')
video.src = url
video.muted = true
await video.play().catch(() => {})
video.pause()
await new Promise((resolve) => {
if (video.readyState >= 2) resolve()
else video.onloadedmetadata = () => resolve()
})
const targetWidth = width
const targetHeight = Math.round((video.videoHeight / video.videoWidth) * width)
const canvas = document.createElement('canvas')
canvas.width = targetWidth
canvas.height = targetHeight
const ctx = canvas.getContext('2d')
const chunks = []
const encoder = new VideoEncoder({
output: (chunk) => {
chunks.push(chunk)
},
error: (e) => {
throw e
},
})
encoder.configure({
codec: 'avc1.42001E',
width: targetWidth,
height: targetHeight,
bitrate,
framerate: 30,
})
const duration = video.duration
const frameCount = Math.floor(duration * 30)
for (let i = 0; i < frameCount; i++) {
video.currentTime = i / 30
await new Promise((res) => (video.onseeked = res))
ctx.drawImage(video, 0, 0, targetWidth, targetHeight)
const bitmap = await createImageBitmap(canvas)
const frame = new VideoFrame(bitmap, { timestamp: (i / 30) * 1000000 })
encoder.encode(frame)
frame.close()
bitmap.close()
onProgress({ stage: 'compressing', progress: Math.round(((i + 1) / frameCount) * 80) })
}
await encoder.flush()
onProgress({ stage: 'finalizing', progress: 90 })
const mp4box = MP4Box.createFile()
const track = mp4box.addTrack({
timescale: 1000,
width: targetWidth,
height: targetHeight,
})
let dts = 0
chunks.forEach((chunk) => {
const data = new Uint8Array(chunk.byteLength)
chunk.copyTo(data)
mp4box.addSample(track, data.buffer, {
duration: chunk.duration ? chunk.duration / 1000 : 33,
dts,
cts: dts,
is_sync: chunk.type === 'key',
})
dts += chunk.duration ? chunk.duration / 1000 : 33
})
const arrayBuffer = mp4box.flush()
const outputFile = new File([arrayBuffer], file.name.replace(/\.[^.]+$/, '.mp4'), {
type: 'video/mp4',
})
onProgress({ stage: 'completed', progress: 100 })
URL.revokeObjectURL(url)
return outputFile
}