feat(project): init
This commit is contained in:
124
app/component/label/video/page.tsx
Normal file
124
app/component/label/video/page.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client"
|
||||
|
||||
import { ChangeEvent, useEffect, useRef, useState } from "react"
|
||||
import type { WorkerMessage, WorkerResponse } from "./types/libav"
|
||||
|
||||
export default function Home() {
|
||||
const workerRef = useRef<Worker | null>(null)
|
||||
const [ready, setReady] = useState(false)
|
||||
const [logs, setLogs] = useState<string[]>([])
|
||||
const [videoUrl, setVideoUrl] = useState<string | null>(null)
|
||||
const [processing, setProcessing] = useState(false)
|
||||
const addLog = (msg: string) => {
|
||||
setLogs((prev) => [...prev.slice(-10), msg]) // 只保留最近10条
|
||||
}
|
||||
useEffect(() => {
|
||||
// 初始化 Worker
|
||||
// Next.js 会自动检测到这个语法并单独打包 Worker
|
||||
workerRef.current = new Worker(
|
||||
new URL("./libav.worker.ts", import.meta.url)
|
||||
)
|
||||
|
||||
// 设置消息监听
|
||||
workerRef.current.onmessage = (event: MessageEvent<WorkerResponse>) => {
|
||||
const { type } = event.data
|
||||
|
||||
switch (type) {
|
||||
case "READY":
|
||||
setReady(true)
|
||||
addLog("FFmpeg core loaded and ready.")
|
||||
break
|
||||
case "LOG":
|
||||
if ("message" in event.data) addLog(event.data.message)
|
||||
break
|
||||
case "DONE":
|
||||
if ("data" in event.data) {
|
||||
const url = URL.createObjectURL(event.data.data)
|
||||
setVideoUrl(url)
|
||||
setProcessing(false)
|
||||
addLog("Transcoding complete!")
|
||||
}
|
||||
break
|
||||
case "ERROR":
|
||||
if ("error" in event.data) {
|
||||
addLog(`Error: ${event.data.error}`)
|
||||
setProcessing(false)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 告诉 Worker 加载 libav
|
||||
// 这里的路径必须指向 public 目录下的实际文件
|
||||
// 使用 default-cli 变体以确保包含 MP4/H.264 等常见格式的解封装器和解码器
|
||||
const LIBAV_PATH = "/libav/libav-6.8.8.0-default.js"
|
||||
workerRef.current.postMessage({
|
||||
type: "LOAD",
|
||||
config: { corePath: location.origin + LIBAV_PATH },
|
||||
} as WorkerMessage)
|
||||
|
||||
return () => {
|
||||
workerRef.current?.terminate()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file || !workerRef.current || !ready) return
|
||||
|
||||
setProcessing(true)
|
||||
setVideoUrl(null)
|
||||
setLogs([])
|
||||
addLog(`Starting upload: ${file.name}`)
|
||||
|
||||
workerRef.current.postMessage({
|
||||
type: "TRANSCODE",
|
||||
file: file,
|
||||
} as WorkerMessage)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="p-8 font-mono">
|
||||
<h1 className="text-2xl font-bold mb-4">Next.js + Libav.js + Worker</h1>
|
||||
|
||||
<div className="mb-4 p-4 border rounded bg-gray-50">
|
||||
<p>
|
||||
Status:{" "}
|
||||
{ready ? (
|
||||
<span className="text-green-600">Ready</span>
|
||||
) : (
|
||||
<span className="text-yellow-600">Loading Core...</span>
|
||||
)}
|
||||
</p>
|
||||
<p>Processing: {processing ? "Yes" : "No"}</p>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
onChange={handleFileChange}
|
||||
disabled={!ready || processing}
|
||||
className="mb-4 block w-full text-sm text-slate-500
|
||||
file:mr-4 file:py-2 file:px-4
|
||||
file:rounded-full file:border-0
|
||||
file:text-sm file:font-semibold
|
||||
file:bg-violet-50 file:text-violet-700
|
||||
hover:file:bg-violet-100"
|
||||
/>
|
||||
|
||||
{videoUrl && (
|
||||
<div className="mb-4">
|
||||
<h2 className="font-bold">Result:</h2>
|
||||
<video controls src={videoUrl} className="w-full max-w-md border" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-black text-white p-4 rounded text-xs h-64 overflow-y-auto">
|
||||
{logs.map((log, i) => (
|
||||
<div key={i} className="border-b border-gray-800 py-1">
|
||||
{log}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user