perf(component): perf
This commit is contained in:
@@ -1,5 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
export default function ComponentBaseDepartmentPage() {
|
|
||||||
return <>ComponentBaseDepartmentPage</>
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Breadcrumbs,
|
|
||||||
Divider,
|
|
||||||
Paper,
|
|
||||||
Stack,
|
|
||||||
UnstyledButton,
|
|
||||||
Text,
|
|
||||||
} from "@mantine/core"
|
|
||||||
|
|
||||||
export default function ComponentBasePlacementPage() {
|
|
||||||
const items = [{ title: "布局组件" }].map((item) => (
|
|
||||||
<UnstyledButton key={item.title}>{item.title}</UnstyledButton>
|
|
||||||
))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Stack w="100%" h="calc(100vh - 56px)" p="md">
|
|
||||||
<Breadcrumbs>{items}</Breadcrumbs>
|
|
||||||
<Paper shadow="xs" p="0" flex={1} display="flex">
|
|
||||||
<Stack flex={1}>
|
|
||||||
<Stack flex={1} justify="center" align="center">
|
|
||||||
<Text c="dimmed">空</Text>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
<Divider orientation="vertical"></Divider>
|
|
||||||
<Stack flex={2}></Stack>
|
|
||||||
</Paper>
|
|
||||||
</Stack>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import TreeSelect from "@/components/tree-select"
|
|
||||||
import { getCityTree } from "@/components/tree-select/api"
|
|
||||||
import { TreeNode } from "@/components/tree-select/api/type"
|
|
||||||
import { VirtualTreeSelect } from "@/components/tree-select/tree"
|
|
||||||
import {
|
|
||||||
Breadcrumbs,
|
|
||||||
Flex,
|
|
||||||
Paper,
|
|
||||||
Stack,
|
|
||||||
TreeNodeData,
|
|
||||||
UnstyledButton,
|
|
||||||
} from "@mantine/core"
|
|
||||||
import { useCallback, useEffect, useState } from "react"
|
|
||||||
|
|
||||||
const convertTreeNodeToTreeNodeData = (
|
|
||||||
list: TreeNode[],
|
|
||||||
level: number = 1
|
|
||||||
): TreeNodeData[] =>
|
|
||||||
list.map((g) => ({
|
|
||||||
label: g.label,
|
|
||||||
value: g.value,
|
|
||||||
nodeProps: { level: level },
|
|
||||||
children:
|
|
||||||
g.children && g.children.length
|
|
||||||
? convertTreeNodeToTreeNodeData(g.children, level + 1)
|
|
||||||
: [],
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default function ComponentBaseSelectPage() {
|
|
||||||
const items = [{ title: "选择组件" }].map((item) => (
|
|
||||||
<UnstyledButton key={item.title}>{item.title}</UnstyledButton>
|
|
||||||
))
|
|
||||||
|
|
||||||
const [value, setValue] = useState<string>("")
|
|
||||||
|
|
||||||
const [treeData, setTreeData] = useState<TreeNodeData[]>([])
|
|
||||||
const asyncGetCityTree = useCallback(async () => {
|
|
||||||
const res = await getCityTree()
|
|
||||||
setTreeData(convertTreeNodeToTreeNodeData(res))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
queueMicrotask(() => asyncGetCityTree())
|
|
||||||
}, [asyncGetCityTree])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Stack w="100%" h="calc(100vh - 56px)" p="md">
|
|
||||||
<Breadcrumbs>{items}</Breadcrumbs>
|
|
||||||
<Paper shadow="xs" p={"md"} flex={1} display="flex">
|
|
||||||
<Flex h={"fit-content"} gap={"lg"}>
|
|
||||||
<TreeSelect treeData={treeData} />
|
|
||||||
|
|
||||||
<VirtualTreeSelect
|
|
||||||
data={treeData}
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
setCurrCityCode={() => {}}
|
|
||||||
w={"200px"}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
<Flex>{value}</Flex>
|
|
||||||
</Paper>
|
|
||||||
</Stack>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Video Frame Extractor (FFmpeg.wasm)
|
|
||||||
|
|
||||||
## 运行位置
|
|
||||||
- 纯前端:所有转码/抽帧在浏览器 Web Worker + WASM 内完成,不依赖后端服务。
|
|
||||||
|
|
||||||
## 大文件最佳实践
|
|
||||||
- 优先使用 WORKERFS 挂载输入文件,避免 `writeFile(arrayBuffer)` 将整段视频复制进 WASM 内存。
|
|
||||||
- 抽帧建议走“单帧模式”:每次 `-ss <time> -frames:v 1` 只产出一张,读取后立即删除,避免在 WASM 内累积。
|
|
||||||
- 输出建议使用 image2 序列 pattern(例如 `frame_%06d.jpg`)并配合 `-start_number`,避免 image2 的“必须是序列 pattern”报错路径。
|
|
||||||
- UI 端必须对日志与图片列表做上限与释放(`URL.revokeObjectURL`),否则长视频会导致 JS 堆持续增长。
|
|
||||||
|
|
||||||
## 故障恢复
|
|
||||||
- 一旦出现 `Aborted()` / `memory access out of bounds`,当前 FFmpeg.wasm 实例可能进入不可恢复状态,需要 terminate 并重建实例/worker。
|
|
||||||
|
|
||||||
@@ -1,401 +0,0 @@
|
|||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
Alert,
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Container,
|
|
||||||
FileInput,
|
|
||||||
Flex,
|
|
||||||
Group,
|
|
||||||
Image,
|
|
||||||
Loader,
|
|
||||||
Progress,
|
|
||||||
ScrollArea,
|
|
||||||
SimpleGrid,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
} from "@mantine/core"
|
|
||||||
import {
|
|
||||||
IconAlertCircle,
|
|
||||||
IconDownload,
|
|
||||||
IconMovie,
|
|
||||||
IconPhoto,
|
|
||||||
} from "@tabler/icons-react"
|
|
||||||
import { useEffect, useRef, useState } from "react"
|
|
||||||
import { appendCapped } from "./cap"
|
|
||||||
import type { WorkerResponse } from "./ffmpeg.worker"
|
|
||||||
import { getServerImage } from "@/components/label/api/label"
|
|
||||||
|
|
||||||
export default function VideoFrameExtractor() {
|
|
||||||
const [loaded, setLoaded] = useState(false)
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [file, setFile] = useState<File | null>(null)
|
|
||||||
const [processing, setProcessing] = useState(false)
|
|
||||||
const [progress, setProgress] = useState(0)
|
|
||||||
const [message, setMessage] = useState("Wait for loading...")
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const [images, setImages] = useState<{ url: string; name: string }[]>([])
|
|
||||||
const [logs, setLogs] = useState<string[]>([])
|
|
||||||
|
|
||||||
const workerRef = useRef<Worker | null>(null)
|
|
||||||
const messageRef = useRef<HTMLDivElement>(null)
|
|
||||||
const extractedCountRef = useRef(0)
|
|
||||||
|
|
||||||
const MAX_LOG_LINES = 300
|
|
||||||
const MAX_IMAGES = 1500
|
|
||||||
|
|
||||||
// Initialize Worker once
|
|
||||||
useEffect(() => {
|
|
||||||
queueMicrotask(() => {
|
|
||||||
if (!workerRef.current) {
|
|
||||||
setIsLoading(true)
|
|
||||||
|
|
||||||
const baseURL = window.location.origin + "/wasm"
|
|
||||||
|
|
||||||
const createWorker = () => {
|
|
||||||
const worker = new Worker(
|
|
||||||
new URL("./ffmpeg.worker.ts", import.meta.url)
|
|
||||||
)
|
|
||||||
workerRef.current = worker
|
|
||||||
return worker
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMessage = (event: MessageEvent<WorkerResponse>) => {
|
|
||||||
const { type } = event.data
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case "READY":
|
|
||||||
setLoaded(true)
|
|
||||||
setIsLoading(false)
|
|
||||||
setMessage("FFmpeg loaded successfully")
|
|
||||||
break
|
|
||||||
case "LOG":
|
|
||||||
if ("message" in event.data) {
|
|
||||||
const msg = event.data.message
|
|
||||||
setLogs((prev) => appendCapped(prev, [msg], MAX_LOG_LINES))
|
|
||||||
if (messageRef.current) {
|
|
||||||
messageRef.current.scrollTop = messageRef.current.scrollHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case "PROGRESS":
|
|
||||||
if ("progress" in event.data) {
|
|
||||||
const p = Math.round(event.data.progress * 100)
|
|
||||||
setProgress(p)
|
|
||||||
setMessage(`Processing... ${p}%`)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case "SEGMENT_DATA":
|
|
||||||
if ("files" in event.data) {
|
|
||||||
const newImages = event.data.files.map((f: any) => {
|
|
||||||
const blob = new Blob([f.data], { type: "image/jpeg" })
|
|
||||||
return { url: URL.createObjectURL(blob), name: f.name }
|
|
||||||
})
|
|
||||||
extractedCountRef.current += newImages.length
|
|
||||||
setImages((prev) =>
|
|
||||||
appendCapped(prev, newImages, MAX_IMAGES, (img) =>
|
|
||||||
URL.revokeObjectURL((img as any).url)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
setMessage(
|
|
||||||
`Processing... Extracted ${extractedCountRef.current} frames.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case "DONE":
|
|
||||||
setMessage(
|
|
||||||
`Completed! Extracted ${extractedCountRef.current} frames.`
|
|
||||||
)
|
|
||||||
setProcessing(false)
|
|
||||||
break
|
|
||||||
case "FATAL":
|
|
||||||
if ("error" in event.data) {
|
|
||||||
setError(event.data.error)
|
|
||||||
setProcessing(false)
|
|
||||||
setLoaded(false)
|
|
||||||
setIsLoading(true)
|
|
||||||
}
|
|
||||||
if (workerRef.current) {
|
|
||||||
workerRef.current.terminate()
|
|
||||||
workerRef.current = null
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const worker = createWorker()
|
|
||||||
worker.onmessage = handleMessage
|
|
||||||
worker.postMessage({ type: "LOAD", baseURL })
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case "ERROR":
|
|
||||||
if ("error" in event.data) {
|
|
||||||
setError(event.data.error)
|
|
||||||
setProcessing(false)
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const worker = createWorker()
|
|
||||||
worker.onmessage = handleMessage
|
|
||||||
|
|
||||||
worker.postMessage({ type: "LOAD", baseURL })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// Only create worker if not exists
|
|
||||||
|
|
||||||
// Cleanup worker on unmount?
|
|
||||||
// Usually good practice, but if we want to keep it alive for navigation back/forth,
|
|
||||||
// we might want to move it to Context.
|
|
||||||
// For this task, local cleanup is fine.
|
|
||||||
return () => {
|
|
||||||
if (workerRef.current) {
|
|
||||||
workerRef.current.terminate()
|
|
||||||
workerRef.current = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将视频 Base64 转换为 File 对象
|
|
||||||
* @param {string} base64String - 视频 Base64 字符串
|
|
||||||
* @param {string} fileName - 文件名 (例如 'video.mp4')
|
|
||||||
* @returns {File}
|
|
||||||
*/
|
|
||||||
function videoBase64ToFile(base64String: string, fileName: string) {
|
|
||||||
// 1. 提取 MIME 类型和数据
|
|
||||||
const parts = base64String.split(",")
|
|
||||||
const mime = parts[0].match(/:(.*?);/)?.[1] // 可能是 video/mp4, video/webm 等
|
|
||||||
|
|
||||||
// 2. 解码
|
|
||||||
const bstr = atob(parts[1])
|
|
||||||
let n = bstr.length
|
|
||||||
const u8arr = new Uint8Array(n)
|
|
||||||
|
|
||||||
// 3. 填充字节数组
|
|
||||||
while (n--) {
|
|
||||||
u8arr[n] = bstr.charCodeAt(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 返回 File 对象
|
|
||||||
return new File([u8arr], fileName, { type: mime })
|
|
||||||
}
|
|
||||||
|
|
||||||
const getServerVideo = async () => {
|
|
||||||
const params = {
|
|
||||||
data_names: ["6874b86e341d1d0e4c75d0d5.h264"],
|
|
||||||
data_type: 0,
|
|
||||||
project_id: 9,
|
|
||||||
}
|
|
||||||
let text
|
|
||||||
const response = await getServerImage(params)
|
|
||||||
text = `data:video/h264;base64,${response}`
|
|
||||||
|
|
||||||
const fileRes = videoBase64ToFile(text, "6874b86e341d1d0e4c75d0d5.h264")
|
|
||||||
|
|
||||||
// const videoFile = new File([response], "6874b86e341d1d0e4c75d0d5.264", {
|
|
||||||
// type: "video/264",
|
|
||||||
// })
|
|
||||||
setFile(fileRes)
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadFile(file: File | null) {
|
|
||||||
if (!file) return
|
|
||||||
// 创建一个临时的 DOM URL 指向内存中的文件
|
|
||||||
const url = URL.createObjectURL(file)
|
|
||||||
|
|
||||||
// 创建一个隐藏的 a 标签
|
|
||||||
const a = document.createElement("a")
|
|
||||||
a.style.display = "none"
|
|
||||||
a.href = url
|
|
||||||
a.download = file.name // 设置下载后的文件名
|
|
||||||
|
|
||||||
// 将标签添加到文档中并触发点击
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
|
|
||||||
// 清理工作:移除标签并释放内存 URL
|
|
||||||
document.body.removeChild(a)
|
|
||||||
window.URL.revokeObjectURL(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
const processVideo = () => {
|
|
||||||
if (!file || !loaded || !workerRef.current) return
|
|
||||||
|
|
||||||
setProcessing(true)
|
|
||||||
setProgress(0)
|
|
||||||
extractedCountRef.current = 0
|
|
||||||
for (const img of images) URL.revokeObjectURL(img.url)
|
|
||||||
setImages([])
|
|
||||||
setLogs([])
|
|
||||||
setError(null)
|
|
||||||
setMessage("Starting processing...")
|
|
||||||
|
|
||||||
// Extract 1 frame per second: -vf fps=1
|
|
||||||
// Optimize: Scale down to 720p max width to save memory
|
|
||||||
workerRef.current.postMessage({
|
|
||||||
type: "EXEC",
|
|
||||||
file,
|
|
||||||
args: [
|
|
||||||
"-vf",
|
|
||||||
"fps=1,scale='min(720,iw)':-2:flags=lanczos+accurate_rnd+full_chroma_int,unsharp=5:5:0.5:3:3:0",
|
|
||||||
"-q:v",
|
|
||||||
"2",
|
|
||||||
"frame_%03d.jpg",
|
|
||||||
],
|
|
||||||
outputPattern: "frame_",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadAll = () => {
|
|
||||||
images.forEach((img, index) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const a = document.createElement("a")
|
|
||||||
a.href = img.url
|
|
||||||
a.download = img.name
|
|
||||||
a.click()
|
|
||||||
}, index * 200)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container size="lg" py="xl">
|
|
||||||
<Stack gap="lg">
|
|
||||||
<Title order={2}>Video Frame Extractor (Worker + Local WASM)</Title>
|
|
||||||
<Text c="dimmed">
|
|
||||||
Extract frames from your video files purely in the browser using
|
|
||||||
FFmpeg.wasm (Async Worker).
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Alert
|
|
||||||
icon={<IconAlertCircle size={16} />}
|
|
||||||
title="Error"
|
|
||||||
color="red"
|
|
||||||
withCloseButton
|
|
||||||
onClose={() => setError(null)}>
|
|
||||||
{error}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Card withBorder shadow="sm" p="lg" radius="md">
|
|
||||||
<Flex gap={"md"}>
|
|
||||||
<Button onClick={getServerVideo}>getServerVideo</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
downloadFile(file)
|
|
||||||
}}>
|
|
||||||
downloadFile
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Stack gap="md">
|
|
||||||
<FileInput
|
|
||||||
label="Select Video File"
|
|
||||||
placeholder="Click to select MP4, WebM..."
|
|
||||||
// accept="video/*"
|
|
||||||
leftSection={<IconMovie size={16} />}
|
|
||||||
value={file}
|
|
||||||
onChange={setFile}
|
|
||||||
disabled={!loaded || processing}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!loaded && isLoading && (
|
|
||||||
<Group>
|
|
||||||
<Loader size="sm" />
|
|
||||||
<Text size="sm">Loading FFmpeg core...</Text>
|
|
||||||
</Group>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{loaded && (
|
|
||||||
<Group justify="flex-end">
|
|
||||||
<Button
|
|
||||||
onClick={processVideo}
|
|
||||||
disabled={!file || processing}
|
|
||||||
loading={processing}
|
|
||||||
leftSection={<IconPhoto size={16} />}>
|
|
||||||
{processing ? "Processing..." : "Start Extraction"}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{processing && (
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Text size="sm">{message}</Text>
|
|
||||||
<Progress value={progress} animated />
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{logs.length > 0 && (
|
|
||||||
<Card withBorder shadow="sm" p="xs" radius="md" bg="gray.0">
|
|
||||||
<Text size="xs" fw={500} mb="xs">
|
|
||||||
Logs
|
|
||||||
</Text>
|
|
||||||
<ScrollArea h={100} type="always" viewportRef={messageRef}>
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
fontFamily: "monospace",
|
|
||||||
fontSize: "11px",
|
|
||||||
}}>
|
|
||||||
{logs.map((log, i) => (
|
|
||||||
<div key={i}>{log}</div>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</ScrollArea>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{images.length > 0 && (
|
|
||||||
<Stack gap="md">
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Title order={3}>Extracted Frames ({images.length})</Title>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
leftSection={<IconDownload size={16} />}
|
|
||||||
onClick={downloadAll}>
|
|
||||||
Download All
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 2, sm: 3, md: 4 }} spacing="md">
|
|
||||||
{images.map((img) => (
|
|
||||||
<Card
|
|
||||||
key={img.name}
|
|
||||||
shadow="sm"
|
|
||||||
padding="xs"
|
|
||||||
radius="md"
|
|
||||||
withBorder>
|
|
||||||
<Card.Section>
|
|
||||||
<Image
|
|
||||||
src={img.url}
|
|
||||||
height={160}
|
|
||||||
alt={img.name}
|
|
||||||
fit="cover"
|
|
||||||
/>
|
|
||||||
</Card.Section>
|
|
||||||
<Group justify="space-between" mt="xs" mb="xs">
|
|
||||||
<Text fw={500} size="xs" truncate>
|
|
||||||
{img.name}
|
|
||||||
</Text>
|
|
||||||
<ActionIcon
|
|
||||||
variant="light"
|
|
||||||
color="blue"
|
|
||||||
component="a"
|
|
||||||
href={img.url}
|
|
||||||
download={img.name}>
|
|
||||||
<IconDownload size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { appendCapped } from "./cap"
|
|
||||||
|
|
||||||
describe("appendCapped", () => {
|
|
||||||
test("keeps all items when under max", () => {
|
|
||||||
expect(appendCapped([1, 2], [3], 10)).toEqual([1, 2, 3])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("drops from head when exceeding max", () => {
|
|
||||||
expect(appendCapped([1, 2], [3, 4, 5], 3)).toEqual([3, 4, 5])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("calls onDrop for dropped items", () => {
|
|
||||||
const dropped: number[] = []
|
|
||||||
const result = appendCapped([1, 2], [3, 4, 5], 3, (x) => dropped.push(x))
|
|
||||||
expect(result).toEqual([3, 4, 5])
|
|
||||||
expect(dropped).toEqual([1, 2])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
export const appendCapped = <T>(
|
|
||||||
prev: T[],
|
|
||||||
next: T[],
|
|
||||||
max: number,
|
|
||||||
onDrop?: (item: T) => void
|
|
||||||
) => {
|
|
||||||
const merged = [...prev, ...next]
|
|
||||||
if (merged.length <= max) return merged
|
|
||||||
const dropCount = merged.length - max
|
|
||||||
if (onDrop) {
|
|
||||||
for (let i = 0; i < dropCount; i += 1) onDrop(merged[i])
|
|
||||||
}
|
|
||||||
return merged.slice(dropCount)
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import VideoFrameExtractor from "./VideoFrameExtractor"
|
|
||||||
|
|
||||||
export default function ComponentLabelFfmpegPage() {
|
|
||||||
return <VideoFrameExtractor />
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { userLogin } from "@/components/label/api/label"
|
|
||||||
import { usePermissionStore } from "@/components/label/store/auth"
|
|
||||||
import { Button } from "@mantine/core"
|
|
||||||
|
|
||||||
export default function ComponentLabelLoginPage() {
|
|
||||||
const { setUserInfo, setUserPassword } = usePermissionStore()
|
|
||||||
const user_id = usePermissionStore.getState().user_id
|
|
||||||
|
|
||||||
// 用户登录
|
|
||||||
const handleUserLoginBtnClick = async () => {
|
|
||||||
try {
|
|
||||||
const res = await userLogin({
|
|
||||||
name: "admin",
|
|
||||||
password: "123456",
|
|
||||||
})
|
|
||||||
const { uid, name, token, refresh_token } = res
|
|
||||||
setUserInfo({
|
|
||||||
user_id: uid,
|
|
||||||
user_name: name,
|
|
||||||
token,
|
|
||||||
refresh_token,
|
|
||||||
detailInfo: res,
|
|
||||||
})
|
|
||||||
setUserPassword("123456")
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button onClick={handleUserLoginBtnClick}>Login</Button>
|
|
||||||
user_id:{user_id}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import LabelContent from "@/components/label"
|
|
||||||
import { useAppLayoutStore } from "@/components/layout/store"
|
|
||||||
import { Breadcrumbs, Flex, Paper, Stack, UnstyledButton } from "@mantine/core"
|
|
||||||
import { useMemo } from "react"
|
|
||||||
|
|
||||||
export default function ComponentLabelPicturePage() {
|
|
||||||
const items = [{ title: "地图展示" }].map((item) => (
|
|
||||||
<UnstyledButton key={item.title}>{item.title}</UnstyledButton>
|
|
||||||
))
|
|
||||||
const { isOpen } = useAppLayoutStore()
|
|
||||||
let leftWidth = useMemo(() => {
|
|
||||||
let width = 69
|
|
||||||
return !isOpen ? width : width + 160
|
|
||||||
}, [isOpen])
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Stack w="100%" h="calc(100vh - 56px)" p={"md"}>
|
|
||||||
<Flex justify={"space-between"} align={"center"} h={"16px"}>
|
|
||||||
<Breadcrumbs>{items}</Breadcrumbs>
|
|
||||||
</Flex>
|
|
||||||
<Paper
|
|
||||||
shadow="xs"
|
|
||||||
p="0"
|
|
||||||
flex={1}
|
|
||||||
display="flex"
|
|
||||||
style={{
|
|
||||||
overflow: "hidden",
|
|
||||||
}}>
|
|
||||||
<LabelContent
|
|
||||||
headerHeight={128}
|
|
||||||
leftWidth={leftWidth}
|
|
||||||
project_id={1}
|
|
||||||
task_id={3}
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
</Stack>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import LabelContent from "@/components/label"
|
|
||||||
|
|
||||||
export default function ComponentLabelPictureStandalonePage() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<LabelContent headerHeight={0} leftWidth={0} project_id={1} task_id={3} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import AppLayout from "@/components/layout/AppLayout"
|
|
||||||
import { PathnameRecorder } from "@/components/layout/PathnameRecorder"
|
|
||||||
import { componentList, showList } from "@/components/layout/common"
|
|
||||||
|
|
||||||
async function getMenu() {
|
|
||||||
return [...componentList, ...showList]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function Layout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
const menu = await getMenu()
|
|
||||||
return (
|
|
||||||
<AppLayout menu={menu}>
|
|
||||||
{children}
|
|
||||||
<PathnameRecorder menu={menu} />
|
|
||||||
</AppLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
export default function ComponentMediaDynamicPage() {
|
|
||||||
return <>ComponentMediaDynamicPage</>
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
export default function ComponentMediaStaticPage() {
|
|
||||||
return <>ComponentMediaStaticPage</>
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useRouter } from "next/navigation"
|
|
||||||
import { useEffect } from "react"
|
|
||||||
|
|
||||||
export default function ComponentPage() {
|
|
||||||
const router = useRouter()
|
|
||||||
useEffect(() => {
|
|
||||||
router.push("/component/base/department")
|
|
||||||
}, [router])
|
|
||||||
return <></>
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
export default function ComponentTableBasePage() {
|
|
||||||
return <>ComponentTableBasePage</>
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
} from "react"
|
} from "react"
|
||||||
import ScaleComponent from "./components/ScaleComponent"
|
import ScaleComponent from "./components/ScaleComponent"
|
||||||
|
|
||||||
import { processVideo } from "@/app/component/label/video2image/processVideo"
|
|
||||||
import { Box, Button, Flex, LoadingOverlay, Stack } from "@mantine/core"
|
import { Box, Button, Flex, LoadingOverlay, Stack } from "@mantine/core"
|
||||||
import { notifications, showNotification } from "@mantine/notifications"
|
import { notifications, showNotification } from "@mantine/notifications"
|
||||||
import { getLabelResult, getServerImage } from "./api/label"
|
import { getLabelResult, getServerImage } from "./api/label"
|
||||||
@@ -35,6 +34,7 @@ import RightQATools from "./components/RightQATools"
|
|||||||
import RightTaskTools from "./components/RightTaskTools"
|
import RightTaskTools from "./components/RightTaskTools"
|
||||||
import ScaleToolContainer from "./components/ScaleToolContainer"
|
import ScaleToolContainer from "./components/ScaleToolContainer"
|
||||||
import TopTools from "./components/TopTools"
|
import TopTools from "./components/TopTools"
|
||||||
|
import { processVideo } from "./ffmpeg/processVideo"
|
||||||
import {
|
import {
|
||||||
useImagesStore,
|
useImagesStore,
|
||||||
useKeyEventStore,
|
useKeyEventStore,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import type { WorkerMessage, WorkerResponse } from "../ffmpeg/ffmpeg.worker"
|
import type { WorkerMessage, WorkerResponse } from "./ffmpeg.worker"
|
||||||
|
|
||||||
export interface VideoFrameFile {
|
export interface VideoFrameFile {
|
||||||
name: string
|
name: string
|
||||||
@@ -161,9 +161,7 @@ export const processVideo = async (
|
|||||||
runTimeoutMs: number,
|
runTimeoutMs: number,
|
||||||
runFirstFrameTimeoutMs: number
|
runFirstFrameTimeoutMs: number
|
||||||
) => {
|
) => {
|
||||||
const worker = new Worker(
|
const worker = new Worker(new URL("./ffmpeg.worker.ts", import.meta.url))
|
||||||
new URL("../ffmpeg/ffmpeg.worker.ts", import.meta.url)
|
|
||||||
)
|
|
||||||
const outputExt = getOutputExtFromArgs(runArgs)
|
const outputExt = getOutputExtFromArgs(runArgs)
|
||||||
let frameIndex = 0
|
let frameIndex = 0
|
||||||
const frames: VideoFrameFile[] = []
|
const frames: VideoFrameFile[] = []
|
||||||
Reference in New Issue
Block a user