Привет, друзья!


Представляю вашему вниманию третью (заключительную) часть перевода этой замечательной книги по WebRTC.



Если вам это интересно, прошу под кат.


Содержание этой части



Обмен другими данными


WebRTC предоставляет каналы (data channels) для обмена данными (data communication). Между двумя пирами можно открыть до 65 534 каналов. Канал данных основан на датаграммах (datagram) и имеет определенную продолжительность (durability). По умолчанию канал данных гарантирует сохранения порядка сообщений.


На первый взгляд каналы данных могут казаться лишними. Зачем нам эта подсистема, когда мы можем использовать HTTP или веб-сокеты для обмена данными?


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


Как это работает?


Для обмена данными WebRTC использует SCTP, определенный в RFC 4960. SCTP — это транспортный протокол, являющийся альтернативой TCP и UDP. В WebRTC он используется как протокол уровня приложения поверх соединения DTLS.


SCTP состоит из потоков, каждый из которых может настраиваться независимо от других. Каналы данных — это всего лишь абстракция потоков. Настройки, связанные с продолжительностью существования канала и порядком доставки сообщений, просто передаются агенту SCTP.


Каналы данных имеют некоторые возможности, отсутствующие в SCTP, например, метки каналов (channel labels). Для этого используется Протокол установки канала данных (Data Channel Establishment Protocol, DCEP), определенный в RFC 8832. DCEP определяет сообщения для взаимодействия с меткой канала и протоколом.


DCEP


DCEP содержит два сообщения: DATA_CHANNEL_OPEN и DATA_CHANNEL_ACK. Открытый канал должен получить ACK от удаленного пира.


DATA_CHANNEL_OPEN


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


Формат пакета


 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Message Type |  Channel Type |            Priority           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Reliability Parameter                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Label Length          |       Protocol Length         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
\                                                               \
/                             Label                             /
\                                                               \
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
\                                                               \
/                            Protocol                           /
\                                                               \
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Message Type


Message Type (тип сообщения) имеет фиксированное значение 0x03.


Channel Type


Channel Type (тип канала) определяет такие атрибуты канала, как продолжительность и порядок. Может иметь следующие значения:


  • DATA_CHANNEL_RELIABLE (0x00) — сообщения не были потеряны и будут прибывать в правильном порядке;
  • DATA_CHANNEL_RELIABLE_UNORDERED (0x80) — сообщения не были потеряны, но могут прибывать в неправильном порядке;
  • DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT (0x01) — сообщения могут быть потеряны (после выполнения всех попыток), но будут прибывать в правильном порядке;
  • DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT_UNORDERED (0x81) — сообщения могут быть потеряны (после выполнения всех попыток) и могут прибывать в неправильном порядке;
  • DATA_CHANNEL_PARTIAL_RELIABLE_TIMED (0x02) — сообщения могут быть потеряны (если они не прибывают в запрошенное время), но будут прибывать в правильном порядке;
  • DATA_CHANNEL_PARTIAL_RELIABLE_TIMED_UNORDERED (0x82) — сообщения могут быть потеряны (если они не прибывают в запрошенное время) и могут прибывать в неправильном порядке.

Priority (приоритет)


Определяет приоритет канала. Каналы с более высоким приоритетом создаются раньше. Большие сообщения с низким приоритетом не задерживают отправку сообщений с высоким приоритетом.


Reliability Parameter (параметр надежности)


Если типом канала является DATA_CHANNEL_PARTIAL_RELIABLE, суффикс определяет следующее поведение:


  • REXMIT — определяет количество попыток отправки сообщения;
  • TIMED — определяет, как долго отправитель будет пытаться отправить сообщение (в мс).

Label (метка, подпись)


Строка в формате UTF-8, содержащая название канала данных. Данная строка может быть пустой.


Protocol (протокол)


Если данная строка является пустой, протокол считается неопределенным. Если строка не является пустой, в ней должно содержаться значение из "Реестра имен подпротокола WebSocket" (WebSocket Subprotocol Name Registry), определенного в RFC 6455.


DATA_CHANNEL_ACK


Данное сообщение является подтверждением открытия канала данных, передаваемого от отправителя получателю.


Формат пакета


0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Message Type |
+-+-+-+-+-+-+-+-+

SCTP


SCTP предоставляет следующие возможности:


  • мультиплексирование;
  • надежная доставка сообщений (для этого используется механизм передачи, подобный TCP);
  • настройки частичной надежности (partial-reliability options);
  • предотвращение перегрузки;
  • контроль потока.

Мы разделим рассмотрение SCTP на три части.


Концепция


SCTP предоставляет большое количество возможностей. Мы рассмотрим только те возможности, которые используются в WebRTC. К возможностям, которые не используются WebRTC, относятся множественная адресация (multi-homing) и выбор пути.


SCTP разрабатывается более 20 лет, так что его непросто понять.


Ассоциация


Ассоциация (assosiation) — термин, относящийся к сессии SCTP. Это состояние, распределяемое между двумя взаимодействующими агентами.


Потоки


Поток (stream) — это двунаправленная последовательность пользовательских данных. Канал данных — это поток. Каждая ассоциация SCTP содержит список потоков. Каждый поток может обладать разным типом надежности.


WebRTC позволяет настраивать поток только в момент создания, но SCTP позволяет менять настройки в любое время.


Датаграммы


Кадры (фреймы, frames) SCTP — это набор датаграмм, а не байтовый поток. Отправка и получение данных похожа на использование UDP вместо TCP. Для передачи нескольких сообщений через один поток не нужен дополнительный код.


Сообщения SCTP не имеют ограничений по размеру, в отличие от сообщений UDP. Одно сообщение SCTP может иметь размер в несколько гигабайт.


Части


Протокол SCTP состоит из частей (chunks). Существуют различные типы частей. Эти части используются для всей коммуникации. Пользовательские данные, установка соединения, контроль перегрузки и т.д. — все это делается с помощью частей.


Каждый пакет SCTP состоит из списка частей. Поэтому в одном пакете UDP может содержаться несколько частей с сообщениями из разных потоков.


Последовательный номер передачи


Последовательный номер передачи (Transmission Sequence Number, TSN) — это глобальный уникальный идентификатор частей DATA. Часть DATA содержит все сообщения, которые желает отправить пользователь. TSN помогает получателю определять потерю или неправильный порядок пакетов.


Если получатель замечает отсутствующий TSN, он не передает данные пользователю до решения этой проблемы.


Идентификатор потока


Каждый поток имеет уникальный идентификатор. При создании канала данных с явным ID, этот ID просто передается SCTP в качестве идентификатора потока. Если такой ID отсутствует, он генерируется автоматически.


Идентификатор протокола полезной нагрузки


Каждая часть DATA также имеет идентификатор протокола полезной нагрузки (Payload Protocol Identifier, PPID). Он используется для идентификации типа передаваемых данных. SCTP имеет много PPID, но в WebRTC используются следующие пять:


  • WebRTC DCEP (50) — сообщения DCEP;
  • WebRTC String (51) — строковые сообщения DataChannel;
  • WebRTC Binary (53) — бинарные сообщения DataChannel;
  • WebRTC String Empty (56) — строковое сообщения DataChannel с нулевой длиной (пустое строковое сообщение);
  • WebRTC Binary Empty (57) — бинарное сообщения DataChannel с нулевой длиной (пустое бинарное сообщение).

Протокол


Ниже представлены некоторые "чанки", которые используются в WebRTC.


Каждый чанк начинается с поля type. Перед списком чанков находится заголовок (header).


DATA


 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Type = 0    | Reserved|U|B|E|    Length                     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                              TSN                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|      Stream Identifier        |   Stream Sequence Number      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  Payload Protocol Identifier                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
\                                                               \
/                            User Data                          /
\                                                               \
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Чанк DATA используется для передачи пользовательских данных. Когда мы что-то отправляем через канал данных, используется этот чанк.


Бит U устанавливается, если данный пакет является неупорядоченным (unordered). Мы можем игнорировать последовательный номер потока.


B и E — это начальный и конечный биты. Если мы хотим отправить сообщение, которое является слишком большим для одного чанка DATA, оно будет разделено (fragmented) на несколько чанков DATA, передаваемых в нескольких пакетах. Для этого SCTP использует биты B и E, а также последовательные номера.


  • B=1, E=0 — первая часть фрагментированного сообщения пользователя;
  • B=0, E=0 — средняя часть фрагментированного сообщения пользователя;
  • B=0, E=1 — последняя часть фрагментированного сообщения пользователя;
  • B=1, E=1 — целое (нефрагментированное) сообщение.

TSN — последовательный номер передачи, уникальный идентификатор данного чанка DATA. После 4 294 967 295 чанка TSN обнуляется. TSN увеличивается на единицу для каждого чанка во фрагментированном сообщении пользователя, что позволяет получателю определять порядок чанков для восстановления исходного сообщения.


Stream Identifier — уникальный идентификатор потока, которому принадлежат данные.


Stream Sequence Number — это 16-битный номер, увеличивающийся при каждом сообщении и включаемый в заголовок сообщения чанка DATA. После 65 535 сообщения SSN обнуляется. Это число используется для определения порядка доставки сообщений получателю, когда U имеет значение 0. SSN похож на TSN, но он увеличивается для каждого сообщения, а не для каждого чанка.


Payload Protocol Identifier — тип данных, передаваемых через поток. В случае с WebRTC он будет DCEP, строкой или двоичными данными.


User Data — это то, что мы отправляем. Все данные, передаваемые через WebRTC, передаются через чанк DATA.


INIT


 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Type = 1    |  Chunk Flags  |      Chunk Length             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Initiate Tag                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Advertised Receiver Window Credit (a_rwnd)          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Number of Outbound Streams   |  Number of Inbound Streams    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          Initial TSN                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
\                                                               \
/              Optional/Variable-Length Parameters              /
\                                                               \
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Чанк INIT запускает процесс создания ассоциации.


Initiate Tag используется для генерации куки (cookie). Куки используются для предотвращения атак "Человек посередине" и "Отказ в обслуживании".


Advertised Receiver Window Credit используется для контроля перегрузки SCTP. Это поле определяет размер буфера, выделенного получателем для данной ассоциации.


Number of Outbound/Inbound Streams уведомляет удаленный пир о количестве потоков, поддерживаемых данным агентом.


Initial TSN — произвольное число (uint32), с которого начинается локальный TSN.


Поле Optional Parameters предназначено для добавления в протокол новых возможностей.


SACK


0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Type = 3    |Chunk  Flags   |      Chunk Length             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      Cumulative TSN Ack                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Advertised Receiver Window Credit (a_rwnd)           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Number of Gap Ack Blocks = N  |  Number of Duplicate TSNs = X |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Gap Ack Block #1 Start       |   Gap Ack Block #1 End        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/                                                               /
\                              ...                              \
/                                                               /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Gap Ack Block #N Start      |  Gap Ack Block #N End         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Duplicate TSN 1                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/                                                               /
\                              ...                              \
/                                                               /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Duplicate TSN X                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Чанк SACK — это подтверждение получения пакета получателем. До получения отправителем SACK для TSN он будет повторно отправлять чанк DATA по запросу. Но SACK не только обновляет TSN.


Cumulative TSN ACK — максимальный полученный TSN.


Advertised Receiver Window Credit — размер буфера получателя. Это значение может меняться в течение сессии, если у получателя появится больше свободной памяти.


Ack Blocks — это TSN, полученные после Cumulative TSN ACK. Это значение используется при наличии бреши в доставленных пакетах. Допустим, были доставлены чанки DATA с TSN, имеющими значения 100, 102, 103 и 104. Cumulative TSN ACK будет иметь значение 100, но Ack Blocks может использоваться для уведомления отправителя об отсутствии необходимости в повторной отправке чанков 102, 103 и 104.


Duplicate TSN информирует отправителя, что он получил указанные чанки DATA более одного раза.


HEARTBEAT


 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Type = 4    | Chunk  Flags  |      Heartbeat Length         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
\                                                               \
/            Heartbeat Information TLV (Variable-Length)        /
\                                                               \
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Чанк HEARTBEAT используется для проверки активности удаленного пира. Может использоваться для обеспечения открытости NAT без отправки чанков DATA.


ABORT


 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Type = 6    |Reserved     |T|           Length              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/                                                               /
\               Zero or more Error Causes                       \
/                                                               /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Чанк ABORT резко закрывает ассоциацию. Используется, когда одна из сторон переходит в состояние ошибки (error state). Для плавного закрытия соединения используется чанк SHUTDOWN.


SHUTDOWN


 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Type = 7    | Chunk  Flags  |      Length = 8               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      Cumulative TSN Ack                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Чанк SHUTDOWN запускает процесс плавного закрытия ассоциации SCTP. Каждый агент информирует другую сторону о последнем полученном TSN. Это позволяет убедиться в том, что никакие пакеты не были потеряны. WebRTC не закрывает ассоциацию автоматически. Поэтому каждый канал данных должен быть закрыт вручную.


Cumulative TSN ACK — последний отправленный TSN. Каждая сторона закрывает соединение только после получения чанка DATA с этим TSN.


ERROR


 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Type = 9    | Chunk  Flags  |           Length              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
\                                                               \
/                 Одна или более причина ошибки                 /
\                                                               \
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Чанк ERROR используется для уведомления другой стороны о возникновении не критичной для ассоциации ошибки.


TSN FORWARD


0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Type = 192  |  Flags = 0x00 |        Length = Variable      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      New Cumulative TSN                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Stream-1              |       Stream Sequence-1       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
\                                                               /
/                                                               \
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Stream-N              |       Stream Sequence-N       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Чанк TSN FORWARD сдвигает глобальный TSN вперед. Это позволяет пропустить пакеты, которые нам больше не нужны. Предположим, что мы отправили пакеты 10 11 12 13 14 15, которые являются валидными только в случае доставки всех пакетов и только в случае своевременной доставки.


Если мы потеряли пакеты 12 и 13, в отправке пакетов 14 и 15 нет смысла. SCTP использует чанк TSN FORWARD для решения этой задачи. Он сообщает получателю, что пакеты 14 и 15 не будут доставлены.


New Cumulative TSN — новый TSN подключения. Любые пакеты, доставленные перед ним, будут отброшены.


Stream и Stream Sequence используются для сдвига вперед Stream Sequence Number.


Конвейер SCTP


Есть несколько интересных вещей, касающихся машины состояния (state machine) SCTP. WebRTC не использует все возможности этой машины — такие возможности мы рассматривать не будем. Мы также упростим некоторые компоненты для их лучшего понимания.


Установка соединения


Чанки INIT и INIT ACK используются для обмена возможностями и настройками каждого пира. SCTP использует куки в процессе рукопожатия для валидации пира, с которым предполагается коммуникация. Это предотвращает перехват рукопожатия и DoS-атаки.


Чанк INIT ACK содержит куки. Куки возвращаются создателю с помощью COOKIE ECHO. Если верификация куки прошла успешно, отправляется COOKIE ACK и можно обмениваться чанками DATA.





Завершение сессии


Для этого используется чанк SHUTDOWN. Когда агент получает чанк SHUTDOWN, он ждет получения запрошенного Cumulative TSN ACK. Это позволяет убедиться в доставке всех данных, даже если соединение является ненадежным.


Механизм поддержания активности


Для поддержания соединения в активном состоянии используются чанки HEARTBEAT REQUEST и HEARTBEAT ACK. Они отправляются с определенной периодичностью. SCTP также выполняет экспоненциальную выдержку (exponential backoff), когда пакеты не доставляются.


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


Применение


Данный раздел посвящен тому, что можно создать с помощью WebRTC, а также тому, как это делается. Все возможности WebRTC имеют свою цену и ценность. Разработка реальных приложений WebRTC — задача не из простых.


Случаи использования


Многие считают, что WebRTC — это про конференции в браузере. Однако, это намного больше. WebRTC используется в широком спектре приложений. Все время появляется что-то новое.


Конференц-связь


Конференц-связь (conferencing) — стандартный случай использования WebRTC. Протокол содержит несколько возможностей, которых не содержит ни один другой протокол. Систему конференц-связи можно построить с помощью WebSocket и она будет работать. Но в условиях реальных сетей WebRTC — лучший выбор.


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


Участники конференции могут отправлять и получать несколько потоков одновременно. Они также могут добавлять и удалять потоки в любое время в течение сессии. Используемые кодеки также предварительно согласовываются. Весь этот функционал предоставляется браузером.


Такая конференция также выигрывает от наличия каналов данных. Пользователи могут обмениваться дополнительными данными (metadata) или документами. Мы можем создать несколько потоков и сделать некоторые из них более производительными в ущерб надежной доставке сообщений.


Вещание


Все больше новых проектов, в которых используется WebRTC, связаны с вещанием (broadcasting) в той или иной степени. Протокол предоставляет большое количество возможностей как для издателя (publisher), так и для потребителя (consumer) медиа.


WebRTC существенно облегчает процесс публикации видео. Пользователям не нужно устанавливать специальное ПО. Любая платформа с браузером может публиковать видео. Издатели могут отправлять несколько треков и модифицировать или удалять их в любое время. Это является существенным преимуществом перед старыми протоколами, которые позволяли передавать только один аудио или видеотрек через одно соединение.


WebRTC предоставляет разработчикам серьезный контроль над задержкой и качеством. Важно, что задержка никогда не превышает максимальный порог (threshold), и мы можем игнорировать некоторые артефакты декодирования. Мы можем запускать воспроизведение видео сразу после его доставки. С другими протоколами, функционирующими поверх TCP, это сделать не так просто. В браузере мы просто запрашиваем данные и сразу их получаем.


Удаленный доступ


Удаленный доступ (remote access) — это когда мы получаем доступ к другому компьютеру через WebRTC. Мы можем контролировать удаленный хост целиком или только отдельное приложение, работающее на нем. Это отлично подходит для выполнения сложных вычислительных задач, когда собственной мощности оказывается недостаточно. WebRTC – это революция сразу в трех направлениях.


WebRTC может использоваться для доступа к удаленному хосту, скрытому от остального мира. С помощью обхода NAT мы можем получить доступ к компьютеру, доступному только через STUN. Это обеспечивает безопасность и конфиденциальность. Пользователям не нужно передавать видео через посредника или "переходник" (ретранслятор). Отображение NAT также облегчает деплой приложения. Нам не надо беспокоиться о перенаправлении портов или настройке статического IP.


Каналы данных также активно применяются в этом сценарии использования. Они могут быть настроены таким образом, что будут принимать только последние данные. В случае с TCP мы рискуем столкнуться с блокировкой начала строки (Head-of-line blocking). Клик мыши или нажатие клавиши могут задержаться в пути и блокировать получение следующих за ними событий. Каналы данных спроектированы специально для обработки таких случаев и позволяют отключить повторную отправку потерянных пакетов. Мы также можем измерять нагрузку и проверять, что отправляем ровно столько данных, сколько способна выдержать наша сеть.


Наличие WebRTC в браузере сильно упрощает жизнь. Нам не нужно скачивать проприетарного (платного) клиента для начала сессии. Все большее количество клиентов имеют встроенную поддержку WebRTC, например, умные телевизоры (Smart TV).


Передача файлов и обход цензуры


Передача файлов (File Sharing) и обход цензуры (Censorship Circumvention) — это две совершенно разные проблемы. Однако, WebRTC успешно решает обе. Он делает коммуникацию легкодоступной и практически не поддающейся блокировке.


Первая задача, которую решает WebRTC — это получение клиента. Если мы хотим присоединиться к файлообменной сети, нам нужно загрузить клиента. Даже если сеть является распределенной, все равно нужен клиент. В ограниченной сети загрузка клиента может быть заблокированной. Даже если получится скачать клиента, сможет ли пользователь самостоятельно установить и запустить его? WebRTC доступен в каждом браузере: это готовый к использованию клиент.


Вторая проблема — блокировка трафика. Если мы используем протокол, предназначенный только для передачи файлов или обхода цензуры, его легко заблокировать. Поскольку WebRTC — это протокол общего назначения, его блокировка затронет каждого. Блокировка WebRTC может помешать подключению новых участников к конференции.


Интернет вещей


Интернет вещей (Internet of Things, IoT) включает в себя несколько вещей. Для многих это означает камеры видеонаблюдения. С помощью WebRTC мы можем передавать видео другому пиру, такому как телефон или браузер. Другим случаем является подключение устройств и обмен чувствительными данными (sensor data). У нас может быть два устройства, функционирующих в локальной сети, которые обмениваются данными о климате, шуме или освещенности.


WebRTC имеет существенные преимущества перед старыми протоколами для обмена потоками. Поскольку WebRTC поддерживает P2P-соединения, мы можем передавать видео с камеры напрямую в браузер. Нет необходимости использовать сторонний сервер. Даже если видео будет зашифрованным, злоумышленник может извлечь некоторую информацию из метаданных звонка.


Совместимость — еще одно преимущество, предоставляемое WebRTC. WebRTC доступен во многих языках программирования: C#, C++, C, Go, Java, Python, Rust, JavaScript и TypeScript. Это означает, что мы можем использовать язык, который хотим. Нам не требуются проприетарные протоколы или форматы для подключения клиентов.


Обмен медиаданными


Предположим, что у нас есть "железо" и ПО, генерирующее видео, но мы пока не можем их обновить. Для просмотра видео пользователь должен загрузить проприетарного клиента. Это является фрустрирующим. Ответ — запуск моста (bridge) WebRTC. Мост транслируется между двумя протоколами, поэтому пользователи могут использовать браузер со старыми настройками.


Во многих форматах, используемые разработчиками, применяются те же протоколы, что и в WebRTC. Протокол установления сеанса (Session Initiation Protocol, SIP), как правило, транслируется через WebRTC и позволяет пользователям совершать телефонные звонки с помощью браузера. Потоковый протокол реального времени (Real Time Streaming Protocol, RTSP) используется в старых камерах видеонаблюдения. Они оба используют одинаковые протоколы (RTP и SDP). Мост требует добавления или удаления только тех вещей, которые специфичны для WebRTC.


Обмен другими данными


Для передачи данных браузер может использовать ограниченный набор протоколов: HTTP, WebSockets, WebRTC и QUIC. Если мы хотим к чему-то подключиться, то должны использовать протокольный мост (protocol bridge). Протокольный мост — это сервер, который преобразует входящий трафик в нечто, доступное для браузера. Хорошим примером является SSH, который используется для доступа к серверу. Каналы данных WebRTC в этом отношении предоставляют несколько преимуществ.


Каналы данных допускают ненадежную и неупорядоченную доставку. Это необходимо в случаях, когда критически важной является низкая задержка доставки пакетов. Мы не хотим, чтобы новые данные блокировались старыми (блокировка начала строки). Представьте, что играете в многопользовательский "шутер" от первого лица. Вам важно, где пользователь был две секунды назад? Если данные не прибывают вовремя, какой смысл продолжать попытки их отправки. Ненадежная и неупорядоченная доставка позволяет использовать данные сразу по их прибытии.


Каналы данных также предоставляют обратную связь о нагрузке (feedback pressure). Это позволяет своевременно определять, что мы отправляем больше данных, чем поддерживает наша сеть. В этом случае у нас есть два варианта: канал данных может помещать сообщения в буфер и доставлять их с задержкой или же мы можем игнорировать "опоздавшие" сообщения.


Телеоперация


Телеоперация (teleoperation) — это управление удаленным устройством через каналы данных WebRTC и обратная отправка видео через RTP. Сегодня WebRTC позволяет разработчикам удаленно управлять автомобилями! Это используется для управления роботами при создании сайтов и доставки посылок.


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


Распределенные CDN


Распределенные CDN (Content Delivery Network — сеть доставки контента) — это разновидность файлообменной сети. Распределяемые файлы настраиваются оператором CDN. Когда пользователь подключается к CDN, он может скачивать и делиться разрешенными (allowed) файлами.


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


Топологии WebRTC


WebRTC — это протокол, предназначенный для прямого соединения двух агентов, но как разработчику подключить тысячу людей? Существует несколько способов, каждый из которых имеет свои преимущества и недостатки. Эти решения условно можно разделить на две категории: равный-к-равному (Peer-to-Peer) или клиент/сервер (Client/Server). Гибкость WebRTC позволяет легко реализовать любой из этих сценариев.


Один-к-одному


Один-к-одному (One-to-One) — первый тип подключения, используемый в WebRTC. Мы соединяем двух агентов напрямую и они могут обмениваться медиа и другими данными. В этом случае подключение выглядит так:





Полная сетка


Полная сетка (Full Mesh) — это решение для разработки конференции или многопользовательской игры. В данном случае каждый пользователь устанавливает соединение с другими пользователями напрямую. Это позволяет создать приложение, но имеет некоторые недостатки.


В полной сетке каждый пользователь подключается напрямую. Это означает, что нам нужно шифровать и загружать видео независимо для каждого участника сессии. Условия сети между каждым соединением будут разными, поэтому мы не может повторно использовать одно и тоже видео. Обработка ошибок — в данном случае также задача не из простых. Необходимо правильно определять степень потери соединения: является ли оно полным или касается только одного удаленного пира.


По этим и другим причинам полная сетка хорошо подходит для небольших групп. Для большого количества участников сессии лучше подходит топология клиент/сервер.





Гибридная сетка


Гибридная сетка (Hybrid Mesh) — это альтернатива полной сетки, которая может решить некоторые проблемы с ней. В гибридной сетке вместо установки соединения между каждым пользователем медиа ретранслируется через пиры в сети. Это означает, что создателю медиа не требуется большая пропускная способность для передачи данных.


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





Единица выборочной пересылки


Единица выборочной пересылки (Selective Forwarding Unit, SFU) также устраняет недостатки полной сетки, но несколько другим способом. SFU реализует топологию клиент/сервер вместо P2P. Каждый пир подключается к SFU и загружает в него свое медиа. Затем SFU пересылает медиа каждому подключенному клиенту.


В SFU каждый агент должен зашифровать и загрузить видео только один раз. Ответственность за передачу видео всем участникам ложится на SFU. Установка SFU-соединения, к тому же, проще, чем установка P2P-соединения. SFU может быть запущен на открытом маршрутизируемом адресе, что облегчает подключение к нему клиентов. Поэтому в данном случае не требуется отображение NAT. Однако нам все еще необходимо обеспечивать доступность SFU через TCP (через ICE-TCP или TURN).


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





Многоточечная конференц-система


Многоточечная конференц-система (Multi-point Conferencing Unit, MCU) — это топология клиент/сервер, похожая на SFU, но объединяющая выходные потоки. Вместо того, чтобы передавать медиаданные в неизменном виде, они декодируются в один поток.





Отладка


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


Локализация проблемы


При отладке приложения WebRTC сначала необходимо определить источник проблемы.


Потеря сигнала


Потеря сети


Проверяем сервер STUN с помощью netcat:


  1. Формируем 20-байтный пакет запроса на привязку (binding request packet):

echo -ne "\x00\x01\x00\x00\x21\x12\xA4\x42TESTTESTTEST" | hexdump -C
00000000  00 01 00 00 21 12 a4 42  54 45 53 54 54 45 53 54  |....!..BTESTTEST|
00000010  54 45 53 54                                       |TEST|
00000014

Здесь:


  • 00 01 — тип сообщения;
  • 00 00 — размер данных (длина соответствующего раздела);
  • 21 12 a4 42 — магическое куки;
  • 54 45 53 54 54 45 53 54 54 45 53 54 (декодированное в ASCII: TESTTESTTEST) — 12-байтовый идентификатор транзакции.

  1. Отправляем запрос и ждем получения 32-байтного ответа:

stunserver=stun1.l.google.com;stunport=19302;listenport=20000;echo -ne "\x00\x01\x00\x00\x21\x12\xA4\x42TESTTESTTEST" | nc -u -p $listenport $stunserver $stunport -w 1 | hexdump -C
00000000  01 01 00 0c 21 12 a4 42  54 45 53 54 54 45 53 54  |....!..BTESTTEST|
00000010  54 45 53 54 00 20 00 08  00 01 6f 32 7f 36 de 89  |TEST. ....o2.6..|
00000020

Здесь:


  • 01 01 — тип сообщения;
  • 00 0c — размер данных, которые декодируются до 12 в десятичном формате;
  • 21 12 a4 42 — магическое куки;
  • 54 45 53 54 54 45 53 54 54 45 53 54 (декодированное в ASCII: TESTTESTTEST) — 12-байтовый идентификатор транзакции;
  • 00 20 00 08 00 01 6f 32 7f 36 de 89 — 12-байтовые данные:
    • 00 20 — это тип: XOR-MAPPED-ADDRESS;
    • 00 08 — размер значения, декодируемого до 8 в десятичном формате;
    • 00 01 6f 32 7f 36 de 89 — значение данных:
    • 00 01 — тип адреса (IPv4);
    • 6f 32 — XOR-сопоставленный (XOR-mapped) порт;
    • 7f 36 de 89 — XOR-сопоставленный IP-адрес.

Декодирование XOR-сопоставленного раздела является сложным, но мы можем обмануть сервер STUN для выполнения фиктивного отображения XOR с помощью фиктивного магического куки со значением 00 00 00 00:


stunserver=stun1.l.google.com;stunport=19302;listenport=20000;echo -ne "\x00\x01\x00\x00\x00\x00\x00\x00TESTTESTTEST" | nc -u -p $listenport $stunserver $stunport -w 1 | hexdump -C
00000000  01 01 00 0c 00 00 00 00  54 45 53 54 54 45 53 54  |........TESTTEST|
00000010  54 45 53 54 00 01 00 08  00 01 4e 20 5e 24 7a cb  |TEST......N ^$z.|
00000020

XOR-операция против фиктивного магического куки будет идемпотентной, поэтому порт и адрес в ответе будут достоверными. Это будет работать не во всех случаях, поскольку некоторые маршрутизаторы манипулируют проходящими через них пакетами, обманывая (cheating) IP-адрес. Последние 8 байтов ответа будут выглядеть следующим образом:


  • 00 01 4e 20 5e 24 7a cb — значение данных:
    • 00 01 — тип адреса (IPv4);
    • 4e 20 — сопоставленный порт, которые декодируется до 20000 в десятичном формате;
    • 5e 24 7a cbIP-адрес, который декодируется в 94.36.122.203.

Ошибка безопасности


Ошибка медиа


Ошибка данных


Инструменты для отладки


netcat (nc)


netcat — сетевая утилита командной строки для чтения из/записи в сетевые соединения с помощью TCP или UDP. Данная утилита, обычно, доступна через команду nc.


tcpdump


tcpdump — это интерфейс командной строки для анализа сетевых пакетов с данными.


Наиболее распространенные команды:


  • перехватываем пакеты UDP в/из порта 19302, отображаем шестнадцатеричный дамп (hexdump) содержимого пакета

sudo tcpdump 'udp port 19302' -xx

  • тоже самое, но сохраняем пакеты в файле PCAP (захват пакета — packet capture) для дальнейшего изучения

sudo tcpdump 'udp port 19302' -w stun.pcap

Файл PCAP можно открыть с помощью команды wireshark stun.pcap.


Wireshark


Wireshark — это широко используемый анализатор сетевых протоколов.


webrtc-internals


Chrome предоставляет встроенную страницу статистики WebRTC: chrome://webrtc-internals.


Задержка


Как понять, что имеет место высокая задержка доставки сообщений? Мы можем заметить, что наше видео лагает (lagging), но как определить точную величину задержки? Для уменьшения задержки, необходимо сначала ее измерить.


Предполагается, что истинная задержка должна измеряться от начала до конца (end-to-end). Это означает учет не только задержки сетевого пути между отправителем и получателем, но общую задержку захвата камеры, шифрования кадров, передачи, получения, расшифровки и отображения, а также возможную буферизацию (постановку задач в очередь — queueing) на любом из этапов.


Задержка от начала до конца — это не просто сумма задержек каждого компонента.


Несмотря на то, что теоретически мы можем измерить задержку каждого компонента потоковой передачи видео по отдельности и суммировать результаты, на практике некоторые компоненты будут либо недоступными для измерения, либо результаты измерения этих компонентов вне конвейера (pipeline) будут сильно отличаться от результатов их измерения в связке с другими компонентами. Разная глубина очереди на разных этапах, топология сети и изменения экспозиции камеры — вот лишь несколько примеров влияния компонентов на конечную задержку.


Задержка каждого компонента в системе потоковой передачи данных может меняться и влиять на нижележащие компоненты. Даже содержимое захваченного видео влияет на задержку. Например, требуется намного больше битов для высокочастотных (high frequency) изображений вроде ветвей дерева по сравнению с низкочастотными кадрами вроде чистого голубого неба. Камера с автоматической экспозицией может захватывать кадр гораздо дольше ожидаемых 33 мс, даже при установке частоты захвата (capture rate) в значение 30 кадров в секунду (frames per second). Передача по сети, особенно по сотовой, также очень динамична из-за постоянно меняющегося спроса (на ресурсы сети). Чем больше пользователей, тем больше "шума" в эфире. Наша физическая локация (например, в зоне с плохим сигналом) и множество других факторов увеличивают потерю пакетов и задержку. Что происходит при отправке пакета по сети, например, через адаптер WiFi или LTE-модем? Если пакет не может быть доставлен незамедлительно, он помещается в очередь, чем больше очередь, тем выше задержка.


Ручное измерение конечной задержки


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


EndToEndLatency = T(observe) - T(happen)

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


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


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


  • фиксируем время на часах;
  • отправляем видео получателю, находящемуся в той же физической локации;
  • фиксируем время появления видео на экране получателя;
  • вычитаем одно время из другого.

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








На приведенном выше фото конечная задержка составляет 101 мс. Время возникновения события составляет 10:16:02.761, а время его наблюдения — 10:16:02.862.


Автоматическое измерение задержки


В настоящее время (май 2021 года) стандарт WebRTC, посвященный конечной задержке, находится в стадии активного обсуждения. Firefox реализовал набор API поверх стандартных API WebRTC для автоматического измерения задержки. Однако далее мы рассмотрим более общий способ.





Суть времени пути (round-trip time, rtt): я отправляю тебе мое время tR1, при получении от тебя tR1 обратно я фиксирую время tR2, таким образом, время пути составляет tR1 - tR2.


При наличии канала коммуникации между отправителем и получателем (например, DataChannel), получатель может смоделировать монотонные часы (monotonic clock) отправителя следующим образом:


  1. Получатель отправляет сообщение со своим временем tR1.
  2. При получении сообщения от получателя, отправитель фиксирует свое время tS1 и отправляет копию tR1 и tS1, а также время видеотрека отправителя tSV1.
  3. При получении сообщений от отправителя, получатель фиксирует свое время tR2 и вычисляет время пути: RTT = tR2 - tR1.
  4. Времени пути RTT и времени отправителя tS1 достаточно для вычисления монотонных часов отправителя. Текущее время отправителя для tR2 будет равняться tS1 + половина времени пути.
  5. Времени отправителя tS1 вместе с временем видеотрека tSV1 достаточно для синхронизации времени видеотрека получателя со временем видеотрека отправителя.

Поскольку нам известно время tSV1, мы можем приблизительно вычислить задержку посредством вычитания текущего времени отображения видеокадра (actual_video_time) из ожидаемого времени:


expected_video_time = tSV1 + time_since(tSV1)
latency = expected_video_time - actual_video_time

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


Пример вычисления задержки


В следующем примере мы открываем канал данных latency на стороне получателя и периодически отправляем время монотонного таймера отправителю. Отправитель отвечает сообщением в формате JSON, и получатель вычисляет задержку на основании этого сообщения.


{
    "received_time": 64714,       // Время, отправленное получателем, которое фиксируется отправителем
    "delay_since_received": 46,   // Время, прошедшее с последнего `received_time`, полученного отправителем
    "local_clock": 1597366470336, // Текущее монотонное время отправителя
    "track_times_msec": {
        "myvideo_track1": [
            13100,        // Временная метка видеокадра RTP (в мс)
            1597366470289 // Временная метка монотонных часов видеокадра
        ]
    }
}

Открываем канал данных на стороне получателя:


dataChannel = peerConnection.createDataChannel('latency')

Периодически отправляем время получателя tR1. В данном случае интервал составляет 2 секунды:


setInterval(() => {
  const tR1 = Math.trunc(performance.now())
  dataChannel.send(String(tR1))
}, 2000)

Обрабатываем сообщение получателя на стороне отправителя:


// предположим, что `event.data` - это строка типа '1234567'
const tR1 = event.data
const now = Math.trunc(performance.now())
const tSV1 = 42000 // Временная метка текущего фрейма RTP, преобразованная в мс
const tS1 = 1597366470289 // Временная метка монотонных часов текущего фрейма
const msg = {
  "received_time": tR1,
  "delay_since_received": 0,
  "local_clock": now,
  "track_times_msec": {
    "myvideo_track1": [tSV1, tS1]
  }
}
dataChannel.send(JSON.stringify(msg))

Обрабатываем сообщение отправителя на стороне получателя, вычисляем задержку и выводим ее в консоль:


const tR2 = Math.trunc(performance.now())
const fromSender = JSON.parse(event.data)
const tR1 = fromSender['']
const delay = fromSender[''] // Сколько прошло времени между получением сообщения отправителем и отправкой ответа
const senderTimeFromResponse = fromSender['']
const rtt = tR2 - delay - tR1
const networkLatency = rtt / 2
const senderTime = (senderTimeFromResponse + delay + networkLatency)
const video$ = document.querySelector('video')
video$.requestVideoFrameCallback((now, framemeta) => {
  // Вычисляем текущее время отправителя
  const delaySinceVideoCallbackRequested = now - tR2
  senderTime += delaySinceVideoCallbackRequested
  const [tSV1, tS1] = Object.entries(fromSender[''])[0][1]
  const timeSinceLastKnownFrame = senderTime - tS1
  const expectedVideoTimeMsec = tSV1 + timeSinceLastKnownFrame
  const actualVideoTimeMsec = Math.trunc(framemeta.rtpTimestamp / 90) // Преобразуем время RTP (90000) в мс
  const latency = expectedVideoTimeMsec - actualVideoTimeMsec
  console.log('latency', latency, 'ms')
})

Реальное время видео в браузере


HTMLVideoElement.requestVideoFrameCallback() позволяет разработчикам получать уведомления о представлении фрейма для композиции.


До недавнего времени (май 2020 года) было практически невозможно получить достоверное время отображения видеокадра в браузере. Данная проблема решалась через HTMLVideoElement.currentTime, но не давала необходимой точности. В настоящее время стандарт W3C HTMLVideoElement.requestVideoFrameCallback(), позволяющий получать текущее время видеокадра, поддерживается Chrome и Firefox. Несмотря на то, что это дополнение кажется тривиальным, оно сделало возможным использование некоторых продвинутых возможностей в приложениях, требующих синхронизации аудио и видео. Функция обратного вызова включает поле rtpTimestamp — временную метку RTP, связанную с текущим видеокадром.


Отладка задержки


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


Задержка камеры


В зависимости от настроек камеры производимая ей задержка может быть разной. Проверяем настройки автоэкспозиции, автофокуса и автоматического баланса белого. Всем автоматическим настройкам требуется дополнительное время для анализа захваченного изображения перед его передачей в стек WebRTC.


Если у вас Linux, для управления настройками камеры можно воспользоваться командой v4l2-ctl:


# Отключаем автофокус
v4l2-ctl -d /dev/video0 -c focus_auto=0
# Устанавливаем фокус в значение бесконечности
v4l2-ctl -d /dev/video0 -c focus_absolute=0

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


Задержка шифрования


Большинство современных кодировщиков буферизуют некоторые кадры перед их шифрованием. Их главный приоритет — баланс между качеством производимого изображения и битрейтом. Многопроходное кодирование (multipass encoding) — экстремальный пример пренебрежения кодировщиком конечной задержкой. На первом проходе кодировщик "проглатывает" видео целиком перед началом производства кадров.


Однако при правильной настройке можно добиться уменьшения подкадровой (sub-frame) задержки. Убедитесь, что ваш кодировщик не использует чрезмерное количество эталонных кадров (reference frames) и не полагается на B-кадры (B-frames). Настройки для разных кодеков будет разными, но для x264 рекомендуется использовать tune=zerolatency и profile=baseline для снижения задержки.


Сетевая задержка


Сетевая задержка — это то, с чем почти ничего нельзя сделать, кроме обновления сети. Сетевая задержка — это как погода: мы не можем остановить дождь, но можем посмотреть прогноз и взять с собой зонт. WebRTC измеряет сетевые условия с точностью до мс. И при этом самые важные метрики:


  • время пути;
  • потеря и ретрансляция пакетов.

Время пути


Стек WebRTC имеет встроенный механизм для измерения сетевого времени пути (RTT). Достаточно хорошее приблизительное вычисление задержки — это половина RTT. Механизм исходит из предположения, что отправка и получение пакетов занимает одинаковое время, что не всегда соответствует действительности. RTT устанавливает нижнюю границу конечной задержки. Наши видеокадры не могут достичь получателя быстрее, чем за RTT / 2, независимо от того, насколько оптимизирована наша камера и конвейер кодировщика.


Встроенный механизм RTT основан на специальных пакетах RTCP, которые называются отчетами отправителя/получателя (sender/receiver reports). Отправитель передает свое время получателю, получатель отвечает тем же временем. Это позволяет отправителю определить количество времени, которое занимает путь пакета туда и обратно.


Потеря и ретрансляция пакетов


Протоколы RTP и RTCP основаны на UDP, что означает отсутствие гарантии сохранения порядка, надежной доставки пакетов или отсутствия дубликатов. Все это может происходить и действительно происходит в реальных WebRTC-приложениях. Простые реализации декодеров ожидают получения всех кадров изображения для его успешного восстановления. В случае потери пакетов P-кадра (P-frame) могут появиться различные артефакты декодирования. Если потеряны пакеты I-кадра (I-frame), все зависимые кадры получат тяжелые артефакты или совсем не будут декодированы. Это может привести к "зависанию" видео на какое-то время.


Для преодоления зависания видео или появления артефактов декодирования WebRTC использует сообщения с негативными благодарностями (Negative Acknowledgement, NACK). Когда получатель не получает ожидаемый пакет RTP, он возвращает отправителю сообщение NACK для повторной отправки недостающего пакета. Получатель ждет повторной отправки пакета. Такое ожидание увеличивает задержку. Количество отправленных и полученных пакетов NACK записывается в статические поля nackCount исходящего потока и nackCount входящего потока.


Графы входящих и исходящих nackCount можно увидеть на странице webrtc-internals. Если мы видим, что nackCount увеличивается, значит, имеет место высокая потеря пакетов, и стек WebRTC делает все возможное для плавного воспроизведения видео или аудио.


При высокой потере пакетов, когда декодер не может произвести изображение или последующие зависимые изображения, например, при полной потере I-кадра, последующие P-кадры не декодируются. Получатель пытается смягчить это путем отправки специального сообщения-индикатора потери изображения (Picture Loss Indicator, PLI). Когда отправитель получает PLI, он повторно отправляет новый I-кадр. I-кадры, как правило, больше P-кадров по размеру. Это увеличивает количество пакетов для передачи. Как и в случае с NACK, получатель ждет получения нового I-кадра, что приводит к увеличению задержки.


Смотрите на значение поля pliCount на странице webrtc-internals. Если значение данного поля увеличивается, установите меньшее количество производимых декодером кадров или включите более лояльный к ошибкам режим.


Задержка на стороне получателя


Задержка также увеличивается при прибытии пакетов в неправильном порядке. Если нижняя часть изображения прибывает раньше верхней, декодирование изображения будет заниматься чуть больше времени, чем когда части изображения прибывают в правильном порядке. Мы говорили об этом в разделе, посвященном джиттеру.


Встроенная метрика jitterBufferDelay показывает, как долго кадр находился в буфере получателя, ожидая прибытия недостающих пакетов, перед его передачей декодеру для расшифровки.


На этом третья часть перевода завершена.


Благодарю за внимание и happy coding!




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