上面这个完全不值得提倡,视频数据从native层传到jni层很耗时间.
开发基于ffmpeg的播放器时,可以使用ffmpeg的各种软解码器,也可以使用android带的OMXCodec解码器,OMXCodec解码器是对OMX的一个封装.其中ffmpeg解码器主要输出YUV420P的帧,而OMXCodec解码器输出的格式就多样化,因具体的平台不同而不同.
使用OMXCodec解码器解出来的视频,可以让它自己输出,只要在打开解码器的时候给它传个ANativeWindow:
- sp<MediaSource> OMXCodec::Create(
- const sp<IOMX> &omx,
- const sp<MetaData> &meta, bool createEncoder,
- const sp<MediaSource> &source,
- const char *matchComponentName,
- uint32_t flags,
- <strong>const sp<ANativeWindow> &nativeWindow)
- g_ANativeWindow->queueBuffer (g_ANativeWindow.get(),mbuf->graphicBuffer()->getNativeBuffer());
- ...
- mbuf->meta_data()->setInt32(kKeyRendered,1);
- ...
- mbuf->release();//这个是告诉OMXCodec
值得说明的是,在创建OMXCodec的时候,也可以传入kKeyColorFormat来指定成想要的格式,但是不幸的是,有时候并不会支持你想要的,比如有的手机就不支持HAL_PIXEL_FORMAT_YV12,而是使用了一种内部格式,纠结吧,所以还是让它自己输出比较好.
在采用ffmpeg解码器时,目前我所知道的,输出YUV420P有两种方法,一种是通过Lock ANativeWindow写入YUV数据,一种是通过opengles在片断着色器中将YUV数据通过公式转换成RGB输出.
参考AwesomePlayer.cpp,会发现有个SoftwareRenderer的东东,这个类可以输出HAL_PIXEL_FORMAT_YV12, YUV420P->YV12的转换其实很简单,就是UV分量的存储位置不一样,拷贝时变换下就可以了.这个是Lock ANativeWindow的方法.里面还有一个AwesomeNativeWindowRenderer,其实就是上面提到的让OMXCodec自己去输出视频的方法.
我在实践中发现,有些手机的SoftwareRenderer没有处理好,特别是刷第三方ROM的时候,会花屏,所以需要自己手动从SoftwareRenderer.cpp中提取代码,其实就是操作ANativeWindow的一些函数:
- native_window_api_connect(gDirectRendererContext.anativeWindow.get(),NATIVE_WINDOW_API_CPU);
- native_window_set_usage(gDirectRendererContext.anativeWindow.get(),GraphicBuffer::USAGE_SW_WRITE_OFTEN|GraphicBuffer::USAGE_HW_TEXTURE);
- native_window_set_buffers_geometry(gDirectRendererContext.anativeWindow.get(),w,h,android_fmt);
- native_window_set_crop(gDirectRendererContext.anativeWindow.get(),&android_crop);
- native_window_set_scaling_mode(gDirectRendererContext.anativeWindow.get(),NATIVE_WINDOW_SCALING_MODE_SCALE_TO_WINDOW);
- ANativeWindow_lock(gDirectRendererContext.anativeWindow.get(), &buffer, NULL);
- //写数据
- ANativeWindow_unlockAndPost(gDirectRendererContext.anativeWindow.get());
Lock AnativeWindow方法优点是底层有平台实现,非常快,缺点是,在lockBuffer之后,我们可以拿到y分量的行字节数,即stride,但是没有办法获取到uv分量的stride,跟据一般情况下应该是y_stride/2然后按16字节对齐,但是有些厂商不按常理出牌,这个值不一定是这样的,导致结果就是花屏或者内存Abort,所以得适配手机,麻烦!
再来说说使用opengles将YUV转换成RGB输出的方法,这种方法很早有就人使用了,最早的时候就使用软件将YUV转换成RGB输出,但是效率太低了,于是大神们将眼光投向了opengles的可编程片段着色器,将YUV的转换放到着色器去做,硬件处理是非常快的,这样就大大提高了效率,具体的做法是:初始化EGL(参考gl2_yuvtex.cpp)->创建着色器程序并编译连接->创建并上传纹理->画三角形.下面是顶点和纹理坐标,着色器代码:
- static float squareV[]={
- -1.0f,-1.0f,
- 1.0f,-1.0f,
- 1.0f,1.0f,
- -1.0f,1.0f
- };
- static float coordV[]={
- 0.0f,1.0f,
- 1.0f,1.0f,
- 1.0f,0.0f,
- 0.0f,0.0f
- };
- static char fragmentShaderCode_yuv420[]=
- "precision mediump float;"
- "uniform sampler2D tex_y;"
- "uniform sampler2D tex_u;"
- "uniform sampler2D tex_v;"
- "varying vec2 tc;"
- "void main(){"
- " mediump vec3 yuv;"
- " mediump vec3 rgb;"
- " yuv.x=texture2D(tex_y,tc).r-0.0625;"
- " yuv.y=texture2D(tex_u,tc).r-0.5;"
- " yuv.z=texture2D(tex_v,tc).r-0.5;"
- " rgb.r=1.164*yuv.x + 1.596*yuv.z;"
- " rgb.g=1.164*yuv.x - 0.813*yuv.z - 0.392*yuv.y;"
- " rgb.b=1.164*yuv.x + 2.017*yuv.y;"
- " gl_FragColor=vec4(rgb,1);"
- "}";
- static char vertexShaderCode[]=
- "attribute vec4 vertexPosition;"
- "attribute vec2 vertexCoordinate;"
- "varying vec2 tc;"
- "void main(){"
- " gl_Position=vertexPosition;"
- " tc=vertexCoordinate;"
- "}";
要注意的是,EGLContex和线程相关,是opengles的状态机.使用这种方法要注意,在有些老的机器上,使用glTexImage2D或glTexSubImage2D更新纹理时非常慢,一帧720x576的图片可达80ms,这样的话,就不要用这种方法渲染了,除非你可以舍弃清晰度,在新的硬件上,一般都能在几个ms内.
使用opengles来渲染,还有其他的方法,就是使用OES扩展,在安卓源码里有例子gl2_yuv.cpp,可以参考使用,使用glEGLImageTargetTexture2DOES上传纹理没有问题,一般都非常快,但是兼容性不好,原因和使用nativewindow直接渲染是相同的.