Разработка
До того, как это стало стримом — рассказываем о настройке потокового вещания
Как эти усилия выглядят на самом деле и о способах настройки потокового вещания на примере работы с протоколом SRT рассказывает iOS-разработчик CleverPumpkin Никита Тархов.
Пользователь запускает приложение, нажимает кнопку — и вжух, уже стримит в сеть, а к его стриму подключаются другие пользователи. В плеере в этот момент происходит огромная работа: скрипты забирают изображение и звук, кодируют, пакуют в контейнеры, и передают данные в местный разгрузочный порт, где контейнер снова разбивают на упакованное в него видео и аудио. Сколько усилий! Как эти усилия выглядят на самом деле и о способах настройки потокового вещания на примере работы с протоколом SRT рассказывает iOS-разработчик CleverPumpkin Никита Тархов.
Как работает информационный канал
Каждое видео в интернете проходит один и тот же путь от источника к принимающей стороне. Предположим: есть камера, которая условно 30 раз в секунду выдает нам raw-изображения, и такой же источник аудио, который выдает звуковые raw-данные. Для более эффективной их передачи используются кодеки, которые сжимают изображения, уменьшают вес и трафик в разы. Чтобы совместить переданные данные, используется мультиплексор, который наши аудио- и видеоданные и информацию о стримах собирает и пакует в некий контейнер, который на сетевом транспорте доставляется в интернет.
Все это происходит в момент после нажатия пользователем кнопки «начать стрим».
![]()
Стоит обратить ваше внимание на параметры частоты кадров и битрейта для видеокодека, которые непосредственно влияют на качество и работу в сети: чем выше битрейт, тем, соответственно, выше требование к сети. В аудио-кодеке используется такой параметр как частота дискретизации или Sample Rate и битрейт. Чем выше частота дискретизации, тем лучше качество звука.
Соответственно, контейнер, который грузится на сетевой транспорт, должен содержать информацию для декодинга, чтобы принимающая сторона поняла, как это расшифровать.
Принимающая сторона — зеркальное отображение отдающей: из интернета по сетевому транспорту приходит контейнер, демультиплексор этот контейнер разбивает на потоки видео и аудио. Мы получаем пакет со сжатым изображением, которое передается в видеокодек и декодируется в серию изображений, а сжатое аудио передается в аудиокодек — и превращается в звук.
Это то, что происходит после того, как зритель подключается к стриму.
![]()
Протокол SRT
Поговорим о происходящем с видео и аудио в стримах на примере SRT — транспортного протокола для медиа контейнеров, который разрабатывался специально для работы в плохих условиях сети. Этот протокол мы использовали при работе над одним из самых свежих проектов CleverPumpkin — Dater.
Разберемся с протоколом побуквенно.
- S — secure, то есть, этот протокол позволит зашифровать нашу информацию.
- R — reliable, что указывает на возможность восстановления после серьезной потери пакетов. Протокол предусматривает работу при нестабильных условиях в сети и может либо перезапросить, либо восстановить данные после потери пакетов. Протокол добавляет стабильности.
- С буквой Т все понятно — это transport, который благодаря S и R подстраивается к условиям сети.
Протокол является транспортным и базируется на UDP-сокетах. Программный интерфейс — тоже сокеты, но уже SRT-сокеты. Сам протокол реализован на C++, но разработчики продумали и вынесли все в С интерфейс, тем самым дав возможность использовать SRT на любом тапочке с выходом в интернет. Также за счет С интерфейса библиотеку можно использовать в Swift, но это несет понятные сложности.
Протокол SRT содержит в себе полезную нагрузку, шифрование и контроль, автоматически сверяя пакеты, чтобы убедиться в их целостности для передачи данных.
В полезной нагрузке могут быть абсолютно любые данные, но чаще всего это MPEG-TS – протокол для передачи аудио и видео в формате MPEG2.
![]()
MPEG-TS — транспортный поток, который поддерживает передачу данных о вещании и может передавать не один стрим, а несколько, чтобы уже принимающая сторона могла выбирать конкретный канал.
Данные о вещании, аудио и видеопотоки передаются в мультиплексоры, которые образуют PES, PSI и NULL пакеты. Последний нужен для контроля битрейта. Например, чтобы поддержать требуемые цифровым телевидением стандарты, MPEG-TS умеет вставлять в поток пустые пакеты. PID (Program ID) такого пакета строго определен и равен 0x1FFF.
PES — это Packet Elementary Stream. Это набор Elementary Stream, каждый из которых может содержать информацию о видео или аудио. Elementary Stream инкапсулируется в транспортный поток (TS), размер которого строго ограничен 188 байтами. Каждый элементарный поток (ES) имеет свой уникальный PID, что позволяет классифицировать разные наборы ES и склеить в соответствующий PES пакет.
PSI — Program Specific Information — содержит в себе таблицу программ (PAT) и таблицу структуры программ (РМТ). Таблица программ показывает, какие стримы передаются, а сама Program Map Table (РМТ) содержит PID и основные характеристики элементарных потоков конкретной программы.
![]()
Итак, чтобы найти определенный канал, первым делом нужно обратить внимание на PAT, где прописано, что информация об интересующих нас каналах лежит в РМТ под соответствующими PID. Например, в РМТ задан PID 51. Соответственно, в Program Table по PID 51 будет находиться информация об интересующих нас стримах.
Способы реализации потокового вещания
Вариант 1. Трудоемкий. Можно написать все самому. Это непросто и требует правильной реализации стандарта MPEG-TS, который сам по себе не очень прост.
Вариант 2. Дорогой и опасный. Можно использовать платное решение, но и тут есть свои минусы. Нет контроля над исходным кодом, ничего менять не получится — можно только спросить и, возможно, не получить ответ.
Вариант 3. Бесплатный, открытый, но немного замороченный вариант — использовать VLC, как мы это сделали на проекте Dater. VLC можно очень быстро интегровать, по сути это уже готовый плеер, который можно использовать Plug & Play. Поэтому он и был нами выбран изначально – из-за высокой скорости внедрения в проект.
Вариант 4. Самый простой и тоже бесплатный — использовать FFMpeg. Эта утилита, скорее всего, предустановлена на каждом Макбуке. Она нужна для преобразования любых картинок, видео во что угодно — в gif, в набор картинок, можно менять видео-кодеки и т.д. Но об этом позже. Разберем по порядку варианты 3 и 4.
Работа с VLC
При разработке на проекте Dater мы использовали его с кастомной реализацией.
Плюсы VLC:
- Он воспроизводит все, что угодно.
- У него очень большое сообщество.
- Бесплатный open source.
- Он разбит на модули, что позволяет легко ориентироваться по проекту.
Минус:
- Он единственный и большой — это готовый плеер. Чтобы что-то поменять, придется лезть в исходники и там что-то менять, потом перекомпилировать сам плеер и т.д. Все это требует немалого количества времени — перекомпиляция на Макбуке занимает примерно 40 минут. Сама необходимость лезть в «подкапотную» часть плеера проблемна еще и тем, что сам код написан достаточно сложно.
Успешно работать с VLC все это не мешает, хотя и доставляет некоторые сложности. Для создания стримов в приложении Dater мы очень много работали именно с VLC: оптимизировали и подбирали оптимальные параметры вещания, чтобы достичь требуемых показателей по задержкам и качеству воспроизведения.
![]()
Какие плюсы можем выделить при работе в конкретном проекте?
- Достаточно легкая интеграция в проект. Это, по сути, фреймворк, который предоставляет плеер, уже содержащий свой view (вьюху, проще говоря). Разработчику нужно всего лишь настроить плеер и настроить некоторые параметры, после чего добавить view в интерфейс.
- Все работает из коробки.
- Стабильность.
Минусами стали:
- Отсутствие изначальной поддержки SRT на iOS, ее пришлось прикручивать самостоятельно.
- Пришлось модифицировать исходники, чтобы добавить шумоподавление и логику повторного подключения.
![]()
Структура VLC такова, что тут есть очень много модулей доступа к потокам, в том числе поддерживается наш SRT. Ничего особенно сложного мы не делали — по аналогии с другими параметрами добавили reconnect interval и maximum reconnected attempts. Эти параметры прокидываются в библиотеку VLC.
В аудиовыходе поменяли Audio Unity, которые здесь используются для вывода звука, также добавили возможность использования VoiceProcessingIO в AudioUnit плеера.
Работа с FFMpeg
FFMpeg — это, пожалуй, самая удобная утилита для решения наших задач.
Плюсы:
- FFMpeg воспроизводит все, что угодно.
- У него такое же большое сообщество.
- Также есть бесплатный open source.
- Есть та же самая модульность.
- Позволяет сконфигурировать любое программное обеспечение по обработке видео. Хотите программу по обработке фотографий – FFMpeg все сделает. Хотите видеоплеер – тоже FFMpeg. Подтверждение этому в том, что очень многие крупные компании для своих видео используют именно эту утилиту.
![]()
Компиляция FFMpeg
Комьюнити утилиты создало FFMpegKit, благодаря чему компиляция происходит очень просто. На картинке видим использование утилит git clone, cd ffmpeg-kit и запуск скрипта компиляции для iOS. В конкретной ситуации мы просто включили поддержку SRT и поддержку VideoToolBox для использования аппаратного декодинга. См. картинку:
![]()
На выходе мы получили сразу восемь фреймворков, каждый из которых можно подключать по-отдельности: ffmpegkit.framework , Libavcodec.framework, Libavdevice.framework, Libavfilter.framework, Libswresample.framework, Libswscale.framework, Libavformat.framework, Libavutil.framework.
![]()
Пишем свой плеер
Мы для реализации плеера выбрали Objective-C, потому что API самого FFMpeg полностью написана на C, что затрудняет работу с ним в Swift. То есть реализация возможна, но получится очень громоздко, в отличие от Objective-C, где можно полноценно работать с С и легче писать код.
Создаем тред и занимаемся банальным подключением FFMpeg: берем URL, C-строку, даем FFMpeg указание открыть тот или иной инпут из libavformat. К нам должен вернуться нулевой код. Затем получаем информацию о стриме, запоминаем его контекст — и после этой строчки у нас появляется информация о кодеках, аудио-кодеках, видеокодеке и о размере картинки.
Теперь мы ищем кодек, а для этого сначала находим нужный видеопоток, проходя по всем стримам, которые есть в потоке. Ищем стрим с нужным типом видео, запоминаем его индекс. Находим кодек и берем его параметр — Codec ID. Выделяем память под кодек и включаем аппаратное ускорение. В этом случае мы просто выдаем пиксель-формат VideoToolBox.
Инициализируется аппаратное ускорение — в случае с iOS оно всегда найдется, потому что VideoToolBox есть на всех устройствах с этой ОС.
Нашли hardware device contexts, записали его в контекст кодека. Открываем кодек — и в принципе уже можем его декодировать, выделяя память под фрейм. В p-frame у нас будет храниться изображение в формате, стандартном для кодеков h264 и h265.
Теперь мы выделяем память под сетевой пакет, проходимся по фреймам. В пакете уже будет информация после демультиплекса — пакет MPEG-TS с данными о сжатом кадре. Мы проверяем, что это пакет от нашего стрим-индекса.
У нас уже настроен аппаратный кодек и пакет, в случае успешного выполнения кода мы из кодека получаем декодированный фрейм. Нам нужно прокинуть полученное во вьюху, которая все отрисует. И все, что нам остается — найти view controller.
Запуск ffmpeg внутри iOS приложения
Для более удобного обращения к API FFMPEG будем использовать Objective-C
Создадим контекст `AVFormat` из `URL` видео/стрима:
```objc
AVFormatContext* pFormatContext = NULL;
const char* cUrl = [url.absoluteString cStringUsingEncoding:kCFStringEncodingUTF8];
if (avformat_open_input(&pFormatContext, cUrl, NULL, NULL) != 0) {
// Не удалось открыть источник
return;
}
if (avformat_find_stream_info(pFormatContext, NULL) < 0) {
// Не удалось получить информацию о стриме
return;
}
```
Условимся, что мы делаем простой плеер, который умеет отображать только один стрим. Чтобы отображать его нам нужно получить информацию о кодеке. Ее можно получить непосредственно из `AVStream`.
Найдем индекс интересующего нас стрима (самый первый видео стрим)
```objc
int videoStreamIndex = -1;
for(int i = 0; i < pFormatContext->nb_streams; i++) {
if(pFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStreamIndex = i;
break;
}
}
if (videoStreamIndex < 0) {
// Стрим не найден
return;
}
```
Далее получим информацию о кодеке путем вызова метода `avcodec_find_decoder`. В который передадим идентификатор кодека, его можно найти в структуре `AVStream`.
```objc
const AVCodec * pCodec = NULL;
// Поиск декодера по идентификатору
pCodec = avcodec_find_decoder(pFormatContext->streams[videoStreamIndex]->codecpar->codec_id);
if (pCodec == NULL) {
// Декодер не найден
return;
}
```
Теперь необходимо создать контекст `AVCodecContext` с которым мы будем дальше работать.
```objc
AVCodecContext *pCodecCtx = NULL;
AVBufferRef * hwDeviceCtx = NULL;
pCodecCtx = avcodec_alloc_context3(pCodec);
if (avcodec_parameters_to_context(pCodecCtx, pFormatContext->streams[videoStreamIndex]->codecpar) != 0) {
// Не Удалось заполнить контекст параметрами
return;
}
// Коллбэк получения идентификатора формата (см. ниже)
pCodecCtx->get_format = get_hw_format;
// Включение возможности аппаратного ускорения
if (av_hwdevice_ctx_create(&hwDeviceCtx, AV_HWDEVICE_TYPE_VIDEOTOOLBOX, NULL, NULL, 0) < 0) {
// Не Удалось включить аппаратное ускорение
return;
}
pCodecCtx->hw_device_ctx = hwDeviceCtx;
// Открытие кодека
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
// Не Удалось открыть кодек
return;
}
```
Коллбэк получения идентификатора формата:
```objc
static enum AVPixelFormat get_hw_format(AVCodecContext *ctx, const enum AVPixelFormat *pix_fmts)
{
return AV_PIX_FMT_VIDEOTOOLBOX;
}
```
С настройкой кодека все, теперь можно получать фреймы видео, декодировать их и отображать
```objc
AVFrame * pFrame = NULL;
// Выделяем память под видео фрейм
pFrame = av_frame_alloc();
if (pFrame == NULL) {
// Не удалось выделить память
return;
}
// Выделяем память под пакет с закодированными видеоданными
AVPacket * pPacket = av_packet_alloc();
if (pPacket == NULL) {
// Не удалось выделить память
return;
}
while (av_read_frame(pFormatContext, pPacket) >= 0) {
// Нам нужны пакеты только от стрима, который мы выбрали выше
if (pPacket->stream_index != videoStreamIndex) {
av_packet_unref(pPacket);
continue;
}
// Перенаправляем пакет с закодированным видео в декодер
int ret = avcodec_send_packet(pCodecCtx, pPacket);
if (ret < 0) {
// Не удалось перенаправить пакет
continue;
}
while (ret >= 0) {
// Пытаемся получить декодированное изображение из декодера
ret = avcodec_receive_frame(pCodecCtx, pFrame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
// Стрим завершен
break;
} else if (ret < 0) {
// Не удалось декодировать пакет
return;
}
// Так как мы указали формат пикселя VideoToolbox, смело кастим pFrame->data[3] в CVPixelBufferRef.
CVPixelBufferRef pixelBuffer = (CVPixelBufferRef)pFrame->data[3];
// На этом моменте можно отображать CVPixelBufferRef любым удобным для вас способом
[self->_delegate playerDidOutputFrame:pixelBuffer];
}
av_packet_unref(pPacket);
}
```
В нашей интеграции мы выделили такие плюсы FFMpeg:
- Все достаточно просто и интуитивно понятно.
- Стабильность работы.
- Полный контроль над всеми промежуточными процессами. Мы можем вклиниться в процесс получения MPEG-TS, самого транспортного потока, можем вклиниться в процесс декодинга информации между самим MPEG-TS и декодером, можем вклиниться уже после этого и добавить свои фильтры.
- Возможность написать свой I/O для вывода аудио и картинки уже внутри приложения и быстро его поменять, если нужно внести какие-то правки.
Минусы
- Работа с С и все вытекающие отсюда последствия.
Выводы
VLC – это по сути самостоятельный огромный проект, и чтобы добавить в него какую либо фичу, надо во всех случаях идти в отдельный проект, что-то менять, пересобирать, а если ожидаемый результат не совпал с фактическим, то все делать заново.
С FFMpeg ситуация получше: это по сути движок, и очень редко надо лезть в него (в наших кейсах почти никогда). А т.к. сам плеер написан нами и имеет небольшой код, то его легко модифицировать. Наши основные модификации: логика реконнекта, фильтры картинок, фильтры аудио и методы отображения фреймов – всё это модификации самого плеера, а не движка.
К примеру, если нам надо реализовать фичу “картинка в картинке”, то на FFMpeg мы бы легко ее сделали, т.к. можем изменить тип рендеринга на не Metal методы, а VLC осуществляет рендеринг только через Metal и там нет такой возможности.
-
Разработка3 недели назадДаем ИИ видеть SwiftUI: Xcode Preview MCP на практике — проблемы и надежды
-
Новости4 недели назадВидео и подкасты о мобильной разработке 2026.39
-
Новости3 недели назадВидео и подкасты о мобильной разработке 2026.40
-
Новости2 недели назадВидео и подкасты о мобильной разработке 2026.41
