硬解直显模式实现抓图功能
Posted Nipuream
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了硬解直显模式实现抓图功能相关的知识,希望对你有一定的参考价值。
一、综述
之前做车载行车记录仪的时候,从摄像头拿到yuv数据经过shader脚本转化为rgb数据,再转化为纹理进行渲染。现在做的这个项目数据源主要是流媒体进行解封装获取到的压缩数据,现在android 的MediaCodec是允许surface作为输入的,结合SurfaceTexture进行updateTexImage(),然后读取到外部纹理,一方面可满足抓图功能的需求,另外也可以通过egl直接交换缓冲区去显示。这只是我们在性能和功能需求实现成本方面一个权衡,因为从gpu拷贝数据到cpu,然后在到虚拟机处理保存还是有一定的时间成本的,特别是4k的码流。如果是录像,这样方式肯定是不可取的。下面来介绍下具体实现。
二、实现
拿到流媒体数据解封装后,肯定是需要配置我们的硬解码器的,这个虽然是用到我们原生的接口MediaCodec,但是具体的实现都是厂商实现的,例如rk,qcom等。这边贴个简单的实现,具体的见demo。
extractor = MediaExtractor()
extractor!!.setDataSource(inputFile.toString())
val trackIndex = selectTrack(extractor!!)
if(trackIndex < 0){
throw RuntimeException("No video track found in : $inputFile")
}
extractor!!.selectTrack(trackIndex)
val format = extractor!!.getTrackFormat(trackIndex)
val mime = format.getString(MediaFormat.KEY_MIME)
decoder = MediaCodec.createDecoderByType(mime)
//这里配置surface作为输出
decoder!!.configure(format, render.surface, null, 0)
decoder!!.start()
注意这里的surface并非用于我们直接用于显示的SurfaceView中的surface,而是我们自己生成的,其实绑定的纹理是我们在c++层创建的。
/**
* 创建surface surfacetexture.
*/
private void setup(){
int texture = GetOfflineTexture();
Log.i(TAG,"texture : "+ texture);
surfaceTexture = new SurfaceTexture(texture);
surfaceTexture.setOnFrameAvailableListener(this);
mSurface = new Surface(surfaceTexture);
}
再来看下c++层,纹理的配置
//创建纹理
GLuint textures[1] = {0};
glGenTextures(1,textures);
textureId = textures[0];
if(DEBUG) LOGI("gen texture successful. texture : %d", textureId);
//绑定纹理
glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureId);
checkGlError("glBindTexture textureId");
//设置过滤器
glTexParameterf(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameterf(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
这样配置后,只要解码器start之后,解码之后的数据会渲染到我们生成的surface上,然后通知到我们的Java层回调上
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
Log.i(TAG,"new frame available.");
synchronized (mFrameSyncObject){
if(mFrameAvailable){
Log.w(TAG,"mFrameAvailable already set, frame could be dropped");
}
mFrameAvailable = true;
mFrameSyncObject.notifyAll();
}
}
然后我们紧接着调用SurfaceTexture去更新纹理:
public void awaitNewImage(){
final int TIMEOUT_MS = 2000;
synchronized (mFrameSyncObject){
while (!mFrameAvailable){
try {
mFrameSyncObject.wait(TIMEOUT_MS);
if(!mFrameAvailable){
Log.w(TAG,"frame wait time out.");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mFrameAvailable = false;
}
//更新纹理
surfaceTexture.updateTexImage();
}
更新纹理之后,我们就可以进行渲染和截图的工作了。
//JAVA层
render.awaitNewImage() //渲染线程等待更新纹理
render.updateMatrix() //更新矩阵
render.drawImage(false) //绘制
//jni层
jclass cls1 = env->GetObjectClass(thiz);
jfieldID fid1 = env->GetFieldID(cls1, "renderHandle", "J");
auto *render = reinterpret_cast<KeyBoardRender*>(env->GetLongField(thiz, fid1));
if(render == NULL){
if(DEBUG) LOGI("render is null..");
return ;
}
render->makeCurrent(); //绑定窗口
render->drawFrame(false); //渲染
//如果需要抓图,则抓图
if(render->capture){
capture_serialize(env, thiz);
render->capture = false;
}
render->swap(); //交换窗口缓冲区
//native层
//1.... 渲染
void
KeyBoardRender::drawFrame(int invert) {
checkGlError("onDraw frame start.");
//获取转换矩阵, 已由JNI层完成, Done.
if(invert){
mSTMatrix[5] = -mSTMatrix[5];
mSTMatrix[13] = 1.0f - mSTMatrix[13];
}
glClearColor(0.0f, 1.0f, 0.0f,1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(program);
checkGlError("glUseProgram");
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureId);
//顶点坐标
glVertexAttribPointer(maPositionHandle, 3, GL_FLOAT, false, 12, mTriangleXYZData);
checkGlError("glVertexAttribPointer maPosition");
glEnableVertexAttribArray(maPositionHandle);
checkGlError("glEnableVertexAttribArray maPositionHandle");
//纹理坐标
glVertexAttribPointer(maTextureHandle, 2, GL_FLOAT, false, 8,mTriangleUVData);
checkGlError("glVertexAttribPointer maTextureHandle");
glEnableVertexAttribArray(maTextureHandle);
checkGlError("glEnableVertexAttribArray maTextureHandle");
matrixSetIdentityM(mMVPMatrix);
//将矩阵传入着色器
glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix);
glUniformMatrix4fv(muSTMatrixHandle, 1, false, mSTMatrix);
//绘制
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
checkGlError("glDrawArrays");
//解绑
glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0);
}
//2........ 抓图
void capture_serialize(JNIEnv* env, jobject thiz){
jclass cls1 = env->GetObjectClass(thiz);
jfieldID fid1 = env->GetFieldID(cls1, "renderHandle", "J");
auto *render = reinterpret_cast<KeyBoardRender*>(env->GetLongField(thiz, fid1));
if(render == NULL){
if(DEBUG) LOGI("render is null..");
return ;
}
//copy texture pixels.
render->snapCapture();
jclass clazz = env->GetObjectClass(thiz);
jmethodID copyPixels = env->GetMethodID(clazz, "copyPixelsFromNative", "([B)V");
jbyte *by = (jbyte*)render->getAddr();
int size = render->getHeight() * render->getWidth() * 4;
jbyteArray jbyteArray = env->NewByteArray(size);
env->SetByteArrayRegion(jbyteArray, 0, size, by);
//将像素传递给java层处理
env->CallVoidMethod(thiz, copyPixels, jbyteArray);
if(DEBUG) LOGI("copy pixels to java.");
}
//3. java层处理:
public void copyPixelsFromNative(byte[] pixels){
mPixelBuf.rewind();
mPixelBuf.put(pixels);
File outputFile = new File(Environment.getExternalStorageDirectory(), "frame.png");
BufferedOutputStream bos = null;
try{
bos = new BufferedOutputStream(new FileOutputStream(outputFile));
Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mPixelBuf.rewind();
bmp.copyPixelsFromBuffer(mPixelBuf);
bmp.compress(Bitmap.CompressFormat.PNG, 90, bos);
bmp.recycle();
}catch (Exception e){
e.printStackTrace();
} finally {
if(bos != null) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
三、总结
以上只是我对工作的一个总结,笔者也写了个demo在github,有同样需求可参考。---->传送门
上面也说道了,这种方式只适合截图的功能,如果录制视频呢?肯定是不合适的,这样显然消耗了渲染线程的工作时间,那么何解?一般的有两种方式,一种是直接保存压缩过的数据,这种比较简单,我们也是这么实现的;如果你说 我非要保存解码后的数据呢?也可以实现,只不过需要创建一条新的线程共享EGLContext来实现,具体的实现后面有时间了在更新博文,敬请期待!
以上是关于硬解直显模式实现抓图功能的主要内容,如果未能解决你的问题,请参考以下文章