承接上一篇FFmpeg解压MP4得YUV,在我们解压媒体文件(MP4,AVI,RMVB等)之后获取得到yuv420p格式的AVFrame之后,该怎么优雅的显示到Android的屏幕上呢?此时我们应该想到Android绘制用的SurfaceView / TextureView。接下来允许我装下*,写一个最简易丑陋的播放器。
public class ZzrFFPlayer {
public native void init(String media_input_str, Surface surface);
public native int play();
public native void release();
static
{
// Try loading libraries...
try {
System.loadLibrary("yuv");
System.loadLibrary("avutil");
System.loadLibrary("swscale");
System.loadLibrary("swresample");
System.loadLibrary("avcodec");
System.loadLibrary("avformat");
System.loadLibrary("postproc");
System.loadLibrary("avfilter");
System.loadLibrary("avdevice");
System.loadLibrary("zzr-ffmpeg-player");
} catch (Exception e) {
e.printStackTrace();
}
}
}
Java文件入手,创建native非static方法。加载的so除了ffmpeg的八大模块,还有另外一个新成员yuv,这个会立马介绍。然后zzr-ffmpeg-player就是我们需要编写的入口。
之后我们到src/main/cpp/ffmpeg/下建立新的文件,zzr_ffmpeg_player.c,立马进行开发。
JNIEnv *gJNIEnv;
jstring gInputPath;
jobject gSurface;
void custom_log(void *ptr, int level, const char* fmt, va_list vl){
FILE *fp=fopen("/storage/emulated/0/av_log.txt","w+");
if(fp){
vfprintf(fp,fmt,vl);
fflush(fp);
fclose(fp);
}
}
JNIEXPORT void JNICALL
Java_org_zzrblog_mp_ZzrFFPlayer_init(JNIEnv *env, jobject jobj, jstring input_jstr, jobject surface)
{
gJNIEnv = env;
//创建输入的媒体资源的全局引用。
gInputPath = (jstring) (*gJNIEnv)->NewGlobalRef(gJNIEnv, input_jstr);
//创建surface全局引用。
gSurface = (*gJNIEnv)->NewGlobalRef(gJNIEnv, surface);
// 0.FFmpeg's av_log output
av_log_set_callback(custom_log);
// 1.注册组件
av_register_all();
avcodec_register_all();
avformat_network_init();
}
JNIEXPORT void JNICALL
Java_org_zzrblog_mp_ZzrFFPlayer_release(JNIEnv *env, jobject jobj)
{
if(gJNIEnv!=NULL)
{
(*gJNIEnv)->DeleteGlobalRef(gJNIEnv, gInputPath);
(*gJNIEnv)->DeleteGlobalRef(gJNIEnv, gSurface);
}
gJNIEnv = NULL;
}
知识点NewGlobalRef之前已经介绍过了,这里我们运用到实际。init方法传入的资源文件路径,以及用于绘制视频的Surface。然后新增一个关于ffmpeg开发调试的能tips,类似于java的system.out,c++的cout,av_log_set_callback。通过设置av_log_set_callback回调函数,我们可以捕获ffmpeg的debug信息,并重定向到本地文件。
JNIEXPORT jint JNICALL
Java_org_zzrblog_mp_ZzrFFPlayer_play(JNIEnv *env, jobject jobj)
{
const char *input_cstr = (*env)->GetStringUTFChars(env, gInputPath, 0);
AVFormatContext *pFormatContext = avformat_alloc_context();
// 打开输入视频文件
if(avformat_open_input(&pFormatContext, input_cstr, NULL, NULL) != 0){
LOGE("%s","打开输入视频文件失败");
return -1;
}
// 获取视频信息
if(avformat_find_stream_info(pFormatContext,NULL) < 0){
LOGE("%s","获取视频信息失败");
return -2;
}
int video_stream_idx = -1;
for(int i=0; inb_streams; i++)
{
//根据类型判断,是否是视频流
if(pFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_idx = i;
break;
}
}
LOGD("VIDEO的索引位置:%d", video_stream_idx);
AVCodec *pCodec = avcodec_find_decoder(pFormatContext->streams[video_stream_idx]->codecpar->codec_id);
if(pCodec == NULL) {
LOGE("%s","解码器创建失败.");
return -3;
}
AVCodecContext * pCodecContext = avcodec_alloc_context3(pCodec);
if(pCodecContext == NULL) {
LOGE("%s","创建解码器对应的上下文失败.");
return -4;
}
avcodec_parameters_to_context(pCodecContext, pFormatContext->streams[video_stream_idx]->codecpar);
if(avcodec_open2(pCodecContext, pCodec, NULL) < 0){
LOGE("%s","解码器无法打开");
return -5;
} else {
LOGI("设置解码器解码格式pix_fmt:%d", pCodecContext->pix_fmt);
}
//编码数据
AVPacket *packet = av_packet_alloc();
//像素数据(解码数据)
AVFrame *yuv_frame = av_frame_alloc();
AVFrame *rgb_frame = av_frame_alloc();
// 准备native绘制的窗体
ANativeWindow* nativeWindow = ANativeWindow_fromSurface(gJNIEnv,gSurface);
// 设置缓冲区的属性(宽、高、像素格式)
ANativeWindow_setBuffersGeometry(nativeWindow, pCodecContext->width, pCodecContext->height,
WINDOW_FORMAT_RGBA_8888);
// 绘制时的缓冲区
ANativeWindow_Buffer nativeWinBuffer;
int ret;
while(av_read_frame(pFormatContext, packet) >= 0)
{
if(packet->stream_index == video_stream_idx)
{
//AVPacket->AVFrame
ret = avcodec_send_packet(pCodecContext, packet);
if(ret < 0){
LOGE("avcodec_send_packet:%d\n", ret);
continue;
}
while(ret >= 0) {
ret = avcodec_receive_frame(pCodecContext, yuv_frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){
LOGD("avcodec_receive_frame:%d\n", ret);
break;
}else if (ret < 0) {
LOGW("avcodec_receive_frame:%d\n", AVERROR(ret));
goto end; //end处进行资源释放等善后处理
}
if (ret >= 0)
{
ANativeWindow_lock(nativeWindow, &nativeWinBuffer, NULL);
// 上锁并关联 ANativeWindow + ANativeWindow_Buffer
av_image_fill_arrays(rgb_frame->data, rgb_frame->linesize, nativeWinBuffer.bits,
AV_PIX_FMT_RGBA, pCodecContext->width, pCodecContext->height, 1 );
// rgb.AVFrame对象 关联 ANativeWindow_Buffer的内存空间
I420ToARGB(yuv_frame->data[0], yuv_frame->linesize[0],
yuv_frame->data[2], yuv_frame->linesize[2],
yuv_frame->data[1], yuv_frame->linesize[1],
rgb_frame->data[0], rgb_frame->linesize[0],
pCodecContext->width, pCodecContext->height);
// yuv.AVFrame 转 rgb.AVFrame。借助第三方库libyuv.so
ANativeWindow_unlockAndPost(nativeWindow);
// 释放锁并 swap交换显示内存到屏幕上。
usleep(100 * 16);
}
}
}
av_packet_unref(packet);
}
end:
ANativeWindow_release(nativeWindow);
av_frame_free(&yuv_frame);
av_frame_free(&rgb_frame);
avcodec_close(pCodecContext);
avcodec_free_context(&pCodecContext);
avformat_close_input(&pFormatContext);
avformat_free_context(pFormatContext);
(*gJNIEnv)->ReleaseStringUTFChars(gJNIEnv, gInputPath, input_cstr);
return 0;
}
然后我们到play方法的实现,ffmpeg知识点和流程与上一篇的内容是一致的。我们就直接说这次文章的主要知识点:NDK的Surface -> ANativeWindow 的使用方法。
1、准备native绘制的窗体。ANativeWindow* ANativeWindow_fromSurface(JNIEnv* env, jobject surface); 需要包含头文件native_window_jni.h。这样就建立了c/c++中关联surface对象ANativeWindow。
2、设置ANativeWindow buffer区的属性(宽、高、像素格式)。ANativeWindow_setBuffersGeometry(ANativeWindow* window,int32_t width, int32_t height, int32_t format); 注意这个方法只是设置属性,对应ANativeWindow buffer区还没有实际存在的!我们需要另行建立一个新的ANativeWindow_Buffer nativeWinBuffer对象。注意这个nativeWinBuffer也还没有与ANativeWindow正式关联的!
3、准备工作差不多了,我们开始正式的绘制。绘制过程在获取解码结果avcodec_receive_frame的循环中进行。流程如下:
以上四步流程都是建立在解压一帧AVFrame的过程当中。其中各个对象关联的关系比较繁杂,又要祭出灵魂画师的图解了
从图解我们要理解到:
FFmpeg解压得出yuv格式的视频帧AVFrame,然后借助libyuv.so,把yuv.AVFrame转换成rgb格式的AVFrame。
rgb.AVFrame对象操作的内存是指向 ANativeWindow_Buffer 对象的真实内存(bits字段)。ANativeWindow_Buffer 对象 又是与ANativeWindow对象绑定的显示内存块。
当yuv.AVFrame转换到rgb.AVFrame之后,rgb格式的数据直接update到所指向的内存区域,即 ANativeWindow_Buffer.bits,然后ANativeWindow_unlockAndPost触发 底层的swap操作把新的一帧图update到屏幕上。
重点介绍完毕了。我们看看CMake编译脚本。这里需要注意一点,我们需要从系统库找出名叫 android 的动态库,这个动态库才能使用ANativeWindow !!! 然后添加 libyuv.so 的引用 + ffmpeg需要的模块,得出我们的 zzr-ffmpeg-player.so
# 在系统找出预编译的android库,指定在CMake脚本下的别名为android-lib
# android-lib for native windows
find_library( android-lib android )
add_library(yuv SHARED IMPORTED )
set_target_properties(yuv PROPERTIES IMPORTED_LOCATION
${PROJECT_SOURCE_DIR}/src/main/cpp/libyuv/libyuv.so)
set_target_properties(yuv PROPERTIES LINKER_LANGUAGE CXX)
add_library( # 生成动态库的名称
zzr-ffmpeg-player
# 指定是动态库SO
SHARED
# 编译库的源代码文件
src/main/cpp/ffmpeg/zzr_ffmpeg_player.c)
target_link_libraries( # 指定目标链接库
zzr-ffmpeg-player
# 添加预编译库到目标链接库中
${log-lib}
${android-lib}
avutil
avcodec
avformat
swscale
yuv )
ZzrFFPlayer使用方法比较简单:
public void clickOnPlay(@SuppressLint("USELESS") View view) {
String path = Environment.getExternalStorageDirectory().getPath();
String input_mp4 = path + "/10s_test.mp4";
if(ffPlayer==null) {
ffPlayer = new ZzrFFPlayer();
ffPlayer.init(input_mp4,surfaceView.getHolder().getSurface());
}
ffPlayer.play();
}
详情请参考GitHub工程:https://github.com/MrZhaozhirong/BlogApp
源码我是通过git 下载的。地址是 https://chromium.googlesource.com/external/libyuv
安装好git,右键Git Bash here,然后git clone https://chromium.googlesource.com/external/libyuv
如果出现 Git clone远程目录443:Timed out 问题解决方案
1.设置本地电脑的代理VPN
2.设置Git工具的代理(命令如下:)
$ git config --global http.proxy "localhost:1080"
下载后的是libyuv为根目录。我们改成jni。然后打包压缩到Linux环境。然后在linux环境ndk-build。这是因为ndk-build要以jni为根目录才能识别NDK的工程。
不想自己编译可以上我的github下载工程,自己从工程的src/main/cpp/libyuv的文件夹获取。