CameraCapture uploads videos

This commit is contained in:
Victor committed 2024-08-20 13:26:28 -07:00
1 parent e624a701e0
commit b5324681da
11 files changed
+448 -158

No files matched your search

@@ -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 {
@@ -91,23 +91,17 @@ const MediaBusy = ({ uploaderBusy, uploadProgress }: SlideProps) => {
return (
<div {...{ className: "fy-entity-input-loader" }}>
{uploaderBusy ? (
<>
Uploading
<div {...{ className: "fy-entity-input-progress-indicator" }}>
<div {...{ className: "fy-entity-input-progress-number" }}>
{uploadProgress}%
</div>
<WorkIndicator
{...{
failure: false,
max: 100,
progressPercentage: uploadProgress,
stage: 1,
success: false,
}}
/>
</div>
</>
<WorkIndicator
{...{
failure: false,
label: "Uploading",
max: 100,
progressPercentage: uploadProgress,
stage: 1,
showPercentage: true,
success: false,
}}
/>
) : (
<Spinner />
)}
@@ -370,6 +364,7 @@ export default function EntityInput({
onSelect,
queryUser,
render: EntityInputEmpty,
selectToken,
showWebcam,
type,
...sharedProps,
@@ -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",
});
@@ -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 (
<>
<div {...{ ...outerProps("fy-inference-job-indicator"), ...hoverSet }}>
<div {...{ className: "fy-inference-job-indicator-container" }}>
<a.div
{...{ className: "fy-inference-job-progress", style: percentFade }}
>
{progressPercentage}%
</a.div>
<WorkIndicator
{...{ failure, progressPercentage, stage: dashStatus(), success }}
/>
</div>
<WorkIndicator
{...{
failure,
progressPercentage,
stage: dashStatus(),
showPercentage: jobIsAlive,
success,
}}
/>
</div>
<div {...{ ...outerProps("fy-inference-job-details"), ...hoverSet }}>
<a.h6 {...{ style: headingBounce }}>
@@ -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;
}
}
}
@@ -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<Webcam>(null);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const [capturing, capturingSet] = useState(false);
const [recordedChunks, setRecordedChunks] = useState([]);
const [recordedChunks, recordedChunksSet] = useState([]);
const [counter, counterSet] = useState(0);
const [blob, blobSet] = useState<Blob | null>(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 (
<div
{...{
className: "fy-camera-capture-modal",
}}
>
<Webcam
audio
<Button
{...{
audioConstraints: {
sampleSize: 16,
channelCount: 2,
},
className: "fy-camera-capture-display",
ref: webcamRef,
videoConstraints: {
width: 512,
height: 512,
facingMode: "user",
},
className: "fy-camera-capture-close",
icon: faClose,
onClick: () => handleClose(),
square: true,
}}
/>
<div {...{ className: "fy-camera-capture-controls" }}>
<button
{...{
className: "fy-camera-capture-record-toggle",
onClick: mainToggleClick,
}}
>
<svg>
<circle
{transitions((style: any, i: number) => {
return [
<a.div {...{ className: "fy-camera-capture-slide", style }}>
<Webcam
audio
{...{
cx: 16,
cy: 16,
r: 15,
mirrored: true,
muted: true,
className: "fy-camera-capture-display",
onUserMedia: () => cameraStartedSet(true),
ref: webcamRef,
videoConstraints: {
width: 512,
height: 512,
facingMode: "user",
},
}}
/>
<a.rect
{cameraStarted ? (
<div {...{ className: "fy-camera-capture-controls" }}>
<button
{...{
className: "fy-camera-capture-record-toggle",
onClick: mainToggleClick,
}}
>
<svg
{...{
className: capturing
? "fy-camera-capture-btn-stop"
: "fy-camera-capture-btn-record",
}}
>
<circle
{...{
cx: 16,
cy: 16,
r: 15,
}}
/>
<a.rect
{...{
x: mainToggleStyle.xy,
y: mainToggleStyle.xy,
height: mainToggleStyle.size,
width: mainToggleStyle.size,
rx: mainToggleStyle.rx,
}}
/>
</svg>
{capturing ? timeString : "Record"}
</button>
</div>
) : (
<div {...{ className: "fy-camera-capture-centered" }}>
<Spinner />
</div>
)}
</a.div>,
<CapturePreview
{...{
blob,
resetCapture,
style,
upload,
}}
/>,
<a.div
{...{
className: "fy-camera-capture-slide fy-camera-capture-centered",
style,
}}
>
<WorkIndicator
{...{
x: mainToggleStyle.xy,
y: mainToggleStyle.xy,
height: mainToggleStyle.size,
width: mainToggleStyle.size,
rx: mainToggleStyle.rx,
failure: false,
label: "Uploading",
max: 100,
progressPercentage: uploadProgress,
stage: 1,
showPercentage: true,
success: false,
}}
/>
</svg>
Record
</button>
{recordedChunks.length > 0 && (
<button onClick={handleDownload}>Download</button>
)}
</div>
{/* {capturing ? (
<button onClick={handleStopCaptureClick}>Stop Capture</button>
) : (
<button onClick={handleStartCaptureClick}>Start Capture</button>
)}
{recordedChunks.length > 0 && (
<button onClick={handleDownload}>Download</button>
)}*/}
</a.div>,
<a.div
{...{
className: "fy-camera-capture-slide fy-camera-capture-centered",
style,
}}
>
error
</a.div>,
][i];
})}
</div>
);
}
@@ -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 (
<a.div {...{ className: "fy-camera-capture-slide", style }}>
<video
controls
{...{ className: "fy-camera-capture-preview", src: url }}
/>
<Button
{...{
className: "fy-camera-capture-float-button",
label: "Try again",
onClick: () => resetCapture(),
variant: "secondary",
}}
/>
<Button
{...{
className: "fy-camera-capture-float-button",
label: "Upload and use",
variant: "primary",
onClick: upload,
}}
/>
</a.div>
);
} else return null;
}
@@ -40,8 +40,8 @@
}
.fy-modal-body-square {
width: 75vh;
height: 75vh;
// width: 75vh;
// height: 75vh;
}
.fy-modal-body-wide,
@@ -56,7 +56,11 @@
box-shadow: 0px 9px 33px 1px rgba(19, 19, 27, 0.8);
-webkit-box-shadow: 0px 9px 33px 1px rgba(19, 19, 27, 0.8);
-moz-box-shadow: 0px 9px 33px 1px rgba(19, 19, 27, 0.8);
}
.fy-modal-body-wide,
.fy-modal-body-narrow,
.fy-modal-body-small {
padding: 1rem;
@include breakpoint($under992) {
@@ -1,17 +1,29 @@
@import "scss/colours.scss";
@import "scss/site-refresh.scss";
.work-indicator-label {
align-items: center;
display: flex;
flex-direction: column;
position: relative;
}
.work-indicator {
@include medium-svg;
// height: 2.25rem;
// width: 2.25rem;
// fill: none;
position: relative;
// circle, line, polyline {
// stroke-width: 4px;
// stroke-linecap: round;
// stroke: #fff;
// }
.work-indicator-progress {
text-align: center;
position: absolute;
font-size: 0.625rem;
line-height: 0.625rem;
height: 0.625rem;
top: 50%;
transform: translateY(-50%);
left: 0;
right: 0;
bottom: 0;
}
polyline {
transform: translate(6px, 6px);
@@ -1,37 +1,71 @@
import React from "react";
import { a, useSpring } from "@react-spring/web";
import { AniX, Check, DashedCircle } from "components/svg";
import { circle } from "../sharedSVGProps";
import "./WorkIndicator.scss";
interface Props {
interface WorkIndicatorProps {
className?: string;
failure: boolean;
max?: number;
progressPercentage: number;
stage: number;
showPercentage?: boolean;
success: boolean;
}
export default function WorkIndicator({
interface WorkIndicatorWrapperProps extends WorkIndicatorProps {
label?: string;
}
const WorkIndicatorSpinner = ({
className,
failure = false,
max,
progressPercentage,
stage = 0,
showPercentage,
success = false,
...rest
}: Props) {
}: WorkIndicatorProps) => {
const style = useSpring({
opacity: showPercentage ? 1 : 0,
});
return (
<svg {...{ className: "work-indicator", ...rest }}>
<circle {...{ ...circle, className: "work-indicator-circle-track" }} />
<DashedCircle
{...{
className: "work-indicator-circle-marker",
max,
progressPercentage,
stage,
}}
/>
<AniX {...{ checked: failure }} />
<Check {...{ checked: success }} />
</svg>
<div
{...{ className: `work-indicator${className ? " " + className : ""}` }}
>
<a.div {...{ className: "work-indicator-progress", style }}>
{progressPercentage}%
</a.div>
<svg {...{ className: "work-indicator-circle", ...rest }}>
<circle {...{ ...circle, className: "work-indicator-circle-track" }} />
<DashedCircle
{...{
className: "work-indicator-circle-marker",
max,
progressPercentage,
stage,
}}
/>
<AniX {...{ checked: failure }} />
<Check {...{ checked: success }} />
</svg>
</div>
);
};
export default function WorkIndicator({
label,
...rest
}: WorkIndicatorWrapperProps) {
return label ? (
<div {...{ className: "work-indicator-label" }}>
{label}
<WorkIndicatorSpinner {...rest} />
</div>
) : (
<WorkIndicatorSpinner {...rest} />
);
}
@@ -98,6 +98,7 @@ export default function useMediaUploader({ autoUpload, onSuccess = n }: Props) {
return {
busy: status === FetchStatus.in_progress,
clear,
createUpload,
engineSubtype,
engineSubtypeChange,
error: status === FetchStatus.error,