在前面的博文中咱们介绍了关于运用NDK编译FFMpeg6.0的一些坑以及相关的解决方法。
详情请参考:NDK编译ffmpeg6.0与x264的坑
在写《NDK编译ffmpeg6.0与x264的坑》一文的时分就说过了,咱们编译FFmpeg6.0的意图便是为了体验一下它NDK式的MediaCodec硬解码以及硬编码。
今日咱们就在android上运用FFmpeg6.0来体验一下它的硬解码,经过FFmpeg调用MediaCodec将视频数据解码为yuv数据并保存。
关于FFmpeg在android上硬解码的相关博文之前已经写过一篇博文:
ffmpeg之硬解码
仅仅之前的需求经过注册JNI的方法调用MediaCodec,但这在FFmpeg6.0之后不需求了。
在这儿趁便提一下一个关于学习ffmpeg的方法,众所周知,其实最好的学习材料便是官方的材料,没有比官方更权威的材料了。
一般在ffmpeg的源码目录doc/examples下就有很多比如,例如咱们想学习下ffmpeg硬解码的比如,就能够研究该目录下的hw_decode.c
这个比如。
FFmpeg6.0运用MediaCodec硬解码
下面说说运用FFmpeg调用MediaCodec进行硬解码的介个过程:
- 翻开编译选项
首要,要让FFmpeg支撑MediaCodec硬解码,在交叉编译时就要翻开相关装备,主要是enable一些与MediaCodec相关的属性:
--enable-hwaccels \
--enable-jni \
--enable-mediacodec \
--enable-decoder=h264_mediacodec \
--enable-decoder=hevc_mediacodec \
--enable-decoder=mpeg4_mediacodec \
--enable-hwaccel=h264_mediacodec \
- 找到对应的解码器
一般情况下假如咱们不重视软解码仍是硬解码的话经过解码器ID运用avcodec_find_decoder
函数获取到对应的解码器即可。
可是假如咱们想要运用硬解码,一般会运用函数avcodec_find_decoder_by_name
获取到对应的解码。
那么问题来了,在FFmepg中MediaCodec对应的硬解码器是啥呢?我怎么知道avcodec_find_decoder_by_name
应该传递的参数是什么呢?
咱们做NDK开发一定要学会妙用源码中configure
这个文件,经过这个文件能够获取到很多咱们想要的装备信息,最简单的,假如咱们不知道有哪些可装备的编译信息,
则能够运用./configure --help
进行检查。
同理,在FFmpeg的源码中,咱们能够经过指令行./configure --list-decoders
检查它所支撑的解码器,如图仍是很多的,可是也并不是说都能直接运用的,因为大多数都是第三方的库,
一般需求在编译时翻开进行链接编译后才干正常运用。
./configure --list-decoders
输出太多了,咱们只关怀MediaCodec相关的,咱们能够运用grep过滤一下:
./configure --list-decoders |grep mediacodec
输出如图,框起来的哪些便是能够作为函数avcodec_find_decoder_by_name
参数的值,进行MediaCodec硬解码。
- 装备硬解码器
要运用硬解码,你还得告诉解码器,你想要输出什么样的格式数据,这个便是装备硬解码器所要干的事情, 也便是说为了告诉解码器你想要获得的终究的YUV数据格式是什么?是NV12仍是NV21仍是其他?
在MediaCodec中硬解码的主要装备如下:
// 装备硬解码器
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;
}
}
- 初始化初始化mediacodec的buffer
咱们知道MediaCodec是基于队列的方法进行工作的,因而咱们还需求
// 硬件解码器初始化
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);
后续的其他过程就和软解码一样了,无非便是翻开解码器、读取视频包、将视频包送入解码器进行解码、从解码器中循环读取解码后的数据包等。 这些在之前的FFmpeg系列文章中已经介绍过很多了,这儿就不再负担了。
经过这么一个demo能够看出,万变不离其宗,FFMpeg6.0的硬解码比照曾经的形似仅仅省了一个av_jni_set_java_vm
过程罢了,可是其内部是绕过了JNI调用MediaCodec的,
至于功能有了多少提升呢?感兴趣的同学们能够自行测验下。
下面是完好的代码:
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavcodec/codec.h>
#include <libavutil/avutil.h>
#include <libavutil/pixdesc.h>
}
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;
}
LOGD_E("FFDecoder","Failed to get HW surface format.\n");
return AV_PIX_FMT_NONE;
}
void FFDecoder::decodeVideo(const char *videoPath, const char *yuvPath) {
AVFormatContext *avFormatContext = avformat_alloc_context();
int ret = avformat_open_input(&avFormatContext, videoPath, nullptr, nullptr);
if (ret < 0) {
LOGD_E("FFDecoder","翻开媒体文件失利");
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) {
LOGD_E("FFDecoder","找不到视频索引");
return;
}
LOGD_E("FFDecoder","找到视频索引:%d", video_index);
const AVCodec *avCodec = nullptr;
AVCodecContext *avCodecContext = nullptr;
AVPacket *avPacket = nullptr;
AVFrame *avFrame = nullptr;
FILE *yuv_file = 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) {
LOGD_E("FFDecoder","没有找到硬解码器h264_mediacodec");
return;
} else {
// 装备硬解码器
int i;
for (i = 0;; i++) {
const AVCodecHWConfig *config = avcodec_get_hw_config(avCodec, i);
if (nullptr == config) {
LOGD_E("FFDecoder","获取硬解码是装备失利");
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;
LOGD_E("FFDecoder","硬件解码器装备成功");
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) {
LOGD_E("FFDecoder","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) {
LOGD_E("FFDecoder","解码器翻开失利:%s",av_err2str(ret));
return;
} else {
LOGD_E("FFDecoder","解码器翻开成功");
}
avPacket = av_packet_alloc();
avFrame = av_frame_alloc();
yuv_file = fopen(yuvPath,"wb");
while (true) {
ret = av_read_frame(avFormatContext, avPacket);
if (ret != 0) {
LOGD_D("FFDecoder","av_read_frame end");
// todo可能解码器内还有缓存的数据,需求avcodec_send_packet空包进行冲刷
break;
}
if(avPacket->stream_index != video_index){
av_packet_unref(avPacket);
continue;
}
ret = avcodec_send_packet(avCodecContext,avPacket);
if(ret == AVERROR(EAGAIN)){
LOGD_E("FFDecoder","avcodec_send_packet EAGAIN");
} else if(ret < 0){
LOGD_E("FFDecoder","avcodec_send_packet fail:%s",av_err2str(ret));
return;
}
av_packet_unref(avPacket);
ret = avcodec_receive_frame(avCodecContext,avFrame);
LOGD_E("FFDecoder","avcodec_receive_frame:%d",ret);
while (ret == 0){
LOGD_D("FFDecoder","获取解码数据成功:%s",av_get_pix_fmt_name(static_cast<AVPixelFormat>(avFrame->format)));
LOGD_D("FFDecoder","linesize0:%d,linesize1:%d,linesize2:%d",avFrame->linesize[0],avFrame->linesize[1],avFrame->linesize[2]);
LOGD_D("FFDecoder","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);
}
}
// 资源开释
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;
}
}
关于解码出来的YUV数据开释正常,咱们能够用adb将yuv文件数据从手机中拉出来到电脑端,运用ffplay指令播映一下验证即可。 正如代码所注释的,假定咱们解码得到的数据是NV12的,那么ffplay的播映指令便是:
ffplay -i yuv文件途径 -pixel_format nv12 -framerate 25 -video_size yuv宽x高
//假如解码出来的数据是nv12
//ffplay -i d:/cap.yuv -pixel_format nv12 -framerate 25 -video_size 640x480
推荐
音视频入门基础
C++进阶
NDK学习入门
安卓camera使用开发
ffmpeg系列
Opengl入门进阶
webRTC
重视我,一起前进,人生不止coding!!!