Building a Video Player using Qt and FFmpeg

Configuring Qt Creator

Often, Qt Creator is a good development tool for Qt. But in 2026, most popular desktop monitors are 24-inch or 27-inch 2K or 4K displays. If you set Windows scaling to 150%, other applications look good. However, in Qt Creator you may see very large icons and fonts.

The good news is that Qt Creator offers some settings to solve this problem. Take my screen as an example: I use a 27-inch 2K monitor on Windows 10 with 150% scaling. After applying these settings, Qt Creator displays nicely, just like the system at 150% scaling.

In Qt Creator, open Settings > Environment > Interface, then change “DPI rounding policy” to “Don’t Round”. Right-click the left panel and choose “Icon Only”.

SharedScreenshot.jpg

Overview for FFmpeg

FFmpeg is a collection of libraries and tools for processing multimedia content such as audio, video, subtitles, and related metadata.

It provides the following seven libraries:

  1. libavcodec – encoding/decoding library
  2. libavfilter – graph-based frame editing library
  3. libavformat – I/O and muxing/demuxing library
  4. libavdevice – muxing/demuxing library for special devices
  5. libavutil – common utility library
  6. libswresample – audio resampling, format conversion, and mixing
  7. libswscale – color conversion and scaling library

For a simple local video player, libavformat demuxes media containers (e.g., .mp4, .flv, .mkv) into compressed packets, and libavcodec decodes them into raw video frames and audio frames (typically PCM), which may then need further conversion (e.g., via libswscale and libswresample) for display and playback.

Overview of This Video Player Project

In this project, we will use the following technologies:

  1. Qt Core with OpenGL (GUI for this video player)
  2. FFmpeg (for demuxing media containers and decoding compressed packets)
  3. QAudioSink (for audio playback)

Media containers contain multiple streams, and a video stream is split into many packets. Each packet carries timing information, such as PTS and DTS, which tells when it needs to be decoded and displayed. Video compression uses I, B, and P frames to compress video based on temporal order.

The good news is that libavformat demuxes a container and returns packets in DTS order. When you push these packets to libavcodec, you will get frames in PTS order.

Another issue is how to make video playback match audio playback. There are three ways to sync A/V: using an external clock, using the audio clock as the master clock, or using the video clock as the master clock. This project will use the audio clock as the master clock.

img

Demuxer Implementation

In the Demuxer, we need to use AVFormatContext to open a media file:

int avformat_open_input(AVFormatContext **ps, const char *url, const AVInputFormat *fmt, AVDictionary **options);

then Find media file stream info:

int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

for (unsigned i = 0; i < fmtCtx_->nb_streams; ++i)
{
    auto type = fmtCtx_->streams[i]->codecpar->codec_type;
    if (type == AVMEDIA_TYPE_VIDEO && videoStreamIdx_ < 0)
        videoStreamIdx_ = static_cast<int>(i);
    else if (type == AVMEDIA_TYPE_AUDIO && audioStreamIdx_ < 0)
        audioStreamIdx_ = static_cast<int>(i);
}

In demuxer thread we need read frame and split by index:

int av_read_frame(AVFormatContext *s, AVPacket *pkt);

if (pkt->stream_index == videoStreamIdx_ && videoQueue_)
{
    videoQueue_->push(pkt);
    ++videoCount;
}
else if (pkt->stream_index == audioStreamIdx_ && audioQueue_)
{
    audioQueue_->push(pkt);
    ++audioCount;
}

Decoder Implementation

To decode a frame we first need to get the media stream need which decoder:

AVFormatContext *fmt = demuxer_->formatContext();
AVStream *vStream = fmt->streams[vIdx];
const AVCodec *vCodec = avcodec_find_decoder(vStream->codecpar->codec_id);

videoCodecCtx_ = avcodec_alloc_context3(vCodec);
avcodec_parameters_to_context(videoCodecCtx_, vStream->codecpar);
avcodec_open2(videoCodecCtx_, vCodec, nullptr);

After get codec context, we can pass packet and get frame:

avcodec_send_packet(codecCtx_, pkt);
while (!stop){
    avcodec_receive_frame(codecCtx_, frame);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
        break;
    if (ret < 0)
        break;
}

Full Code

The complete project is published at: https://github.com/NoahBishop/TilePlayer

Capture.jpg