1const e=`import { useState, useRef, useEffect } from 'react'; 2import { useAuth } from '@/hooks/useAuth'; 3import { supabase } from '@/integrations/supabase/client'; 4import { Button } from '@/components/ui/button'; 5import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; 6import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; 7import { 8 Video, 9 Monitor, 10 Mic, 11 Square, 12 Circle, 13 Download, 14 Upload, 15 Trash2, 16 Play, 17 Pause 18} from 'lucide-react'; 19import { toast } from 'sonner'; 20import { Input } from '@/components/ui/input'; 21import { Label } from '@/components/ui/label'; 22 23type RecordingMode = 'audio' | 'video' | 'screen' | 'screen_with_audio'; 24 25export function MediaRecorder() { 26 const { user } = useAuth(); 27 const [mode, setMode] = useState<RecordingMode>('audio'); 28 const [isRecording, setIsRecording] = useState(false); 29 const [isPaused, setIsPaused] = useState(false); 30 const [recordingTime, setRecordingTime] = useState(0); 31 const [recordedBlob, setRecordedBlob] = useState<Blob | null>(null); 32 const [recordingTitle, setRecordingTitle] = useState(''); 33 const [uploading, setUploading] = useState(false); 34 35 const mediaRecorderRef = useRef<MediaRecorder | null>(null); 36 const streamRef = useRef<MediaStream | null>(null); 37 const chunksRef = useRef<Blob[]>([]); 38 const timerRef = useRef<number | null>(null); 39 const videoPreviewRef = useRef<HTMLVideoElement>(null); 40 41 useEffect(() => { 42 return () => { 43 stopRecording(); 44 if (streamRef.current) { 45 streamRef.current.getTracks().forEach(track => track.stop()); 46 } 47 }; 48 }, []); 49 50 const startRecording = async () => { 51 try { 52 let stream: MediaStream; 53 54 switch (mode) { 55 case 'audio': 56 stream = await navigator.mediaDevices.getUserMedia({ audio: true }); 57 break; 58 59 case 'video': 60 stream = await navigator.mediaDevices.getUserMedia({ 61 video: { width: 1920, height: 1080 }, 62 audio: true 63 }); 64 if (videoPreviewRef.current) { 65 videoPreviewRef.current.srcObject = stream; 66 } 67 break; 68 69 case 'screen': 70 stream = await (navigator.mediaDevices as any).getDisplayMedia({ 71 video: { width: 1920, height: 1080 } 72 }); 73 if (videoPreviewRef.current) { 74 videoPreviewRef.current.srcObject = stream; 75 } 76 break; 77 78 case 'screen_with_audio': 79 const screenStream = await (navigator.mediaDevices as any).getDisplayMedia({ 80 video: { width: 1920, height: 1080 } 81 }); 82 const audioStream = await navigator.mediaDevices.getUserMedia({ audio: true }); 83 stream = new MediaStream(); 84 screenStream.getVideoTracks().forEach((track: MediaStreamTrack) => stream.addTrack(track)); 85 audioStream.getAudioTracks().forEach((track: MediaStreamTrack) => stream.addTrack(track)); 86 if (videoPreviewRef.current) { 87 videoPreviewRef.current.srcObject = stream; 88 } 89 break; 90 91 default: 92 throw new Error('Invalid recording mode'); 93 } 94 95 streamRef.current = stream; 96 chunksRef.current = []; 97 98 const mediaRecorder = new (window as any).MediaRecorder(stream, { 99 mimeType: mode === 'audio' ? 'audio/webm' : 'video/webm' 100 }) as MediaRecorder; 101 102 mediaRecorder.ondataavailable = (e) => { 103 if (e.data.size > 0) { 104 chunksRef.current.push(e.data); 105 } 106 }; 107 108 mediaRecorder.onstop = () => { 109 const blob = new Blob(chunksRef.current, { 110 type: mode === 'audio' ? 'audio/webm' : 'video/webm' 111 }); 112 setRecordedBlob(blob); 113 114 if (streamRef.current) { 115 streamRef.current.getTracks().forEach(track => track.stop()); 116 } 117 if (timerRef.current) { 118 clearInterval(timerRef.current); 119 } 120 }; 121 122 mediaRecorder.start(1000); // Capture data every second 123 mediaRecorderRef.current = mediaRecorder; 124 setIsRecording(true); 125 126 // Start timer 127 timerRef.current = window.setInterval(() => { 128 setRecordingTime(prev => prev + 1); 129 }, 1000); 130 131 toast.success(\`\${mode} recording started\`); 132 } catch (error: any) { 133 console.error('Error starting recording:', error); 134 toast.error(\`Failed to start recording: \${error.message}\`); 135 } 136 }; 137 138 const stopRecording = () => { 139 if (mediaRecorderRef.current && isRecording) { 140 mediaRecorderRef.current.stop(); 141 setIsRecording(false); 142 setIsPaused(false); 143 144 if (videoPreviewRef.current) { 145 videoPreviewRef.current.srcObject = null; 146 } 147 } 148 }; 149 150 const pauseRecording = () => { 151 if (mediaRecorderRef.current && isRecording) { 152 if (isPaused) { 153 mediaRecorderRef.current.resume(); 154 setIsPaused(false); 155 if (timerRef.current) { 156 timerRef.current = window.setInterval(() => { 157 setRecordingTime(prev => prev + 1); 158 }, 1000); 159 } 160 } else { 161 mediaRecorderRef.current.pause(); 162 setIsPaused(true); 163 if (timerRef.current) { 164 clearInterval(timerRef.current); 165 } 166 } 167 } 168 }; 169
170 const uploadToCloud = async () => { 171 if (!recordedBlob || !user) return; 172 173 setUploading(true); 174 try { 175 const fileName = \`\${user.id}/\${Date.now()}-\${mode}.webm\`; 176 const bucket = mode === 'audio' ? 'audio-snippets' : 177 mode === 'video' ? 'video-recordings' : 178 'screen-recordings'; 179 180 // Upload to storage 181 const { error: uploadError } = await supabase.storage 182 .from(bucket) 183 .upload(fileName, recordedBlob); 184 185 if (uploadError) throw uploadError; 186 187 // Save metadata to database 188 const { error: dbError } = await supabase 189 .from('recordings') 190 .insert({ 191 user_id: user.id, 192 title: recordingTitle || \`\${mode} Recording \${new Date().toLocaleString()}\`, 193 recording_type: mode, 194 storage_path: \`\${bucket}/\${fileName}\`, 195 duration: recordingTime, 196 file_size: recordedBlob.size, 197 }); 198 199 if (dbError) throw dbError; 200 201 toast.success('Recording uploaded successfully!'); 202 setRecordedBlob(null); 203 setRecordingTitle(''); 204 setRecordingTime(0); 205 } catch (error: any) { 206 console.error('Error uploading recording:', error); 207 toast.error(\`Upload failed: \${error.message}\`); 208 } finally { 209 setUploading(false); 210 } 211 }; 212 213 const downloadToDesktop = () => { 214 if (!recordedBlob) return; 215 216 const url = URL.createObjectURL(recordedBlob); 217 const a = document.createElement('a'); 218 a.href = url; 219 a.download = \`\${mode}-recording-\${Date.now()}.webm\`; 220 document.body.appendChild(a); 221 a.click(); 222 document.body.removeChild(a); 223 URL.revokeObjectURL(url); 224 225 toast.success('Recording downloaded!'); 226 }; 227 228 const formatTime = (seconds: number) => { 229 const mins = Math.floor(seconds / 60); 230 const secs = seconds % 60; 231 return \`\${mins.toString().padStart(2, '0')}:\${secs.toString().padStart(2, '0')}\`; 232 }; 233 234 return ( 235 <div className="space-y-6"> 236 <Card> 237 <CardHeader> 238 <CardTitle>Recording Mode</CardTitle> 239 <CardDescription>Choose what type of recording you want to create</CardDescription> 240 </CardHeader> 241 <CardContent> 242 <Tabs value={mode} onValueChange={(v) => setMode(v as RecordingMode)}> 243 <TabsList className="grid w-full grid-cols-4"> 244 <TabsTrigger value="audio" className="gap-2"> 245 <Mic className="h-4 w-4" /> 246 Audio 247 </TabsTrigger> 248 <TabsTrigger value="video" className="gap-2"> 249 <Video className="h-4 w-4" /> 250 Video 251 </TabsTrigger> 252 <TabsTrigger value="screen" className="gap-2">
253 <Monitor className="h-4 w-4" /> 254 Screen 255 </TabsTrigger> 256 <TabsTrigger value="screen_with_audio" className="gap-2"> 257 <Monitor className="h-4 w-4" /> 258 <Mic className="h-3 w-3" /> 259 Screen + Mic 260 </TabsTrigger> 261 </TabsList> 262 263 <TabsContent value={mode} className="mt-6"> 264 <div className="space-y-4"> 265 {/* Video Preview */} 266 {(mode === 'video' || mode === 'screen' || mode === 'screen_with_audio') && ( 267 <div className="aspect-video bg-muted rounded-lg overflow-hidden"> 268 <video 269 ref={videoPreviewRef} 270 autoPlay 271 muted={mode !== 'video'} 272 className="w-full h-full object-contain" 273 /> 274 </div> 275 )} 276 277 {/* Recording Controls */} 278 <div className="flex items-center justify-center gap-4"> 279 {!isRecording && !recordedBlob && ( 280 <Button 281 size="lg" 282 onClick={startRecording} 283 className="gap-2" 284 > 285 <Circle className="h-5 w-5 fill-current" /> 286 Start Recording 287 </Button> 288 )} 289 290 {isRecording && ( 291 <> 292 <div className="text-2xl font-mono font-bold text-red-500"> 293 {formatTime(recordingTime)} 294 </div> 295 <Button 296 size="lg" 297 variant="outline" 298 onClick={pauseRecording} 299 className="gap-2" 300 > 301 {isPaused ? ( 302 <><Play className="h-5 w-5" /> Resume</> 303 ) : ( 304 <><Pause className="h-5 w-5" /> Pause</> 305 )} 306 </Button> 307 <Button 308 size="lg" 309 variant="destructive" 310 onClick={stopRecording} 311 className="gap-2" 312 > 313 <Square className="h-5 w-5" /> 314 Stop 315 </Button> 316 </> 317 )} 318 </div> 319 320 {/* Recorded Blob Actions */} 321 {recordedBlob && !isRecording && ( 322 <div className="space-y-4 p-4 border border-border rounded-lg"> 323 <div className="space-y-2"> 324 <Label htmlFor="title">Recording Title</Label> 325 <Input 326 id="title" 327 value={recordingTitle} 328 onChange={(e) => setRecordingTitle(e.target.value)} 329 placeholder={\`\${mode} Recording \${new Date().toLocaleString()}\`} 330 /> 331 </div> 332 333 <div className="flex gap-2"> 334 <Button 335 onClick={uploadToCloud} 336 disabled={uploading} 337 className="flex-1 gap-2" 338 > 339 <Upload className="h-4 w-4" /> 340 {uploading ? 'Uploading...' : 'Save to Pendragon Cloud'} 341 </Button> 342 <Button 343 onClick={downloadToDesktop} 344 variant="outline" 345 className="flex-1 gap-2" 346 > 347 <Download className="h-4 w-4" /> 348 Download to Desktop 349 </Button> 350 <Button 351 onClick={() => { 352 setRecordedBlob(null); 353 setRecordingTitle(''); 354 setRecordingTime(0); 355 }} 356 variant="destructive" 357 size="icon" 358 > 359 <Trash2 className="h-4 w-4" /> 360 </Button> 361 </div> 362 363 <div className="text-sm text-muted-foreground"> 364 Duration: {formatTime(recordingTime)} | Size: {(recordedBlob.size / 1024 / 1024).toFixed(2)} MB 365 </div> 366 </div> 367 )} 368 </div> 369 </TabsContent> 370 </Tabs> 371 </CardContent> 372 </Card> 373 </div> 374 ); 375} 376`;export{e as default};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.