browserpilot / frontend /src /components /StreamingViewer.tsx
ncolex's picture
Deploy BrowserPilot with NumPy fix (2.2.6)
c1f04cf verified
import React, { useState, useEffect, useRef } from 'react';
import { Monitor, Play, Square, Camera, Settings, Circle, Maximize2 } from 'lucide-react';
import { WebSocketManager } from '../services/WebSocketManager';
interface StreamingViewerProps {
wsManager: WebSocketManager;
jobId: string | null;
autoConnect?: boolean;
}
export const StreamingViewer: React.FC<StreamingViewerProps> = ({ wsManager, jobId, autoConnect = false }) => {
const [isConnected, setIsConnected] = useState(false);
const [currentFrame, setCurrentFrame] = useState<string | null>(null);
const [streamStats, setStreamStats] = useState({ frameCount: 0, fps: 0 });
const [showStream, setShowStream] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const lastFrameTimeRef = useRef<number | null>(null);
useEffect(() => {
wsManager.on('stream_connected', () => {
setIsConnected(true);
});
wsManager.on('stream_disconnected', () => {
setIsConnected(false);
setStreamStats({ frameCount: 0, fps: 0 });
lastFrameTimeRef.current = null;
});
wsManager.on('stream_frame', (data: any) => {
setCurrentFrame(data.data);
const now = performance.now();
const lastFrame = lastFrameTimeRef.current;
const fps = lastFrame ? Math.round(1000 / Math.max(now - lastFrame, 1)) : 0;
lastFrameTimeRef.current = now;
setStreamStats(prev => ({
frameCount: prev.frameCount + 1,
fps: fps || prev.fps
}));
});
wsManager.on('streaming_info', (data: any) => {
if (data.streaming?.enabled) {
setShowStream(true);
}
});
}, [wsManager]);
useEffect(() => {
if (autoConnect && jobId && !wsManager.isStreamConnected()) {
setShowStream(true);
wsManager.connectStream(jobId);
} else {
console.log('Auto-connect conditions not met:', {
autoConnect: !!autoConnect,
jobId: !!jobId,
notConnected: !wsManager.isStreamConnected()
});
}
if (!jobId) {
setCurrentFrame(null);
setStreamStats({ frameCount: 0, fps: 0 });
lastFrameTimeRef.current = null;
setIsConnected(false);
}
}, [autoConnect, jobId, wsManager]);
const handleConnect = () => {
if (jobId) {
wsManager.connectStream(jobId);
}
};
const handleDisconnect = () => {
wsManager.disconnectStream();
};
const handleStreamClick = (e: React.MouseEvent<HTMLImageElement>) => {
if (!isConnected) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = Math.round((e.clientX - rect.left) * (1280 / rect.width));
const y = Math.round((e.clientY - rect.top) * (800 / rect.height));
wsManager.sendStreamMessage({
type: 'mouse',
eventType: 'mousePressed',
x,
y,
button: 'left',
clickCount: 1
});
setTimeout(() => {
wsManager.sendStreamMessage({
type: 'mouse',
eventType: 'mouseReleased',
x,
y,
button: 'left'
});
}, 100);
};
if (!showStream) {
return (
<div className="bg-white/70 backdrop-blur-sm rounded-2xl shadow-sm border border-stone-200/60 overflow-hidden transition-all duration-300 hover:shadow-md hover:bg-white/80">
<div className="p-8">
<div className="flex items-center space-x-3 mb-6">
<div className="w-10 h-10 bg-gradient-to-br from-purple-400 to-pink-500 rounded-xl flex items-center justify-center shadow-md">
<Monitor className="w-5 h-5 text-white" />
</div>
<div>
<h2 className="text-lg font-medium text-stone-800">Live Browser View</h2>
<p className="text-sm text-stone-600 font-light">Real-time browser streaming with interaction</p>
</div>
</div>
<div className="bg-stone-50/80 dark:bg-stone-800/50 backdrop-blur-sm rounded-xl border-2 border-dashed border-stone-300/60 dark:border-stone-600/60 p-16 text-center">
<div className="w-20 h-20 bg-stone-200/80 dark:bg-stone-700/80 rounded-2xl mx-auto mb-6 flex items-center justify-center">
<Monitor className="w-10 h-10 text-stone-400 dark:text-stone-500" />
</div>
<h3 className="text-xl font-medium text-stone-800 dark:text-stone-200 mb-3">Browser Streaming</h3>
<p className="text-stone-600 dark:text-stone-400 font-light mb-6 max-w-md mx-auto leading-relaxed">
Enable streaming to watch BrowserPilot navigate websites in real-time
</p>
<button
onClick={() => setShowStream(true)}
className="px-6 py-3 bg-gradient-to-r from-stone-500 to-stone-600 text-white rounded-xl hover:from-stone-600 hover:to-stone-700 transition-all duration-300 flex items-center space-x-3 mx-auto group shadow-md hover:shadow-lg"
>
<Play className="w-5 h-5 group-hover:scale-110 transition-transform duration-300" />
<span className="font-medium">Enable Live Streaming</span>
</button>
</div>
</div>
</div>
);
}
return (
<div className="bg-white/70 backdrop-blur-sm rounded-2xl shadow-sm border border-stone-200/60 overflow-hidden transition-all duration-300 hover:shadow-md hover:bg-white/80">
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-br from-stone-400 to-stone-500 dark:from-stone-500 dark:to-stone-600 rounded-xl flex items-center justify-center shadow-md">
<Monitor className="w-5 h-5 text-white" />
</div>
<div>
<h2 className="text-lg font-medium text-stone-800 dark:text-stone-200">Live Browser View</h2>
<p className="text-sm text-stone-600 dark:text-stone-400 font-light">Real-time browser streaming with interaction</p>
</div>
</div>
<div className="flex items-center space-x-4">
<div className={`flex items-center space-x-2 px-3 py-1 rounded-full text-xs font-medium transition-all duration-300 ${
isConnected
? 'bg-sage-100/80 dark:bg-sage-800/50 text-sage-800 dark:text-sage-200 backdrop-blur-sm'
: 'bg-blush-100/80 dark:bg-blush-800/50 text-blush-800 dark:text-blush-200 backdrop-blur-sm'
}`}>
<Circle className={`w-2 h-2 ${isConnected ? 'text-sage-600 dark:text-sage-400 fill-current animate-pulse' : 'text-blush-600 dark:text-blush-400 fill-current'}`} />
<span>{isConnected ? 'Live' : 'Offline'}</span>
</div>
<span className="text-xs text-stone-500 dark:text-stone-400 font-light">{streamStats.fps} FPS</span>
</div>
</div>
<div className="relative bg-stone-900 dark:bg-stone-950 rounded-xl overflow-hidden border border-stone-200/60 dark:border-stone-600/60 shadow-lg">
{currentFrame ? (
<img
src={`data:image/jpeg;base64,${currentFrame}`}
alt="Browser Stream"
className="w-full h-auto cursor-pointer hover:opacity-95 transition-opacity duration-200"
onClick={handleStreamClick}
/>
) : (
<div className="aspect-video flex items-center justify-center bg-stone-900 dark:bg-stone-950">
<div className="text-center animate-in fade-in duration-500">
<Monitor className="w-16 h-16 text-stone-400 dark:text-stone-500 mx-auto mb-4" />
<p className="text-stone-300 dark:text-stone-400 text-sm font-light">Waiting for stream data...</p>
</div>
</div>
)}
{/* Stream Controls Overlay */}
<div className="absolute bottom-4 left-4 right-4">
<div className="bg-black/70 backdrop-blur-md rounded-xl p-4 border border-white/10">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<button
onClick={handleConnect}
disabled={isConnected || !jobId}
className="px-4 py-2 bg-emerald-500 text-white rounded-lg text-sm hover:bg-emerald-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center space-x-2 group"
>
<Play className="w-4 h-4 group-hover:scale-110 transition-transform duration-200" />
<span>Connect</span>
</button>
<button
onClick={handleDisconnect}
disabled={!isConnected}
className="px-4 py-2 bg-rose-500 text-white rounded-lg text-sm hover:bg-rose-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center space-x-2 group"
>
<Square className="w-4 h-4 group-hover:scale-110 transition-transform duration-200" />
<span>Disconnect</span>
</button>
<button className="px-4 py-2 bg-blue-500 text-white rounded-lg text-sm hover:bg-blue-600 transition-all duration-200 flex items-center space-x-2 group">
<Camera className="w-4 h-4 group-hover:scale-110 transition-transform duration-200" />
<span>Capture</span>
</button>
</div>
<div className="flex items-center space-x-3 text-white text-sm">
<button className="p-2 hover:bg-white/10 rounded-lg transition-colors duration-200">
<Maximize2 className="w-4 h-4" />
</button>
<Settings className="w-4 h-4" />
<select className="bg-stone-700/80 text-white rounded-lg px-3 py-1 text-xs border-0 focus:ring-2 focus:ring-purple-500 backdrop-blur-sm">
<option value="60">Low Quality</option>
<option value="80" selected>Medium Quality</option>
<option value="100">High Quality</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};