NDK学习笔记:FFmpeg + SurfaceView = 播放 解码后的AVFrame(ANativeWindow_fromSurface)

NDK学习笔记:FFmpeg + SurfaceView = 播放AVFrame

 

承接上一篇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的循环中进行。流程如下:

  • ANativeWindow_lock(nativeWindow, &nativeWinBuffer, NULL); 上锁并关联 ANativeWindow + ANativeWindow_Buffer
  • av_image_fill_arrays 将rgb.AVFrame对象 关联 ANativeWindow_Buffer的真实内存空间actual bits.
  • 借助第三算法库libyuv.so提供的方法I420ToARGB,将yuv.AVFrame 转成 rgb.AVFrame
  • ANativeWindow_unlockAndPost 释放锁并swap交换显示内存到屏幕上。

以上四步流程都是建立在解压一帧AVFrame的过程当中。其中各个对象关联的关系比较繁杂,又要祭出灵魂画师的图解了

NDK学习笔记:FFmpeg + SurfaceView = 播放 解码后的AVFrame(ANativeWindow_fromSurface)_第1张图片

从图解我们要理解到:

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

 

 

 

 

番外:关于libyuv.so的编译方法。

源码我是通过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的文件夹获取。

 

你可能感兴趣的:(NDK学习笔记)