硬解直显模式实现抓图功能

Posted Nipuream

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了硬解直显模式实现抓图功能相关的知识,希望对你有一定的参考价值。

一、概述

上篇文章提到了在android平台中自己创建和构造外部纹理提供给MediaCodec作为输入端接受解码后的数据,并通过onFrameAvailable()方法的回调来更新纹理,并通过opengl的来渲染和屏幕绑定缓存帧来实现。这样做的好处就是有效的减少cpu和gpu的交互,从而提高应用上的体验。但是如果要实现抓图功能,只是使用glReadPixels()方法需要从显存到内存的交换过程时间作为代价的,笔者亲测,抓一张4k高清图像,需要卡顿渲染线程2秒钟左右的时间,这是万万不能接受的;而且在使用glReadPixels方法过程中,发现这个方法只能获取当前窗口显示的区域,并非是原图。接下来,介绍一种利用FBO附着纹理的方式来实现抓图,来减少渲染线程在抓图过程中耗时。

二、实现

首先简单的来介绍FBO,它的全程是 frame buffer object,在它出现之前,opengl 渲染到纹理都使用pbuffer(屏幕外的缓存区域),但是它有一些方面的弊端,所以单独提供一种FBO的方式作为渲染到纹理的方式,它自身并未占用缓存区的内存,而是以附着的方式来绑定纹理。

那么就在聊聊减少渲染线程执行glReadPixels的工作量,这个毋庸置疑肯定是用另外一条线程共享eglcontext,来获取到当前渲染的线程,然后拷贝纹理,拷贝完了之后再通知渲染线程继续执行渲染,然后这条线程在使用FBO绑定这个纹理(确保获取到原图),然后执行glReadPixels方法下载图片。

下面提供一些代码参考:

render->makeCurrent();
render->drawFrame(false);
render->swap();

if(render->capture)
	pthread_mutex_lock(&render->getFrameCopier()->preview_lock);

	//set frame copier texture.
	copier->setTexture(textureId, capture_width, capture_height);
	pthread_mutex_lock(&render->getFrameCopier()->mLock);
	pthread_cond_signal(&render->getFrameCopier()->mCondition);
	pthread_mutex_unlock(&render->getFrameCopier()->mLock);

	pthread_cond_wait(&render->getFrameCopier()->preview_condition, &render->getFrameCopier()->preview_lock);
	render->capture = false;
	pthread_mutex_unlock(&render->getFrameCopier()->preview_lock);

最上面是常规渲染相关的代码封装,如果用户点击了抓图,则先向拷贝线程设置当前的纹理,然后通知拷贝线程去拷贝纹理,然后就在这一直等待拷贝线程拷贝纹理结束,再恢复渲染。(拷贝纹理是在gpu操作的,所以速度很快)。

void frame_copier::setTexture(int texture, int width, int height) 

    this->inputTexture = texture;
    this->width = width;
    this->height = height;

    //init output txtId.
    glGenTextures(1, getOutputTexture());
    glBindTexture(GL_TEXTURE_EXTERNAL_OES, *getOutputTexture());
    checkGlError("glBindTexture textureId");
    glTexParameterf(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    //线性过滤,使用距离当前渲染像素中心最近的4个纹素加权平均值
    glTexParameterf(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // S 方向的贴图模式,将纹理坐标限制在 0.0 1.0的范围之内,如果超出了,会边缘拉伸填充处理
    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);

    LOGI("<-----  frame_copier notify copy ----->  ");


//拷贝线程启动后所执行的函数地址
void* copyPixels(void *addr) 

    LOGI("=======  frame_copier create copy pixels thread. ==========");

    auto copier = reinterpret_cast<frame_copier *>(addr);
    if(copier == NULL)
        LOGI("frame_copier thread %d exit run.", getpid());
        return NULL;
    
    //初始化opengl
    initOpengl(copier);

    while (!copier->exit)

        //这里一直等待渲染线程的拷贝通知
        pthread_mutex_lock(&copier->mLock);
        LOGI("frame_copier wait render thread set Texture ...");
        pthread_cond_wait(&copier->mCondition, &copier->mLock);

        LOGI("frame_copier copy textureId %d pixels", copier->getInputTexture());
        if(copier->buf != NULL)
            delete [] copier->buf;
        
     
        makeCurrent(copier);
        //1. copy preview texture.
        copyWithCoords(copier);

        pthread_mutex_lock(&copier->preview_lock);
        pthread_cond_signal(&copier->preview_condition);
        pthread_mutex_unlock(&copier->preview_lock);

        //2. bind frame buffer copy pixels.
        fboCopy(copier);

        //3. copy pixels to Java.
        copyToJava(copier);
        //unlock.
        pthread_mutex_unlock(&copier->mLock);
    

    releaseOpengl(copier);
    LOGI("============== frame copier thread exit. ============");


在setTexture()函数里只是获取到渲染线程那边的参数再初始化输出纹理,然后渲染线程知道拷贝线程开始工作,这边首先会拷贝纹理,然后在通过fbo的方式拷贝像素,最后通知Java层,代码中以列举出步骤,接下来逐步贴出参考代码:

void copyWithCoords(frame_copier* copier)

    GLuint fbo;
    glGenFramebuffers(1, &fbo);
    glBindFramebuffer(GL_FRAMEBUFFER, fbo);

    glBindTexture(GL_TEXTURE_EXTERNAL_OES, *copier->getOutputTexture());
    checkGlError("glBindTexture");

    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_EXTERNAL_OES, *copier->getOutputTexture(), 0);
    checkGlError("glFramebufferTexture2D");

    glUseProgram(copier->mGLProgId);

    glVertexAttribPointer(copier->mGLVertexCoords, 2, GL_FLOAT, GL_FALSE, 0 , mTriangleXYZData);
    glEnableVertexAttribArray(copier->mGLVertexCoords);
    glVertexAttribPointer(copier->mGLTextureCoords, 2, GL_FLOAT, GL_FALSE, 0, mTriangleUVData);
    glEnableVertexAttribArray(copier->mGLTextureCoords);

    //binding input texture.
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_EXTERNAL_OES, *copier->getInputTexture());
    glUniform1i(copier->mGLUniformTexture, 0);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    glDisableVertexAttribArray(copier->mGLVertexCoords);
    glDisableVertexAttribArray(copier->mGLTextureCoords);
    glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_EXTERNAL_OES, 0, 0);
    glDeleteFramebuffers(1, &fbo);

拷贝纹理之前通过glCopyTexSubImage2D方法来拷贝,可是最后读取纹理像素的时候,FBO附着失败了。(有知道的同学可以交流下),之后用了着色器来拷贝纹理,这种效率还是非常高的。这步执行完之后,然后立马通知渲染线程继续执行渲染工作,因为我们已经拿到了纹理了,我们拷贝线程可以接下来执行读取像素的工作:

void fboCopy(frame_copier* copier)

    if(DEBUG) LOGI("frame_copier capture_width : %d, capture height : %d", copier->width, copier->height);
    copier->buf = new unsigned char[copier->width * copier->height * 4];
    memset(copier->buf, 0, copier->width * copier->height * 4);

    GLuint FBO;
    glGenFramebuffers(1, &FBO);
    checkGlError("frame_copier glGenFrameBuffers");

    glBindFramebuffer(GL_FRAMEBUFFER, FBO);
    checkGlError("frame_copier bind framebuffer");

    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,GL_TEXTURE_EXTERNAL_OES, *copier->getInputTexture(), 0);
    checkGlError("frame_copier glFramebufferTexture2D");

    glActiveTexture(GL_TEXTURE0);
    checkGlError("frame_copier glActiveTexture");
    glBindTexture(GL_TEXTURE_EXTERNAL_OES, *copier->getInputTexture());
    checkGlError("frame_copier bindTexture");

    int status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if(status == GL_FRAMEBUFFER_COMPLETE)
        glReadPixels(0,0,(GLsizei)copier->width, (GLsizei)copier->height, GL_RGBA, GL_UNSIGNED_BYTE, (GLvoid *)copier->buf);
        checkGlError("frame_copier glReadPixels");
     else 
        if(DEBUG) LOGI("frame_copier framebuffer status : %d", status);
    

    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_EXTERNAL_OES, 0,0);
    checkGlError("frame_copier glFrameBufferTexture2D");

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    checkGlError("frame_copier glBindFrameBuffer");

    glDeleteFramebuffers(1, &FBO);
    checkGlError("frame_copier glDeleteFrameBuffers");

最后通知将拷贝出来的buffer,通过jni的方式通知Java层:

void copyToJava(frame_copier* copier)

    JNIEnv *env;
    if(sVm->AttachCurrentThread(&env, NULL) != JNI_OK)
        LOGI("frame_copier attach vm failed.");
        return ;
    

    jclass clazz = env->GetObjectClass(copier->obj);
    jmethodID copyPixels = env->GetMethodID(clazz, "copyPixelsFromNative", "([B)V");

    jbyte *by = (jbyte*)copier->buf;
    int size = copier->width * copier->height * 4;

    jbyteArray jbyteArray = env->NewByteArray(size);
    env->SetByteArrayRegion(jbyteArray, 0, size, by);

    env->CallVoidMethod(copier->obj, copyPixels, jbyteArray);
    if(DEBUG) LOGI("frame_copier copy pixels to java.");

三、总结

本文主要介绍了,通过另起一条拷贝线程来通过着色器来拷贝纹理,和通过FBO绑定纹理的方式来获取位图的buffer。这种方案有效的解决了获取大图导致渲染卡顿,glReadPixels不能读取原始分辨率位图的问题。

以上是关于硬解直显模式实现抓图功能的主要内容,如果未能解决你的问题,请参考以下文章

硬解直显模式实现抓图功能

硬解直显模式实现抓图功能

硬解直显模式实现抓图功能

硬解直显模式实现抓图功能

硬解直显模式实现抓图功能

硬解直显模式实现抓图功能