mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-08 08:01:16 +08:00
99 lines
2.7 KiB
JavaScript
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
|
|
}
|