JSFiddle - React, Tailwind, and code Playground
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Browser <-> Pion (Go) WebRTC Demo</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h2>Browser <-> 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);
...