[Перевод] Как аккуратно залезть в кишки WebRTC при передаче голоса и видео

f3400118fa0d426e8fb64c7107307248.pngWebRTC — технология интересная, но чуток запутанная. В первую очередь тем, что это не одна технология, а комбайн. Захват видео с камеры и звука с микрофона. Установка peer-to-peer подключения между двумя браузерами с протыканием NAT по мере возможности. Передача звука и видео по этому подключению, с пониманием, что передаются realtime данные: кодеки, пропускная способность, потеря кадров, вот это всё. Ну и, наконец, воспроизведение полученного в окне другого браузера. Или не браузера, это уже как зайдет. Ах да, еще — realtime передача пользовательских данных по той же схеме для игр, датчиков и всего того, где недопустимы лаги tcp websocket. Мы в Voximplant постоянно копаемся в кишках технологии, чтобы у клиентов были качественные звук и видео во всех случаях, а не только по локальной 100-мегабитке. И нам было очень приятно почитать на прошлой неделе интересную статью, которая рассказывает, как в этих кишках правильно копаться. Предлагаем вам тоже почитать адаптированный перевод, специально для Хабра!

WebRTC 1.0 использует SDP, чтобы узнать возможности двух устанавливающих соединение сторон. Многим не нравится использование протокола из телефонии 90-х, но жестокая реальность такова, что SDP будет с нами ещё долго. И если вы хотите залезть в кишки WebRTC действительно глубоко: переключать кодеки, менять ширину канала передачи данных, то вам придется испачкать руки в SDP.

Недавно в Бостоне прошла конференция о WebRTC. Nick Gauthier из MeetSpace рассказал, как он менял SDP и использовал другие трюки, чтобы сделать видеоконференцию на 10 человек. Без единого сервера, то есть каждый браузер отправлял видеопоток 9 другим. Такие задачи возникают нечасто, но возможность ручного контроля за шириной канала WebRTC может быть очень полезна. Видеовыступление можно посмотреть здесь. А ниже я расскажу, как он все это делал.

Без нашего вмешательства PeerConnection использует всю доступную ширину канала для обеспечения максимального качества видео. Или звука. Что очень круто, если видеоконференция — это единственное, что сейчас делает ваш компьютер. Но что, если вы параллельно пользуетесь GMail? Или у вас мобильное подключение с «плавающей» шириной канала? Или, как у нас в MeetSpace, вы устанавливаете 10-стороннее подключение и PeerConnection'ы общаются друг с другом?

В этом посте я хочу показать вам, как можно «на лету» распарсить и модифицировать SDP с помощью JavaScript для установки максимальной ширины используемого канала.

Где модифицировать SDP


Сначала нам нужно получить данные SDP. Самый первый пакет SDP создается, когда объект PeerConnection создает Offer, который вам нужно передать второй договаривающейся о соединении стороне:
Посмотреть код
peerConnection.createOffer(
  function(offer) {
    console.debug("The offer SDP:", offer.sdp);
    peerConnection.setLocalDescription(offer);
    // your signaling code to communicate the offer goes here
  }
);

peerConnection.createOffer(
  function(offer) {
    console.debug("The offer SDP:", offer.sdp);
    peerConnection.setLocalDescription(offer);
    // your signaling code to communicate the offer goes here
  }
);


Что нужно сделать? Модифицировать SDP пакет до того, как мы его передадим второй стороне. Удачно, что WebRTC не включает в стандарт «signaling» и на плечи разработчика ложится задача передачи Offer’ов между двумя устанавливающими соединение сторонами:
Посмотреть код
peerConnection.createOffer(
  function(offer) {    
    peerConnection.setLocalDescription(offer);
    // modify the SDP after calling setLocalDescription
    offer.sdp = setMediaBitrates(offer.sdp);
    // your signaling code to communicate the offer goes here
  }
);

peerConnection.createOffer(
  function(offer) {    
    peerConnection.setLocalDescription(offer);
    // modify the SDP after calling setLocalDescription
    offer.sdp = setMediaBitrates(offer.sdp);
    // your signaling code to communicate the offer goes here
  }
);


В коде выше мы вызываем функцию setMediaBitrates, которая применит нужные нам модификации и вернет измененный пакет SDP (детали я расскажу чуть позже). Любопытный нюанс: нельзя менять пакет между вызовами createOffer/createAnswer и setLocalDescription. Так что мы поменяем его перед передачей второй договаривающейся стороне. Когда пакет достигнет второй стороны, мы должны будем также поменять второй SDP пакет, который WebRTC на второй стороне создаст как «Answer». Это необходимо, так как «Offer» звучит «Это та ширина канала, которую я могу использовать», но и «Answer» тоже звучит как «А это та ширина канала, которую могу использовать я». Ограничивать надо с обоих концов трубы:
Посмотреть код
peerConnection.setRemoteDescription(new RTCSessionDescription(offer)).then(function() {
  peerConnection.createAnswer().then(function(answer) {    
    peerConnection.setLocalDescription(answer);    
    // modify the SDP after calling setLocalDescription    
    answer.sdp = setMediaBitrates(answer.sdp);
    // your signaling code to communicate the answer goes here
  };
};

peerConnection.setRemoteDescription(new RTCSessionDescription(offer)).then(function() {
  peerConnection.createAnswer().then(function(answer) {    
    peerConnection.setLocalDescription(answer);    
    // modify the SDP after calling setLocalDescription    
    answer.sdp = setMediaBitrates(answer.sdp);
    // your signaling code to communicate the answer goes here
  };
};


Теперь, когда мы выбрали места для модификации SDP, можно начинать саму модификацию!

Как распарсить SDP


Очень рекомендую почитать пост от Antón Román «Анатомия WebRTC SDP», он поможет разобраться, что такое SDP и как он устроен. Именно с этого поста началось мое собственное приключение. Еще рекомендую спецификацию: RFC 4566 SDP: Session Description Protocol. Ссылка приведет вас на 5-ю секцию 7-й страницы, где как раз описан формат. Для тех, кто не любит читать длинные специ, краткая выжимака: SDP представляет собой UTF-8 текст, разбитый на строки вида » = ».

Обратите внимание на важную штуку, спрятанную в глубине 5-й секции документации: порядок указания types. Не буду повторять здесь огромный кусок текста, и снова дам выжимку. В начале SDP есть секция, за которой следуют повторяющиеся «media descriptions». Их порядок всегда будет один и тот же: «m», «i», «c», «b», «k», «a».

Это еще не все. Теперь нужно заглянуть в FC 3556 Session Description Protocol (SDP) Bandwidth Modifiers for RTP Control Protocol (RTCP) Bandwidth. В этой спецификации рассказано, как устанавливать ширину канала с помощью «type» со значением «b». Соответствующая строка SDP имеет вид «b=AS: XXX», где XXX — ширина канала, которую мы хотим установить. Акроним «AS» расшифровывается как «Application Specific Maximum», то есть максимальная допустимая ширина канала. Также из RFC мы видим, что значение устанавливается в килобитах в секунду, kbps. Итого, наш код будет работать по такому алгоритму:

Пропускаем строки, пока не найдем "m=audio" или "m=video"
Пропускаем строки с type "i" и "c"
Если строка имеет type "b", заменяем ее
Если строка имеет другой тип, вставляем строку с type "b"

Как модифицировать SDP


Для большинства видеозвонков WebRTC в протоколе будет использована media description для видео и media description для звука. В нашем примере мы ограничиваем видеопоток 500kb/s и звуковой поток 50kb/s:
Посмотреть код
function setMediaBitrates(sdp) {
  return setMediaBitrate(setMediaBitrate(sdp, "video", 500), "audio", 50);
}

function setMediaBitrate(sdp, media, bitrate) {
  var lines = sdp.split("\n");
  var line = -1;
  for (var i = 0; i < lines.length; i++) {
    if (lines[i].indexOf("m="+media) === 0) {
      line = i;
      break;
    }
  }
  if (line === -1) {
    console.debug("Could not find the m line for", media);
    return sdp;
  }
  console.debug("Found the m line for", media, "at line", line);

  // Pass the m line
  line++;

  // Skip i and c lines
  while(lines[line].indexOf("i=") === 0 || lines[line].indexOf("c=") === 0) {
    line++;
  }

  // If we're on a b line, replace it
  if (lines[line].indexOf("b") === 0 {
    console.debug("Replaced b line at line", line);
    lines[line] = "b=AS:"+bitrate;
    return lines.join("\n");
  }
  
  // Add a new b line
  console.debug("Adding new b line before line", line);
  var newLines = lines.slice(0, line)
  newLines.push("b=AS:"+bitrate)
  newLines = newLines.concat(lines.slice(line, lines.length))
  return newLines.join("\n")
}

function setMediaBitrates(sdp) {
  return setMediaBitrate(setMediaBitrate(sdp, "video", 500), "audio", 50);
}
 
function setMediaBitrate(sdp, media, bitrate) {
  var lines = sdp.split("\n");
  var line = -1;
  for (var i = 0; i < lines.length; i++) {
    if (lines[i].indexOf("m="+media) === 0) {
      line = i;
      break;
    }
  }
  if (line === -1) {
    console.debug("Could not find the m line for", media);
    return sdp;
  }
  console.debug("Found the m line for", media, "at line", line);
 
  // Pass the m line
  line++;
 
  // Skip i and c lines
  while(lines[line].indexOf("i=") === 0 || lines[line].indexOf("c=") === 0) {
    line++;
  }
 
  // If we're on a b line, replace it
  if (lines[line].indexOf("b") === 0 {
    console.debug("Replaced b line at line", line);
    lines[line] = "b=AS:"+bitrate;
    return lines.join("\n");
  }
  
  // Add a new b line
  console.debug("Adding new b line before line", line);
  var newLines = lines.slice(0, line)
  newLines.push("b=AS:"+bitrate)
  newLines = newLines.concat(lines.slice(line, lines.length))
  return newLines.join("\n")
}


Это всё! Честно говоря, я сильно напрягся, когда я первый раз столкнулся с SDP. Подавляющее количество мелких деталей, которые все надо понять. Но по большему счету это всего лишь набор строк, каждая из которых что-нибудь определяет для подключения. Нам не нужны регэкспы, так как секции всегда имеют один и тот же порядок. В нашем случае мы просто заменяли строку с type «b», так что даже парсить ничего не пришлось.

Надеюсь, эта статья поможет вам лучше понять как работает WebRTC и как модифицировать её под свои нужды.

Комментарии (0)

© Habrahabr.ru