由于我们渲染使用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。
查阅资料为了想要得到我们需要的数据格式,我们可以通过getOutputBuffe
r(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。
看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层去渲染,不用传递数据,避免数据拷贝。
这几步优化完成之后,感觉效率起飞了。