代码可以参考: Github地址
本文主要介绍如何通过FFmpeg将MP4格式的视频数据解码为一帧一帧的RGBA像素格式数据来播放。
因为主要是视频的解码及播放,对于音频只是解码出了音频对应的pcm数据,并没有播放pcm。因此也不会涉及到音视频的同步。
主要流程是
解封装
文章目录
Java层的主要配置
首先建一个支持cpp的项目
1 app module build.grdle配置
externalNativeBuild {
cmake {
//cpp编译器flag
cppFlags "-std=c++11"
}
ndk{
//指定所支持的cpu架构
abiFilters "armeabi-v7a"
}
}
sourceSets{
main{
//指定ffmpeg路径
jniLibs.srcDirs=['libs']
}
}
指定cmake路径
android {
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
2 cmake文件配置
#1 声明cmake版本
cmake_minimum_required(VERSION 3.4.1)
#2 添加头文件路径(相对于本文件路径)
include_directories(include)
#3 设置ffmpeg库所在路径的变量
set(FF ${CMAKE_CURRENT_SOURCE_DIR}/libs/${ANDROID_ABI})
# 4添加ffmpeg相关库
# 4.1解码
add_library(avcodec SHARED IMPORTED)
set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION ${FF}/libavcodec.so)
# 4.2格式转换
add_library(avformat SHARED IMPORTED)
set_target_properties(avformat PROPERTIES IMPORTED_LOCATION ${FF}/libavformat.so)
# 4.3基础库
add_library(avutil SHARED IMPORTED)
set_target_properties(avutil PROPERTIES IMPORTED_LOCATION ${FF}/libavutil.so)
# 4.4格式转换
add_library(swscale SHARED IMPORTED)
set_target_properties(swscale PROPERTIES IMPORTED_LOCATION ${FF}/libswscale.so)
#4.5 音频重采样
add_library(swresample SHARED IMPORTED)
set_target_properties(swresample PROPERTIES IMPORTED_LOCATION ${FF}/libswresample.so)
#5 指定本地cpp文件 和 打包对应的so库
add_library(native-lib
SHARED
src/main/cpp/native-lib.cpp
)
find_library(log-lib
log )
# 7 链接库
target_link_libraries(native-lib
avcodec avformat avutil swscale swresample
android
${log-lib} )
3 创建GLSurfaceView的子类
这一步骤主要是在开启的子线程中,将GLSurfaceView的Surface传递到底层来渲染数据
//1 创建GlSurfaceView的子类
public class PlayView extends GLSurfaceView implements Runnable, SurfaceHolder.Callback, GLSurfaceView.Renderer {
public PlayView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void start(){
//2 手动创建线程
new Thread(this).start();
}
@Override
public void run() {
String videoPath = Environment.getExternalStorageDirectory() + "/video.mp4";
//3 在子线程中 将视频url和Surface对象传递到native层
open(videoPath, getHolder().getSurface());
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
//4 android8.0必须调用此方法,否则无法显示
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
setRenderer(this);
}
}
public native void open(String url, Object surface);
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
}
@Override
public void onSurfaceChanged(GL10 gl10, int i, int i1) {
}
@Override
public void onDrawFrame(GL10 gl10) {
}
}
Native层的相关代码
1 初始化
首先注册解封装器,打开文件。然后就可以拿到封装数据里面的音频和视频的索引。
//1 初始化解封装
av_register_all();
AVFormatContext *ic = NULL;
//2 打开文件
int re = avformat_open_input(&ic, path, 0, 0);
if (re != 0) {
LOGEW("avformat_open_input %s success!", path);
} else {
LOGEW("avformat_open_input failed!: %s", av_err2str(re));
}
//3 获取流信息
re = avformat_find_stream_info(ic, 0);
if (re != 0) {
LOGEW("avformat_find_stream_info failed!");
}
LOGEW("duration = %lld nb_streams = %d", ic->duration, ic->nb_streams);
int fps = 0;
int videoStream = 0;
int audioStream = 1;
//4 获取视频音频流位置
for (int i = 0; i < ic->nb_streams; i++) {
AVStream *as = ic->streams[i];
if (as->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
LOGEW("视频数据");
videoStream = i;
fps = r2d(as->avg_frame_rate);
LOGEW("fps = %d, width = %d height = %d codeid = %d pixformat = %d", fps,
as->codecpar->width,
as->codecpar->height,
as->codecpar->codec_id,
as->codecpar->format);
} else if (as->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
LOGEW("音频数据");
audioStream = i;
LOGEW("sample_rate = %d channels = %d sample_format = %d",
as->codecpar->sample_rate,
as->codecpar->channels,
as->codecpar->format);
}
}
对于多路流可以通过遍历的方式拿到音频和视频流的索引地址,也可以直接指定流数据类型来获取索引地址
//5 获取音频流信息 和上面遍历取出视音频的流信息是一样的,这种方式更直接
audioStream = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
2 视解码器
在这一步骤中,首先定义并初始化解码器,然后视频流索引的地址传给解码器。最后打开解码器。
//1 软解码器
AVCodec *vcodec = avcodec_find_decoder(ic->streams[videoStream]->codecpar->codec_id);
if (!vcodec) {
LOGEW("avcodec_find failed");
}
//2 解码器初始化
AVCodecContext *vc = avcodec_alloc_context3(vcodec);
//3 解码器参数赋值
avcodec_parameters_to_context(vc, ic->streams[videoStream]->codecpar);
// 定义解码的线程
vc->thread_count = 8;
//4 打开解码器
re = avcodec_open2(vc, 0, 0);
LOGEW("vc timebase = %d/ %d", vc->time_base.num, vc->time_base.den);
if (re != 0) {
LOGEW("avcodec_open2 video failed!");
}
需要注意的是,这里可以自定义解码的线程,来控制解码速度。可以自定义大小,后续代码会有介绍调整这个值以后,来测试解码速度大小。
另外,这部分只是通过软解码的方式,软解比较消耗CPU,但是兼容性好
硬解不消耗CPU,更省电,但是硬解可能会有兼容性的问题
下面是获取硬解码器
AVCodec *vcodec = avcodec_find_decoder_by_name("h264_mediacodec");
在使用硬解码器,还要额外定义此方法。用于确保在获取硬解码器之前调用av_jni_set_java_vm()函数,通过调用av_jni_set_java_vm()才可以获取到硬解码器。
extern "C"
JNIEXPORT
jint JNI_OnLoad(JavaVM *vm,void *res)
{
av_jni_set_java_vm(vm,0);
return JNI_VERSION_1_4;
}
3 音频解码器
和之前的视频解码器的步骤相同,只是部分参数不同
//1 软解码器
AVCodec *acodec = avcodec_find_decoder(ic->streams[audioStream]->codecpar->codec_id);
if (!acodec) {
LOGEW("avcodec_find failed!");
}
//2 解码器初始化
AVCodecContext *ac = avcodec_alloc_context3(acodec);
avcodec_parameters_to_context(ac, ic->streams[audioStream]->codecpar);
ac->thread_count = 1;
//3 打开解码器
re = avcodec_open2(ac, 0, 0);
if (re != 0) {
LOGEW("avcodec_open2 audio failed!");
}
4 开始解码
以下是解码过程的完整代码
//1 定义Packet和Frame
AVPacket *pkt = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
//用于测试性能
long long start = GetNowMs();
int frameCount = 0;
//2 像素格式转换的上下文
SwsContext *vctx = NULL;
int outwWidth = 1280;
int outHeight = 720;
char *rgb = new char[1920*1080*4];
char *pcm = new char[48000*4*2];
//3 音频重采样上下文初始化
SwrContext *actx = swr_alloc();
actx = swr_alloc_set_opts(actx,
av_get_default_channel_layout(2),
AV_SAMPLE_FMT_S16,
ac->sample_rate,
av_get_default_channel_layout(ac->channels),
ac->sample_fmt,ac->sample_rate,0,0);
re = swr_init(actx);
if(re != 0)
{
LOGEW("swr_init failed!");
}else
{
LOGEW("swr_init success!");
}
//4 显示窗口初始化
ANativeWindow *nwin = ANativeWindow_fromSurface(env,surface);
ANativeWindow_setBuffersGeometry(nwin,outwWidth,outHeight,WINDOW_FORMAT_RGBA_8888);
ANativeWindow_Buffer wbuf;
for (;;) {
//这里是测试每秒解码的帧数 每三秒解码多少帧
if(GetNowMs() - start >= 3000)
{
LOGEW("now decode fps is %d", frameCount/3);
start = GetNowMs();
frameCount = 0;
}
int re = av_read_frame(ic, pkt);
if (re != 0) {
LOGEW("读取到结尾处!");
int pos = 20 * r2d(ic->streams[videoStream]->time_base);
av_seek_frame(ic, videoStream, pos, AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME);
break;
}
AVCodecContext *cc = vc;
if (pkt->stream_index == audioStream) {
cc = ac;
}
//1 发送到线程中解码
re = avcodec_send_packet(cc, pkt);
//清理
int p = pkt->pts;
av_packet_unref(pkt);
if (re != 0) {
LOGEW("avcodec_send_packet failed!");
continue;
}
//每一帧可能对应多个帧数据,所以要遍历取
for (;;) {
//2 解帧数据
re = avcodec_receive_frame(cc, frame);
if (re != 0) {
break;
}
//LOGEW("avcodec_receive_frame %lld", frame->pts);
//如果是视频帧
if(cc == vc){
frameCount++;
//3 初始化像素格式转换的上下文
vctx = sws_getCachedContext(vctx,
frame->width,
frame->height,
(AVPixelFormat)frame->format,
outwWidth,
outHeight,
AV_PIX_FMT_RGBA,
SWS_FAST_BILINEAR,
0,0,0);
if(!vctx){
LOGEW("sws_getCachedContext failed!");
}else
{
uint8_t *data[AV_NUM_DATA_POINTERS] = {0};
data[0] = (uint8_t *)rgb;
int lines[AV_NUM_DATA_POINTERS] = {0};
lines[0] = outwWidth * 4;
int h = sws_scale(vctx,
(const uint8_t **)frame->data,
frame->linesize,
0,
frame->height,
data,
lines);
LOGEW("sws_scale = %d",h);
if(h > 0)
{
ANativeWindow_lock(nwin,&wbuf,0);
uint8_t *dst = (uint8_t*)wbuf.bits;
memcpy(dst, rgb, outwWidth*outHeight*4);
ANativeWindow_unlockAndPost(nwin);
}
}
}else //音频帧
{
uint8_t *out[2] = {0};
out[0] = (uint8_t*)pcm;
//音频重采样
int len = swr_convert(actx,out,frame->nb_samples,(const uint8_t**)frame->data,frame->nb_samples);
LOGEW("swr_convert = %d", len);
}
}
}
delete rgb;
delete pcm;
1 这段代码中,外层循环通过av_read_frame方法来给packet赋值,因为一个packet可能对应多个frame,所以packet每次通过avcodec_send_packet()方法发送到解码线程后,需要多次调用avcodec_receive_frame()来获取frame。
2 测试性能部分通过每三秒解码的帧数,除以3来计算平均每秒解码的帧数。
下面是获取当前时间的方法
long long GetNowMs()
{
struct timeval tv;
gettimeofday(&tv,NULL);
int sec = tv.tv_sec%360000;
long long t = sec*1000+tv.tv_usec/1000;
return t;
}
3 对于获取播放时间戳pts
在ffmpeg中用的是分数的时间基AVRational来表示时间的基本单位。
AVRational有两个变量分子和分母
typedef struct AVRational{
int num; ///< Numerator
int den; ///< Denominator
} AVRational;
例如时间基为1/1000。
通过转换可以把此分数转换为浮点数
static double r2d(AVRational rational)
{
return rational.num == 0 || rational.den == 0 ? 0.:(double)rational.num/ (double)rational.den;
}
这样就能算出每一帧对应的时间戳pts
pkt->pts = pkt->pts * (1000*r2d(ic->streams[pkt->stream_index]->time_base));
获取解码时间戳dts同理
5 硬解码和多线层解码性能测试
1 单线程解码平局速度
now decode fps is 18
now decode fps is 18
now decode fps is 19
2 六线程解码均速
now decode fps is 105
3 硬解码均速
now decode fps is 95
now decode fps is 92