Merge branch 'dev' into 'main'

fix(video): develop

See merge request uirepo/labelui!2
This commit is contained in:
刘耀勇
2026-02-12 17:55:08 +08:00
25 changed files with 229 additions and 54 deletions

View File

@@ -1,5 +1,8 @@
NEXT_PUBLIC_BASE_PATH=""
NEXT_PUBLIC_ENV=development
# redis
# NEXT_PUBLIC_REDIS_URL="172.16.112.128"
NEXT_PUBLIC_REDIS_URL="192.168.193.237"

View File

@@ -1,5 +1,7 @@
NEXT_PUBLIC_BASE_PATH=""
NEXT_PUBLIC_ENV=production
# redis
NEXT_PUBLIC_REDIS_URL="172.16.105.154"
NEXT_PUBLIC_REDIS_PORT=8015

View File

@@ -1,5 +1,7 @@
NEXT_PUBLIC_BASE_PATH=""
NEXT_PUBLIC_ENV=staging
# redis
NEXT_PUBLIC_REDIS_URL="172.16.112.128"
NEXT_PUBLIC_REDIS_PORT=9765

View File

@@ -6,6 +6,7 @@ import {
Card,
Container,
FileInput,
Flex,
Group,
Image,
Loader,
@@ -25,6 +26,7 @@ import {
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)
@@ -155,6 +157,69 @@ export default function VideoFrameExtractor() {
}
}, [])
/**
* 将视频 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
@@ -215,11 +280,21 @@ export default function VideoFrameExtractor() {
)}
<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/*"
// accept="video/*"
leftSection={<IconMovie size={16} />}
value={file}
onChange={setFile}

View File

@@ -61,7 +61,7 @@ ctx.onmessage = async (event: MessageEvent<WorkerMessage>) => {
})
const mountDir = "/input_mount"
const inputPath = `${mountDir}/${file.name}`
let inputPath = `${mountDir}/${file.name}`
const mountInput = async () => {
await ffmpeg.createDir(mountDir)
@@ -69,12 +69,64 @@ ctx.onmessage = async (event: MessageEvent<WorkerMessage>) => {
}
const unmountInput = async () => {
await ffmpeg.unmount(mountDir).catch(() => {})
await ffmpeg.deleteDir(mountDir).catch(() => {})
await ffmpeg.unmount(mountDir).catch((err) => {
console.log("unmounterr", err)
})
await ffmpeg.deleteDir(mountDir).catch((err) => {
console.log("deleteDir", err)
})
}
await mountInput()
const isRawH264 =
file.name.toLowerCase().endsWith(".264") ||
file.name.toLowerCase().endsWith(".h264")
if (isRawH264) {
ctx.postMessage({
type: "LOG",
message: "[fix] 检测到 H.264 裸流,正在封装为 MP4 以修复索引...",
})
const remuxedPath = "fixed_container.mp4"
try {
// -f h264: 强制指定输入格式为 h264 裸流
// -i ... : 输入
// -c copy: 直接复制流,不转码(速度极快)
// -f mp4 : 输出为 MP4 容器
const ret = await ffmpeg.exec([
"-f",
"h264",
"-i",
inputPath,
"-c",
"copy",
remuxedPath,
])
if (ret === 0) {
ctx.postMessage({
type: "LOG",
message: "[fix] 封装成功,切换输入源",
})
// 关键:将后续操作的输入路径指向新生成的 MP4
inputPath = remuxedPath
} else {
ctx.postMessage({
type: "LOG",
message: "[fix] 封装失败,尝试继续使用原始文件...",
})
}
} catch (e) {
ctx.postMessage({
type: "LOG",
message: `[fix] 预处理出错: ${String(e)}`,
})
}
}
try {
const normalizeError = (e: unknown) => {
if (typeof e === "string") return e
@@ -145,7 +197,9 @@ ctx.onmessage = async (event: MessageEvent<WorkerMessage>) => {
message: `[probe] ffprobe failed: ${normalizeError(e)}`,
})
} finally {
await ffmpeg.deleteFile(durationProbePath).catch(() => {})
await ffmpeg.deleteFile(durationProbePath).catch((err) => {
console.log("deleteFile error", err)
})
}
if (!durationSec) {
@@ -388,7 +442,9 @@ ctx.onmessage = async (event: MessageEvent<WorkerMessage>) => {
)}`,
})
}
await ffmpeg.deleteFile(outputName).catch(() => {})
await ffmpeg.deleteFile(outputName).catch((err) => {
console.log("deleteFile error", err)
})
index += 1
continue
}
@@ -397,7 +453,9 @@ ctx.onmessage = async (event: MessageEvent<WorkerMessage>) => {
const raw = (await ffmpeg.readFile(outputName)) as Uint8Array
const copy = raw.slice()
sentBytes += copy.byteLength
await ffmpeg.deleteFile(outputName).catch(() => {})
await ffmpeg.deleteFile(outputName).catch((err) => {
console.log("deleteFile error", err)
})
ctx.postMessage(
{
@@ -484,13 +542,10 @@ ctx.onmessage = async (event: MessageEvent<WorkerMessage>) => {
})
}
} catch (e2) {
let flag = typeof e2 === "string"
ctx.postMessage({
type: "LOG",
message: `[fatal] reload failed: ${
typeof e2 === "string"
? e2
: (e2 as any)?.message || String(e2)
}`,
message: `[fatal] reload failed: ${flag ? e2 : (e2 as any)?.message || String(e2)}`,
})
}

View File

@@ -1,5 +1,9 @@
// src/workers/ffmpeg.worker.ts
import type { LibAVWrapper, WorkerMessage, WorkerResponse } from "./types/libav"
// app/component/label/video/libav.worker.ts
import type {
LibAVWrapper,
WorkerMessage,
WorkerResponse,
} from "@/app/component/label/video/types/libav"
const ctx: Worker = self as any
let libav: any = null // Use any to access full API

View File

@@ -1,7 +1,10 @@
"use client"
import { ChangeEvent, useEffect, useRef, useState } from "react"
import type { WorkerMessage, WorkerResponse } from "./types/libav"
import type {
WorkerMessage,
WorkerResponse,
} from "@/app/component/label/video/types/libav"
export default function Home() {
const workerRef = useRef<Worker | null>(null)

View File

@@ -500,7 +500,7 @@ export default function ProjectAuditPage() {
onChange={(e) =>
setFilters((s) => ({
...s,
create_at_start: e.currentTarget.value,
create_at_start: e.target.value,
}))
}
/>
@@ -510,12 +510,12 @@ export default function ProjectAuditPage() {
radius="xs"
type="date"
value={filters.create_at_end}
onChange={(e) =>
onChange={(e) => {
setFilters((s) => ({
...s,
create_at_end: e.currentTarget.value,
create_at_end: e.target.value,
}))
}
}}
/>
</SimpleGrid>
</Stack>

View File

@@ -71,15 +71,15 @@ pnpm run $BUILD_FLAG
if [[ $? != 0 ]]; then echo -e "${RED}build error${NC}"; exit 100; fi
# Docker构建和推送
MAIN_NAME=digitalspace_webside
MAIN_NAME=digitalspace_labelui
DOCKER_FILE=$SHELL_FOLDER/Dockerfile
if [[ $OUTPUT_TYPE == export ]]; then
DOCKER_FILE=$SHELL_FOLDER/docker/nginx.dockerfile
MAIN_NAME=digitalspace_webside_static
MAIN_NAME=digitalspace_labelui_static
elif [[ $OUTPUT_TYPE == standalone ]]; then
DOCKER_FILE=$SHELL_FOLDER/docker/node.dockerfile
MAIN_NAME=digitalspace_webside_ssr
MAIN_NAME=digitalspace_labelui_ssr
fi
echo -e "${GREEN}Building docker image...${NC}"

View File

@@ -0,0 +1,4 @@
export const BASE_LABEL_API =
process.env.NEXT_PUBLIC_ENV === "production"
? "https://label.cowarobot.com"
: "http://172.16.112.8:9110"

View File

@@ -1,7 +1,7 @@
import httpFetch from "@/api/fetch"
import { usePermissionStore } from "../../store/auth"
import { BASE_LABEL_API } from "../label"
import { Daily } from "./typing"
import { BASE_LABEL_API } from "../const"
export const getDailyWorkList = (data: Daily.Request) => {
return httpFetch<Daily.Response>({

View File

@@ -6,8 +6,7 @@ import {
RequestLabelResult,
ServerResponse,
} from "./typing"
export const BASE_LABEL_API = "https://label.cowarobot.com"
import { BASE_LABEL_API } from "../const"
// 获取标注结果
export const getLabelResult = (task_id: number) => {
@@ -58,7 +57,9 @@ export const getServerImage = async (data: {
} else {
if (res.data_list && res.data_list.length) {
const { image_data } = res.data_list[0]
resolve({ image_data })
resolve({
image_data,
})
}
}
})

View File

@@ -1,7 +1,7 @@
import httpFetch from "@/api/fetch"
import { usePermissionStore } from "../../store/auth"
import { BASE_LABEL_API } from "../label"
import { Project, ProjectDetail, ReviewUsers } from "./typing"
import { BASE_LABEL_API } from "../const"
// 获取项目列表
export const getProjectList = (data: Project.ListRequest) => {

View File

@@ -1,7 +1,7 @@
import httpFetch from "@/api/fetch"
import { usePermissionStore } from "../../store/auth"
import { BASE_LABEL_API } from "../label"
import { Scheme } from "./typing"
import { BASE_LABEL_API } from "../const"
// 获取标注方案列表
export const getLabelSchemeList = (params: Scheme.ListRequest) => {

View File

@@ -1,7 +1,7 @@
import httpFetch from "@/api/fetch"
import { usePermissionStore } from "../../store/auth"
import { BASE_LABEL_API } from "../label"
import { Task } from "./typing"
import { BASE_LABEL_API } from "../const"
// 获取任务列表
export const getTaskList = (data: Task.ListRequest) => {

View File

@@ -2,10 +2,15 @@ import httpFetch from "@/api/fetch"
import { ResultData } from "@/api/typing"
import { Login } from "./types"
const BASE_PATH =
process.env.NEXT_PUBLIC_ENV === "production"
? "https://basis.soft.cowarobot.com"
: "https://basis.softtest.cowarobot.cn"
// 获取验证码
export const getVerifyCode = (params: { phone: string }) => {
return httpFetch<ResultData<{}>>({
url: `/api/v1/basis/login/get_verify_code`,
url: BASE_PATH + `/api/v1/basis/login/get_verify_code`,
method: "POST",
body: JSON.stringify(params),
})
@@ -14,7 +19,7 @@ export const getVerifyCode = (params: { phone: string }) => {
// 账户登录
export const passwdLogin = (params: Login.ReqPasswdLogin) => {
return httpFetch<ResultData<Login.ResLogin>>({
url: `/api/v1/basis/login/passwd`,
url: BASE_PATH + `/api/v1/basis/login/passwd`,
method: "POST",
body: JSON.stringify(params),
isLogin: true,
@@ -24,7 +29,7 @@ export const passwdLogin = (params: Login.ReqPasswdLogin) => {
// 验证码登录
export const verificationLogin = (params: Login.ReqVerificationLogin) => {
return httpFetch<ResultData<Login.ResLogin>>({
url: `/api/v1/basis/login/verification`,
url: BASE_PATH + `/api/v1/basis/login/verification`,
method: "POST",
body: JSON.stringify(params),
isLogin: true,
@@ -34,7 +39,7 @@ export const verificationLogin = (params: Login.ReqVerificationLogin) => {
// 飞书登录
export const fetchFeishuLogin = (params: Login.ReqFeishuLogin) => {
return httpFetch<ResultData<Login.ResLogin>>({
url: `/api/v1/basis/login/feishu`,
url: BASE_PATH + `/api/v1/basis/login/feishu`,
method: "POST",
body: JSON.stringify(params),
isLogin: true,
@@ -44,7 +49,7 @@ export const fetchFeishuLogin = (params: Login.ReqFeishuLogin) => {
// 刷新token
export const refreshKey = (params: { token: string }) => {
return httpFetch<ResultData<Login.ResRefreshKey>>({
url: `/api/v1/basis/key/refresh`,
url: BASE_PATH + `/api/v1/basis/key/refresh`,
method: "POST",
body: JSON.stringify(params),
isRefresh: true,
@@ -59,3 +64,11 @@ export const deleteCookie = () => {
isDeleteCookie: true,
})
}
// /api/v1/label_server/user/permission
export const getUserPermission = () => {
return httpFetch<any>({
url: `/api/v1/label_server/user/permission`,
method: "GET",
})
}

View File

@@ -4,7 +4,7 @@ import { useLoginStore } from "@/components/login/store"
import { useRouter } from "next/navigation"
import { APP, PLATFORM, TENANT } from "@/components/login/libs/common"
import { APP_ID, APP_SECRECT } from "./util"
import { fetchFeishuLogin } from "../api"
import { fetchFeishuLogin, getUserPermission } from "../api"
const HeaderIcon = DigitalIcon
@@ -60,6 +60,8 @@ const FeishuAutoLogin = ({ useUserStore }: { useUserStore: any }) => {
access_token: info.access_token || "",
refresh_token: info.refresh_token || "",
})
const permission = await getUserPermission()
console.log(permission)
window.location.href = "/"
} catch (err) {
console.log(err)

View File

@@ -9,7 +9,7 @@ import {
TENANT,
getQueryVariable,
} from "@/components/login/libs/common"
import { fetchFeishuLogin } from "../api"
import { fetchFeishuLogin, getUserPermission } from "../api"
const QrLogin = ({ useUserStore }: { useUserStore: any }) => {
const fingerprint = useLoginStore.getState().fingerprint
@@ -57,6 +57,8 @@ const QrLogin = ({ useUserStore }: { useUserStore: any }) => {
access_token: info.access_token || "",
refresh_token: info.refresh_token || "",
})
const permission = await getUserPermission()
console.log(permission)
window.location.href = "/"
} catch (err) {
console.log(err)

View File

@@ -26,7 +26,12 @@ import {
import { useForm } from "@mantine/form"
import { useDisclosure } from "@mantine/hooks"
import { useCallback, useEffect, useMemo, useState } from "react"
import { getVerifyCode, passwdLogin, verificationLogin } from "./api"
import {
getUserPermission,
getVerifyCode,
passwdLogin,
verificationLogin,
} from "./api"
import QrLogin from "./feishu/qr-login"
import { APP, PLATFORM, TENANT } from "./libs/common"
import { DigitalIcon } from "./resource/icons/digital"
@@ -204,6 +209,8 @@ export default function LoginPage({ useUserStore }: { useUserStore: any }) {
access_token: info.access_token || "",
refresh_token: info.refresh_token || "",
})
const permission = await getUserPermission()
console.log(permission)
window.location.href = "/"
} catch (error) {
if (error) {
@@ -295,6 +302,8 @@ export default function LoginPage({ useUserStore }: { useUserStore: any }) {
access_token: info.access_token || "",
refresh_token: info.refresh_token || "",
})
const permission = await getUserPermission()
console.log(permission)
window.location.href = "/"
} catch (err) {
const message = err instanceof Error ? err.message : "登录失败,请重试"

View File

@@ -17,5 +17,5 @@ export const getQueryVariable = (variable: string) => {
}
export const APP = "basis"
export const PLATFORM = "basis"
export const PLATFORM = "标注平台"
export const TENANT = "cowarobot"

View File

@@ -32,7 +32,7 @@ http {
gzip_comp_level 6;
server {
listen 5521;
listen 5531;
server_name localhost;
#access_log /var/log/nginx/host.access.log main;

View File

@@ -5,4 +5,4 @@ ADD ./docker/nginx.conf /etc/nginx/nginx.conf
RUN chmod -R 777 /usr/share/nginx/html
ENV PORT 5521
ENV PORT 5531

View File

@@ -7,10 +7,10 @@ COPY ./.next/standalone ./standalone
COPY ./public /app/standalone/public
COPY ./.next/static /app/standalone/.next/static
ENV PORT 5521
ENV PORT 5531
ENV NEXT_TELEMETRY_DISABLED 1
EXPOSE 5521
EXPOSE 5531
CMD ["node", "./standalone/server.js"]

View File

@@ -1,18 +1,18 @@
{
"name": "webside",
"name": "labelui",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "NEXT_PUBLIC_URL=https://frontside2.soft.cowarobot.com NEXT_PUBLIC_OUTPUT_TYPE=standalone next dev --turbopack -p 5521",
"dev:export": "NEXT_PUBLIC_URL=https://basis.softtest.cowarobot.com NEXT_PUBLIC_OUTPUT_TYPE=export next dev --turbopack -p 5521",
"dev:standalone": "NEXT_PUBLIC_URL=https://frontside2.soft.cowarobot.com NEXT_PUBLIC_OUTPUT_TYPE=standalone next dev next dev --turbopack -p 5521",
"dev": "NEXT_PUBLIC_URL=https://frontside2.soft.cowarobot.com NEXT_PUBLIC_OUTPUT_TYPE=standalone next dev --turbopack -p 5531",
"dev:export": "NEXT_PUBLIC_URL=http://172.16.112.8:9110 NEXT_PUBLIC_OUTPUT_TYPE=export next dev --turbopack -p 5531",
"dev:standalone": "NEXT_PUBLIC_URL=https://frontside2.soft.cowarobot.com NEXT_PUBLIC_OUTPUT_TYPE=standalone next dev next dev --turbopack -p 5531",
"build:standalone-stage": "NEXT_PUBLIC_URL=https://frontside2.soft.cowarobot.com NEXT_PUBLIC_OUTPUT_TYPE=standalone env-cmd -f .env.staging next build --turbopack",
"prebuild:export-stage": "node scripts/disable-api-route.mjs",
"build:export-stage": "NEXT_PUBLIC_URL=https://basis.softtest.cowarobot.com NEXT_PUBLIC_OUTPUT_TYPE=export env-cmd -f .env.staging next build --turbopack",
"build:export-stage": "NEXT_PUBLIC_URL=http://172.16.112.8:9110 NEXT_PUBLIC_OUTPUT_TYPE=export env-cmd -f .env.staging next build --turbopack",
"postbuild:export-stage": "node scripts/restore-api-route.mjs",
"build:standalone-prod": "NEXT_PUBLIC_URL=https://frontside2.soft.cowarobot.com NEXT_PUBLIC_OUTPUT_TYPE=standalone env-cmd -f .env.production next build --turbopack",
"prebuild:export-prod": "node scripts/disable-api-route.mjs",
"build:export-prod": "NEXT_PUBLIC_URL=https://basis.softtest.cowarobot.com NEXT_PUBLIC_OUTPUT_TYPE=export env-cmd -f .env.production next build --turbopack",
"build:export-prod": "NEXT_PUBLIC_URL=http://172.16.112.8:9110 NEXT_PUBLIC_OUTPUT_TYPE=export env-cmd -f .env.production next build --turbopack",
"postbuild:export-prod": "node scripts/restore-api-route.mjs",
"start": "next start",
"test": "jest",

View File

@@ -1,18 +1,18 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "webside",
"productName": "labelui",
"version": "0.1.0",
"identifier": "com.webside.dev",
"identifier": "com.labelui.dev",
"build": {
"frontendDist": "../out",
"devUrl": "http://localhost:5521",
"devUrl": "http://localhost:5531",
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build:export-stage"
},
"app": {
"windows": [
{
"title": "webside",
"title": "labelui",
"width": 800,
"height": 600,
"resizable": true,