From 03b00b24997415fa878b33e019a9122db785b34a Mon Sep 17 00:00:00 2001 From: RandyJC Date: Mon, 8 Dec 2025 21:32:27 +0100 Subject: [PATCH] adding skip in cooldown and probing for media files --- packages/common/src/types/game/socket.ts | 1 + packages/socket/src/index.ts | 4 + packages/socket/src/services/game.ts | 18 ++- .../src/app/game/manager/[gameId]/page.tsx | 5 + .../src/components/game/create/QuizEditor.tsx | 106 ++++++++++++++++-- packages/web/src/utils/constants.ts | 2 +- 6 files changed, 120 insertions(+), 16 deletions(-) diff --git a/packages/common/src/types/game/socket.ts b/packages/common/src/types/game/socket.ts index 26dea76..26d94e7 100644 --- a/packages/common/src/types/game/socket.ts +++ b/packages/common/src/types/game/socket.ts @@ -73,6 +73,7 @@ export interface ClientToServerEvents { "manager:kickPlayer": (_message: { gameId: string; playerId: string }) => void "manager:startGame": (_message: MessageGameId) => void "manager:abortQuiz": (_message: MessageGameId) => void + "manager:skipQuestionIntro": (_message: MessageGameId) => void "manager:nextQuestion": (_message: MessageGameId) => void "manager:showLeaderboard": (_message: MessageGameId) => void "manager:getQuizz": (_quizzId: string) => void diff --git a/packages/socket/src/index.ts b/packages/socket/src/index.ts index 722a179..74dc9bc 100644 --- a/packages/socket/src/index.ts +++ b/packages/socket/src/index.ts @@ -171,6 +171,10 @@ io.on("connection", (socket) => { withGame(gameId, socket, (game) => game.nextRound(socket)) ) + socket.on("manager:skipQuestionIntro", ({ gameId }) => + withGame(gameId, socket, (game) => game.skipQuestionIntro(socket)) + ) + socket.on("manager:showLeaderboard", ({ gameId }) => withGame(gameId, socket, (game) => game.showLeaderboard()) ) diff --git a/packages/socket/src/services/game.ts b/packages/socket/src/services/game.ts index b67f608..0e7bf40 100644 --- a/packages/socket/src/services/game.ts +++ b/packages/socket/src/services/game.ts @@ -293,6 +293,18 @@ class Game { this.cooldown.active &&= false } + skipQuestionIntro(socket: Socket) { + if (this.manager.id !== socket.id) { + return + } + + if (!this.started) { + return + } + + this.abortCooldown() + } + async start(socket: Socket) { if (this.manager.id !== socket.id) { return @@ -346,11 +358,11 @@ class Game { this.broadcastStatus(STATUS.SHOW_QUESTION, { question: question.question, image: question.image, - media: question.media, + media: question.media, cooldown: question.cooldown, }) - await sleep(question.cooldown) + await this.startCooldown(question.cooldown) if (!this.started) { return @@ -362,7 +374,7 @@ class Game { question: question.question, answers: question.answers, image: question.image, - media: question.media, + media: question.media, time: question.time, totalPlayer: this.players.length, }) diff --git a/packages/web/src/app/game/manager/[gameId]/page.tsx b/packages/web/src/app/game/manager/[gameId]/page.tsx index c8976c0..d4a1bde 100644 --- a/packages/web/src/app/game/manager/[gameId]/page.tsx +++ b/packages/web/src/app/game/manager/[gameId]/page.tsx @@ -65,6 +65,11 @@ const ManagerGame = () => { break + case STATUS.SHOW_QUESTION: + socket?.emit("manager:skipQuestionIntro", { gameId }) + + break + case STATUS.SELECT_ANSWER: socket?.emit("manager:abortQuiz", { gameId }) diff --git a/packages/web/src/components/game/create/QuizEditor.tsx b/packages/web/src/components/game/create/QuizEditor.tsx index aee5aac..411070a 100644 --- a/packages/web/src/components/game/create/QuizEditor.tsx +++ b/packages/web/src/components/game/create/QuizEditor.tsx @@ -65,6 +65,7 @@ const QuizEditor = ({ quizzList, onBack, onListUpdate }: Props) => { const [uploading, setUploading] = useState>({}) const [deleting, setDeleting] = useState>({}) const [refreshingLibrary, setRefreshingLibrary] = useState(false) + const [probing, setProbing] = useState>({}) useEvent("manager:quizzLoaded", (quizz) => { setDraft(quizz) @@ -257,12 +258,86 @@ const QuizEditor = ({ quizzList, onBack, onListUpdate }: Props) => { } setQuestionMedia(qIndex, nextMedia) + adjustTimingWithMedia(qIndex, nextMedia) } const clearQuestionMedia = (qIndex: number) => { setQuestionMedia(qIndex, undefined) } + const probeMediaDuration = async (url: string, type: QuestionMedia["type"]) => { + if (!url || (type !== "audio" && type !== "video")) { + return null + } + + try { + const el = document.createElement(type) + el.preload = "metadata" + el.src = url + + await new Promise((resolve, reject) => { + const cleanup = () => { + el.onloadedmetadata = null + el.onerror = null + } + el.onloadedmetadata = () => { + cleanup() + resolve() + } + el.onerror = () => { + cleanup() + reject(new Error("Failed to load media metadata")) + } + }) + + const duration = el.duration + return Number.isFinite(duration) && duration > 0 ? duration : null + } catch (error) { + console.warn("Failed to probe media duration", error) + return null + } + } + + const adjustTimingWithMedia = async ( + qIndex: number, + media: QuestionMedia | undefined, + ) => { + if (!draft || !media?.url || !media.type || media.type === "image") { + return + } + + setProbing((prev) => ({ ...prev, [qIndex]: true })) + + try { + const duration = await probeMediaDuration(media.url, media.type) + if (!duration || !draft) { + return + } + + const rounded = Math.ceil(duration) + const buffer = 3 + const minCooldown = rounded + const minAnswer = rounded + buffer + const question = draft.questions[qIndex] + + const nextCooldown = Math.max(question.cooldown, minCooldown) + const nextTime = Math.max(question.time, minAnswer) + + if (nextCooldown !== question.cooldown || nextTime !== question.time) { + updateQuestion(qIndex, { + cooldown: nextCooldown, + time: nextTime, + }) + toast.success( + `Adjusted timing to media length (~${rounded}s, answers ${nextTime}s)`, + { id: `timing-${qIndex}` }, + ) + } + } finally { + setProbing((prev) => ({ ...prev, [qIndex]: false })) + } + } + const handleMediaUpload = async (qIndex: number, file: File) => { if (!draft) return const question = draft.questions[qIndex] @@ -296,6 +371,11 @@ const QuizEditor = ({ quizzList, onBack, onListUpdate }: Props) => { url: uploaded.url, fileName: uploaded.fileName, }) + adjustTimingWithMedia(qIndex, { + type, + url: uploaded.url, + fileName: uploaded.fileName, + }) toast.success("Media uploaded") refreshMediaLibrary() } catch (error) { @@ -509,18 +589,20 @@ const QuizEditor = ({ quizzList, onBack, onListUpdate }: Props) => { -
-
- Media upload - - {isUploading - ? "Uploading..." - : refreshingLibrary - ? "Refreshing..." - : mediaFileName - ? "Stored" - : "Not saved"} - +
+
+ Media upload + + {isUploading + ? "Uploading..." + : probing[qIndex] + ? "Probing..." + : refreshingLibrary + ? "Refreshing..." + : mediaFileName + ? "Stored" + : "Not saved"} +