import { useEffect, useRef, useState } from "react";
import RFB from "@novnc/novnc";
const INFERENCE_API_BASE_URL = "https://inference-api.optexity.com";
async function getStreamUrl(taskId, apiKey) {
const res = await fetch(
`${INFERENCE_API_BASE_URL}/api/v1/tasks/${taskId}/stream`,
{ headers: { "x-api-key": apiKey } },
);
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.detail || `HTTP ${res.status}`);
}
return res.json();
}
export function LiveStreamViewer({ taskId, apiKey }) {
const containerRef = useRef(null);
const rfbRef = useRef(null);
const [state, setState] = useState("loading");
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
(async () => {
try {
const { stream_url } = await getStreamUrl(taskId, apiKey);
if (cancelled || !containerRef.current) return;
setState("connecting");
const rfb = new RFB(containerRef.current, stream_url, {
wsProtocols: ["binary"],
credentials: {},
});
rfb.viewOnly = true;
rfb.scaleViewport = true;
rfb.resizeSession = false;
rfb.addEventListener("connect", () => setState("connected"));
rfb.addEventListener("disconnect", (e) => {
setState(e.detail?.clean ? "disconnected" : "error");
if (!e.detail?.clean) setError("Connection lost");
});
rfb.addEventListener("credentialsrequired", () =>
rfb.sendCredentials({}),
);
rfb.addEventListener("securityfailure", () => {
setError("Security negotiation failed");
setState("error");
});
rfbRef.current = rfb;
} catch (err) {
if (!cancelled) {
setError(err.message);
setState("error");
}
}
})();
return () => {
cancelled = true;
if (rfbRef.current) {
rfbRef.current.disconnect();
rfbRef.current = null;
}
};
}, [taskId, apiKey]);
return (
<div>
{state !== "connected" && (
<div>
{state === "loading" && "Initializing stream…"}
{state === "connecting" && "Connecting to browser…"}
{state === "disconnected" && "Stream ended"}
{state === "error" && (error || "Stream error")}
</div>
)}
<div
ref={containerRef}
style={{
width: "100%",
height: "600px",
display: state === "connected" ? "block" : "none",
}}
/>
</div>
);
}