Spaces:
Running
Running
import { useState, useEffect, useCallback, useMemo } from "react"; | |
import GlassContainer from "./GlassContainer"; | |
import GlassButton from "./GlassButton"; | |
import { GLASS_EFFECTS } from "../constants"; | |
const ERROR_TYPES = { | |
HTTPS: "https", | |
NOT_SUPPORTED: "not-supported", | |
PERMISSION: "permission", | |
GENERAL: "general", | |
} as const; | |
const VIDEO_CONSTRAINTS = { | |
video: { | |
width: { ideal: 1920, max: 1920 }, | |
height: { ideal: 1080, max: 1080 }, | |
facingMode: "user", | |
}, | |
}; | |
const SCREEN_CONSTRAINTS = { | |
video: { | |
width: { ideal: 1920, max: 1920 }, | |
height: { ideal: 1080, max: 1080 }, | |
}, | |
audio: false, | |
}; | |
interface ErrorInfo { | |
type: (typeof ERROR_TYPES)[keyof typeof ERROR_TYPES]; | |
message: string; | |
} | |
interface InputSourceDialogProps { | |
onSourceSelected: (stream: MediaStream, sourceType: 'webcam' | 'screen' | 'file') => void; | |
} | |
type InputSource = 'webcam' | 'screen' | 'file'; | |
export default function InputSourceDialog({ onSourceSelected }: InputSourceDialogProps) { | |
const [selectedSource, setSelectedSource] = useState<InputSource | null>(null); | |
const [isRequesting, setIsRequesting] = useState(false); | |
const [error, setError] = useState<ErrorInfo | null>(null); | |
const getErrorInfo = (err: unknown): ErrorInfo => { | |
if (!navigator.mediaDevices) { | |
return { | |
type: ERROR_TYPES.HTTPS, | |
message: "Media access requires a secure connection (HTTPS)", | |
}; | |
} | |
if (err instanceof DOMException) { | |
switch (err.name) { | |
case "NotAllowedError": | |
return { | |
type: ERROR_TYPES.PERMISSION, | |
message: "Media access denied", | |
}; | |
case "NotFoundError": | |
return { | |
type: ERROR_TYPES.GENERAL, | |
message: "No camera found", | |
}; | |
case "NotReadableError": | |
return { | |
type: ERROR_TYPES.GENERAL, | |
message: "Camera is in use by another application", | |
}; | |
case "OverconstrainedError": | |
return { | |
type: ERROR_TYPES.GENERAL, | |
message: "Camera doesn't meet requirements", | |
}; | |
case "SecurityError": | |
return { | |
type: ERROR_TYPES.HTTPS, | |
message: "Security error accessing media", | |
}; | |
default: | |
return { | |
type: ERROR_TYPES.GENERAL, | |
message: `Media error: ${err.name}`, | |
}; | |
} | |
} | |
return { | |
type: ERROR_TYPES.GENERAL, | |
message: "Failed to access media", | |
}; | |
}; | |
const requestWebcamAccess = useCallback(async () => { | |
setIsRequesting(true); | |
setError(null); | |
try { | |
if (!navigator.mediaDevices?.getUserMedia) { | |
throw new Error("NOT_SUPPORTED"); | |
} | |
const stream = await navigator.mediaDevices.getUserMedia(VIDEO_CONSTRAINTS); | |
onSourceSelected(stream, 'webcam'); | |
} catch (err) { | |
const errorInfo = getErrorInfo(err); | |
setError(errorInfo); | |
console.error("Error accessing webcam:", err, errorInfo); | |
} finally { | |
setIsRequesting(false); | |
} | |
}, [onSourceSelected]); | |
const requestScreenAccess = useCallback(async () => { | |
setIsRequesting(true); | |
setError(null); | |
try { | |
if (!navigator.mediaDevices?.getDisplayMedia) { | |
throw new Error("Screen sharing not supported"); | |
} | |
const stream = await navigator.mediaDevices.getDisplayMedia(SCREEN_CONSTRAINTS); | |
onSourceSelected(stream, 'screen'); | |
} catch (err) { | |
const errorInfo = getErrorInfo(err); | |
setError(errorInfo); | |
console.error("Error accessing screen:", err, errorInfo); | |
} finally { | |
setIsRequesting(false); | |
} | |
}, [onSourceSelected]); | |
const handleFileSelect = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | |
const file = event.target.files?.[0]; | |
if (!file) return; | |
// Create a video element that will be used directly instead of canvas stream | |
const videoUrl = URL.createObjectURL(file); | |
// Create a mock stream that signals this is a file source | |
const canvas = document.createElement('canvas'); | |
canvas.width = 1; | |
canvas.height = 1; | |
const mockStream = canvas.captureStream(1); | |
// Store the video file URL on the stream for later use | |
(mockStream as any).videoFileUrl = videoUrl; | |
onSourceSelected(mockStream, 'file'); | |
}, [onSourceSelected]); | |
const renderIcon = (source: InputSource) => { | |
const iconClass = "w-8 h-8"; | |
switch (source) { | |
case 'webcam': | |
return ( | |
<svg className={`${iconClass} text-blue-400`} fill="currentColor" viewBox="0 0 20 20"> | |
<path fillRule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clipRule="evenodd" /> | |
</svg> | |
); | |
case 'screen': | |
return ( | |
<svg className={`${iconClass} text-green-400`} fill="currentColor" viewBox="0 0 20 20"> | |
<path fillRule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 011 1v8a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm2 1v6h10V5H5z" clipRule="evenodd" /> | |
<path d="M10 15a1 1 0 011-1h2a1 1 0 110 2h-2a1 1 0 01-1-1zM7 14a1 1 0 100 2h2a1 1 0 100-2H7z" /> | |
</svg> | |
); | |
case 'file': | |
return ( | |
<svg className={`${iconClass} text-purple-400`} fill="currentColor" viewBox="0 0 20 20"> | |
<path fillRule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 011 1v1a1 1 0 01-1 1H4a1 1 0 01-1-1v-1zM3 7a1 1 0 011-1h12a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1V7zM4 9h12v2H4V9z" clipRule="evenodd" /> | |
</svg> | |
); | |
} | |
}; | |
if (selectedSource && isRequesting) { | |
return ( | |
<div className="absolute inset-0 text-white flex items-center justify-center p-8"> | |
<GlassContainer className="rounded-3xl shadow-2xl"> | |
<div className="p-8 text-center space-y-6"> | |
<div className="animate-spin rounded-full h-16 w-16 border-4 border-blue-500 border-t-transparent mx-auto" /> | |
<h2 className="text-2xl font-bold text-gray-100"> | |
{selectedSource === 'webcam' ? 'Requesting Camera Access' : | |
selectedSource === 'screen' ? 'Requesting Screen Access' : | |
'Loading Video File'} | |
</h2> | |
<p className="text-gray-400">Please allow access in your browser to continue...</p> | |
</div> | |
</GlassContainer> | |
</div> | |
); | |
} | |
return ( | |
<div className="absolute inset-0 text-white flex items-center justify-center p-8"> | |
<div className="max-w-2xl w-full space-y-6"> | |
<GlassContainer className="rounded-3xl shadow-2xl"> | |
<div className="p-8 text-center space-y-6"> | |
<h2 className="text-3xl font-bold text-gray-100">Choose Input Source</h2> | |
<p className="text-gray-400">Select how you want to provide video for captioning</p> | |
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-8"> | |
{/* Webcam Option */} | |
<GlassContainer | |
className="rounded-2xl p-6 cursor-pointer hover:scale-105 transition-transform duration-200" | |
bgColor={GLASS_EFFECTS.COLORS.DEFAULT_BG} | |
onClick={() => { | |
setSelectedSource('webcam'); | |
requestWebcamAccess(); | |
}} | |
> | |
<div className="flex flex-col items-center space-y-3"> | |
<div className="w-16 h-16 rounded-full bg-blue-500/20 flex items-center justify-center"> | |
{renderIcon('webcam')} | |
</div> | |
<h3 className="text-lg font-semibold text-gray-200">Webcam</h3> | |
<p className="text-sm text-gray-400 text-center">Use your camera for live captioning</p> | |
</div> | |
</GlassContainer> | |
{/* Screen Recording Option */} | |
<GlassContainer | |
className="rounded-2xl p-6 cursor-pointer hover:scale-105 transition-transform duration-200" | |
bgColor={GLASS_EFFECTS.COLORS.DEFAULT_BG} | |
onClick={() => { | |
setSelectedSource('screen'); | |
requestScreenAccess(); | |
}} | |
> | |
<div className="flex flex-col items-center space-y-3"> | |
<div className="w-16 h-16 rounded-full bg-green-500/20 flex items-center justify-center"> | |
{renderIcon('screen')} | |
</div> | |
<h3 className="text-lg font-semibold text-gray-200">Screen</h3> | |
<p className="text-sm text-gray-400 text-center">Record and caption your screen</p> | |
</div> | |
</GlassContainer> | |
{/* Video File Option */} | |
<GlassContainer | |
className="rounded-2xl p-6 cursor-pointer hover:scale-105 transition-transform duration-200" | |
bgColor={GLASS_EFFECTS.COLORS.DEFAULT_BG} | |
> | |
<label className="flex flex-col items-center space-y-3 cursor-pointer"> | |
<div className="w-16 h-16 rounded-full bg-purple-500/20 flex items-center justify-center"> | |
{renderIcon('file')} | |
</div> | |
<h3 className="text-lg font-semibold text-gray-200">Video File</h3> | |
<p className="text-sm text-gray-400 text-center">Upload a video file to caption</p> | |
<input | |
type="file" | |
accept="video/*" | |
className="hidden" | |
onChange={handleFileSelect} | |
/> | |
</label> | |
</GlassContainer> | |
</div> | |
</div> | |
</GlassContainer> | |
{/* Error Display */} | |
{error && ( | |
<GlassContainer | |
className="rounded-2xl shadow-2xl" | |
bgColor={GLASS_EFFECTS.COLORS.ERROR_BG} | |
> | |
<div className="p-6 text-center"> | |
<div className="w-16 h-16 rounded-full bg-red-500/20 flex items-center justify-center mx-auto mb-4"> | |
<svg className="w-8 h-8 text-red-400" fill="currentColor" viewBox="0 0 20 20"> | |
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" /> | |
</svg> | |
</div> | |
<h3 className="text-xl font-bold text-gray-100 mb-2">Access Failed</h3> | |
<p className="text-red-400 mb-4">{error.message}</p> | |
<GlassButton | |
onClick={() => { | |
setError(null); | |
setSelectedSource(null); | |
}} | |
className="px-6 py-3" | |
> | |
Try Different Source | |
</GlassButton> | |
</div> | |
</GlassContainer> | |
)} | |
</div> | |
</div> | |
); | |
} |