Saya sedang membangun aplikasi WebRTC di mana pengguna dapat berbagi kamera dan layar mereka. Saat klien menerima aliran/trek, klien perlu mengetahui apakah itu aliran kamera atau aliran perekaman layar. Perbedaan ini terlihat jelas pada ujung pengirim, tetapi perbedaan tersebut hilang pada saat trek mencapai rekan penerima.

Berikut beberapa contoh kode dari aplikasi saya:

// 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 imajiner ideal saya memungkinkan penambahan .label ke trek atau aliran, seperti ini:

// 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
}

Tapi API ini sebenarnya tidak ada. Ada properti MediaStreamTrack.label, tapi itu hanya-baca, dan tidak disimpan dalam transmisi. Dengan eksperimen, properti .label di ujung pengiriman bersifat informatif (mis. label: "FaceTime HD Camera (Built-in) (05ac:8514)"). Tetapi di sisi penerima, .label untuk trek yang sama tidak dipertahankan. (Tampaknya diganti dengan .id trek - setidaknya di Chrome.)

Artikel ini oleh Kevin Moreland menggambarkan masalah yang sama, dan merekomendasikan solusi yang agak menakutkan: munge SDP di ujung pengiriman, dan kemudian ambil SDP di sisi penerima. Tetapi solusi ini terasa sangat rapuh dan level rendah.

Saya tahu ada properti MediaStreamTrack.id. Ada juga properti MediaStream.id. Kedua hal ini tampaknya dipertahankan dalam transmisi. Ini berarti saya dapat mengirim metadata di saluran samping, seperti saluran pensinyalan atau DataChannel. Dari ujung pengiriman, saya akan mengirim { "myStreams": { "screen": "<some stream id>", "camera": "<another stream id>" } }. Ujung penerima akan menunggu hingga metadata dan alirannya ada sebelum menampilkan apa pun. Namun, pendekatan ini memperkenalkan saluran samping (dan tantangan konkurensi yang tak terhindarkan terkait dengan itu), di mana saluran samping terasa tidak perlu.

Saya mencari solusi yang kuat dan idiomatis. Bagaimana cara memberi label/mengidentifikasi MediaStreams di ujung pengirim, sehingga penerima tahu aliran mana?

1
jameshfisher 22 Desember 2020, 15:27

3 jawaban

Jawaban Terbaik

Saya akhirnya mengirim metadata ini di saluran pensinyalan. Setiap pesan pensinyalan yang berisi SessionDescription (SDP) sekarang juga berisi objek metadata di sampingnya, yang menjelaskan MediaStream yang dijelaskan dalam SDP. Ini tidak memiliki masalah konkurensi, karena klien akan selalu menerima SDP+metadata untuk MediaStream sebelum peristiwa track diaktifkan untuk MediaStream tersebut.

Jadi sebelumnya saya mendapat pesan sinyal seperti ini:

{
  "kind": "sessionDescription",

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

Sekarang saya memiliki pesan sinyal seperti ini:

{
  "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" }
  }
}
4
jameshfisher 22 Desember 2020, 16:14

Pendekatan yang lebih kanonik untuk memberi sinyal pada label streaming khusus dengan metadata adalah dengan memodifikasi SDP sebelum mengirim (tetapi setelah setLocalDescription) dan memodifikasi atribut msid (yang merupakan singkatan dari media stream id, lihat spesifikasinya). Keuntungannya di sini adalah bahwa di ujung jarak jauh atribut aliran media id diuraikan dan terlihat di aliran acara ontrack. Lihat biola ini

Perhatikan bahwa Anda tidak dapat membuat asumsi tentang id trek. Di Firefox, id trek di SDP bahkan tidak cocok dengan id trek di sisi pengirim.

3
Philipp Hancke 22 Desember 2020, 18:34

Cara ketiga adalah dengan mengandalkan urutan deterministik transceiver:

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>

Ini bekerja dengan baik ketika Anda mengendalikan negosiasi, seperti ketika negosiasi awal hanya terjadi dari satu sisi.

Ini bekerja kurang baik ketika kedua belah pihak dapat memulai negosiasi, karena ketika kedua belah pihak membuat transceiver, urutan mereka tidak harus deterministik lagi.

Dalam kasus tersebut, Anda lebih baik memberi sinyal id seperti transceiver.mid atau stream.id di luar pita seperti yang ditunjukkan oleh jawaban lainnya. Saya membahas ini secara mendetail di blog saya.

2
jib 23 Desember 2020, 00:00