Работа с видео / Доработка видеоплеера ffmpeg

в 9:25, , рубрики: audio player, ffmpeg, sdl, video player, метки: , , , ,

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

Введение

В основе большинства приложений и фреймворков для работы с мультимедиа лежит граф. Узлами графа являются объекты, выполняющие конкретную задачу. Для примера рассмотрим такой граф видеоплеера.
Работа с видео / Доработка видеоплеера ffmpeg
Работа начинается с блока Чтение данных, в котором в нашем случае осуществляется чтение файла. В более общем случае — это получение данных по сети, либо с аппаратного источника.
Демультиплексирование выполняет разделение входящего потока на несколько исходящих (например, аудио и видео). Демультиплексирование работает на уровне контейнера данных, то есть на этом этапе совершенно безразлично каким кодеком закодирован тот или иной поток. Примеры контейнеров: AVI, MPEG-TS, MP4, FLV.
После демультиплексирования осуществляется декодирование полученных потоков в блоках Декодирование видео и Декодирование аудио. На выходе декодера будут данные в стандартных форматах — YUV или RGB фреймы для видео и PCM данные для аудио. Декодирование обычно осуществляется в отдельных потоках. Отображение видео выводит видео на экран, а Воспроизведение аудио проигрывает получившийся аудио поток.

Реализация

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

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

Первым делом объединим все основные переменные в один общий контекст:

typedef struct MainContext { 	AVFormatContext *format_context; 	 	// Streams 	Stream video_stream; 	Stream audio_stream; 	 	// Queues 	PacketQueue videoq; 	PacketQueue audioq; 	 	/* ... */ } MainContext; 

Этот контекст будет передаваться во все потоки. video_stream и audio_stream содержат информацию и видео и аудио потоках соответственно. videoq и audioq представляют собой очереди, в которые поток демультиплексирования будет складывать прочитанные пакеты. С учетом этих изменений код демультиплексирования (demux_thread) получится совсем простым и примет следующий вид:

AVPacket packet; while (av_read_frame(main_context->format_context, &packet) >= 0) { 	if (packet.stream_index == video_stream_index) { 		// Video packet 		packet_queue_put(&main_context->videoq, &packet); 	} else if (packet.stream_index == audio_stream_index) { 		// Audio packet   		packet_queue_put(&main_context->audioq, &packet); 	} else {   		av_free_packet(&packet); 	} } 

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

Воспроизведение видео

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

Обновление изображение делается по таймеру, а не в отдельном потоке, так как SDL требует, чтобы операции работы с видео выполнялись в главном потоке приложения. Аналогично, нельзя создать overlay из произвольного потока. Это ограничение можно обойти, например, с использованием событий SDL и условной переменной. Но мы не будем это делать. Ограничимся одним overlay'ем, который создадим до начала декодирования.

Воспроизведение аудио

Звук на компьютере представляет собой непрерывный поток семплов. Каждый семпл это значение формы волны. Звуки записываются с определенной частотой дискретизации, и воспроизводиться должны с такой же частотой. Частота дискретизации представляет собой число семплов в секунду. Например, 44100 семпла в секунду — частота дискретизации аудио CD. Кроме того, аудио могут содержать несколько каналов. Например, для стерео семплы будут приходить по два за раз. При получении данных из файла неизвестно сколько семплов будет получено, но FFmpeg не будет отдавать неполные семплы. Также это означает, что FFmpeg не будет разделять стерео-семплы.

Первым делом надо настроить SDL для вывода аудио. В функцию инициализации необходимо добавить флаг SDL_INIT_AUDIO. Затем заполнить структуру SDL_AudioSpec и передать ее в функцию SDL_OpenAudio:

SDL_AudioSpec wanted_spec, spec; // Set audio settings from codec info wanted_spec.freq = codec_context->sample_rate; wanted_spec.format = AUDIO_S16SYS; wanted_spec.channels = codec_context->channels; wanted_spec.silence = 0; wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE; wanted_spec.callback = audio_callback; wanted_spec.userdata = main_context;  if (SDL_OpenAudio(&wanted_spec, &spec) < 0) {   fprintf(stderr, "SDL: %sn", SDL_GetError());   return -1; }  SDL_PauseAudio(0); 

SDL для вывода аудио использует вызов callback-функции.
Структура имеет следующие параметры:

  • freq: Частота дискретизации.
  • format: Формат передаваемых данных. Символ «S» в «AUDIO_S16SYS» означает что данные будут знаковые, 16 — размер семпла равен 16-ти битам, «SYS» — используется системный порядок байтов. Именно в этом формате FFmpeg возвращает декодированные данные.
  • channels: Количество аудио каналов.
  • silence: Значение «тишины». Для знаковых данных, обычно используется 0.
  • samples: Размер аудио-буфера SDL. Нормальными значениями считаются значения от 512 до 8192 байт. Мы будем использовать 1024.
  • callback: callback-функция для заполнения буфера данными.
  • userdata: Пользовательские данные, передаваемые в функцию обратного вызова. Используем здесь наш основной контекст.

Вызов SDL_PauseAudio(0) запускает воспроизведение звука. В случае отсутствия данных в буфере, будет воспроизводиться «тишина».

Декодирование аудио

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

В качестве такого буфера будем использовать кольцевой буфер. Прототипы основных функций:

int ring_buffer_write(RingBuffer* rb, void* buffer, int len, int block); int ring_buffer_read(RingBuffer* rb, void* buffer, int len, int block); 

Назначение аргументов, должно быть понятно из названия. Аргумент block указывает должна ли функция блокироваться в случае недостатка места в буфере или отсутствия данных для чтения.

Итак, целиком функция декодирования выглядит следующим образом:

static int audio_decode_thread(void *arg) { 	assert(arg != NULL); 	 	MainContext* main_context = (MainContext*)arg; 	Stream* audio_stream = &main_context->audio_stream; 	 	AVFrame frame; 	while (1) { 		avcodec_get_frame_defaults(&frame); 		 		// Get packet from queue 		AVPacket pkt; 		packet_queue_get(&main_context->audioq, &pkt, 1); 		 		// The audio packet can contain several frames 		int got_frame; 		int len = avcodec_decode_audio4(audio_stream->codec_context, &frame, &got_frame, &pkt); 		if (len < 0) { 			av_free_packet(&pkt); 			fprintf(stderr, "Failed to decode audio framen"); 			break; 		} 		 		if (got_frame) { 			// Store frame 			// Get decoded buffer size 			int data_size = av_samples_get_buffer_size(NULL, audio_stream->codec_context->channels, 														frame.nb_samples, 														audio_stream->codec_context->sample_fmt, 1); 			ring_buffer_write(&main_context->audio_buf, frame.data[0], data_size, 1); 		} 		av_free_packet(&pkt); 	} 	return 0; } 

Декодирование аудио-пакета осуществляется функцией avcodec_decode_audio4, В случае если декодирован целый фрейм (флаг got_frame), определяем размер буфера в байтах с помощью функции av_samples_get_buffer_size и записываем его в наш кольцевой буфер.

Воспроизведение аудио

Осталось совсем чуть-чуть, а именно воспроизвести декодированные семплы. Это делается в callback-функции audio_callback:

static void audio_callback(void* userdata, uint8_t* stream, int len) { 	assert(userdata != NULL); 	 	MainContext* main_context = (MainContext*)userdata; 	ring_buffer_read(&main_context->audio_buf, stream, len, 1); } 

Здесь все элементарно. Достаем из буфера len байт и сохраняем их в предоставленном буфере SDL.

В отличие от видео, аудио сразу проигрывается с правильной скоростью. Так происходит потому что частота дискретизации была явно указана при конфигурировании вывода аудио, и вызов callback-функции SDL будет выполняться именно с этой частотой.

Синхронизация

Потоки видео и аудио в файле имеют информацию о том в какой момент и с какой скоростью их надо воспроизводить. Для аудио потоков это частота дискретизации, с которой мы познакомились в предыдущей части, а для видео потоков это количество кадров в секунду (FPS). Однако нельзя выполнять синхронизацию только на основе этих значений, так как компьютер не является идеальным устройством, и большинство видео файлов имеют неточные значения этих параметров. Вместо этого, каждый пакет в потоке содержит два значения — метка декодирования (decoding timestamp, DTS) и метка отображения (presentation timestamp, PTS). Существование двух различных значений связано с тем что фреймы в файле могут идти не по порядку. Такое возможно в случае наличия в видео B-фреймов (Bi-predictive picture, фрейм который зависит как от предыдущего, так и от следующего фреймов). Также на видео могут присутствовать повторяющиеся фреймы.

Существуют три варианта синхронизации:

  • синхронизация видео к аудио;
  • синхронизация аудио к видео;
  • синхронизация видео и аудио с внешним генератором;

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

В наш основной контекст добавим следующие поля:

typedef struct MainContext { 	/* ... */ 	 	double video_clock; 	double audio_clock; 	 	double frame_timer; 	double frame_last_pts; 	double frame_last_delay; 	 	/* ... */ } MainContext; 

  • video_clock: частота отображения видео;
  • audio_clock: частота воспроизведения аудио;
  • frame_timer: текущее значение времени отображения;
  • frame_last_pts: значение PTS последнего показанного фрейма;
  • frame_last_delay: значение задержки последнего показанного фрейма;

Во время инициализации присвоим начальное значение для frame_timer:

main_context->frame_timer = (double)av_gettime() / 1000000.0; 

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

double pts = frame.pkt_dts; if (pts == AV_NOPTS_VALUE) { 	pts = frame.pkt_pts; } if (pts == AV_NOPTS_VALUE) { 	pts = 0; } pts *= av_q2d(main_context->video_stream->time_base); pts = synchronize_video(main_context, &frame, pts); 

Можно заметить, что значение pts может принимать одно из трех значений:

  • frame.pkt_dts: FFmpeg переупорядочивает фреймы во время декодирования так, что значение DTS соответствует значению PTS декодируемого фрейма. В это случае мы используем DTS.
  • frame.pkt_pts: В случае если значение DTS отсутствует, пробуем использовать PTS.
  • 0: Если оба значения отсутствуют, будем использовать последнее сохраненное значение частоты видео.

Код функции synchronize_video:

double synchronize_video(MainContext* main_context, AVFrame *src_frame, double pts) { 	assert(main_context != NULL); 	assert(src_frame != NULL); 	 	AVCodecContext* video_codec_context = main_context->video_stream->codec; 	if(pts != 0) { 		/* if we have pts, set video clock to it */ 		main_context->video_clock = pts; 	} else { 		/* if we aren't given a pts, set it to the clock */ 		pts = main_context->video_clock; 	} 	/* update the video clock */ 	double frame_delay = av_q2d(video_codec_context->time_base); 	/* if we are repeating a frame, adjust clock accordingly */ 	frame_delay += src_frame->repeat_pict * (frame_delay * 0.5); 	main_context->video_clock += frame_delay; 	 	return pts; } 

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

В потоке декодирования аудио сохраним частоту аудио, чтобы потом к ней синхронизироваться:

if (pkt.pts != AV_NOPTS_VALUE) { 	main_context->audio_clock = av_q2d(main_context->audio_stream->time_base) * pkt.pts; } else { 	/* if no pts, then compute it */ 	main_context->audio_clock += (double)data_size / 							  				(audio_codec_context->channels * 											audio_codec_context->sample_rate * 											av_get_bytes_per_sample(audio_codec_context->sample_fmt)); } 

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

double delay = compute_delay(main_context); schedule_refresh(main_context, (int)(delay * 1000 + 0.5)); 

Ну и «сердце» нашей синхронизации — это функция compute_delay:

static double compute_delay(MainContext* main_context) { 	double delay = main_context->pict.pts - main_context->frame_last_pts; 	if (delay <= 0.0 || delay >= 1.0) { 		// Delay incorrect - use previous one 		delay = main_context->frame_last_delay; 	} 	// Save for next time 	main_context->frame_last_pts = main_context->pict.pts; 	main_context->frame_last_delay = delay;  	// Update delay to sync to audio 	double ref_clock = get_audio_clock(main_context); 	double diff = main_context->pict.pts - ref_clock; 	double sync_threshold = FFMAX(AV_SYNC_THRESHOLD, delay); 	if (fabs(diff) < AV_NOSYNC_THRESHOLD) { 		if (diff <= -sync_threshold) { 			delay = 0; 		} else if (diff >= sync_threshold) { 			delay = 2 * delay; 		} 	}  	main_context->frame_timer += delay; 	 	double actual_delay = main_context->frame_timer - (av_gettime() / 1000000.0); 	if(actual_delay < 0.010) { 		/* Really it should skip the picture instead */ 		actual_delay = 0.010; 	} 	return actual_delay; } 

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

На этом все! Запускаем плеер и наслаждаемся просмотром!

Заключение

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

Исходный код плеера.

Всем спасибо за внимание!

Автор: zerodivisi0n


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js