import { useEffect, useRef, useState } from "react"; import { addTokenToUrl } from "../../services/auth"; import * as api from "../../services/api"; interface Props { instanceId: string; tabId: string; label: string; url: string; quality?: number; maxWidth?: number; fps?: number; showTitle?: boolean; } type Status = "connecting" | "streaming" | "error"; export default function ScreencastTile({ instanceId, tabId, label, url, quality = 30, maxWidth = 800, fps = 1, showTitle = true, }: Props) { const canvasRef = useRef(null); const socketRef = useRef(null); const [status, setStatus] = useState("connecting"); const [fpsDisplay, setFpsDisplay] = useState("—"); const [sizeDisplay, setSizeDisplay] = useState("—"); const [localFps, setLocalFps] = useState(fps); const [isCapturing, setIsCapturing] = useState(false); const [isPdfGenerating, setIsPdfGenerating] = useState(false); const [fallbackUrl, setFallbackUrl] = useState(null); // Reset local FPS when the tab changes to match the new tab's initial request useEffect(() => { setLocalFps(fps); setStatus("connecting"); setFallbackUrl(null); }, [tabId, fps]); // Clean up static preview URL on unmount or tab change useEffect(() => { return () => { if (fallbackUrl) { URL.revokeObjectURL(fallbackUrl); } }; }, [fallbackUrl]); const takeScreenshot = async () => { if (isCapturing) return; setIsCapturing(true); try { const blob = await api.fetchTabScreenshot(tabId, "png"); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `screenshot-${tabId}-${Date.now()}.png`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } catch (e) { console.error("Screenshot capture failed", e); } finally { setIsCapturing(false); } }; const downloadPdf = async () => { if (isPdfGenerating) return; setIsPdfGenerating(true); try { const blob = await api.fetchTabPdf(tabId); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `page-${tabId}-${Date.now()}.pdf`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } catch (e) { console.error("PDF generation failed", e); } finally { setIsPdfGenerating(false); } }; const captureFallback = async () => { try { const blob = await api.fetchTabScreenshot(tabId, "png"); if (fallbackUrl) URL.revokeObjectURL(fallbackUrl); setFallbackUrl(URL.createObjectURL(blob)); } catch (e) { console.error("Fallback capture failed", e); } }; useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; const params = new URLSearchParams({ tabId, quality: String(quality), maxWidth: String(maxWidth), fps: String(localFps), }); const path = addTokenToUrl( `/instances/${encodeURIComponent(instanceId)}/proxy/screencast?${params.toString()}`, ); const wsUrl = new URL(path, window.location.origin); wsUrl.protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const socket = new WebSocket(wsUrl.toString()); socket.binaryType = "arraybuffer"; socketRef.current = socket; let frameCount = 0; let lastFpsTime = Date.now(); socket.onopen = () => { setStatus("streaming"); }; socket.onmessage = (evt) => { const blob = new Blob([evt.data], { type: "image/jpeg" }); const imgUrl = URL.createObjectURL(blob); const img = new Image(); img.onload = () => { canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); URL.revokeObjectURL(imgUrl); }; img.src = imgUrl; frameCount++; const now = Date.now(); if (now - lastFpsTime >= 1000) { setFpsDisplay(`${frameCount} fps`); setSizeDisplay(`${(evt.data.byteLength / 1024).toFixed(0)} KB/frame`); frameCount = 0; lastFpsTime = now; } }; socket.onerror = () => { setStatus("error"); }; socket.onclose = () => { setStatus("error"); }; return () => { socket.close(); socketRef.current = null; }; }, [instanceId, tabId, quality, maxWidth, localFps]); const statusColor = status === "streaming" ? "bg-success" : status === "connecting" ? "bg-warning" : "bg-destructive"; return (
{/* Header */} {showTitle && (
{label}
{url}
)} {/* Canvas */}
{status === "error" && fallbackUrl ? ( Tab preview ) : ( )} {status === "error" && (
Connection lost
{!fallbackUrl && ( )}
)}
{localFps} FPS ({fpsDisplay})
{sizeDisplay}
); }