导读
前面我们已经使用NDK编译出了FFmpeg并且已经集成到了Android Studio中去,相关文章:NDK21编译ffmpeg5.0.1
众所周知,软解码虽然兼容性一流,但是却非常依赖CPU,所以性能消耗笔记大;硬解码使用内置的DSP芯片进行解码,性能高,但是兼容性一般。
虽说硬解码兼容性不太好,但是在实际开发中出于对性能的考虑我们依然会采用能硬解则硬解,不能硬解则软解兜底的方案。
我们知道安卓上可以使用MediaCodec进行硬解码,新版本FFmpeg内部也支持了MediaCodec硬解码,今天我们就使用FFMpeg在安卓上使用MediaCodec进行硬解码。
笔者测试的FFmpeg版本是最新的5.0.1,不同版本之间可以会有差异。
编译支持硬解码的FFmpeg
要编译支持硬解码的FFmpeg,在进行交叉编译时我们只需要打开以下几个属性即可:
--enable-hwaccels \
--enable-jni \
--enable-mediacodec \
--enable-decoder=h264_mediacodec \
--enable-decoder=hevc_mediacodec \
--enable-decoder=mpeg4_mediacodec \
--enable-hwaccel=h264_mediacodec \
使用FFMpeg进行硬解码
使用FFmpeg无论是硬解码还是软解码流程都是差不多的,对使用FFmpeg编解码API不熟悉的童鞋们可以回看之前发表的博客文章...
在FFmpeg源文件hwcontext.c
中我们可以看出mediacodec对应的type类型是AV_HWDEVICE_TYPE_MEDIACODEC
,这个AV_HWDEVICE_TYPE_MEDIACODEC
很重要,
在配置硬解码器时都是需要使用到这个type。
static const char *const hw_type_names[] = {
[AV_HWDEVICE_TYPE_CUDA] = "cuda",
[AV_HWDEVICE_TYPE_DRM] = "drm",
[AV_HWDEVICE_TYPE_DXVA2] = "dxva2",
[AV_HWDEVICE_TYPE_D3D11VA] = "d3d11va",
[AV_HWDEVICE_TYPE_OPENCL] = "opencl",
[AV_HWDEVICE_TYPE_QSV] = "qsv",
[AV_HWDEVICE_TYPE_VAAPI] = "vaapi",
[AV_HWDEVICE_TYPE_VDPAU] = "vdpau",
[AV_HWDEVICE_TYPE_VIDEOTOOLBOX] = "videotoolbox",
[AV_HWDEVICE_TYPE_MEDIACODEC] = "mediacodec",
[AV_HWDEVICE_TYPE_VULKAN] = "vulkan",
};
下面说说在FFMpeg配置硬解码器的大体步骤:
1、给FFMpeg设置虚拟机环境
首先在库加载函数JNI_OnLoad
中调用FFmpeg的函数av_jni_set_java_vm
,给FFMpeg设置虚拟机环境:
// 类库加载时自动调用
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reversed) {
JNIEnv *env = NULL;
// 初始化JNIEnv
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_FALSE;
}
// 设置JavaVM,否则无法进行硬解码
av_jni_set_java_vm(vm, nullptr);
RegisterNativeMethods(env, "com/fly/ffmpeg/practice/ffmpeg/FFmpegHWDecoder",
const_cast<JNINativeMethod *>(hw_decoder_nativeMethod), sizeof(hw_decoder_nativeMethod) / sizeof (JNINativeMethod));
// 返回JNI使用的版本
return JNI_VERSION_1_4;
}
2、通过名字查找硬解码器
以h264为例,在安卓上它的硬解码器名字为h264_mediacodec
,可以通过函数avcodec_find_decoder_by_name("h264_mediacodec")
查找解码器,
如果返回空,一般就是不支持硬解码了。
3、配置硬解码器
这个配置主要是为了获取解码得到的YUV是什么格式的。
// 配置硬解码器
int i;
for (i = 0;; i++) {
const AVCodecHWConfig *config = avcodec_get_hw_config(avCodec, i);
if (nullptr == config) {
LOGCATE("获取硬解码是配置失败");
return;
}
if (config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX &&
config->device_type == AV_HWDEVICE_TYPE_MEDIACODEC) {
hw_pix_fmt = config->pix_fmt;
LOGCATE("硬件解码器配置成功");
break;
}
}
4、初始化mediacodec的buffer
avCodecContext = avcodec_alloc_context3(avCodec);
avcodec_parameters_to_context(avCodecContext,avFormatContext->streams[video_index]->codecpar);
avCodecContext->get_format = get_hw_format;
// 硬件解码器初始化
AVBufferRef *hw_device_ctx = nullptr;
ret = av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_MEDIACODEC,
nullptr, nullptr, 0);
if (ret < 0) {
LOGCATE("Failed to create specified HW device");
return;
}
avCodecContext->hw_device_ctx = av_buffer_ref(hw_device_ctx);
5、打开解码器
和软解码一样,使用函数avcodec_open2
打开解码器即可。后面的操作就是和软解码一样了。
从以上可以看出,硬解码和软解的区别就是硬解码需要多配置一点信息而已,下面贴一下主要代码:
#include "HWDecoder.h"
#include <log_cat.h>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavcodec/codec.h>
#include <libavutil/avutil.h>
#include <libavutil/pixdesc.h>
}
AVFormatContext *avFormatContext;
AVPacket *avPacket;
AVFrame *avFrame;
AVCodecContext *avCodecContext;
FILE *yuv_file;
HWDecoder::HWDecoder() {
}
HWDecoder::~HWDecoder() {
if (nullptr != avFormatContext) {
avformat_free_context(avFormatContext);
avFormatContext = nullptr;
}
if (nullptr != avCodecContext) {
avcodec_free_context(&avCodecContext);
avCodecContext = nullptr;
}
if (nullptr != avPacket) {
av_packet_free(&avPacket);
avPacket = nullptr;
}
if (nullptr != avFrame) {
av_frame_free(&avFrame);
avFrame = nullptr;
}
if(nullptr != yuv_file){
fclose(yuv_file);
yuv_file = nullptr;
}
}
AVPixelFormat hw_pix_fmt;
static enum AVPixelFormat get_hw_format(AVCodecContext *ctx,
const enum AVPixelFormat *pix_fmts)
{
const enum AVPixelFormat *p;
for (p = pix_fmts; *p != -1; p++) {
if (*p == hw_pix_fmt)
return *p;
}
LOGCATE("Failed to get HW surface format.\n");
return AV_PIX_FMT_NONE;
}
void HWDecoder::decode_video(const char *video_path, const char *yuv_path) {
avFormatContext = avformat_alloc_context();
int ret = avformat_open_input(&avFormatContext, video_path, nullptr, nullptr);
if (ret < 0) {
LOGCATE("打开媒体文件失败");
return;
}
avformat_find_stream_info(avFormatContext, nullptr);
int video_index = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
if (video_index < 0) {
LOGCATE("找不到视频索引");
return;
}
LOGCATE("找到视频索引:%d", video_index);
const AVCodec *avCodec = nullptr;
switch (avFormatContext->streams[video_index]->codecpar->codec_id) {
// 这里以h264为例
case AV_CODEC_ID_H264:
avCodec = avcodec_find_decoder_by_name("h264_mediacodec");
if (nullptr == avCodec) {
LOGCATE("没有找到硬解码器h264_mediacodec");
return;
} else {
// 配置硬解码器
int i;
for (i = 0;; i++) {
const AVCodecHWConfig *config = avcodec_get_hw_config(avCodec, i);
if (nullptr == config) {
LOGCATE("获取硬解码是配置失败");
return;
}
if (config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX &&
config->device_type == AV_HWDEVICE_TYPE_MEDIACODEC) {
hw_pix_fmt = config->pix_fmt;
LOGCATE("硬件解码器配置成功");
break;
}
}
break;
}
}
avCodecContext = avcodec_alloc_context3(avCodec);
avcodec_parameters_to_context(avCodecContext,avFormatContext->streams[video_index]->codecpar);
avCodecContext->get_format = get_hw_format;
// 硬件解码器初始化
AVBufferRef *hw_device_ctx = nullptr;
ret = av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_MEDIACODEC,
nullptr, nullptr, 0);
if (ret < 0) {
LOGCATE("Failed to create specified HW device");
return;
}
avCodecContext->hw_device_ctx = av_buffer_ref(hw_device_ctx);
// 打开解码器
ret = avcodec_open2(avCodecContext, avCodec, nullptr);
if (ret != 0) {
LOGCATE("解码器打开失败:%s",av_err2str(ret));
return;
} else {
LOGCATE("解码器打开成功");
}
avPacket = av_packet_alloc();
avFrame = av_frame_alloc();
yuv_file = fopen(yuv_path,"wb");
while (true) {
ret = av_read_frame(avFormatContext, avPacket);
if (ret != 0) {
LOGCATE("av_read_frame end");
break;
}
if(avPacket->stream_index != video_index){
av_packet_unref(avPacket);
continue;
}
ret = avcodec_send_packet(avCodecContext,avPacket);
if(ret == AVERROR(EAGAIN)){
LOGCATD("avcodec_send_packet EAGAIN");
} else if(ret < 0){
LOGCATE("avcodec_send_packet fail:%s",av_err2str(ret));
return;
}
av_packet_unref(avPacket);
ret = avcodec_receive_frame(avCodecContext,avFrame);
LOGCATE("avcodec_receive_frame:%d",ret);
while (ret == 0){
LOGCATE("获取解码数据成功:%s",av_get_pix_fmt_name(static_cast<AVPixelFormat>(avFrame->format)));
LOGCATE("linesize0:%d,linesize1:%d,linesize2:%d",avFrame->linesize[0],avFrame->linesize[1],avFrame->linesize[2]);
LOGCATE("width:%d,height:%d",avFrame->width,avFrame->height);
ret = avcodec_receive_frame(avCodecContext,avFrame);
// 如果解码出来的数据是nv12
// 播放 ffplay -i d:/cap.yuv -pixel_format nv12 -framerate 25 -video_size 640x480
// 写入y
for(int j=0; j<avFrame->height; j++)
fwrite(avFrame->data[0] + j * avFrame->linesize[0], 1, avFrame->width, yuv_file);
// 写入uv
for(int j=0; j<avFrame->height/2; j++)
fwrite(avFrame->data[1] + j * avFrame->linesize[1], 1, avFrame->width, yuv_file);
}
}
}
解码成功将YUV写入文件后可以通过ffplay播放一下,看画面是否正常,怎么播放具体看注释。
遇到的问题
1、笔者在测试的过程中发现打开解码器报错:
Generic error in an external library
经查验代码发现是没有给FFmpeg设置JavaJVM,需要调用函数设置av_jni_set_java_vm
JavaJVM参数即可。
2、如果解码得到的AVFrame的格式不是NV12或者NV21的话,表示数据有可能保存在GPU中,可以通过函数av_hwframe_transfer_data
将数据取出到CPU。
推荐阅读
FFmpeg连载1-开发环境搭建
FFmpeg连载2-分离视频和音频
FFmpeg连载3-视频解码
FFmpeg连载4-音频解码
FFmpeg连载5-音视频编码
FFmpeg连载6-音频重采样
FFmpeg连载8-视频合并以及替换视频背景音乐实战
ffplay调试环境搭建
ffplay整体框架
ffplay数据读取线程
ffplay音视频解码线程
ffplay音视频同步
NDK21编译ffmpeg5.0.1
关注我,一起进步,人生不止coding!!!