FFmpeg 获取 视频首帧 转 封面图Bitmap

这是一篇学习 FFmpeg 的技术文章,主要是使用 FFmpeg 获取本地视频文件的第一帧数据转换为 Bitmap,然后抛给上层ImageView显示。

大致流程可以分为:

  1. 传入视频文件路径,解封装

  2. 找到视频流,从流中找到解码器

  3. 打开解码器,读取第一个完整的AVFrame帧

  4. 创建bitmap,使用libyuv将yuv转为argb关联给bitmap显示

  5. 释放资源

先定义 JNI 函数

Java 函数:

public static native Bitmap getCover(String path);

JNI 函数:

JNIEXPORT jobject JNICALL
Java_demo_simple_example_1ffmpeg_MainActivity_getCover(JNIEnv *env,
                                                       jclass clazz,
                                                       jstring path) {
  const char *_path = env->GetStringUTFChars(path, JNI_FALSE);
  int ret = -1;
}

打开视频文件,解封装

    //封装格式上下文    
    AVFormatContext *ifmt_ctx = NULL;
    //打开输入源
    ret = avformat_open_input(&ifmt_ctx, _path, 0, 0);
    if (ret < 0) {
        logDebug("解封装失败 -- %s", av_err2str(ret));
        return nullptr;
    }

使用 avformat_open_input() 函数从输入文件中找到格式化I/O上下文 AVFormatContext 结构体,如果是编码要新建 AVFormatContext 要使用 avformat_alloc_context() 函数。

使用 avformat_open_input() 务必记得在程序执行完成后调用 avformat_close_input() 释放资源,并且该方法有一个int的返回值,0表示执行成功,负数表示执行失败,我们也可以用 av_err2str() 函数获取执行函数失败的详细日志,该函数定义在 libavutil/error.h 头文件中。

这里有个小技巧,ffmpeg大多有返回值函数,大于等于0都为成功,小于0都为执行失败。

找到流,从流的数组中找到视频流

    ret = avformat_find_stream_info(ifmt_ctx, 0);
    int video_stream_index = -1;
    AVStream *pStream = NULL;
    AVCodecParameters *codecpar = NULL;
    //找出视频流
    for (int i = 0; i < ifmt_ctx->nb_streams; ++i) {
        pStream = ifmt_ctx->streams[i];
        if (pStream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            codecpar = pStream->codecpar;
            video_stream_index = i;
        }
    }

avformat_find_stream_info() 函数从媒体文件的数据包中获取流信息赋值给AVFormatContext。

AVFormatContext 结构体中的 streams 字段包含了媒体文件中所以的流信息,包含视频流,音频流,字幕流等。

AVCodecParameters 结构体描述了被编码后的流的属性。

找到解码器,申请编解码器上下文

logDebug("解码器 == %s", avcodec_get_name(codecpar->codec_id));
AVCodec *codec = avcodec_find_decoder(codecpar->codec_id);
//申请编解码器上下文
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);

打开编解码器,获取首帧

    //拷贝parameters 到 编解码器的context
    ret = avcodec_parameters_to_context(codec_ctx, codecpar);
    //打开编解码器
    ret = avcodec_open2(codec_ctx, codec, NULL);
    //申请一个帧结构体
    AVFrame *pFrame = av_frame_alloc();

   int frameFinished;
    while (av_read_frame(ifmt_ctx, &pkg) >= 0) {
        if (pkg.stream_index != video_stream_index) {
            continue;
        }
        ret = avcodec_decode_video2(codec_ctx, pFrame, &frameFinished, &pkg);
        if (!frameFinished)
            continue;
    }

av_read_frame() 函数读取视频流信息,并将其存放到 AVPacket 结构的 pkt 变量中,这里我们只需分配 AVPacket 结构体的内存,数据(pkt->data)的数据则由 FFmpeg 在其内部自动分配,不过使用完毕后,要调用av_packet_unref() 函数释放。

av_read_frame()函数的返回值如果小于0代表发生了错误或者是读到了文件的末尾。

avcodec_decode_video()函数可以将 packet 转换成 frame,不过,解码一个 packet 不一定能够获得 frame 的全部信息,所以需要借助frameFinished 标志位用于判断这一过程。

其实新版已经使用avcodec_send_packet() 和avcodec_receive_frame()代替了这个函数,但是这里用下也没关系。

frameFinished参数=0时表示没有帧可以解压缩了,反之不为0时就还可以继续解压缩。

创建 Bitmap

jobject createBitmap(JNIEnv *env,
                     int width, int height) {

    jclass bitmapCls = env->FindClass("android/graphics/Bitmap");
    jmethodID createBitmapFunction = env->GetStaticMethodID(bitmapCls,
                                                            "createBitmap",
                                                            "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
    jstring configName = env->NewStringUTF("ARGB_8888");
    jclass bitmapConfigClass = env->FindClass("android/graphics/Bitmap$Config");
    jmethodID valueOfBitmapConfigFunction = env->GetStaticMethodID(bitmapConfigClass,
                                                                   "valueOf",
                                                                   "(Ljava/lang/String;)Landroid/graphics/Bitmap$Config;");

    jobject bitmapConfig = env->CallStaticObjectMethod(bitmapConfigClass,
                                                       valueOfBitmapConfigFunction,
                                                       configName);

    jobject newBitmap = env->CallStaticObjectMethod(bitmapCls,
                                                    createBitmapFunction,
                                                    width, height,
                                                    bitmapConfig);

    return newBitmap;
}

常规操作,在native层调用java层的方法 。

使用 libyuv 写入 rgb 像素信息

先使用 bitmap.h 头文件中 AndroidBitmap_lockPixels() 的函数绑定像素指针的地址,使用 libyuv 中的 I420ToABGR() 函数将 yuv420p 转换为 argb,记得最后使用 AndroidBitmap_unlockPixels() 函数回收Bitmap。

    jobject bmp;
    bmp = createBitmap(env, codec_ctx->width, codec_ctx->height);
  
  void *addr_pixels;
    ret = AndroidBitmap_lockPixels(env, bmp, &addr_pixels);

    //yuv420p to argb
    int linesize = pFrame->width * 4;
    libyuv::I420ToABGR(pFrame->data[0], pFrame->linesize[0], // Y
                       pFrame->data[1], pFrame->linesize[1], // U
                       pFrame->data[2], pFrame->linesize[2], // V
                       (uint8_t *) addr_pixels, linesize,  // RGBA
                       pFrame->width, pFrame->height);

上面的 lineSize 计算规则为:一个像素点有4个字节,所以要宽度x4 .

Yuv420p转ARGB的函数名是I420ToABGR,并不是I420ToARGB。

我坑,之前手打代码过快,导致生成的Bitmap颜色显示一直都不对,找了好久都没发现错误在哪里,还是百度到了一篇博主也是犯了同样的错误,这就是人的固化思维啊!

释放资源

    av_packet_unref(&pkg);
    AndroidBitmap_unlockPixels(env, bmp);
    av_free(pFrame);
    avcodec_close(codec_ctx);
    avformat_close_input(&ifmt_ctx);
    env->ReleaseStringUTFChars(path, _path);

Activity 中的代码

        String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
                + "get_cover.mp4";
        Bitmap bitmap = getCover(path);

        Log.d(TAG, "bitmap width == " + bitmap.getWidth());
        Log.d(TAG, "bitmap height == " + bitmap.getHeight());
        Log.d(TAG, "bitmap config == " + bitmap.getConfig().name());
        Log.d(TAG, "bitmap byteCount == " + bitmap.getByteCount());

        ImageView ivCover = findViewById(R.id.ivCover);
        ivCover.setImageBitmap(bitmap);

Log输出和界面显示

demo.simple.example_ffmpeg D/MainActivity: bitmap width == 1080

demo.simple.example_ffmpeg D/MainActivity: bitmap height == 1920

demo.simple.example_ffmpeg D/MainActivity: bitmap config == ARGB_8888

demo.simple.example_ffmpeg D/MainActivity: bitmap byteCount == 8294400

 FFmpeg 获取 视频首帧 转 封面图Bitmap_第1张图片 

源码地址

https://github.com/simplepeng/AndroidExamples/tree/master/example_ffmpeg

作者:simplepeng

地址:https://juejin.im/post/5f02ec8b6fb9a07e753c8a03


技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。

推荐阅读:

音视频面试基础题

OpenGL ES 学习资源分享

一文读懂 YUV 的采样与格式

OpenGL 之 GPUImage 源码分析

推荐几个堪称教科书级别的 Android 音视频入门项目

觉得不错,点个在看呗~

你可能感兴趣的:(FFmpeg 获取 视频首帧 转 封面图Bitmap)