WebRTC uzorak u HTML+JS

Iz razvojnog projekta iz istraživanja WebRTC, komunikacije putem interneta. Npr. igre za više igrača u internetskom pregledniku.

Više detalja uskoro.

Primjena konkretnoga primjera je omogućiti izravnu komunikaciju i povezivanje korisnika ili aplikacija za vrijeme razvoja bez potrebe za posrednom uslugom (STUN, TURN). Međutim korisnici ipak trebaju medij kojim će razmijeniti parametre za uspostavu komunikacije, npr. e-mail porukom.

Razvijeno upotrebom besplatne UI (AI) usluge.

S namjerom smanjenja potrebne infrastrukture za vrijeme razvoja modela komunikacije i logike upravljanja instancama.

“Chat” aplikacija za internetski preglednik u jednoj datoteci (index.html). Ova verzija radi u Chromium internetski preglednicima, dok u FireFox pregledniku ima poteškoća (ali ipak radi).

Za korištenje nije potreban poslužitelj, dovoljno je otvoriti lokalnu datoteku na korisnikovom računalu u internetskom pregledniku.

To je sve za sada u ovom trenutku. Slobodno ostavite upit u polju za komentiranje na dnu stranice.

Upute

Koristi “kopiraj – zalijepi” donji kod u vlastitu lokalnu datoteku.

Na ovom serveru nije moguće posluživati datoteke.


Pojednostavljeno:

Jedan (prvi, inicijator) korisnik pritiskom na “Create Offer” stvori “Ponudu” i dobiveni kod iz polja “Local SDP (copy out):” pošalje npr. u mail poruci korisniku sa kojim se želi povezati.

Drugi korisnik tada može kreirati “Answer” kod i odgovoriti na “poziv”. Potom je nužno poslati natrag dobiveni kod odgovora prvom korisniku.

I, sada je razgovor putem pisanja teksta moguć nakon razmjene inicijalnih parametara komunikacije.

Nakon provedenih testiranja, utvrdija san da je moguće slati poruke. Između korisnika na stolnom računalu i korisnika na mobilnom uređaju koji pristupa internetu putem mobilne mreže (ne putem lokalnog WiFi).

“index.html” – Kod aplikacije za WebRTC “chat” u jednoj datoteci (HTML+Js)

<!doctype html>
<html><head><meta charset="utf-8"><title>WebRTC P2P — Auto ICE restart (robust manual)</title>
<style>body{font-family:system-ui;margin:16px}textarea{width:100%;height:90px}pre{background:#f6f6f6;padding:8px;max-height:240px;overflow:auto}button{margin:4px 0}</style>
</head><body>
<h3>WebRTC P2P — Auto ICE restart (handles delayed manual exchange)</h3>

<div>
  <button id="createOffer">Create Offer (caller)</button>
  <button id="prepareResponder">Prepare Responder UI</button>
</div>

<div><label>Local SDP (copy out):</label><textarea id="local" readonly></textarea></div>
<div><label>Remote SDP (paste offer/answer):</label><textarea id="remote"></textarea><button id="setRemote">Set Remote</button></div>

<div><label>Local ICE (copy out for trickle):</label><textarea id="localIce" readonly></textarea></div>
<div><label>Remote ICE (paste one JSON per line):</label><textarea id="remoteIce"></textarea><button id="addIce">Add ICEs</button></div>

<div><input id="msg" placeholder="Message" style="width:70%"><button id="send">Send</button></div>
<pre id="log"></pre>

<script>
/* Robust manual WebRTC with automatic ICE-restart on failure.
   No STUN/TURN required. */
const pcConfig = { iceServers: [] };
let pc = null, dc = null, role = null, offerCreated = false;
let pendingRemoteSDP = null, pendingIce = [];
let pendingAnswerIfNoLocal = null;
let iceRestartAttempts = 0, maxIceRestarts = 3;
const $ = id => document.getElementById(id);
const log = t => { $('log').textContent += t + '\n'; };

function ensurePC(){
  if (pc) return;
  pc = new RTCPeerConnection(pcConfig);

  pc.onicecandidate = e => {
    if (!e.candidate) return;
    const obj = e.candidate.toJSON ? e.candidate.toJSON() : e.candidate;
    $('localIce').value += JSON.stringify(obj) + '\n';
  };

  pc.ondatachannel = ev => { dc = ev.channel; setupDC(); log('ondatachannel'); };
  pc.onconnectionstatechange = () => {
    log('connectionState: ' + pc.connectionState);
    if (pc.connectionState === 'failed') handleIceFailure();
  };
  pc.oniceconnectionstatechange = () => {
    log('iceConnectionState: ' + pc.iceConnectionState);
    if (pc.iceConnectionState === 'failed') handleIceFailure();
  };
}

function setupDC(){
  if (!dc) return;
  dc.onopen = () => log('DataChannel open');
  dc.onmessage = ev => log('Remote: ' + ev.data);
  dc.onclose = () => log('DataChannel closed');
}

function waitIceGatherComplete(timeoutMs = 7000){
  if (!pc) return Promise.resolve();
  if (pc.iceGatheringState === 'complete') return Promise.resolve();
  return new Promise(resolve => {
    const t = setTimeout(() => { pc.removeEventListener('icegatheringstatechange', onchg); resolve(); }, timeoutMs);
    function onchg(){ if (pc.iceGatheringState === 'complete'){ clearTimeout(t); pc.removeEventListener('icegatheringstatechange', onchg); resolve(); } }
    pc.addEventListener('icegatheringstatechange', onchg);
  });
}

async function applyBufferedIce(){
  if (!pc) return;
  while (pendingIce.length){
    const cand = pendingIce.shift();
    try {
      const init = { candidate: cand.candidate, sdpMid: cand.sdpMid, sdpMLineIndex: cand.sdpMLineIndex };
      await pc.addIceCandidate(new RTCIceCandidate(init));
      log('Applied buffered ICE candidate');
    } catch(e){ log('Buffered addIce failed: ' + e); }
  }
}

async function applyBufferedSDP(){
  if (!pendingRemoteSDP) return;
  const obj = pendingRemoteSDP; pendingRemoteSDP = null;
  try {
    await pc.setRemoteDescription({ type: obj.type, sdp: obj.sdp });
    log('Applied buffered remote SDP: ' + obj.type);

    if (role === 'responder' && obj.type === 'offer') {
      const answer = await pc.createAnswer();
      await pc.setLocalDescription({ type: answer.type, sdp: answer.sdp });
      await waitIceGatherComplete();
      $('local').value = JSON.stringify({ type: pc.localDescription.type, sdp: pc.localDescription.sdp });
      log('Answer created (gather complete)');
    }
    await applyBufferedIce();
  } catch(e){
    log('Applying buffered SDP failed: ' + e);
  }
}

async function handleIceFailure(){
  if (iceRestartAttempts >= maxIceRestarts) { log('Max ICE restarts reached — manual retry required'); return; }
  iceRestartAttempts++;
  log('Connection failed — starting ICE restart #' + iceRestartAttempts);

  try {
    // For caller: create offer with iceRestart: true and publish it.
    // For responder: create answer with iceRestart: true (setRemote must be present for createAnswer)
    ensurePC();

    if (role === 'caller') {
      const offer = await pc.createOffer({ iceRestart: true });
      await pc.setLocalDescription({ type: offer.type, sdp: offer.sdp });
      await waitIceGatherComplete();
      $('local').value = JSON.stringify({ type: pc.localDescription.type, sdp: pc.localDescription.sdp });
      log('New offer with iceRestart created — resend this SDP to peer');
    } else if (role === 'responder') {
      // If remote description present and is an offer, create new answer with iceRestart
      if (pc.remoteDescription && pc.remoteDescription.type === 'offer') {
        const answer = await pc.createAnswer({ iceRestart: true });
        await pc.setLocalDescription({ type: answer.type, sdp: answer.sdp });
        await waitIceGatherComplete();
        $('local').value = JSON.stringify({ type: pc.localDescription.type, sdp: pc.localDescription.sdp });
        log('New answer with iceRestart created — resend this SDP to peer');
      } else {
        log('Responder cannot iceRestart until an offer is set remotely; waiting for offer');
      }
    }
  } catch(e){ log('ICE restart failed: ' + e); }
}

// UI actions
$('createOffer').onclick = async () => {
  role = 'caller'; ensurePC();
  if (offerCreated) return;
  offerCreated = true;
  dc = pc.createDataChannel('p2p'); setupDC();

  const offer = await pc.createOffer();
  await pc.setLocalDescription({ type: offer.type, sdp: offer.sdp });
  await waitIceGatherComplete();
  $('local').value = JSON.stringify({ type: pc.localDescription.type, sdp: pc.localDescription.sdp });
  log('Offer created (gather complete)');
  // Apply any buffered remote answer that arrived earlier
  if (pendingRemoteSDP && pendingRemoteSDP.type === 'answer') await applyBufferedSDP();
  await applyBufferedIce();
};

$('prepareResponder').onclick = () => {
  role = 'responder'; ensurePC(); log('Responder ready');
  if (pendingRemoteSDP && pendingRemoteSDP.type === 'offer') applyBufferedSDP();
};

$('setRemote').onclick = async () => {
  const txt = $('remote').value.trim(); if (!txt) { log('Remote empty'); return; }
  let obj;
  try { obj = JSON.parse(txt); } catch(e){ log('Invalid JSON'); return; }
  ensurePC();

  // If caller receives an answer before their local exists, buffer it
  if (obj.type === 'answer' && (!pc.localDescription || !pc.localDescription.sdp)) {
    pendingRemoteSDP = obj; log('Answer buffered until localDescription exists'); return;
  }

  try {
    await pc.setRemoteDescription({ type: obj.type, sdp: obj.sdp });
    log('Remote description set: ' + obj.type);
    if (role === 'responder' && obj.type === 'offer') {
      const answer = await pc.createAnswer();
      await pc.setLocalDescription({ type: answer.type, sdp: answer.sdp });
      await waitIceGatherComplete();
      $('local').value = JSON.stringify({ type: pc.localDescription.type, sdp: pc.localDescription.sdp });
      log('Answer created (gather complete)');
    }
    await applyBufferedIce();
  } catch(e){
    // Buffer if cannot apply now
    pendingRemoteSDP = obj;
    log('Could not apply remote now — buffered: ' + e);
  }
};

$('addIce').onclick = () => {
  const lines = $('remoteIce').value.split('\n').map(s=>s.trim()).filter(Boolean);
  if (!lines.length) { log('No ICE'); return; }
  for (const line of lines) {
    try {
      const cand = JSON.parse(line);
      if (pc && pc.remoteDescription && pc.remoteDescription.sdp) {
        const init = { candidate: cand.candidate, sdpMid: cand.sdpMid, sdpMLineIndex: cand.sdpMLineIndex };
        pc.addIceCandidate(new RTCIceCandidate(init)).then(()=>log('Added ICE candidate')).catch(e=>log('addIce failed: '+e));
      } else {
        pendingIce.push(cand); log('Buffered ICE candidate');
      }
    } catch(e){ log('Invalid ICE JSON: ' + e); }
  }
  $('remoteIce').value = '';
};

$('send').onclick = () => {
  const m = $('msg').value;
  if (!dc) { log('No DataChannel'); return; }
  if (dc.readyState !== 'open') { log('Channel not open ('+dc.readyState+')'); return; }
  dc.send(m); log('You: ' + m); $('msg').value = '';
};

// Periodic attempt to apply buffers if PC becomes available
setInterval(() => {
  if (!pendingRemoteSDP) return;
  if (!pc) return;
  applyBufferedSDP().catch(e=>log('Periodic apply failed: ' + e));
}, 2000);
</script>
</body></html>

Autor Edi

Računarlira od 1984. u kibernetičkom svemiru.

Ostavite komentar

Vaša adresa e-pošte neće biti objavljena. Obavezna polja su označena sa *

Ova web-stranica koristi Akismet za zaštitu protiv spama. Saznajte kako se obrađuju podaci komentara.