PageSourceSearch

https://pendragonx.com/assets/MediaRecorder-C_s6oKFC.js

js pendragonx.com collected 2026-10-02 01:43:34 UTC 12,842 bytes, 376 lines download raw bytes

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.