Передача видео с глубоководного робота

Хочу поделиться с сообществом опытом разработки программного обеспечения для просмотра и записи видео-сигнала передаваемого с глубоководного робота Moby Dick. Разработка проводилась по заказу лаборатории подводной робототехники The Whale. Проект был призван обеспечить:
— работу с любыми IP-камерами поддерживающими протокол RTSP;
— просмотр и запись видео от нескольких IP-камер;
— просмотр и запись стерео-видео от двух выделенных IP-камер;
— запись видео с экрана;
— комфортный просмотр видео при кратковременном падении скорости передачи данных.

c3074d2faa2c4d46a6c26304aaf3d7ad.png
Глубоководный робот Moby Dick проекта 1–0–1 десантированый с борта трансрейдера ВКС России «Лунная радуга» исследует океан Европы (в представлении художника, коллаж)

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

Moby Dick


Moby Dick — телеуправляемый подводный аппарат (англ. Underwater Remotely Operated Vehicle (Underwater ROV или UROV)) разработанный в лаборатории подводной робототехники The Whale. Основной целью при проектировании и создании Moby Dick было создание небольшого робота, способного погружаться на максимальные глубины мирового океана (в составе подъемно-спускового механизма), выдавать видео высокого разрешения (Full HD), брать пробы, образцы, проводить спасательные работы.

41cc8c78a2a24d889311796b1d00be69.jpg
Глубоководный робот Moby Dick спереди (видны две передние камеры в верхней части под баллонами)

8f75d555523a4decb8b08fb3f7888c4a.jpg
Глубоководный робот Moby Dick сзади (видна задняя камера в нижней части под креплением кабеля)

Как правило на Moby Dick устанавливают три цифровые видеокамеры, причем две из них устанавливаются на вращающимся подвесе с вращением на 360° по всем трем осям. Вращение на 360° по всем трем осям позволяет осматривать двигатели аппарата и общую обстановку вокруг и внутри рамы, а также быстро осматривать длинномерные объекты (трубопроводы, суда и т.д.) при положении видеокамеры перпендикулярно движению робота. Эта особенность так же позволяет получать стереоскопическое изображение морского дна, появляется возможность оценивать расстояние до объектов и их размер непосредственно с экрана монитора.

Всего на Moby Dick можно установить до 9 цифровых камер получив обзор всей сферы. Правда в максимальной комплектации приходится довольствоваться Full HD только для одной камеры — остальные придется подключать как 720p из-за ограничения скорости передачи данных по кабелю.

Следует отметить, что это единственный подводный робот с возможностью передачи стерео-видео в нашей стране. Аналогичные по своим возможностям роботы имеются за рубежом, но по сравнению с Moby Dick они сильно большие по размерам.

Получение и запись видео


Для получения и записи видео была выбрана библиотека FFMPEG. Для изоляции разработчика от специфического API FFMPEG используется DLL-посредник (собран в старой доброй Microsoft Visual C++ 2008 Express Edition). Рассмотрим наиболее интересные части его кода.

Для получения отметок времени высокой точности используется код

static bool timer_supported;
static LARGE_INTEGER timer_f;
static bool timer_init(void)
{
    return timer_supported = QueryPerformanceFrequency(&timer_f);
}
static LARGE_INTEGER timer_t;
static bool timer_get(void)
{
    return QueryPerformanceCounter(&timer_t);
}
static LONGLONG get_microseconds_hi(void)
{
    return timer_get()? timer_t.QuadPart * 1000000 / timer_f.QuadPart : 0;
}
static LONGLONG get_microseconds_lo(void)
{
    return GetTickCount() * 1000i64;
}
static LONGLONG (*get_microseconds)(void) = get_microseconds_lo;


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

static TIMECAPS tc;
...
//при инициализации DLL
memset(&tc, 0, sizeof(tc));
...
//если нужно использовать отметки времени и задержки высокой точности
if (flag)
{
        get_microseconds = timer_init()? get_microseconds_hi : get_microseconds_lo;

        //если установка точности не производилась и структура tc получена, то...
        if (!tc.wPeriodMin && timeGetDevCaps(&tc, sizeof(tc)) == MMSYSERR_NOERROR)
        {
                timeBeginPeriod(tc.wPeriodMin); //устанавливаем самую высокую точность
                //теперь Sleep будет работать с высокой точностью
        }
}
//если точность не нужна
else
{
        if (tc.wPeriodMin) //если установка точности производилась
        {
                timeEndPeriod(tc.wPeriodMin); //возвращаем все как было
                memset(&tc, 0, sizeof(tc));
        }
}


Каждая камера представлена классом ctx_t.

class ctx_t:
public mt_obj //класс поддерживающий блокировку
{
public:
        AVFormatContext *format_ctx; //контекст формата

        int stream; //номер видео-потока

        AVCodecContext *codec_ctx; //контекст декодера видео-потока

        AVFrame *frame[2]; //буфер из двух кадров
        int frame_idx; //номер формируемого кадра (готовый кадр имеет номер !frame_idx если frames_count > 0)

        //BGR-представление кадра и контекст масштабирования...
        //используются приложением вызывающим DLL-посредник для чтения сформированного кадра
        AVFrame *x_bgr_frame; 
        SwsContext *x_bgr_ctx;

        AVFormatContext *ofcx; //контекст формата для записи
        AVStream *ost; //видео-поток для записи
        int rec_started; //признак старта записи (старт только с ключевого кадра)
        mt_obj rec_cs; //блокировка для записи

        int64_t pts[2]; //отметка времени кадра
        int continue_on_error; //продолжать работу при возникновении ошибок
        int disable_decode; //отключить декодер

        void (*cb_fn)(int cb_param); //обработчик вызываемый после формирования кадра
        int cb_param; //параметр передаваемый обработчику

        ms_rec_ctx_t *owner; //объект отвечающий за стерео-запись
        int out_stream; //номер видео-потока (задается)

        int frames_count; //количество сформированных кадров

        DWORD base_ms; //время вызова функции FFMPEG
        DWORD timeout_ms; //предельное время выполнения функции FFMPEG

        HANDLE thread_h; //поток
        int terminated; //признак уничтожения потока
};


Для контроля времени выполнения функций FFMPEG используется функция обратного вызова

static int interrupt_cb(void *_ctx)
{
        ctx_t *ctx = (ctx_t *)_ctx;
        //если время выполнения функции FFMPEG больше предельного, то прерываем ее работу
        if (GetTickCount() - ctx->base_ms > ctx->timeout_ms) return 1;
        return 0;
}


Благодаря этой функции мы можем контролировать время выполнения функций FFMPEG таких как avformat_open_input / avformat_find_stream_info / av_read_frame / avformat_close_input

format_ctx = avformat_alloc_context();
if (!format_ctx)
{...}

format_ctx->interrupt_callback.callback = interrupt_cb;
format_ctx->interrupt_callback.opaque = this;

AVDictionary *opts = 0;
if (tcp_flag) av_dict_set(&opts, "rtsp_transport", "tcp", 0); //только TCP-транспорт

base_ms = GetTickCount(); //устанавливаем время вызова функции FFMPEG
if (avformat_open_input(&format_ctx, src, 0, &opts) != 0) //вызов функции FFMPEG (с контролем времени)
{...}


С функцией обратного вызова avformat_open_input будет выполнятся не дольше чем заданное время timeout_ms. Без функции обратного вызова выполнение программы было бы приостановлено до установления связи с IP-камерой. Обратите внимание на то, что в качестве транспорта выбран TCP. При использовании UDP большая часть приходящих кадров оказывалась повреждена (линия связи имеет значительную длину и передача идет с помехами от работающих агрегатов робота).

Каждую камеру обслуживает отдельный поток следующего вида

AVPacket packet;
int finished;
while (!ctx->terminated)
{
        ctx->base_ms = GetTickCount();
        if (av_read_frame(ctx->format_ctx, &packet) != 0)
        {
                //продолжать работу при возникновении ошибок если задан соответствующий флаг
                if (ctx->continue_on_error)
                {}
                else
                {
                        ctx->terminated = 1;
                        ExitThread(0);
                }
        }
        else if (packet.stream_index == ctx->stream)
        {
                int64_t original_pts = packet.pts;

                if (ctx->disable_decode) //если декодер отключен записываем пакет (если запись включена)
                {
                        ctx->rec_cs.lock(); //блокировка для записи
                        if (ctx->owner) lock(ctx->owner); //блокировка объекта отвечающего за стерео-запись
                        if (ctx->ofcx)
                        {
                                //старт записи только с ключевого кадра
                                if (!ctx->rec_started && (packet.flags & AV_PKT_FLAG_KEY))
                                {
                                        ctx->rec_started = 1;
                                }
                                if (ctx->rec_started)
                                {
                                        //отметка времени показа
                                        int64_t pts = packet.pts;
                                        //разность отметок времени декодирования и показа
                                        int64_t delta = packet.dts - packet.pts;
                                        if (ctx->owner) //если есть объект отвечающий за стерео-запись, то...
                                        {
                                                LONGLONG start;
                                                //получаем время начала стерео-записи
                                                get_start(ctx->owner, &start);
                                                //получаем текущую отметку времени
                                                LONGLONG dt = (get_microseconds() - start) / 1000;
                                                //формируем отметку времени показа в частотах оригинала
                                                pts = dt * ctx->format_ctx->streams[ctx->stream]->time_base.den / 1000;
                                        }
                                        packet.stream_index = ctx->out_stream; //устанавливаем номер потока
                                        //устанавливаем отметку времени показа в частотах формируемой записи
                                        packet.pts = av_rescale_q(pts, ctx->format_ctx->streams[ctx->stream]->time_base, ctx->ost->time_base);
                                        //устанавливаем отметку времени декодирования в частотах формируемой...
                                        //записи используя смещение по отношению к времени показа
                                        packet.dts = av_rescale_q(packet.dts == AV_NOPTS_VALUE? AV_NOPTS_VALUE : (pts + delta), ctx->format_ctx->streams[ctx->stream]->time_base, ctx->ost->time_base);
                                        //на всякий случай устанавливаем продолжительность
                                        packet.duration = av_rescale_q(packet.duration, ctx->format_ctx->streams[ctx->stream]->time_base, ctx->ost->time_base);
                                        //на всякий случай еще немного магии
                                        packet.pos = -1;
                                        //ставим пакет на запись - очередность записи пакетов определяется...
                                        //FFMPEG по типу пакета
                                        av_interleaved_write_frame( ctx->ofcx, &packet );
                                        //на всякий случай восстанавливаем номер потока
                                        packet.stream_index = ctx->stream;
                                }
                        }
                        if (ctx->owner) unlock(ctx->owner);
                        ctx->rec_cs.unlock();
                }
                //если декодер включен, то в начале декодируем пакет и только потом...
                else if (avcodec_decode_video2(ctx->codec_ctx, ctx->frame[ctx->frame_idx], &finished, &packet) > 0)
                {
                        //записываем его (если запись включена) так же как мы делали выше
                        ctx->rec_cs.lock();
                        if (ctx->owner) lock(ctx->owner);
                        if (ctx->ofcx)
                        {
                                if (!ctx->rec_started && (packet.flags & AV_PKT_FLAG_KEY))
                                {
                                        ctx->rec_started = 1;
                                }
                                if (ctx->rec_started)
                                {
                                        int64_t pts = packet.pts;
                                        int64_t delta = packet.dts - packet.pts;
                                        if (ctx->owner)
                                        {
                                                LONGLONG start;
                                                get_start(ctx->owner, &start);
                                                LONGLONG dt = (get_microseconds() - start) / 1000;
                                                pts = dt * ctx->format_ctx->streams[ctx->stream]->time_base.den / 1000;
                                        }
                                        packet.stream_index = ctx->out_stream;
                                        packet.pts = av_rescale_q(pts, ctx->format_ctx->streams[ctx->stream]->time_base, ctx->ost->time_base);
                                        packet.dts = av_rescale_q(packet.dts == AV_NOPTS_VALUE? AV_NOPTS_VALUE : (pts + delta), ctx->format_ctx->streams[ctx->stream]->time_base, ctx->ost->time_base);
                                        packet.duration = av_rescale_q(packet.duration, ctx->format_ctx->streams[ctx->stream]->time_base, ctx->ost->time_base);
                                        packet.pos = -1;
                                        av_interleaved_write_frame( ctx->ofcx, &packet );
                                        packet.stream_index = ctx->stream;
                                }
                        }
                        if (ctx->owner) unlock(ctx->owner);
                        ctx->rec_cs.unlock();

                        if (finished) //если кадр готов
                        {
                                ctx->lock(); //блокируем камеру
                                ctx->pts[ctx->frame_idx] = original_pts; //устанавливаем отметку времени кадра
                                ctx->frame_idx = !ctx->frame_idx; //меняем номер формируемого кадра
                                //если задан обработчик вызываемый после формирования кадра, то вызываем его
                                if (ctx->cb_fn) ctx->cb_fn(ctx->cb_param);
                                ctx->unlock();

                                if (ctx->frames_count + 1 < 0) ctx->frames_count = 1;
                                else ctx->frames_count++;
                        }
                }
        }
        av_free_packet(&packet);
}


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

Относительно нагрузки на процессор. Основная нагрузка на процессор идет при декодировании потока. Нагрузка от одного потока 1280×720, 24 fps, 2727 kbps для аппаратной конфигурации Intel Core2 Duo E8300 2.83 ГГц составляет порядка 17%. Для сравнения родной ffplay нагружает процессор на такую же величину. Запись видео с камеры практически не нагружает процессор так как идет без перекодирования.

Стерео-видео от двух выделенных IP-камер записывается в один файл (каждой камере соответствует свой номер потока).

Для того что бы в случае нештатного прерывания процесса кодирования запись не была повреждена (не выдавалось сообщение moov atom not found) при инициализации контекста формата для записи выбираем формат который нечувствителен к нештатному прерыванию процесса кодирования

AVOutputFormat *ofmt = av_guess_format( "VOB", NULL, NULL ); //выбираем VOB
ofcx = avformat_alloc_context();
if (!ofcx)
{...}
ofcx->oformat = ofmt;


Сформированный кадр может быть прочитан при помощи следующей функции

//неблокирующий вызов
int ffmpeg_get_bmp_NB_(int _ctx, void *bmp_bits, int w, int h, int br, int co, int sa, void *pts)
{
        ctx_t *ctx = (ctx_t *)_ctx;

        if (!ctx || !ctx->frames_count) return 0; //если нет камеры или нет кадров - выходим

        //если нет BGR-представления кадра или запрошенные размеры не совпадают с текущими, то...
        if (!ctx->x_bgr_frame || ctx->x_bgr_frame->width != w || ctx->x_bgr_frame->height != h)
        {
                //освобождаем контекст масштабирования и BGR-представление кадра

                sws_freeContext(x_bgr_ctx);
                av_free(x_bgr_frame);

                x_bgr_frame = 0;
                x_bgr_ctx = 0;

                //создаем BGR-представление кадра

                ctx->x_bgr_frame = av_frame_alloc();
                if (!ctx->x_bgr_frame)
                {
                        sws_freeContext(x_bgr_ctx);
                        av_free(x_bgr_frame);

                        x_bgr_frame = 0;
                        x_bgr_ctx = 0;

                        return 0;
                }

                ctx->x_bgr_frame->width = w;
                ctx->x_bgr_frame->height = h;

                if 
                (
                        avpicture_fill((AVPicture *)ctx->x_bgr_frame, 0, PIX_FMT_RGB32, w, h) < 0
                )
                {
                        sws_freeContext(x_bgr_ctx);
                        av_free(x_bgr_frame);

                        x_bgr_frame = 0;
                        x_bgr_ctx = 0;

                        return 0;
                }

                //создаем контекст масштабирования
                ctx->x_bgr_ctx = sws_getContext(ctx->codec_ctx->width, ctx->codec_ctx->height, ctx->codec_ctx->pix_fmt, w, h, PIX_FMT_RGB32, SWS_BICUBIC, 0, 0, 0);
                if (!ctx->x_bgr_ctx)
                {
                        sws_freeContext(x_bgr_ctx);
                        av_free(x_bgr_frame);

                        x_bgr_frame = 0;
                        x_bgr_ctx = 0;

                        return 0;
                }
        }

        //настраиваем BGR-представление кадра на переданный указатель на содержимое кадра
        ctx->x_bgr_frame->data[0] = (uint8_t *)bmp_bits;
        ctx->x_bgr_frame->data[0] += ctx->x_bgr_frame->linesize[0] * (h - 1);
        ctx->x_bgr_frame->linesize[0] = -ctx->x_bgr_frame->linesize[0];   

        //меняем яркость, контраст, насыщенность
        int *table;
        int *inv_table;
        int brightness, contrast, saturation, srcRange, dstRange; 
        sws_getColorspaceDetails(ctx->x_bgr_ctx, &inv_table, &srcRange, &table, &dstRange, &brightness, &contrast, &saturation); 
        brightness = ((br<<16) + 50) / 100;
        contrast = (((co+100)<<16) + 50) / 100;
        saturation = (((sa+100)<<16) + 50) / 100;
        sws_setColorspaceDetails(ctx->x_bgr_ctx, inv_table, srcRange, table, dstRange, brightness, contrast, saturation); 

        //масштабируем
        sws_scale(ctx->x_bgr_ctx, ctx->frame[!ctx->frame_idx]->data, ctx->frame[!ctx->frame_idx]->linesize, 0, ctx->codec_ctx->height, ctx->x_bgr_frame->data, ctx->x_bgr_frame->linesize);

        //возвращаем настройки BGR-представления кадра к оригинальным
        ctx->x_bgr_frame->linesize[0] = -ctx->x_bgr_frame->linesize[0]; 

        //отметка времени кадра
        *(int64_t *)pts = ctx->pts[!ctx->frame_idx] * 1000 / ctx->format_ctx->streams[ctx->stream]->time_base.den;

        return 1;
}

//блокирующий вызов
int ffmpeg_get_bmp(int _ctx, void *bmp_bits, int w, int h, int br, int co, int sa, void *pts)
{
        ctx_t *ctx = (ctx_t *)_ctx;

        if (!ctx || !ctx->frames_count) return 0;

        ctx->lock();

        int res = ffmpeg_get_bmp_NB_(_ctx, bmp_bits, w, h, br, co, sa, pts);

        ctx->unlock();

        return res;
}


Отображение видео


Программа выполнена в виде множества окон просмотра каждое из которых ассоциировано с определенной камерой (приложение собиралось в теплом ламповом Borland C++ Builder 6.0 Enterprise Suite).

d07442c0ebe2422c8b68337bdce3d4c2.jpg
Внешний вид программы во время работы (обратите внимание на то, что одна камера — внешняя — висит на кабеле сзади робота; о том КТО попал в кадр читайте далее)


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

Для удобства пользования видимость, позиция, глубина расположения, размер, яркость, контраст, насыщенность и прозрачность каждого окна могут меняться и запоминаются. В окна просмотра поверх кадров полученных от камеры может выводится различная служебная информация: название камеры, размер окна, FPS отображения и записи, состояние буфера вывода, нагрузка процессора, индикаторы записи (с камеры, с экрана, стерео), область видимости для левого и правого глаза (очки) и т.п.

Вывод очередного кадра на экран происходит в обработчике сообщения WM_PAINT окна просмотра. Сообщения WM_PAINT посылаются окну просмотра достаточно замысловатым способом. В начале рассмотрим необходимый для работы служебный функционал. Для получения средней задержки между кадрами используется объект следующего класса

class t_buf_t
{
public:
    int n; //размер циклического буфера
    __int64 *t; //циклический буфер отметок времени
    int idx; //текущий индекс для записи в циклический буфер
    int ttl; //общее количество записанных отметок времени (если буфер не заполнен)

    t_buf_t(void): n(0), t(0), idx(0), ttl(0) {}
    virtual ~t_buf_t(void) {delete [] t;}

    void reset_size(int n)
    {
        delete [] t;
        this->n = n;
        t = new __int64[n];
        idx = 0;
        ttl = 0;
    }
    void set_t(__int64 t) //запись отметки времени в циклический буфер
    {
        this->t[idx] = t;
        idx++;
        if (idx == n) idx = 0;
        if (ttl < n) ttl++;
    }
    __int64 get_dt(void) //расчет средней задержки между кадрами
    {
        if (ttl != n) return 0;
        int idx_2 = idx - 1;
        if (idx_2 < 0) idx_2 = n - 1;
        return (t[idx_2] - t[idx]) / n;
    }
};


Объект этого класса хранит циклический буфер содержащий отметки времени кадров. Для расчета средней задержки между кадрами отметка времени первого кадра циклического буфера вычитается из отметки времени последнего кадра циклического буфера и полученная разница делится на размер циклического буфера.

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

char **form_bmp_bits; //общий буфер для хранения содержимого кадров
HBITMAP *form_bmp_h; //общий буфер для хранения идентификаторов кадров
bool *frame_good; //общий буфер для хранения отметок качества кадров

int frame_buffer_size; //размер буфера (ввода или вывода)
int frame_buffer_size_2; //удвоенный размер буфера (ввода или вывода)
int in_frames_count_ttl; //общее количество полученных кадров (если буфер не заполнен)

int in_frame_idx; //текущий индекс для записи кадров в буфер ввода
int out_frame_idx; //текущий индекс для чтения кадра из буфера вывода

int in_frames_count; //количество кадров в буфере ввода
int out_frames_count; //количество кадров в буфере вывода

int in_base_idx; //базовый индекс буфера ввода (смещение в общем буфере)
int out_base_idx; //базовый индекс буфера вывода (смещение в общем буфере)

int buffer_w, buffer_h; //размер кадра

t_buf_t in_t_buf; //объект используемый для расчета средней задержки между кадрами

__int64 out_dt; //средняя задержка между кадрами

__int64 pts; //отметка времени последнего сформированного кадра
__int64 dpts; //разница отметок времени двух последних сформированных кадров

//события используемые для вывода кадров при размере буфера равном одному кадру
HANDLE h_a;
HANDLE h_b;

//количество отметок времени используемое для расчета средней задержки между кадрами
int n_param;
//поправка к средней задержке между кадрами для предотвращения опустошения/переполнения буфера
int active_dt_param;

void reset_frame_buffer(void)
{
    in_frames_count_ttl = frame_buffer_size == 1? 1 : 0;

    in_frame_idx = 0;
    out_frame_idx = 0;

    in_frames_count = frame_buffer_size == 1? 1 : 0;
    out_frames_count = frame_buffer_size == 1? 1 : 0;

    in_base_idx = 0;
    out_base_idx = frame_buffer_size == 1? 0 : frame_buffer_size;

    //инициализация объекта используемого для расчета средней задержки между кадрами
    in_t_buf.reset_size(n_param);

    out_dt = 20; //начальная задержка между кадрами 20 мс
}
void create_frame_buffer(void)
{
    reset_frame_buffer();

    frame_buffer_size_2 = frame_buffer_size == 1? 1 : frame_buffer_size * 2;

    buffer_w = ClientWidth;
    buffer_h = ClientHeight;

    //общий буфер состоит из двух буферов (ввода и вывода) с увеличенными в два раза размерами
    int n = frame_buffer_size == 1? 1 : frame_buffer_size * 4;

    form_bmp_bits = new char *[n];
    form_bmp_h = new HBITMAP[n];
    frame_good = new bool[n];

    for (int i = 0; i < n; i++)
    {
        BITMAPINFOHEADER form_bmi_hdr;
        form_bmi_hdr.biSize = sizeof(BITMAPINFOHEADER);
        form_bmi_hdr.biWidth = buffer_w;
        form_bmi_hdr.biHeight = buffer_h;
        form_bmi_hdr.biPlanes = 1;
        form_bmi_hdr.biBitCount = 32;
        form_bmi_hdr.biCompression = BI_RGB;
        form_bmi_hdr.biSizeImage = form_bmi_hdr.biWidth * form_bmi_hdr.biHeight * 4;
        form_bmi_hdr.biXPelsPerMeter = 0;
        form_bmi_hdr.biYPelsPerMeter = 0;
        form_bmi_hdr.biClrUsed = 0;
        form_bmi_hdr.biClrImportant = 0;

        form_bmp_h[i] = CreateDIBSection(0, (BITMAPINFO *)&form_bmi_hdr, DIB_RGB_COLORS, (void **)&form_bmp_bits[i], 0, 0);

        frame_good[i] = false;
    }
}
void destroy_frame_buffer(void)
{
    int n = frame_buffer_size == 1? 1 : frame_buffer_size * 4;

    for (int i = 0; i < n; i++)
        DeleteObject(form_bmp_h[i]);

    delete [] form_bmp_bits;
    delete [] form_bmp_h;
    delete [] frame_good;
}


Увеличение размера буферов (ввода и вывода) в два раза по сравнению с заданным делает возможным дрейф средней задержки между кадрами как в положительную так и в отрицательную сторону.

Теперь перейдем непосредственно к процессу вывода. При формировании кадра DLL-посредник вызывает обработчик читающий кадр в буфер ввода

//вспомогательная функция читающая кадр в буфер ввода
bool u_obj_t::get_frame(const bool _NB_, void *pts)
{
    bool res;
    if (_NB_)
    {
        res = ffmpeg_get_bmp_NB_(form->ctx, form->form_bmp_bits[form->in_frame_idx + form->in_base_idx], form->buffer_w, form->buffer_h, form->br, form->co, form->sa, pts);
    }
    else
    {
        res = ffmpeg_get_bmp(form->ctx, form->form_bmp_bits[form->in_frame_idx + form->in_base_idx], form->buffer_w, form->buffer_h, form->br, form->co, form->sa, pts);
    }
    return res;
}

//вспомогательная функция вызываемая обработчиком
void u_obj_t::exec(const bool _NB_)
{
    __int64 pts;
    //если буфер инициализирован и кадр прочитан, то...
    if (form->form_bmp_h[form->in_frame_idx + form->in_base_idx] && get_frame(_NB_, &pts))
    {
        if (pts != form->pts) //если это реально новый кадр (отметки времени разные), то...
        {
            form->frame_good[form->in_frame_idx + form->in_base_idx] = true;

            //вычисляем разницу отметок времени двух последних сформированных кадров...
            //и запоминаем отметку времени последнего сформированного кадра
            form->dpts = pts - form->pts;
            form->pts = pts;

            //записываем отметку времени в объект используемый для расчета средней задержки между кадрами
            form->in_t_buf.set_t(ffmpeg_get_microseconds() / 1000);

            //вычисляем среднюю задержку между кадрами
            form->out_dt = form->in_t_buf.get_dt();
            //ограничиваем задержку на уровне 250 мс
            if (form->out_dt > 250) form->out_dt = 250;

            //увеличиваем текущий индекс для записи кадров в буфер ввода
            form->in_frame_idx++;
            //если буфер ввода заполнен - не меняем текущий индекс для записи кадров в буфер ввода
            if (form->in_frame_idx == form->frame_buffer_size_2) form->in_frame_idx--;

            //увеличиваем количество кадров в буфере ввода
            form->in_frames_count++;

            //пока буфер ввода не заполнен увеличиваем общее количество полученных кадров
            if (form->in_frames_count_ttl != form->frame_buffer_size) form->in_frames_count_ttl++;
        }
    }
    else //кадр не был прочитан
    {
        form->frame_good[form->in_frame_idx + form->in_base_idx] = false;
    }
}

//обработчик
void cb_fn(int cb_param)
{
    Tmain_form *form = (Tmain_form *)cb_param;
    form->u_obj->lock(); //блокируем объект отвечающий за чтение кадров
    form->u_obj->exec(true); //читаем кадр
    form->u_obj->unlock(); //деблокируем объект отвечающий за чтение кадров

    if (form->frame_buffer_size == 1) //при размере буфера равном одному кадру
    {
        ResetEvent(form->h_b); //сбрасываем событие A
        SetEvent(form->h_a); //устанавливаем событие A
        ResetEvent(form->h_a); //сбрасываем событие B
        SetEvent(form->h_b); //устанавливаем событие B
    }
}


Параллельно работает поток посылающий сообщения WM_PAINT окну просмотра с вычисляемым в процессе работы интервалом

//вспомогательная функция вызываемая функцией потока
void __fastcall repaint_thread_t::repaint(void)
{
    form->Repaint(); //здесь будет послано сообщение WM_PAINT окну просмотра

    if (form->frame_buffer_size == 1) //при размере буфера равном одному кадру больше ничего не требуется
    {}
    else //иначе...
    {
        //увеличиваем текущий индекс для чтения кадра из буфера вывода
        form->out_frame_idx++;
        //если буфер вывода пуст или буфер ввода заполнен, то пробуем переключить буфера
        if (form->out_frame_idx >= form->out_frames_count || form->in_frames_count >= form->frame_buffer_size_2)
        {
            form->u_obj->lock();

            if (form->in_frames_count) //если в буфере ввода есть кадры, то...
            {
                //реально переключаем буфера

                //количество кадров которое окажется в буфере вывода
                int n = form->in_frames_count >= form->frame_buffer_size_2? form->frame_buffer_size_2 : form->in_frames_count;

                form->out_frame_idx = 0;
                form->out_frames_count = n;
                form->out_base_idx = form->out_base_idx? 0 : form->frame_buffer_size_2;

                form->in_frame_idx = 0;
                form->in_frames_count = 0;
                form->in_base_idx = form->in_base_idx? 0 : form->frame_buffer_size_2;
            }
            else //иначе придется подождать пока в буфере ввода появиться хотя бы один кадр
            {
                form->out_frame_idx--;
            }

            form->u_obj->unlock();
        }
    }
}

//функция потока
void __fastcall repaint_thread_t::Execute(void)
{
    while (!Terminated)
    {
        if (form->frame_buffer_size == 1) //если размер буфера равен одному кадру, то...
        {
            WaitForSingleObject(form->h_a, 250); //ожидаем событие A в течение 250 мс
            Synchronize(repaint); //вызываем функцию которая пошлет сообщение WM_PAINT окну просмотра
            WaitForSingleObject(form->h_b, 250); //ожидаем событие B в течение 250 мс

            /*таким образом при размере буфера равном одному кадру данный код будет работать
            исключительно от событий без использования задержек что позволит оптимально использовать
            ресурсы процессора при минимальной задержке от момента получения кадра до его вывода на экран*/
        }
        else //иначе выводим кадр и делаем после него задержку для чего...
        {
            //в начале рассчитаем поправку к средней задержке между кадрами...
            //для предотвращения опустошения/переполнения буфера
            form->active_dt_param =
            (

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

            form->frame_buffer_size -
            (form->out_frames_count - form->out_frame_idx + form->in_frames_count)

            /*мы получили количество кадров которого нам не хватает для того что бы у нас в запасе было
            количество кадров равное размеру буфера

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

            поэтому нам надо управлять кадрами хранимыми в запасе стремясь к тому что бы их количество было
            равно размеру буфера (ни больше ни меньше)

            для этого мы будем сводить к нулю полученную разность при помощи поправки к средней задержке
            между кадрами:
            поправка будет положительная для положительной разности
            (нам нужно больше времени что бы получить больше кадров что бы наполнить ими буфер который начал
            опустошаться)
            поправка будет отрицательной для отрицательной разности
            (нам нужно быстрее выводить кадры что бы сбросить их избыток из буфера который начал переполняться)

            для этого полученную разность умножим на среднюю задержку между кадрами и разделим на размер буфера
            тем самым мы получим задержку необходимую для стабилизации размера буфера - это время которое нужно
            добавить к средней задержке между кадрами для того чтобы получить/пропустить рассчитанное нами
            количество кадров за период в течение которого мы получаем количество кадров равное размеру буфера
            (мы могли бы выбрать любой другой период в течение которого мы хотели бы привести количество кадров
            находящихся у нас в запасе к размеру буфера однако этот период ничем не хуже)... уфф :)*/

            ) * form->out_dt / form->frame_buffer_size;

            LONGLONG t = ffmpeg_get_microseconds() / 1000;
            Synchronize(repaint); //вызываем функцию которая пошлет сообщение WM_PAINT окну просмотра
            LONGLONG dt = ffmpeg_get_microseconds() / 1000 - t;

            //вычисляем фактическую задержку между кадрами:
            //к средней задержке между кадрами прибавляем поправку и вычитаем время затраченное на рисование
            int delay = form->out_dt + form->active_dt_param - dt;
            if (delay > 0) Sleep(delay);
        }
    }
}


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

Процедуры непосредственно отвечающие за рисование выглядят следующим образом

mem_c = new TCanvas; //канва для вывода элементов интерфейса поверх кадра естественными средствами
...
//функция заполняющая канву цветом фона
void Tmain_form::draw_bg_img(TCanvas *c, int w, int h)
{
    c->Brush->Style = bsSolid;
    c->Brush->Color = clBlack;
    c->FillRect(TRect(0, 0, w, h));
}

//пример функции выводящей сообщение через канву
void Tmain_form::draw_no_signal_img(TCanvas *c, int w, int h)
{
    AnsiString s = "NO SIGNAL";

    c->Brush->Style = bsClear;
    c->Font->Color = clWhite;
    c->Font->Size = 24;
    c->Font->Style = TFontStyles()<< fsBold;
    c->TextOutA(w / 2 - c->TextWidth(s) / 2, h / 2 - c->TextHeight(s) / 2, s);
}
...
//обработчик сообщения WM_PAINT
void __fastcall Tmain_form::WMPaint(TWMPaint& Message)
{
    if (form_bmp_h[out_frame_idx + out_base_idx]) //если буфер инициализирован
    {
        PAINTSTRUCT ps;
        HDC paint_hdc = BeginPaint(Handle, &ps);

        HDC hdc_1 = CreateCompatibleDC(paint_hdc); //создаем контекст 1
        HDC hdc_2 = CreateCompatibleDC(paint_hdc); //создаем контекст 2
        mem_c->Handle = hdc_1; //настраиваем канву на контекст 1

        HBITMAP h_1 = CreateCompatibleBitmap(paint_hdc, buffer_w, buffer_h); //создаем BMP
        HBITMAP old_h_1 = (HBITMAP)SelectObject(hdc_1, h_1); //ассоциируем контекст 1 с BMP

        //все что мы будем выводить через канву (а значит через контекст 1) будет попадать в BMP

        //ассоциируем контекст 2 с кадром
        HBITMAP old_h_2 = (HBITMAP)SelectObject(hdc_2, form_bmp_h[out_frame_idx + out_base_idx]);
        //копируем кадр в BMP
        BitBlt(hdc_1, 0, 0, buffer_w, buffer_h, hdc_2, 0, 0, SRCCOPY);

        //если включен режим тестирования стерео и форма на которой мы рисуем ассоциирована с левой или...
        //правой камерой (то есть транслирующей изображение для левого или правого глаза), то...
        if (start_form->stereo_test && (start_form->left_form == this || start_form->right_form == this))
        {
            //рисуем через канву тестовое изображение (для левого или правого глаза соответственно)
            mem_c->CopyMode = cmSrcCopy;
            mem_c->StretchDraw(TRect(0, 0, buffer_w, buffer_h), start_form->left_form == this? start_form->left_img : start_form->right_img);
        }
        else
        {
            //если с камерой что-то не так выводим через канву сообщение NO SIGNAL
            if (!ffmpeg_get_status(ctx))
            {
                draw_bg_img(mem_c, buffer_w, buffer_h);
                draw_no_signal_img(mem_c, buffer_w, buffer_h);
            }
            //если в буфере вывода еще нет кадров выводим через канву сообщение ...BUFFERING...
            else if (!out_frames_count)            {
                draw_bg_img(mem_c, buffer_w, buffer_h);
                draw_buffering_img(mem_c, buffer_w, buffer_h);
            }
            //если с кадром что-то не так выводим через канву сообщение NO SIGNAL
            else if (!frame_good[out_frame_idx + out_base_idx])
            {
                draw_bg_img(mem_c, buffer_w, buffer_h);
                draw_no_signal_img(mem_c, buffer_w, buffer_h);
            }
        }
        //если включен режим показа области видимости рисуем через канву соответствующее изображение (очки)
        if (area_type) draw_area_img(mem_c, buffer_w, buffer_h);
        //если включен режим показа информации (размер окна, FPS, задержка, нагрузка процессора и т.п.)...
        //выводим через канву соответствующую информацию
        if (show_ui) draw_info_img(mem_c, buffer_w, buffer_h);

        //копируем изображение из оперативной памяти (BMP) на экран
        BitBlt(paint_hdc, 0, 0, buffer_w, buffer_h, hdc_1, 0, 0, SRCCOPY);

        //удаляем временные объекты

        SelectObject(hdc_1, old_h_1);
        SelectObject(hdc_2, old_h_2);

        DeleteObject(h_1);

        DeleteDC(hdc_1);
        DeleteDC(hdc_2);

        EndPaint(Handle, &ps);
    }
}


Обратите внимание на некоторую избыточность кода — мы рисуем в оперативной памяти в том числе копируя в нее сам кадр который также является объектом расположенным в оперативной памяти (!), в конце рисования изображение копируется из оперативной памяти (BMP) на экран. Почему так? Дело в том, что если рисовать непосредственно на кадре (эй, он ведь тоже расположен в оперативной памяти, давайте рисовать сразу на нем) и в конце рисования так же копировать изображение из него на экран, то при определенных частотах вывода кадров может возникнуть совершенно необъяснимый и неуместный в данном случае эффект мерцания…

Относительно нагрузки на процессор. Вывод кадров на экран практически не нагружает процессор. Основная нагрузка на процессор как и раньше идет при декодировании потока.

Просмотр стерео-видео


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

3330fecfa4ec43b489a8c9e57edd1c69.jpg
Внешний вид программы во время работы в режиме показа области видимости для левого и правого глаза (очки)


Полевые испытания (существо на видео — это форма жизни которая называется налим и он судя по всему сидит в засаде)

Простой просмотр стерео-видео был организован следующим образом:
-в VR шлем помещался смартфон на базе ОС Android;
-на смартфон устанавливалось приложение iDisplay (платно);
-на ПК с программой устанавливался модуль iDisplay (бесплатно);
-смартфон подключался к ПК при помощи USB (требуется ADB драйвер) или Wi-Fi и становился «дополнительным дисплеем» (обратите внимание на кавычки — вскоре вы поймете почему);
-окна просмотра программы перетаскивались на этот «дополнительный дисплей» и настраивались под экран и VR шлем.

Данный способ организации просмотра стерео-видео обладает следующими неоспоримыми преимуществами:
-сравнительно низкая цена решения (VR шлем под смартфон можно приобрести по цене от 0 USD:), приложение iDisplay для смартфона обойдется вам в 5 USD);
-простота настройки — подключил и работай.

Теперь о недостатках:
-«дополнительный дисплей» — это ничуть не дисплей в нормальном понимании этого слова — то есть это не подключение устройства как дисплея — нет, смартфон остается смартфоном, а ПК остается ПК и как бы разработчики iDisplay не лезли из кожи вон, но на тестах была видна одна и та же картина: для аппаратной конфигурации Intel Core2 Duo E8300 2.83 ГГц iDisplay забирает 50% ресурсов процессора (ровно одно ядро; например, для процессора Intel Core2 Quad Q9400 2.66 ГГц у которого четыре ядра нагрузка будет равна 25%) обеспечивая транспорт картинки, что конечно же очень печально; кроме того в некоторых трудновоспроизводимых случаях наблюдалось очень кратковременное (но заметное глазом) замирание картинки с периодом порядка одной секунды — iDisplay явно что-то мудрит с буферизацией или просто не успевает доставить кадры (пропускает те которые не были переданы за некий интервал сразу перескакивая к текущему); плюс ко всему некоторые комбинации настроек iDisplay могут дать задержку между реальным изменением картинки и отображением этих изменений на «дополнительном дисплее»;
-картинка не может соперничать с профессиональными VR решениями, но и не настолько плоха что бы не рассматривать этот вариант.

Запись видео с экрана


Запись видео с экрана реализована достаточно просто: программа создает поток который с заданной частотой делает снимок экрана и передает его DLL-посреднику для записи. Код потока выглядит следующим образом

void scr_rec_thread_t::main(void) //функция потока
{
    LONGLONG t = ffmpeg_get_microseconds() / 1000;

    HBITMAP old_h = (HBITMAP)SelectObject(scr_bmp_hdc, scr_bmp_h);
    if (!BitBlt //захватываем картинку
    (
        scr_bmp_hdc, //HDC hdcDest
        0, //int nXDest
        0, //int nYDest
        scr_w, //int nWidth
        scr_h, //int nHeight
        scr_hdc, //HDC hdcSrc
        scr_x, //int nXSrc
        scr_y, //int nYSrc
        CAPTUREBLT | SRCCOPY //DWORD dwRop
    ))
    {}
    SelectObject(scr_bmp_hdc, old_h);

    ffmpeg_rec_bmp(scr_rec_ctx, scr_bmp_bits); //записываем карт
    
            

© Habrahabr.ru