[모두의 메모] Surface - boostcampwm-2021/android05-boomerang GitHub Wiki

동영상에 그림을 그려넣기 위해서 동영상을 각 프레임으로 나누어 그린 후 사용자에게 보여주고, 재생하기로 하였습니다. 그렇다면 어떻게 동영상을 프레임으로 나누어 그릴 수 있을까요?

SurfaceTexture는 이미지 스트림에서 프레임을 캡처하여 OpenGL ES의 Texture로 저장합니다.

이미지 스트림에 대한 설명은 AOSP 그래픽 개요 문서에서 적혀 있듯이, 이미지 스트림 생산자에서 생성한 그래픽 버퍼를 이미지 스트림 소비자로 옮깁니다. 그 과정에서 버퍼를 옮기기 위해 사용하는 클래스가 Surface입니다.

SurfaceTexture를 Surface + Texture로 생각하고 이 둘을 먼저 이해하는 것이 기능의 이해에 도움이 될 것입니다.

Surface

Surface (AOSP - Surface)는 이미지 스트림 생산자와 소비자 사이에서 버퍼를 전달하도록 하는 역할을 합니다.

Surface는 이미지 스트림 소비자에서 생성하여 이미지 스트림 생산자에 전달하여 그리게 합니다. 그림을 그릴 종이를 전해주고, 생산자가 그 곳에 그림을 그리면 다시 종이를 가져온다고 생각하면 됩니다. 종이는 소비자가 생산자에게 주었지만, 그림은 생산자가 소비자에게 준 것이죠.

아래부터 실제 구현 방식을 정리하겠습니다. Surface의 생성자 중 하나를 예로 들어보겠습니다.

생성자

Surface(surfaceTexture: SurfaceTexture)

생성자의 파라미터로 이미지 스트림 소비자 중 하나인 SurfaceTexture가 들어갑니다. 여기서 만들어진 Surface 인스턴스에 버퍼를 넣으면, SurfaceTexture에서 사용할 수 있게 됩니다. 그렇다면 이제 이미지 스트림 생성자를 찾아 Surface 인스턴스를 전달하면 될 것입니다.

Surface를 가져오는 다른 방법

Surface 클래스에서 직접 인스턴스를 생성하는 방법 외에도 Surface를 가져올 수 있는 방법이 있습니다. 몇몇 클래스에서 Surface를 가져올 수 있는 기능을 제공하는데, 바로 이 앱에서 사용자에게 동영상을 보여주는 SurfaceView와, 동영상을 저장하는 MediaCodec이 이에 해당합니다! 동영상에 그림을 그리는 기능은 전체에 걸쳐 Surface가 중요한 기능을 하는 것이죠.

SurfaceView

SurfaceView는 화면에 보여지는 부분이므로 Button, TextView와 같은 다른 View처럼 레이아웃 파일에 뷰 계층구조의 일부로 포함되어야합니다. 하지만 Inflate 이후 바로 사용할 수 있는 다른 View들과 다르게 SurfaceView는 Inflate 이후 바로 사용할 수 없습니다. Surface가 생성된 이후에 사용을 해야 하는데, 이에 대한 콜백 인터페이스가 제공되기 때문에, 이를 사용하면 됩니다. 이에 대한 자세한 설명은 아래 SurfaceHolder에서 이어가겠습니다.

MediaCodec

MediaCodec도 인코딩할 동영상의 버퍼를 Surface로 받을 수 있습니다. createInputSurface() 함수를 사용하면 Surface를 반환합니다. 이 Surface에 렌더링을 할 때는 하드웨어 가속을 지원하는 API를 사용하는 것이 권장됩니다. 대표적으로 OpenGL ES가 있고, Canvas도 lockHardwareCanvas() 함수를 사용하여 반환받을 시에는 하드웨어 가속이 적용됩니다.

SurfaceHolder

Surface의 소유권을 앱과 공유하기 위한 인터페이스입니다. 콜백을 통해 Surface의 상태 변화를 수신할 수 있습니다.

public interface Callback {
    void surfaceCreated(@NonNull SurfaceHolder var1);

    void surfaceChanged(@NonNull SurfaceHolder var1, int var2, int var3, int var4);

    void surfaceDestroyed(@NonNull SurfaceHolder var1);
}

SurfaceHolder.class의 코드 중 콜백에 해당하는 부분입니다. 액티비티나 프래그먼트에서 SurfaceHolder.Callback을 인터페이스 상속하여 구현하면 됩니다. 콜백이 호출되는 순서는 일반적으로 아래와 같습니다.

액티비티가 시작되면

onCreate() -> onStart() -> onResume() -> surfaceCreated() -> surfaceChanged()

의 순서로 호출이 됩니다. 또한 액티비티가 포그라운드에서 사라질 때

onPause() -> surfaceDestroyed()

의 순서로 호출이 됩니다.

SurfaceView의 Surface를 가져오기 위해서는 SurfaceView에서 직접 가져오는 방식이 아닌, SurfaceHolder에서 가져오는 방식을 사용하여야 합니다.

override fun surfaceCreated(p0: SurfaceHolder) {
  surface = p0.surface
}

이렇게 가져오면 됩니다.

액티비티, 프래그먼트 생명 주기와 WeakReference인 Surface의 특성으로 인해 surfaceDestroyed()가 호출될 가능성이 항상 존재하기 때문에 Surface가 파괴되고 다시 생성될 가능성에 항상 대비하여야 합니다.

Surface의 사용

모두의 메모 구조를 다시 한 번 살펴봅시다.

videomemo

여기에서 화살표는 모두 Surface 클래스를 통해 이미지 버퍼가 전달되는 만큼, Surface 클래스는 기능의 구현에 있어 가장 핵심적인 요소입니다. 그렇다면 이제 화살표가 어떻게 연결되는지, 즉 이미지 스트림의 소비자와 생산자는 어떻게 연결되는지 알아보겠습니다.

이미지 스트림 소비자와 생산자 연결

MediaPlayer.setSurface(surface: Surface)

동영상을 재생하는 MediaPlayer 클래스를 예로 들면, setSurface 함수에 위에서 생성한 Surface 인스턴스를 전달하면 됩니다.

이제 MediaPlayer에서 동영상을 재생하면, Surface를 통해 SurfaceTexture로 버퍼가 넘어갈 것입니다. 이제는 프레임 단위로 나눌 차례입니다.

SurfaceTexture

onFrameAvailableListener

surfaceTexture.setOnFrameAvailableListener(this)

override fun onFrameAvailable(p0: SurfaceTexture?) {
    // 새로운 Frame에 그림 그리기
}

방법은 의외로 간단합니다! SurfaceTexture가 새로운 프레임을 받을 때마다 호출되는 콜백 인터페이스를 가지고 있기 때문입니다. 콜백 인터페이스를 상속하고, 오버라이드하기만 하면 됩니다. 콜백 인터페이스의 대상이 되는 Fragment나 Activity를 setOnFrameAvailableListner()를 통해 전달하는 코드 한 줄만 더하면 됩니다.

SurfaceTexture에 대한 내용은 이후 텍스처에 대한 설명과 함께 자세히 다루어보도록 하겠습니다.