FFmpeg and SDL : Part 1 of Coding a Video Player in less than 1000 lines of C code
Part 1 of a hands-on guide to the FFmpeg C programming language API in 2023
You’ll generate a similar picture entirely in C!
Hey guys,
My name is Murage Kibicho and my team helps programmers escape FFmpeg commandline hell. We teach developers to utilize the underlying C data structures and thus, avoid long and complicated command line text ✨. This tutorial is somewhat long so we split the guide into 3 parts. In Part 1, we extract image data from a video file. In Part 2, we extract audio data from a video file. Finally, we create the video player using SDL in part 3.
Before we start, here’s an FFmpeg Memory Leak Cheatsheet. We need this to free memory correctly. We assume you have FFmpeg installed on your system. Here’s a YouTube video setting up FFmpeg inside the Replit online IDE.
Let’s start! Here’s the GitHub repo and if you’re interested in a premium, guided introduction to the FFmpeg C API you can join us here.
Step 1: Include all the libraries we need to compile FFmpeg.
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h> //uint8, PRI64 macro
#include <libavcodec/avcodec.h> //encoders and decoders
#include <libavformat/avformat.h> //Muxing and demuxing
#include <libavutil/avutil.h> //C Data Structures
#include <libavutil/imgutils.h> //C Data Structures
#include <libswscale/swscale.h> //Scaling and colorspaces
int main()
{
return 0;
}
Step 2: Ensure our FFmpeg environment compiles using GCC. This command links avcodec, avutil, avformat and swscale.
gcc Part1.c -o Part1.o -lm -lavcodec -lavutil -lavformat -lswscale && ./Part1.o
Bonus Content: If you have pkg-config installed on your system you can link and compile the FFmpeg C API this way! Subscribe to get other tips!
gcc Part1.c -o Part1.o `pkg-config --cflags --libs libavformat libavcodec libswresample libswscale libavutil` && ./Part1.o
Step 3 : Declare all the variables we need to open a video stream. I won’t get into too much detail about these data structures. If you’re interested, I teach a course on FFmpeg data structures and navigating the C documentation here.
char *fileName = "MurageKibicho.mp4";
//Main Data structures to audio and video data
AVFormatContext *formatContext = NULL;
AVPacket *packet = NULL;
AVFrame *videoFrame = NULL;
AVCodecParameters *videoCodecParameters = NULL;
AVCodec *videoCodec = NULL;
AVCodecContext *videoCodecContext = NULL;
int videoStreamIndex = -1;
struct SwsContext *swsContext = NULL;
AVCodecParameters *audioCodecParameters = NULL;
AVCodec *audioCodec = NULL;
AVCodecContext *audioCodecContext = NULL;
int audioStreamIndex = -1;
int returnValue = 0;
Step 4: Ensure your video file exists and allocate memory for FFmpeg Data Structures.
/*Check File Exists and allocate format context*/
returnValue = avformat_open_input(&formatContext, fileName, NULL, NULL);
if(returnValue != 0){av_log(NULL, AV_LOG_ERROR, "Error opening file");return -1;}
/*Allocate Packet*/
packet = av_packet_alloc();
if(!packet){av_log(NULL, AV_LOG_ERROR, "Error allocating packet");return -1;}
/*Allocate Frame*/
videoFrame = av_frame_alloc();
if(!videoFrame){av_log(NULL, AV_LOG_ERROR, "Error allocating packet");return -1;}
/*Ensure stream information exists*/
if(avformat_find_stream_info(formatContext, NULL) < 0){av_log(NULL, AV_LOG_ERROR, "Stream information not found");return -1;}
//maybe print streams for sanity check
Step 5 : Find audio, video and subtitle data inside a loop.
for(int i = 0; i < formatContext->nb_streams; i++)
{
AVCodecParameters *currentCodecParameters = NULL;
AVCodec *currentCodec = NULL;
AVStream *currentStream = NULL;
currentStream = formatContext->streams[i];
currentCodecParameters = currentStream->codecpar;
currentCodec = avcodec_find_decoder(currentCodecParameters->codec_id);
if(currentCodec == NULL){av_log(NULL, AV_LOG_ERROR, "Codec not supported");continue;}
if(currentCodecParameters->codec_type == AVMEDIA_TYPE_VIDEO)
{
double frameRate = av_q2d(currentStream->r_frame_rate);
videoStreamIndex = i;
videoCodec = currentCodec;
videoCodecParameters = currentCodecParameters;
printf("\nFound video stream\n");
printf("ID: %d\n Codec: %s\n BitRate: %ld\n Width :%d, Height %d\n Framerate: %f fps\n", currentCodecParameters->codec_id, currentCodec->name, currentCodecParameters->bit_rate, currentCodecParameters->width, currentCodecParameters->height, frameRate);
}
else if(currentCodecParameters->codec_type == AVMEDIA_TYPE_AUDIO)
{
audioStreamIndex = i;
audioCodec = currentCodec;
audioCodecParameters = currentCodecParameters;
printf("\nFound audio stream\n");
printf("ID: %d\n Codec: %s\n BitRate: %ld\nChannels :%d, Sample Rate %d\n\n",currentCodecParameters->codec_id, currentCodec->name, currentCodecParameters->bit_rate, currentCodecParameters->channels, currentCodecParameters->sample_rate);
}
else if(currentCodecParameters->codec_type == AVMEDIA_TYPE_SUBTITLE)
{
printf("\nFound subtitle stream\n");
}
}
Step 6 : Ensure a video stream exists and allocate memory for the codec context. Here, we also open the codec.
if(videoStreamIndex == -1){av_log(NULL, AV_LOG_ERROR, "Error Not found video stream");return -1;}
/*Allocate codec context*/
videoCodecContext = avcodec_alloc_context3(videoCodec);
if(!videoCodecContext){av_log(NULL, AV_LOG_ERROR, "Error allocating codec context");return -1;}
returnValue = avcodec_parameters_to_context(videoCodecContext, videoCodecParameters);
if(returnValue != 0){av_log(NULL, AV_LOG_ERROR, "Error copying codec parameters to context");return -1;}
returnValue = avcodec_open2(videoCodecContext, videoCodec, NULL);
if(returnValue != 0){av_log(NULL, AV_LOG_ERROR, "Error opening avcodec");return -1;}
int packetCount = 0;
Step 7 : Read the frame and extract compressed packets.
NOTE: In step 10 we write the function DecodeVideoPacket_GreyFrame().
while(av_read_frame(formatContext,packet) >= 0)
{
if(packet->stream_index == videoStreamIndex)
{
int64_t duration = packet->pts;
printf("Video packet : ");
FormatDuration(duration);
returnValue = DecodeVideoPacket_GreyFrame(packet, videoCodecContext, videoFrame);//We will write this function in step 10
//break;
}
else if(packet->stream_index == audioStreamIndex)
{
int64_t duration = packet->pts;
printf("Audio packet : ");
FormatDuration(duration);
}
packetCount += 1;
av_packet_unref(packet);
if(packetCount == 10){break;}//Comment this out to loop through the entire file
}
Step 8: Free memory to avoid leaks
/*Free memory*/
sws_freeContext(swsContext);
avformat_close_input(&formatContext);
av_packet_free(&packet);
av_free(videoFrame);
avcodec_free_context(&videoCodecContext);
Step 9 : Sanity Check. Your program should look like this and should compile with gcc
#include <assert.h>
#include <inttypes.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//gcc main.c -o main.o -lm -lavcodec -lavutil -lavformat && ./main.o
int main(void) {
char *fileName = "out.mp4";
AVFormatContext *formatContext = NULL;
AVPacket *packet = NULL;
AVFrame *videoFrame = NULL;
AVCodecParameters *videoCodecParameters = NULL;
AVCodec *videoCodec = NULL;
AVCodecContext *videoCodecContext = NULL;
int videoStreamIndex = -1;
AVCodecParameters *audioCodecParameters = NULL;
AVCodec *audioCodec = NULL;
AVCodecContext *audioCodecContext = NULL;
int audioStreamIndex = -1;
int returnValue = 0;
/*Check File Exists and allocate format context*/
returnValue = avformat_open_input(&formatContext, fileName, NULL, NULL);
if(returnValue != 0){av_log(NULL, AV_LOG_ERROR, "Error opening file");return -1;}
/*Allocate Packet*/
packet = av_packet_alloc();
if(!packet){av_log(NULL, AV_LOG_ERROR, "Error allocating packet");return -1;}
/*Allocate Frame*/
videoFrame = av_frame_alloc();
if(!videoFrame){av_log(NULL, AV_LOG_ERROR, "Error allocating packet");return -1;}
/*Ensure stream information exists*/
if(avformat_find_stream_info(formatContext, NULL) < 0){av_log(NULL, AV_LOG_ERROR, "Stream information not found");return -1;}
for(int i = 0; i < formatContext->nb_streams; i++)
{
AVCodecParameters *currentCodecParameters = NULL;
AVCodec *currentCodec = NULL;
AVStream *currentStream = NULL;
currentStream = formatContext->streams[i];
currentCodecParameters = currentStream->codecpar;
currentCodec = avcodec_find_decoder(currentCodecParameters->codec_id);
if(currentCodec == NULL){av_log(NULL, AV_LOG_ERROR, "Codec not supported");continue;}
if(currentCodecParameters->codec_type == AVMEDIA_TYPE_VIDEO)
{
double frameRate = av_q2d(currentStream->r_frame_rate);
videoStreamIndex = i;
videoCodec = currentCodec;
videoCodecParameters = currentCodecParameters;
printf("\nFound video stream\n");
printf("ID: %d\n Codec: %s\n BitRate: %ld\n Width :%d, Height %d\n Framerate: %f fps\n", currentCodecParameters->codec_id, currentCodec->name, currentCodecParameters->bit_rate, currentCodecParameters->width, currentCodecParameters->height, frameRate);
}
else if(currentCodecParameters->codec_type == AVMEDIA_TYPE_AUDIO)
{
audioStreamIndex = i;
audioCodec = currentCodec;
audioCodecParameters = currentCodecParameters;
printf("\nFound audio stream\n");
printf("ID: %d\n Codec: %s\n BitRate: %ld\nChannels :%d, Sample Rate %d\n\n",currentCodecParameters->codec_id, currentCodec->name, currentCodecParameters->bit_rate, currentCodecParameters->channels, currentCodecParameters->sample_rate);
}
else if(currentCodecParameters->codec_type == AVMEDIA_TYPE_SUBTITLE)
{
printf("\nFound subtitle stream\n");
}
}
if(videoStreamIndex == -1){av_log(NULL, AV_LOG_ERROR, "Error Not found video stream");return -1;}
/*Allocate codec context*/
videoCodecContext = avcodec_alloc_context3(videoCodec);
if(!videoCodecContext){av_log(NULL, AV_LOG_ERROR, "Error allocating codec context");return -1;}
//https://ffmpeg.org/doxygen/trunk/group__lavc__core.html#ga8a4998c9d1695abb01d379539d313227
returnValue = avcodec_parameters_to_context(videoCodecContext, videoCodecParameters);
if(returnValue != 0){av_log(NULL, AV_LOG_ERROR, "Error copying codec parameters to context");return -1;}
returnValue = avcodec_open2(videoCodecContext, videoCodec, NULL);
if(returnValue != 0){av_log(NULL, AV_LOG_ERROR, "Error opening avcodec");return -1;}
int packetCount = 0;
while(av_read_frame(formatContext,packet) >= 0)
{
if(packet->stream_index == videoStreamIndex)
{
int64_t duration = packet->pts;
printf("Video packet : ");
//returnValue = DecodeVideoPacket_GreyFrame(packet, videoCodecContext, videoFrame);
//break;
}
else if(packet->stream_index == audioStreamIndex)
{
int64_t duration = packet->pts;
printf("Audio packet : ");
}
packetCount += 1;
if(packetCount == 10){break;}
}
/*Free memory*/
avformat_close_input(&formatContext);
av_packet_free(&packet);
av_free(videoFrame);
avcodec_free_context(&videoCodecContext);
return 0;
}
Step 10: Let’s decode a video packet. We extract images and save them as PPM files. Remember to uncomment this line. DecodeVideoPacket_GreyFrame
int DecodeVideoPacket_GreyFrame(AVPacket *packet, AVCodecContext *codecContext, AVFrame *frame)
{
int returnValue = 0;
//Send compressed packet for decompression
returnValue = avcodec_send_packet(codecContext, packet);
if(returnValue != 0){av_log(NULL, AV_LOG_ERROR, "Error decompressing packet");return returnValue;}
while(returnValue >= 0)
{
returnValue = avcodec_receive_frame(codecContext, frame);
if(returnValue == AVERROR(EAGAIN))
{
//Not data memory for frame, have to free and get more data
printf("Not enough data\n");
av_frame_unref(frame);
av_freep(frame);
break;
}
else if(returnValue == AVERROR_EOF){av_log(NULL, AV_LOG_ERROR, "End Of File Reached");av_frame_unref(frame);av_freep(frame);return returnValue;}
else if(returnValue < 0){av_log(NULL, AV_LOG_ERROR, "Error in receiving frame");av_frame_unref(frame);av_freep(frame);return returnValue;}
else
{
// We got a picture!
printf( "Frame number %d (type=%c frame, size = %d bytes, width = %d, height = %d) pts %ld key_frame %d [DTS %d]\n",codecContext->frame_number,av_get_picture_type_char(frame->pict_type),frame->pkt_size,frame->width,frame->height,frame->pts,frame->key_frame,frame->coded_picture_number);
//SaveGreyFramePPM(frame->data[0],frame->linesize[0],frame->height,frame->width, "Test.ppm");
}
}
return returnValue;
}
Step 11. Uncomment and write the function to save grey images, SaveGreyFramePPM
void SaveGreyFramePPM(uint8_t *pixels, int wrap, int imageHeight, int imageWidth, char *fileName)
{
FILE *fp = fopen(fileName, "w");
printf("\n\nWrap: %d\n\n", wrap);
fprintf(fp,"P5\n%d %d\n%d\n",imageWidth,imageHeight,255);
for(int i = 0; i < imageHeight; i++)
{
unsigned char *ch = (pixels+i*wrap);
fwrite(ch,1,imageWidth,fp);
}
fclose(fp);
}
Step 12. We have a grey picture in ppm. This opens on Ubuntu. You can view the image on this website. Here’s my output.
In the next post, we extract raw audio from the video file. I hope you enjoyed and please subscribe!