Android中使用FFmpeg和OpenGL ES渲染视频

4
我正在尝试通过NDK渲染视频,以添加一些在SDK中不支持的功能。我正在使用FFmpeg解码视频,并可以通过NDK编译它,并使用这个作为起点。我已经修改了该示例,不再使用glDrawTexiOES来绘制纹理,而是设置了一些顶点,并在其上渲染纹理(OpenGL ES方式渲染四边形)。
下面是我的渲染代码,但创建glTexImage2D很慢。我想知道是否有任何方法可以加快此过程或使其看起来更快,例如尝试在后台设置一些纹理并渲染预先设置的纹理。或者是否有其他更快速地将视频帧绘制到屏幕上的Android方法?目前,我只能获得约12fps。
glClear(GL_COLOR_BUFFER_BIT);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glBindTexture(GL_TEXTURE_2D, textureConverted);

//this is slow
glTexImage2D(GL_TEXTURE_2D, /* target */
0, /* level */
GL_RGBA, /* internal format */
textureWidth, /* width */
textureHeight, /* height */
0, /* border */
GL_RGBA, /* format */
GL_UNSIGNED_BYTE,/* type */
pFrameConverted->data[0]);

glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glTexCoordPointer(2, GL_FLOAT, 0, texCoords);
glVertexPointer(3, GL_FLOAT, 0, vertices);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, indices);
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);

编辑 我修改了我的代码,只初始化一个gltextImage2D,并使用glSubTexImage2D进行修改,但对帧率并没有太大的改善。

然后我修改了代码,用NDK修改了一个本地Bitmap对象。通过这种方法,我有一个后台线程运行,处理下一帧并填充本地端的位图对象。我认为这很有潜力,但需要加快将FFmpeg的AVFrame对象转换为本地位图的速度。以下是我目前用于转换的简单暴力方法。是否有任何方法可以增加速度或优化此转换?

static void fill_bitmap(AndroidBitmapInfo*  info, void *pixels, AVFrame *pFrame)
{
uint8_t *frameLine;

int  yy;
for (yy = 0; yy < info->height; yy++) {
    uint8_t*  line = (uint8_t*)pixels;
    frameLine = (uint8_t *)pFrame->data[0] + (yy * pFrame->linesize[0]);

    int xx;
    for (xx = 0; xx < info->width; xx++) {
        int out_offset = xx * 4;
        int in_offset = xx * 3;

        line[out_offset] = frameLine[in_offset];
        line[out_offset+1] = frameLine[in_offset+1];
        line[out_offset+2] = frameLine[in_offset+2];
        line[out_offset+3] = 0;
    }
    pixels = (char*)pixels + info->stride;
}
}

更新:我获得了更好的帧速率,但是当使用上面的fill_bitmap方法将ffmpeg的帧输出转换为图像时,该图像是黑白的,并且我得到了图像并排重复的副本。 - broschb
4个回答

6

是的,纹理(以及缓冲区、着色器和帧缓冲区)的创建速度很慢。

这就是为什么你应该只创建一次纹理。创建后,您可以通过调用 glSubTexImage2D 来修改其数据。

为了使上传纹理数据更快 - 创建两个纹理。当您使用一个纹理来显示时,将纹理数据从 ffmpeg 上传到第二个纹理。当您显示第二个纹理时,将数据上传到第一个纹理。然后从头开始重复。

我认为它仍然不会非常快。您可以尝试使用 jnigraphics 库,该库允许从 NDK 访问 Bitmap 对象像素。之后 - 您只需在 java 端屏幕上显示此 Bitmap。


感谢您的建议,请查看我对上面问题的更新。 - broschb
位图方法让我得到了接近所需帧率的结果。现在的问题是,当使用我原来提出的方法将 mmpeg 的输出帧转换为本机位图时,我得到了一张黑白图像。你有什么想法吗? - broschb
我不熟悉ffmpeg,但是也许您的像素格式不同。ffmpeg是否解码为RGB?此外,请检查Bitmap是否使用正确的像素格式-ARGB8创建。 - Mārtiņš Možeiko
没错,就是这个问题,我在尝试加载像素之前没有将原生格式转换为ARGB8。 - broschb

1
一些小的加入可以解决您的问题,首先使用swscale将您的AVFrame转换为RGB,然后直接将其应用于您的纹理即可,如下所示:
AVPicture *pFrameConverted;
struct SwsContext img_convert_ctx;

void init(){
    pFrameConverted=(AVPicture *)avcodec_alloc_frame();
    avpicture_alloc(pFrameConverted, AV_PIX_FMT_RGB565, videoWidth, videoHeight);
    img_convert_ctx = sws_getCachedContext(&img_convert_ctx, 
                    videoWidth, 
                    videoHeight,
                    pCodecCtx->pix_fmt,
                    videoWidth,
                    videoHeight,
                    AV_PIX_FMT_RGB565,
                    SWS_FAST_BILINEAR, 
                    NULL, NULL, NULL );
    ff_get_unscaled_swscale(img_convert_ctx);
}

void render(AVFrame* pFrame){
    sws_scale(img_convert_ctx, (uint8_t const * const *)pFrame->data, pFrame->linesize, 0, pFrame->height, pFrameConverted->data, pFrameConverted->lineSize);
    glClear(GL_COLOR_BUFFER_BIT);
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, videoWidth, videoHeight, GL_RGB, GL_UNSIGNED_BYTE, pFrameConverted->data[0]);
    glDrawTexiOES(0, 0, 0, videoWidth, videoHeight);
}

1

是的,你可以优化这段代码:

static void fill_bitmap(AndroidBitmapInfo*  info, void *pixels, AVFrame *pFrame)
{
uint8_t *frameLine;

int  yy;
for (yy = 0; yy < info->height; yy++)
 {
    uint8_t*  line = (uint8_t*)pixels;
    frameLine = (uint8_t *)pFrame->data[0] + (yy * pFrame->linesize[0]);

    int xx;
    for (xx = 0; xx < info->width; xx++) {
        int out_offset = xx * 4;
        int in_offset = xx * 3;

        line[out_offset] = frameLine[in_offset];
        line[out_offset+1] = frameLine[in_offset+1];
        line[out_offset+2] = frameLine[in_offset+2];
        line[out_offset+3] = 0;
    }
    pixels = (char*)pixels + info->stride;
}
}

应该是这样的:

static void fill_bitmap(AndroidBitmapInfo*  info, void *pixels, AVFrame *pFrame)
{
uint8_t *frameLine = (uint8_t *)pFrame->data[0];

int  yy;
for (yy = 0; yy < info->height; yy++)
 {
    uint8_t*  line = (uint8_t*)pixels;

    int xx;

    int out_offset = 0;
    int in_offset = 0;

    for (xx = 0; xx < info->width; xx++) {
        int out_offset += 4;
        int in_offset += 3;

        line[out_offset] = frameLine[in_offset];
        line[out_offset+1] = frameLine[in_offset+1];
        line[out_offset+2] = frameLine[in_offset+2];
        line[out_offset+3] = 0;
    }
    pixels = (char*)pixels + info->stride;

    frameLine += pFrame->linesize[0];
}
}

那会为你节省一些计算周期。


0

噢,也许你可以使用jnigraphics,就像https://github.com/havlenapetr/FFMpeg/commits/debug中所示的那样。 但是,如果在解码帧后获取yuv数据时,你需要将其转换为RGB555,这会非常慢。使用Android的MediaPlayer是个好主意。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接