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


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

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

Moby Dick


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


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


Глубоководный робот 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 x 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).


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


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

Для удобства пользования видимость, позиция, глубина расположения, размер, яркость, контраст, насыщенность и прозрачность каждого окна могут меняться и запоминаются. В окна просмотра поверх кадров полученных от камеры может выводится различная служебная информация: название камеры, размер окна, 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-камер организован следующим образом: любая камера может быть назначена левой или правой (то есть транслирующей изображение для левого или правого глаза). После этого положение и размер окон настраиваются в ручную или с помощью одного из предопределенных профилей таким образом что бы обеспечить удобный просмотр на выбранных вами технических средствах. При необходимости может быть включен режим показа области видимости для левого и правого глаза (очки).


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


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

Простой просмотр стерео-видео был организован следующим образом:
-в 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); //записываем картинку

    /*задержка между картинками рассчитывается по заданному FPS однако для предотвращения
    перегрузки процессора значение задержки ограничивается снизу заданной минимальной задержкой
    между кадрами*/

    LONGLONG dt = ffmpeg_get_microseconds() / 1000 - t;
    int ms_per_frame = 1000 / scr_rec_max_fps;
    if (dt < ms_per_frame)
    {
        dt = ms_per_frame - dt;
        if (dt < scr_rec_min_delay) dt = scr_rec_min_delay;
    }
    else
    {
        dt = scr_rec_min_delay;
    }
    Sleep(dt);

    //раз в секунду рассчитываем FPS записи с экрана
    frames_count++;
    if (ffmpeg_get_microseconds() / 1000 - start > 1000)
    {
        start = ffmpeg_get_microseconds() / 1000;
        fps = frames_count;
        frames_count = 0;
    }
}

scr_rec_thread_t *scr_rec_thread; //объект-поток

//положение и размеры картинки
int scr_x;
int scr_y;
int scr_w;
int scr_h;
HDC scr_hdc; //контекст экрана
HDC scr_bmp_hdc; //контекст BMP
char *scr_bmp_bits; //содержимое захваченной картинки
HBITMAP scr_bmp_h; //идентификатор BMP

//инициализируем запись с экрана
void scr_rec_init(void)
{
    //создаем объект в DLL-посреднике отвечающий за запись
    scr_rec_ctx = ffmpeg_alloc_rec_ctx();

    //получаем положение и размеры картинки
    scr_x = GetSystemMetrics(SM_XVIRTUALSCREEN);
    scr_y = GetSystemMetrics(SM_YVIRTUALSCREEN);
    scr_w = GetSystemMetrics(SM_CXVIRTUALSCREEN);
    scr_h = GetSystemMetrics(SM_CYVIRTUALSCREEN);

    //создаем контекст экрана и BMP
    scr_hdc = GetDC(0);
    scr_bmp_hdc = CreateCompatibleDC(scr_hdc);

    //создаем BMP

    BITMAPINFOHEADER scr_bmi_hdr;
    scr_bmi_hdr.biSize = sizeof(BITMAPINFOHEADER);
    scr_bmi_hdr.biWidth = scr_w;
    scr_bmi_hdr.biHeight = scr_h;
    scr_bmi_hdr.biPlanes = 1;
    scr_bmi_hdr.biBitCount = 32;
    scr_bmi_hdr.biCompression = BI_RGB;
    scr_bmi_hdr.biSizeImage = scr_bmi_hdr.biWidth * scr_bmi_hdr.biHeight * 4;
    scr_bmi_hdr.biXPelsPerMeter = 0;
    scr_bmi_hdr.biYPelsPerMeter = 0;
    scr_bmi_hdr.biClrUsed = 0;
    scr_bmi_hdr.biClrImportant = 0;

    scr_bmp_h = CreateDIBSection(0, (BITMAPINFO *)&scr_bmi_hdr, DIB_RGB_COLORS, (void **)&scr_bmp_bits, 0, 0);

    scr_rec_thread = new scr_rec_thread_t(); //создаем объект-поток
}

//освобождение ресурсов
void scr_rec_uninit(void)
{
    delete scr_rec_thread;

    DeleteObject(scr_bmp_h);
    DeleteDC(scr_bmp_hdc);
    DeleteDC(scr_hdc);

    ffmpeg_free_ctx(scr_rec_ctx);
}

int scr_rec_ctx;

Как видим все достаточно просто. Более интересен код DLL-посредника отвечающий за запись
//класс отвечающий за запись
class rec_ctx_t:
public mt_obj
{
public:
	AVFormatContext *ofcx; //контекст формата
	AVStream *ost; //поток
	LONGLONG start; //время начала записи
	AVPacket pkt; //пакет

 	AVFrame *frame; //кадр YUV420P
	AVFrame *_frame; //кадр RGB32
	/*не ищите противоречий в ранее использованном термине BGR-представление кадра и используемом в этом
	коде термине RGB32-представление кадра - это одно и то же просто ранее акцент делался на расположение
	компонентов цвета в BMP, а сейчас акцент делается на имена форматов пикселя используемые FFMPEG*/
	uint8_t *_buffer; //буфер
	//контекст масштабирования (в данном случае используется для преобразования кадра из RGB32 в YUV420P)
	SwsContext *ctx;
	//ID кодека для записи - выбирайте, но не увлекайтесь, некоторые кодеки могут требовать задания...
	//дополнительных параметров которые здесь не устанавливаются
	int codec_id;
	//профиль для записи - позволяет выбрать качество и скорость кодирования
	char *preset;
	//CRF для записи - позволяет выбрать качество и скорость кодирования, но несколько иным образом...
	//(подробнее см. документацию FFMPEG)
	int crf;

	rec_ctx_t(void):
		ofcx(0),
		ost(0),

		frame(0),
		_frame(0),
		_buffer(0),
		ctx(0),

		codec_id(CODEC_ID_H264),
		preset(strdup("ultrafast")),
		crf(23)
	{
		memset(&start, 0, sizeof(start));
		av_init_packet( &pkt ); 
	}
	virtual ~rec_ctx_t()
	{
		ffmpeg_stop_rec2((int)this);
		free(preset);
	}
	void clean(void) //освобождение ресурсов
	{
		sws_freeContext(ctx);
		av_free(_buffer);
		av_free(frame);
		av_free(_frame);

		if (ost) avcodec_close( ost->codec );
		avformat_free_context( ofcx );

		ofcx = 0;
		ost = 0;
		memset(&start, 0, sizeof(start));
		av_init_packet( &pkt ); 

		frame = 0;
		_frame = 0;
		_buffer = 0;
		ctx = 0;
	}
	int prepare(char *dst, int w, int h, int den, int gop_size) //подготовка объекта
	{
		//создаем и настраиваем контекст формата
		AVOutputFormat *ofmt = av_guess_format( "VOB", NULL, NULL ); //выбираем VOB
		if (!ofmt)
		{
			clean();
			return 0;
		}
		ofcx = avformat_alloc_context();
		if (!ofcx)
		{
			clean();
			return 0;
		}
		ofcx->oformat = ofmt;

		//ищем кодек, создаем и настраиваем поток
		AVCodec *ocodec = avcodec_find_encoder( (AVCodecID)codec_id );
		if (!ocodec || !ocodec->pix_fmts || ocodec->pix_fmts[0] == -1)
		{
			clean();
			return 0;
		}
		ost = avformat_new_stream( ofcx, ocodec );
		if (!ost)
		{
			clean();
			return 0;
		}
		ost->codec->width = w;
		ost->codec->height = h;
		ost->codec->pix_fmt = ocodec->pix_fmts[0];
		ost->codec->time_base.num = 1; 
		ost->codec->time_base.den = den; 
		ost->time_base.num = 1;
		ost->time_base.den = den;
		ost->codec->gop_size = gop_size; 
		if ( ofcx->oformat->flags & AVFMT_GLOBALHEADER ) ost->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
		AVDictionary *opts = 0;
		av_dict_set(&opts, "preset", preset, 0); //устанавливаем профиль
		char crf_str[3];
		sprintf(crf_str, "%i", crf); //устанавливаем CRF
		av_dict_set(&opts, "crf", crf_str, 0);
		if (avcodec_open2( ost->codec, ocodec, &opts ) != 0)
		{
			clean();
			return 0;
		}

		//создаем и настраиваем кадры, буфер и контекст масштабирования
		frame = av_frame_alloc();
		_frame = av_frame_alloc();
		if (!frame || !_frame)
		{
			clean();
			return 0;
		}
		frame->format = PIX_FMT_RGB32;
		frame->width = w;
		frame->height = h;
		_frame->format = PIX_FMT_YUV420P;
		_frame->width = w;
		_frame->height = h;
		int _buffer_size = avpicture_get_size(PIX_FMT_YUV420P, w, h);
		if (_buffer_size < 0)
		{
			clean();
			return 0;
		}
		uint8_t *_buffer = (uint8_t *)av_malloc(_buffer_size * sizeof(uint8_t));
		if (!_buffer)
		{
			clean();
			return 0;
		}
		if 
		(
			avpicture_fill((AVPicture *)frame, 0, PIX_FMT_RGB32, w, h) < 0 ||
			avpicture_fill((AVPicture *)_frame, _buffer, PIX_FMT_YUV420P, w, h) < 0
		)
		{
			clean();
			return 0;
		}
		ctx = sws_getContext(w, h, PIX_FMT_RGB32, w, h, PIX_FMT_YUV420P, SWS_BICUBIC, 0, 0, 0);
		if (!ctx)
		{
			clean();
			return 0;
		}

		//открываем файл
		if (avio_open2( &ofcx->pb, dst, AVIO_FLAG_WRITE, NULL, NULL ) < 0)
		{
			clean();
			return 0;
		}

		//пишем заголовок
		if (avformat_write_header( ofcx, NULL ) != 0)
		{
			avio_close( ofcx->pb );
			clean();
			return 0;
		}

		//запоминаем время начала записи
		start = get_microseconds();

		return 1;
	}
};

//функция запускающая запись
int ffmpeg_start_rec2
(
    int rec_ctx,
    char *dst, int w, int h, int den, int gop_size
)
{
	rec_ctx_t *ctx = (rec_ctx_t *)rec_ctx;

	if (!ctx) return 0;

	ffmpeg_stop_rec2(rec_ctx); //остановим

	return ctx->prepare(dst, w, h, den, gop_size);
}

//функция останавливающая запись
void ffmpeg_stop_rec2(int rec_ctx)
{
	rec_ctx_t *ctx = (rec_ctx_t *)rec_ctx;

	if (!ctx || !ctx->ofcx) return;

	//завершаем и закрываем файл
	av_write_trailer( ctx->ofcx );
	avio_close( ctx->ofcx->pb );

	ctx->clean();
}

//функция записывающая картинку
int ffmpeg_rec_bmp(int rec_ctx, void *bmp_bits)
{
	rec_ctx_t *ctx = (rec_ctx_t *)rec_ctx;

	if (!ctx) return 0;

	//настраиваем RGB32-представление кадра на переданный указатель на содержимое кадра 
	ctx->frame->data[0] = (uint8_t *)bmp_bits;
	ctx->frame->data[0] += ctx->frame->linesize[0] * (ctx->frame->height - 1);
	ctx->frame->linesize[0] = -ctx->frame->linesize[0];  
	//преобразуем RGB32-представление кадра в YUV420P-представление кадра
	sws_scale(ctx->ctx, ctx->frame->data, ctx->frame->linesize, 0, ctx->frame->height, ctx->_frame->data, ctx->_frame->linesize);
	//возвращаем настройки RGB32-представления кадра к оригинальным
	ctx->frame->linesize[0] = -ctx->frame->linesize[0];   

	//формируем отметку времени показа от начала записи в заданных частотах
	LONGLONG dt = (get_microseconds() - ctx->start) / 1000;
	ctx->_frame->pts = dt * ctx->ost->time_base.den / 1000;

	//кодируем
	int got_packet;
	if (avcodec_encode_video2(ctx->ost->codec, &ctx->pkt, ctx->_frame, &got_packet) != 0) return 0;

	//если пакет сформирован сбрасываем его в файл
	int res = 1;
	if (got_packet) 
	{
		res = av_interleaved_write_frame( ctx->ofcx, &ctx->pkt ) == 0? 1 : 0;
		av_free_packet( &ctx->pkt );
	}
	return res;
}

Следует отметить, что запись с экрана (1280 x 1024, 25 fps) для аппаратной конфигурации Intel Core2 Duo E8300 2.83 ГГц потребляет порядка 30% ресурсов процессора поэтому следует разумно подходить к выбору FPS, минимальной задержки между кадрами, профиля для записи и аппаратного обеспечения на котором вы собираетесь запускать программу.

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

Для тех кого заинтересовала тематика имеется ссылка на мою предыдущую статью схожей направленности:
Делаем систему видеонаблюдения.
Поделиться с друзьями
-->

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


  1. grieverrr
    17.05.2016 11:13
    +4

    Очень круто, но ничего не понятно. А какая задача-то решалась в итоге? Просмотр видео с IP камер? Почему не подходили типовые варианты? Какие были начальные условия и ограничения? Какие нюансы передачи информации с глубины? Что именно делает ваш код? Зачем вообще эти простыни кода в теле статьи? Циклический буфер, метки высокой точности, запись с экрана, ок, ладно.
    *картинка про троллейбус из хлеба жпг*


    1. vxg
      17.05.2016 11:36

      Все что вы спрашиваете перечислено в начале статьи.

      Задача одной фразой: смотреть и записывать видео-поток с IP-камер робота.

      Типовых вариантов нет. Если конечно вы не имеете ввиду стандартный софт от производителя камеры или доступ к камере через web-интерфейс или различные решения которые в основной массе рассчитаны под задачи видеонаблюдения.

      Начальные условия одной фразой: есть IP-камеры передающие RTSP-поток.

      Ограничения одной фразой: их нет. Разве что поместится в производительность процессора, но это не цель разработки, а именно ограничение нашего физического мира.

      Нюансы передачи информации с глубины на данную разработку не влияют. Разве что упомянутое в начале ограничение на пропускную способность кабеля не позволяющее передавать сразу 9 потоков в качестве Full HD.

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


      1. grieverrr
        17.05.2016 11:44
        +1

        Дело в том, что несколько лет назад на старой работе передо мной стояли схожие задачи — писать, транскодить и ретранслировать потоки с IP камер. Возможно я сейчас не упомню всех нюансов, но задача это решалась слегка перекомпиленным ffmpeg и стрим-сервером fms, wowza. Вот я и не понимаю, без вникания в глубины глубин кода, что именно вы сделали такого, чего не было сделано до вас.
        И про пропускную способность кабеля не очень понятно, кабель — 100мбит ethernet?


        1. vxg
          17.05.2016 11:51

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


          1. grieverrr
            17.05.2016 11:54
            +1

            аа, всё, я понял, yet another обвязка вокруг ffmpeg. с третьего раза дошло. про кабель только расскажите, допустим глубина 11 км, что за кабель будет использоваться?


            1. vxg
              17.05.2016 12:03

              На таких глубинах этот аппарат пока не работал. Кабель по-честному лично я в руках не держал — для того что бы писать софт этого не требуется. А вообще кабель представляет из себя многооболочную антипетлевую конструкцию, усиленную кевларом, пропитанным водоостанавливающим составом, имеет нулевую плавучесть, выдерживает поперечное и продольное давление до 60 атм (даже в случае повреждения внешней оболочки). Усилие на разрыв кабеля до 350 кг.


              1. ToSHiC
                17.05.2016 12:47
                +1

                При этом, судя по вашим комментариям в тексте, для передачи используется не оптика, а медь. Не знаете, почему так?


                1. vxg
                  17.05.2016 12:56
                  +1

                  Оптика нужна если длина более 1 км, а так — с ней серьезные проблемы в походных условиях, при ремонте кабеля, разъем сложнее и сам кабель на порядок дороже.


        1. vxg
          17.05.2016 13:13
          +1

          Нет, это не ethernet кабель. Транспорт данных идет по тем же проводам по которым подается питание к агрегатам робота (450 В) через модемы работающие на частотах 10-50 МГц (сразу на нескольких частотах одновременно). Пропускная способность кабеля зависит от его длины. На 100 м можно достичь 50 Мб/с, а на 1 км уже 0,5 Мб/с.


          1. dkv
            17.05.2016 19:01

            Понимаю, что тут вопрос не совсем к вам, но но интересно было узнать, что это за модемы. Ибо стандартные модемы VDLS2 на 8a-Bandplan, ограниченные диапазоном в 8,5 Мгц, способны протащить порядка 10 Мбит/с в нисходящем потоке при длине линии в 2,5 км на кабеле ТПП 0,5 (от диапазона остаются рабочими на данной длине только 3 Мгц). Да, здесь иные напряжения в кабеле и иные уровни помех, но странно, что выбранные под это дело модемы обеспечивают столь малую дальность передачи. через 1 Мбит/с три FullHD картинки не протащить (да ещё и учётом ретрансмитов).


            1. vxg
              17.05.2016 19:27

              К сожалению про модемы большего чем рассказал не знаю


  1. AlexLeonov
    17.05.2016 11:29
    +1

    Хабр торт!

    P.S. Но ссылки на гитхаб выглядели бы лучше, конечно, чем столько кода в тексте.


    1. vxg
      17.05.2016 11:37

      К сожалению пока не открыл для себя этот несомненно полезный ресурс.


      1. AlexLeonov
        17.05.2016 11:44
        +1

        Прекрасный повод начать!


  1. saboteur_kiev
    17.05.2016 13:50
    +2

    Так а на какую глубину погружался аппарат?
    На какую глубину рассчитан — в начале статьи указано, что максимальные глубины мирового океана, но даже 1 км — уже кабель не рассчитан…

    Почему часто упоминается нагрузка на процессор? Там же водяное охлаждение, процессор можно легко поставить помощнее, а если 6е поколение интела, то как раз с декодированием видео он справляется на ура, а по питанию раза в два экономнее


    1. vxg
      17.05.2016 14:07
      -1

      Во время описанных в статье испытаний аппарат погружался в р. Волга — там глубину сложно назвать большой, можно попробовать отыскать конкретное значение глубины среди индикаторов интерфейса управления на первом видео, думаю до 5 м. Вообще данная модель рассчитана на глубину до 600 м (при некоторой доработке — до 2 км). Фраза «максимальные глубины мирового океана» в статье не использовалась.

      Нагрузка на процессор как и модели процессоров упоминаются в статье для специалистов что бы они могли сопоставить сколько ресурсов потребляет тот или иной участок кода и приблизительно оценить хватит ли ресурсов того или иного оборудования для решения похожей задачи. Безусловно в идеальном мире где человек может позволить себе для любого чиха устанавливать дата центр с 1024 майнфреймами охлаждаемыми жидким гелием это знание никак не пригодится.


      1. saboteur_kiev
        17.05.2016 14:26
        +2

        Как это не использовалась?

        «Основной целью при проектировании и создании Moby Dick было создание небольшого робота, способного погружаться на максимальные глубины мирового океана (в составе подъемно-спускового механизма), выдавать видео высокого разрешения (Full HD), брать пробы, образцы, проводить спасательные работы.»


        1. vxg
          17.05.2016 14:32

          Да, пропустил фразу, вы правы. Тут вся тонкость в многогранности русского языка. Глядите как нужно читать это предложение. Есть проект. Конечной целью этого проекта как и любого другого является абсолют — то есть некая сущность являющаяся конечным благом для всей Вселенной. Однако движение к цели процесс сложный и длительный. Moby Dick создали как первый этап движения к этой цели. Он может погружаться на глубины 600-2000 м (эти глубины нельзя назвать максимальными, но и малыми назвать я бы не решился). Однако в перспективе, развивая направление и аккумулируя опыт, лаборатория планирует получить аппарат с глубиной погружения до 12 км.


          1. grieverrr
            17.05.2016 16:07
            +4

            Ну то есть всё как обычно — заявлено одно, а при демонстрации результата включаются «тонкости многогранности русского языка»


            1. vxg
              17.05.2016 16:14
              +1

              Совершенно согласен с вами исправил двусмысленность в статье добавив "(в перспективе)"


          1. SidMeier
            17.05.2016 16:47
            +2

            Вы точно уверены, что Ваш «подводный аппарат» способен погрузиться хотя бы на 600 метров?
            Практика показывает, что даже подводные аппараты с более «продуманным» конструктивом — не способны на подобное.
            Так, например, наш аппарат TurtleROV мы позиционируем только до глубин в 400 м.
            Кроме того, отдельный интересный вопрос — это передача питания на аппарат.
            Вы представляете себе сечение проводов питания, чтобы «догнать» напряжение до глубины в 2 км.?
            А вообще — мечтать не вредно :)


            1. vxg
              17.05.2016 16:50

              1 Добрый день коллеги
              2 Это не мой аппарат, читайте внимательнее статью.
              3 По техническим вопросам и обмену опытом именно в части аппарата вы можете связаться с лабораторией — ссылка есть в статье.
              4 У меня хорошее воображение, я рад что есть люди которые как и я думают на подобные интересные темы.
              5 Согласен, мечтать погрузиться до глубин 400 м на вашем аппарате тоже смело ;)


            1. SidMeier
              17.05.2016 16:58
              +2

              ссылка не вставилась: https://www.youtube.com/watch?v=b_2BZ2qjg2w


              1. vxg
                17.05.2016 19:34

                Видео понравилось. Робот тоже. Здорово плавает!


      1. saboteur_kiev
        17.05.2016 14:40
        +2

        «Безусловно в идеальном мире где человек может позволить себе для любого чиха устанавливать дата центр с 1024 майнфреймами охлаждаемыми жидким гелием это знание никак не пригодится. „

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


        1. vxg
          17.05.2016 14:45
          -1

          Робот питается по кабелю. Процессор который упоминается в статье стоит в компьютере с которого управляется робот. То есть стоит наверху, а не в роботе. У робота, конечно, есть своя система для управления его агрегатами расположенная на борту, но это не моя область и я про эту систему практически ничего не знаю. Комментарий про стоимость не понял, наверное пока я писал статью отменили деньги. Охлаждать процессоры морской водой с частицами… на глубине… нет, о таком не слышал.


          1. saboteur_kiev
            17.05.2016 17:49
            +2

            А если принимать информацию от камер на глубине, там обрабатывать и по кабелю передавать уже пережатый поток — пропускная способность могла бы увеличиться, и можно было бы использовать несколько камер в FULL HD режиме?

            Ваш комментарий про морскую воду с частицами не понял. Главное во всем этом — отвод тепла. В воде прохладно, далеко отводить не нужно…


            1. vxg
              17.05.2016 18:21

              Почему-то логика подсказывает мне что пережать несколько Full HD в сигнал который который пройдёт там где такой поток информации не проходит нельзя. Или это будет уже не Full HD. Кодеки на IP-камерах достаточно совершенные — улучшить сжатие без потерь выряд ли удастся, а технических проблем добавится я думаю прилично.

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


              1. ZyXI
                17.05.2016 18:58
                +1

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


                1. vxg
                  17.05.2016 19:24

                  Честно говоря по настойчивости с которой предлагалось охлаждение морской водой подумал именно о дикости с отмыванием процессора)


                  1. saboteur_kiev
                    17.05.2016 19:56

                    Извините, я не настойчивый, просто в меру любопытный ;)

                    Что же касается камер — x264 требует в общем-то неслабого процессора, не уверен, что в камерах стоит мощный. А сжатие отличается в разы… Ну и вы же сами пишете, что у вас Full HD одна камера, остальные 720 — я так понял это именно из-за нехватки канала передачи


                    1. vxg
                      17.05.2016 20:02
                      +1

                      Камеры отдают 264 поток. Ограничение — это для максимальной комплектации (когда 9 камер).


                    1. dkv
                      17.05.2016 20:29
                      +1

                      Поверьте, в IP-камерах стоят специализированные SoC, которые именно для этого и предназначены. Зачастую те-же SoC, что стоят в авторегистраторах и экшн-камерах, зачастую и более производительные. Да, можно взять камеры Prosilica от Alliedvision (https://www.alliedvision.com/en/products/cameras.html) или им подобные и подобрать более удачные параметры кодирования, придумать, как разместить и как питать ещё и отдельно стоящий видеосервер на этом агрегате. Но сколько будет стоить такая система? Время разработки тут не считаем, поскольку у этих камер отличное SDK и документация и получить с них изображение дело пары часов.


            1. dkv
              17.05.2016 18:39
              +1

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


  1. dreamer-dead
    17.05.2016 13:57
    +4

    Относительно кода, мне бы хотелось порекомендовать автору несколько вещей:
    — Взять нормальный компилятор с поддержкой более новых стандартов. Например, бесплатную MSVS 2015, если так хочется именно Windows и продукты от MS.
    Это позволит не городить велосипеды для работы со временем и потоками. Велосипеды тут плохи тем, что они врядли лучше кода в STL, а стороннему человеку вовсе не будут нужны.
    — Почитать про RAII
    Сюда же отнесу использование std::string/std::vector вместо ручного strdup + free.
    — Именовать переменные согласно их назначения, профит очевиден.

    AVFrame *frame; //кадр YUV420P
    AVFrame *_frame; //кадр RGB32
    

    Насколько проще было бы именовать
    AVFrame *yuv_frame;
    AVFrame *rgb_frame;
    

    И комментарий не нужен, и код читать намного проще.
    — Выложить код на github, чтобы не постить такие простыни кода.

    Да, еще вопрос — зачем нужен динамический массив в классе t_buf_t?
    Ведь там используется всегда только 2 последних сохраненных значения времени?


    1. vxg
      17.05.2016 14:26
      -4

      1 То что вам не нравятся WinAPI вызовы производимые для поддержки отметок времени и задержек высокой точности или создающие поток не означает что их нет в конкретной реализации тех самых классов ссылки на которые вы дали.
      2 Вашу ссылку на RAII я не понял — вам не нравится что код DLL выполнен в стиле функциона?льного программи?рования? Ну как бы это DLL вообще то. Туда конечно можно и класс впихнуть, но мне это не нужно.
      3 Я знаю что такое std::string/std::vector — здесь они не требуются
      4 Относительно именования переменных полностью с вами согласен в данном месте вольность допущена исключительно по историческим причинам. Комментарии в любом случае были добавлены только в статью — в коде, пардон, их нет вообще :)
      5 Если вы работаете с github это не означает что другие тоже работают с github. Я не вижу криминала в выкладывании тут кода если он есть проинформируйте меня что было сделано неверно в том что я выложил тут наиболее интересные на мой взгляд участки кода? Я же не впихнул сюда просто склеенный из всех исходников текстовый ужас.
      6 Динамический массив нужен потому что размер заранее неизвестен. Используются два последних значения, но они каждый раз новые так как пополнение массива новым значением вытесняет самое старое значение.


      1. QtRoS
        17.05.2016 16:01
        +2

        Отрицание это первая стадия...
        Я соглашусь с товарищем dreamer-dead — пока читал статью возникали примерно те же впечатления, но просто они не оформились в комментарий. Зачем в наше время так много платформозависимого WinAPI, ведь C++ эволюционировал и в стандартной библиотеке уже есть отличные инструменты многопоточности и синхронизации, например?
        По поводу RAII — у Вас повсюду «честный» С-шный код с кучей очисток в конце функций. Справедливое замечание, и тип выходного файла (DLL) тут не при чем.


        1. vxg
          17.05.2016 16:12
          -1

          Я уже нахожусь на стадии Принятие…
          Не нравится мне «современный» стандарт языка. Не признаю я его. Причины? Религиозного характера. Только вам их открою — нет нормального инструмента разработки под этот стандарт. Нормального в моем сугубо личном понимании. А ковыряться в том что считаешь ненормальным нормально ли для нормального человека)?

          От кучи очисток в конце функций куда вы предлагаете мне деться если я оборачиваю «честный» C-шный код FFMPEG? Или вы его тоже считаете адовым?


          1. DarkEld3r
            18.05.2016 13:07

            нет нормального инструмента разработки под этот стандарт.

            И чем же более новые и бесплатные (причём без ограничения на плагины) версии Visual Studio хуже, чем "старая добрая Microsoft Visual C++ 2008 Express Edition"?


            От кучи очисток в конце функций куда вы предлагаете мне деться если я оборачиваю «честный» C-шный код FFMPEG?

            Дык, вам предлагают использовать RAII.


            1. vxg
              18.05.2016 13:14

              Обсуждение достоинств и недостатков того или иного инструмента уведет нас в совсем ненужные дебри. Для меня достаточно того что «новые и бесплатные версии» жрут мое время — я должен выкачать все эти гигабайты, войти в учетную запись и… получить продукт который Hello world собирает аномальное количество времени и то после того как ему перебрали половину двигателя. Я уверен что это не ваш случай и на вашем оборудовании и с вашим опытом все работает хорошо.

              Относительно RAII покажите мастер-класс: выделите проблемный участок и приведите ваше видение его реализации. Может мы просто не понимаем друг друга.


  1. BalinTomsk
    17.05.2016 17:26
    -1

    --Дело в том, что если рисовать непосредственно на кадре

    я делал похожую задачу для одной компании что пишет для голивуда софт типа диспечерского места.

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

    Никакого программирования не нужно для вашей задачи.


    1. vxg
      17.05.2016 17:37
      -1

      Как было уже отвечено выше «я конечно же догадываюсь что все что сделано можно сделать… просто склеив тучу софта скотчем и бинтами. Однако у всякого готового решения есть цена. Вы должны знать что склеить, как склеить, как сделать так что бы не расклеилось и куда бежать когда нужно будет из фундамента этой конструкции вынуть кирпич» Я знаю свой код и могу сделать с ним все что нужно если это вообще возможно. А вот сколько пробежит на этой дистанции человек жонглирующий оркестром из внешнего софта я сказать не берусь. FFMPEG как и многие другие решения которые можно объединить для решения этой задачи очень эффективные и мощные. Но во многих случаях для элементарного действия без кода вам придется забивать дрелью шуруп в гранитную стену.


    1. dkv
      17.05.2016 18:44
      +1

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


      1. vxg
        17.05.2016 19:13

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

        О том чего достаточно и недостаточно для моей задачи я с удовольствием поговорю с вами в любое удобное для нас обоих время)


        1. dkv
          17.05.2016 19:41
          +1

          Моё вышестоящее сообщение было адресовано BalinTomsk. Для вас в нём содержится разве что объяснение причин мерцания, с которым вы столкнулись. Моё сообщение, которое вы потеряли, вы случайно отклонили при модерации, поэтому я первую часть вопроса по модемам продублировал выше. «я буду затирать любое окно покрывающее область рисования в том числе и элементы интерфейса» — вот тут я сильно сомневаюсь. Насколько помню, если вы получаете HDC от HANDLE конкретного элемента формы, то вы просто рисуете на нём. Все вышестоящие элементы должны остаться неповреждёнными, об этом думает винда, это её забота. Вы сможете из закрасить только в случае, если получите DeviceСontext от HANDLE, равного нулю, и будете на нём рисовать. Обработка WM_PAINT полезна в случае, если вам нужно перерисовать лишь часть вашего элемента, поскольку так можно получить конкретный регион, требующий перерисовки (который был перекрыт вышестоящим окном). В случае рисования в реальном времени от неё лучше уходить, кмк. Т.е. в repaint() вам вместо посыла форме сообщения на отрисовку можно было было бы сразу делать рендер, которые вы делаете в WM_PAINT. Но, задача решена, если все всех устраивает — то и замечательно.


          1. vxg
            17.05.2016 20:05

            Почему было мерцание я так и не понял — все рисовалось в памяти и только потом копировалось на экран. Об остальном подумаю, интересный ход мыслей.


            1. dkv
              17.05.2016 20:16
              +1

              В любом случае с рисованием средствами GDI через равные интервалы вам не уйти от возможных разрывов изображения на две части на экране, связанных с отсутствием контроля вертикальной синхронизации. На записи этого не видно поскольку вы просто записываете то, что отрендерили, но при наблюдении в реальном времени наверняка этот эффект прослеживается. По хорошему, вам нужно максимально уйти от временных меток и за опорный сигнал брать вертикальную синхронизацию. Как это лучше сделать сейчас — не подскажу, первый и последний раз делал это давным давно силами DirectDraw, а его успешно закопали. Попробуйте поэкспериментировать с LAV Filters, возможно добьётесь ещё и аппаратного декодирования видео.


              1. vxg
                17.05.2016 22:08

                Почему то у меня отложилось в памяти что проблемы синхронизации исчезли вместе с мониторами отличными от LCD… Или такое понятие и сама проблема все еще имеется?


                1. dkv
                  17.05.2016 22:57
                  +1

                  Имеется, как ни странно. Если у вас его не наблюдается — ну и отлично. Если наблюдается — знаете, куда копать. Но, привязавшись именно к частоте обновления экрана, вы смоли бы получить наиболее плавный вывод картинки.


                  1. vxg
                    17.05.2016 23:18

                    Отличный путь, попробую исследовать это направление!


  1. victor1234
    17.05.2016 18:43

    Почему выбрали именно ffmpeg, который уже давно libav, а не gstreamer, например?


    1. ToSHiC
      17.05.2016 18:59

      Я бы задал другой вопрос: зачем вообще выбрали ffmpeg, если можно было просто DirectShow заиспользовать, и не морочиться с кодеками вообще. Тут приходит в голову только сложность с пунктом «комфортный просмотр видео при кратковременном падении скорости передачи данных», но, вероятно, есть способы его решения.


      1. dkv
        17.05.2016 19:10

        Или вообще LAV Filters и получить ещё и аппаратный декодинг из коробки. Мои тесты на древних ATI-картах показывают, что три потока FullHD они на DXVA в параллели тянут без проблем. Четвёртый уже придётся декодировать программно.


    1. vxg
      17.05.2016 19:18

      Могу задать встречный вопрос — а почему не ещё чего-нибудь? Это статья о том как заточить карандаш точилкой, а не о том как залить спирт внутрь фломастера — FFMPEG решал мою задачу и этого мне было достаточно. Конечно есть и другие способы.


      1. victor1234
        17.05.2016 20:17
        +1

        Переформулирую: вы выбрали libav потому что она решала ваши задачи и другие варианты вы не рассматривали за ненадобностью или потому что другие не подошли по каким-то причинам?


        1. vxg
          17.05.2016 20:55

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


          1. k_e_s_t
            17.05.2016 22:09
            +1

            из личного опыта — видеокарты nvidia: встроенное видеоядро тянет до 45 мегабит видео(а в линейке GTX9xx похоже уже больше, судя по загрузке видеоядра), причем это могут быть несколько потоков(проверяли 10 потоков — успешно)


            1. vxg
              17.05.2016 22:10

              А чем их туда подать если имеете такой опыт не подскажите?


              1. k_e_s_t
                17.05.2016 23:53
                +1

                В примерах CUDA SDK:
                http://docs.nvidia.com/cuda/cuda-samples/index.html#cuda-video-decoder-gl-api
                Там же описание:
                http://docs.nvidia.com/cuda/samples/3_Imaging/cudaDecodeGL/doc/nvcuvid.pdf

                Есть еще NVENC, но в «бытовых картах» там ограничили количество параллельных потоков.


                1. vxg
                  18.05.2016 06:51

                  Спасибо, посмотрю!


              1. Videoman
                18.05.2016 00:03
                +1

                ffmpeg прекрасно умеет использовать различные API для аппаратного декодирования видео. В частности, для Windows поддерживается DXVA. Современный офисный компьютер (при поддержке со стороны Intel или NVidia) спокойно тянет 25 окон 720x576 (SD). Знаю, так как сам разрабатываю софт для телевидения и мы используем данную поддержку для Multiview телевизионных каналов. При этом загрузка CPU 20%.


                1. vxg
                  18.05.2016 00:06

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


                  1. dkv
                    18.05.2016 01:23
                    +1

                    Посмотрите на решения от Intel — https://software.intel.com/en-us/intel-media-server-studio Ничего не могу по ним сказать, кроме того, что качество кодирования у них ниже софтового варианта судя по отзывам, но вам не кино в IMAX показывать.


                  1. Videoman
                    18.05.2016 12:23
                    +1

                    К счастью, для вас, кодирование с ускорением, с использованием ffmpeg, намного проще чем декодирование. Для кодирования вообще ничего особенного не нужно делать, только указать qsv encoder, например. Декодирование с ускорением, существенно сложнее (для ffmpeg, но можно использовать DirectShow). Это связано с тем, что декодирование (в общем виде) происходит во внутренние буфер видеокарты (copy-back очень затратная операция), и эти буфера нужно будет готовить и обслуживать самостоятельно. Но, при желании, в этом можно разобраться.


                    1. vxg
                      18.05.2016 12:48

                      Значит не тот гугл я смотрел, пойду перечитаю почему кодирование показалось мне сложным. Странно, если там все так хорошо и аппаратная поддержка реализована на сколько-нибудь значимом количестве железок почему просмотр и кодирование видео как правило не сильно быстро происходят? Во всяком случае не с нагрузкой 0% как вроде бы должно было быть, а приблизительно с такой нагрузкой с какой работает и моя программа…


                      1. Videoman
                        18.05.2016 13:19

                        Сложно сказать без конкретики, нужно смотреть. В идеале, декодирование HD отнимает 0% — 2% CPU. Кодирование побольше, так как, просто копирование (поставка) раскомпрессированого потока уже будет что-то занимать. Плюс, по словам Intel, например, некоторые промежуточные операции выполняются на CPU. При декодировании CPU почти не участвует. Могу лишь предположить, что вы пытаетесь копировать раскомпрессированный кадр обратно в RAM (copyback) чего делать не стоит. При декодировании, все, чем занимается CPU — это копирует несколько килобайт сжатого потока в буфера декодера и в нужный момент вызывает Blit. CPU там нечего делать.


                        1. vxg
                          18.05.2016 13:27

                          Я не пытаюсь ничего обратно копировать — где вы это увидели? Все остальные приложения включая сам FFMPEG — тоже этим занимаются? Я допускаю что неверно интерпретирую данные, но ваше альтернативное объяснение пока никак не ложится на то что наблюдается. Про CPU и Blit не понял. CPU в любом случае лишь исполнительное устройство. Копирует и вызывает если можно так выразится программа. Да, она так и делает — вызывает функцию декодирования когда приходит новая порция сжатого потока до тех пор пока функция не сообщит о том что кадр получен. После этого готовый кадр копируется из буфера декодера. После этого в нужный момент вызывается BitBlt. Все это происходит в коде. Теория и рекламные статьи — это хорошо, но я вижу то что описал.


                          1. Videoman
                            18.05.2016 15:25
                            +1

                            Это было сказано применительно к кодированию, в общем. Я видел код, и пытаюсь объяснить вам в чем будут проблемы при таком подходе. На практике, нужно очень хорошо представлять что кроется за вызовом той или иной функции. Direct-X более детерминированный путь по контролю за выводом графики чем очередь сообщений. Задача сгладить все неровности в распределении времени системой. Далее, я вас уверяю, что вы заблуждаетесь насчет практики и реально все работает и не жрет CPU. Про рекламу я не понял.


                            1. vxg
                              18.05.2016 15:44

                              Да, это не DX. Это сделано по совсем другим принципам. Безусловно DX более прогрессивен чем рисование на форме однако программа не нарушает «законов» системы и не смотря на то что это менее производительное решение программа выполняет свои функции. Что на самом деле радует — можно сделать то что делает DX и иные тяжелые по уровню входа решения просто рисуя на форме.

                              Про рекламу я имел ввиду фразу «по словам Intel» — слова это хорошо, но ими оперировать не совсем честно в дискуссии ибо в этом случае обсуждение превращается в конкурс цитат в котором конечно же победит компания Intel — я не настолько мощен что бы завалить ее своими словами ;)

                              Не совсем понял как можно заблуждаться насчет практики если я вижу что нагрузка процессора при работе программы и при работе ffplay практически одинаковая — я же не голословно это утверждаю.


                              1. Videoman
                                18.05.2016 15:57

                                По скорости вывода битмапов GDI сравним с DirectX. В DirectX просто больше контроля данного процесса.
                                По поводу Intel: я их привел просто для примера. Под windows вообще лучше использовать DXVA, тогда вам будет все-равно что использовать для ускорения Intel, NVidia, AMD. Он сам позаботится об этом. Для проверки скорости я бы не рекомендовал ffplay. Я не знаю как он реализован внутри. Для сравнения лучше использовать windows mdia player, mpc, vlc или вообще GraphEdit из WindowsSDK. Выбрать в настройках поддержку аппаратного ускорения и самому убедиться в загрузке.


                                1. vxg
                                  18.05.2016 16:37

                                  Я сравнивал скорость с ffplay просто для того что бы убедиться в том что производительность программы сопоставима с производительностью приложения от разработчика библиотеки. Иными словами я хотел убедить сам себя в том что я правильно работаю с функциями библиотеки и не вношу существенных задержек в процесс из-за того что рисую традиционными средствами. Этой проверки для меня достаточно — то есть я не ставил задачу сравнить решения примененные в программе со всеми возможными решениями. Для визуального восприятия привожу снимки экрана полученные только что. По ним видно что ffplay, программа и VLC имеют приблизительно одинаковые скорости. Незначительные отличия связаны с дрейфом загрузки. Аппаратное ускорение не включалось. Точнее я не знаю включено ли оно в VLC или где либо еще.



                                1. dkv
                                  18.05.2016 17:24

                                  На наколеночных тестах у меня год назад лучше всего себя в скорость DXVA показал именно MPC. При этом в качестве декодера он использовал LAV Filters. Это просто для информации :)


                    1. vxg
                      18.05.2016 13:02

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


                    1. vxg
                      18.05.2016 13:07

                      Особенно настораживает документация FFMPEG в которой прописано о необходимости "...build, configure with..." — то есть эта шутка вроде как по умолчанию не стоит и нам предлагают пересобрать FFMPEG (


                      1. Videoman
                        18.05.2016 13:24

                        Возможно требуется собрать с поддержкой QSV, например, это да. Просто — с точки зрения API. Все что от вас требуется — это настроить нужный аппаратный кодек, как и любой другой, и преобразовать кадры в формат NV12.


                        1. vxg
                          18.05.2016 13:31

                          Безусловно это просто если слово просто означает «напишите код в соответствии с API аппаратного ускорителя». Но мне не ясно почему они просто не могли реализовать это в самом низу? То есть почему я не мог указать в словаре что хочу использовать аппаратное ускорение и продолжать использовать имеющийся код вызывающий avcodec_decode_video без изменений, но с большей производительностью? Зачем они сделали так, что я должен модифицировать код? Безусловно на это были причины. Просто жаль.


                          1. Videoman
                            18.05.2016 13:59
                            +1

                            Ну так устроен ffmpeg/ Для кодирование все работает именно так, как вы хотите, все «внизу». Для декодирования это невозможно, так как ffmpeg не заниматся рендерингом. Он не знает какие API, платы, вы будете использовать для показа и как плотно интегрированы с GUI. Вам нужно, немного, ему помочь с аппаратными буферами, куда нужно будет класть декодированные кадры.


                          1. dkv
                            18.05.2016 17:32

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


                            1. vxg
                              18.05.2016 18:25

                              Я наверное понял о чем вы — декодирование на стороне карты не только выгоднее из-за аппаратной поддержки но и потому что кадр уже будет сформирован там где будет отображаться? Но мне нужно наложить на него свои элементы и вообще накапливать кадры для сглаживания вывода — получается что использовать оба преимущества я не смогу — только аппаратную поддержку, но не разгрузку шины, верно?


                              1. dkv
                                18.05.2016 18:52

                                Создаёте столько буферов, сколько вам нужно, средствами DirectX. И все иные операции делаете этими же средствами.


                                1. vxg
                                  18.05.2016 19:17

                                  Хм, звучит заманчиво попробую разобраться. Нет ли поваренной статьи — как вывести картину через DX?


                                  1. dkv
                                    18.05.2016 20:16

                                    Я это делал только средствами DirectDraw, который уже мёртв. Так то это к Videoman больше вопрос.


                                    1. Videoman
                                      18.05.2016 20:58

                                      Я готов ответить, но:
                                      1. Не пойму что нужно, пример кода? Типа взяли буфер из памяти и нарисовали в нужном месте через Direct3D? Не многовато кода будет в ответе?
                                      2. Где можно посмотреть теги или что-то подобное для оформления ответов на habr-е?

                                      Еще хотелось бы добавить по поводу вывода сразу на поверхности в видеопамяти:
                                      В плане интерфейса, нет практически никаких ограничений. Мы например используем QT и рендерим через ANGLE.Так вот видео поверхности, без проблем, можно комбинировать с QT виджетами и рисовать над и под видео.


                                      1. vxg
                                        18.05.2016 21:12

                                        Нас могут наказать за код в ответе?
                                        Тег я думаю тут будет один — C++ код — кнопочка выглядит так </>


                                        1. Videoman
                                          18.05.2016 21:22

                                          Попробую задать вопрос понятнее: где можно посмотреть какие теги поддерживаются и как оформлять ответы. Если ли подсказка? Я тут первый раз просто.


                                      1. dkv
                                        18.05.2016 21:22

                                        Выдержка из https://wiki.qt.io/Qt_5_on_Windows_ANGLE_and_OpenGL:
                                        Use Desktop OpenGL if
                                        Your application uses OpenGL calls not part of OpenGL ES 2.0
                                        Your application does not use OpenGL at all (since ANGLE implies additional deployment dependencies which are then not needed).
                                        Your application needs to run on Windows XP. Although it is tempting to use ANGLE as a replacement for missing graphics drivers on this platform, it does not fully work and may lead to crashes, for example, when the security dialog is opened.

                                        Use ANGLE if
                                        You need OpenGL ES features, but not full OpenGL
                                        You have a heterogeneous user base with different Windows versions & graphics cards
                                        You do not want your user to have to install a recent graphics card driver
                                        You want to use the video playback functionality of QtMultimedia (see QTBUG-31800 )
                                        Your application needs to run over Windows Remote Desktop Protocol (see OpenGL and Remote Desktop)
                                        Я так смотрю, Майкрософт продолжает вставлять палки в колёса OpenGL, что вынуждает даже гугл родить свой фреймворк. В итоге через ANGLE приходится писать на OpenGL?


                                        1. Videoman
                                          18.05.2016 21:31

                                          Да. Microsoft продолжает продвигать свои продукты и разработки и их можно понять — конкуренция. По-этому, что бы делать interop DXVA (проприетарная технология которая, в свою очередь, требует Direct-X) и OpenGL (которая используется в QT) приходится компилировать QT с Angle. Так можно в рамках Angle конвертировать Direct3D поверхности в OpenGL поверхности, которые, в свою очередь, находятся в видео-памяти, без копирования.


                                          1. dkv
                                            18.05.2016 21:47

                                            Спасибо за информацию по этому фреймворку. Нет ли случаем полезных ссылок под рукой по DXVA силами ffmpeg? И, кстати, DirectShow вообще не используете? Какие преимущества/недостатки в использовании ffmpeg + Angle перед DirectShow?


                                            1. Videoman
                                              18.05.2016 23:14
                                              +1

                                              Нет ли случаем полезных ссылок под рукой по DXVA силами ffmpeg?
                                              Нет, вся информация собиралась по крупицам. Советую посмотреть исходники утилиты ffmpeg, файл ffmpeg_dxva2.c, если не ошибаюсь. Благо он один и вся работа с DXVA там сконцентрирована.
                                              И, кстати, DirectShow вообще не используете?
                                              У нас написана библиотека для проигрывания различных данных и используется в софте наподобие нелинейных редакторов. Библиотека написана на С++ и архитектура там своя, а вот компоненты библиотеки; источники, демуксеры, декодеры и т.д. используют как на ffmpeg, так и DirectShow фильтры. За счет единого интерфейса их можно комбинировать как угодно друг с другом. Т.е. сплиттер может быть ffmpeg, а декодер DirectShow, и наоборот. Рендереры все свои. В результате можно играть данные произвольных форматов, вперемешку, с произвольной скоростью, вперед и назад.
                                              Какие преимущества/недостатки в использовании ffmpeg + Angle перед DirectShow?
                                              ffmpeg — более гибкий, всегда можно посмотреть что и где неправильно работает, поддержка кучи экзотических форматов используемых в камерах и оборудовании.
                                              DirectShow — проще использовать, но: платные компоненты, медленный старт-стоп, жрет ресурсы системы (память, хендлы и т.д.). Пришлось изобретать wrapper для фильтров, наподобие струпцины, чтобы пихать и забирать данные.


                                              1. dkv
                                                18.05.2016 23:23

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


                                              1. vxg
                                                19.05.2016 10:11

                                                Да, найти время и терпение для статьи нелегко. С момента внедрения приложения до выпуска этой статьи прошло 3-4 месяца. Надеюсь занятость не отнимет у нас ваш глубокий опыт в этом вопросе.


                                                1. Videoman
                                                  19.05.2016 11:17

                                                  Как раз в занятости и проблема. Тема мультимедиа обширна и требуют некоторого начального багажа для понимания. Из-за этого всегда стоит дилемма, писать полностью с нуля, чтобы было интересно читать всем, или подразумевать что статья уже для продвинутых, сужая этим охват.
                                                  Приведу пример: вот хотим мы написать статью о том как правильно писать фильтры разных типов под DirectShow. Вроде бы все знаем, кода куча — короче, садись и пиши. И тут начинается. А там COM.
                                                  А о нем нужно рассказывать?! А там жуткая многопоточность, навязанная самой архитектурой. А про многопоточность нужно рассказывать, или предполагается, что человек знает что это такое и представляет какие есть примитивы в WinAPI ?! Для быстрой разработки у нас кругом свои обертки и врапперы. А наверное интереснее будет на голом С?! Значит нужно все переписывать и тестировать. В общем, я хочу сказать, нужно четкое понимания для кого писать и какая задача ставится. Для себя, я уже давно понял, что написание статей это отдельная большая и кропотливая работа, которая требует уйму времени.


                                                  1. vxg
                                                    19.05.2016 11:46

                                                    Да, мне тоже было легче остаться при своем. Желание поделиться было сильнее.


                                                  1. dkv
                                                    19.05.2016 12:05

                                                    Именно что мультимедиа обширна и вменяемого цикла статей не попадалось мне на глаза. Да и десять лет назад, когда мне было интересно окунуться в DicrectX, все развалы в городе излазил с дисками — ничего по DX не нашёл. Приходилось брать вместо этого диски с игрушками, отвязывать их через SoftICE от желания диска в приводе и обменивать за копейки на следующие. Но потом энтузиазм закончился и появилась потребность в деньгах. А сейчас начинаю задумываться, что свернул в своё время не туда. Теперь с моими обрывочными знаниями только индусов на апворке пугать бидами в пять долларов.


  1. Videoman
    18.05.2016 00:05
    +1

    А можно еще несколько вопросов:
    1. Почему не используется аппаратное ускорение (например DXVA под Windows) — это позволило бы показывать видео намного плавнее, т.к. загрузка CPU обычно падает до 1-5%.
    2. Почему не используется отдельный поток для прямого рендеринга из него средствами Dirext-X. Посылка сообщений из другого потока, таких как WM_PAINT, по средством SendMessage, все-равно происходит синхронно. Т.е. тормозится вызывающий поток, потом стартует тот, которому отправлено сообщение, сообщение выполняется в другом потоке (у вас это WM_PAINT в основном потоке), после управление опять передается в вызывающий поток. Какой смысл отдельного потока, если отрисовка все-равно происходит синхронно в главном потоке.

    Это, как минимум, две причины которые приводят к рывкам. Даже на том видео, которое вы прислали, заметно, что частота вывода плавает. Я уверен, что на более динамичном потоке эффект будет еще более заметным.


    1. vxg
      18.05.2016 07:03

      1 У меня ещё не было возможности разобраться с этим. Если у вас есть наработки я думаю былобы интересно их оформить статьей.
      2 DX я не использую — пока ещё не было необходимости разбираться с этим — на мой вкус он достаточно тяжёл для входа. Посылка сообщения через функцию синхронизации действительно останавливает поток, я об этом знаю и это не влияет на производительность — поток который шлёт эти сообщения либо рисует либо спит — зачем мне рисовать быстрее если кадры идут с такой частотой с которой он справляется и после рисования все равно спит? Артефакты на видео (если вам не сложно укажите место где вы их заметили что бы я смог дополнительно проанализировать этот момент) скорее всего связаны не с работой программы, а с тяжёлыми условиями транспорта данных.


      1. Videoman
        18.05.2016 09:53
        +1

        Наработок куча, так как уже лет 10 пишем профессиональный софт для телевидения. Это масса, достаточно сложных, «технологий» типа ffmpeg, DirectShow, Direct-X. Также весь этот зоопарк используется для кодирования и декодирования видео. У нас написаны свои рендереры видео и аудио максимально использующие поддержку со стороны аппаратуры. Тут, к сожалению, статьей не отделаться, тут нужно несколько книг писать, как все это использовать, применительно к видео обработке. Но собирать качественный рендерер видео из готовых компонентов уже не сложно.
        По поводу вопроса про поток который показывает: его задача максимально быстро, без задержек, показать следующий кадр. Задержки больше чем 10 мс на кадр, а особенно если они плавающие, уже заметны на глаз. Подготовка самих кадров и все тяжелые операции должны выполняться в другом потоке. При использовании SendMessage, задержки на переключение контекста потоком не предсказуемы.
        По артефакты: я имею ввиду «плавающую» частоту воспроизведения кадров. Когда робот движется, это более заметно.
        Задача у вас интересная. Пока, как я понял по коду, решена в черновом варианте, но, при желании, тут много что можно улучшить.


        1. vxg
          18.05.2016 10:11

          Как вы совершенно правильно пишите все операции подготовки выполняются у меня в отдельном потоке обслуживающим камеру. Вывод идет в другом потоке и не делает с кадром ничего кроме самого вывода (от этой операции никуда не уйти учитывая назначение этого потока). Задержек такого порядка как вы описали я не наблюдал. Относительно максимальной быстроты вывода: как я уже сказал если поток вывода вывел кадр за 5 мс и спит 35 мс, то чем это отличается от ситуации когда он вывел кадр за 1 мс и спит 39 мс?


          1. Videoman
            18.05.2016 12:15
            +1

            Для плавности важно не только что время есть в запасе, но и вывод кадра в строго запланированное время. Что бы было видно, достаточно что бы задержка при показе кадров плавала от 0мс до 40мс. Потом, видео-каналов момент быть много и все они будут друг на друга влиять. По коду, к сожаление, не видно как вы инициируете WM_PAINT. То что вывод связан с WM_PAINT, уже, говорит о том что вывод всегда будет с непредсказуемыми задержками и рывками, если только не создать отдельный цикл выборки сообщений в отдельном потоке, который рендерит. Но зачем это делать, если есть DirectX. Весь смысл такого сложного дизайна — показывать кадры со строго заданной частотой (насколько это возможно в системах не реального времени), а также нивелировать случайные непредсказуемые задержки и нагрузки CPU. У нас, типичная задержка при непосредственном выводе кадра — микросекунды (0 мс). Также, для плавной синхронизации, я очень рекомендую завести аналог референс-часов, по которым все компоненты синхронизируются друг с другом (в случае стереоскопичекого видео или звука очень даже актуально). В противном случае, у вас всегда будут проявляться все особенности реализации современных операционных систем в виде рывков…


            1. vxg
              18.05.2016 12:50

              Если бы задержка плавала 0-40 мс это было бы видно так что у людей бы глаза повыбивало. Судя по всему описанные вами эффекты в моем случае не проявляются, а вполне обоснованные предположения о задержках весьма преувеличены.


            1. dkv
              18.05.2016 17:38

              Вот, кстати, как к профессионалу в данной области вопрос — что здесь лучше использовать как референс? Пытаться привязаться к вертикальной синхронизации монитора или использовать какой-то мультимедиа-таймер из DirectShow? Или на современном железе системные таймеры стали довольно точны?


              1. Videoman
                18.05.2016 21:16

                С вертикальной синхронизацией сейчас все плохо, т.к. сигнал эмулируется на современных цифровых интерфейсах и может быть задан каким угодно. Он оставлен для обратной совместимости со старым софтом. Проблема не в точности таймера, можно использовать хоть Sleep, проблема в точности планировщика потоков windows, то, как быстро он пробуждает поток после поступления сигнала. Тут лучше использовать мультимедийный таймер. Как только вы задаете частоту его срабатывания, windows меняет размер кванта потоков данного процесса в соответствии с заданной частой таймера со стандартной (порядка 15мс) до заданной.Другими словами, если вы создадите в процессе мультимедийный таймер с частотой срабатывания 1мс, то все потоки начнут планироваться и срабатывать с заданной частотой 1мс. Сам таймер можно использовать любой. В DirectShow например он выбирается динамически, сначала пытается брать часы источника (если есть) потом часы рендерера (если есть).


                1. dkv
                  18.05.2016 22:04

                  Хм. Т.е. разрешение Sleep в 15 мс, о котором я в курсе, это не разрешение Sleep, а время переключения контекста между потоками? Хорошо, допустим в таком случае мы не пишем медиакомбайн и не хотим использовать мультимедиатаймеры, а просто полагаемся на мьютексы и семафоры. У нас будет уходить до 15 мс на каждое переключение между потоками? Жесть. Средства C++11/14 предлагают что-то из коробки? Не нужно развёрнутого ответа, просто хочу понять, решена ли подобная проблема да и есть ли она вообще. Просто последний раз, когда для меня было критично по времени синхронно обрабатывать данные в рилтайм, поступающие с камеры, уходящие в железку по USB, приходящие обратно и после постобрабатываемые на процессоре между четырьмя потоками, я ещё на стадии разгребания каши после предыдущего разработчика с его понатыканными везде Sleep(1) был вынужден увеличить разрешение этого Sleep средствами системы Win7 (указал в качестве референсного таймер высокого разрешения) и успешно про это забыл к моменту, когда всё переписал и раскидал нормально по потокам без «магических» задержек вообще. Снимаемые логи показали, что я полностью укладываюсь в таймлайн, но с лишь с небольшим запасом на цикл, что всех устроило. Получается, я мог не уложиться, не проведя такую подготовочку системы на первом этапе?


                  1. Videoman
                    18.05.2016 23:24

                    Если коротко, то частое переключение потоков не приведет к быстрой обработке данных, скорее наоборот, за счет потери времени на переключение. Но, уменьшив квант, мы можем точнее реагировать на события. Для быстрой и эффективной передачи данных между потоками или устройствами лучше использовать очереди.


                    1. dkv
                      18.05.2016 23:29
                      +1

                      Спасибо. Придётся почитать литературу. Ибо пора менять род занятий с пусконаладки систем безопасности на что-то более нужное в этом мире.


            1. vxg
              20.05.2016 11:46

              «Опровержение слухов — слушайте факты» (С)
              Как и говорилось ранее выводы о наличии дополнительных задержек связанных с работой очереди сообщений, синхронизацией потока с потоком GUI и рисованием на форме являются лишь теоретически верными, но практически не влияющими на работу программы. Тема заинтересовала меня и сегодня провел дополнительное тестирование — замерил время между вызовами WMPaint (то есть время между реальным рисованием кадров в окне) — пытался увидеть указанный вами эффект рывков и плавания задержек. Ничего не подтвердилось. Ни рывков ни дрейфа задержек нет. Во всяком случае на Intel Core2 Duo E8300 2.83 ГГц. Разность отметок между кадрами остается постоянной и равной фактическому периоду с которым поступают кадры — 40 мс. Единичные отклонения амплитудой до 180 мкс могут быть связаны с колебаниями загрузки, точностью отметок времени, описанными вами задержками или фазой Луны. На графике по оси X время в мс, по оси Y задержка между вызовами WMPaint в мс.


              1. dkv
                20.05.2016 11:55

                Как обычно — только логи расставляют всё по своим местам :)
                «Человек предполагает, а тест располагает» ©
                Осталось вам ещё на промышленные камеры (например, PointGray) и обеспечить полную синхронизацию картинок на аппаратном уровне.


                1. vxg
                  20.05.2016 12:07

                  На самом деле насущная проблема — это аппаратное кодирование и, возможно, движения в сторону отлова вертикальной синхронизации. Как я понял это требуют танцев со сборкой и обнимашек с DX. А гуру не хотят делится двумя с половиной строчками кода ;)…


                  1. dkv
                    20.05.2016 12:31

                    при беглом поиске попадаются даже вот такие решения: https://github.com/trbotha/PointGrey_RPi
                    правда, я не понял из описания, через какую они шину протаскивают данные, но заявляют о рилтайм кодировании двух камер силами одной малины. для вас крайне бюджетное решение. не считая конской цены на сами камеры, но один только заявляемый Global Shutter на CMOS матрице того стоит. Global Shutter на CMOS, Карл! и как я мог пропустить такую фишку новых матриц Sony.


                    1. vxg
                      20.05.2016 12:51

                      Совершенство все совершеннее ;)


              1. Videoman
                20.05.2016 12:10

                Теория здесь как-раз важнее, так как у вас нет возможности проверить ваш софт на огромном парке машин. Никто не спорит, что на отдельно взятой машине, в вакуме, в идеальных условиях, где кроме одного приложения с одним окном видео больше ничего не работает, вы увидите вышеописанную картину. Вы не приводите ни разрешение видео, ни разрешение с которым оно показывается, ни текущую загрузку CPU в вашем тесте. Загрузите машину хорошо, процентов на 50, выведите 2 HD видео одновременно и еще 8 SD дополнительно. Желательно проделать данный эксперимент на 10 машинах с разной конфигураций. Только, когда во всех этих условиях вы получите идеальный результат, можно будет о чем-то говорить. Количество машин, на которых мы отлаживали систему, порядка сотни, и поверьте, написать хороший рендерер, это не такая простая задача как вам кажется.


                1. vxg
                  20.05.2016 12:17

                  Я не спорю с теорией просто в ряде случаев оптимизация превращается в пугало. Когда поставят задачу запустить 100 видео-потоков на кофеварке я конечно же задумаюсь где сэкономить еще 1 мс родив в результате непереносимого и несопровождаемого монстра эффективное решение. Относительно сферичности теста — запуск шел на железе которое как бы нельзя назвать топовым поэтому обоснованным является предположение о том что тест пройдет не менее успешно и на других машинах. Пока я еще не вижу в себе сил и не пытаюсь написать рендерер сопоставимый с теми что выпускаются компаниями которые специализируются в данной области.


                  1. Videoman
                    20.05.2016 12:30

                    Да я сам читаю, что не нужно переусложнять там, где можно обойтись более простым решением. Так все-таки, вы можете подтвердить, что ваши одна HD камера и 9 SD которые работаю одновременно для кругового обзора, дают такой же результат, как вы привели. Если да, то замечательно и больше тут ничего не нужно, задача решена.


                    1. vxg
                      20.05.2016 12:49

                      Понаблюдать за работой системы в полной комплектации пока к сожалению не доводилось — негде просто взять столько камер. В статье лишь указано на такую возможность и на то, что полная комплектация начнет упираться в ограничения по транспортировке данных. Кроме того учитывая показатели загрузки процессора приведенные в статье запускать программу обслуживающую полную комплектацию придется как минимум на в два раза более производительном Intel Core2 Quad Q9400 2.66 ГГц. Таким образом поразить вас и себя фразой «все работает» мне не удастся. В дополнение лишь уточню, что тест задержек который я привел проводился для разрешения использованного в статье (1280 x 1024, одно окно просмотра), время работы процедуры WMPaint при этом стабильно находилось на уровне 4,2-5,0 мс, нагрузка на процессор составляла 4-5% (декодирование не проводилось, это был тест без потока — исключительно для проверки подсистемы вывода на экран).


                      1. vxg
                        20.05.2016 12:56

                        Пардон, разрешение было 1280 x 720


                      1. dkv
                        20.05.2016 12:59

                        Камеры обычно способны отдать несколько одинаковых потоков с себя. Так что для тестов полной загрузки вам хватит и одной камеры.


                      1. Videoman
                        20.05.2016 13:06

                        Ну и зачем такие синтетические тесты?! Рендерер, в моем понимании, это компонент, который сглаживает неравномерную загрузку, в том числе. Как можно говорить о том, что он плавно работает, если нет загрузки?!.. И потом 4% и 4мс, при холостом выводе, это очень много. Нужно ориентироваться на 0%.и микро секунды. Ну сами подумайте, вы выводите 25 раз в секунду с паузами между этими выводами. Для современного процессора и для видео процессора это вечность, они просто стоят и курят в сторонке. Для ссылки: средненький аппаратный декодер спокойно декодирует H264 со скоростью 500 кадров в секунду, а уж выводить на экран битмапки 1280х1024 — это тысячи.


                        1. vxg
                          20.05.2016 13:27

                          Синтетический тест нужен что бы отделить пену от воды. Перед вами конкретные цифры которые приведены для отдельно взятой подсистемы которую вы оценили критически. Согласно этим цифрам на рисование тратится 5 мс при стабильных интервалах между кадрами 40 мс. Я в этих цифрах не вижу ни «задержки больше чем 10 мс на кадр» ни того что бы «задержка при показе кадров плавала от 0мс до 40мс». Я согласен с вами в том что аппаратная реализация быстрее. Все это я лишь привожу для того что бы показать что очередь сообщений являясь архаичным, но 100% переносимым по причине своей фундаментальности, механизмом в состоянии выполнять свои функции.


                          1. Videoman
                            20.05.2016 13:47

                            Я в этих цифрах не вижу ни «задержки больше чем 10 мс на кадр» ни того что бы «задержка при показе кадров плавала от 0мс до 40мс»..
                            Так, я все понял. Исходя из ваших ответов, вы не правильно представляете себе как работают современные операционные системы. Задержки берутся не из воздуха, задержки появляются тогда, когда планировщик распределяет общее время между несколькими работающими потоками. Если у вас нет загрузки системы, то и задержкам взяться не от куда.
                            Все это я лишь привожу для того что бы показать что очередь сообщений являясь архаичным, но 100% переносимым по причине своей фундаментальности, механизмом в состоянии выполнять свои функции.
                            Вы этим тестом показали лишь то, что без нагрузки процессор занят только вашей «логикой» и выполняет ваш код без прерываний на параллельные задания. Все. Больше этот тест ничего не показывает. Как только вы напишите «боевой» код и нагрузите процессор, ваши тайминги сразу же поплывут.


                            1. vxg
                              20.05.2016 13:56

                              Я отдаю себе отчет в том что при оценке нагрузки 2 + 2 может не оказаться равным 4. Я затрудняюсь определить как именно мне нагрузить процессор что бы тест был признан вами правильным. Очевидно, что мне нужно просто «повесить» систему и отрапортовать о том, что тайминги действительно поплыли. Только тогда все будет признано достоверным.


                              1. Videoman
                                20.05.2016 14:04

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


                                1. vxg
                                  20.05.2016 14:10

                                  Хм. Неожиданно. А что это даст? Серьезно.


                                  1. Videoman
                                    20.05.2016 14:14

                                    Увидите как реально работает очередь.


                                    1. vxg
                                      20.05.2016 14:27

                                      Совсем не понял. Вы утверждаете что ffmpeg построен на очереди сообщений %/


                                      1. dkv
                                        20.05.2016 14:31

                                        Нет. Здесь речь о том, что на очередях построена ОС. А ffmpeg — очередной клиент в этой очереди, способной оттяпать от неё хорошенький кусочек.


                                        1. vxg
                                          20.05.2016 14:35

                                          Какое-то логическое зацикливание и бег по кругу… ОС построена на очередях… Речь идет об очереди сообщений окна? Сомневаюсь в том что приложения интенсивно не обращающиеся к этому механизму к коим я причисляю и ffmpeg что-то там способны оттяпать. Они способны оттяпать кванты — очередь им до лампочки.


                                      1. Videoman
                                        20.05.2016 14:53

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


                                        1. vxg
                                          20.05.2016 14:57

                                          Причем тут ffmpeg, применительно к очереди?

                                          эм…
                                          ffmpeg — очередной клиент в этой очереди, способной оттяпать от неё хорошенький кусочек

                                          0.0

                                          декодирование у меня идет в отдельном потоке и никак не взаимодействует с очередью сообщений. Вы смотрели код вообще?


                                        1. vxg
                                          21.05.2016 08:58

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

                                          1 — я знаю и согласен с вами что аппаратное кодирование / декодирование и вывод на экран средствами DirectX / SDL / иного_специализированного_решения более эффективен, однако, как вы сами знаете, эти методы имеют более высокую планку входа, сильнее зависят от аппаратного обеспечения и программной среды в которой исполняется приложение, несут с собой целый ряд побочных задач никак не связанных с основной, но обязательных для решения (первое что приходит в голову — построение графического интерфейса пользователя и реализация несколько отличающегося от традиционно принятого в Windows механизма работы с окнами требуемого по условиям задачи).

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

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

                                          Таким образом приведённое решение приближено к идеальному в своём классе, но не в глобальном плане (как и любое другое — нет серебряной пули).


                                          1. Videoman
                                            21.05.2016 16:14

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


                                            1. vxg
                                              21.05.2016 18:08

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


                                    1. dkv
                                      20.05.2016 14:29

                                      А можно попросить остальные потоки слегка подвинуться, запросив у системы повышенный приоритет для своих. Но, похоже, всё сводится к мультимедиа-таймерам в потоке рендеринга.


                                      1. vxg
                                        20.05.2016 14:33

                                        При создании потока можно задать приоритет


  1. Lelik13a
    18.05.2016 05:31

    Картинки интересные, жаль только спойлеров не завезли.
    Подкину из личных запасов:

    <spoiler title="">
    </spoiler>
    


    1. vxg
      18.05.2016 07:03

      Есть один) Хабракат)


  1. Ryppka
    18.05.2016 08:53
    +1

    «Основной целью при проектировании и создании *Moby Dick* было создание *небольшого* робота...» — это, все-таки, пять)))


    1. vxg
      18.05.2016 08:59
      +1

      Не совсем понимаю ваше веселье) Робот действительно небольшой — человек может поднять его одной рукой
      image


      1. DarkEld3r
        18.05.2016 13:13
        +2

        Я так понимаю, что дело в названии. Ведь "Моби Дик" — гигантский кит.


        1. vxg
          18.05.2016 13:15

          Игра слов? Тонко ;)


        1. vxg
          18.05.2016 14:38

          «Моби Дик» не от размеров, а от глубины погружения — кашалоты погружаются на глубину до 3 км)


  1. dimon11
    19.05.2016 18:48

    Интересная статья, спасибо.
    Кстати по теме отлично было рассказано и показано в фильме опасное погружение, там капсула в океане застряла, а водолазы пытаются выжить, кислород кончается… Много съемок там на глубине около 5 км.