fix live analysis multi-device lock

这个提交包含在:
cryptocommuniums-afk
2026-03-16 18:05:58 +08:00
父节点 13e59b8e8a
当前提交 f9db6ef590
修改 7 个文件,包含 221 行新增28 行删除

查看文件

@@ -189,6 +189,10 @@ function summarizeActions(actionSummary: Record<ActionType, number>) {
export default function Recorder() {
const { user } = useAuth();
const utils = trpc.useUtils();
const runtimeQuery = trpc.analysis.runtimeGet.useQuery(undefined, {
refetchInterval: 1000,
refetchIntervalInBackground: true,
});
const finalizeTaskMutation = trpc.task.createMediaFinalize.useMutation({
onSuccess: (data) => {
setArchiveTaskId(data.taskId);
@@ -262,6 +266,9 @@ export default function Recorder() {
const mobile = useMemo(() => isMobileDevice(), []);
const mimeType = useMemo(() => pickRecorderMimeType(), []);
const runtimeRole = runtimeQuery.data?.role ?? "idle";
const liveAnalysisRuntime = runtimeQuery.data?.runtimeSession;
const liveAnalysisOccupied = runtimeRole === "viewer" && liveAnalysisRuntime?.status === "active";
const currentPlaybackUrl = mediaSession?.playback.mp4Url || mediaSession?.playback.webmUrl || "";
const archiveTaskQuery = useBackgroundTask(archiveTaskId);
const archiveProgress = archiveTaskQuery.data?.progress ?? getArchiveProgress(mediaSession);
@@ -402,6 +409,11 @@ export default function Recorder() {
preferredZoom = zoomTargetRef.current,
preset: keyof typeof QUALITY_PRESETS = qualityPreset,
) => {
if (liveAnalysisOccupied) {
const title = liveAnalysisRuntime?.title || "其他设备正在实时分析";
toast.error(`${title},当前设备不能再开启录制摄像头`);
throw new Error("当前账号已有其他设备正在实时分析");
}
try {
if (streamRef.current) {
streamRef.current.getTracks().forEach((track) => track.stop());
@@ -440,7 +452,7 @@ export default function Recorder() {
toast.error(`摄像头启动失败: ${message}`);
throw error;
}
}), [facingMode, mobile, qualityPreset, syncZoomState]);
}), [facingMode, liveAnalysisOccupied, liveAnalysisRuntime?.title, mobile, qualityPreset, syncZoomState]);
const ensurePreviewStream = useCallback(async () => {
if (streamRef.current) {
@@ -849,6 +861,11 @@ export default function Recorder() {
toast.error("请先登录后再开始录制");
return;
}
if (liveAnalysisOccupied) {
const title = liveAnalysisRuntime?.title || "其他设备正在实时分析";
toast.error(`${title},当前设备不能同时开始录制`);
return;
}
try {
setMode("preparing");
@@ -898,7 +915,21 @@ export default function Recorder() {
setMode("idle");
toast.error(`启动录制失败: ${error?.message || "未知错误"}`);
}
}, [ensurePreviewStream, facingMode, mimeType, mobile, qualityPreset, startActionSampling, startRealtimePush, startRecorderLoop, syncSessionState, title, user]);
}, [
ensurePreviewStream,
facingMode,
liveAnalysisOccupied,
liveAnalysisRuntime?.title,
mimeType,
mobile,
qualityPreset,
startActionSampling,
startRealtimePush,
startRecorderLoop,
syncSessionState,
title,
user,
]);
const finishRecording = useCallback(async () => {
const session = currentSessionRef.current;
@@ -1140,9 +1171,10 @@ export default function Recorder() {
data-testid="recorder-start-camera-button"
onClick={() => void startCamera()}
className={buttonClass()}
disabled={liveAnalysisOccupied}
>
<Camera className={iconClass} />
{labelFor("启动摄像头", "启动")}
{labelFor(liveAnalysisOccupied ? "实时分析占用中" : "启动摄像头", liveAnalysisOccupied ? "占用" : "启动")}
</Button>
) : (
<>
@@ -1150,9 +1182,10 @@ export default function Recorder() {
data-testid="recorder-start-recording-button"
onClick={() => void beginRecording()}
className={buttonClass("record")}
disabled={liveAnalysisOccupied}
>
<Circle className={`${iconClass} ${rail ? "fill-current" : "fill-current"}`} />
{labelFor("开始录制", "录制")}
{labelFor(liveAnalysisOccupied ? "实时分析占用中" : "开始录制", liveAnalysisOccupied ? "占用" : "录制")}
</Button>
<Button variant="outline" onClick={stopCamera} className={buttonClass("outline")}>
<VideoOff className={iconClass} />
@@ -1362,6 +1395,23 @@ export default function Recorder() {
</Alert>
) : null}
{liveAnalysisOccupied ? (
<Alert className="border-amber-300/70 bg-amber-50 text-amber-950">
<ShieldAlert className="h-4 w-4" />
<AlertTitle></AlertTitle>
<AlertDescription>
{liveAnalysisRuntime?.title || "其他设备正在实时分析"}
{" "}
<a href="/live-camera" className="font-medium underline underline-offset-4">
</a>
{" "}
</AlertDescription>
</Alert>
) : null}
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.7fr)_minmax(340px,0.9fr)]">
<section className="space-y-4">
<Card className="overflow-hidden border-0 shadow-lg">