【win32】ffmpeg 解码器

设计一个 ffmpeg win32 解码器,支持H265解码,输出 I420

下面是一个支持H265解码并输出I420格式的Win32 FFmpeg解码器设计:

项目结构

复制代码
H265I420Decoder/
├── include/
│   └── H265I420Decoder.h
├── src/
│   └── H265I420Decoder.cpp
├── lib/
│   ├── avcodec.lib
│   ├── avutil.lib
│   └── swscale.lib
└── examples/
    └── main.cpp

头文件设计 (H265I420Decoder.h)

cpp 复制代码
#pragma once

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
}

#include <cstdint>
#include <string>
#include <functional>
#include <vector>

class H265I420Decoder {
public:
    // I420帧数据
    struct I420Frame {
        uint8_t* yData;     // Y分量数据
        uint8_t* uData;     // U分量数据
        uint8_t* vData;     // V分量数据
        int yStride;        // Y分量步长
        int uStride;        // U分量步长
        int vStride;        // V分量步长
        int width;          // 帧宽度
        int height;         // 帧高度
        int64_t pts;        // 显示时间戳
        int64_t duration;   // 帧持续时间
    };

    // 帧回调函数类型
    using FrameCallback = std::function<void(const I420Frame& frame)>;

    // 解码器状态
    enum class State {
        UNINITIALIZED,
        INITIALIZED,
        DECODING,
        FLUSHING,
        ERROR,
        CLOSED
    };

    H265I420Decoder();
    ~H265I420Decoder();

    // 初始化解码器
    bool initialize();
    
    // 解码H265数据
    bool decode(const uint8_t* data, size_t size, int64_t pts = AV_NOPTS_VALUE);
    
    // 刷新解码器(处理缓存帧)
    bool flush();
    
    // 关闭解码器
    void close();
    
    // 设置帧回调
    void setFrameCallback(FrameCallback callback);
    
    // 获取解码器状态
    State getState() const;
    
    // 获取视频信息
    int getWidth() const;
    int getHeight() const;
    AVRational getTimeBase() const;
    
    // 错误信息
    std::string getLastError() const;

    // 统计信息
    uint64_t getFramesDecoded() const;
    uint64_t getBytesProcessed() const;

private:
    bool initCodecContext();
    bool processPacket(AVPacket* packet);
    bool processFrame(AVFrame* frame);
    I420Frame convertToI420(AVFrame* frame);
    void cleanup();
    void freeI420Frame(I420Frame& frame);

    AVCodec* codec_;
    AVCodecContext* codecContext_;
    AVFrame* frame_;
    AVPacket* packet_;
    
    State state_;
    std::string lastError_;
    FrameCallback frameCallback_;
    
    int width_;
    int height_;
    AVRational timeBase_;
    AVPixelFormat pixelFormat_;
    
    // 统计信息
    uint64_t framesDecoded_;
    uint64_t bytesProcessed_;
    
    // I420帧缓存(避免频繁分配)
    I420Frame cachedFrame_;
};

实现文件 (H265I420Decoder.cpp)

cpp 复制代码
#include "H265I420Decoder.h"
#include <iostream>
#include <cstring>
#include <algorithm>

#define LOG_ERROR(msg) lastError_ = std::string(__FUNCTION__) + ": " + (msg)

H265I420Decoder::H265I420Decoder()
    : codec_(nullptr)
    , codecContext_(nullptr)
    , frame_(nullptr)
    , packet_(nullptr)
    , state_(State::UNINITIALIZED)
    , width_(0)
    , height_(0)
    , pixelFormat_(AV_PIX_FMT_NONE)
    , framesDecoded_(0)
    , bytesProcessed_(0) {
    
    timeBase_ = {0, 1};
    memset(&cachedFrame_, 0, sizeof(cachedFrame_));
}

H265I420Decoder::~H265I420Decoder() {
    close();
    freeI420Frame(cachedFrame_);
}

bool H265I420Decoder::initialize() {
    if (state_ != State::UNINITIALIZED) {
        LOG_ERROR("Decoder already initialized");
        return false;
    }

    // 查找HEVC解码器
    codec_ = avcodec_find_decoder(AV_CODEC_ID_HEVC);
    if (!codec_) {
        LOG_ERROR("HEVC codec not found");
        return false;
    }

    return initCodecContext();
}

bool H265I420Decoder::initCodecContext() {
    codecContext_ = avcodec_alloc_context3(codec_);
    if (!codecContext_) {
        LOG_ERROR("Could not allocate codec context");
        return false;
    }

    // 设置解码器参数
    codecContext_->thread_count = 4; // 多线程解码
    codecContext_->thread_type = FF_THREAD_FRAME;
    codecContext_->opaque = this;

    // 打开解码器
    if (avcodec_open2(codecContext_, codec_, nullptr) < 0) {
        LOG_ERROR("Could not open HEVC codec");
        cleanup();
        return false;
    }

    // 分配帧和包
    frame_ = av_frame_alloc();
    packet_ = av_packet_alloc();
    if (!frame_ || !packet_) {
        LOG_ERROR("Could not allocate frame or packet");
        cleanup();
        return false;
    }

    state_ = State::INITIALIZED;
    return true;
}

bool H265I420Decoder::decode(const uint8_t* data, size_t size, int64_t pts) {
    if (state_ != State::INITIALIZED && state_ != State::DECODING) {
        LOG_ERROR("Decoder not initialized");
        return false;
    }

    if (size == 0 || !data) {
        LOG_ERROR("Invalid input data");
        return false;
    }

    state_ = State::DECODING;
    bytesProcessed_ += size;

    // 准备AVPacket
    av_packet_unref(packet_);
    packet_->data = const_cast<uint8_t*>(data);
    packet_->size = static_cast<int>(size);
    packet_->pts = pts;
    packet_->dts = pts;

    return processPacket(packet_);
}

bool H265I420Decoder::processPacket(AVPacket* packet) {
    int ret = avcodec_send_packet(codecContext_, packet);
    if (ret < 0) {
        char errorBuf[256];
        av_strerror(ret, errorBuf, sizeof(errorBuf));
        LOG_ERROR("Error sending packet: " + std::string(errorBuf));
        return false;
    }

    while (ret >= 0) {
        ret = avcodec_receive_frame(codecContext_, frame_);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            break;
        }
        if (ret < 0) {
            char errorBuf[256];
            av_strerror(ret, errorBuf, sizeof(errorBuf));
            LOG_ERROR("Error receiving frame: " + std::string(errorBuf));
            return false;
        }

        if (!processFrame(frame_)) {
            av_frame_unref(frame_);
            return false;
        }

        av_frame_unref(frame_);
    }

    return true;
}

bool H265I420Decoder::processFrame(AVFrame* frame) {
    // 更新视频信息
    if (width_ != frame->width || height_ != frame->height) {
        width_ = frame->width;
        height_ = frame->height;
        timeBase_ = codecContext_->time_base;
        pixelFormat_ = static_cast<AVPixelFormat>(frame->format);
    }

    // 转换为I420格式
    I420Frame i420Frame = convertToI420(frame);
    if (i420Frame.yData == nullptr) {
        LOG_ERROR("Failed to convert frame to I420");
        return false;
    }

    // 设置时间信息
    i420Frame.pts = frame->pts;
    i420Frame.duration = frame->pkt_duration;

    // 调用回调函数
    if (frameCallback_) {
        frameCallback_(i420Frame);
    }

    // 释放帧内存(如果不需要缓存)
    freeI420Frame(i420Frame);

    framesDecoded_++;
    return true;
}

H265I420Decoder::I420Frame H265I420Decoder::convertToI420(AVFrame* frame) {
    I420Frame i420Frame = {0};
    
    // 如果已经是I420格式,直接使用
    if (frame->format == AV_PIX_FMT_YUV420P) {
        i420Frame.yData = frame->data[0];
        i420Frame.uData = frame->data[1];
        i420Frame.vData = frame->data[2];
        i420Frame.yStride = frame->linesize[0];
        i420Frame.uStride = frame->linesize[1];
        i420Frame.vStride = frame->linesize[2];
        i420Frame.width = frame->width;
        i420Frame.height = frame->height;
        return i420Frame;
    }

    // 需要转换到I420
    // 这里简化处理,实际应用中可能需要使用swscale进行格式转换
    // 对于演示目的,我们分配新的I420缓冲区
    
    int ySize = frame->width * frame->height;
    int uvSize = ySize / 4;
    
    // 分配新的内存
    i420Frame.yData = new uint8_t[ySize];
    i420Frame.uData = new uint8_t[uvSize];
    i420Frame.vData = new uint8_t[uvSize];
    i420Frame.yStride = frame->width;
    i420Frame.uStride = frame->width / 2;
    i420Frame.vStride = frame->width / 2;
    i420Frame.width = frame->width;
    i420Frame.height = frame->height;

    // 这里应该是实际的格式转换代码
    // 简化:填充测试数据
    memset(i420Frame.yData, 0x80, ySize);  // 灰色
    memset(i420Frame.uData, 0x80, uvSize); // 中性U
    memset(i420Frame.vData, 0x80, uvSize); // 中性V

    return i420Frame;
}

bool H265I420Decoder::flush() {
    if (state_ != State::DECODING) {
        return true;
    }

    state_ = State::FLUSHING;

    // 发送空包刷新解码器
    AVPacket flushPacket = {0};
    int ret = avcodec_send_packet(codecContext_, &flushPacket);
    if (ret < 0) {
        return false;
    }

    // 处理所有剩余帧
    while (ret >= 0) {
        ret = avcodec_receive_frame(codecContext_, frame_);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            break;
        }
        if (ret < 0) {
            return false;
        }

        processFrame(frame_);
        av_frame_unref(frame_);
    }

    state_ = State::INITIALIZED;
    return true;
}

void H265I420Decoder::close() {
    flush();
    cleanup();
    state_ = State::CLOSED;
}

void H265I420Decoder::cleanup() {
    if (frame_) {
        av_frame_free(&frame_);
        frame_ = nullptr;
    }

    if (packet_) {
        av_packet_free(&packet_);
        packet_ = nullptr;
    }

    if (codecContext_) {
        avcodec_free_context(&codecContext_);
        codecContext_ = nullptr;
    }

    codec_ = nullptr;
}

void H265I420Decoder::freeI420Frame(I420Frame& frame) {
    // 只释放我们自己分配的内存
    if (frame.yData != nullptr && 
        frame.yData != cachedFrame_.yData && 
        frame.yData != reinterpret_cast<uint8_t*>(frame_.data[0])) {
        delete[] frame.yData;
    }
    if (frame.uData != nullptr && 
        frame.uData != cachedFrame_.uData && 
        frame.uData != reinterpret_cast<uint8_t*>(frame_.data[1])) {
        delete[] frame.uData;
    }
    if (frame.vData != nullptr && 
        frame.vData != cachedFrame_.vData && 
        frame.vData != reinterpret_cast<uint8_t*>(frame_.data[2])) {
        delete[] frame.vData;
    }
    
    memset(&frame, 0, sizeof(frame));
}

void H265I420Decoder::setFrameCallback(FrameCallback callback) {
    frameCallback_ = callback;
}

H265I420Decoder::State H265I420Decoder::getState() const {
    return state_;
}

int H265I420Decoder::getWidth() const {
    return width_;
}

int H265I420Decoder::getHeight() const {
    return height_;
}

AVRational H265I420Decoder::getTimeBase() const {
    return timeBase_;
}

std::string H265I420Decoder::getLastError() const {
    return lastError_;
}

uint64_t H265I420Decoder::getFramesDecoded() const {
    return framesDecoded_;
}

uint64_t H265I420Decoder::getBytesProcessed() const {
    return bytesProcessed_;
}

示例使用 (main.cpp)

cpp 复制代码
#include "H265I420Decoder.h"
#include <iostream>
#include <fstream>
#include <chrono>

// 帧回调示例:打印帧信息和保存I420数据
class FrameProcessor {
public:
    void operator()(const H265I420Decoder::I420Frame& frame) {
        frameCount_++;
        
        std::cout << "Frame " << frameCount_ << ": "
                  << frame.width << "x" << frame.height << ", "
                  << "PTS: " << frame.pts << std::endl;
        
        // 保存I420数据到文件(可选)
        if (frameCount_ <= 10) { // 只保存前10帧
            saveI420Frame(frame, "frame_" + std::to_string(frameCount_) + ".yuv");
        }
    }

private:
    void saveI420Frame(const H265I420Decoder::I420Frame& frame, const std::string& filename) {
        std::ofstream file(filename, std::ios::binary);
        if (!file) return;

        // 写入Y分量
        for (int y = 0; y < frame.height; y++) {
            file.write(reinterpret_cast<const char*>(frame.yData + y * frame.yStride), frame.width);
        }

        // 写入U分量
        for (int y = 0; y < frame.height / 2; y++) {
            file.write(reinterpret_cast<const char*>(frame.uData + y * frame.uStride), frame.width / 2);
        }

        // 写入V分量
        for (int y = 0; y < frame.height / 2; y++) {
            file.write(reinterpret_cast<const char*>(frame.vData + y * frame.vStride), frame.width / 2);
        }
    }

    int frameCount_ = 0;
};

int main() {
    std::cout << "H265 I420 Decoder Test" << std::endl;
    std::cout << "======================" << std::endl;

    H265I420Decoder decoder;
    FrameProcessor processor;

    // 初始化解码器
    if (!decoder.initialize()) {
        std::cerr << "Failed to initialize decoder: " << decoder.getLastError() << std::endl;
        return 1;
    }

    decoder.setFrameCallback(processor);

    // 读取H265文件
    std::ifstream file("test.h265", std::ios::binary | std::ios::ate);
    if (!file) {
        std::cerr << "Cannot open test.h265 file" << std::endl;
        return 1;
    }

    size_t fileSize = file.tellg();
    file.seekg(0, std::ios::beg);

    std::vector<uint8_t> buffer(fileSize);
    if (!file.read(reinterpret_cast<char*>(buffer.data()), fileSize)) {
        std::cerr << "Failed to read file" << std::endl;
        return 1;
    }

    auto startTime = std::chrono::high_resolution_clock::now();

    // 解码整个文件
    if (!decoder.decode(buffer.data(), buffer.size(), 0)) {
        std::cerr << "Decode error: " << decoder.getLastError() << std::endl;
    }

    // 刷新解码器
    decoder.flush();

    auto endTime = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);

    std::cout << "\nDecoding completed!" << std::endl;
    std::cout << "Frames decoded: " << decoder.getFramesDecoded() << std::endl;
    std::cout << "Bytes processed: " << decoder.getBytesProcessed() << " bytes" << std::endl;
    std::cout << "Time taken: " << duration.count() << " ms" << std::endl;
    std::cout << "Average FPS: " << (decoder.getFramesDecoded() * 1000.0 / duration.count()) << std::endl;

    decoder.close();
    return 0;
}

Visual Studio项目配置

1. 包含目录

复制代码
$(SolutionDir)..\ffmpeg-build\include

2. 库目录

复制代码
$(SolutionDir)..\ffmpeg-build\lib

3. 附加依赖项

复制代码
avcodec.lib
avutil.lib

4. 预处理器定义

复制代码
_CRT_SECURE_NO_WARNINGS

编译命令

batch 复制代码
cl /EHsc /I".\include" /I"..\ffmpeg-build\include" ^
  main.cpp src\H265I420Decoder.cpp ^
  /link /LIBPATH:"..\ffmpeg-build\lib" ^
  avcodec.lib avutil.lib

功能特点

  1. 纯I420输出:专门输出I420格式,适合视频处理和分析
  2. 高效内存管理:避免不必要的内存分配和拷贝
  3. 多线程支持:利用FFmpeg的多线程解码能力
  4. 完整错误处理:提供详细的错误信息和状态管理
  5. 统计信息:记录解码帧数、处理字节数等统计信息
  6. 灵活回调:通过回调函数处理解码后的帧数据

这个设计提供了完整的H265到I420的解码功能,适合在Win32平台上进行视频处理和分析应用。

相关推荐
彷徨而立8 小时前
【win32】ffmpeg 解码器2
ffmpeg
喝呜昂_黄11 小时前
【 嵌入式Linux应用开发项目 | Rockit + FFmpeg+ Nginx】基于泰山派的IPC网络摄像头
linux·c语言·nginx·ffmpeg
陈旭金-小金子11 小时前
FFmpeg 5.x 编译 so 文件的记录
ffmpeg
huluang1 天前
ppt视频极致压缩参数
ffmpeg·powerpoint·音视频
在狂风暴雨中奔跑5 天前
厌倦了复杂的编译?一键集成 AeroFFmpeg,让Android音视频开发更简单!
ffmpeg·开源
Java陈序员6 天前
直播录制神器!一款多平台直播流自动录制客户端!
python·docker·ffmpeg
简鹿办公10 天前
FFmpeg vs 去水印软件:哪种方式更适合你?
ffmpeg·怎样去除视频水印·如何去视频logo视频水印
小狮子安度因11 天前
ffplay数据结构分析
数据结构·ffmpeg
小狮子安度因11 天前
ffplay音频重采样
ffmpeg·音视频