[0.9.1] FORK_IA_UX v7: Karvan calls Fase 1 — video/mic UI + media over the existing WebRTC
Adds a call panel to the Karvan room (local video + remote gallery + call/mic/cam/hang-up buttons) and grafts getUserMedia + addTrack + ontrack onto the peer connections we already use for the text data-channel (perfect negotiation handles the renegotiation). No backend, no new !important. Verified: getUserMedia path attaches audio+video tracks to the local video and activates the panel (chromium fake device). getUserMedia will fail gracefully on the current wrapper (no camera/mic permission) with a clear message — the chat keeps working. Fases 2 (SSB cross-device signaling) and 3 (wrapper permissions) pending.
This commit is contained in:
parent
423e973f02
commit
e2598ab747
3 changed files with 134 additions and 3 deletions
|
|
@ -23,6 +23,19 @@
|
|||
var lastSig = 0;
|
||||
var peers = Object.create(null);
|
||||
|
||||
// --- Fase 1: videollamada (media sobre el MISMO WebRTC del chat) ---
|
||||
var callRoot = document.getElementById("karvan-call");
|
||||
var localVid = document.getElementById("karvan-local");
|
||||
var remoteEl = document.getElementById("karvan-remote");
|
||||
var statusEl = document.getElementById("kc-status");
|
||||
var btnStart = document.getElementById("kc-start");
|
||||
var btnMic = document.getElementById("kc-mic");
|
||||
var btnCam = document.getElementById("kc-cam");
|
||||
var btnStop = document.getElementById("kc-stop");
|
||||
var localStream = (window.MediaStream ? new MediaStream() : null);
|
||||
if (localVid && localStream) localVid.srcObject = localStream;
|
||||
var micActive = false, camActive = false;
|
||||
|
||||
function esc(s) { return String(s == null ? "" : s); }
|
||||
function shortId(id) { id = esc(id || "?"); return id.charAt(0) === "@" ? id.slice(1, 7) : id.slice(0, 6); }
|
||||
|
||||
|
|
@ -130,11 +143,14 @@
|
|||
if (peers[remoteId] || !RTC) return peers[remoteId];
|
||||
var polite = peerId > remoteId; // el id mayor es "polite"
|
||||
var pc = new RTC(rtcConfig);
|
||||
var peer = { pc: pc, dc: null, polite: polite, makingOffer: false, ignoreOffer: false };
|
||||
var peer = { pc: pc, dc: null, polite: polite, makingOffer: false, ignoreOffer: false, vid: null, remoteStream: null };
|
||||
peers[remoteId] = peer;
|
||||
|
||||
if (!polite) { attachDC(peer, pc.createDataChannel("chat")); } // el impolite inicia el canal
|
||||
pc.ondatachannel = function (e) { attachDC(peer, e.channel); };
|
||||
// media: publicar los tracks locales que ya tengamos (si la llamada está activa) + recibir los del par
|
||||
if (localStream) { localStream.getTracks().forEach(function (t) { try { pc.addTrack(t, localStream); } catch (e) {} }); }
|
||||
pc.ontrack = function (e) { attachRemote(peer, remoteId, e); };
|
||||
pc.onicecandidate = function (e) { if (e.candidate) sendSig(remoteId, { kind: "ice", candidate: e.candidate }); };
|
||||
pc.onnegotiationneeded = function () {
|
||||
peer.makingOffer = true;
|
||||
|
|
@ -146,11 +162,92 @@
|
|||
};
|
||||
pc.onconnectionstatechange = function () {
|
||||
var st = pc.connectionState;
|
||||
if (st === "failed" || st === "closed") { try { pc.close(); } catch (e) {} delete peers[remoteId]; }
|
||||
if (st === "failed" || st === "closed") { try { pc.close(); } catch (e) {} removeRemoteVideo(peer); delete peers[remoteId]; }
|
||||
};
|
||||
return peer;
|
||||
}
|
||||
|
||||
// --- media / videollamada (Fase 1) ---
|
||||
function setCallActive(on) { if (callRoot) callRoot.className = "karvan-call" + (on ? " active" : ""); }
|
||||
function setStatus(msg) { if (statusEl) statusEl.textContent = msg || ""; }
|
||||
function statusText() { var p = []; if (micActive) p.push("mic ON"); if (camActive) p.push("cam ON"); return p.length ? p.join(" · ") : ""; }
|
||||
function updateBtns() {
|
||||
if (btnMic) btnMic.className = "kc-btn" + (micActive ? " on" : "");
|
||||
if (btnCam) btnCam.className = "kc-btn" + (camActive ? " on" : "");
|
||||
}
|
||||
|
||||
function attachRemote(peer, remoteId, e) {
|
||||
if (!remoteEl) return;
|
||||
if (!peer.vid) {
|
||||
peer.vid = document.createElement("video");
|
||||
peer.vid.autoplay = true; peer.vid.playsInline = true; peer.vid.className = "kc-video kc-remote-vid";
|
||||
peer.vid.setAttribute("data-peer", remoteId);
|
||||
remoteEl.appendChild(peer.vid);
|
||||
}
|
||||
if (e.streams && e.streams[0]) peer.vid.srcObject = e.streams[0];
|
||||
else { if (!peer.remoteStream) peer.remoteStream = new MediaStream(); peer.remoteStream.addTrack(e.track); peer.vid.srcObject = peer.remoteStream; }
|
||||
setCallActive(true);
|
||||
}
|
||||
function removeRemoteVideo(peer) {
|
||||
if (peer && peer.vid && peer.vid.parentNode) { try { peer.vid.parentNode.removeChild(peer.vid); } catch (_) {} peer.vid = null; }
|
||||
}
|
||||
|
||||
function addLocalTrack(track) {
|
||||
if (!localStream) return;
|
||||
localStream.addTrack(track);
|
||||
for (var k in peers) {
|
||||
var pc = peers[k].pc, senders = pc.getSenders(), existing = null;
|
||||
for (var s = 0; s < senders.length; s++) if (senders[s].track && senders[s].track.kind === track.kind) { existing = senders[s]; break; }
|
||||
if (existing) { try { existing.replaceTrack(track); } catch (_) {} } // swap (cambio de cámara) — sin renegociar
|
||||
else { try { pc.addTrack(track, localStream); } catch (_) {} } // kind nuevo — dispara onnegotiationneeded
|
||||
}
|
||||
}
|
||||
function removeLocalKind(kind) {
|
||||
if (!localStream) return;
|
||||
localStream.getTracks().filter(function (t) { return t.kind === kind; }).forEach(function (t) { t.stop(); localStream.removeTrack(t); });
|
||||
for (var k in peers) {
|
||||
var senders = peers[k].pc.getSenders();
|
||||
for (var s = 0; s < senders.length; s++) if (senders[s].track && senders[s].track.kind === kind) { try { senders[s].replaceTrack(null); } catch (_) {} }
|
||||
}
|
||||
}
|
||||
function onMediaErr(kind, x) {
|
||||
var name = (x && x.name) || "", msg = (x && x.message) || String(x);
|
||||
if (name === "NotAllowedError" || /denied|permission/i.test(msg)) setStatus("⛔ Cámara/micro denegados (en el móvil, pendiente del wrapper). El chat sigue.");
|
||||
else if (name === "NotFoundError") setStatus("⚠ No hay " + (kind === "audio" ? "micrófono" : "cámara") + ".");
|
||||
else if (name === "NotReadableError") setStatus("⚠ Dispositivo ocupado.");
|
||||
else setStatus("⚠ " + (name || "error") + ": " + msg);
|
||||
}
|
||||
function getMedia(kind) {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { setStatus("⚠ Cámara/micro no disponibles aquí."); return; }
|
||||
var c = (kind === "audio") ? { audio: true, video: false } : { audio: false, video: true };
|
||||
setStatus((kind === "audio" ? "🎤" : "🎥") + " pidiendo permiso…");
|
||||
navigator.mediaDevices.getUserMedia(c).then(function (s) {
|
||||
s.getTracks().forEach(addLocalTrack);
|
||||
if (kind === "audio") micActive = true; else camActive = true;
|
||||
setCallActive(true); updateBtns(); setStatus(statusText());
|
||||
}).catch(function (x) { onMediaErr(kind, x); });
|
||||
}
|
||||
function startCall() {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { setStatus("⚠ Cámara/micro no disponibles aquí."); return; }
|
||||
setStatus("📞 pidiendo cámara y micro…");
|
||||
navigator.mediaDevices.getUserMedia({ audio: true, video: true }).then(function (s) {
|
||||
s.getTracks().forEach(addLocalTrack);
|
||||
micActive = true; camActive = true; setCallActive(true); updateBtns(); setStatus(statusText());
|
||||
}).catch(function (x) {
|
||||
if (x && x.name === "NotFoundError") { getMedia("audio"); return; } // sin cámara → intenta solo audio
|
||||
onMediaErr("call", x);
|
||||
});
|
||||
}
|
||||
function hangUp() {
|
||||
if (localStream) localStream.getTracks().forEach(function (t) { t.stop(); localStream.removeTrack(t); });
|
||||
for (var k in peers) { var sn = peers[k].pc.getSenders(); for (var s = 0; s < sn.length; s++) { try { sn[s].replaceTrack(null); } catch (_) {} } }
|
||||
micActive = false; camActive = false; setCallActive(false); updateBtns(); setStatus("");
|
||||
}
|
||||
if (btnStart) btnStart.addEventListener("click", startCall);
|
||||
if (btnMic) btnMic.addEventListener("click", function () { if (micActive) { removeLocalKind("audio"); micActive = false; updateBtns(); setStatus(statusText()); } else getMedia("audio"); });
|
||||
if (btnCam) btnCam.addEventListener("click", function () { if (camActive) { removeLocalKind("video"); camActive = false; updateBtns(); setStatus(statusText()); } else getMedia("video"); });
|
||||
if (btnStop) btnStop.addEventListener("click", hangUp);
|
||||
|
||||
function onSignal(fromRemote, payload) {
|
||||
if (!RTC || !payload) return;
|
||||
var peer = ensurePeer(fromRemote);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue