JSFiddle - React, Tailwind, and code Playground

HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Browser &lt;-&gt; Pion (Go) WebRTC Demo</title>
<link rel="stylesheet" href="style.css">
</head>
<body>

<h2>Browser &lt;-&gt; Pion (Go) WebRTC Demo</h2>
<p>This page is the browser side. The other side is a Go program using <a href="https://github.com/pion/webrtc" target="_blank" rel="noopener">pion/webrtc</a> running in your terminal.</p>

<div class="videos">
  <div>
    <h4>Your camera (sent to terminal)</h4>
    <video id="localVideo" autoplay muted playsinline></video>
  </div>
  <div>
    <h4>Terminal's video (received from Go)</h4>
    <video id="remoteVideo" autoplay playsinline></video>
  </div>
</div>

<div class="step">
  <strong>Step 1:</strong> Click "Start Call". This grabs your camera and creates an SDP offer below.
  <br><button id="startBtn">Start Call (Create Offer)</button>
</div>

<div class="step">
  <strong>Step 2:</strong> Copy "Your SDP" below and paste it into the terminal running the Go program, then press Enter there.
</div>

<div class="step">
  <strong>Step 3:</strong> The Go program will print an answer. Paste it into "Remote SDP (answer)" below and click "Set Answer".
  <br><button id="setAnswerBtn">Set Answer</button>
</div>

<h4>Your SDP (copy & paste into the terminal):</h4>
<textarea id="localSDP" readonly placeholder="Will appear here after you click Start Call..."></textarea>

<h4>Remote SDP (paste the answer printed by the Go program):</h4>
<textarea id="remoteSDP" placeholder="Paste the base64 answer from the terminal here..."></textarea>

<p id="status">Status: idle</p>

<script src="script.js"></script>
</body>
</html>

CSS

body {
  font-family: sans-serif;
  max-width: 760px;
  margin: 20px auto;
}

.videos {
  display: flex;
  gap: 12px;
}

.videos > div {
  flex: 1;
}

video {
  width: 100%;
  background: #222;
  border-radius: 8px;
}

textarea {
  width: 100%;
  height: 90px;
  margin-top: 5px;
  font-family: monospace;
  font-size: 12px;
  word-break: break-all;
}

button {
  padding: 8px 14px;
  margin: 6px 6px 6px 0;
  cursor: pointer;
}

h4 {
  margin-bottom: 4px;
}

.step {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 12px;
  margin-bottom: 14px;
}

JavaScript

const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
const localSDP = document.getElementById('localSDP');
const remoteSDP = document.getElementById('remoteSDP');
const statusEl = document.getElementById('status');

let pc;

function setStatus(msg) {
  statusEl.textContent = 'Status: ' + msg;
  console.log(msg);
}

// Same scheme the Go side uses: JSON-encode the SessionDescription,
// then base64 it into a single line that's easy to copy/paste.
function encode(desc) {
  return btoa(JSON.stringify(desc));
}

function decode(str) {
  return JSON.parse(atob(str));
}

function createPeerConnection() {
  pc = new RTCPeerConnection({
    iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
  });

  pc.ontrack = (event) => {
    remoteVideo.srcObject = event.streams[0];
    setStatus('Receiving video from terminal.');
  };

  pc.oniceconnectionstatechange = () => {
    setStatus('ICE connection state: ' + pc.iceConnectionState);
  };

  return pc;
}

function waitForIceGatheringComplete(pc) {
  return new Promise((resolve) => {
    if (pc.iceGatheringState === 'complete') {
      resolve();
    } else {
      function check() {
        if (pc.iceGatheringState === 'complete') {
          pc.removeEventListener('icegatheringstatechange', check);
          resolve();
        }
      }
      pc.addEventListener('icegatheringstatechange', check);
    }
  });
}

document.getElementById('startBtn').onclick = async () => {
  try {
    createPeerConnection();

    const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
    localVideo.srcObject = stream;
    stream.getTracks().forEach(track => pc.addTrack(track, stream));

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

    setStatus('Gathering ICE candidates...');
    await waitForIceGatheringComplete(pc);

   ...