Spaces:
Configuration error
Configuration error
Create WebcamCapture.tsx
Browse files- src/components/WebcamCapture.tsx +147 -0
src/components/WebcamCapture.tsx
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Button from "./Button";
|
| 2 |
+
import { THEME } from "../constants";
|
| 3 |
+
|
| 4 |
+
interface WebcamCaptureProps {
|
| 5 |
+
isRunning: boolean;
|
| 6 |
+
onToggleRunning: () => void;
|
| 7 |
+
error?: string | null;
|
| 8 |
+
imageSize?: number;
|
| 9 |
+
onImageSizeChange?: (size: number) => void;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export default function WebcamCapture({
|
| 13 |
+
isRunning,
|
| 14 |
+
onToggleRunning,
|
| 15 |
+
error,
|
| 16 |
+
imageSize,
|
| 17 |
+
onImageSizeChange,
|
| 18 |
+
}: WebcamCaptureProps) {
|
| 19 |
+
const hasError = Boolean(error);
|
| 20 |
+
|
| 21 |
+
const statusConfig = hasError
|
| 22 |
+
? {
|
| 23 |
+
text: "SIGNAL LOSS",
|
| 24 |
+
color: "bg-[var(--mistral-red)]",
|
| 25 |
+
border: "border-[var(--mistral-red)]",
|
| 26 |
+
}
|
| 27 |
+
: isRunning
|
| 28 |
+
? {
|
| 29 |
+
text: "LIVE FEED",
|
| 30 |
+
color: "bg-[var(--mistral-orange)] animate-pulse",
|
| 31 |
+
border: "border-[var(--mistral-orange)]",
|
| 32 |
+
}
|
| 33 |
+
: {
|
| 34 |
+
text: "PAUSED",
|
| 35 |
+
color: "bg-[var(--mistral-orange-dark)]",
|
| 36 |
+
border: "border-[var(--mistral-beige-dark)]",
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
return (
|
| 40 |
+
<>
|
| 41 |
+
{/* Controls Layer */}
|
| 42 |
+
<div className="absolute inset-0 z-20 flex flex-col justify-between p-6 pointer-events-none">
|
| 43 |
+
{/* Top Bar */}
|
| 44 |
+
<div className="flex justify-between items-start pointer-events-auto">
|
| 45 |
+
{/* Status Indicator */}
|
| 46 |
+
<div
|
| 47 |
+
className="backdrop-blur-md border rounded-sm px-3 py-1.5 flex items-center space-x-3 shadow-sm"
|
| 48 |
+
style={{
|
| 49 |
+
backgroundColor: `${THEME.beigeLight}E6`,
|
| 50 |
+
borderColor: THEME.beigeDark,
|
| 51 |
+
}}
|
| 52 |
+
>
|
| 53 |
+
<div className="relative flex h-2.5 w-2.5">
|
| 54 |
+
{isRunning && !hasError && (
|
| 55 |
+
<span
|
| 56 |
+
className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${statusConfig.color}`}
|
| 57 |
+
></span>
|
| 58 |
+
)}
|
| 59 |
+
<span
|
| 60 |
+
className={`relative inline-flex rounded-full h-2.5 w-2.5 ${statusConfig.color}`}
|
| 61 |
+
></span>
|
| 62 |
+
</div>
|
| 63 |
+
<span
|
| 64 |
+
className="text-xs font-mono font-bold tracking-widest"
|
| 65 |
+
style={{ color: THEME.textBlack }}
|
| 66 |
+
>
|
| 67 |
+
{statusConfig.text}
|
| 68 |
+
</span>
|
| 69 |
+
</div>
|
| 70 |
+
{/* Controls */}
|
| 71 |
+
<div className="flex gap-2 items-center">
|
| 72 |
+
{/* Resolution Slider */}
|
| 73 |
+
{imageSize && onImageSizeChange && (
|
| 74 |
+
<div
|
| 75 |
+
className="hidden md:flex items-center gap-3 backdrop-blur-md border rounded-sm px-3 py-1.5 shadow-sm mr-2 group relative"
|
| 76 |
+
style={{
|
| 77 |
+
backgroundColor: `${THEME.beigeLight}E6`,
|
| 78 |
+
borderColor: THEME.beigeDark,
|
| 79 |
+
}}
|
| 80 |
+
>
|
| 81 |
+
<div className="flex flex-col items-start gap-1 my-1">
|
| 82 |
+
<span className="text-[8px] font-mono text-gray-400 uppercase tracking-wider leading-none mb-1">
|
| 83 |
+
Input Size: {imageSize}px
|
| 84 |
+
</span>
|
| 85 |
+
<input
|
| 86 |
+
type="range"
|
| 87 |
+
min="256"
|
| 88 |
+
max="960"
|
| 89 |
+
step="32"
|
| 90 |
+
value={imageSize}
|
| 91 |
+
onChange={(e) => onImageSizeChange(Number(e.target.value))}
|
| 92 |
+
className="w-24 h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-[var(--mistral-orange)]"
|
| 93 |
+
/>
|
| 94 |
+
</div>
|
| 95 |
+
{/* Tooltip */}
|
| 96 |
+
<div
|
| 97 |
+
className="absolute top-full right-0 mt-2 w-54 p-2 text-white text-[10px] rounded shadow-xl opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50 font-mono"
|
| 98 |
+
style={{ backgroundColor: THEME.textBlack }}
|
| 99 |
+
>
|
| 100 |
+
<p className="mb-1">
|
| 101 |
+
<span style={{ color: THEME.mistralOrange }}><</span>{" "}
|
| 102 |
+
Lower = Faster (Less accurate)
|
| 103 |
+
</p>
|
| 104 |
+
<p>
|
| 105 |
+
<span style={{ color: THEME.mistralOrange }}>></span>{" "}
|
| 106 |
+
Higher = Slower (More accurate)
|
| 107 |
+
</p>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
)}
|
| 111 |
+
<Button
|
| 112 |
+
onClick={onToggleRunning}
|
| 113 |
+
aria-label={isRunning ? "Pause captioning" : "Resume captioning"}
|
| 114 |
+
className="backdrop-blur-md bg-white/90 hover:bg-white border hover:border-[var(--mistral-orange)] hover:text-[var(--mistral-orange)] p-2.5 rounded-sm shadow-sm transition-all"
|
| 115 |
+
>
|
| 116 |
+
{isRunning ? (
|
| 117 |
+
<svg
|
| 118 |
+
className="w-6 h-6"
|
| 119 |
+
fill="currentColor"
|
| 120 |
+
viewBox="0 0 20 20"
|
| 121 |
+
>
|
| 122 |
+
<path
|
| 123 |
+
fillRule="evenodd"
|
| 124 |
+
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z"
|
| 125 |
+
clipRule="evenodd"
|
| 126 |
+
/>
|
| 127 |
+
</svg>
|
| 128 |
+
) : (
|
| 129 |
+
<svg
|
| 130 |
+
className="w-6 h-6"
|
| 131 |
+
fill="currentColor"
|
| 132 |
+
viewBox="0 0 20 20"
|
| 133 |
+
>
|
| 134 |
+
<path
|
| 135 |
+
fillRule="evenodd"
|
| 136 |
+
d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
|
| 137 |
+
clipRule="evenodd"
|
| 138 |
+
/>
|
| 139 |
+
</svg>
|
| 140 |
+
)}
|
| 141 |
+
</Button>
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
</>
|
| 146 |
+
);
|
| 147 |
+
}
|