Всем доброго времени суток! Появилась тут как-то задача: воспроизвести RTSP-видеопоток с камеры. Т.к. я в достаточной мере знаком с API OpenCV, было принято решение использовать именно его. Для захвата видеопотока в OpenCV используется класс VideoCapture. К сожалению, сеть достаточно часто у нас обрывается, и проблема эта на моем уровне не решается, поэтому необходимым условием комфортной работы стала достаточно быстрая реакция на падение видеопотока — стандартный таймаут на подключение и ожидание следующего кадра составляет 30 секунд, причем внутри VideoCapture вызовы open() и read() блокирующие, что заставляет писать вокруг простого на самом-то деле кода различные обертки вроде вызова их в отдельном потоке и ожидания получения результата в асинхронном режиме. Естественно, никакой радости по этому поводу я не испытывал — все это ресурсы, которые в программе должны были уходить на иные цели, не говоря уже об усложнении кода. Было принято решение: изменить стандартный таймаут, либо добавить возможность его внешней установки. Получился достаточно грязный хак, который, впрочем, может кому-то пригодиться. Возможно, если есть способ лучше — если таковой имеется — очень бы хотелось его узнать, так что прошу комментариев. В идеале — может быть, среди читателей Хабра найдутся разработчики OpenCV, которые таки обратят внимание на данную проблему. Целью было заставить код «работать, как надо, под Windows x64».

Кому интересно — прошу под кат.

Исследования

При помощи Гугла было выяснено, что данная проблема корнями упирается в ffmpeg — там есть callback'и, которые дают информацию о разрыве соединения. Таймаут в 30 секунд установили в пулл-реквесте #6053. Проблема добавилась в следующем виде: на текущий момент cmake-сборщик скачивает файл opencv_ffmpeg.dll вместо его сборки на месте, причем инструкция по сборке с ffmpeg исчезла. Код с константами таймаута (который, по крайней мере, в Windows никаким образом не компилируется) находится в файле modules/videoio/src/cap_ffmpeg_impl.hpp:

#define LIBAVFORMAT_INTERRUPT_OPEN_TIMEOUT_MS 30000
#define LIBAVFORMAT_INTERRUPT_READ_TIMEOUT_MS 30000

Задача, таким образом, сформировалась следующим образом:

  1. Заставить данный файл собираться под Windows;
  2. Изменить в нем константы;
  3. Убедиться в отсутствии проблем при работе.

Недолго думая, расскажу о том, каким образом она была решена. Для начала нужно скачать последнюю девелоперскую версию ffmpeg с заголовочными файлами, dll, и lib, и положить все это по нужным местам в source/3rdparty — ради удобства, на самом деле можно сложить куда угодно. Далее, нужно внести следующие изменения в исходные файлы OpenCV:

modules/videoio/src/cap_ffmpeg.cpp:

Закомментировать в инклудах строки, касающиеся потенциального включения cap_ffmpeg_api.hpp

//#if defined HAVE_FFMPEG && !defined WIN32
#include "cap_ffmpeg_impl.hpp"
//#else
//#include "cap_ffmpeg_api.hpp"
//#endif

В функции icvInitFFMPEG()

...
#ifdef WIN32...
//все закомментировать, включая директивы препроцессора
#elif defined HAVE_FFMPEG
//оставить только то, что есть внутри
#endif
...

Что здесь произошло — мы отказались от использования подгрузки функциональности ffmpeg из opencv_ffmpeg.dll при помощи LoadLibrary, и переключились на внутреннюю реализацию, которая находится внутри файла cap_ffmpeg_impl.hpp.

modules/videoio/src/cap_ffmpeg_impl.hpp:

Находим вышеуказанные константы, меняем их на желаемые. В моем случае —

#define LIBAVFORMAT_INTERRUPT_OPEN_TIMEOUT_MS 2000
#define LIBAVFORMAT_INTERRUPT_READ_TIMEOUT_MS 1000

Данный файл в общем-то не создан для сборки под Windows, поэтому, кое-какая функциональность, требуемая для его компиляции, в ней отсутствует — речь идет о snprintf. Код был стянут откуда-то с StackOverflow. Вставить в любое удобное место.

#if defined(_MSC_VER) && _MSC_VER < 1900
#define snprintf c99_snprintf
#define vsnprintf c99_vsnprintf
__inline int c99_vsnprintf(char *outBuf, size_t size, const char *format, va_list ap)
{
	int count = -1;
	if (size != 0)
		count = _vsnprintf_s(outBuf, size, _TRUNCATE, format, ap);
	if (count == -1)
		count = _vscprintf(format, ap);
	return count;
}

__inline int c99_snprintf(char *outBuf, size_t size, const char *format, ...)
{
	int count;
	va_list ap;
	va_start(ap, format);
	count = c99_vsnprintf(outBuf, size, format, ap);
	va_end(ap);
	return count;
}
#endif

После внесения изменений, нужно заставить все это добро компилироваться. Для начала, нужно сгенерировать *.sln для OpenCV при помощи CMake. Попытка сборки opencv_videoio на текущий момент не сработает. Поправимо. Открываем Visual Studio-проект для модуля videoio: build/modules/videoio/opencv_videoio.sln. В include directories добавляем заголовочные файлы ffmpeg, его *.lib добавляем к линковке.

Как только opencv_videoio будет собираться, эту dll можно спокойно подкладывать вместо «стандартной». Помним, что теперь для работы также будут нужны все dll из поставки ffmpeg — без них приложение работать не будет.

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

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

Спасибо за внимание.
Поделиться с друзьями
-->

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


  1. Veliant
    14.12.2016 19:12
    +2

    А причём тут реверс инженерия, если Вы с исходными кодами работали?


    1. jknight
      14.12.2016 20:45

      Действительно, лишнее. Подумал и убрал. Спасибо за замечание :)


  1. user_id
    15.12.2016 11:38
    +1

    Не уверен, что использование пропатченой вами OpenCV, причём далеко не лучшим образом чем некоторое усложение программы асинхронность. Тем более, что в современном C++ есть все необходимые стандартные инст рументы — async, promise, future, packaged_task…
    Я бы может вам и посовтовал бы отправить пулл реквест в OpenCV с вашими изменениями ( концептуально то они правильные), но его ведь не приймут, так как это действительно грязные хак.
    В общем, вы уж извините, но именно из-за такого обилия грязных хаков в нашей сфере и нежелания некоторых личностей операционки кишал программами, которые нормально не работаютпосле обновления ОС, после замены библиотек, при любых нестандартных условиях выполнения, при не ожидаемом поведении пользователя… И тут должна быть ссылка про программисто ви самолёт. В общем грустно, что Вам в голову пришло такое решение и проблемы, но мало того, Вы решили ещё его распространить, чтоб в мире стало больше хаоса.


    1. roversochi
      26.03.2017 14:01
      +3

      Мне кажется, что в дешевом китайском роботе-пылесосе надо будет менять все :)


      1. gw8311
        16.12.2016 09:45

        Всё-таки OpenCV предназначена в первую очередь для разработки алгоритмов компьютерного зрения. Создание единого интерфейса для чтения видео сюда не очень вписывается. VideoCapture предоставляет лишь базовые возможности, если вам нужно что-то особенное, то лучше использовать библиотеку ffmpeg напрямую.
        Кстати, скрипты для сборки opencv_ffmpeg.dll можно найти в этом репозитории.


        1. jknight
          16.12.2016 10:01

          Гхм, спасибо за ссылку :) Вечером вернусь с работы — попробую сделать менее инвазивный способ без извращений. Возможно, будет обновление поста.