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 email poruci nekom korisniku sa kojim se želi povezati.

“Pozvani” korisnik tada može kreirati “Answer” kod i odgovoriti na “poziv”. Zatim, poslati natrag dobiveni “Answer” kod odgovora prvom korisniku.

Sada je razgovor putem pisanja teksta u internetskom pregledniku 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).

Kod <script> aplikacije za WebRTC “chat” u jednoj datoteci (HTML+Js)

/wrc-app-dir/index.html – prijedlog lokalne organizacije datoteka, na primjer projekta web aplikacije sa jednom html stranicom.

<!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.