From b5324681da76b644b343fee56b22a95203fbc2a4 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 16 Aug 2024 14:23:24 -0700 Subject: [PATCH] CameraCapture uploads videos --- .../entities/EntityInput/EntityInput.scss | 36 +-- .../entities/EntityInput/EntityInput.tsx | 29 +- .../entities/EntityInput/EntityInputEmpty.tsx | 3 + .../layout/InferenceJobsList/JobItem.tsx | 23 +- .../modals/CameraCapture/CameraCapture.scss | 96 ++++++- .../modals/CameraCapture/CameraCapture.tsx | 270 +++++++++++++----- .../modals/CameraCapture/CapturePreview.tsx | 46 +++ .../providers/ModalProvider/ModalLayer.scss | 8 +- .../svg/WorkIndicator/WorkIndicator.scss | 28 +- .../svg/WorkIndicator/WorkIndicator.tsx | 66 +++-- .../useMediaUploader/useMediaUploader.tsx | 1 + 11 files changed, 448 insertions(+), 158 deletions(-) create mode 100644 src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CapturePreview.tsx diff --git a/src/website/packages/fakeyou.com/src/components/entities/EntityInput/EntityInput.scss b/src/website/packages/fakeyou.com/src/components/entities/EntityInput/EntityInput.scss index 81894f5e18..afa4186211 100644 --- a/src/website/packages/fakeyou.com/src/components/entities/EntityInput/EntityInput.scss +++ b/src/website/packages/fakeyou.com/src/components/entities/EntityInput/EntityInput.scss @@ -82,25 +82,25 @@ pointer-events: none; } - .fy-entity-input-progress-indicator { - align-items: center; - display: flex; - flex-direction: column; - position: relative; + // .fy-entity-input-progress-indicator { + // align-items: center; + // display: flex; + // flex-direction: column; + // position: relative; - .fy-entity-input-progress-number { - position: absolute; - text-align: center; - font-size: 0.625rem; - line-height: 0.625rem; - height: 0.625rem; - top: 50%; - transform: translateY(-50%); - left: 0; - right: 0; - bottom: 0; - } - } + // .fy-entity-input-progress-number { + // position: absolute; + // text-align: center; + // font-size: 0.625rem; + // line-height: 0.625rem; + // height: 0.625rem; + // top: 50%; + // transform: translateY(-50%); + // left: 0; + // right: 0; + // bottom: 0; + // } + // } } .fy-entity-input-landscape { diff --git a/src/website/packages/fakeyou.com/src/components/entities/EntityInput/EntityInput.tsx b/src/website/packages/fakeyou.com/src/components/entities/EntityInput/EntityInput.tsx index 14e5ea5cb7..e0cecc58be 100644 --- a/src/website/packages/fakeyou.com/src/components/entities/EntityInput/EntityInput.tsx +++ b/src/website/packages/fakeyou.com/src/components/entities/EntityInput/EntityInput.tsx @@ -91,23 +91,17 @@ const MediaBusy = ({ uploaderBusy, uploadProgress }: SlideProps) => { return (
{uploaderBusy ? ( - <> - Uploading -
-
- {uploadProgress}% -
- -
- + ) : ( )} @@ -370,6 +364,7 @@ export default function EntityInput({ onSelect, queryUser, render: EntityInputEmpty, + selectToken, showWebcam, type, ...sharedProps, diff --git a/src/website/packages/fakeyou.com/src/components/entities/EntityInput/EntityInputEmpty.tsx b/src/website/packages/fakeyou.com/src/components/entities/EntityInput/EntityInputEmpty.tsx index 6199669653..20f74efda7 100644 --- a/src/website/packages/fakeyou.com/src/components/entities/EntityInput/EntityInputEmpty.tsx +++ b/src/website/packages/fakeyou.com/src/components/entities/EntityInput/EntityInputEmpty.tsx @@ -36,6 +36,7 @@ interface EmptySlideProps extends SlideProps { accept: AcceptTypes | AcceptTypes[]; inputProps?: any; queryUser?: string; + selectToken: (token: string) => void; showWebcam: boolean; type: EntityModeProp; } @@ -44,6 +45,7 @@ export default function EntityInputEmpty({ accept, inputProps, queryUser, + selectToken, showWebcam, type, ...rest @@ -80,6 +82,7 @@ export default function EntityInputEmpty({ open({ component: CameraCapture, padding: false, + props: { selectToken }, width: "square", }); diff --git a/src/website/packages/fakeyou.com/src/components/layout/InferenceJobsList/JobItem.tsx b/src/website/packages/fakeyou.com/src/components/layout/InferenceJobsList/JobItem.tsx index b6d9cd9d8f..1c94a612f3 100644 --- a/src/website/packages/fakeyou.com/src/components/layout/InferenceJobsList/JobItem.tsx +++ b/src/website/packages/fakeyou.com/src/components/layout/InferenceJobsList/JobItem.tsx @@ -136,9 +136,7 @@ export default function JobItem({ }); const headingBounce = useSpring(makeBounce(8)); const subtitleBounce = useSpring(makeBounce(6, 30)); - const percentFade = useSpring({ - opacity: jobIsAlive ? 1 : 0, - }); + const subtitle = maybeFailureCategory ? `${failures(maybeFailureCategory)}` : t(`subtitles.${jobStatus}`); @@ -188,16 +186,15 @@ export default function JobItem({ return ( <>
-
- - {progressPercentage}% - - -
+
diff --git a/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CameraCapture.scss b/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CameraCapture.scss index adcd21e7ab..f58e12ad76 100644 --- a/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CameraCapture.scss +++ b/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CameraCapture.scss @@ -2,12 +2,39 @@ @import "scss/site-refresh.scss"; .fy-camera-capture-modal { - width: 100%; - height: 100%; + // background: red; + width: 75vh; + height: 75vh; position: relative; + .fy-camera-capture-close { + @include video-tint; + border: none; + position: absolute; + right: 1rem; + top: 1rem; + + &:hover { + background-color: rgba($secondary-very-dark, 0.45); + } + } + + .fy-camera-capture-slide { + position: absolute; + width: 100%; + } + + .fy-camera-capture-centered { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + } + .fy-camera-capture-display { + // background: red; + width: 100%; height: 100%; margin: auto; } @@ -41,19 +68,74 @@ svg { display: block; - // background: rgba(#00f, 0.25); height: 2rem; width: 2rem; margin-bottom: 0.25rem; + &.fy-camera-capture-btn-record { + circle { + stroke: $primary; + } + rect { + fill: $primary; + } + &:hover { + circle { + stroke: $primary-light; + } + rect { + fill: $primary-light; + } + } + } + + &.fy-camera-capture-btn-stop { + circle { + stroke: $secondary-lightest; + } + rect { + fill: $secondary-lightest; + } + &:hover { + circle { + stroke: #fff; + } + rect { + fill: #fff; + } + } + } + + circle, + rect { + transition: all 0.25s ease-in-out; + } + circle { fill: none; - stroke: $primary; stroke-width: 2px; } - rect { - fill: $primary; - } + } + } + + .fy-camera-capture-preview { + width: 100%; + position: absolute; + } + + .fy-camera-capture-float-button { + position: absolute; + top: 1rem; + + &:first-of-type { + left: 1rem; + } + + &:last-of-type { + margin: auto; + width: fit-content; + left: 0; + right: 0; } } } diff --git a/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CameraCapture.tsx b/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CameraCapture.tsx index e22dd3d3c3..0772e4ee55 100644 --- a/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CameraCapture.tsx +++ b/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CameraCapture.tsx @@ -1,55 +1,57 @@ -import React, { useCallback, useRef, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import Webcam from "react-webcam"; -import { a, useSpring } from "@react-spring/web"; -// import { MediaRecorder, IMediaRecorder } from "extendable-media-recorder"; +import { a, TransitionFn, useSpring, useTransition } from "@react-spring/web"; +import { WorkIndicator } from "components/svg"; +import { useInterval, useMediaUploader } from "hooks"; +import { UploaderResponse } from "components/entities/EntityTypes"; +import { Button, ModalUtilities, Spinner } from "components/common"; +import { faClose } from "@fortawesome/pro-solid-svg-icons"; +import CapturePreview from "./CapturePreview"; import "./CameraCapture.scss"; -import { ModalUtilities } from "components/common"; +interface CameraCaptureProps extends ModalUtilities { + selectToken: (token: string) => void; +} -// interface Props { -// value?: any; -// } - -export default function CameraCapture({ handleClose }: ModalUtilities) { +export default function CameraCapture({ + handleClose, + selectToken, +}: CameraCaptureProps) { const webcamRef = useRef(null); const mediaRecorderRef = useRef(null); const [capturing, capturingSet] = useState(false); - const [recordedChunks, setRecordedChunks] = useState([]); + const [recordedChunks, recordedChunksSet] = useState([]); + const [counter, counterSet] = useState(0); + const [blob, blobSet] = useState(null); + const [cameraStarted, cameraStartedSet] = useState(false); + + const hours = Math.floor(counter / 3600); + const minutes = Math.floor((counter - hours * 3600) / 60); + const seconds = counter - hours * 3600 - minutes * 60; + + const timeString = + hours.toString().padStart(2, "0") + + ":" + + minutes.toString().padStart(2, "0") + + ":" + + seconds.toString().padStart(2, "0"); const handleDataAvailable = useCallback( ({ data }) => { if (data.size > 0) { - setRecordedChunks(prev => prev.concat(data)); + recordedChunksSet(prev => prev.concat(data)); } }, - [setRecordedChunks] + [recordedChunksSet] ); - const handleDownload = React.useCallback(() => { - if (recordedChunks.length) { - const blob = new Blob(recordedChunks, { - type: "video/webm", - }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - document.body.appendChild(a); - // a.style = "display: none"; - a.href = url; - a.download = "react-webcam-stream-capture.webm"; - a.click(); - window.URL.revokeObjectURL(url); - setRecordedChunks([]); - } - }, [recordedChunks]); - const mainToggleClick = () => { if (!capturing) { capturingSet(true); if (webcamRef.current && webcamRef.current.stream) { - console.log("🧢", webcamRef.current.stream); mediaRecorderRef.current = new MediaRecorder(webcamRef.current.stream, { - mimeType: "video/webm", + mimeType: "video/mp4", }); mediaRecorderRef.current.addEventListener( @@ -73,67 +75,181 @@ export default function CameraCapture({ handleClose }: ModalUtilities) { xy: capturing ? 9 : 8, }); + const resetCapture = () => { + cameraStartedSet(false); + blobSet(null); + recordedChunksSet([]); + counterSet(0); + }; + + const { + busy: uploaderBusy, + createUpload, + error: uploaderError, + uploadProgress, + } = useMediaUploader({ + onSuccess: (res: UploaderResponse) => { + handleClose(); + selectToken(res.media_file_token); + }, + }); + + const index = uploaderError + ? 3 + : uploaderBusy + ? 2 + : blob && !capturing + ? 1 + : 0; + + const upload = () => { + if (blob) { + const file = new File([blob], "test-upload.mp4", { type: "video/mp4" }); + createUpload(file); + } + }; + + const transitions: TransitionFn< + number, + { opacity: number; transform: string } + > = useTransition(index, { + config: { mass: 1, tension: 80, friction: 10 }, + from: { opacity: 0, transform: `translateX(${5}rem)` }, + enter: { opacity: 1, transform: `translateX(0)` }, + leave: { opacity: 0, transform: `translateX(${5}rem)` }, + }); + + useInterval({ + eventProps: { capturing, counter }, + interval: 1000, + locked: !capturing, + onTick: () => { + if (capturing) { + counterSet(currentCounter => currentCounter + 1); + } + }, + }); + + useEffect(() => { + if (!blob && !capturing && recordedChunks.length) { + blobSet( + new Blob(recordedChunks, { + type: "video/mp4", + }) + ); + } + }, [blob, capturing, recordedChunks]); + return (
- handleClose(), + square: true, }} /> -
-
+ ) : ( +
+ +
+ )} + , + + , + + - - Record - - {recordedChunks.length > 0 && ( - - )} -
- {/* {capturing ? ( - - ) : ( - - )} - {recordedChunks.length > 0 && ( - - )}*/} + , + + error + , + ][i]; + })}
); } diff --git a/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CapturePreview.tsx b/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CapturePreview.tsx new file mode 100644 index 0000000000..b0ea6bb6a3 --- /dev/null +++ b/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CapturePreview.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { a, SpringValues } from "@react-spring/web"; +import { Button } from "components/common"; + +interface CapturePreviewProps { + blob: Blob | null; + resetCapture: () => void; + style: SpringValues; + upload: () => void; +} + +export default function CapturePreview({ + blob, + resetCapture, + style, + upload, +}: CapturePreviewProps) { + if (blob) { + const url = URL.createObjectURL(blob); + + return ( + +