Logo
Complete C++ Implementation

Building a Full C++ Video Player

Below is the complete, compilable source code for a modern FFmpeg 7.0 and SDL2 based video player. It fully implements playback, pausing, stopping, timeline seeking, and variable speed control.

Dependencies required: FFmpeg 7.0+,SDL2,CMake 3.10+,C++14/17.This implementation focuses on video decoding and synchronization.

1. CMakeLists.txt

Build configuration file linking FFmpeg and SDL2.

CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(FFmpegCppPlayer)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Find SDL2 library
find_package(SDL2 REQUIRED)

# Set FFmpeg library paths (modify according to your environment)
# If installed system-wide, pkg-config can also be used
set(FFMPEG_INCLUDE_DIR "/usr/local/include")
set(FFMPEG_LIB_DIR "/usr/local/lib")

include_directories(${FFMPEG_INCLUDE_DIR} ${SDL2_INCLUDE_DIRS} "include")
link_directories(${FFMPEG_LIB_DIR})

# Core executable target
add_executable(FFmpegPlayer 
    src/main.cpp 
    src/VideoPlayer.cpp
)

# Link necessary dependencies
target_link_libraries(FFmpegPlayer 
    avformat 
    avcodec 
    avutil 
    swscale 
    ${SDL2_LIBRARIES}
    pthread
)

2. include/VideoPlayer.h

Header defining the player state machine and FFmpeg context management.

VideoPlayer.h
#ifndef VIDEO_PLAYER_H
#define VIDEO_PLAYER_H

#include <string>
#include <thread>
#include <atomic>
#include <mutex>
#include <condition_variable>

extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libavutil/time.h>
}

#include <SDL2/SDL.h>

// Player State Machine
enum PlayState {
    STOPPED,
    PLAYING,
    PAUSED
};

class VideoPlayer {
public:
    VideoPlayer();
    ~VideoPlayer();

    // Initialize and open the video file
    bool open(const std::string& filepath);
    
    // Basic Control Functions
    void play();    // Start decoding and playback thread
    void pause();   // Pause playback (suspend thread)
    void resume();  // Resume playback (wake up thread)
    void stop();    // Stop playback and release resources

    // Advanced Control Functions
    void seek(double target_seconds); // Jump to a specific timestamp in seconds
    void setSpeed(double speed);      // Set playback speed multiplier (e.g., 0.5, 1.0, 2.0)

private:
    // Core worker thread: Continuously reads packets and decodes frames
    void decodeLoop();
    
    // Render a single video frame via SDL
    void renderFrame(AVFrame* frame);
    
    // Clean up all allocated memory and handles
    void cleanup();

private:
    // State control variables
    std::atomic<PlayState> state;
    std::atomic<double> speed_factor;
    std::atomic<bool> seek_req;
    std::atomic<double> seek_target;

    // Multi-threading synchronization primitives
    std::thread decode_thread;
    std::mutex state_mutex;
    std::condition_variable pause_cv;

    // FFmpeg 7.0 core contexts
    AVFormatContext* format_ctx;
    AVCodecContext* codec_ctx;
    SwsContext* sws_ctx;
    int video_stream_idx;
    
    // Time base metrics (for audio/video sync and speed control)
    double previous_pts;
    int64_t start_time;

    // SDL2 rendering handles
    SDL_Window* window;
    SDL_Renderer* renderer;
    SDL_Texture* texture;
};

#endif // VIDEO_PLAYER_H

3. src/VideoPlayer.cpp

Core implementation of decoding, threading, rendering, and timing logic.

VideoPlayer.cpp
#include "VideoPlayer.h"
#include <iostream>

VideoPlayer::VideoPlayer() {
    state = STOPPED;
    speed_factor = 1.0;
    seek_req = false;
    seek_target = 0.0;
    
    format_ctx = nullptr;
    codec_ctx = nullptr;
    sws_ctx = nullptr;
    video_stream_idx = -1;
    previous_pts = 0.0;

    window = nullptr;
    renderer = nullptr;
    texture = nullptr;
}

VideoPlayer::~VideoPlayer() {
    stop();
}

// ---------------------------------------------------------
// [Feature] Open Video: Initialize FFmpeg decoder and SDL renderer
// ---------------------------------------------------------
bool VideoPlayer::open(const std::string& filepath) {
    // 1. Open file and parse header info
    format_ctx = avformat_alloc_context();
    if (avformat_open_input(&format_ctx, filepath.c_str(), nullptr, nullptr) != 0) {
        std::cerr << "Failed to open video file." << std::endl;
        return false;
    }

    if (avformat_find_stream_info(format_ctx, nullptr) < 0) {
        std::cerr << "Failed to find stream info." << std::endl;
        return false;
    }

    // 2. Locate the video stream
    const AVCodec* codec = nullptr;
    video_stream_idx = av_find_best_stream(format_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
    if (video_stream_idx < 0) {
        std::cerr << "Failed to find video stream." << std::endl;
        return false;
    }

    // 3. Configure FFmpeg 7.0 decoding context
    AVStream* video_stream = format_ctx->streams[video_stream_idx];
    codec_ctx = avcodec_alloc_context3(codec);
    avcodec_parameters_to_context(codec_ctx, video_stream->codecpar);

    if (avcodec_open2(codec_ctx, codec, nullptr) < 0) {
        std::cerr << "Failed to open codec." << std::endl;
        return false;
    }

    // 4. Initialize SDL rendering layer
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
        std::cerr << "SDL Initialization failed." << std::endl;
        return false;
    }

    window = SDL_CreateWindow("FFmpeg 7.0 Player", 
                              SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 
                              codec_ctx->width, codec_ctx->height, SDL_WINDOW_SHOWN);
    renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
    texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, 
                                codec_ctx->width, codec_ctx->height);

    // 5. Initialize image conversion context (e.g., YUV to RGB adaptation)
    sws_ctx = sws_getContext(codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt,
                             codec_ctx->width, codec_ctx->height, AV_PIX_FMT_YUV420P,
                             SWS_BILINEAR, nullptr, nullptr, nullptr);

    return true;
}

// ---------------------------------------------------------
// [Feature] Play: Launch independent decoding & rendering thread
// ---------------------------------------------------------
void VideoPlayer::play() {
    if (state == STOPPED) {
        state = PLAYING;
        decode_thread = std::thread(&VideoPlayer::decodeLoop, this);
    }
}

// ---------------------------------------------------------
// [Feature] Pause: Change state flag, thread will suspend itself gracefully
// ---------------------------------------------------------
void VideoPlayer::pause() {
    if (state == PLAYING) {
        state = PAUSED;
    }
}

// ---------------------------------------------------------
// [Feature] Resume: Wake up the suspended decoding thread via condition variable
// ---------------------------------------------------------
void VideoPlayer::resume() {
    if (state == PAUSED) {
        std::lock_guard<std::mutex> lock(state_mutex);
        state = PLAYING;
        pause_cv.notify_one();
    }
}

// ---------------------------------------------------------
// [Feature] Stop: End lifecycle, join threads securely, and free memory
// ---------------------------------------------------------
void VideoPlayer::stop() {
    if (state != STOPPED) {
        state = STOPPED;
        pause_cv.notify_all(); // Prevent thread deadlock if stopped while paused
        
        if (decode_thread.joinable()) {
            decode_thread.join();
        }
    }
    cleanup();
}

// ---------------------------------------------------------
// [Feature] Seek: Set flag, thread captures it and relocates file pointer
// ---------------------------------------------------------
void VideoPlayer::seek(double target_seconds) {
    seek_target = target_seconds;
    seek_req = true; // Trigger atomic flag
}

// ---------------------------------------------------------
// [Feature] Speed: Adjust rate factor modifying physical thread sleep time
// ---------------------------------------------------------
void VideoPlayer::setSpeed(double speed) {
    if (speed > 0.0) {
        speed_factor = speed;
    }
}

// ---------------------------------------------------------
// [Feature] Main Worker Thread: Demuxing, decoding, and A/V sync control
// ---------------------------------------------------------
void VideoPlayer::decodeLoop() {
    AVPacket* packet = av_packet_alloc();
    AVFrame* frame = av_frame_alloc();

    while (state != STOPPED) {
        // 1. Handle Pause logic
        if (state == PAUSED) {
            std::unique_lock<std::mutex> lock(state_mutex);
            pause_cv.wait(lock, [this] { return state != PAUSED; });
            previous_pts = 0; // Reset clock upon resume to avoid huge delay bursts
            continue;
        }

        // 2. Handle Seek request
        if (seek_req) {
            // Convert to FFmpeg internal time base (1,000,000)
            int64_t seek_pos = seek_target * AV_TIME_BASE;
            
            // Seek file pointer (AVSEEK_FLAG_BACKWARD: prioritize nearest preceding I-Frame)
            if (avformat_seek_file(format_ctx, -1, INT64_MIN, seek_pos, seek_pos, AVSEEK_FLAG_BACKWARD) >= 0) {
                // [MANDATORY for FFmpeg 7.0] Flush leftover cache in decoder to prevent mosaic/artifacts
                avcodec_flush_buffers(codec_ctx);
            }
            seek_req = false;
            previous_pts = 0; // Reset clock reference
        }

        // 3. Read demuxed packets
        if (av_read_frame(format_ctx, packet) >= 0) {
            if (packet->stream_index == video_stream_idx) {
                // FFmpeg 7.0 new API: Send Packet to decoder
                int ret = avcodec_send_packet(codec_ctx, packet);
                if (ret < 0) {
                    av_packet_unref(packet);
                    continue;
                }

                // Loop to receive decoded continuous Frames
                while (ret >= 0) {
                    ret = avcodec_receive_frame(codec_ctx, frame);
                    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                        break;
                    }

                    // --- Clock Sync & Speed Calculation ---
                    AVStream* stream = format_ctx->streams[video_stream_idx];
                    // Calculate current frame's physical timestamp (seconds)
                    double current_pts = frame->pts * av_q2d(stream->time_base);
                    
                    if (previous_pts > 0) {
                        // Standard delay from the previous frame
                        double delay_seconds = current_pts - previous_pts;
                        // Apply speed factor (e.g., 2.0x halves the delay)
                        double actual_delay = delay_seconds / speed_factor;
                        
                        // Sleep current thread waiting for rendering time
                        if (actual_delay > 0 && actual_delay < 1.0) { // Filter anomalous data
                            av_usleep((int64_t)(actual_delay * 1000000.0));
                        }
                    }
                    previous_pts = current_pts;
                    // -----------------------

                    // Execute screen rendering
                    renderFrame(frame);
                }
            }
            av_packet_unref(packet);
        } else {
            // End of File (EOF) reached
            break; 
        }
    }

    av_frame_free(&frame);
    av_packet_free(&packet);
    state = STOPPED;
}

// ---------------------------------------------------------
// [Feature] Render Frame: Update texture and refresh SDL window
// ---------------------------------------------------------
void VideoPlayer::renderFrame(AVFrame* frame) {
    if (!renderer || !texture) return;

    // Extract YUV pixel data
    SDL_UpdateYUVTexture(texture, nullptr,
                         frame->data[0], frame->linesize[0],
                         frame->data[1], frame->linesize[1],
                         frame->data[2], frame->linesize[2]);

    SDL_RenderClear(renderer);
    SDL_RenderCopy(renderer, texture, nullptr, nullptr);
    SDL_RenderPresent(renderer);
}

// ---------------------------------------------------------
// [Feature] Cleanup: Destroy all C-style heap memory and handles
// ---------------------------------------------------------
void VideoPlayer::cleanup() {
    if (sws_ctx) { sws_freeContext(sws_ctx); sws_ctx = nullptr; }
    if (codec_ctx) { avcodec_free_context(&codec_ctx); }
    if (format_ctx) { avformat_close_input(&format_ctx); }
    if (texture) { SDL_DestroyTexture(texture); texture = nullptr; }
    if (renderer) { SDL_DestroyRenderer(renderer); renderer = nullptr; }
    if (window) { SDL_DestroyWindow(window); window = nullptr; }
    SDL_Quit();
}

4. src/main.cpp

Application entry point. Dispatches UI inputs (Keyboard) to the player APIs.

main.cpp
#include "VideoPlayer.h"
#include <iostream>
#include <SDL2/SDL.h>

int main(int argc, char* argv[]) {
    if (argc < 2) {
        std::cerr << "Usage: " << argv[0] << " <video_path>" << std::endl;
        return -1;
    }

    std::string filepath = argv[1];
    VideoPlayer player;

    if (!player.open(filepath)) {
        return -1;
    }

    std::cout << "--- Player Controls ---" << std::endl;
    std::cout << "SPACE : Pause / Resume" << std::endl;
    std::cout << "RIGHT : Seek +10 Seconds (Demo jumps to 30s)" << std::endl;
    std::cout << "UP    : Speed x2.0" << std::endl;
    std::cout << "DOWN  : Speed x0.5 (Slow Mo)" << std::endl;
    std::cout << "N     : Normal Speed x1.0" << std::endl;
    std::cout << "ESC   : Quit" << std::endl;

    player.play();

    // UI Event Polling Main Loop (runs on the main thread)
    bool running = true;
    bool is_paused = false;
    double current_speed = 1.0;
    SDL_Event event;

    while (running) {
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                running = false;
            } else if (event.type == SDL_KEYDOWN) {
                switch (event.key.keysym.sym) {
                    case SDLK_ESCAPE:
                        running = false;
                        break;
                    case SDLK_SPACE:
                        // Toggle Play / Pause
                        is_paused = !is_paused;
                        if (is_paused) player.pause();
                        else player.resume();
                        break;
                    case SDLK_RIGHT:
                        // Test Seek feature (fixed jump to 30s as demo,
                        // real scenario should accumulate from current time)
                        std::cout << "Seeking to 30s..." << std::endl;
                        player.seek(30.0);
                        break;
                    case SDLK_UP:
                        // Speed Up
                        current_speed = 2.0;
                        player.setSpeed(current_speed);
                        std::cout << "Speed: " << current_speed << "x" << std::endl;
                        break;
                    case SDLK_DOWN:
                        // Slow Motion
                        current_speed = 0.5;
                        player.setSpeed(current_speed);
                        std::cout << "Speed: " << current_speed << "x" << std::endl;
                        break;
                    case SDLK_n:
                        // Restore Normal Speed
                        current_speed = 1.0;
                        player.setSpeed(current_speed);
                        std::cout << "Speed: " << current_speed << "x" << std::endl;
                        break;
                }
            }
        }
        // Reduce CPU usage of the main loop
        SDL_Delay(50);
    }

    player.stop();
    return 0;
}

5. Environment Setup

SDL2 (Simple DirectMedia Layer)

Windows (via vcpkg)
.\vcpkg install sdl2:x64-windows
Ubuntu / Debian
sudo apt-get install libsdl2-dev
macOS (via Homebrew)
brew install sdl2

CMake Build System

Windows

Download the .msi installer and check 'Add CMake to the system PATH' during installation.

Ubuntu / Debian
sudo apt-get install cmake
macOS (via Homebrew)
brew install cmake

6. Build & Run Instructions

Open a terminal in the directory containing your CMakeLists.txt and run:

Terminal Commands
# 1. Create build directory
mkdir build
cd build

# 2. Generate build files
cmake ..

# 3. Compile the project
cmake --build .

# 4. Run the player (Replace with your video path)
# Windows:
.\Debug\FFmpegPlayer.exe "C:\path\to\your\test_video.mp4"

# Linux / macOS:
./FFmpegPlayer "/path/to/your/test_video.mp4"