feat: add live camera multi-device viewer mode
这个提交包含在:
136
server/db.ts
136
server/db.ts
@@ -8,6 +8,7 @@ import {
|
||||
poseAnalyses, InsertPoseAnalysis,
|
||||
trainingRecords, InsertTrainingRecord,
|
||||
liveAnalysisSessions, InsertLiveAnalysisSession,
|
||||
liveAnalysisRuntime, InsertLiveAnalysisRuntime,
|
||||
liveActionSegments, InsertLiveActionSegment,
|
||||
dailyTrainingAggregates, InsertDailyTrainingAggregate,
|
||||
ratingHistory, InsertRatingHistory,
|
||||
@@ -32,6 +33,7 @@ import { fetchTutorialMetrics, shouldRefreshTutorialMetrics } from "./tutorialMe
|
||||
let _db: ReturnType<typeof drizzle> | null = null;
|
||||
|
||||
const APP_TIMEZONE = process.env.TZ || "Asia/Shanghai";
|
||||
export const LIVE_ANALYSIS_RUNTIME_TIMEOUT_MS = 15_000;
|
||||
|
||||
function getDateFormatter() {
|
||||
return new Intl.DateTimeFormat("en-CA", {
|
||||
@@ -888,6 +890,140 @@ export async function createLiveAnalysisSession(session: InsertLiveAnalysisSessi
|
||||
return result[0].insertId;
|
||||
}
|
||||
|
||||
export async function getUserLiveAnalysisRuntime(userId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return undefined;
|
||||
const result = await db.select().from(liveAnalysisRuntime)
|
||||
.where(eq(liveAnalysisRuntime.userId, userId))
|
||||
.limit(1);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
export async function upsertUserLiveAnalysisRuntime(
|
||||
userId: number,
|
||||
patch: Omit<InsertLiveAnalysisRuntime, "id" | "createdAt" | "updatedAt" | "userId">,
|
||||
) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
const existing = await getUserLiveAnalysisRuntime(userId);
|
||||
if (existing) {
|
||||
await db.update(liveAnalysisRuntime)
|
||||
.set({
|
||||
ownerSid: patch.ownerSid ?? existing.ownerSid,
|
||||
status: patch.status ?? existing.status,
|
||||
title: patch.title ?? existing.title,
|
||||
sessionMode: patch.sessionMode ?? existing.sessionMode,
|
||||
mediaSessionId: patch.mediaSessionId === undefined ? existing.mediaSessionId : patch.mediaSessionId,
|
||||
startedAt: patch.startedAt === undefined ? existing.startedAt : patch.startedAt,
|
||||
endedAt: patch.endedAt === undefined ? existing.endedAt : patch.endedAt,
|
||||
lastHeartbeatAt: patch.lastHeartbeatAt === undefined ? existing.lastHeartbeatAt : patch.lastHeartbeatAt,
|
||||
snapshot: patch.snapshot === undefined ? existing.snapshot : patch.snapshot,
|
||||
})
|
||||
.where(eq(liveAnalysisRuntime.userId, userId));
|
||||
return getUserLiveAnalysisRuntime(userId);
|
||||
}
|
||||
|
||||
const result = await db.insert(liveAnalysisRuntime).values({
|
||||
userId,
|
||||
ownerSid: patch.ownerSid ?? null,
|
||||
status: patch.status ?? "idle",
|
||||
title: patch.title ?? null,
|
||||
sessionMode: patch.sessionMode ?? "practice",
|
||||
mediaSessionId: patch.mediaSessionId ?? null,
|
||||
startedAt: patch.startedAt ?? null,
|
||||
endedAt: patch.endedAt ?? null,
|
||||
lastHeartbeatAt: patch.lastHeartbeatAt ?? null,
|
||||
snapshot: patch.snapshot ?? null,
|
||||
});
|
||||
|
||||
const runtimeId = result[0].insertId;
|
||||
const rows = await db.select().from(liveAnalysisRuntime).where(eq(liveAnalysisRuntime.id, runtimeId)).limit(1);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
export async function updateUserLiveAnalysisRuntime(
|
||||
userId: number,
|
||||
patch: Partial<Omit<InsertLiveAnalysisRuntime, "id" | "createdAt" | "updatedAt" | "userId">>,
|
||||
) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
const existing = await getUserLiveAnalysisRuntime(userId);
|
||||
if (!existing) return undefined;
|
||||
|
||||
await db.update(liveAnalysisRuntime)
|
||||
.set({
|
||||
ownerSid: patch.ownerSid === undefined ? existing.ownerSid : patch.ownerSid,
|
||||
status: patch.status ?? existing.status,
|
||||
title: patch.title === undefined ? existing.title : patch.title,
|
||||
sessionMode: patch.sessionMode ?? existing.sessionMode,
|
||||
mediaSessionId: patch.mediaSessionId === undefined ? existing.mediaSessionId : patch.mediaSessionId,
|
||||
startedAt: patch.startedAt === undefined ? existing.startedAt : patch.startedAt,
|
||||
endedAt: patch.endedAt === undefined ? existing.endedAt : patch.endedAt,
|
||||
lastHeartbeatAt: patch.lastHeartbeatAt === undefined ? existing.lastHeartbeatAt : patch.lastHeartbeatAt,
|
||||
snapshot: patch.snapshot === undefined ? existing.snapshot : patch.snapshot,
|
||||
})
|
||||
.where(eq(liveAnalysisRuntime.userId, userId));
|
||||
|
||||
return getUserLiveAnalysisRuntime(userId);
|
||||
}
|
||||
|
||||
export async function updateLiveAnalysisRuntimeHeartbeat(input: {
|
||||
userId: number;
|
||||
ownerSid: string;
|
||||
runtimeId: number;
|
||||
mediaSessionId?: string | null;
|
||||
snapshot?: unknown;
|
||||
}) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
const existing = await getUserLiveAnalysisRuntime(input.userId);
|
||||
if (!existing || existing.id !== input.runtimeId || existing.ownerSid !== input.ownerSid || existing.status !== "active") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
await db.update(liveAnalysisRuntime)
|
||||
.set({
|
||||
mediaSessionId: input.mediaSessionId === undefined ? existing.mediaSessionId : input.mediaSessionId,
|
||||
snapshot: input.snapshot === undefined ? existing.snapshot : input.snapshot,
|
||||
lastHeartbeatAt: new Date(),
|
||||
endedAt: null,
|
||||
})
|
||||
.where(and(
|
||||
eq(liveAnalysisRuntime.userId, input.userId),
|
||||
eq(liveAnalysisRuntime.id, input.runtimeId),
|
||||
));
|
||||
|
||||
return getUserLiveAnalysisRuntime(input.userId);
|
||||
}
|
||||
|
||||
export async function endUserLiveAnalysisRuntime(input: {
|
||||
userId: number;
|
||||
ownerSid?: string | null;
|
||||
runtimeId?: number;
|
||||
snapshot?: unknown;
|
||||
}) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
const existing = await getUserLiveAnalysisRuntime(input.userId);
|
||||
if (!existing) return undefined;
|
||||
if (input.runtimeId != null && existing.id !== input.runtimeId) return undefined;
|
||||
if (input.ownerSid != null && existing.ownerSid !== input.ownerSid) return undefined;
|
||||
|
||||
await db.update(liveAnalysisRuntime)
|
||||
.set({
|
||||
status: "ended",
|
||||
mediaSessionId: null,
|
||||
endedAt: new Date(),
|
||||
snapshot: input.snapshot === undefined ? existing.snapshot : input.snapshot,
|
||||
})
|
||||
.where(eq(liveAnalysisRuntime.userId, input.userId));
|
||||
|
||||
return getUserLiveAnalysisRuntime(input.userId);
|
||||
}
|
||||
|
||||
export async function createLiveActionSegments(segments: InsertLiveActionSegment[]) {
|
||||
const db = await getDb();
|
||||
if (!db || segments.length === 0) return;
|
||||
|
||||
在新工单中引用
屏蔽一个用户