Как добавитьTrack в MediaStream в WebRTC

Я использую webrtc для связи с коллегами. Я не хочу добавлять новую дорожку к старому сгенерированному потоку, так как я не хочу давать пользователям возможность переключать свои микрофоны во время аудиосвязи. Код, который я использую,

Пусть «pc» будет объектом peerConnection, через который происходит аудиосвязь, а «newStream» будет новым сгенерированным потоком MediaStream, полученным из функции getUserMedia с новым выбранным микрофонным устройством.

            var localStreams = pc.getLocalStreams()[0];
            localStreams.removeTrack(localStreams.getAudioTracks()[0]);


            var audioTrack = newStream.getAudioTracks()[0];
            localStreams.addTrack(audioTrack);

Есть ли у них какой-либо способ, которым вновь добавленный трек начинает доходить до другого ранее подключенного однорангового узла, не предлагая ему снова весь SDP?

Каким будет оптимизированный способ использования в таком случае коммутируемого мультимедийного устройства, то есть микрофонов, когда соединения уже установлены между одноранговыми узлами?


person Akshay Rathore    schedule 19.02.2016    source источник
comment
Я считаю, что повторное согласование всегда необходимо, когда вы меняете что-либо в медиапотоках.   -  person deceze♦    schedule 19.02.2016
comment
Есть ли другой выход, кроме повторных переговоров? Если нет, то как правильно выполнить повторное согласование?   -  person Akshay Rathore    schedule 19.02.2016
comment
Я не могу авторитетно сказать, что это верно только для треков, но, безусловно, для любых потоков. Для повторного согласования вам просто нужно создать другое предложение, отправить его setRemoteDescription получателю, создать ответ, отправить его обратно и установить его как удаленное описание. Это почти все. Никакого разъединения или согласования ICE не должно происходить, нужно только обменять обновленный SDP.   -  person deceze♦    schedule 19.02.2016


Ответы (1)


Обновление: рабочий пример внизу.

Это во многом зависит от того, какой браузер вы используете в данный момент, из-за развития спецификации.

В спецификации и Firefox одноранговые соединения теперь в основном отслеживаются: основаны и не зависят от локальных потоковых ассоциаций. У вас есть var sender = pc.addTrack(track, stream), pc.removeTrack(sender) и даже sender.replaceTrack(track), причем последнее не требует повторного согласования вообще.

В Chrome у вас все еще есть только pc.addStream и pc.removeStream, и удаление дорожки из локального потока приводит к прекращению ее отправки, но добавление ее обратно не сработало. Мне повезло с удалением и повторным добавлением всего потока в одноранговое соединение с последующим повторным согласованием.

К сожалению, использование adapter.js здесь не помогает, поскольку addTrack сложно заполнить полифилом.

Повторные переговоры

Повторные переговоры не начинаются заново. Все что тебе нужно это:

pc.onnegotiationneeded = e => pc.createOffer()
  .then(offer => pc.setLocalDescription(offer))
  .then(() => signalingChannel.send(JSON.stringify({sdp: pc.localDescription})));
  .catch(failed);

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

С его помощью вы можете добавлять / удалять треки во время живого соединения, и это должно работать.

Если этого недостаточно, вы можете даже pc.createDataChannel("yourOwnSignalingChannel")

Пример

Вот пример всего этого (используйте https://jsfiddle.net/7vxzbybp/ в Chrome):

var config = { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] };
var signalingDelayMs = 0;

var dc, sc, pc = new RTCPeerConnection(config), live = false;
pc.onaddstream = e => v2.srcObject = e.stream;
pc.ondatachannel = e => dc? scInit(sc = e.channel) : dcInit(dc = e.channel);

var streams = [];
var haveGum = navigator.mediaDevices.getUserMedia({fake:true, video:true})
.then(stream => streams[1] = stream)
.then(() => navigator.mediaDevices.getUserMedia({ video: true }))
.then(stream => v1.srcObject = streams[0] = stream);

pc.oniceconnectionstatechange = () => update(pc.iceConnectionState);

var negotiating; // Chrome workaround
pc.onnegotiationneeded = () => {
  if (negotiating) return;
  negotiating = true;
  pc.createOffer().then(d => pc.setLocalDescription(d))
  .then(() => live && sc.send(JSON.stringify({ sdp: pc.localDescription })))
  .catch(log);
};
pc.onsignalingstatechange = () => negotiating = pc.signalingState != "stable";

function scInit() {
  sc.onmessage = e => wait(signalingDelayMs).then(() => { 
    var msg = JSON.parse(e.data);
    if (msg.sdp) {
      var desc = new RTCSessionDescription(JSON.parse(e.data).sdp);
      if (desc.type == "offer") {
        pc.setRemoteDescription(desc).then(() => pc.createAnswer())
        .then(answer => pc.setLocalDescription(answer)).then(() => {
          sc.send(JSON.stringify({ sdp: pc.localDescription }));
        }).catch(log);
      } else {
        pc.setRemoteDescription(desc).catch(log);
      }
    } else if (msg.candidate) {
      pc.addIceCandidate(new RTCIceCandidate(msg.candidate)).catch(log);
    }
  }).catch(log);
}

function dcInit() {
  dc.onopen = () => {
    live = true; update("Chat:"); chat.disabled = false; chat.select();
  };
  dc.onmessage = e => log(e.data);
}

function createOffer() {
  button.disabled = true;
  pc.onicecandidate = e => {
    if (live) {
      sc.send(JSON.stringify({ "candidate": e.candidate }));
    } else if (!e.candidate) {
      offer.value = pc.localDescription.sdp;
      offer.select();
      answer.placeholder = "Paste answer here";
    }
  };
  dcInit(dc = pc.createDataChannel("chat"));
  scInit(sc = pc.createDataChannel("signaling"));
};

offer.onkeypress = e => {
  if (e.keyCode != 13 || pc.signalingState != "stable") return;
  button.disabled = offer.disabled = true;
  var obj = { type:"offer", sdp:offer.value };
  pc.setRemoteDescription(new RTCSessionDescription(obj))
  .then(() => pc.createAnswer()).then(d => pc.setLocalDescription(d))
  .catch(log);
  pc.onicecandidate = e => {
    if (e.candidate) return;
    if (!live) {
      answer.focus();
      answer.value = pc.localDescription.sdp;
      answer.select();
    } else {
      sc.send(JSON.stringify({ "candidate": e.candidate }));
    }
  };
};

answer.onkeypress = e => {
  if (e.keyCode != 13 || pc.signalingState != "have-local-offer") return;
  answer.disabled = true;
  var obj = { type:"answer", sdp:answer.value };
  pc.setRemoteDescription(new RTCSessionDescription(obj)).catch(log);
};

chat.onkeypress = e => {
  if (e.keyCode != 13) return;
  dc.send(chat.value);
  log("> " + chat.value);
  chat.value = "";
};

function addTrack() {
  pc.addStream(streams[0]);
  flipButton.disabled = false;
  removeAddButton.disabled = false;
}

var flipped = 0;
function flip() {
  pc.getSenders()[0].replaceTrack(streams[flipped = 1 - flipped].getVideoTracks()[0])
  .catch(log);
}

function removeAdd() {
  if ("removeTrack" in pc) {
    pc.removeTrack(pc.getSenders()[0]);
    pc.addStream(streams[flipped = 1 - flipped]);
  } else {
    pc.removeStream(streams[flipped]);
    pc.addStream(streams[flipped = 1 - flipped]);
  }
}

var wait = ms => new Promise(resolve => setTimeout(resolve, ms));
var update = msg => div2.innerHTML = msg;
var log = msg => div.innerHTML += msg + "<br>";
<video id="v1" width="120" height="90" autoplay muted></video>
<video id="v2" width="120" height="90" autoplay></video><br>
<button id="button" onclick="createOffer()">Offer:</button>
<textarea id="offer" placeholder="Paste offer here"></textarea><br>
Answer: <textarea id="answer"></textarea><br>
<button id="button" onclick="addTrack()">AddTrack</button>
<button id="removeAddButton" onclick="removeAdd()" disabled>Remove+Add</button>
<button id="flipButton" onclick="flip()" disabled>ReplaceTrack (FF only)</button>
<div id="div"><p></div><br>
<table><tr><td><div id="div2">Not connected</div></td>
  <td><input id="chat" disabled></input></td></tr></table><br>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

Инструкции:

Сервер не задействован, поэтому нажмите Offer, затем вставьте предложение и ответьте вручную между двумя вкладками (после вставки нажмите клавишу ENTER).

После этого вы можете поговорить по каналу данных и нажать addTrack, чтобы добавить видео на другую сторону.

Затем вы можете отключить видео, отображаемое удаленно, с помощью Remove + Add или replaceTrack (FF only) (измените скрипт в Chrome, если у вас есть дополнительная камера, которую вы хотите использовать.)

Пересогласование сейчас происходит по каналу данных (больше не нужно вырезать и вставлять).

person jib    schedule 19.02.2016
comment
Требуется ли pc.onnegotiation для работы со всеми браузерами, поддерживающими webtrc? - person Akshay Rathore; 29.02.2016
comment
Да, это основная функция. - person jib; 29.02.2016
comment
получение ошибки с указанным выше скриптом TypeError: Аргумент 1 RTCPeerConnection.addStream не является объектом. когда onclick addTrack - person user969068; 25.05.2017
comment
@ user969068 Спасибо, что сообщили мне об этом! Вы используете бета-версию Firefox? Это проблема с фрагментом кода. Я зарегистрировал в нем ошибку. Вместо этого используйте https://jsfiddle.net/7vxzbybp/ пока. - person jib; 25.05.2017
comment
@jib linux chrome Версия 58.0.3029.110 (64-разрядная версия), я пытался отправить / получить предложение с помощью FireFox53.0.2 (64-разрядная версия). На самом деле я изо всех сил пытаюсь услышать последние пару дней, я изучаю webrtc, мне удалось выполнить обмен текстовыми сообщениями, но ... Я пытаюсь соединить двух сверстников, при загрузке страницы они могут выполнять текстовый чат, есть две кнопки Аудиовызов, видеозвонок, они могут подключать / отключать аудио / видеозвонки, когда захотят, но текстовый чат должен оставаться открытым. ваш фрагмент помог мне немного понять о повторных переговорах, но все еще застрял, у вас есть какой-нибудь фрагмент? это было бы большим подспорьем, спасибо - person user969068; 25.05.2017
comment
@jib Еще одна вещь в том, что я пытаюсь разработать, когда страница загружается, доступен текстовый чат, и он не вызывает getUserMedia (), он должен вызывать getUserMedia при нажатии на Audio Call или Video Call, на этот раз он должен спросить разрешение пользователя отображать всплывающее окно для разрешения микрофона / веб-камеры. , Я просмотрел все ваши ответы webRTC, вы, кажется, очень хорошо осведомлены в этом вопросе, любезно, если вы можете помочь мне с помощью фрагмента, здесь вы найдете пример, который я использую, чтобы заставить работать текстовый чат и пытаюсь расширить функциональность - person user969068; 25.05.2017
comment
Я описал выше stackoverflow.com/ вопросы / 44167006 /, еще раз спасибо - person user969068; 25.05.2017
comment
@jib, переговоры не работают ч / б chrome и firefox, знаете об этом? - person Vivek Doshi; 30.05.2018
comment
@VivekDoshi Звучит как новый вопрос. wfm. - person jib; 30.05.2018
comment
@wfm, stackoverflow.com/questions/50562768/ - person Vivek Doshi; 31.05.2018
comment
Отсутствует закрывающая скобка @ '.then (() = ›signalingChannel.send (JSON.stringify ({sdp: pc.localDescription}))' .stringify не закрывается. - person robertfoenix; 27.09.2020
comment
@robertfoenix исправлен, спасибо! По крайней мере, в рабочих примерах это было правильно. ;) - person jib; 29.09.2020
comment
Да, это помогает! ха-ха - person robertfoenix; 29.09.2020