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

Перед нами и нашими швейцарскими партнёрами из компании Helvetis встала задача транслировать видео в режиме реального времени с web-камеры с маломощного embedded-устройства на дроне по WiFi на Windows-планшет. В идеале бы нам хотелось:

  • задержку < 0.3с;
  • низкую загрузку CPU на embedded-системе (меньше 10% на одно ядро);
  • разрешение хотя бы 480p (лучше 720p).

Казалось бы, что может пойти не так?



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

  • Minnowboard 2-core, Atom E3826 @ 1.4 GHz, ОС: Ubuntu 16.04
  • Web-камера ELP USB100W04H, поддерживающая несколько форматов (YUV, MJPEG, H264)
  • Windows-планшет ASUS VivoTab Note 8

Попытки обойтись стандартными решениями


Простое решение с Python+OpenCV


Сначала мы попробовали использовать простой Python-скрипт, который с помощью OpenCV получал кадры с камеры, сжимал их, используя JPEG, и отдавал по HTTP приложению-клиенту.

Http mjpg стриминг на python
from flask import Flask, render_template, Response
import cv2

class VideoCamera(object):
    def __init__(self):
        self.video = cv2.VideoCapture(0)
    
    def __del__(self):
        self.video.release()
    
    def get_frame(self):
        success, image = self.video.read()
        ret, jpeg = cv2.imencode('.jpg', image)
        return jpeg.tobytes()

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

def gen(camera):
    while True:
        frame = camera.get_frame()
        yield (b'--frame\r\n'
               b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n\r\n')

@app.route('/video_feed')
def video_feed():
    return Response(gen(VideoCamera()),
                    mimetype='multipart/x-mixed-replace; boundary=frame')

if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=True)


Этот подход оказался (почти) работающим. В качестве приложения для просмотра можно было использовать любой web-браузер. Однако мы сразу заметили, что частота кадров была ниже ожидаемой, а уровень загрузки CPU на Minnowboard был постоянно на уровне 100%. Embedded-устройство просто не справлялось с кодированием кадров в режиме реального времени. Из плюсов данного решения стоит отметить очень небольшую задержку при передаче 480p видео с частотой не более 10 кадров в секунду.

В ходе обыска была обнаружена web-камера, которая помимо несжатых YUV-кадров могла выдавать кадры в формате MJPEG. Было решено воспользоваться такой полезной функцией, чтобы уменьшить нагрузку на CPU и найти способ передать видео без перекодирования.

FFmpeg / VLC


Первым делом мы попробовали всеми любимый open-source комбайн ffmpeg, позволяющий, среди прочего, считывать видео-поток с UVC-устройства, кодировать его и передавать. После небольшого погружения в мануал были найдены ключи командной строки, которые позволяли получить и передать сжатый MJPEG видеопоток без перекодирования.

ffmpeg -f v4l2 -s 640x480 -input_format mjpeg -i /dev/video0 -c:v copy -f mjpeg udp://ip:port

Уровень загрузки CPU был невысок. Обрадовавшись, мы с нетерпением открыли поток в плеере ffplay… К нашему разочарованию, уровень задержки видео был абсолютно неприемлемым (около 2 — 3 секунд). Попробовав все отсюда и прошерстив Интернет, мы так и не смогли добиться положительного результата и решили отказаться от ffmpeg.

После провала с ffmpeg пришла очередь медиаплеера VLC, а точнее консольной утилиты cvlc. VLC по умолчанию использует кучу всяких буферов, которые с одной стороны помогают добиться плавности изображения, но с другой дают серьезную задержку в несколько секунд. Изрядно помучавшись, мы подобрали параметры, с которыми стриминг выглядел достаточно сносно, т.е. задержка была не очень большой (около 0.5 с), не было перекодирования, и клиент показывал видео достаточно плавно (пришлось, правда, на клиенте оставить небольшой буфер в 150 мс).

Так выглядит итоговая строка для cvlc:

cvlc -v v4l2:///dev/video0:chroma="MJPG":width=640:height=480:fps=30 --sout="#rtp{sdp=rtsp://:port/live,caching=0}" --rtsp-timeout=-1 --sout-udp-caching=0 --network-caching=0 --live-caching=0

К сожалению, видео работало не вполне стабильно, да и задержка в 0.5 с была для нас неприемлема.

Mjpg-streamer


Наткнувшись на статью о практически нашей задаче, решили попробовать mjpg-streamer. Попробовали, понравилось! Абсолютно без изменений получилось использовать mjpg-streamer для наших нужд без существенной задержки видео на разрешении 480p.

На фоне предыдущих неудач мы довольно долго были счастливы, но потом мы захотели большего. А именно: чуть меньше забивать канал и повысить качество видео до 720p.

H264 стриминг


Чтобы уменьшить загрузку канала, мы решили поменять используемый кодек на h264 (найдя в наших запасах подходящую web-камеру). Mjpg-streamer не имел поддержки h264, так что было решено его доработать. Во время разработки мы использовали две камеры со встроенным кодеком h264, производства Logitech и ELP. Как оказалось, содержимое потока h264 у этих камер существенно различалось.

Камеры и структура потока


Поток h264 состоит из пакетов NAL (network abstraction layer) нескольких типов. Наши камеры генерировали 5 типов пакетов:

  • Picture parameter set (PPS)
  • Sequence parameter set (SPS)
  • Coded slice layer without partitioning, IDR picture
  • Coded slice layer without partitioning, non-IDR picture
  • Coded slice data partition

IDR (Instantaneous decoding refresh) — пакет, содержащий кодированное изображение. При этом все необходимые данные для декодирования изображения находятся в этом пакете. Этот пакет необходим декодеру, чтобы начать формировать изображение. Обычно первый кадр любого видео сжатого h264 — IDR picture.

Non-IDR — пакет, содержащий кодированное изображение, содержащее ссылки на другие кадры. Декодер не в состоянии восстановить изображение по одному Non-IDR кадру без наличия других пакетов.

Помимо IDR-кадра, декодеру нужны пакеты PPS и SPS для декодирования изображения. Эти пакеты содержат метаданные об изображении и потоке кадров.

Основываясь на коде mjpg-streamer, мы воспользовались API V4L2 (video4linux2) для считывания данных от камер. Как выяснилось, один “кадр” видео содержал несколько NAL пакетов.

Именно в содержимом “кадров” обнаружилось существенное различие между камерами. Мы воспользовались библиотекой h264bitstream для парсинга потока. Существуют standalone-утилиты, позволяющие просмотреть содержимое потока.



Поток кадров камеры Logitech состоял в основном из non-IDR кадров, к тому же разделенных на несколько data partition. Раз в 30 секунд камера генерировала пакет, содержащий IDR picture, SPS и PPS. Так как декодеру нужен IDR пакет для того, чтобы начать декодировать видео, нас эта ситуация сразу не устроила. К нашему сожалению, оказалось, что нет адекватного способа установить период, с которым камера генерирует IDR пакеты. Поэтому нам пришлось отказаться от использования этой камеры.



Камера производства ELP оказалась существенно удобнее. Каждый получаемый нами кадр содержал в себе пакеты PPS и SPS. К тому же, камера генерировала IDR пакет раз в 30 кадров (период ~1с). Это нас вполне устраивало и мы остановили свой выбор на этой камере.

Реализация сервера вещания на основе mjpg-streamer


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

В V4L2 достаточно было указать что мы хотим получать кадры в формате V4L2_PIX_FMT_H264, чтобы начать получать поток h264.Так как для декодирования потока необходим IDR-кадр, мы парсили поток и ожидали IDR-кадр. Приложению-клиенту поток отправлялся по HTTP начиная с этого кадра.

На клиентской части решили воспользоваться libavformat и libavcodec из проекта ffmpeg для чтения и декодирования потока h264. В первом тестовом прототипе получение потока по сети, разбиение его на кадры и декодирование было возложено на ffmpeg, конвертирование получаемого декодированного изображения из формата NV12 в RGB и отображение было реализовано на OpenCV.

Первые тесты показали, что данный способ транслирования видео работоспособен, но имеется существенная задержка (около 1 секунды). Наше подозрение пало на протокол http, поэтому было решено использовать для передачи пакетов UDP.

Так как у нас не было необходимости поддержки существующих протоколов вроде RTP, мы реализовали свой простейший велосипед протокол, в котором внутри UDP-датаграмм передавались NAL-пакеты потока h264. После небольшой доработки принимающей части мы были приятно удивлены малой задержкой видео на настольном ПК. Однако первые же тесты на мобильном устройстве показали, что программное декодирование h264 — не конёк мобильных процессоров. Планшет просто не успевал обрабатывать кадры в режиме реального времени.

Так как процессор Atom Z3740, используемый на нашем планшете, поддерживает технологию Quick Sync Video (QSV), мы попробовали использовать QSV h264 декодер из libavcodec. К нашему удивлению, он не только не улучшил ситуацию, но и увеличил задержку до 1.5 секунд даже на мощном настольном ПК! Однако этот подход действительно существенно снизил нагрузку на CPU.

Перепробовав различные варианты конфигурации декодера в ffmpeg, было решено отказаться от libavcodec и использовать Intel Media SDK напрямую.

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

Декодирование стрима при помощи Intel Media SDK
mfxStatus sts = MFX_ERR_NONE;

// Буфер с содержимым потока h264
mfxBitstream mfx_bitstream;
memset(&mfx_bitstream, 0, sizeof(_mfxBS));
mfx_bitstream.MaxLength = 1 * 1024 * 1024; // 1MB
mfx_bitstream.Data = new mfxU8[mfx_bitstream.MaxLength];

// Реализация протокола на основе UDP
StreamReader *reader = new StreamReader(/*...*/);

MFXVideoDECODE *mfx_dec;
mfxVideoParam mfx_video_params;
MFXVideoSession session;
mfxFrameAllocator *mfx_allocator;

// Инициализация сессии MFX
mfxIMPL impl = MFX_IMPL_AUTO;
mfxVersion ver = { { 0, 1 } };
session.Init(sts, &ver);
if (sts < MFX_ERR_NONE)
    return 0; // :(

// Создаем декодер, устанавливаем кодек AVC (h.264)
mfx_dec = new MFXVideoDECODE(session);
memset(&mfx_video_params, 0, sizeof(mfx_video_params));
mfx_video_params.mfx.CodecId = MFX_CODEC_AVC;
// Декодируем в системную память
mfx_video_params.IOPattern = MFX_IOPATTERN_OUT_SYSTEM_MEMORY;
// Устанавливаем глубину очереди в минимальное значение
mfx_video_params.AsyncDepth = 1;

// получаем метаинформацию о видео
reader->ReadToBitstream(&mfx_bitstream);
sts = mfx_dec->DecodeHeader(&mfx_bitstream, &mfx_video_params);

if (sts < MFX_ERR_NONE)
    return 0; // :(

// Запросим информацию о размере кадров
mfxFrameAllocRequest request;
memset(&request, 0, sizeof(request));
sts = mfx_dec->QueryIOSurf(&mfx_video_params, &request);
if (sts < MFX_ERR_NONE)
    return 0; // :(

mfxU16 numSurfaces = request.NumFrameSuggested;

// Для декодера необходимо чтобы ширина и высота были кратны 32
mfxU16 width = (mfxU16)MSDK_ALIGN32(request.Info.Width);
mfxU16 height = (mfxU16)MSDK_ALIGN32(request.Info.Height);
// NV12 - формат YUV 4:2:0, 12 бит на пиксель
mfxU8 bitsPerPixel = 12;
mfxU32 surfaceSize = width * height * bitsPerPixel / 8;

// Выделим память для поверхностей в которые будут декодироваться кадры 
mfxU8* surfaceBuffers = new mfxU8[surfaceSize * numSurfaces];

// Метаинформация о поверхностях для декодера
mfxFrameSurface1** pmfxSurfaces = 
                new mfxFrameSurface1*[numSurfaces];
for(int i = 0; i < numSurfaces; i++)
{
    pmfxSurfaces[i] = new mfxFrameSurface1;
    memset(pmfxSurfaces[i], 0, sizeof(mfxFrameSurface1));
    memcpy(&(pmfxSurfaces[i]->Info), 
      &(_mfxVideoParams.mfx.FrameInfo), sizeof(mfxFrameInfo));
    pmfxSurfaces[i]->Data.Y = &surfaceBuffers[surfaceSize * i];
    pmfxSurfaces[i]->Data.U = 
                      pmfxSurfaces[i]->Data.Y + width * height;
    pmfxSurfaces[i]->Data.V = pmfxSurfaces[i]->Data.U + 1;
    pmfxSurfaces[i]->Data.Pitch = width;
}

sts = mfx_dec->Init(&mfx_video_params);
if (sts < MFX_ERR_NONE)
    return 0; // :(

mfxSyncPoint syncp;
mfxFrameSurface1* pmfxOutSurface = NULL;
mfxU32 nFrame = 0;

// Начало декодирования потока
while (reader->IsActive() &&
    (MFX_ERR_NONE <= sts
        || MFX_ERR_MORE_DATA == sts
        || MFX_ERR_MORE_SURFACE == sts))
{
    // Ждем если устройство было занято 
    if (MFX_WRN_DEVICE_BUSY == sts)
        Sleep(1);
    if (MFX_ERR_MORE_DATA == sts)
        reader->ReadToBitstream(mfx_bitstream);

    if (MFX_ERR_MORE_SURFACE == sts || MFX_ERR_NONE == sts)
    {
        nIndex = GetFreeSurfaceIndex(pmfxSurfaces, numSurfaces);
        if (nIndex == MFX_ERR_NOT_FOUND)
            break;
    }

    // Декодирование кадра
    // Декодер самостоятельно находит NAL-пакеты в потоке и забирает их
    sts = mfx_dec->DecodeFrameAsync(mfx_bitstream, 
            pmfxSurfaces[nIndex], &pmfxOutSurface, &syncp);

    // Игнорируем предупреждения
    if (MFX_ERR_NONE < sts && syncp)
        sts = MFX_ERR_NONE;

    // Ожидаем окончания декодирования кадра
    if (MFX_ERR_NONE == sts)
        sts = session.SyncOperation(syncp, 60000);

    if (MFX_ERR_NONE == sts)
    {
        // Кадр готов!
        mfxFrameInfo* pInfo = &pmfxOutSurface->Info;
        mfxFrameData* pData = &pmfxOutSurface->Data;

        // Декодированный кадр имеет формат NV12
        // плоскость Y: pData->Y, полное разрешение
        // плоскость UV: pData-UV, разрешение в 2 раза ниже чем у Y
    }
} // Конец цикла декодирования


После реализации декодирования видео при помощи Media SDK мы столкнулись с аналогичной ситуацией — задержка видео составила 1.5 секунды. Отчаявшись, мы обратились к форумам и нашли советы, которые должны были снизить задержку при декодировании видео.

Декодер h264 из состава Media SDK накапливает кадры прежде чем выдавать декодированное изображение. Было обнаружено, что если в структуре данных, передаваемых в декодер (mfxBitstream), установить флаг “конец потока”, то задержка снижается до ~0.5 секунд:

mfx_bitstream.DataFlag = MFX_BITSTREAM_EOS;

Далее экспериментальным путем было обнаружено, что декодер держит 5 кадров в очереди, даже если установлен флаг окончания потока. В итоге нам пришлось добавить код, который симулировал “окончательное окончание потока” и заставлял декодер выдавать кадры из этой очереди:

if( no_frames_in_queue )
    sts = mfx_dec->DecodeFrameAsync(mfx_bitstream, pmfxSurfaces[nIndex], 
                                    &pmfxOutSurface, &syncp);
else
    sts = mfx_dec->DecodeFrameAsync(0, pmfxSurfaces[nIndex], &pmfxOutSurface, &syncp);


if (sts == MFX_ERR_MORE_DATA)
{
    no_frames_in_queue = true;
}

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

Выводы


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

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

Практически случайно наткнувшись на проект mjpg-streamer, мы были очарованы его простотой и четкой работой в деле трансляции видео в формате MJPG. Если вам вдруг понадобится передавать именно этот формат, то мы категорически рекомендуем его использовать. Неслучайно, что именно на его основе мы и реализовали свое решение.

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

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


  1. al_sh
    28.11.2017 11:56
    +1

    А почему не передавать записанные SPS и PPS перед каждым IDR NAL? Я пишу SPS + PPS и передаю преред каждым IDR. Кроме того, отдельно храню IDR и при первом подключении клиента сыплю сохраненный IDR+SPS+PPS (можно хранить и сыпать все налы между IDR), с минимальным интервалом между датаграммами, что позволяет получить картинку сразу после подключения.


    1. OShapovalov Автор
      28.11.2017 12:20

      С SPS и PPS кадрами в нашем случае проблем не было: наша камера шлет их регулярно, с каждым кадром (см. парсинг потока камеры ELP). Что касается сохранения старых IDR кадрой, то в таком случае в начале трансляции будет показан старый IDR кадр, но последующие Non-IDR кадры буду накладываться на старый IDR, что будет давать плохую картинку. Нас такая ситуация не устраивала, к тому же небольшая задержка старта видео (менее секунды) нас не беспокоила.


      1. al_sh
        28.11.2017 12:31

        ничего накладываться не будет. При подключении посылаете записанные IDR+NIDRы, только не с интервалом 20-30мс, как при трансляции, а 3-4мс. Получаете готовый кадр и приемник готов к приему след. нонидров. Можно послать идр и игнорить все налы до след. идра, тогда вы получите статическую картинку на пару секунд. Все это верно и при трансляции записанного AnnexB файла, когда, как правило SPS/PPS передаются только раз.


        1. OShapovalov Автор
          28.11.2017 12:50

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


  1. OShapovalov Автор
    28.11.2017 12:19

    (ошибся веткой)


  1. sibnick
    28.11.2017 14:49

    Есть еще GStreamer с кучей плагинов и вроде с плагином от интела в том числе (GStreamer Media SDK plugins) В первом комменте в статье про mjpg-streamer его тоже вспоминают. Его не пробовали?


    1. OShapovalov Автор
      28.11.2017 15:09

      Пробовали, практически в самом начале разработки. Чем-то он нам сразу не подошел, уже точно не помню чем. По-моему, не удалось передавать видео без перекодирования.


      1. a1ien_n3t
        28.11.2017 22:46

        Значит просто не разобрались в gstreamer.
        Обычно то, что нерешает vlc и ffmpeg можно небольшими шаманствами с плагинами решить gstreamer'ом. Притом что в gstreamer'е можно спокойно использовать ffmpeg


        1. OShapovalov Автор
          28.11.2017 22:52

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


          1. Amomum
            28.11.2017 23:41
            +1

            Если у вас камера выдает картинку в mjpg или в raw, а вы хотите h264, то перекодировать придется.
            Если же камера сама умеет выдавать h264, то это будет что-то вроде:
            gst-launch v4lsrc ! video/x-h264,width=640,height=480 ! rtph264pay ! udpsink host=127.0.0.1 port=5555
            Писал по памяти, скорее всего нагнал, пишите в личку, если не взлетит.


            1. OShapovalov Автор
              28.11.2017 23:53

              Спасибо, завтра попробую


            1. ZRas
              29.11.2017 08:42

              в данном конкретном случае capsfilter (тот элемент пайплайна который «video/x-h264,width=640,height=480») даже не нужен наверно, так как rtph264pay по своей природе на вход просит следующее:
              video/x-h264, stream-format=(string)avc, alignment=(string)au
              video/x-h264, stream-format=(string)byte-stream, alignment=(string){ nal, au }

              а вот если паковать в какой-то мультиформатный контейнер (у меня для стриминга в свое время очень хорошо mpegtsmux себя показал) то конечно стоит прописать капсфильтр чтобы исключить перекодирование.

              Кроме того, если передача будет вестись по не самому надежному каналу (а тут у нас именно такой, БПЛА же) возникает проблема потерь udp пакетов (возникнет как только выйдете из лаборатории) соответственно я бы все же советовал tcp использовать, иначе рискуете вообще никакой картинки не получить. MJPEG кстати в этом отношении лучше, так как нет межкадрового, что получил то и отобразил, в худшем случае теряя фреймрейт.
              Так вот, если использовать tcp то помимо той задержки которую вы видите на старте появляется назойливая «накапливающаяся» задержка.
              Выглядит это следующим образом: связь устанавливается, картинка отображается, задержка небольшая. вдруг откуда ни возьмись возникают проблемы в канале связи, картинка фризится, стример/приемник начинает увеличивать буферы чтобы давать плавную картинку. В итоге через пару таких фризов имеем вместо стартовой задержки в 200 мс аж 2-3 секунды задержки. что конечно неприемлемо.
              Всех путей решения данной проблемы я если честно и не вспомню уже сейчас, года 3-4 назад занимался. один из вариантов добавить leaky queue который будет дропать слишком старые, все равно никого не интересующие данные если есть более новые.

              Кстати. vlc имеет настроечку network-caching позволяющую настроить политику кеширования. если его в качестве клиента использовать — отлично работает


              1. ZRas
                29.11.2017 08:48

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


                1. Amomum
                  29.11.2017 11:38

                  Да, мы такую же методику измерения применяли.
                  Но для наших применений пропадания пакетов были приемлемы, а вот накапливающаяся задержка от tcp — вообще никак.

                  Поскольку у нас не было камеры, которая может в h264, мы в него перекодировали, что давало возможность поиграться с параметрами энкодера. Если делать вот так:
                  x264enc speed-preset=ultrafast tune=zerolatency threads=8 bitrate=2048, то энкодер достаточно сильно уменьшает расстояние между ключевыми кадрами, а из-за threads=8 все кадры режутся на 8 горизонтальных полосок, которые передаются отдельно.
                  Соответственно, при потерях выпадают не кадры целиком, а только полоски. Получилось более-менее.

                  Но h264 слишком нагружал процессор, поэтому в итоге мы использовали mjpeg.

                  Был еще волшебный способ с использованием rtpbin, в котором по одному порту шло видео по udp, а по второму синхронизация rtcp, а по третьему шла синхронизация от приемника.
                  Но тут проблема была в том, что приемником был не gstreamer, а приложение для Windows, оно так не умело.


                  1. ZRas
                    29.11.2017 16:54

                    без аппаратного кодирования в h264 лучше не соваться. а набор параметров энкодера аппаратного кодировщика уже сильно варьируется от случая к случаю


  1. ZRas
    28.11.2017 16:15
    +1

    Решал практически точно такую же задачу при помощи gstreamer. Из плюсов — возможность запрототипировать предполагаемый функционал с помощью gst-launch (а иногда даже и в продакшен вытолкнуть если никто не видит а сроки поджимают)
    Имеет некоторые подводные камни, но обладает отличной портируемостью, хорошей гибкостью и весьма приличной поддержкой самых разных аппаратных решений


  1. xaoc80
    28.11.2017 18:01

    Если камера не использует B-Frames, то при использовании libavcodec никаких задержек не будет, во всяком случае, когда я писал подобный софт (HTTP Live Streaming) никаких задержек не было. В ffmpeg сейчас новый асинхронный API запилили, и декодер пишется букально в три строчки.


  1. AlexGluck
    28.11.2017 21:45

    Весной был на хакатоне с идеей подключения дополнительных мониторов к ноутбуку без проводов, и чтобы через вайфай при этом работал интернет. Решил, что создавать виртуальные дисплеи встроенным функционалом ОС и после захватывать их хорошая идея(такое возможно на маках, виндах и я сижу на линуксе). Наткнулся на проблему, что задержка даже на локалхосте составляла добрых 0.3-0.5 секунд, что существенно для восприятия видео со звуком и даже при написании кода. Задача чисто офисная, поэтому вопрос о стриминге игр не вставал вообще. Расчётным был порог задержки менее 66 мс, дальше уже становилось не комфортно воспринимать задержку звука. При 60 кадрах в секунду, генерация одного кадра составляет около 17 мс, соответственно моя задача была добиться буферизации не более 3х кадров на клиенте для отображения картинки. На моём ноуте есть дискретная nvidia с nvenc и встроенная интел видеокарта с quick sync аппаратным кодировщиком, но я увы не смог за 48 часов вместе с другим разработчиком победить буферизацию кодировщика как я в этой статье понял. Так же я пытался закинуть raw данные в udp пакет, а на клиенте читал напрямую из сокета без буферизации. В итоге малинка в качестве клиента беспроводного монитора осталась валятся на полке. Вопросы остались просты о скорости софтовых грабберов монитора, скорости кодирования картинки, объёме данных FullHD@60fps передаваемых по сети при таком использовании, так как это РоС идея была, качественная реализация была намечена на использование общего канала 802.11ac wave2 или уже 802.11.ax, но чтобы не привязывалось к вайфаю, а работало и по проводной сети, чего не дают miracast и интеловские wireless display.


    1. OShapovalov Автор
      28.11.2017 21:59

      Какую интересную задачу Вы решали! Мы конечно потратили существенно больше 48 часов на вот это вот всё.


  1. goodwin_c
    29.11.2017 12:46

    Для БПЛА 0.3 секунды? Смешно. То же самое что кино в кинотеатре смотреть с рейтом в 15 кадров — в общем смотреть можно, но как-то не то.
    А если серьезно — обратите внимание на проект Wifibroadcast — всю работу сделали за вас и запилили трансляцию овер 2.4 либо 5Ггц через обычные Wifi донглы, используя малинку с родной камерой — и с задержкой сильно ниже 100мс. Если что — ответы на вопросы искать тут github.com/bortek/EZ-WifiBroadcast


    1. OShapovalov Автор
      29.11.2017 12:47

      Спасибо, посмотрим. Сами мы сейчас Sky Hopper Pro ковыряем.


  1. ABaygulov
    29.11.2017 12:51

    Использовать MFX_BITSTREAM_EOS все равно, что забивать гвозди микроскопом. Если вы точно знаете, что в потоке нет B кадров, то можно смело использовать DecodeOrder. Плюс если передаете строго по кадрам, то можно добавлять еще и MFX_BITSTREAM_COMPLETE_FRAME. В этом случае декодер не будет ждать начало следующего кадра, чтобы закончить текущий.


    1. OShapovalov Автор
      29.11.2017 13:01

      MFX_BITSTREAM_COMPLETE_FRAME мы добавляли, но он никак не повлиял на задержку. MFX_BITSTREAM_EOS оказалось достаточно.


      1. ABaygulov
        29.11.2017 13:27

        MFX_BITSTREAM_COMPLETE_FRAME уменьшает задержку на один кадр (Без этого флага декодер не знает, что переданный фрейм полный, поэтому вынужден ждать прихода первого слайса (или SPS/PPS) следующего фрейма, тогда он понимает, что текущий фрейм завершен, для видеоконференций один кадр — доп. задержка в 0.33 мс при 30fps). Понятно, что при задержке в 5-10 кадров выставление complete frame малозаметно.
        EOS (end of stream) — это плохая попытка эмуляции DecodeOrder. Выставляя EOS вы, по сути, говорите декодеру, что больше фреймов не будет, значит то, что есть в буфере можно смело выдать в порядке возрастания поков (не придет фрейм, который нужно показать раньше тех, что есть в буфере). DecodeOrder означает, что вам не нужен реордеринг фреймов вообще (если B фреймов нет, то он действительно не нужен), декодер просто будет выдавать фреймы по готовности.
        MFX_BITSTREAM_COMPLETE_FRAME вместе с DecodeOrder позволяет передать битстрим декодеру и сразу же получить выход.


        1. OShapovalov Автор
          29.11.2017 14:20

          Спасибо за комментарий. Попробуем использовать DecodeOrder.


          1. xaoc80
            29.11.2017 16:42

            Вот здесь описание похожей проблемы есть software.intel.com/en-us/forums/intel-integrated-performance-primitives/topic/294840


        1. OShapovalov Автор
          29.11.2017 16:45

          Полазили по документации и форумам. Видимо Вы имели в виду установку DecodedOrder=1. Сделали это, а также избавились от флага MFX_BITSTREAM_EOS и хака с “окончательным окончанием потока”. Флаг MFX_BITSTREAM_COMPLETE_FRAME уже у нас стоял. В итоге все получилось, задержка даже слегка уменьшилась. Спасибо за очень полезный комментарий!

          P.S. Ложка дёгтя: DecodedOrder сейчас deprecated.


          1. ABaygulov
            29.11.2017 17:26

            Он последние лет 5-7 в deprecated. Его даже убирали ненадолго. Но потом вернули и убирать вряд ли кто-то станет (Вернули специально для решения вашей проблемы). Просто раньше он делал больше, чем то, что вам нужно (в этом режиме декодер также хранил выданные сразу фреймы и позже выставлял корректный frameOrder на серфейс). От этого «больше» отказались. Немедленную выдачу оставили.


            1. OShapovalov Автор
              01.12.2017 11:51

              К сожалению выяснилась еще одна проблема: на нашем полетном планшете стоит процессор Intel Celeron N2807, и, если установить флаг DecodedOrder=1, видео перестает работать.


              1. OShapovalov Автор
                01.12.2017 14:40

                И на Atom Z3740 тоже не работает.


  1. xaoc80
    29.11.2017 16:51

    Если использовали ffmpeg (точнее его API), то можно посмотреть на AVCodecContext.delay = 0 при инициализации контекста декодера. Эта проблема точно решалась настройками контекстов декодера.