ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Camera2로 전면 카메라 보이는대로 녹화하기 - 1
    개발/안드로이드 2020. 11. 8. 14:01

    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 파일을 만드는 작업까지 가야 하는데 이 내용은 다음 포스트에서 다룰 예정이다.

    댓글

Designed by Tistory.