C++ MediaCodec H264解码

cpp 复制代码
#include <media/NdkMediaCodec.h>
#include <media/NdkMediaFormat.h>
#include <android/log.h>

#define LOG_TAG "H264Decoder"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)

class H264Decoder {
public:
    H264Decoder() : mCodec(nullptr), mFormat(nullptr) {}
    
    ~H264Decoder() {
        if (mCodec) {
            AMediaCodec_stop(mCodec);
            AMediaCodec_delete(mCodec);
        }
        if (mFormat) {
            AMediaFormat_delete(mFormat);
        }
    }
    
    bool init(int width, int height, const uint8_t* sps, size_t spsSize, 
              const uint8_t* pps, size_t ppsSize) {
        // 1. 创建解码器
        mCodec = AMediaCodec_createDecoderByType("video/avc");
        if (!mCodec) {
            LOGE("Failed to create H.264 decoder");
            return false;
        }
        
        // 2. 创建媒体格式并配置
        mFormat = AMediaFormat_new();
        AMediaFormat_setString(mFormat, AMEDIAFORMAT_KEY_MIME, "video/avc");
        AMediaFormat_setInt32(mFormat, AMEDIAFORMAT_KEY_WIDTH, width);
        AMediaFormat_setInt32(mFormat, AMEDIAFORMAT_KEY_HEIGHT, height);
        
        // 设置SPS和PPS (序列参数集和图像参数集)
        AMediaFormat_setBuffer(mFormat, "csd-0", sps, spsSize);
        AMediaFormat_setBuffer(mFormat, "csd-1", pps, ppsSize);
        
        // 3. 配置解码器
        media_status_t status = AMediaCodec_configure(mCodec, mFormat, nullptr, nullptr, 0);
        if (status != AMEDIA_OK) {
            LOGE("Failed to configure decoder: %d", status);
            return false;
        }
        
        // 4. 启动解码器
        status = AMediaCodec_start(mCodec);
        if (status != AMEDIA_OK) {
            LOGE("Failed to start decoder: %d", status);
            return false;
        }
        
        LOGI("H.264 decoder initialized successfully");
        return true;
    }
    
    void decode(const uint8_t* data, size_t size, uint64_t pts) {
        if (!mCodec) return;
        
        // 1. 获取输入缓冲区索引
        ssize_t inputIndex = AMediaCodec_dequeueInputBuffer(mCodec, 2000);
        if (inputIndex < 0) {
            if (inputIndex == AMEDIACODEC_INFO_TRY_AGAIN_LATER) {
                LOGI("No input buffer available");
            } else {
                LOGE("Error dequeueing input buffer: %zd", inputIndex);
            }
            return;
        }
        
        // 2. 获取输入缓冲区并填充数据
        size_t bufferSize = 0;
        uint8_t* buffer = AMediaCodec_getInputBuffer(mCodec, inputIndex, &bufferSize);
        if (!buffer || bufferSize < size) {
            LOGE("Input buffer too small or null");
            AMediaCodec_queueInputBuffer(mCodec, inputIndex, 0, 0, 0, 
                                       AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM);
            return;
        }
        
        memcpy(buffer, data, size);
        
        // 3. 将输入缓冲区提交给解码器
        media_status_t status = AMediaCodec_queueInputBuffer(mCodec, inputIndex, 0, size, pts, 0);
        if (status != AMEDIA_OK) {
            LOGE("Failed to queue input buffer: %d", status);
            return;
        }
        
        // 4. 处理输出缓冲区
        AMediaCodecBufferInfo info;
        ssize_t outputIndex = AMediaCodec_dequeueOutputBuffer(mCodec, &info, 2000);
        if (outputIndex >= 0) {
            // 成功解码一帧
            if (info.size > 0) {
                // 获取解码后的数据
                size_t outSize = 0;
                uint8_t* outBuffer = AMediaCodec_getOutputBuffer(mCodec, outputIndex, &outSize);
                
                if (outBuffer) {
                    // 处理解码后的帧数据 (YUV格式)
                    processDecodedFrame(outBuffer, info.size, info.presentationTimeUs);
                }
            }
            
            // 释放输出缓冲区
            AMediaCodec_releaseOutputBuffer(mCodec, outputIndex, info.size != 0);
        } else if (outputIndex == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED) {
            LOGI("Output buffers changed");
        } else if (outputIndex == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {
            AMediaFormat* newFormat = AMediaCodec_getOutputFormat(mCodec);
            LOGI("Output format changed: %s", AMediaFormat_toString(newFormat));
            AMediaFormat_delete(newFormat);
        } else if (outputIndex == AMEDIACODEC_INFO_TRY_AGAIN_LATER) {
            LOGI("No output buffer available");
        } else {
            LOGE("Error dequeueing output buffer: %zd", outputIndex);
        }
    }
    
private:
    void processDecodedFrame(const uint8_t* data, size_t size, uint64_t pts) {
        // 这里处理解码后的YUV帧数据
        // 可以渲染到Surface或进行其他处理
        LOGI("Decoded frame: size=%zu, pts=%llu", size, pts);
    }
    
    AMediaCodec* mCodec;
    AMediaFormat* mFormat;
};

代码解析和知识点讲解:

1. 头文件和宏定义

cpp 复制代码
#include <android/log.h>
#include <media/NdkMediaCodec.h>
#include <media/NdkMediaFormat.h>
#include <unistd.h>

#define LOGTAG "H264Decoder"
#define LOGE(...) android_log_print(ANDROID_LOG_ERROR, LOGTAG, __VA_ARGS__)
#define LOGI(...) android_log_print(ANDROID_LOG_INFO, LOGTAG, __VA_ARGS__)
  • android/log.h:用于Android日志输出

  • media/NdkMediaCodec.h:Android NDK提供的媒体编解码API

  • media/NdkMediaFormat.h:媒体格式相关API

  • unistd.h:包含sleep等函数

  • LOGTAG:日志标签,便于在logcat中过滤日志

  • LOGELOGI:日志宏,分别用于输出错误和普通信息日志

    • __VA_ARGS__:可变参数宏,允许宏接受可变数量的参数

2. 类定义和构造/析构函数

cpp 复制代码
class H264Decoder {
public:
    H264Decoder() : mCodec(nullptr), mFormat(nullptr) {}

    ~H264Decoder() {
        if (mCodec) {
            AMediaCodec_stop(mCodec);
            AMediaCodec_delete(mCodec);
        }

        if (mFormat) {
            AMediaFormat_delete(mFormat);
        }
    }
  • 定义了一个H264Decoder类,封装了H.264解码功能
  • 构造函数使用成员初始化列表初始化指针成员
  • 析构函数中释放了所有分配的资源:
    • AMediaCodec_stop:停止解码器
    • AMediaCodec_delete:销毁解码器实例
    • AMediaFormat_delete:销毁媒体格式对象

3. 初始化方法

cpp 复制代码
bool init(int width, int height, const uint8_t* sps, size_t spsSize,
          const uint8_t* pps, size_t ppsSize) {
    // 1. 创建解码器
    mCodec = AMediaCodec_createDecoderByType("video/avc");
    if (!mCodec) {
        LOGE("Failed to create decoder");
        return false;
    }

    // 2. 创建并配置解码器格式
    mFormat = AMediaFormat_new();
    if (!mFormat) {
        LOGE("Failed to create media format");
        AMediaCodec_delete(mCodec);
        mCodec = nullptr;
        return false;
    }

    // 设置视频宽度和高度
    AMediaFormat_setInt32(mFormat, AMEDIAFORMAT_KEY_WIDTH, width);
    AMediaFormat_setInt32(mFormat, AMEDIAFORMAT_KEY_HEIGHT, height);

    // 设置帧率 (假设30fps)
    AMediaFormat_setInt32(mFormat, AMEDIAFORMAT_KEY_FRAME_RATE, 30);

    // 设置色彩格式 (通常为 COLOR_FormatYUV420SemiPlanar)
    AMediaFormat_setInt32(mFormat, AMEDIAFORMAT_KEY_COLOR_FORMAT,
                         AMEDIACODEC_COLOR_FormatYUV420SemiPlanar);

    // 设置比特率 (可以根据实际情况调整)
    AMediaFormat_setInt32(mFormat, AMEDIAFORMAT_KEY_BIT_RATE, width * height * 2);

    // 设置 SPS 和 PPS 参数
    AMediaFormat_setBuffer(mFormat, AMEDIAFORMAT_KEY_SPS, sps, spsSize);
    AMediaFormat_setBuffer(mFormat, AMEDIAFORMAT_KEY_PPS, pps, ppsSize);

    // 3. 配置解码器
    status_t err = AMediaCodec_configure(mCodec, mFormat, nullptr, nullptr, 0);
    if (err != AMEDIA_OK) {
        LOGE("Failed to configure decoder: %d", err);
        AMediaFormat_delete(mFormat);
        mFormat = nullptr;
        AMediaCodec_delete(mCodec);
        mCodec = nullptr;
        return false;
    }

    // 4. 启动解码器
    err = AMediaCodec_start(mCodec);
    if (err != AMEDIA_OK) {
        LOGE("Failed to start decoder: %d", err);
        AMediaFormat_delete(mFormat);
        mFormat = nullptr;
        AMediaCodec_delete(mCodec);
        mCodec = nullptr;
        return false;
    }

    return true;
}
  • init方法用于初始化解码器,需要提供视频的宽高和SPS/PPS参数
  • 关键步骤:
    1. 创建解码器
      • AMediaCodec_createDecoderByType("video/avc"):创建H.264解码器
      • "video/avc"是H.264的MIME类型
    2. 创建并配置解码器格式
      • AMediaFormat_new:创建新的媒体格式对象
      • 设置视频参数:
        • 宽度和高度
        • 帧率
        • 色彩格式(YUV420半平面格式)
        • 比特率
        • SPS和PPS参数(序列参数集和图像参数集)
    3. 配置解码器
      • AMediaCodec_configure:根据格式配置解码器
      • 最后两个参数为nullptr表示不使用Surface输出
    4. 启动解码器
      • AMediaCodec_start:启动解码器
  • const uint8_t* 表示一个指向不可修改的 uint8_t 数据的指针。
  • uint8_t* const:指针本身是常量(不可重新指向其他地址),但指向的数据可修改。
  • const uint8_t* const:指针和指向的数据都不可修改。

4. 解码帧方法

cpp 复制代码
bool decodeFrame(const uint8_t* inputData, size_t inputSize, uint64_t pts) {
    if (!mCodec) {
        LOGE("Decoder not initialized");
        return false;
    }

    // 1. 获取输入缓冲区
    ssize_t inputIndex = AMediaCodec_dequeueInputBuffer(mCodec, 10000);
    if (inputIndex < 0) {
        if (inputIndex == AMEDIACODEC_INFO_TRY_AGAIN_LATER) {
            LOGI("No input buffer available");
        } else {
            LOGE("Error dequeueing input buffer: %zd", inputIndex);
        }
        return false;
    }

    // 2. 获取缓冲区指针并填充数据
    void* inputBuffer = AMediaCodec_getInputBuffer(mCodec, inputIndex);
    if (!inputBuffer) {
        LOGE("Failed to get input buffer");
        return false;
    }

    // 将输入数据复制到缓冲区
    memcpy(inputBuffer, inputData, inputSize);

    // 3. 提交输入缓冲区到解码器
    status_t err = AMediaCodec_queueInputBuffer(mCodec, inputIndex, 0, inputSize,
                                               pts, 0);
    if (err != AMEDIA_OK) {
        LOGE("Failed to queue input buffer: %d", err);
        return false;
    }

    // 4. 处理输出缓冲区
    while (true) {
        // 获取输出缓冲区
        AMediaCodecBufferInfo info;
        ssize_t outputIndex = AMediaCodec_dequeueOutputBuffer(mCodec, &info, 0);
        if (outputIndex < 0) {
            if (outputIndex == AMEDIACODEC_INFO_TRY_AGAIN_LATER) {
                break;
            } else if (outputIndex == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {
                AMediaFormat *newFormat = AMediaCodec_getOutputFormat(mCodec);
                LOGI("Output format changed: %s", AMediaFormat_toString(newFormat));
                AMediaFormat_delete(newFormat);
            } else {
                LOGE("Error dequeueing output buffer: %zd", outputIndex);
                return false;
            }
        } else {
            // 获取输出缓冲区数据
            uint8_t* outputBuffer = static_cast<uint8_t*>(AMediaCodec_getOutputBuffer(mCodec, outputIndex));
            if (!outputBuffer) {
                LOGE("Failed to get output buffer");
                AMediaCodec_releaseOutputBuffer(mCodec, outputIndex, false);
                return false;
            }

            // 处理解码后的帧
            processDecodedFrame(outputBuffer + info.offset, info.size, info.presentationTimeUs);

            // 释放输出缓冲区
            AMediaCodec_releaseOutputBuffer(mCodec, outputIndex, false);
        }
    }

    return true;
}
  • decodeFrame方法用于解码一帧H.264数据
  • 关键步骤:
    1. 获取输入缓冲区
      • AMediaCodec_dequeueInputBuffer:从解码器获取一个可用的输入缓冲区
      • 10000是超时时间(毫秒)
      • 返回值可能为:
        • 正数:缓冲区索引
        • AMEDIACODEC_INFO_TRY_AGAIN_LATER:暂时没有可用缓冲区
    2. 填充输入数据
      • AMediaCodec_getInputBuffer:获取缓冲区指针
      • 将输入数据复制到缓冲区
    3. 提交输入缓冲区
      • AMediaCodec_queueInputBuffer:将缓冲区提交给解码器
      • 参数:
        • 缓冲区索引
        • 数据偏移量(通常为0)
        • 数据大小
        • PTS(Presentation Time Stamp,用于同步)
        • 谷歌扩展标志(通常为0)
    4. 处理输出缓冲区
      • 循环获取解码后的输出帧
      • AMediaCodec_dequeueOutputBuffer:获取输出缓冲区
      • 返回值可能为:
        • 正数:缓冲区索引
        • AMEDIACODEC_INFO_TRY_AGAIN_LATER:暂时没有输出
        • AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED:输出格式已改变
      • 处理解码后的帧:
        • AMediaCodec_getOutputBuffer:获取输出缓冲区数据
        • processDecodedFrame:处理解码后的帧(YUV数据)
        • AMediaCodec_releaseOutputBuffer:释放输出缓冲区

5. 解码帧处理方法

cpp 复制代码
void processDecodedFrame(const uint8_t* data, size_t size, uint64_t pts) {
    // 这里处理解码后的YUV帧数据
    // 可以渲染到Surface或进行其他处理
    LOGI("Decoded frame: size %zu, pts %llu", size, pts);
}
  • 这是一个虚函数,子类可以重写它来处理解码后的帧
  • 目前只是简单打印帧大小和PTS
  • 实际应用中,这里可以:
    • 将YUV数据渲染到Surface
    • 转换为RGB格式
    • 进行图像处理等

6. 成员变量

cpp 复制代码
private:
    AMediaCodec* mCodec; // 解码器实例
    AMediaFormat* mFormat; // 解码器配置格式
  • mCodec:存储解码器实例
  • mFormat:存储解码器配置格式

为什么使用*?

1. 访问NDK的C风格接口

Android NDK的MediaCodec API是用C语言设计的,C语言中通常使用指针来:

  • 表示动态分配的对象

  • 允许函数修改传入的参数

  • 实现多态和抽象

2. 管理Native资源

这些指针指向的是Native层(C/C++)分配的资源:

cpp 复制代码
// 创建解码器(返回的是指针)
AMediaCodec* mCodec = AMediaCodec_createDecoderByType("video/avc");

// 创建格式对象(返回的是指针)
AMediaFormat* mFormat = AMediaFormat_new();

3. 效率考虑

  • 传递指针(4/8字节)比传递整个对象更高效

  • 避免不必要的对象拷贝

4. 生命周期控制

通过指针明确控制资源创建和销毁:

cpp 复制代码
// 初始化时创建
mCodec = AMediaCodec_createDecoderByType(...);

// 析构时释放
AMediaCodec_delete(mCodec);

指针 vs 非指针

特性 指针 (AMediaCodec*) 非指针 (AMediaCodec)
内存 指向堆内存对象 直接包含栈上对象
大小 固定大小(4/8字节) 对象实际大小
传递 传递地址 传递副本
修改 可修改指向的对象 只能修改副本
生命周期 需手动管理 自动管理

java:

java 复制代码
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.util.Log;

public class H264Decoder {
    private static final String TAG = "H264Decoder";
    private MediaCodec mCodec;
    private MediaFormat mFormat;

    public H264Decoder() {
        mCodec = null;
        mFormat = null;
    }

    public void initialize(String mime) throws Exception {
        mCodec = MediaCodec.createDecoderByType(mime);
        mFormat = MediaFormat.createVideoFormat(mime, 640, 480);
        mFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
        mFormat.setInteger(MediaFormat.KEY_BIT_RATE, 400000);
        mFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
        mFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
        mCodec.configure(mFormat, null, null, 0);
        mCodec.start();
        Log.i(TAG, "Decoder initialized");
    }

    public void decode(byte[] inputData, int inputSize, long presentationTimeUs) throws Exception {
        if (mCodec == null) {
            Log.e(TAG, "Decoder not initialized");
            return;
        }

        int inputBufferIndex = mCodec.dequeueInputBuffer(10000);
        if (inputBufferIndex >= 0) {
            byte[] inputBuffer = mCodec.getInputBuffer(inputBufferIndex).array();
            System.arraycopy(inputData, 0, inputBuffer, 0, inputSize);
            mCodec.queueInputBuffer(inputBufferIndex, 0, inputSize, presentationTimeUs, 0);
        }

        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int outputBufferIndex = mCodec.dequeueOutputBuffer(bufferInfo, 10000);
        while (outputBufferIndex >= 0) {
            byte[] outputBuffer = mCodec.getOutputBuffer(outputBufferIndex).array();
            processDecodedFrame(outputBuffer, bufferInfo.size, bufferInfo.presentationTimeUs);
            mCodec.releaseOutputBuffer(outputBufferIndex, false);
            outputBufferIndex = mCodec.dequeueOutputBuffer(bufferInfo, 0);
        }
    }

    private void processDecodedFrame(byte[] data, int size, long pts) {
        // 这里处理解码后的YUV帧数据
        // 可以渲染到Surface或进行其他处理
        Log.i(TAG, "Decoded frame: size " + size + ", pts " + pts);
    }

    public void release() {
        if (mCodec != null) {
            mCodec.stop();
            mCodec.release();
            mCodec = null;
        }
        if (mFormat != null) {
            mFormat = null;
        }
        Log.i(TAG, "Decoder released");
    }
}
相关推荐
dongzhenmao2 小时前
P1484 种树,特殊情形下的 WQS 二分转化。
数据结构·c++·windows·线性代数·算法·数学建模·动态规划
Dxy12393102162 小时前
Python PDFplumber详解:从入门到精通的PDF处理指南
开发语言·python·pdf
EutoCool3 小时前
Qt:布局管理器Layout
开发语言·c++·windows·嵌入式硬件·qt·前端框架
Cyanto3 小时前
Spring注解IoC与JUnit整合实战
java·开发语言·spring·mybatis
写不出来就跑路4 小时前
WebClient与HTTPInterface远程调用对比
java·开发语言·后端·spring·springboot
张人玉4 小时前
c#中Random类、DateTime类、String类
开发语言·c#
Jinkxs4 小时前
JavaScript性能优化实战技术
开发语言·javascript·性能优化
ydm_ymz5 小时前
C语言初阶4-数组
c语言·开发语言
presenttttt6 小时前
用Python和OpenCV从零搭建一个完整的双目视觉系统(六 最终篇)
开发语言·python·opencv·计算机视觉