Як акуратно залізти в кишки WebRTC при передачі голосу і відео

WebRTC — технологія цікава, але трохи заплутана. У першу чергу тим, що це не одна технологія, а комбайн. Захоплення відео з камери і звуку з мікрофону. Установка 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 ми бачимо, що значення встановлюється в кілобітах в секунду, сек. Отже, наш код буде працювати за таким алгоритмом:

Пропускаємо рядка, поки не знайдемо "m=audio" або "m=video"
Пропускаємо рядки з type "і" і "с"
Якщо рядок має 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 line m
line++;

// Skip i and lines c
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 line m
line++;

// Skip i and lines c
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 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.