diff --git a/public/sounds/camera_shutter.mp3 b/public/sounds/camera_shutter.mp3 new file mode 100644 index 0000000..0cda4c0 Binary files /dev/null and b/public/sounds/camera_shutter.mp3 differ diff --git a/public/sounds/camera_shutter.ogg b/public/sounds/camera_shutter.ogg new file mode 100644 index 0000000..309e5db Binary files /dev/null and b/public/sounds/camera_shutter.ogg differ diff --git a/src/hooks/sounds.ts b/src/hooks/sounds.ts index 9ac6ed4..27b00b8 100644 --- a/src/hooks/sounds.ts +++ b/src/hooks/sounds.ts @@ -35,6 +35,8 @@ const SoundNames = [ "tryk_paa_den_lange_tast", "ultrakill", "wicked", + "firework", + "camera_shutter", ] as const; type SoundName = (typeof SoundNames)[number]; diff --git a/src/views/Game/components/GamFinishedDialog.tsx b/src/views/Game/components/GamFinishedDialog.tsx index d3f8187..61044c9 100644 --- a/src/views/Game/components/GamFinishedDialog.tsx +++ b/src/views/Game/components/GamFinishedDialog.tsx @@ -2,6 +2,7 @@ import { Fireworks, type FireworksHandlers } from "@fireworks-js/react"; import { Box, Button, + CircularProgress, Dialog, DialogActions, DialogContent, @@ -13,8 +14,10 @@ import { useTheme, } from "@mui/material"; import { FunctionComponent, memo, useEffect, useRef, useState } from "react"; +import { AiOutlineDelete } from "react-icons/ai"; import { BsCamera } from "react-icons/bs"; -import { PiCameraRotate } from "react-icons/pi"; +import { FaArrowsRotate } from "react-icons/fa6"; +import { addPhoto } from "../../../api/endpoints/game"; import { useVideoDevices } from "../../../hooks/camera"; import { useSounds } from "../../../hooks/sounds"; import useGame from "../../../stores/game"; @@ -90,7 +93,7 @@ const GameFinishedDialog: FunctionComponent = ( {...props} PaperProps={{ sx: { - minWidth: 500, + minWidth: 600, }, }} > @@ -103,8 +106,7 @@ const GameFinishedDialog: FunctionComponent = ( padding: 0, }} > - {/* TODO: implement */} - {/* */} + = ( }; const Camera: FunctionComponent = memo(() => { + const MaxWidth = 600; + const MaxHeight = 500; + const { devices: cameraDevices, error: cameraError } = useVideoDevices(); + const [selectedDevice, setSelectedDevice] = useState( null, ); + const [selectedDeviceSize, setSelectedDeviceSize] = useState<{ + width: number; + height: number; + } | null>(null); + const hasNoDevices = cameraDevices && cameraDevices.length === 0; const hasMultipleDevices = cameraDevices && cameraDevices.length > 1; + const [image, setImage] = useState(null); + const [countDown, setCountDown] = useState(); + + const isCountingDown = countDown !== undefined; + + const game = useGame((state) => ({ + gameToken: state.token, + gameId: state.id, + })); + + const sounds = useSounds(); + useEffect(() => { - if (cameraDevices) { - setSelectedDevice(cameraDevices[0]); + if (!cameraDevices || cameraDevices.length === 0) { + return; } - }, [cameraDevices]); - const takePicture = async () => { - if (!selectedDevice) { + if (selectedDevice) { return; } - const video = document.querySelector("video") as HTMLVideoElement; + selectDevice(cameraDevices[0]); + }, [cameraDevices]); - const canvas = document.createElement("canvas"); - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; + const selectDevice = async (device: MediaDeviceInfo) => { + const meta = await getDeviceCapabilities(device.deviceId); + const size = getScaledViewSize(meta.width, meta.height); - const context = canvas.getContext("2d"); - context?.drawImage(video, 0, 0, canvas.width, canvas.height); + setSelectedDeviceSize(size); + setSelectedDevice(device); + }; - canvas.toBlob((blob) => { - if (!blob) { - return; - } + const getDeviceCapabilities = async (id: string) => { + const stream = await navigator.mediaDevices.getUserMedia({ + video: { + deviceId: id, + }, }); + + const track = stream.getVideoTracks()[0]; + + return track.getSettings(); + }; + + let countDownInterval: number; + const takePicture = () => { + if (!selectedDevice || isCountingDown) { + return; + } + + setCountDown(3); + countDownInterval = setInterval(() => { + setCountDown((prev) => { + if (prev === undefined) { + return undefined; + } + + if (prev === 1) { + setCountDown(undefined); + clearInterval(countDownInterval); + capture(); + + return undefined; + } + + return prev - 1; + }); + }, 1000); + + const capture = async () => { + sounds.play("camera_shutter"); + + const video = document.querySelector("video") as HTMLVideoElement; + + const canvas = document.createElement("canvas"); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + const context = canvas.getContext("2d"); + context?.drawImage(video, 0, 0, canvas.width, canvas.height); + + canvas.toBlob(async (blob) => { + if (!blob || !game.gameToken || !game.gameId) { + return; + } + + setImage(blob); + + await addPhoto(game.gameToken, game.gameId, blob); + }); + }; }; - const changeCamera = () => { + const removePicture = () => { + setImage(null); + }; + + const changeCamera = async () => { if (!cameraDevices) { return; } @@ -203,72 +284,138 @@ const Camera: FunctionComponent = memo(() => { const nextIndex = (currentIndex + 1) % cameraDevices.length; - setSelectedDevice(cameraDevices[nextIndex]); + const device = cameraDevices[nextIndex]; + + selectDevice(device); }; + const getScaledViewSize = (width?: number, height?: number) => { + if (!width || !height) { + return { + width: 0, + height: 0, + }; + } + + const ratio = Math.min(MaxWidth / width, MaxHeight / height); + + return { + width: width * ratio, + height: height * ratio, + }; + }; + + if (!!cameraError) { + return null; + } + return ( - + + {image && ( + + )} + + {selectedDevice && - + - + - + {image ? : } - + - + @@ -276,4 +423,35 @@ const Camera: FunctionComponent = memo(() => { ); }); +interface VideoProps { + device: MediaDeviceInfo | null; +} + +const Video: FunctionComponent = memo(({ device }) => { + return ( +