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

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

Вот пример кода из моего приложения:

// Note the distinction between streams is obvious at the sending end.
const localWebcamStream = await navigator.mediaDevices.getUserMedia({ ... });
const screenCaptureStream = await navigator.mediaDevices.getDisplayMedia({ ... });

// This is called by signalling logic
function addLocalTracksToPeerConn(peerConn) {
  // Our approach here loses information because our two distinct streams 
  // are added to the PeerConnection's homogeneous bag of streams

  for (const track of screenCaptureStream.getTracks()) {
    peerConn.addTrack(track, screenCaptureStream);
  }

  for (const track of localWebcamStream.getTracks()) {
    peerConn.addTrack(track, localWebcamStream);
  }
}

// This is called by signalling logic
function handleRemoteTracksFromPeerConn(peerConn) {
    peerConn.ontrack = ev => {
      const stream = ev.streams[0];
      if (stream is a camera stream) {  // FIXME how to distinguish reliably?
        remoteWebcamVideoEl.srcObject = stream;
      }
      else if (stream is a screen capture) {  // FIXME how to distinguish reliably?
        remoteScreenCaptureVideoEl.srcObject = stream;
      }
  };
}

Мой идеальный воображаемый API позволил бы добавить .label к дорожке или потоку, например:

// On sending end, add arbitrary metadata
track.label = "screenCapture";
peerConn.addTrack(track, screenCaptureStream);

// On receiving end, retrieve arbitrary metadata
peerConn.ontrack = ev => {
      const trackType = ev.track.label;  // get the label when receiving the track
}

Но этого API на самом деле не существует. Есть свойство MediaStreamTrack.label, но оно читается - только и не сохранился в передаче. Экспериментируя, свойство .label на отправляющей стороне является информативным (например, label: "FaceTime HD Camera (Built-in) (05ac:8514)"). Но на принимающей стороне .label для того же трека не сохраняется. (Кажется, он заменен на .id трека - по крайней мере, в Chrome.)

В этой статье Кевина Морленда описывается та же проблема, и рекомендует умеренно устрашающее решение: подключите SDP к отправляющей стороне, а затем выполните grep с SDP на принимающей стороне. Но это решение кажется очень хрупким и низкоуровневым.

Я знаю, что есть свойство MediaStreamTrack.id. Также есть свойство MediaStream.id. Оба они, похоже, сохраняются при передаче. Это означает, что я мог отправлять метаданные по побочному каналу, например сигнальному каналу или DataChannel. Со стороны отправки я бы отправил { "myStreams": { "screen": "<some stream id>", "camera": "<another stream id>" } }. Принимающая сторона будет ждать, пока у нее появятся и метаданные, и поток, прежде чем что-либо отображать. Однако этот подход вводит побочный канал (и неизбежные проблемы параллелизма, связанные с этим), где побочный канал кажется ненужным.

Я ищу идиоматическое надежное решение. Как мне пометить / идентифицировать MediaStreams на отправляющей стороне, чтобы принимающая сторона знала, какой поток какой?


person jameshfisher    schedule 22.12.2020    source источник


Ответы (3)


В итоге я отправил эти метаданные в сигнальный канал. Каждое сигнальное сообщение, содержащее SessionDescription (SDP), теперь также содержит рядом с собой объект метаданных, который аннотирует MediaStream, которые описаны в SDP. Здесь нет проблем с параллелизмом, потому что клиенты всегда будут получать метаданные SDP + для MediaStream до того, как для этого MediaStream будет запущено событие track.

Итак, раньше у меня были такие сигнальные сообщения:

{
  "kind": "sessionDescription",

  // An RTCSessionDescriptionInit
  "sessionDescription": { "type": "offer", "sdp": "..." }
}

Теперь у меня есть такие сигнальные сообщения:

{
  "kind": "sessionDescription",

  // An RTCSessionDescriptionInit
  "sessionDescription": { "type": "offer", "sdp": "..." },

  // A map from MediaStream IDs to arbitrary domain-specific metadata
  "mediaStreamMetadata": {
    "y6w4u6e57654at3s5y43at4y5s46": { "type": "camera" },
    "ki8a3greu6e53a4s46uu7dtdjtyt": { "type": "screen" }
  }
}
person jameshfisher    schedule 22.12.2020
comment
Хороший ответ. Возможно, вы захотите упомянуть, что это работает, потому что screenCaptureStream и localWebcamStream реплицируются удаленно с соответствующими ids, потому что вы упомянули их в addTrack. - person jib; 23.12.2020

Более каноническим подходом к передаче метаданных настраиваемой метки потока было бы изменение SDP перед отправкой (но после setLocalDescription) и изменение атрибута msid (который обозначает идентификатор медиапотока, см. спецификацию). Преимущество здесь заключается в том, что на удаленном конце атрибут id медиапотока анализируется и отображается в потоке события ontrack. См. эту скрипку

Обратите внимание, что вы не можете делать никаких предположений об идентификаторе трека. В Firefox идентификатор дорожки в SDP даже не совпадает с идентификатором дорожки на стороне отправителя.

person Philipp Hancke    schedule 22.12.2020
comment
Моя основная проблема заключается в том, что опыт научил меня видеть «МИГАЮЩИЕ КРАСНЫЕ СИГНАЛЫ АВАРИЙНЫХ СИГНАЛОВ» всякий раз, когда регулярное выражение используется для структурированных данных ... Мне было бы удобнее, если бы был правильный парсер / сериализатор SDP или разумный API для структурированного редактирования SDP - person jameshfisher; 30.12.2020
comment
см. github.com/otalk/sdp, который я написал, или github.com/clux/sdp-transform И да, обработка SDP как stringsoup кашель ... - person Philipp Hancke; 31.12.2020

Третий способ - полагаться на детерминированный порядок приемопередатчиков:

const pc1 = new RTCPeerConnection(), pc2 = new RTCPeerConnection();

go.onclick = () => ["Y","M","C","A"].forEach(l => pc1.addTrack(getTrack(l)));

pc2.ontrack = ({track, transceiver}) => {
  const video = [v1, v2, v3, v4][pc2.getTransceivers().indexOf(transceiver)];
  video.srcObject = new MediaStream([track]);
};

pc1.onicecandidate = e => e.candidate && pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => e.candidate && pc1.addIceCandidate(e.candidate);
pc1.onnegotiationneeded = async () => {
  await pc1.setLocalDescription(await pc1.createOffer());
  await pc2.setRemoteDescription(pc1.localDescription);
  await pc2.setLocalDescription(await pc2.createAnswer());
  await pc1.setRemoteDescription(pc2.localDescription);
};

function getTrack(txt, width = 100, height = 100, font = "100px Arial") {
  const can = Object.assign(document.createElement("canvas"), {width,height});
  const ctx = Object.assign(can.getContext('2d'), {font});
  requestAnimationFrame(function draw() {
    ctx.fillStyle = '#eeeeee';
    ctx.fillRect(0, 0, width, width);
    ctx.fillStyle = "#000000";
    ctx.fillText(txt, width/2 - 14*width/32, width/2 + 10*width/32);
    requestAnimationFrame(draw);
  });
  return can.captureStream().getTracks()[0];
};
<button id="go">Go!</button><br>
<video id="v1" autoplay></video>
<video id="v2" autoplay></video>
<video id="v3" autoplay></video>
<video id="v4" autoplay></video>
<div id="div"></div>

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

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

В этих случаях вам лучше сигнализировать об идентификаторах типа transceiver.mid или stream.id вне диапазона, как показывают другие ответы. Я подробно рассказываю об этом в моем блоге.

person jib    schedule 22.12.2020
comment
Хороший. Я не знал, что они заказаны. Я думаю, что предпочитаю свое решение, потому что оно делает меньше предположений, но это полезно знать :-) - person jameshfisher; 23.12.2020