Как надёжно доставить видео на Android?

Перед нами стояла задача обеспечить надежную доставку и прием “live” видеопотока на Андроид для всего парка устройств с прошивкой 2.х.

Для этого совместными усилиями была предложена идея не создавать еще один медиа-плеер, который бы понимал нужный нам протокол, а написать приложение, выполняющее функции прокси-сервера на устройства. Прокси бы запускался в фоне, получал бы данные с удалённого сервера TCP-транспортом, и мог отдавать видеопоток по UDP в RTSP. Соответственно, встроенный плеер должен подсоединиться к локальному порту, на котором сидит сервер, и просто начать воспроизведение. На схеме изображён принцип работы системы.

Рисунок 1 – Схема

Реализация RTSP-общение клиента и сервера

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

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

Запросы имеют формат заголовка:

названиеМетода абсолютныйАдресКонтента версияПротокола

и тела:

названиеПараметра<:><пробел>значение

Например, следующим запросом клиент просит сервер начать передачу ему медиа-потока:

PLAY rtsp://foo.com/bar.file RTSP/1.0

СSeq:3

Session: 12345678

Обычно процесс общения выглядит так:

Клиент (К): посылает запрос OPTIONS серверу (С) с указанием названия видео.

C: Если запрос устраивает сервер, то в ответе он указывает, какие методы тот поддерживает.

К: отправляет запрос DESCRIBE

С: формирует ответ, содержащий SDP-описание потока.

К: если клиента устроил ответ на предыдущий запрос, то он присылает метод SETUP, в ответ на который хочет получить описание транспорта, которым будет доставлен медиа-поток.

С: Отвечает нa SETUP.

К: Присылает метод PLAY.

С: Отвечает и начинает передачу данных.

Каждая пара сообщений (запрос-ответ) должна иметь свой номер в рамках сессии, который передаётся в виде параметра СSeq.

Реализация общения по RTSP не вызывала проблем, пока на пути не встала задача сформировать SDP-описание видео, предназначенного для передачи.

SDP (Session Description Protocol) — это формат для описания конфигурационных параметров потокового видео.  Может использоваться как самостоятельно — в этом случае он представляет собой файл с разрешением sdp, так и в составе других протоколов, как в нашем случае.

Прочитав сначала бегло спецификацию RTSP, затем спецификацию SDP, захотелось сразу вставить пример со страницы RFC, быстренько его прогнать и продолжить разработку дальше. Но не тут-то было. Первые же грабли лежали в поле реализации ответа на метод DESCRIBE, который должен был содержать в своём теле sdp-сообщение, описывающее видео.  То, что было приведено в качестве примера в официальной документации, мягко сказать, было слишком примерным описанием. Вот оно:

v=0
o=jdoe 2890844526 2890842807 IN IP4 127.0.0.1
s=rtsp
i=rtsp proxy
e=mymail@gmail.ru
c=IN IP4 224.2.17.12/127
t=2873397496 2873404696
a=recvonly
m=audio 49170 RTP/AVP 0
m=video 51372 RTP/AVP 99
a=rtpmap:99 h263-1998/90000

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

Так как мы будем передавать клиенту, по сути, два разных потока (аудио и видео), мы обязательно должны об этом сказать клиенту таким образом:

a=control:*

// различные параметры описания аудио

a=control:trackID=1 — этим мы говорим, что только что закончилось описание первого потока.

// различные параметры описания видео

a=control:trackID=2 — последний параметр в моём sdp говорит о том, что закончилось описание видео.

Описание аудио:

m = audio 0 RTP/AVP 96 – указывает на тип передачи и тип данных, которые будут передаваться в первом потоке. RTP-медиа данные будут “завёрнуты” в RTP  пакеты,  96 – значение динамического типа аудио потока. Информацию об этом и других типах можно найти здесь :

a = rtpmap: 96 mpeg4-generic/44100/2
96 – тип потока (Правильнее сказать, тип полезной RTP-нагрузки. Что это такое, будет рассказано ниже)
mpeg4-generic – тип кодирования данных
44100 – частота
2 – количество каналов.

Это наиболее общее описание аудио-потока. Так же имеются ещё некоторые важные моменты.

Подробное описание этих параметров можно найти в следующей статье.

Описание видео:

m = video 0 RTP/AVP 97 – отличие от аудио в том, что это видео. 97 – динамический тип видео.

a = rtpmap:97 H264/90000 – этот параметр говорит клиенту о том, что видео будет закодировано с помощью h264 кодека и будет иметь битрейт равный 90000

Следующие параметры, которые необходимо указать, более просты и интуитивно понятны:

a=cliprect:0,0,height,width;

a = framesize:97 width-height;

Первый параметр – прямоугольная область видео.

Второй – размер фрейма. Здесь всё понятно.

И, наконец, мы должны указать, что у нас закончилось описание видео с помощью параметра

a = control:trackID=2

Есть ещё пара параметров, которые в угоду краткости были опущены.

Такое sdp-описание было принято сервером, и появилась возможность посылать другие команды. Следующей командой была SETUP.  Если в sdp-описании было сказано, что будут присутствовать и видео, и аудио данные, то команда SETUP присылается дважды. Для аудио и видео (В той последовательности, в которой они описаны в sdp. В моём случае сначала аудио, потом видео). К обычным параметрам этого метода  в ответе необходимо добавить параметр Transport.

Transport:RTP/AVP/UDP;unicast;client_port=<порты клиента>;server_port=<порты сервера>;ssrc=<источник синхронизации>));

RTP/AVP/UDP – RTP: говорим, что будет использоваться для передачи медиа данных этот протокол реального времени ; AVP – audio video profile – наши мультимедиа данные – это аудио и видео; UDP – транспортный протокол передачи данных.

<порты клиента> – обычно пара портов, на которые должны передаваться данные клиенту. Я передаю данные на первый из указанных портов.

<порты сервера> – тоже пара портов, с которых сервер будет передавать данные. Второй порт можно открывать для прослушивания отчётов о доставке в виде RTCP протокола.

<источник синхронизации> – 32-битный числовой SSRC-идентификатор, который содержится в заголовке RTP пакета и определяет уникальность потока, то есть отделяет видео поток от аудио потока в нашем случае. В случае многоканальной трансляции его значение играет более серьёзную роль.

После того как удалось отправить настроечные данные клиенту, если они его устраивают, то но присылает команду PLAY.  В ответ на данную команду, помимо обязательного параметра CSeq, необходимо добавить параметры Range и RTP-Info.

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

Range: 0-188;

Второй служит для описания параметров RTP-передачи.

После отправки ответа на команду PLAY можно начинать передачу видео данных на те порты  и тем способом, о которых удалось договориться в ходе RTSP-общения.

Получение FLV-видеопотока с сервера

После того как удалось наладить RTSP-обмен, встал вопрос о передаче самого медиа-контента с удалённого севера. Поскольку сервер уже был настроен на отдачу данных в формате FLV, решили использовать этот контейнер.

Подробное описание структуры FLV находится в документе под названием Adobe Flash Video File Format Specification, начиная со страницы 68. Скачать можно здесь. На словах, он выглядит так:

Сначала идёт стандартный заголовок, который недвусмысленно говорит о том, что перед нами FLV файл (3 байта). За ним несколько байт несут информацию о том, какие медиа потоки содержатся в файле, то есть, есть ли в нём видео и аудио. Затем идут основные блоки данных, которые в контексте FLV описания называются тегами. Всего есть три вида тегов: SCRIPT DATA, AUDIO и VIDEO. Все они располагаются последовательно друг за другом. В общем виде, структура файла такая:

<Первые 11 байт – flv-заголовок> < 4 байта – размер предыдущего тега – всегда 0> <flv тег> <4 байта – размер предыдущего тега> <flv тег> <4 байта – размер предыдущего тега>  и так до конца файла.

Каждый тег так же имеет свой заголовок (11 байт). Внутри него содержится размер полезной нагрузки, которую он несёт и много другой необходимой и полезной информации, такой как тип тега и временная метка данных. Каждый тег имеет свой идентификатор (тип), который располагается в определённом месте в заголовке тега. Для SCRIPT DATA это 18 (не забываем, что в файле будет это число в hex, то есть 12), AUDIO — 8, VIDEO — 9.

Полезная нагрузка, однако, не является “сырым” видео, а так же, в зависимости от типа тега, содержит свой определённый заголовок и тело. Рассмотрим пример с видео.

Подробное описание находится на 72 странице Adobe Flash Video File Format Specification, словами же опишу основные моменты.

Первые 4 бита определяют тип видео-фрейма, который содержится в теле тега. В данном случае нас могут интересовать только два типа фреймов – это key frame = 1  и inter frame = 2.

Следующие 4 бита содержат значение, которое определяет тип кодека, который используется для этого видео. Значение равное 7 соответствует AVC (Advanced Video Coding — одно из названий способа кодирования при помощи h264 кодека).

Следующий байт имеется только если предыдущие 4 бита содержат значение равное 7, то есть у нас имеется видео, закодированное AVC.

Байт может иметь одно из трех значений:

0  — в теле тега содержится AVC sequence header – специальный заголовок для подобного типа видео, который содержит информацию о SPS и PPS, которая в свою очередь необходима для правильного декодирования видео. Он так же имеет название AVCDecoderConfigurationRecord, под которым его можно найти в спецификации, ссылка на которую расположена ниже.

1 — содержится AVC NALU

2 — AVC end of sequence, конец последовательности.

Для получения подробной информации советую почитать спецификацию, начиная с первой страницы.

Следующие 3 байта, условие наличия которых такое же, как и у предыдущего байта, содержат в себе composition time. Это время смещения показа текущего фрейма. То есть декодируется он плеером в порядке очерёдности, но выводится на экран именно в это время. Оно может быть равно нулю, как в нашем случае, если выбрать соответствующий  профайл для кодирования.

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

Отправка RTP-пакетов по UDP клиенту

Формирование RTP-заголовка

Так как к данному моменту имелось уже всё необходимое для отправки, то пришло время изучить протокол RTP. Как уже говорилось, данный протокол предназначен для передачи медиа-траффика в реальном времени. Данные представляются в виде пакетов с фиксированным заголовком. Представляет данный пакет из себя следующее (Подробное и всеобъемлющее описание находится здесь,  и по-русски здесь ):

Первые 12 байт — железно —  RTP-заголовок. Заголовок может быть и больше, но для передачи данных одному клиенту, 12 байтовый заголовок — необходимый и достаточный минимум. Поясню некоторые наиболее важные моменты:

Рассмотрим первые 4 байта заголовка.

PT (payload type) — тип полезной нагрузки, которую несёт в себе пакет. Связь с тем, что было в SDP — прямая. 97 — видео, 96 — аудио.

sequence number — номер пакета в общей последовательности отправки. Так как имеется возможность того, что пакеты будут приходить клиенту не в той последовательности, что отправляет сервер, то каждый пакет и снабжён своим особым номером в последовательности. Причём, для видео и аудио данных это две разные последовательности, которые увеличиваются независимо друг от друга. Отсчёт каждая последовательность начинает с единицы.

Следующие 4 байта — это временная метка тех данных, что будут в пакете полезной нагрузкой. Эти данные мы будем брать из временного хранилища, в которое мы их поместили, после извлечения из FLV-файла. Каждая единица этих данных — это видео фрейм или аудио сэмпл (sample). Соответственно, каждая эта единица имеет своё положение в общей последовательности видео потока — это положение и задаёт временная метка.

Следующее, что необходимо указать в заголовке RTP-пакета, это 32-битное значение ssrc. Данный идентификатор позволяет клиенту из общего потока RTP-пакетов выделить отдельно аудио и видео потоки. То есть для аудио и видео данный идентификатор должен быть разным.

Сформировав таким образом битовую последовательность из 12 байт и добавив к ней полезную нагрузку — наши аудио или видео данные, можно смело начинать отправку пакетов клиенту.

Формирование полезной нагрузки для RTP-пакета

В роли полезной нагрузки в нашем случае выступает аудио и видео. Но просто взять содержимое каждого FLV-тега и представить его в виде полезной нагрузки rtp-пакета нельзя. Существует специальная спецификация, которая указывает формат полезной нагрузки RTP-пакета, причём конкретно для видео, которое закодировано с помощью h264-кодека.

Полезная нагрузка может быть представлена в виде трёх форматов:

Single NAL Unit Packet — полезная нагрузка представлена единственным NALU. NALU (в общих чертах) – это видео фрейм, кадр, представленный h264 кодеком.

Aggregation Packets — полезная нагрузка, внутри которой расположено несколько NALU.  Существует несколько разновидностей подобного типа.

Fragmentation Units — когда один  NALU  не влезает в RTP-пакет. RTP-пакет имеет ограничение по размеру, которое выясняется в ходе RTSP-общения. Клиент присылает параметр Blocksize — максимальный размер пакета, который он готов принять.

Схема работы сервера

Перейдем теперь от описания механизмов взаимодействия компонентов к описанию их общей схемы работы.

Приложение-прокси на Android-устройстве начинает свою работу в тот момент, когда пользователь запрашивает по URL некий поток для просмотра. В это время в отдельном потоке процесса прокси происходит обращение к удалённому серверу за данными, которые запрашивает клиент. RTSP-общение между плеером и прокси происходит намного быстрее, чем общение между прокси и удаленным сервером. Поскольку описание видео клиенту уже нужно на втором RTSP-сообщении, то ему приходится ждать, пока прокси получит данные и сможет сформировать правильное SDP-описание. (Серверу для этого необходимо получить и разобрать script data). Впрочем, при скорости интернет-соединения, пригодного для просмотра видео, это ожидание не играет существенной роли.  Как только завершается RTSP-обмен сообщениями, сервер уже имеет некоторый буфер видеоданных.

В процессе просмотра ролика на стороне прокси формируются RTP-заголовки, к ним добавляются видео- или аудио-данные и пакет отправляется клиенту в третьем по счету потоке данных. Всё, что необходимо — правильно обрабатывать и реагировать на RTSP-команды.

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

Определение формата данных, пригодного для отправки клиенту

Видео, которое прокси-сервер получал с удалённого сервера, кодировалось с помощью h264 кодека и упаковывалось в формат FLV.  Формат FLV-файл мы уже рассмотрели, теперь поглядим на его содержание. В случае с видеоданными, в каждом видеотеге содержится небольшой кусок закодированного h264-кодеком видео, в случае с аудио — небольшой кусок аудио данных, закодированных с помощью ААС. Эти кусочки мы должны упаковывать в RTP-пакет и передавать клиенту. Однако при таком подходе “в лоб” клиент отказывался воспроизводить видео. Чтобы выяснить, почему, пришлось разобраться в том, что получается с видео, после того как оно проходит через кодек.

Разбор видео, закодированного h264 кодеком

Итак, видео-поток, закодированный h264-кодеком. Существует достаточное количество спецификаций, которые описывают эту прелесть и которые продаются на сайте стандартизации ISO. Зачем — не ясно, ибо в открытом доступе они находятся на соседней ссылке.

  • ITU-T Rec. H.264 (05-2003) Advanced video coding for generic audiovisual services [IT] — данная спецификация описывает то, что получается, после того как h264 видео кодек обработает видео. То есть, формат и состав видео в виде байтов. Исчерпывающая информация на английском языке, от которой поначалу бросало в дрожь (страница 61 и далее – то что нужно было мне)

  • MPEG-4 Part 10 AVC (H.264) Video Encoding [MP-AVC] — кратко описывает h264 формат и поясняет, какие профили кодирования имеются.

  • ISO/IES AVC file format [ISO-AVC] — самое понятное описание содержимого видео файла, прошедшего через h264-кодек.

Декодирование видео (как и кодирование) — процесс ресурсоемкий, поэтому мобильные устройства с медленными процессорами в реальном времени могут декодировать только то видео, что закодировано не самым сложным образом.

Уровень сложности в данном случае определяется профилем кодирования (Profile). Для h264 кодека существует множество профилей кодирования. С их многообразием можно ознакомиться здесь или более подробнее в [MP-AVC] (см.выше). Мобильные устройства поддерживают далеко не все профили кодирования. Наш Android, например, может декодировать только видео Baseline профиля, но в разных вариациях.

Для кодирования видео существуют различные утилиты и приложения. Одним из популярных GNU-LGPL приложений является FFMPEG. Чтобы FFMPEG смог закодировать нужное нам видео нужным нам профилем, необходимо задать множество входных параметров и ключей. Для удобства их выносят в отдельный файл, называемый “пресет”. В сети Интернет можно найти уже подобранные пресеты для всех профилей кодирования. Однако, как показала практика, стандартный пресет для Baseline профиля воспринимается Android’ом не всегда корректно. К сожалению, тонкая оптимизация FFMPEG и его пресетов — объемный вопрос, выходящий за рамки этой статьи.

Отправка клиенту данных и поиск проблем воспроизведения

Теперь у нас есть установленное соединение с клиентом, разобранное видео, получаемое с удалённого сервера, правила формирования пакетов для отправки. Самое время начать её.

Для отправки пакетов создаётся новый поток, который используя UDP-транспорт, отправляет RTP-пакеты. Видео и аудио пакеты формируются в одном потоке в той очерёдности, в которой они были в FLV-файле.

Интересным моментом является временной интервал, с которым данные должны передаваться клиенту. Если отправлять клиенту пакеты непрерывным потоком, то он, во избежание переполнения буфера, отправляет команду PAUSE  по RTSP. Можно её обрабатывать и приостанавливать поток. Но можно сделать проще. Достаточно установить интервал отправки пакетов таким, который бы соответствовал или был чуть быстрее скорости воспроизведения видео. Так как частота смена кадров, в среднем, 25-30, то чтобы не было задержек, я выбрал интервал, равный 25 миллисекундам. Для звука — 15. Если данные из внешней сети приходят вовремя, то проблем с воспроизведением не наблюдается.

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

У нас есть похожие новости по этим темам:
Наверх

15 комментариев