이전 포스트에서는 카메라에서 담고 있는 프레임을 OpenGL로 그린 후 GLSurfaceView로 그려주는 작업을 했었다. 지금부터는 그려진 이미지를 비디오 파일로 만드는 작업에 대해서 분석해보고자 한다.

 

3. 미리보기 영상 인코딩하기 

 

MediaCodec을 사용한 비디오 인코딩 작업도 Renderer와 동일하게 OpenGL을 이용한 그리기 작업이 필요하다. 전반적인 구현 아이디어는 비디오 녹화용 EGL Context를 선언한 후 Renderer 클래스로부터 현재 촬영 중인 카메라의 이미지를 받아와 OpenGL로 다시 그려주고 Media Codec에서 받을 수 있는 Surface 형태로 보내는 것이다.

 

3.1 비디오 인코딩용 EGL Context 선언 

 

비디오 녹화 작업도 OpenGL 작업이 필요하므로 OpenGL 작업용 EGLContext를 만들어준다. 이때 Renderer로부터 카메라 촬영 이미지를 받아오기 위해 EGL 초기화 작업에 공유 EGL Context 정보(shared_context)를 추가한다. 

fun setVideoEncoder(videoEncoder: MediaVideoEncoder?) {
    this.videoEncoder = videoEncoder

    videoEncoder?.setEglContext(EGL14.eglGetCurrentContext(), textureId)
}

private fun createContext(shared_context: EGLContext?): EGLContext {
    val attrib_list = intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE)
    val context =
        EGL14.eglCreateContext(mEglDisplay, mEglConfig, shared_context, attrib_list, 0)
    checkEglError("eglCreateContext")
    return context
}

 

3.2 Renderer로부터 카메라 이미지 받아오기 

 

Renderer로부터 새로운 프레임이 발생했다는 콜백을 받으면 VideoEncoder는 카메라로부터 이미지를 받아와서 새롭게 그려주게 된다. 카메라 이미지는 Renderer 클래스 내의 texture에 있으며 고유한 texture id를 EGL내에서 bind 해서 받아 올 수 있게 된다. VideoEncoder 클래스에 해당 textureId를 전달해서 VideoEncoder의 EGLDisplay에 그려준다.

fun draw(tex_id: Int, tex_matrix: FloatArray?) {
    GLES20.glUseProgram(hProgram)
    if (tex_matrix != null) GLES20.glUniformMatrix4fv(muTexMatrixLoc, 1, false, tex_matrix, 0)
    GLES20.glUniformMatrix4fv(muMVPMatrixLoc, 1, false, mMvpMatrix, 0)
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, tex_id)
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, VERTEX_NUM)
    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0)
    GLES20.glUseProgram(0)
}

 

3.3 MediaCodec으로 이미지 버퍼 전달 

 

인코딩을 위해 새롭게 그린 이미지를 MediaCodec에서 만든 Surface에 버퍼로 전달한다. OpenGL 에서 제공하는 eglSwapBuffers 함수를 사용하면 MediaCodec이 받을 수 있는 surface에 전달이 가능하다.

private fun swap(surface: EGLSurface?): Int {
    if (!EGL14.eglSwapBuffers(mEglDisplay, surface)) {
        val err = EGL14.eglGetError()
        if (DEBUG) Log.w(TAG, "swap:err=$err")
        return err
    }
    return EGL14.EGL_SUCCESS
}

 

3.4 전달 받은 정보를 인코딩 

 

MediaCodec 고유 함수를 이용해 전달받은 정보를 인코딩한다. MediaCodec 관련 코드는 생략한다. 

 

4. 마치며 

 

포스트에선 전반적인 구현 아이디어만 다루었기 때문에 보기에는 쉽지만 실제 사용된 코드는 꽤 복잡했다. google에서 짜둔 클래스 간의 상속과 인터페이스 관계를 따라가는 게 생각보다 시간이 걸렸고 아직도 생소한 OpenGL 클래스의 역할과 내부 코드를 알지 못해 문서를 찾아가느라 어려웠다.

 

그래도 고생하면서 생소했던 카메라와 OpenGL 관련 지식을 배운게 개발자로서 큰 소득이다. 스노나 틱톡의 카메라 효과 코드를 보진 못했지만 아마 위 구현 방식과 크게 차이가 나지 않을 것 같다. 여기에 OpenGL 코드를 더 확장시키면 나도 촬영 중인 화면에 여기에 필터를 변경하고 스티커도 추가해볼 수 있겠다. 현재 구현된 코드를 한층 더 업그레이드시켜봐야겠다.

 

오디오까지 같이 녹화하고 싶다면 여기 깃허브 코드를 참조하면 좋다.

 

https://github.com/saki4510t/AudioVideoRecordingSample

0. MediaRecorder의 한계

 

구글이 운영 중인 안드로이드 카메라 Sample 코드 저장소에선 Camera2Camera 2 라이브러리를 이용해 사진을 찍거나 비디오 녹화를 할 수 있는 예제가 있다. Camera 2 Video 프로젝트의 비디오 녹화 예제 코드의 경우 카메라에서 출력되는 프레임을 MediaRecorder라는 클래스를 이용해서 녹화할 수 있도록 했는데 이 방식은 후면 녹화의 경우에는 별로 문제가 없으나 전면 카메라를 이용하는 경우 미리보기에서 나온 영상이 그대로 저장되지 않고 좌우가 반전돼서 나오게 되는 문제가 있다. 대부분 카메라 어플에서 제공하는 옵션인 보이는 대로 저장 하기 기능을 사용할 수 없는 큰 문제점(?) 이 존재한다

 

보이는대로 저장 옵션을 사용할 수 없다. 그래서 촬영한후 저장한 내 모습이 아주 어색하게 저장된다

 

전면 카메라에 출력된 내 모습 그대로 저장하기 위해선 MediaRecorder 클래스 대신 대신 카메라에서 출력된 프레임을 OpenGL 그래픽 라이브러리를 이용해 렌더링 한 후 화면에 출력된 프레임을 MediaCodec을 이용해 직접 비디오 파일을 만드는 과정이 필요하다. MediaRecorder를 사용하는 코드가 워낙 간편했거니와 그래픽 라이브러리와 MediaCodec을 사용하는 작업은 대부분 개발자들에게도 생소한 OpenGL 지식이 필요하기 때문에 다소 까다롭다. 하지만 이것 말고는 전면 카메라를 반전시킬 방법은 없기 때문에 어렵더라도 직접 구현해봤다.

 

1. 오픈소스

 

다행히 구글의 비공식 저장소인 grafika에서 이미 구현한 코드가 있었다. 카메라에서 촬영중인 프레임을 안드로이드 그래픽 라이브러리에 렌더링 한 후 화면에 출력된 이미지를 MediaCodec을 이용해 MP4의 파일로 만드는, 앞서 의도한 방식을 그대로 구현한 코드였다. 그런데 3-4년 전에 작성한 코드라서 현재는 Deprecated 된 Camera 라이브러리를 사용 중이어서(현재는 Camera 2를 주로 쓰고 CameraX 알파 버전이 개발 중이다) grafika 코드를 분석하고 여기서 동작하는 모듈을 Camera 2랑도 연동이 될 수 있도록 하는 방향으로 개발했다.

 

2. Camera2와의 GLSurfaceView 연동 과정 

 

camera2 구조

grafika에서 이미 구현한 부분은 Surface, Renderer, GLSurface 간의 연동 과정이고 내가 추가적으로 넣은 부분은 Camera 2와 Renderer에서 만든 Surface를 연동한 부분뿐이다. 연동 과정과 각 클래스의 역할을 분석한 내용을 단계별로 정리했다.

 

2.1 Renderer 초기화 작업

 

GLSurfaceView는 OpenGL로 그려진 이미지를 안드로이드 UI에 노출 시켜줄 수 있는 클래스다. Renderer 클래스는 GLSurfaceView에 표시할 이미지를 OpenGL로 그리는 역할을 한다. Renderer 클래스가 GLSurfaceView의 그리는 역할을 담당할 수 있도록 setRenderer  함수를 이용해 두 클래스를 연결시켜준다. 이러면 Renderer에서 그린 OpenGL 이미지가 GLSurfaceView에 표시된다.

 

class CameraSurfaceRenderer(private val glSurfaceView: GLSurfaceView) : GLSurfaceView.Renderer,

    init {
        Matrix.setIdentityM(mvpMatrix, 0)
        glSurfaceView.setEGLContextClientVersion(2)
        glSurfaceView.setRenderer(this)
        glSurfaceView.renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY
    }

 

연결 작업후 OpenGL을 이용해 그릴 수 있는 공간을 선언하는 초기 작업이 필요한데 이 작업은 Surface가 생성된 이후에 불리는 onSurfaceCreated 콜백 함수에서 담당한다. 이 함수 내에서는 OpenGL Texture를 생성하는 초기화 외부로부터 이미지 스트림을 받을 SurfaceTexture를 선언한다. SurfaceTexture는 OpenGL Texture로 이미지 스트림을 보내는 역할을 한다.

 

class CameraSurfaceRenderer(private val glSurfaceView: GLSurfaceView): GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener {
    @SuppressLint("Recycle")
    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        hTex = GLDrawer2D.initTex()
        surfaceTexture = SurfaceTexture(hTex)
        surfaceTexture?.setOnFrameAvailableListener(this)

        // clear screen with yellow color so that you can see rendering rectangle
        GLES20.glClearColor(1.0f, 1.0f, 0.0f, 1.0f)

        drawer = GLDrawer2D()
        drawer.setMatrix(mvpMatrix, 0)
    }
}

 

2.2 Camera2 촬영 중인 공간 표시

 

Camera 2에서는 Surface 클래스의 형태로 카메라에서 보고 있는 이미지를 전달받을 수 있다. 앞서 Renderer에서 외부로부터 이미지를 받을 공간을 SurfaceTexture로 선언했는데 카메라의 이 클래스를 이용해 Surface를 만들면 카메라로부터 이미지를 전달받고 미리보기로 보여줄 수 있게 된다.

private fun startRecordingVideo() {
    if (cameraDevice == null) return

    try {
        closePreviewSession()

        // Set up Surface for camera preview
        val previewSurface = Surface(renderer.surfaceTexture)
        val surfaces = ArrayList<Surface>().apply {
            add(previewSurface)
        }
        previewRequestBuilder = cameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_RECORD).apply {
            addTarget(previewSurface)
        }

 

2.3. 카메라로 부터 받은 이미지 스트림을 OpenGL로 그리기

 

카메라에서 Renderer에서 선언한 SurfaceTexture에 이미지 스트림으로 보내주는 것이 됐으니 카메라가 보고 있는 이미지 스트림 정보를 실제로 OpenGL 코드로 그려주는 작업이 필요하다. Renderer 함수에는 두 개의 콜백 함수가 있는데 카메라에서 이미지를 전달받은 SurfaceTexture가 호출하는 onFrameAvailable() 콜백이 먼저 불린다. 이 함수에선 최신 이미지가 도착했으니 현재 Renderer와 연동된 GLSurfaceView에게 업데이트를 요청하는 함수인 requestRender() 함수를 호출한다.

 

requestRender() 호출 후엔 연달아서 onDrawFrame() 이 불리는데 여기선 SurfaceTexture로 전달받은 카메라의 프레임 정보를 OpenGL Texture에 업데이트 시킨 후 OpenGL 명령어로 화면에 그리는 작업을 한다. 이 작업이 생략되면 카메라 초기화 작업은 잘 됐음에도 불구하고 화면에는 검은 화면만 뜨게 된다. OpenGL로 그림을 그리는 코드는 생략했다.

 

override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
    requesrUpdateTex = true
    glSurfaceView.requestRender()
}

override fun onDrawFrame(gl: GL10?) {
    if (requesrUpdateTex) {
        requesrUpdateTex = false
        surfaceTexture?.updateTexImage() // 전달 받은 카메라 이미지 프레임을 OpenGL Texture 에 업데이트
        surfaceTexture?.getTransformMatrix(stMatrix)
    }

    drawer.draw(hTex, stMatrix) // Texture 가지고 OpenGL로 그림을 그린다
    if (recording) {
        videoEncoder?.frameAvailableSoon(stMatrix, mvpMatrix)
    }
}

 

여기까지 완료하면 Camera2를 이용해서 출력 중인 화면을 OpenGL 코드로 화면에 보여주는 것까지 가능하다. 그러나 녹화를 하기 위해선 Renderer에서 출력되고 있는 프레임 정보를 MediaCodec으로 보내서 MP4 파일을 만드는 작업까지 가야 하는데 이 내용은 다음 포스트에서 다룰 예정이다.