Эхо и завершение звонка в webrtc-php аудио звонке

Код практически работает но слышно эхо на каждой стороне, эхо собственного голоса. Так же нужна помощь в завершении звонка. Я полностью не понимаю технологию, и не могу найти ответ в гугле или документации.

var answer = 0;
var pc = null
var localStream = null;
var ws = null;

// Не нужно с вебсокетами, но т.к используется хак, то нужно для различий между звонками
// Уникальный идентификатор пользователя чата, используется в php файлах для отсылки и 
// приема сообщений
var unique = Math.floor(100000 + Math.random() * 900000);

// Для передачи аудио потоков, видео тоже возможно но есть сложности
// в запуске где один клиент не имеет камеры
var localVideo = document.getElementById('localVideo');
var remoteVideo = document.getElementById('remoteVideo');

// Что такое ICE сервер - https://voxlink.ru/kb/asterisk-configuration/obzor-protokolov-stun-turn-i-ice-ih-principy-raboty-s-nat-i-ispolzovanie-dlja-voip/
// Необходим для соединения клиентов за роутерами без белого ип
// Ниже бесплатные сервера в свободном доступе, хотя гугловский вроде перестал работать
var configuration = {
  'iceServers': [{
      'urls': 'stun:stun.stunprotocol.org:3478'
    },
    {
      'urls': 'stun:stun.l.google.com:19302'
    }
  ]
};

// Функция старта звонка, как запустится(по нажатию кнопки) на обоих клиентах
// произойдет соединение
function StartCall() {
  // Интерфейс MediaDevices предоставляет доступ к подключенным медиа-устройствам
  // ввода, таким как камера, микрофон
  
  // Метод MediaDevices.getUserMedia(), при выполнении, вызывает всплывающий 
  // диалог, запрашивающий разрешение пользователя на использование медиа устройства 
  // (камера, микрофон). Результат возвращает промис, содержащий поток
  // !!! Без https не работает !!!
  navigator.mediaDevices.getUserMedia({
    audio: true
  }).then(function(stream) {     // Объект Promise (промис) используется для 
                                 // отложенных и асинхронных вычислений.
                                 // Не будет вызван если пользователь не разрешил микрофон
                                 // Параметром получает аудио поток
    localVideo.srcObject = stream;
    localStream = stream;
    try {
      // Замена вебсокету которого нет на хостинге
      // Интерфейс EventSource используется для получения серверных событий
      ws = new EventSource('serverGet.php?unique=' + unique);
    } catch (e) {
      console.error("Could not create eventSource ", e);
    }
    // Хак веб сокета, т.к EventSource не отсылает данные то используется АЯКС
    // Теперь EventSource(ws) больше похож на вебсокет
    ws.send = function send(message) {
      var xhttp = new XMLHttpRequest();
      xhttp.onreadystatechange = function() {
        if (this.readyState != 4) {
          return;
        }
        if (this.status != 200) {
          console.log("Error sending to server with message: " + message);
        }
      };
      xhttp.open('POST', 'serverPost.php?unique=' + unique, true);
      xhttp.setRequestHeader("Content-Type", "Application/X-Www-Form-Urlencoded");
      xhttp.send(message);
    }
    // onmessage сделано так чтобы одновременно получать несколько эвентов
    // для ускорения работы
    ws.onmessage = function(e) {
      if (e.data.includes("_MULTIPLEVENTS_")) {
        multiple = e.data.split("_MULTIPLEVENTS_");
        for (x = 0; x < multiple.length; x++) {
          onsinglemessage(multiple[x]);
        }
      } else {
        onsinglemessage(e.data);
      }
    }
    
    localVideo.addEventListener('loadedmetadata',
      function() {
        publish('client-call', null)
      }
    );
  }).catch(function(e) {
    alert("Проблема получения доступа к микрофону, возможно не защищенное соединение");
    console.log("Problem while getting audio stuff ", e);
  });
}

// Привязка кнопки к началу звонка
var btnStart = document.getElementById('btnStart');
btnStart.onclick = function() {
  StartCall();
};

// вызывается из EventSource при получении сообщения
function onsinglemessage(data) {
  var package = JSON.parse(data);
  var data = package.data;
  console.log("received single message: " + package.event);
  switch (package.event) {
    case 'client-call':
      icecandidate(localStream);
      pc.createOffer({
        offerToReceiveAudio: 1,
        offerToReceiveVideo: 1
      }).then(function(desc) {
        pc.setLocalDescription(desc).then(
          function() {
            publish('client-offer', pc.localDescription);
          }
        ).catch(function(e) {
          console.log("Problem with publishing client offer" + e);
        });
      }).catch(function(e) {
        console.log("Problem while doing client-call: " + e);
      });
      break;
    case 'client-answer':
      if (pc == null) {
        console.error('Before processing the client-answer, I need a client-offer');
        break;
      }
      pc.setRemoteDescription(new RTCSessionDescription(data), function() {},
        function(e) {
          console.log("Problem while doing client-answer: ", e);
        });
      break;
    case 'client-offer':
      // Подключение аудио потока через сервер - пир-ту-пир
      icecandidate(localStream);
      // pc = RTCPeerConnection класс WebRTC

      pc.setRemoteDescription(new RTCSessionDescription(data), function() {
        if (!answer) {
          pc.createAnswer(function(desc) {
            pc.setLocalDescription(desc, function() {
              publish('client-answer', pc.localDescription);
            }, function(e) {
              console.log("Problem getting client answer: ", e);
            });
          }, function(e) {
            console.log("Problem while doing client-offer: ", e);
          });
          answer = 1;
        }
      }, function(e) {
        console.log("Problem while doing client-offer2: ", e);
      });
      break;
    case 'client-candidate':
      if (pc == null) {
        console.error('Before processing the client-answer, I need a client-offer');
        break;
      }
      pc.addIceCandidate(new RTCIceCandidate(data), function() {},
        function(e) {
          console.log("Problem adding ice candidate: " + e);
        });
      break;
  }
};

function icecandidate(localStream) {
  // Интерфейс RTCPeerConnection представляет соединение WebRTC между локальным
  // пиром (участником соединения) на локальном компьютере и удаленным пиром на
  // удаленном компьютере. Он предоставляет методы для соединения с удаленным
  // участником соединения, обслуживания, мониторинга и закрытия соединения.
  pc = new RTCPeerConnection(configuration);
  pc.onicecandidate = function(event) {
    if (event.candidate) {
      publish('client-candidate', event.candidate);
    }
  };
  try {
    // Добавление аудио потока в соединение, происходит в начале аудио звонка 
    // на обоих сторонах
    pc.addStream(localStream);
  } catch (e) {
    // Если бы у потока было несколько треков(аудио/видео)
    var tracks = localStream.getTracks();
    for (var i = 0; i < tracks.length; i++) {
      pc.addTrack(tracks[i], localStream);
    }
  }
  pc.ontrack = function(e) {
    // Подключение удаленного аудио потока
    remoteVideo.srcObject = e.streams[0];
  };
}

// Отослать эвент другой стороне
function publish(event, data) {
  console.log("sending ws.send: " + event);
  ws.send(JSON.stringify({
    event: event,
    data: data
  }));
}

Ответы (0 шт):