This commit is contained in:
Peter Harpending 2026-02-12 10:14:16 -08:00
parent b6436c84ee
commit b63130a061
4 changed files with 226 additions and 0 deletions

14
priv/static/js/dist/webrtc.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
/**
* webrtc page script
*
* Author: Peter Harpending <peterharpending@qpq.swiss>
* Date: 2026-02-04
* Copyright: Copyright (c) 2026 QPQ AG
*
* Reference: https://git.qpq.swiss/QPQ-AG/research-megadoc/src/commit/c7c4592d4b21ad120145ef63334471a1a7ec1e60/paste/2026-02/grok-webrtc.html
*
* @module
*/
declare function main(): Promise<void>;
declare function handle_join(init: HTMLDivElement, init_name: HTMLInputElement, peers: HTMLDivElement, ws: WebSocket): Promise<void>;
declare function ws_send_json(ws: WebSocket, x: any): void;

45
priv/static/js/dist/webrtc.js vendored Normal file
View File

@ -0,0 +1,45 @@
"use strict";
/**
* webrtc page script
*
* Author: Peter Harpending <peterharpending@qpq.swiss>
* Date: 2026-02-04
* Copyright: Copyright (c) 2026 QPQ AG
*
* Reference: https://git.qpq.swiss/QPQ-AG/research-megadoc/src/commit/c7c4592d4b21ad120145ef63334471a1a7ec1e60/paste/2026-02/grok-webrtc.html
*
* @module
*/
main();
async function main() {
// start websocket immediately
let ws = new WebSocket('/ws/webrtc');
// grab document elements
let init = document.getElementById('init');
let peers = document.getElementById('peers');
let init_name = document.getElementById('init-name');
let init_join = document.getElementById('init-join');
// handle button click
init_join.addEventListener('click', function () {
handle_join(init, init_name, peers, ws);
});
// handle message from ws
ws.onopen = function (e) { console.log('ws open:', e); };
ws.onclose = function (e) { console.log('ws closed:', e); };
ws.onerror = function (e) { console.error('ws error:', e); };
ws.onmessage = function (e) { console.log('ws message', e); };
}
async function handle_join(init, init_name, peers, ws) {
console.log('connecting...');
let user_name = init_name.value.trim();
console.log('username:', user_name);
ws_send_json(ws, ['username', user_name]);
init.hidden = true;
peers.hidden = false;
}
function ws_send_json(ws, x) {
let s = JSON.stringify(x, undefined, 4);
console.log('sending:\n', s);
ws.send(s);
}
//# sourceMappingURL=webrtc.js.map

1
priv/static/js/dist/webrtc.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"webrtc.js","sourceRoot":"","sources":["../ts/webrtc.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;AAEH,IAAI,EAAE,CAAC;AAEP,KAAK,UACL,IAAI;IAIA,8BAA8B;IAC9B,IAAI,EAAE,GAAG,IAAI,SAAS,CAAC,YAAY,CAAC,CAAC;IAErC,yBAAyB;IACzB,IAAI,IAAI,GAAI,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAsB,CAAC;IACjE,IAAI,KAAK,GAAG,QAAQ,CAAC,cAAc,CAAC,OAAO,CAAqB,CAAC;IAEjE,IAAI,SAAS,GAAG,QAAQ,CAAC,cAAc,CAAC,WAAW,CAAqB,CAAC;IACzE,IAAI,SAAS,GAAG,QAAQ,CAAC,cAAc,CAAC,WAAW,CAAsB,CAAC;IAE1E,sBAAsB;IACtB,SAAS,CAAC,gBAAgB,CAAC,OAAO,EAC9B;QACI,WAAW,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;IAC5C,CAAC,CACJ,CAAC;IAEF,yBAAyB;IACzB,EAAE,CAAC,MAAM,GAAM,UAAS,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3D,EAAE,CAAC,OAAO,GAAK,UAAS,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7D,EAAE,CAAC,OAAO,GAAK,UAAS,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9D,EAAE,CAAC,SAAS,GAAG,UAAS,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACjE,CAAC;AAED,KAAK,UACL,WAAW,CACN,IAA0B,EAC1B,SAA4B,EAC5B,KAA0B,EAC1B,EAAqB;IAGtB,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAC7B,IAAI,SAAS,GAAW,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IAC/C,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IAEpC,YAAY,CAAC,EAAE,EAAE,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC;IAE1C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC;AACzB,CAAC;AAGD,SACA,YAAY,CACP,EAAc,EACd,CAAQ;IAGT,IAAI,CAAC,GAAW,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;IAChD,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC;IAE7B,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACf,CAAC"}

166
scratch/grok-webrtc.html Normal file
View File

@ -0,0 +1,166 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Super Simple WebRTC Manual Signaling</title>
<style>
body { font-family: sans-serif; margin: 20px; }
textarea { width: 100%; height: 90px; font-family: monospace; }
button { margin: 6px 0; padding: 8px 16px; }
video { max-width: 320px; border: 1px solid #444; margin: 8px; background: #000; }
</style>
</head>
<body>
<h2>WebRTC 1:1 test (copy-paste signaling)</h2>
<div>
<button id="btnCreateOffer">1. Create Offer (Peer A)</button>
<button id="btnSetRemoteOffer">2. Paste offer → Set remote description</button><br>
<textarea id="offerSdp" placeholder="Offer SDP will appear here (Peer A) or paste offer here (Peer B)"></textarea>
</div>
<div style="margin-top: 24px;">
<button id="btnCreateAnswer">3. Create Answer (Peer B)</button>
<button id="btnSetRemoteAnswer">4. Paste answer → Set remote description</button><br>
<textarea id="answerSdp" placeholder="Answer SDP will appear here (Peer B) or paste answer here (Peer A)"></textarea>
</div>
<div style="margin-top: 20px;">
<button id="btnAddIce">Add ICE candidate manually (if needed)</button><br>
<textarea id="iceInput" placeholder="Paste remote ICE candidate here"></textarea>
<div id="iceLog"></div>
</div>
<div style="margin-top: 20px;">
<video id="localVideo" autoplay playsinline muted></video>
<video id="remoteVideo" autoplay playsinline></video>
</div>
<pre id="status">Status: ready</pre>
<script>
// ────────────────────────────────────────────────
const $ = s => document.querySelector(s);
const status = $('pre#status');
const offerArea = $('#offerSdp');
const answerArea = $('#answerSdp');
const iceInput = $('#iceInput');
const iceLog = $('#iceLog');
let pc = null;
let localStream = null;
// ────────────────────────────────────────────────
async function initPeerConnection() {
if (pc) pc.close();
pc = new RTCPeerConnection({
iceServers: [
{ urls: "stun:stun.l.google.com:19302" },
{ urls: "stun:stun.stunprotocol.org" }
]
});
pc.onicecandidate = e => {
if (e.candidate) {
console.log("New ICE candidate:", e.candidate);
iceLog.innerHTML += "<div style='color:#c33'>→ " + e.candidate.candidate + "</div>";
}
};
pc.oniceconnectionstatechange = () => {
status.textContent = "ICE state: " + pc.iceConnectionState;
};
pc.ontrack = e => {
console.log("ontrack", e);
const remoteVideo = $('#remoteVideo');
remoteVideo.srcObject = e.streams[0];
status.textContent = "ICE state: " + pc.iceConnectionState + " (receiving media!)";
};
// Add local stream if we have camera/mic
if (localStream) {
localStream.getTracks().forEach(track => {
pc.addTrack(track, localStream);
});
}
}
// ────────────────────────────────────────────────
async function startLocalCamera() {
try {
localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: false // change to true if you want audio too
});
$('#localVideo').srcObject = localStream;
} catch (err) {
console.error("Camera access failed", err);
status.textContent = "Camera access failed: " + err.message;
}
}
// ────────────────────────────────────────────────
$('#btnCreateOffer').onclick = async () => {
await initPeerConnection();
await startLocalCamera();
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
offerArea.value = JSON.stringify(pc.localDescription, null, 2);
status.textContent = "Offer created copy and send to peer B";
};
// ────────────────────────────────────────────────
$('#btnCreateAnswer').onclick = async () => {
await initPeerConnection();
await startLocalCamera();
const offer = JSON.parse(offerArea.value);
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
answerArea.value = JSON.stringify(pc.localDescription, null, 2);
status.textContent = "Answer created copy and send to peer A";
};
// ────────────────────────────────────────────────
$('#btnSetRemoteOffer').onclick = async () => {
if (!pc) await initPeerConnection();
const remote = JSON.parse(offerArea.value);
await pc.setRemoteDescription(remote);
status.textContent = "Remote offer set";
};
// ────────────────────────────────────────────────
$('#btnSetRemoteAnswer').onclick = async () => {
const remote = JSON.parse(answerArea.value);
await pc.setRemoteDescription(remote);
status.textContent = "Remote answer set waiting for ICE/media";
};
// ────────────────────────────────────────────────
$('#btnAddIce').onclick = async () => {
if (!iceInput.value.trim()) return;
try {
const candidate = new RTCIceCandidate(JSON.parse(iceInput.value));
await pc.addIceCandidate(candidate);
iceLog.innerHTML += "<div style='color:#3a3'>← added ICE candidate</div>";
iceInput.value = "";
} catch (err) {
console.error("Bad ICE candidate", err);
}
};
// Optional: auto-start camera when page loads
// startLocalCamera();
</script>
</body>
</html>