<!DOCTYPE html>
<html>
<head>
  <title>Your Stream</title>
  <meta charset="UTF-8"/>
  <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
  <script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
  <style>
    body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; padding: 16px; }
    #status { opacity: .8; margin: 8px 0 16px; }
    video { width: min(720px, 100%); border-radius: 12px; background: #111; }
  </style>
</head>
<body>
  <h1>Streaming as {{ username }}</h1>
  <div id="status">Connecting…</div>

  <video id="localVideo" autoplay playsinline muted style="transform: scaleX(-1);"></video>

  <script>
    const username = "{{ username }}";

    // Token allows same browser to refresh and reclaim the username without waiting TTL
    const tokenKey = `stream_token:${username}`;
    let streamToken = localStorage.getItem(tokenKey);
    if (!streamToken) {
      streamToken = crypto.randomUUID();
      localStorage.setItem(tokenKey, streamToken);
    }

    const socket = io({ transports: ["websocket"] });

    const statusEl = document.getElementById("status");
    const peerConnections = {};

    let localStream = null;
    let heartbeatTimer = null;
    let accepted = false;

    socket.on("connect", () => {
      console.log("[Streamer] Connected:", socket.id);
      statusEl.textContent = "Connected. Claiming username…";
      socket.emit("join_room", { username, role: "streamer", streamToken });
    });

    socket.on("disconnect", () => {
      statusEl.textContent = "Disconnected.";
      accepted = false;
      if (heartbeatTimer) clearInterval(heartbeatTimer);
      heartbeatTimer = null;
    });

    socket.on("stream_accepted", async () => {
      accepted = true;
      statusEl.textContent = "Live ✅ Starting camera…";
      await startLocalCamera();

      heartbeatTimer = setInterval(() => {
        socket.emit("streamer_heartbeat", { username });
      }, 20000);

      statusEl.textContent = "Live ✅ Waiting for viewers…";
    });

    socket.on("stream_denied", (msg) => {
      const reason = msg?.reason || "denied";
      console.warn("[Streamer] stream_denied", msg);

      if (reason === "taken") {
        statusEl.textContent = `Username "${username}" is already live.`;
        alert(`Can't stream as "${username}" — already taken.`);
      } else {
        statusEl.textContent = `Streaming denied: ${reason}`;
        alert(`Streaming denied: ${reason}`);
      }
      cleanupLocal();
    });

    async function startLocalCamera() {
      if (localStream) return;
      try {
        localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
        document.getElementById("localVideo").srcObject = localStream;
      } catch (err) {
        console.error("Error accessing camera:", err);
        statusEl.textContent = "Camera/mic permission error.";
      }
    }

    function cleanupLocal() {
      try {
        if (localStream) localStream.getTracks().forEach(t => t.stop());
      } catch (e) {}
      localStream = null;

      Object.values(peerConnections).forEach(pc => { try { pc.close(); } catch (e) {} });
      for (const k of Object.keys(peerConnections)) delete peerConnections[k];

      if (heartbeatTimer) clearInterval(heartbeatTimer);
      heartbeatTimer = null;
    }

    socket.on("new_watcher", (data) => {
      if (!accepted) return;
      if (!localStream) return;

      const watcherSid = data.watcherSid;
      console.log("[Streamer] new_watcher:", watcherSid);
      createOfferForWatcher(watcherSid);
    });

    async function createOfferForWatcher(watcherSid) {
      const pc = new RTCPeerConnection({
        iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
      });
      peerConnections[watcherSid] = pc;

      localStream.getTracks().forEach((track) => pc.addTrack(track, localStream));

      pc.onicecandidate = (event) => {
        if (event.candidate) {
          socket.emit("ice-candidate", {
            username,
            role: "streamer",
            candidate: event.candidate.candidate,
            sdpMid: event.candidate.sdpMid,
            sdpMLineIndex: event.candidate.sdpMLineIndex,
            targetSid: watcherSid
          });
        }
      };

      const offer = await pc.createOffer();
      await pc.setLocalDescription(offer);

      socket.emit("offer", {
        username,
        offer: offer.sdp,
        offerType: offer.type,
        watcherSid
      });
    }

    socket.on("answer", async (data) => {
      const { answer, answerType, watcherSid } = data;
      const pc = peerConnections[watcherSid];
      if (!pc) return;

      await pc.setRemoteDescription(new RTCSessionDescription({
        type: answerType,
        sdp: answer
      }));
    });

    socket.on("ice-candidate", (data) => {
      const { candidate, sdpMid, sdpMLineIndex, senderSid } = data;
      const pc = peerConnections[senderSid];
      if (!pc || !candidate) return;

      pc.addIceCandidate(new RTCIceCandidate({ candidate, sdpMid, sdpMLineIndex }))
        .catch(err => console.error("Error adding ICE candidate:", err));
    });
  </script>
</body>
</html>

