使用ffmpeg + ndk21读取视频帧并转为bitmap

流程:ffmpeg读取视频帧的yuv-> jni层创建Bitmap,拿到bitmap表示像素数据的指针->将YUV转换到bitmap的像素数据中(ARGB_8888)

一. ffmpeg读取视频帧的yuv

这里只处理格式为yuv420p的视频帧

初始化AVFormatContext

    const char *cstr = videoPath.c_str();

    LOGD("inputFmtContext = %p", iFmtContext);
    
    
    //打开AVFormatContext,用于解封装的上下文
    int ret = avformat_open_input(&iFmtContext, cstr, nullptr, nullptr);

    if (ret != 0) {
        LOGE("avformat_open_input file %s failed,%s", cstr, av_err2str(ret));
        return;
    }

    LOGI("av_find_best_stream file %s success", cstr);

    avformat_find_stream_info(iFmtContext, nullptr);
    
    //找到视频流在iFmtContext内部数组里的索引
    int videoIndex = av_find_best_stream(iFmtContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, -1);

    if (videoIndex < 0) {
        LOGE("av_find_best_stream file %s failed,%d", cstr, videoIndex);
        return;
    }

    videoStream = iFmtContext->streams[videoIndex];
    LOGD("video stream index = %d,duration = %lu,real duration = %f", videoIndex,
         videoStream->duration, videoStream->duration * timeBaseToDuration(videoStream->time_base));

打开解码器,并获取YUV


    if (!iCodecContext) {

       //查找解码器
        AVCodec *avCodec = avcodec_find_decoder(videoStream->codecpar->codec_id);
        if (!avCodec) {
            LOGW("getFrameAt avcodec_find_decoder failed");
            return nullptr;
        }

        LOGD2(LOG_TAG, "codec name:%s", avCodec->name);
        iCodecContext = avcodec_alloc_context3(avCodec);
        if (!iCodecContext) {
            LOGW("getFrameAt avcodec_alloc_context3 failed");
            return nullptr;
        }

       //从AVStream里面复制解码参数
        int err = avcodec_parameters_to_context(iCodecContext, videoStream->codecpar);
        if (err < 0) {
            LOGW("getFrameAt avcodec_parameters_to_context failed,err:%s", av_err2str(err));
            return nullptr;
        }

        err = avcodec_open2(iCodecContext, avCodec, nullptr);
        if (err < 0) {
            LOGW("getFrameAt avcodec_open2 failed,err:%s", av_err2str(err));
            return nullptr;
        }
    }

    LOGI("codec init success!!!");

    // 未解码数据结构体
    AVPacket *packet = av_packet_alloc();

    //已解码数据结构体
    AVFrame *frame = av_frame_alloc();

    int64_t frameNum = 0;


    int length = 0;
    int read = 0;

  // seek到指定时间cuo
    int seek = av_seek_frame(iFmtContext, videoStream->index,
                             timeMills / 1000 / timeBaseToDuration(videoStream->time_base),
                             AVSEEK_FLAG_BACKWARD);

    if (seek < 0) {
        LOGW("seek failed,code:%d", seek);
        goto end;
    }

    while (!(read = av_read_frame(iFmtContext, packet))) {

        LOGD2(LOG_TAG, "packet index:%d", packet->stream_index);
        if (packet->stream_index == videoStream->index) {
            //LOGD("read frame:%" PRId64 ,frameNum);
            //将数据发送到解码器解码
            int code = avcodec_send_packet(iCodecContext, packet);
            if (code != 0) {
                LOGW("avcodec_send_packet failed");
                av_packet_unref(packet);
                break;
            }

            frameNum++;
            int ret = 0;
            int num = 0;
          
            //读取解码后的视频数据
            if ((ret = avcodec_receive_frame(iCodecContext, frame)) == AVERROR(EAGAIN)) {
                LOGD("avcodec_receive_frame ret:%d,", ret);
                continue;
            }

            if (!ret) {
                num++;
                LOGD("single codec return:%d,ret:%d", num, ret);
                LOGD("frame width: %d,height: %d", frame->width, frame->height);

                // writeSingleFrame2File(frame);
                // yuv4202RGB(frame);
              
                //这里拿到的frame数据就包含一帧yuv视频数据了
                yuv420ToRgb(frame, rgb);

            }
            if (ret < 0) {
                LOGW("avcodec_receive_frame err:%d,%s", ret, av_err2str(ret));
            }


            av_packet_unref(packet);

            break;

        }
    }

    LOGD("frame num:%" PRId64 ",frame read:%" PRId64 ",read %d", videoStream->nb_frames, frameNum,
         read);

    end:
    av_packet_free(&packet);
    av_frame_free(&frame);



二. jni层创建Bitmap,拿到bitmap表示像素数据的指针

  1. native创建一个bitmap
static jobject createBitmap(JNIEnv *env, int width, int height) {
    jclass bitmapCls = env->FindClass("android/graphics/Bitmap");

    if (!bitmapCls) {
        LOGW("bitmapCls failed");
        return nullptr;
    }
    jmethodID createBitmapFunction = env->GetStaticMethodID(bitmapCls,"createBitmap",
                                                            "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");

    if (!createBitmapFunction) {
        LOGW("createBitmapFunction failed");
        return nullptr;
    }

    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;");

    if (!valueOfBitmapConfigFunction) {
        LOGW("valueOfBitmapConfigFunction failed");
        return nullptr;
    }

    LOGI("valueOfBitmapConfigFunction success");

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

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

    return bitmap;
}

2 拿到bitmap表示像素数据的指针
需要添加内置本地库: jnigraphics

  jobject bitmap = createBitmap(env, width, height);

    int ret;
    uint8_t *rgbData = nullptr;
    //AndroidBitmap_lockPixels后,rgbData就指向bitmap的像素数据了
    if ((ret = AndroidBitmap_lockPixels(env, bitmap, (void**)&rgbData)) < 0) {
        LOGW("AndroidBitmap_lockPixels() failed ! error=%d", ret);
        return nullptr;
    }


    LOGD("AndroidBitmap_lockPixels ret=%d", ret);

    reader->getFrameAt(time_mills,&rgbData);
    LOGD("getFrameAt end");
     //TODO
    AndroidBitmap_unlockPixels(env, bitmap);

  //返回bitmap到java层
   return bitmap;

三 将YUV数据转换到bitmap的像素数据中


static void yuv420ToRgb(AVFrame *frame, uint8_t **rgb) {
    int img_width = frame->width;
    int img_height = frame->height;
    //int buffer_len = frame->width * frame->height;
    //uint8_t *buffer = static_cast<uint8_t *>(malloc(sizeof(uint8_t) * buffer_len * 4));
    int channels = 4;

    uint8_t *buffer = *rgb;

    for (int y = 0; y < img_height; y++) {
        for (int x = 0; x < img_width; x++) {

            //linesize[0]表示一行Y数据需要多少字节存储, 由于字节对齐的优化,一般会大于图片的宽度,例如,测试视频linesize[0]为864,img_width为854
            int indexY = y * frame->linesize[0] + x;
            int indexU = y / 2 * frame->linesize[1] + x / 2;
            int indexV = y / 2 * frame->linesize[2] + x / 2;
            uint8_t Y = frame->data[0][indexY];
            uint8_t U = frame->data[1][indexU];
            uint8_t V = frame->data[2][indexV];

            // 这里可以参考YUV420转rgb公式
            int R = Y + 1.402 * (V - 128);  // 由于计算的结果可能不在0~255之间,所以R不能用uint8_t表示
            int G = Y - 0.34413 * (U - 128) - 0.71414 * (V - 128);
            int B = Y + 1.772 * (U - 128);
            R = (R < 0) ? 0 : R;
            G = (G < 0) ? 0 : G;
            B = (B < 0) ? 0 : B;
            R = (R > 255) ? 255 : R;
            G = (G > 255) ? 255 : G;
            B = (B > 255) ? 255 : B;
            buffer[(y * img_width + x) * channels + 0] = (uint8_t) R;
            buffer[(y * img_width + x) * channels + 1] = (uint8_t) G;
            buffer[(y * img_width + x) * channels + 2] = (uint8_t) B;
            //补充 alpha通道数据, android转bitmap需要
            buffer[(y * img_width + x) * channels + 3] = 0xff;
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容