Merge pull request #668 from storytold/camera-capture-testing

initial camera capture input progress for testing
This commit is contained in:
Victor O authored and GitHub committed 2024-08-15 15:03:20 -07:00
commit 25109d2dff
15 files changed
+415 -80

No files matched your search

+2
View File
@@ -34,10 +34,12 @@
"@babel/runtime": "^7.25.0",
"@react-spring/types": "^9.7.3",
"@typeform/embed": "^4.8.1",
"@types/dom-mediacapture-record": "^1.0.19",
"framer-motion": "6.5.1",
"local-storage": "^2.0.0",
"moment": "^2.29.4",
"react-easy-crop": "^5.0.7",
"react-webcam": "^7.2.0",
"universal-cookie": "^6.1.1",
"yarn": "^1.22.19"
},
@@ -1,11 +1,6 @@
@import "scss/colours.scss";
@import "scss/site-refresh.scss";
@mixin video-tint {
background-color: rgba($secondary-very-dark, 0.5);
backdrop-filter: blur(5px);
}
.fy-slide-frame {
position: absolute;
height: 100%;
@@ -41,6 +41,7 @@ interface EntityInputProps {
name?: string;
onChange?: any;
onPromptUpdate?: (prompt: Prompt | null) => void;
showWebcam?: boolean;
type: EntityModeProp;
value?: string;
}
@@ -177,6 +178,7 @@ export default function EntityInput({
name = "",
onChange,
onPromptUpdate,
showWebcam = false,
type,
value,
...rest
@@ -305,6 +307,7 @@ export default function EntityInput({
onSelect,
queryUser,
render: EntityInputEmpty,
showWebcam,
type,
...sharedProps,
...rest,
@@ -5,9 +5,11 @@ import {
useModal,
useSession,
} from "hooks";
import MediaBrowser, {
import {
CameraCapture,
MediaBrowser,
MediaBrowserProps,
} from "components/modals/MediaBrowser";
} from "components/modals";
import { FileWrapper } from "components/common";
import {
AcceptTypes,
@@ -20,6 +22,7 @@ import {
} from "components/entities/EntityTypes";
import { FontAwesomeIcon as Icon } from "@fortawesome/react-fontawesome";
import {
faCamera,
faDiagramSankey,
faFile,
faFileArrowUp,
@@ -33,6 +36,7 @@ interface EmptySlideProps extends SlideProps {
accept: AcceptTypes | AcceptTypes[];
inputProps?: any;
queryUser?: string;
showWebcam: boolean;
type: EntityModeProp;
}
@@ -40,6 +44,7 @@ export default function EntityInputEmpty({
accept,
inputProps,
queryUser,
showWebcam,
type,
...rest
}: EmptySlideProps) {
@@ -71,6 +76,13 @@ export default function EntityInputEmpty({
props,
});
const cameraClick = () =>
open({
component: CameraCapture,
padding: false,
width: "square",
});
const mediaIcons = () => {
if (isSelectedType(MediaFilters.audio, accepted[0])) return faWaveform;
if (isSelectedType(MediaFilters.engine_asset, accepted[0]))
@@ -130,6 +142,19 @@ export default function EntityInputEmpty({
/>
Choose from your {["bookmarks", "media", "weights"][inputMode]}
</div>
{showWebcam && (
<div
{...{
className: "fy-entity-input-row fy-entity-input-button",
onClick: cameraClick,
}}
>
<Icon
{...{ className: "fy-entity-input-label-icon", icon: faCamera }}
/>
Record with your camera
</div>
)}
</div>
</>
);
@@ -0,0 +1,59 @@
@import "scss/colours.scss";
@import "scss/site-refresh.scss";
.fy-camera-capture-modal {
width: 100%;
height: 100%;
position: relative;
.fy-camera-capture-display {
height: 100%;
margin: auto;
}
.fy-camera-capture-controls {
@include video-tint;
position: absolute;
bottom: 1rem;
border-radius: 0.5rem;
padding: 0.5rem 0.5rem 0.25rem;
left: 0;
right: 0;
margin: auto;
width: 50%;
display: flex;
justify-content: center;
}
.fy-camera-capture-record-toggle {
appearance: none;
border: none;
background: none;
display: inline-flex;
justify-content: center;
flex-direction: column;
align-items: center;
color: #fff;
font-size: 0.75rem;
// background: green;
svg {
display: block;
// background: rgba(#00f, 0.25);
height: 2rem;
width: 2rem;
margin-bottom: 0.25rem;
circle {
fill: none;
stroke: $primary;
stroke-width: 2px;
}
rect {
fill: $primary;
}
}
}
}
@@ -0,0 +1,139 @@
import React, { useCallback, useRef, useState } from "react";
import Webcam from "react-webcam";
import { a, useSpring } from "@react-spring/web";
// import { MediaRecorder, IMediaRecorder } from "extendable-media-recorder";
import "./CameraCapture.scss";
import { ModalUtilities } from "components/common";
// interface Props {
// value?: any;
// }
export default function CameraCapture({ handleClose }: ModalUtilities) {
const webcamRef = useRef<Webcam>(null);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const [capturing, capturingSet] = useState(false);
const [recordedChunks, setRecordedChunks] = useState([]);
const handleDataAvailable = useCallback(
({ data }) => {
if (data.size > 0) {
setRecordedChunks(prev => prev.concat(data));
}
},
[setRecordedChunks]
);
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",
});
mediaRecorderRef.current.addEventListener(
"dataavailable",
handleDataAvailable
);
mediaRecorderRef.current.start();
}
} else {
capturingSet(false);
if (mediaRecorderRef.current !== null) {
mediaRecorderRef.current.stop();
}
}
};
const mainToggleStyle = useSpring({
rx: capturing ? 1 : 8,
size: capturing ? 14 : 16,
xy: capturing ? 9 : 8,
});
return (
<div
{...{
className: "fy-camera-capture-modal",
}}
>
<Webcam
audio
{...{
audioConstraints: {
sampleSize: 16,
channelCount: 2,
},
className: "fy-camera-capture-display",
ref: webcamRef,
videoConstraints: {
width: 512,
height: 512,
facingMode: "user",
},
}}
/>
<div {...{ className: "fy-camera-capture-controls" }}>
<button
{...{
className: "fy-camera-capture-record-toggle",
onClick: mainToggleClick,
}}
>
<svg>
<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>
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>
)}*/}
</div>
);
}
@@ -0,0 +1 @@
export { default } from "./CameraCapture";
@@ -1,4 +1,7 @@
import CameraCapture from "./CameraCapture";
import InferenceJobsModal from "./InferenceJobsModal";
import MediaBrowser from "./MediaBrowser";
import MediaBrowser, { MediaBrowserProps } from "./MediaBrowser";
export { InferenceJobsModal, MediaBrowser };
export { CameraCapture, InferenceJobsModal, MediaBrowser };
export type { MediaBrowserProps };
@@ -37,6 +37,17 @@
.fy-modal-body-small {
width: 100%;
height: fit-content;
}
.fy-modal-body-square {
width: 75vh;
height: 75vh;
}
.fy-modal-body-wide,
.fy-modal-body-narrow,
.fy-modal-body-small,
.fy-modal-body-square {
overflow: hidden;
background-color: $panel;
border-color: lighten($panel, 8%) !important;
@@ -54,4 +65,8 @@
border-radius: 0;
}
}
.fy-modal-no-padding {
padding: 0;
}
}
@@ -12,6 +12,7 @@ interface Props {
lockTint?: boolean;
modalOpen: boolean;
onModalCloseEnd: (x: any) => void;
padding?: boolean;
width?: ModalWidth;
}
@@ -23,6 +24,7 @@ export default function ModalLayer({
lockTint,
modalOpen,
onModalCloseEnd,
padding = true,
width = "wide",
}: Props) {
const mainClassName = "fy-modal-layer";
@@ -57,7 +59,13 @@ export default function ModalLayer({
},
}}
>
<div {...{ className: `fy-modal-body-${width}` }}>
<div
{...{
className: `fy-modal-body-${width}${
padding ? "" : " fy-modal-no-padding"
}`,
}}
>
{Content && (
<Content {...{ ...contentProps, handleClose: close }} />
)}
@@ -50,6 +50,7 @@ export default function ModalProvider({ children }: ModalProviderProps) {
lockTint: modalState?.lockTint,
modalOpen,
onModalCloseEnd,
padding: modalState?.padding,
width: modalState?.width,
}}
/>
@@ -1,11 +1,12 @@
import { useEffect, useState } from "react";
import { isMobile } from "react-device-detect";
export type ModalWidth = "wide" | "narrow" | "small";
export type ModalWidth = "wide" | "narrow" | "small" | "square";
export interface ModalConfig {
component: React.ElementType;
lockTint?: boolean;
padding?: boolean;
props?: any;
width?: ModalWidth;
}
@@ -58,6 +58,11 @@ styles with important tags:
}
}
@mixin video-tint {
background-color: rgba($secondary-very-dark, 0.5);
backdrop-filter: blur(5px);
}
.fakeyou-refresh {
//- BS5 classnames -//
.object-fit-cover {
@@ -1,11 +1,25 @@
import React, { useState } from "react";
import { EntityInput } from "components/entities";
import { EntityFilterOptions, EntityInputMode, EntityModeProp, ListEntityFilters } from "components/entities/EntityTypes";
import { useModal, useNotifications,
// useSession
import {
EntityFilterOptions,
EntityInputMode,
EntityModeProp,
ListEntityFilters,
} from "components/entities/EntityTypes";
import {
useModal,
useNotifications,
// useSession
} from "hooks";
import { InferenceJobsModal } from "components/modals";
import { Button, Checkbox, NumberSlider, SegmentButtons, TempInput, TempSelect } from "components/common";
import {
Button,
Checkbox,
NumberSlider,
SegmentButtons,
TempInput,
TempSelect,
} from "components/common";
import { onChanger } from "resources";
import "./DevEntityInput.scss";
@@ -14,35 +28,37 @@ interface Props {
}
interface Yank {
[key: string]: any[]
[key: string]: any[];
}
export default function DevMediaInput({ value }: Props) {
// const { studioAccessCheck } = useSession();
const { create } = useNotifications();
const [mediaToken,mediaTokenSet] = useState();
const [notTitle,notTitleSet] = useState("Notification Title");
const [notContent,notContentSet] = useState("Notification Content");
const [autoRemove,autoRemoveSet] = useState(true);
const [btnCount,btnCountSet] = useState(0);
const [mediaToken, mediaTokenSet] = useState();
const [notTitle, notTitleSet] = useState("Notification Title");
const [notContent, notContentSet] = useState("Notification Content");
const [autoRemove, autoRemoveSet] = useState(true);
const [btnCount, btnCountSet] = useState(0);
const [mode, modeSet] = useState<EntityModeProp>("media");
const yadda = Object.keys(EntityInputMode).filter(val => isNaN(Number(val))).reduce((obj,modeType,i) => {
return {
[modeType]: ListEntityFilters(i),
...obj
};
},{});
const yadda = Object.keys(EntityInputMode)
.filter(val => isNaN(Number(val)))
.reduce((obj, modeType, i) => {
return {
[modeType]: ListEntityFilters(i),
...obj,
};
}, {});
const onChange = onChanger({
autoRemoveSet,
btnCountSet,
mediaTokenSet,
notContentSet,
notTitleSet
notTitleSet,
});
const [filters,filtersSet] = useState<Yank>(yadda);
const [owner,ownerSet] = useState("");
const [filters, filtersSet] = useState<Yank>(yadda);
const [owner, ownerSet] = useState("");
// const onChange = ({ target }: any) => mediaTokenSet(target.value);
const { open } = useModal();
@@ -53,61 +69,104 @@ export default function DevMediaInput({ value }: Props) {
console.log("❌", mode, inputMode, filterOptions, filters[mode]);
const changeFilter = ({ target }: { target: any }) => filtersSet({ ...filters, [mode]: target.value });
const changeFilter = ({ target }: { target: any }) =>
filtersSet({ ...filters, [mode]: target.value });
const openModal = () => open({ component: InferenceJobsModal });
const testBtn = { label: "abc" };
const actions = Array.from({length: btnCount},() => testBtn);
const actions = Array.from({ length: btnCount }, () => testBtn);
const createNotification = () => create({
actions,
autoRemove,
title: notTitle,
content: notContent,
onClick: () => console.log("🍉")
});
const createNotification = () =>
create({
actions,
autoRemove,
title: notTitle,
content: notContent,
onClick: () => console.log("🍉"),
});
return (<div {...{ className: "fy-engine-compositor p-3"}}>
<h2>{ [526,187].map((num = 0) => String.fromCodePoint(128000 + num)) }</h2>
<div {...{ className: "panel p-3 mb-3 fy-basic-grid" }}>
<header>
<h5>Make-a-notification</h5>
</header>
<TempInput {...{ value: notTitle, name: "notTitle", onChange }}/>
<TempInput {...{ value: notContent, name: "notContent", onChange }}/>
<NumberSlider {...{ value: btnCount, name: "btnCount", onChange, label: "Button amount", min: 0, max: 3 }}/>
<div {...{ className: "lower-controls" }}>
<Checkbox {...{ checked: autoRemove, name: "autoRemove", onChange, label: "Auto remove" }}/>
<Button {...{ label: "Create Notification", onClick: createNotification, variant: "primary" }}/>
return (
<div {...{ className: "fy-engine-compositor p-3" }}>
<h2>{[526, 187].map((num = 0) => String.fromCodePoint(128000 + num))}</h2>
<div {...{ className: "panel p-3 mb-3 fy-basic-grid" }}>
<header>
<h5>Make-a-notification</h5>
</header>
<TempInput {...{ value: notTitle, name: "notTitle", onChange }} />
<TempInput {...{ value: notContent, name: "notContent", onChange }} />
<NumberSlider
{...{
value: btnCount,
name: "btnCount",
onChange,
label: "Button amount",
min: 0,
max: 3,
}}
/>
<div {...{ className: "lower-controls" }}>
<Checkbox
{...{
checked: autoRemove,
name: "autoRemove",
onChange,
label: "Auto remove",
}}
/>
<Button
{...{
label: "Create Notification",
onClick: createNotification,
variant: "primary",
}}
/>
</div>
</div>
<div {...{ className: "panel p-3 fy-basic-grid" }}>
<h5>Select Entities</h5>
<header>
<SegmentButtons
{...{
onChange: ({ target }: { target: any }) => modeSet(target.value),
options,
value: mode,
}}
/>
<TempInput
{...{
value: owner,
onChange: ({ target }: { target: any }) => ownerSet(target.value),
placeholder: "User",
}}
/>
</header>
<TempSelect
{...{
options: filterOptions,
value: filters[mode],
onChange: changeFilter,
}}
/>
<EntityInput
{...{
accept: filters[mode],
aspectRatio: "landscape",
label: "Choose entity",
name: "mediaToken",
// label: `Choose ${ ["","media file","weight"][entityType] }`,
onChange,
owner,
// search: "Dream"
showWebcam: true,
type: mode,
}}
/>
<Button
{...{ label: "Open modal", onClick: openModal, variant: "primary" }}
/>
<div>{mediaToken}</div>
</div>
</div>
<div {...{ className: "panel p-3 fy-basic-grid" }}>
<h5>Select Entities</h5>
<header>
<SegmentButtons {...{ onChange: ({ target }: { target: any }) => modeSet(target.value), options, value: mode }}/>
<TempInput {...{ value: owner, onChange: ({ target }: { target: any }) => ownerSet(target.value), placeholder: "User" }}/>
</header>
<TempSelect {...{
options: filterOptions,
value: filters[mode],
onChange: changeFilter
}}/>
<EntityInput {...{
accept: filters[mode],
aspectRatio: "landscape",
label: "Choose entity",
name: "mediaToken",
// label: `Choose ${ ["","media file","weight"][entityType] }`,
onChange,
owner,
// search: "Dream",
type: mode
}}/>
<Button {...{ label: "Open modal", onClick: openModal, variant: "primary" }}/>
<div>
{ mediaToken }
</div>
</div>
</div>);
};
);
}
+19
View File
@@ -4487,6 +4487,7 @@ __metadata:
"@babel/runtime": ^7.25.0
"@react-spring/types": ^9.7.3
"@typeform/embed": ^4.8.1
"@types/dom-mediacapture-record": ^1.0.19
craco: ^0.0.3
cross-env: ^7.0.3
framer-motion: 6.5.1
@@ -4497,6 +4498,7 @@ __metadata:
prettier: ^3.1.0
pretty-quick: ^3.1.3
react-easy-crop: ^5.0.7
react-webcam: ^7.2.0
universal-cookie: ^6.1.1
yarn: ^1.22.19
languageName: unknown
@@ -5100,6 +5102,13 @@ __metadata:
languageName: node
linkType: hard
"@types/dom-mediacapture-record@npm:^1.0.19":
version: 1.0.19
resolution: "@types/dom-mediacapture-record@npm:1.0.19"
checksum: 4fb7b0bb7bcc6378fa9f0cfce2e144fcc41b5df3c7842bab07f6b812084bd41c18110ed27043ab28b24e2c251233fe4ae5ab0597e6b8509fef9647b29eed2d40
languageName: node
linkType: hard
"@types/eslint-scope@npm:^3.7.0":
version: 3.7.3
resolution: "@types/eslint-scope@npm:3.7.3"
@@ -18207,6 +18216,16 @@ __metadata:
languageName: node
linkType: hard
"react-webcam@npm:^7.2.0":
version: 7.2.0
resolution: "react-webcam@npm:7.2.0"
peerDependencies:
react: ">=16.2.0"
react-dom: ">=16.2.0"
checksum: 55aecc9e33ea711525f9b68a7c14c9cf4089777b9edd6d9c93813568d701a101f3da09ffd7df04b64eca8c9783a37830522376049589699c6015555ec8d7ccd8
languageName: node
linkType: hard
"react@npm:^16.13.1":
version: 16.14.0
resolution: "react@npm:16.14.0"