Будучи в поисках учебного пособия/книги, которые научили бы меня использовать FFmpeg в качестве библиотеки (aka libav) я нашел статью «How to Write a Video Player in Less Than 1000 Lines». К сожалению, информация там устарела, поэтому я решил написать свой материал
Большая часть представленого здесь кода будет на C, но не волнуйтесь: вы можете легко понять и применить ее на предпочитаемом вами языке.
FFmpeg libav имеет множество биндингов для множества языков, таких как python, go и даже если ваш любимый язык не имеет готовый биндинг, вы все равно можете использовать его через ffi (вот пример с Lua).
Мы начнем с введения о том, что такое видео, аудио, кодек и контейнер, а затем мы перейдем к ускоренному курсу о том, как использовать приложение для командной строки FFmpeg, и, в конце, мы напишем код. Не стесняйтесь переходить напрямую в раздел Learn FFmpeg libav the Hard Way.
Некоторые люди считают, что потоковое видео в Интернете - это будущее традиционного телевидения. В любом случае, FFmpeg - стоит изучения
Оглавление
Если серию последовательных карнинок поочередно менять с определенной переодичностью (предположим, 24 кадра в секунду), создастся иллюзия движения. В итоге, это простая идея легла в основу видео: серия картинок/кадров меняющихся с определенной частотой.
Иллюстрация Zeitgenössische (1886)
Несмотря на то, что видеодорожка сама по себе может вызывать большое количество чувств, добавление к ней звука приносит заставляет приносить ее больше удовольствия.
Звук - это вибрация, распростроняющаяся в виде упругих волн механических колебаний твёрдой, жидкой или газообразной среде (например, воздухе).
В цифровой аудиосистеме микрофон преобразует звук в аналоговый электрический сигнал, затем аналого-цифровой преобразователь (АЦП) - с использованием импульсно-кодовой модуляции (ИКМ) преобразует аналоговый сигнал в цифровой сигнал.
Видеокодек — программа/алгоритм сжатия (то есть уменьшения размера) видеоданных (видеофайла, видеопотока) и восстановления сжатых данных. Кодек — файл-формула, которая определяет, каким образом можно «упаковать» видеоконтент и, соответственно, воспроизвести видео. https://ru.wikipedia.org/wiki/Видеокодек
Но если мы решим упаковать миллионы изображений в один файл и назвать это фильмом, мы получим огромный файл. Давайте сделаем некоторые подсчеты:
Допустим, мы создаем видео с разрешением 1080 x 1920 (высота x ширина), тратя 3 байта на каждый пиксель, чтобы закодировать цвет (так называемый TrueColor, который дает нам 16,777,216 разных цветов) и это видео продолжительностью 30 минут содержит 24 кадра в секунду.
toppf = 1080 * 1920 //total_of_pixels_per_frame
cpp = 3 //cost_per_pixel
tis = 30 * 60 //time_in_seconds
fps = 24 //frames_per_second
required_storage = tis * fps * toppf * cppДля видео понадобилость бы примерно 250.28GB места или 1.11Gbps пропускной способности! Поэтому, если необходимость в использовании кодека.
A container or wrapper format is a metafile format whose specification describes how different elements of data and metadata coexist in a computer file. https://en.wikipedia.org/wiki/Digital_container_format
Отдельный файл, содержащий все потоки (в основном аудио и видео), а также предоставляющий синхронизацию и основные метаданные, такие как название, разрешение и т.д.
Обычно мы можем определить формат файла, посмотрев на его расширение: например, «video.webm» - это, вероятно, видео использующее контейнер webm.
Готовое, кросс-платформенное решение для записи, конвертирования и передачи аудио и видео.
Для работы с мультимедиа мы можем использовать чудесную утилиту/библиотеку под названием FFmpeg. Скорее всего, вы уже знаете о ней, или даже используете ее прямо или косвенно (вы используете Chrome?).
В него входит утилита для командной строки, именнуемая ffmpeg, очень простая но мощная программа.
Например, вы можете конвертировать из mp4 в контейнер avi, просто используя следующую команду:
$ ffmpeg -i input.mp4 output.aviМы просто совершили remuxing, который конвертирует из одного контейнера в другой. Технически, FFmpeg также может делать транскодинг, но о нем мы поговорим немного позже.
FFmpeg поставляется с хорошей документацией (англ.) которая прекрасно объясняет, как он работает.
В двух словах, утилита FFmpeg для работы ожидает следующий формат аргументов ffmpeg {1} {2} -i {3} {4} {5}, где:
- глобальные параметры
- параметры входного файла
- адрес входного файла
- параметры выходного файла
- адрес выходного файла
Части 2, 3, 4 и 5 могут использованы столько раз, сколько это необходимо. Этот формат лучше понимается на примерах:
# Предупреждение: размер файла около 300MB
$ wget -O bunny_1080p_60fps.mp4 http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_60fps_normal.mp4
$ ffmpeg \
-y \ # {1} глобальные параметры
-c:a libfdk_aac -c:v libx264 \ # {2} параметры входного файла
-i bunny_1080p_60fps.mp4 \ # {3} адрес входного файла
-c:v libvpx-vp9 -c:a libvorbis \ # {4} параметры выходного файла
bunny_1080p_60fps_vp9.webm # {5} адрес выходного файлаThis command takes an input file mp4 containing two streams (an audio encoded with aac CODEC and a video encoded using h264 CODEC) and convert it to webm, changing its audio and video CODECs too.
We could simplify the command above but then be aware that FFmpeg will adopt or guess the default values for you.
For instance when you just type ffmpeg -i input.avi output.mp4 what audio/video CODEC does it use to produce the output.mp4?
Werner Robitza wrote a must read/execute tutorial about encoding and editing with FFmpeg.
While working with audio/video we usually do a set of tasks with the media.
What? the act of converting one of the streams (audio or video) from one CODEC to another one.
Why? sometimes some devices (TVs, smartphones, console and etc) doesn't support X but Y and newer CODECs provide better compression rate.
How? converting an H264 (AVC) video to an H265 (HEVC).
$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-c:v libx265 \
bunny_1080p_60fps_h265.mp4What? the act of converting from one format (container) to another one.
Why? sometimes some devices (TVs, smartphones, console and etc) doesn't support X but Y and sometimes newer containers provide modern required features.
How? converting a mp4 to a webm.
$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-c copy \ # just saying to ffmpeg to skip encoding
bunny_1080p_60fps.webmWhat? the act of changing the bit rate, or producing other renditions.
Why? people will try to watch your video in a 2G (edge) connection using a less powerful smartphone or in a fiber Internet connection on their 4K TVs therefore you should offer more than on rendition of the same video with different bit rate.
How? producing a rendition with bit rate between 3856K and 2000K.
$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-minrate 964K -maxrate 3856K -bufsize 2000K \
bunny_1080p_60fps_transrating_964_3856.mp4Usually we'll be using transrating with transsizing. Werner Robitza wrote another must read/execute series of posts about FFmpeg rate control.
What? the act of converting from one resolution to another one. As said before transsizing is often used with transrating.
Why? reasons are about the same as for the transrating.
How? converting a 1080p to a 480p resolution.
$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-vf scale=480:-1 \
bunny_1080p_60fps_transsizing_480.mp4What? the act of producing many resolutions (bit rates) and split the media into chunks and serve them via http.
Why? to provide a flexible media that can be watched on a low end smartphone or on a 4K TV, it's also easy to scale and deploy but it can add latency.
How? creating an adaptive WebM using DASH.
# video streams
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 160x90 -b:v 250k -keyint_min 150 -g 150 -an -f webm -dash 1 video_160x90_250k.webm
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 320x180 -b:v 500k -keyint_min 150 -g 150 -an -f webm -dash 1 video_320x180_500k.webm
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 640x360 -b:v 750k -keyint_min 150 -g 150 -an -f webm -dash 1 video_640x360_750k.webm
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 640x360 -b:v 1000k -keyint_min 150 -g 150 -an -f webm -dash 1 video_640x360_1000k.webm
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 1280x720 -b:v 1500k -keyint_min 150 -g 150 -an -f webm -dash 1 video_1280x720_1500k.webm
# audio streams
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:a libvorbis -b:a 128k -vn -f webm -dash 1 audio_128k.webm
# the DASH manifest
$ ffmpeg \
-f webm_dash_manifest -i video_160x90_250k.webm \
-f webm_dash_manifest -i video_320x180_500k.webm \
-f webm_dash_manifest -i video_640x360_750k.webm \
-f webm_dash_manifest -i video_640x360_1000k.webm \
-f webm_dash_manifest -i video_1280x720_500k.webm \
-f webm_dash_manifest -i audio_128k.webm \
-c copy -map 0 -map 1 -map 2 -map 3 -map 4 -map 5 \
-f webm_dash_manifest \
-adaptation_sets "id=0,streams=0,1,2,3,4 id=1,streams=5" \
manifest.mpdPS: I stole this example from the Instructions to playback Adaptive WebM using DASH
There are many and many other usages for FFmpeg. I use it in conjunction with iMovie to produce/edit some videos for YouTube and you can certainly use it professionally.
Don't you wonder sometimes 'bout sound and vision? David Robert Jones
Since the FFmpeg is so useful as a command line tool to do essential tasks over the media files, how can we use it in our programs?
FFmpeg is composed by several libraries that can be integrated into our own programs. Usually, when you install FFmpeg, it installs automatically all these libraries. I'll be referring to the set of these libraries as FFmpeg libav.
This title is a homage to Zed Shaw's series Learn X the Hard Way, particularly his book Learn C the Hard Way.
This hello world actually won't show the message "hello world" in the terminal 👅
Instead we're going to print out information about the video, things like its format (container), duration, resolution, audio channels and, in the end, we'll decode some frames and save them as image files.
But before we start to code, let's learn how FFmpeg libav architecture works and how its components communicate with others.
Here's a diagram of the process of decoding a video:
You'll first need to load your media file into a component called AVFormatContext (the video container is also known as format).
It actually doesn't fully load the whole file: it often only reads the header.
Once we loaded the minimal header of our container, we can access its streams (think of them as a rudimentary audio and video data).
Each stream will be available in a component called AVStream.
Stream is a fancy name for a continuous flow of data.
Suppose our video has two streams: an audio encoded with AAC CODEC and a video encoded with H264 (AVC) CODEC. From each stream we can extract pieces (slices) of data called packets that will be loaded into components named AVPacket.
The data inside the packets are still coded (compressed) and in order to decode the packets, we need to pass them to a specific AVCodec.
The AVCodec will decode them into AVFrame and finally, this component gives us the uncompressed frame. Noticed that the same terminology/process is used either by audio and video stream.
TLDR; show me the code and execution.
# WARNING: this file is around 300MB $ make
We'll skip some details, but don't worry: the source code is available at github.
The first thing we need to do is to register all the codecs, formats and protocols.
To do it, we just need to call the function av_register_all:
av_register_all();Now we're going to allocate memory to the component AVFormatContext that will hold information about the format (container).
AVFormatContext *pFormatContext = avformat_alloc_context();Now we're going to open the file and read its header and fill the AVFormatContext with minimal information about the format (notice that usually the codecs are not opened).
The function used to do this is avformat_open_input. It expects an AVFormatContext, a filename and two optional arguments: the AVInputFormat (if you pass NULL, FFmpeg will guess the format) and the AVDictionary (which are the options to the demuxer).
avformat_open_input(&pFormatContext, filename, NULL, NULL);We can print the format name and the media duration:
printf("Format %s, duration %lld us", pFormatContext->iformat->long_name, pFormatContext->duration);To access the streams, we need to read data from the media. The function avformat_find_stream_info does that.
Now, the pFormatContext->nb_streams will hold the amount of streams and the pFormatContext->streams[i] will give us the i stream (an AVStream).
avformat_find_stream_info(pFormatContext, NULL);Now we'll loop through all the streams.
for (int i = 0; i < pFormatContext->nb_streams; i++)
{
//
}For each stream, we're going to keep the AVCodecParameters, which describes the properties of a codec used by the stream i.
AVCodecParameters *pLocalCodecParameters = pFormatContext->streams[i]->codecpar;With the codec properties we can look up the proper CODEC querying the function avcodec_find_decoder and find the registered decoder for the codec id and return an AVCodec, the component that knows how to enCOde and DECode the stream.
AVCodec *pLocalCodec = avcodec_find_decoder(pLocalCodecParameters->codec_id);Now we can print information about the codecs.
// specific for video and audio
if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_VIDEO) {
printf("Video Codec: resolution %d x %d", pLocalCodecParameters->width, pLocalCodecParameters->height);
} else if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_AUDIO) {
printf("Audio Codec: %d channels, sample rate %d", pLocalCodecParameters->channels, pLocalCodecParameters->sample_rate);
}
// general
printf("\tCodec %s ID %d bit_rate %lld", pLocalCodec->long_name, pLocalCodec->id, pCodecParameters->bit_rate);With the codec, we can allocate memory for the AVCodecContext, which will hold the context for our decode/encode process, but then we need to fill this codec context with CODEC parameters; we do that with avcodec_parameters_to_context.
Once we filled the codec context, we need to open the codec. We call the function avcodec_open2 and then we can use it.
AVCodecContext *pCodecContext = avcodec_alloc_context3(pCodec);
avcodec_parameters_to_context(pCodecContext, pCodecParameters);
avcodec_open2(pCodecContext, pCodec, NULL);Now we're going to read the packets from the stream and decode them into frames but first, we need to allocate memory for both components, the AVPacket and AVFrame.
AVPacket *pPacket = av_packet_alloc();
AVFrame *pFrame = av_frame_alloc();Let's feed our packets from the streams with the function av_read_frame while it has packets.
while (av_read_frame(pFormatContext, pPacket) >= 0) {
//...
}Let's send the raw data packet (compressed frame) to the decoder, through the codec context, using the function avcodec_send_packet.
avcodec_send_packet(pCodecContext, pPacket);And let's receive the raw data frame (uncompressed frame) from the decoder, through the same codec context, using the function avcodec_receive_frame.
avcodec_receive_frame(pCodecContext, pFrame);We can print the frame number, the PTS, DTS, frame type and etc.
printf(
"Frame %c (%d) pts %d dts %d key_frame %d [coded_picture_number %d, display_picture_number %d]",
av_get_picture_type_char(pFrame->pict_type),
pCodecContext->frame_number,
pFrame->pts,
pFrame->pkt_dts,
pFrame->key_frame,
pFrame->coded_picture_number,
pFrame->display_picture_number
);Finally we can save our decoded frame into a simple gray image. The process is very simple, we'll use the pFrame->data where the index is related to the planes Y, Cb and Cr, we just picked 0 (Y) to save our gray image.
save_gray_frame(pFrame->data[0], pFrame->linesize[0], pFrame->width, pFrame->height, frame_filename);
static void save_gray_frame(unsigned char *buf, int wrap, int xsize, int ysize, char *filename)
{
FILE *f;
int i;
f = fopen(filename,"w");
// writing the minimal required header for a pgm file format
// portable graymap format -> https://en.wikipedia.org/wiki/Netpbm_format#PGM_example
fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255);
// writing line by line
for (i = 0; i < ysize; i++)
fwrite(buf + i * wrap, 1, xsize, f);
fclose(f);
}And voilà! Now we have a gray scale image with 2MB:
Be the player - a young JS developer writing a new MSE video player.
Before we move to code a transcoding example let's talk about timing, or how a video player knows the right time to play a frame.
In the last example, we saved some frames that can be seen here:
When we're designing a video player we need to play each frame at a given pace, otherwise it would be hard to pleasantly see the video either because it's playing so fast or so slow.
Therefore we need to introduce some logic to play each frame smoothly. For that matter, each frame has a presentation timestamp (PTS) which is an increasing number factored in a timebase that is a rational number (where the denominator is known as timescale) divisible by the frame rate (fps).
It's easier to understand when we look at some examples, let's simulate some scenarios.
For a fps=60/1 and timebase=1/60000 each PTS will increase timescale / fps = 1000 therefore the PTS real time for each frame could be (supposing it started at 0):
frame=0, PTS = 0, PTS_TIME = 0frame=1, PTS = 1000, PTS_TIME = PTS * timebase = 0.016frame=2, PTS = 2000, PTS_TIME = PTS * timebase = 0.033
For almost the same scenario but with a timebase equal to 1/60.
frame=0, PTS = 0, PTS_TIME = 0frame=1, PTS = 1, PTS_TIME = PTS * timebase = 0.016frame=2, PTS = 2, PTS_TIME = PTS * timebase = 0.033frame=3, PTS = 3, PTS_TIME = PTS * timebase = 0.050
For a fps=25/1 and timebase=1/75 each PTS will increase timescale / fps = 3 and the PTS time could be:
frame=0, PTS = 0, PTS_TIME = 0frame=1, PTS = 3, PTS_TIME = PTS * timebase = 0.04frame=2, PTS = 6, PTS_TIME = PTS * timebase = 0.08frame=3, PTS = 9, PTS_TIME = PTS * timebase = 0.12- ...
frame=24, PTS = 72, PTS_TIME = PTS * timebase = 0.96- ...
frame=4064, PTS = 12192, PTS_TIME = PTS * timebase = 162.56
Now with the pts_time we can find a way to render this synched with audio pts_time or with a system clock. The FFmpeg libav provides these info through its API:
- fps =
AVStream->avg_frame_rate - tbr =
AVStream->r_frame_rate - tbn =
AVStream->time_base
Just out of curiosity, the frames we saved were sent in a DTS order (frames: 1,6,4,2,3,5) but played at a PTS order (frames: 1,2,3,4,5). Also, notice how cheap are B-Frames in comparison to P or I-Frames.
LOG: AVStream->r_frame_rate 60/1
LOG: AVStream->time_base 1/60000
...
LOG: Frame 1 (type=I, size=153797 bytes) pts 6000 key_frame 1 [DTS 0]
LOG: Frame 2 (type=B, size=8117 bytes) pts 7000 key_frame 0 [DTS 3]
LOG: Frame 3 (type=B, size=8226 bytes) pts 8000 key_frame 0 [DTS 4]
LOG: Frame 4 (type=B, size=17699 bytes) pts 9000 key_frame 0 [DTS 2]
LOG: Frame 5 (type=B, size=6253 bytes) pts 10000 key_frame 0 [DTS 5]
LOG: Frame 6 (type=P, size=34992 bytes) pts 11000 key_frame 0 [DTS 1]














