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中过滤日志 -
LOGE
和LOGI
:日志宏,分别用于输出错误和普通信息日志__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参数- 关键步骤:
- 创建解码器 :
AMediaCodec_createDecoderByType("video/avc")
:创建H.264解码器- "video/avc"是H.264的MIME类型
- 创建并配置解码器格式 :
AMediaFormat_new
:创建新的媒体格式对象- 设置视频参数:
- 宽度和高度
- 帧率
- 色彩格式(YUV420半平面格式)
- 比特率
- SPS和PPS参数(序列参数集和图像参数集)
- 配置解码器 :
AMediaCodec_configure
:根据格式配置解码器- 最后两个参数为nullptr表示不使用Surface输出
- 启动解码器 :
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数据- 关键步骤:
- 获取输入缓冲区 :
AMediaCodec_dequeueInputBuffer
:从解码器获取一个可用的输入缓冲区- 10000是超时时间(毫秒)
- 返回值可能为:
- 正数:缓冲区索引
AMEDIACODEC_INFO_TRY_AGAIN_LATER
:暂时没有可用缓冲区
- 填充输入数据 :
AMediaCodec_getInputBuffer
:获取缓冲区指针- 将输入数据复制到缓冲区
- 提交输入缓冲区 :
AMediaCodec_queueInputBuffer
:将缓冲区提交给解码器- 参数:
- 缓冲区索引
- 数据偏移量(通常为0)
- 数据大小
- PTS(Presentation Time Stamp,用于同步)
- 谷歌扩展标志(通常为0)
- 处理输出缓冲区 :
- 循环获取解码后的输出帧
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");
}
}