IJKPlayer 硬解码转数据格式

由于我们渲染使用unity,所以不管iOS还是android都需要把解码出来的数据转成unity能使用的数据格式。软解码直接指定为YUV420P就可以了,但是硬解码则需要我们在解码出来之后进行数据格式的转换。

iOS比较简单,硬件芯片为统一的 videoToolBox解码出来的数据为NV12。在ijk初始化的时候我创建了dest_frame,创建的个数为picq中frame的个数。每次转格式的时候 我都把真实的数据存到了dest_frame中。在ijk的销毁时候 再去把创建的dest_frame销毁。videoToolBox解码出来的数据为CVPixelBufferRef,转格式代码如下:

static void convert_data_format(SDL_VoutOverlay *overlay, CVPixelBufferRef imageBuffer, AVFrame **dest_frame)
{
    OSType sourcePixelFormat;
    sourcePixelFormat = CVPixelBufferGetPixelFormatType(imageBuffer);
    UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
    UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);
    if (!bufferPtr || !bufferPtr1) {
        printf("!bufferPtr || !bufferPtr1\n");
        return;
    }
    size_t width = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0);
    size_t height = CVPixelBufferGetHeightOfPlane(imageBuffer,0);
    AVFrame *pFrame=*dest_frame;
    if (pFrame == NULL)
    {
        pFrame = av_frame_alloc();
        av_image_alloc(pFrame->data, pFrame->linesize, (int)width, (int)height, AV_PIX_FMT_YUV420P,1);
        *dest_frame = pFrame;
    }
    
    static int sws_flags =  SWS_FAST_BILINEAR;

    DebugPrintf("SPB_LOG fill frame  width : %d,  height : %d,  overlay->w : %d, overlay->h : %d\n", (int)width, (int)height, overlay->w,overlay->h);
    
    uint8_t * srcSlice[8] ={bufferPtr, bufferPtr1, NULL, NULL,NULL,NULL,NULL,NULL};
    int srcStride[8] = {(int)width, (int)width, 0, 0, 0,0,0,0};
    
    if (img_convert_ctx == NULL)
    {
        img_convert_ctx = sws_getContext(
                                         (int)width,
                                         (int)height,
                                         AV_PIX_FMT_NV12,
                                         (int)width,
                                         overlay->h,
                                         AV_PIX_FMT_YUV420P,
                                         sws_flags,
                                         NULL, NULL, NULL);
    }
    
    int ret = sws_scale(
              img_convert_ctx,
              srcSlice,
              srcStride,
              0,
              overlay->h,
              pFrame->data,
              pFrame->linesize);
   
    overlay->pixels[0]=pFrame->data[0];
    overlay->pixels[1]=pFrame->data[1];
    overlay->pixels[2]=pFrame->data[2];
    overlay->pitches[0]=pFrame->linesize[0];
    overlay->pitches[1]=pFrame->linesize[1];
    overlay->pitches[2]=pFrame->linesize[2];

}

上面的函数方法是转数据格式的代码,需要在ijksdl_vout_overlay_videotoolbox.m中的func_fill_frame方法中使用

func_fill_frame函数是SDL_VoutFillFrameYUVOverlay(vp->bmp, src_frame, &vp->dest_frame)函数指针的指向
  • func_fill_frame修改如下:
static int func_fill_frame(SDL_VoutOverlay *overlay, const AVFrame *frame, AVFrame **dest_frame)
{
    assert(frame->format == IJK_AV_PIX_FMT__VIDEO_TOOLBOX);

    SDL_VoutOverlay_Opaque *opaque = overlay->opaque;

    CVPixelBufferRef pixel_buf = CVBufferRetain(frame->opaque);
    
    if (opaque->pixel_buffer != NULL) {
        CVBufferRelease(opaque->pixel_buffer);
    }
    CVPixelBufferLockBaseAddress(pixel_buf, 0);
    
    convert_data_format(overlay, pixel_buf, dest_frame);

    CVPixelBufferUnlockBaseAddress(pixel_buf, 0);
    CVBufferRelease(frame->opaque);
    overlay->format = SDL_FCC__VTB;
    overlay->planes = 2;

#if 0
    if (CVPixelBufferLockBaseAddress(pixel_buffer, 0) != kCVReturnSuccess) {
        overlay->pixels[0]  = NULL;
        overlay->pixels[1]  = NULL;
        overlay->pitches[0] = 0;
        overlay->pitches[1] = 0;
        overlay->w = 0;
        overlay->h = 0;
        CVBufferRelease(pixel_buffer);
        opaque->pixel_buffer = NULL;
        return -1;
    }
    overlay->pixels[0]  = CVPixelBufferGetBaseAddressOfPlane(pixel_buffer, 0);
    overlay->pixels[1]  = CVPixelBufferGetBaseAddressOfPlane(pixel_buffer, 1);
    overlay->pitches[0] = CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, 0);
    overlay->pitches[1] = CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, 1);
    CVPixelBufferUnlockBaseAddress(pixel_buffer, 0);
    
#else
    /*spb modify
    overlay->pixels[0]  = NULL;
    overlay->pixels[1]  = NULL;
    overlay->pitches[0] = 0;
    overlay->pitches[1] = 0;
    overlay->is_private = 1;
     */
#endif

    overlay->w = (int)frame->width;
    overlay->h = (int)frame->height;
    return 0;
}

android比较麻烦,我是做iOS的,对android并不太了解。而且主要由于不知道mediacodec解码出来的数据格式是什么,mediacodec解码的数据格式更手机的硬件厂商有关。研究怎么把这个数据转成我们需要的格式。

经过研究mediacodec从解码到渲染显示整个流程并没有看到data在哪里。mediacodec解码的时候在初始化会绑定一个surfaceView,取出outbuffer的时候也只是返回一个output_index下标。渲染显示也是通过这个index。

查阅资料为了想要得到我们需要的数据格式,我们可以通过getOutputBuffer(int index)获取这一帧的buffer ijkplayer调用android的mediacodec并没有通过JNI调用Java层的封装,而是直接调用mediacodec的原生接口。mediacodec跟硬件交互,底层应该也是C的,直接调用不通过Java层的封装可能是效率会更高。但是这种调用也比普通的JNI调用Java复杂一些。ijkplayer的mediacodec代码量本来就很多,也比较复杂,修改起来也比较麻烦。等这个功能完成,我会记录一下mediacodec的修改流程。

根据我们项目的需要在unity中渲染并不需要surface所以在创建ijkplayer的时候我并没有创建和使用surface。 同时看mediacodec开发文档得知如果使用了surface那么getOutputBuffer这个函数将会返回NULL,所以如果你想要获得这个buffer就不要使用surface。

0.png

看ijk底层源码得知如果没有surface为空的话,ijk并不会走mediacodec的解码流程,而是会走一个假的模型流程。我们需要修改一下,即使surface为null也会走mediacodec的解码流程。

  • 修改后代码如下:
static SDL_AMediaCodec *create_codec_l(JNIEnv *env, IJKFF_Pipenode *node)
{
    IJKFF_Pipenode_Opaque        *opaque   = node->opaque;
    ijkmp_mediacodecinfo_context *mcc      = &opaque->mcc;
    SDL_AMediaCodec              *acodec   = NULL;

    if (opaque->jsurface == NULL) {
        /*spb modify*/
        av_log(NULL, AV_LOG_INFO, "spb_log opaque->jsurface == NULL\n");
        acodec = SDL_AMediaCodecJava_createByCodecName(env, mcc->codec_name);
        if (acodec) {
            strncpy(opaque->acodec_name, mcc->codec_name, sizeof(opaque->acodec_name) / sizeof(*opaque->acodec_name));
            opaque->acodec_name[sizeof(opaque->acodec_name) / sizeof(*opaque->acodec_name) - 1] = 0;
        }
        /*end*/

        // we don't need real codec if we don't have a surface
        /*spb modify
        acodec = SDL_AMediaCodecDummy_create();
        end */
    } else {
        acodec = SDL_AMediaCodecJava_createByCodecName(env, mcc->codec_name);
        if (acodec) {
            strncpy(opaque->acodec_name, mcc->codec_name, sizeof(opaque->acodec_name) / sizeof(*opaque->acodec_name));
            opaque->acodec_name[sizeof(opaque->acodec_name) / sizeof(*opaque->acodec_name) - 1] = 0;
        }
    }

#if 0
    if (!acodec)
        acodec = SDL_AMediaCodecJava_createDecoderByType(env, mcc->mime_type);
#endif

    if (acodec) {
        // QUIRK: always recreate MediaCodec for reconfigure
        opaque->quirk_reconfigure_with_new_codec = true;
        /*-
        if (0 == strncasecmp(mcc->codec_name, "OMX.TI.DUCATI1.", 15)) {
            opaque->quirk_reconfigure_with_new_codec = true;
        }
        */
        /* delaying output makes it possible to correct frame order, hopefully */
        if (0 == strncasecmp(mcc->codec_name, "OMX.TI.DUCATI1.", 15)) {
            /* this is the only acceptable value on Nexus S */
            opaque->n_buf_out = 1;
            ALOGD("using buffered output for %s", mcc->codec_name);
        }
    }

    return acodec;
}

在没有surface的时候创建的acodec只是一个假的模型,并不是真正的mediacodec,我们需要在没有surface的时候 也要使用mediacodec,因为我们只是需要的解码出来的数据,并不需要surface进行渲染播放。
OK,现在没有surface,mediacodec也可以正常运行了。

  • 简述转格式流程如下:
1. 我们通过现有的接口SDL_AMediaCodec_getOutputFormat(opaque->acodec);这个函数可以获取到outputbuffer的format size stride
slice-height width height。
2. 再添加自己的接口通过JNI调用mediacodec的getOutputBuffer获取到buffer数据
3. 有了数据和数据的格式,我们就可以直接调用FFmpeg的接口将数据转成我们需要的格式。
  • 首先添加一个C与mediacodec交互的接口SDL_AMediaCodecJava_getOutputBuffer,mediacodec的getOutputBuffer这个函数返回的是一个byteBuffer,我们用的jobject接收,看JNI文件可以知道jobject类型就是void的指针,void指针可以强制转换成其他类型的指针,所以我们把这个指针强制转换为uint8_t *类型。这样我们就得到了这一帧的buffer,也就是数据。
uint8_t *SDL_AMediaCodecJava_getOutputBuffer(SDL_AMediaCodec* acodec, size_t idx)
{
    AMCTRACE("%s", __func__);
    ssize_t write_ret = -1;
    uint8_t *output_ptr = NULL;
    jobject output_buffer = NULL;
    JNIEnv *env = NULL;
    if (JNI_OK != SDL_JNI_SetupThreadEnv(&env)) {
        ALOGE("%s: SetupThreadEnv failed", __func__);
        return NULL;
    }
    SDL_AMediaCodec_Opaque *opaque = (SDL_AMediaCodec_Opaque *)acodec->opaque;
    output_buffer = J4AC_MediaCodec__getOutputBuffer__catchAll(env, opaque->android_media_codec, idx);

    if (J4A_ExceptionCheck__catchAll(env) || !output_buffer) {
        ALOGE("%s: J4AC_MediaCodec__getOutputBuffer__catchAll failed\n", __func__);
        goto fail;
    }

    jlong buf_size = (*env)->GetDirectBufferCapacity(env, output_buffer);
    void *buf_ptr  = (*env)->GetDirectBufferAddress(env, output_buffer);
    write_ret = (ssize_t)buf_size;
    if(write_ret <= 0)
       goto fail;
    output_ptr = (uint8_t *)buf_ptr;
    
fail:
    SDL_JNI_DeleteLocalRefP(env, &output_buffer);
    return output_ptr;
}

我只是把核心的代码贴了出来,其他的相关代码C具体是怎么通过JNI调用mediacodec还是需要自己去添加的,因为文件比较多,所以就不一一拿出来了。output_buffer = SDL_AMediaCodec_getOutputBuffer(opaque->acodec, output_buffer_index);
获取到了解码后的数据output_buffer现在我们需要知道数据的格式,我们能获取到mediacodec的数据格式,mediacodec的数据格式对应FFmpeg的哪种数据格式呢。新版的FFmpeg已经支持封装了mediacodec,从FFmpeg源码中我们能找到这个格式的对应:FFmpeg只支持两种格式;YUV420P和NV12对应如下:

enum {
    COLOR_FormatYUV420Planar                              = 0x13,
    COLOR_FormatYUV420SemiPlanar                          = 0x15,
    COLOR_FormatYCbYCr                                    = 0x19,
    COLOR_FormatAndroidOpaque                             = 0x7F000789,
    COLOR_QCOM_FormatYUV420SemiPlanar                     = 0x7fa30c00,
    COLOR_QCOM_FormatYUV420SemiPlanar32m                  = 0x7fa30c04,
    COLOR_QCOM_FormatYUV420PackedSemiPlanar64x32Tile2m8ka = 0x7fa30c03,
    COLOR_TI_FormatYUV420PackedSemiPlanar                 = 0x7f000100,
    COLOR_TI_FormatYUV420PackedSemiPlanarInterlaced       = 0x7f000001,
};
    
static const struct {
   
    int color_format;
    enum AVPixelFormat pix_fmt;
   
} color_formats[] = {
   
    { COLOR_FormatYUV420Planar,                              AV_PIX_FMT_YUV420P },
    { COLOR_FormatYUV420SemiPlanar,                          AV_PIX_FMT_NV21    },
    { COLOR_QCOM_FormatYUV420SemiPlanar,                     AV_PIX_FMT_NV21    },
    { COLOR_QCOM_FormatYUV420SemiPlanar32m,                  AV_PIX_FMT_NV21    },
    { COLOR_QCOM_FormatYUV420PackedSemiPlanar64x32Tile2m8ka, AV_PIX_FMT_NV21    },
    { COLOR_TI_FormatYUV420PackedSemiPlanar,                 AV_PIX_FMT_NV21    },
    { COLOR_TI_FormatYUV420PackedSemiPlanarInterlaced,       AV_PIX_FMT_NV21    },
    { 0 }
};

/**
 根据mediacodec中的数据格式 映射到对应的 FFmpeg中的数据格式

 @param color_format mediacodec中的数据格式类型
 @return FFmpeg中数据格式类型
 */
static enum AVPixelFormat mcdec_map_color_format(int color_format)
{
    int i;
    enum AVPixelFormat ret = AV_PIX_FMT_NONE;
    for (i = 0; i < FF_ARRAY_ELEMS(color_formats); i++) {
        if (color_formats[i].color_format == color_format) {
            return color_formats[i].pix_fmt;
        }
    }
    av_log(NULL, AV_LOG_ERROR, "spb_log Output color format 0x%x (value=%d) is not supported\n",color_format, color_format);
    return ret;
}

/**
 获取mediacodec解码数据格式 并根据mediacodec中的数据格式 映射到对应的 FFmpeg中的数据格式

 @param opaque IJKFF_Pipenode_Opaque
 @return FFmpeg中数据格式类型
 */
static enum AVPixelFormat ijk_AMediaCodec_OutputFormat_Shine_FFMpegPixelFormat(IJKFF_Pipenode_Opaque *opaque)
{
    opaque->output_aformat = SDL_AMediaCodec_getOutputFormat(opaque->acodec);
    if (opaque->output_aformat) {
        SDL_AMediaFormat_getInt32(opaque->output_aformat, "width",          &width);
        SDL_AMediaFormat_getInt32(opaque->output_aformat, "height",         &height);
        SDL_AMediaFormat_getInt32(opaque->output_aformat, "color-format",   &color_format);

        SDL_AMediaFormat_getInt32(opaque->output_aformat, "stride",         &stride);
        SDL_AMediaFormat_getInt32(opaque->output_aformat, "slice-height",   &slice_height);
        SDL_AMediaFormat_getInt32(opaque->output_aformat, "crop-left",      &crop_left);
        SDL_AMediaFormat_getInt32(opaque->output_aformat, "crop-top",       &crop_top);
        SDL_AMediaFormat_getInt32(opaque->output_aformat, "crop-right",     &crop_right);
        SDL_AMediaFormat_getInt32(opaque->output_aformat, "crop-bottom",    &crop_bottom);
            // TI decoder could crash after reconfigure
            // ffp_notify_msg3(ffp, FFP_MSG_VIDEO_SIZE_CHANGED, width, height);
            // opaque->frame_width  = width;
            // opaque->frame_height = height;
        ALOGI(
            "ijk_AMediaCodec_getOutputFormat\n"
            "    width-height: (%d x %d)\n"
            "    color-format: (%s: 0x%x)\n"
            "    stride:       (%d)\n"
            "    slice-height: (%d)\n"
            "    crop:         (%d, %d, %d, %d)\n"
            ,
            width, height,
            SDL_AMediaCodec_getColorFormatName(color_format), color_format,
            stride,
            slice_height,
            crop_left, crop_top, crop_right, crop_bottom);
        
        return mcdec_map_color_format(color_format);
    }
    return AV_PIX_FMT_NONE;
}

/**
 mediacodec解码出的数据格式就是YUV420P,把这个数据copy一份到 dest_frame

 @param dest_frame YUV420P数据存储的地址
 */
static void convert_YUV420P_data_format(AVFrame **dest_frame)
{
    AVFrame *frame = *dest_frame;
    if (frame == NULL)
    {
        frame = av_frame_alloc();
        av_image_alloc(frame->data, frame->linesize, stride, slice_height, AV_PIX_FMT_YUV420P,1);
        *dest_frame = frame;
    }
    /*
    frame->data[0] = output_buffer;
    frame->data[1] = output_buffer+stride*slice_height;
    frame->data[2] = output_buffer+stride*slice_height*1.25;
    */
    uint8_t *srcSlice[4] ={output_buffer, output_buffer+stride*slice_height, output_buffer+stride*slice_height*5/4, NULL};
    int srcStride[4] = {stride, stride/2, stride/2, 0};
    av_image_copy(
        frame->data,
        frame->linesize,
        srcStride,
        srcSlice,
        AV_PIX_FMT_YUV420P,
        width,
        height
        );
    /*
    sws_scale(
              img_convert_ctx,
              srcSlice,
              srcStride,
              0,
              height,
              frame->data,
              frame->linesize);
    */             
}

/**
 mediacodec解码出来的数据格式是NV21类型,转格式为YUV420P,并存储到dest_frame

 @param dest_frame 数据要存储的位置
 */
static void convert_NV21_data_format(AVFrame **dest_frame)
{
    AVFrame *frame = *dest_frame;
    if (frame == NULL)
    {
        frame = av_frame_alloc();
        av_image_alloc(frame->data, frame->linesize, stride, slice_height, AV_PIX_FMT_YUV420P,1);
        *dest_frame = frame;
    }
    uint8_t *srcSlice[8] ={output_buffer, output_buffer+stride*slice_height, NULL, NULL,NULL,NULL,NULL,NULL};
    int srcStride[8] = {stride, stride, 0, 0, 0,0,0,0};
    
    sws_scale(
              img_convert_ctx,
              srcSlice,
              srcStride,
              0,
              height,
              frame->data,
              frame->linesize);
}

/**
 根据mediacodec解码出来的数据格式,初始化FFmpeg中数据转格式的结构体

 @param pix_fmt mediacodec解码出来的数据格式
 @return
 */
static int convert_data_format_init_sws(enum AVPixelFormat pix_fmt)
{
    int ret = -1;
    if(pix_fmt == AV_PIX_FMT_NONE)
    {   
        ret = -1;
        ALOGE("%s:mediacodec_get_ffmpeg_color_format: color_format is AV_PIX_FMT_NONE \n", __func__);
    }
    else if(pix_fmt == AV_PIX_FMT_YUV420P)
    {   
        src_format = AV_PIX_FMT_YUV420P;
        ret = 0;
        convert_AMC_data_format = convert_YUV420P_data_format;
    }
    else if(pix_fmt == AV_PIX_FMT_NV21)
    {
        src_format = AV_PIX_FMT_NV21;
        img_convert_ctx = sws_getContext(
                                        stride,
                                        slice_height,
                                        src_format,
                                        stride,
                                        slice_height,
                                        AV_PIX_FMT_YUV420P,
                                        sws_flags,
                                        NULL, NULL, NULL);

        convert_AMC_data_format = convert_NV21_data_format;
        ret = 0;
    }
    return ret;
}

convert_AMC_data_format这是定义的一个函数指针,根据mediacodec解码出来的数据格式映射到FFmpeg中的不同数据格式,给这个函数指针赋值。这样我们在转格式的时候,调用convert_AMC_data_format这个函数就可以了。

  • drain_output_buffer_l这个函数是mediacodec解码后取数据的函数,在这个函数中进行获取数据格式,初始化FFmpeg中的数据转格式结构体,获取解码后的数据,并将数据转成YUV420P,修改如下:
static int drain_output_buffer_l(JNIEnv *env, IJKFF_Pipenode *node, int64_t timeUs, int *dequeue_count, AVFrame *frame, AVFrame **dest_frame,int *got_frame)
{
    IJKFF_Pipenode_Opaque *opaque   = node->opaque;
    FFPlayer              *ffp      = opaque->ffp;
    int                    ret      = 0;
    SDL_AMediaCodecBufferInfo bufferInfo;
    ssize_t                   output_buffer_index = 0;
    
    if (dequeue_count)
        *dequeue_count = 0;

    if (JNI_OK != SDL_JNI_SetupThreadEnv(&env)) {
        ALOGE("%s:create: SetupThreadEnv failed\n", __func__);
        goto fail;
    }

    output_buffer_index = SDL_AMediaCodecFake_dequeueOutputBuffer(opaque->acodec, &bufferInfo, timeUs);
    
    /*spb modify
    if (convert_AMC_data_format == NULL) {
        av_log(NULL, AV_LOG_INFO, "spb_log  img_convert_ctx == NULL\n");
        if (convert_data_format_init_sws(ijk_AMediaCodec_OutputFormat_Shine_FFMpegPixelFormat(opaque)) == -1)
            ffp->is->mediacodec_fail = true;
           
    }
    end*/
    if (output_buffer_index == AMEDIACODEC__INFO_OUTPUT_BUFFERS_CHANGED) {
        ALOGI("AMEDIACODEC__INFO_OUTPUT_BUFFERS_CHANGED\n");
        // continue;
    } else if (output_buffer_index == AMEDIACODEC__INFO_OUTPUT_FORMAT_CHANGED) {
        ALOGI("AMEDIACODEC__INFO_OUTPUT_FORMAT_CHANGED\n");
        SDL_AMediaFormat_deleteP(&opaque->output_aformat);
        /*spb modify*/
        sws_freeContext(img_convert_ctx);
        img_convert_ctx = NULL;
        if (convert_data_format_init_sws(ijk_AMediaCodec_OutputFormat_Shine_FFMpegPixelFormat(opaque)) == -1)
            ffp->is->mediacodec_fail = true;
        /*end*/
        // continue;
    } else if (output_buffer_index == AMEDIACODEC__INFO_TRY_AGAIN_LATER) {
        AMCTRACE("AMEDIACODEC__INFO_TRY_AGAIN_LATER\n");
        // continue;
    } else if (output_buffer_index < 0) {
        SDL_LockMutex(opaque->any_input_mutex);
        SDL_CondWaitTimeout(opaque->any_input_cond, opaque->any_input_mutex, 1000);
        SDL_UnlockMutex(opaque->any_input_mutex);

        goto done;
    } else if (output_buffer_index >= 0) {
        /*spb modify*/
        if (ffp->is->mediacodec_fail)
        {
            SDL_AMediaCodec_releaseOutputBuffer(opaque->acodec, output_buffer_index, false);
            *got_frame = 0;
            return 0;
        }
        output_buffer = SDL_AMediaCodec_getOutputBuffer(opaque->acodec, output_buffer_index);
        
        if (output_buffer && convert_AMC_data_format != NULL)
        {   
            convert_AMC_data_format(dest_frame);
        } else {
            ffp->is->mediacodec_fail = true;
            av_log(NULL, AV_LOG_INFO, "spb_log SDL_AMediaCodec_getOutputBuffer output_buffer is NULL");
        }
        /*end*/
       
        ffp->stat.vdps = SDL_SpeedSamplerAdd(&opaque->sampler, FFP_SHOW_VDPS_MEDIACODEC, "vdps[MediaCodec]");

        if (dequeue_count)
            ++*dequeue_count;

#ifdef FFP_SHOW_AMC_VDPS
        {
            if (opaque->benchmark_start_time == 0) {
                opaque->benchmark_start_time   = SDL_GetTickHR();
            }
            opaque->benchmark_frame_count += 1;
            if (0 == (opaque->benchmark_frame_count % 240)) {
                Uint64 diff = SDL_GetTickHR() - opaque->benchmark_start_time;
                double per_frame_ms = ((double) diff) / opaque->benchmark_frame_count;
                double fps          = ((double) opaque->benchmark_frame_count) * 1000 / diff;
                ALOGE("%lf fps, %lf ms/frame, %"PRIu64" frames\n",
                      fps, per_frame_ms, opaque->benchmark_frame_count);
            }
        }
#endif
#ifdef FFP_AMC_DISABLE_OUTPUT
        if (!(bufferInfo.flags & AMEDIACODEC__BUFFER_FLAG_FAKE_FRAME)) {
            SDL_AMediaCodec_releaseOutputBuffer(opaque->acodec, output_buffer_index, false);   
        }
        goto done;
#endif

        if (opaque->n_buf_out) {
            AMC_Buf_Out *buf_out;

            if (opaque->off_buf_out < opaque->n_buf_out) {
                // ALOGD("filling buffer... %d", opaque->off_buf_out);
                buf_out = &opaque->amc_buf_out[opaque->off_buf_out++];
                buf_out->acodec_serial = SDL_AMediaCodec_getSerial(opaque->acodec);
                buf_out->port = output_buffer_index;
                buf_out->info = bufferInfo;
                buf_out->pts = pts_from_buffer_info(node, &bufferInfo);
                sort_amc_buf_out(opaque->amc_buf_out, opaque->off_buf_out);
            } else {
                double pts;

                pts = pts_from_buffer_info(node, &bufferInfo);
                if (opaque->last_queued_pts != AV_NOPTS_VALUE &&
                    pts < opaque->last_queued_pts) {
                    // FIXME: drop unordered picture to avoid dither
                    // ALOGE("early picture, drop!");
                    // SDL_AMediaCodec_releaseOutputBuffer(opaque->acodec, output_buffer_index, false);
                    // goto done;
                }
                /* already sorted */
                buf_out = &opaque->amc_buf_out[opaque->off_buf_out - 1];
                /* new picture is the most aged, send now */
                if (pts < buf_out->pts) {
                    ret = amc_fill_frame(node, frame, got_frame, output_buffer_index, SDL_AMediaCodec_getSerial(opaque->acodec), &bufferInfo);

                    opaque->last_queued_pts = pts;
                    // ALOGD("pts = %f", pts);
                } else {
                    int i;

                    /* find one to send */
                    for (i = opaque->off_buf_out - 1; i >= 0; i--) {
                        buf_out = &opaque->amc_buf_out[i];
                        if (pts > buf_out->pts) {
                            ret = amc_fill_frame(node, frame, got_frame, buf_out->port, buf_out->acodec_serial, &buf_out->info);
                            opaque->last_queued_pts = buf_out->pts;
                            // ALOGD("pts = %f", buf_out->pts);
                            /* replace for sort later */
                            buf_out->acodec_serial = SDL_AMediaCodec_getSerial(opaque->acodec);
                            buf_out->port = output_buffer_index;
                            buf_out->info = bufferInfo;
                            buf_out->pts = pts_from_buffer_info(node, &bufferInfo);
                            sort_amc_buf_out(opaque->amc_buf_out, opaque->n_buf_out);
                            break;
                        }
                    }
                    /* need to discard current buffer */
                    if (i < 0) {
                        // ALOGE("buffer too small, drop picture!");
                        if (!(bufferInfo.flags & AMEDIACODEC__BUFFER_FLAG_FAKE_FRAME)) {
                            SDL_AMediaCodec_releaseOutputBuffer(opaque->acodec, output_buffer_index, false);
                            goto done;
                        }
                    }
                }
            }
        } else {
            ret = amc_fill_frame(node, frame, got_frame, output_buffer_index, SDL_AMediaCodec_getSerial(opaque->acodec), &bufferInfo);
        }
    }

done:
    if (opaque->decoder->queue->abort_request)
        ret = -1;
    else
        ret = 0;
fail:
    return ret;
}

下一步还需要修改一个函数。函数修改如下:

static int func_fill_frame(SDL_VoutOverlay *overlay, const AVFrame *frame, AVFrame **dest_frame)
{
    assert(frame->format == IJK_AV_PIX_FMT__ANDROID_MEDIACODEC);

    SDL_VoutOverlay_Opaque *opaque = overlay->opaque;

    if (!check_object(overlay, __func__))
        return -1;

    if (opaque->buffer_proxy){
        av_log(NULL, AV_LOG_INFO, "_____SDL_VoutAndroid_releaseBufferProxyP\n");
        SDL_VoutAndroid_releaseBufferProxyP(opaque->vout, (SDL_AMediaCodecBufferProxy **)&opaque->buffer_proxy, false);
    }
    
    opaque->acodec       = SDL_VoutAndroid_peekAMediaCodec(opaque->vout);
    // TODO: ref-count buffer_proxy?
    opaque->buffer_proxy = (SDL_AMediaCodecBufferProxy *)frame->opaque;/*mediacodec 解码出来的数据*/
    // SDL_AMediaCodecBufferInfo buffer_info = opaque->buffer_proxy->buffer_info;
    // av_log(NULL, AV_LOG_INFO, "spb_log buffer_info->size : %d, buffer_info->presentationTimeUs : %ld, buffer_info->flags : %d, buffer_info->offset : %d\n", buffer_info.size, buffer_info.presentationTimeUs, buffer_info.flags, buffer_info.offset);
    overlay->opaque_class = &g_vout_overlay_amediacodec_class;
    overlay->format     = SDL_FCC__AMC;
    overlay->planes     = 1;
    overlay->pixels[0]  = NULL;
    overlay->pixels[1]  = NULL;
    overlay->pitches[0] = 0;
    overlay->pitches[1] = 0;
    overlay->is_private = 1;
    overlay->w = (int)frame->width;
    overlay->h = (int)frame->height;
    /*spb modify*/
    AVFrame *dst_frame = *dest_frame;
    overlay->pixels[0]=dst_frame->data[0];
    overlay->pixels[1]=dst_frame->data[1];
    overlay->pixels[2]=dst_frame->data[2];
    overlay->pitches[0]=dst_frame->linesize[0];
    overlay->pitches[1]=dst_frame->linesize[1];
    overlay->pitches[2]=dst_frame->linesize[2];
    SDL_VoutOverlayAMediaCodec_releaseFrame_l(overlay, NULL, false);
    /*end*/
    return 0;
}

我们渲染是通过overlay拿到数据的,所以我让overlay的指针也指向了数据。在这里按照你们的需求是否需要这么做,但是必须调用SDL_VoutOverlayAMediaCodec_releaseFrame_l(overlay, NULL, false);这个函数,释放掉mediacodec解码的数据。因为如果我们走的是自己的渲染没有走ijk的渲染这个函数永远也不会调用,就是mediacodec解码池中的数据一直在没有取出来,不只是内存泄漏,而且程序运行起来立马就会宕机。

下面说一下 要是mediacodec解码格式 FFmpeg中没有找到对应的格式 没办法转码,这时候去走软解码 这个过程是怎么实现的。找到mediacodec的初始化方法:

static IJKFF_Pipenode *func_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
{
    IJKFF_Pipeline_Opaque *opaque = pipeline->opaque;
    IJKFF_Pipenode        *node = NULL;

    if (ffp->mediacodec_all_videos || ffp->mediacodec_avc || ffp->mediacodec_hevc || ffp->mediacodec_mpeg2)
        node = ffpipenode_create_video_decoder_from_android_mediacodec(ffp, pipeline, opaque->weak_vout);
    if (!node) {
        node = ffpipenode_create_video_decoder_from_ffplay(ffp);
    }

    return node;
}

由此可以看到 如果ffpipenode_create_video_decoder_from_android_mediacodec返回的node为NULL 那么就会走软解

看下这个方法 核心的几句初始化:

node->func_destroy  = func_destroy;

node->func_run_sync = func_run_sync;

node->func_flush    = func_flush;

opaque->pipeline    = pipeline;

opaque->ffp        = ffp;

opaque->decoder    = &is->viddec;

opaque->weak_vout  = vout;

opaque->acodec_mutex                      = SDL_CreateMutex();

opaque->acodec_cond                      = SDL_CreateCond();

opaque->acodec_first_dequeue_output_mutex = SDL_CreateMutex();

opaque->acodec_first_dequeue_output_cond  = SDL_CreateCond();

opaque->any_input_mutex                  = SDL_CreateMutex();

opaque->any_input_cond                    = SDL_CreateCond();

opaque->input_aformat = SDL_AMediaFormatJava_createVideoFormat(env, opaque->mcc.mime_type, opaque->avctx->width, opaque->avctx->height);

SDL_AMediaFormat_setBuffer(opaque->input_aformat, "csd-0", opaque->avctx->extradata, opaque->avctx->extradata_size); //设置 sps pps数据

我们找到了一个我们需要更改的关键函数:reconfigure_codec_l

jsurface = ffpipeline_get_surface_as_global_ref(env, pipeline);

ret = reconfigure_codec_l(env, node, jsurface);

J4A_DeleteGlobalRef__p(env, &jsurface);

if (ret != 0)

goto fail;

fail:

ffpipenode_free_p(&node);

return NULL;

如果我们能在reconfigure_codec_l这个函数中获取到解码数据格式 并判断FFmpeg支不支持,不支持返回ret != 0,那么就会goto fail ,并return NULL。就会去走软解码。
我们看下这个函数,在这个函数中的关键语句就是 opaque->acodec = create_codec_l(env, node);创建了acodec
这时候 我们就可以获取解码数据格式 并判断FFmpeg是否支持。调用该方法:

ret = convert_data_format_init_sws(ijk_AMediaCodec_OutputFormat_Shine_FFMpegPixelFormat(opaque)); 

drain_output_buffer_l这个函数中 output_buffer_index == AMEDIACODEC__INFO_OUTPUT_FORMAT_CHANGED有这个判断 数据格式发生改变。所以我们在这里也要进行格式的判断 FFmpeg是否支持。 修改如下:

else if (output_buffer_index == AMEDIACODEC__INFO_OUTPUT_FORMAT_CHANGED) {
        ALOGI("AMEDIACODEC__INFO_OUTPUT_FORMAT_CHANGED\n");
        SDL_AMediaFormat_deleteP(&opaque->output_aformat);
        /*spb modify*/
        sws_freeContext(img_convert_ctx);
        img_convert_ctx = NULL;
        if (convert_data_format_init_sws(ijk_AMediaCodec_OutputFormat_Shine_FFMpegPixelFormat(opaque)) == -1)
            ffp->is->mediacodec_fail = true;
        /*end*/
        // continue;
    } 

如果这时候格式不支持 那么ffp->is->mediacodec_fail = true; mediacodec_fail是我在videostate结构体中添加的一个成员变量。

} else if (output_buffer_index >= 0) {
        /*spb modify*/
        if (ffp->is->mediacodec_fail)
        {
            SDL_AMediaCodec_releaseOutputBuffer(opaque->acodec, output_buffer_index, false);
            *got_frame = 0;
            return 0;
        }
        output_buffer = SDL_AMediaCodec_getOutputBuffer(opaque->acodec, output_buffer_index);
        
        if (output_buffer && convert_AMC_data_format != NULL)
        {   
            convert_AMC_data_format(dest_frame);
        } else {
            ffp->is->mediacodec_fail = true;
            av_log(NULL, AV_LOG_INFO, "spb_log SDL_AMediaCodec_getOutputBuffer output_buffer is NULL");
        }
        /*end*/
       
        ffp->stat.vdps = SDL_SpeedSamplerAdd(&opaque->sampler, FFP_SHOW_VDPS_MEDIACODEC, "vdps[MediaCodec]");

        if (dequeue_count)
            ++*dequeue_count;

我们在video_refresh_thread中使用这个变量如下:

static int video_refresh_thread(void *arg)
{
    FFPlayer *ffp = arg;
    VideoState *is = ffp->is;
    double remaining_time = 0.0;
    
    while (!is->abort_request) {
        
        if (is->mediacodec_fail && mediacodec_fail_callback)
            mediacodec_fail_callback();
            
        if (remaining_time > 0.0)
            av_usleep((int)(int64_t)(remaining_time * 1000000.0));
        
        remaining_time = REFRESH_RATE;
        if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
            video_refresh(ffp, &remaining_time);
    }

    return 0;
}

mediacodec_fail_callback()我们自己添加的一个回调函数,这个回调函数会调用ijk的stop,然后重新初始化ijk 并使用软解,注意mediacodec_fail_callback函数必须开启一个新的线程去做这些事情,因为mediacodec_fail_callback函数在video_refresh_thread线程中,如果不开启新的线程就stop,stop会结束video_refresh_thread这个线程,自己结束自己?必然宕机。

以上就是ijk 硬解码转格式的 核心代码了。
若有问题欢迎指正,后期会继续根据我们的项目继续优化。
此方法获取mediacodec解码数据 效率偏低 过段时间优化之后再将改动贴出来。

android优化方案:

1.解码优化:
目前实验改动使用 getOutputImage 获取到image 再获取buffer,也使用ImageReader通过它的回调获取image得到buffer,效率感觉差不多,都不是很高,可能ImageReader的使用存在问题,暂时没有查清,此问题需要查看Google源码,目前没有时间去看mediacodec的源码,继续优化的话 还是需要查清的。毕竟我不是做android的,我觉得在JNI中获取image,经过了Java层到C的数据拷贝,如果是这样确实会有效率问题,如果有android熟悉JNI开发的欢迎指点。 至于怎么使用 getOutputImage 怎么使用 ImageReader 需要的可以联系我,等有时间我会把教程步骤整理出来。

2.数据格式优化:
之前硬解码后数据格式为YUV420SP ,需要转成YUV420P,一直使用FFmpeg做的,FFmpeg使用的是CPU,这个过程可以放到Unity中去做,unity在GPU中做,效率显然更高,同时减轻CPU的压力。

3.渲染优化:
之前一直通过JNI把解码的数据 从C拷贝到Java虚拟机内存,通常android机1080P的视频 每帧YUV420数据大概是3MB左右,视频一秒30帧左右,拷贝效率尚可跟上。但是4K视频 每一帧数据量在10MB以上,视频如果一秒30帧,那么这个拷贝就需要每秒拷贝300—400MB,大部分手机都会面临效率问题,针对这个优化,在C中使用OpenGL获取到textureID,把ID传递到unity层去渲染,不用传递数据,避免数据拷贝。

这几步优化完成之后,感觉效率起飞了。

推荐阅读更多精彩内容