Nuke Olaf - Log Store

[Android] 안드로이드 - camera2 api 로 동영상을 촬영, 재생하며 생긴 코덱 문제 해결 방법 본문

Android

[Android] 안드로이드 - camera2 api 로 동영상을 촬영, 재생하며 생긴 코덱 문제 해결 방법

NukeOlaf 2019. 12. 16. 15:07

E/MediaMetadataRetrieverJNI: getFrameAtTime: videoFrame is a NULL pointer

 

1. 문제 발생 원인 : (어떤 문제가 있었는지)

camera2 api 를 이용하여 만든 카메라 앱에 동영상 촬영기능을 추가하였다. 앱에서 textureView 를 통해 카메라 미리보기 화면을 보여줄때, 나는 미리보기를 전체 화면에 꽉 차게 보여주고 싶어서 textureView의 size 를 핸드폰 화면에 딱 맞도록 임의로 설정해주었다. 이러한 방식을 사용하면 화면이 왜곡되어 보인다는 문제점은 있었으나, 사진을 촬영해서 사진을 읽는 부분에서는 문제가 없었기 때문에 크게 신경쓰지 않았던것이 나중에 동영상 촬영 기능에서 문제를 발생시켰다.

카메라앱에 동영상 촬영기능을 넣었다. 동영상을 촬영할때, 촬영과 저장에는 문제가 없었다. 그러나 MediaRecorder 가 촬영한 동영상의 해상도를 textureView 의 사이즈 크기대로 인코딩해버려서 나중에 동영상을 불러와 재생시킬때, 동영상이 지원하지 않는 형식의 해상도로 저장이 되어있었기 때문에 코덱문제가 발생했다. 그래서 "지원하지 않는 코덱입니다" 라는 문구와 함께 동영상이 재생이되지 않았다.

안드로이드 자체에서 제공하는 코덱으로는 동영상을 재생할 수는 없었지만, 시중의 다른 동영상 플레이어 앱으로는 내 동영상을 재생할 수 있었다. 자세히 알아보지는 않았지만, 시중의 동영상 플레이어에서는 동영상의 해상도와 상관없이 재생할 수 있는 코덱을 지원하기 때문인듯 했다.

 


2. 문제 해결 과정 : (어떻게 해결했는지)

문제를 해결하기 위해 내가 생각 한 방법은 두 가지였다.

 

(1) textureView 의 사이즈를 안드로이드에서 지원하는 형식으로 저장하는 방법 : (https://developer.android.com/guide/topics/media/media-formats)

(2) 코덱에 대해 공부하고, 내 카메라앱에서 해당 동영상을 재생할 수 있는 코덱을 설치해서 해결하는 방법

 

두 번째 방법으로 해결하는 것은 최후의 보루라고 생각했다. 코덱이 무엇인지, 왜 쓰는지 어떻게 쓰는지 처음부터 전부 다 알아봐야하기 때문에, 문제를 해결하는데 들이는 시간에 비해 가성비가 안좋다고 생각했다. 물론 코덱을 공부하면 나중에 도움이 되긴 하겠지만 안드로이드 개발자 문서에서도 "장치에 구애받지 않는 미디어 인코딩 프로파일을 사용하는 것이 가장 좋습니다" 라고 기술되어 있는 것을 보면, 처음부터 동영상을 저장(인코딩) 할때 안드로이드에서 지원하는 미디어 형식으로 저장하는 방법으로 해결해보는 것이 낫다고 판단했다.

그리고, 내 동영상 파일을 미리 볼 수 있도록 하기 위해 MediaRetriever 로 프레임 한 장을 갖고 오려고 시도해 보았었다. 그런데 E/MediaMetadataRetrieverJNI: getFrameAtTime: videoFrame is a NULL pointer 에러가 뜬 것을 보아, 코덱으로 동영상 재생 문제를 해결한다고 해도 MediaRetriever 가 프레임을 가져오지 못할 수도 있겠다는 생각이 들었다. MediaRetriever 까지 건드려야 한다면 코덱으로 문제를 해결하는 것은 정말 더더욱 산으로 가는 느낌.

이 문제를 해결하기 위해 google 의 android-Camera2Video-master 와 android-Camera2Basic-master 의 예제를 참고 했다. (https://github.com/googlearchive/android-Camera2Video)

해당 예제에서는 TextureView 를 extends 하는 AutoFitTextureView 클래스를 따로 작성하여 그 안에 setAspectRatio() 함수를 만들고, onMeasure() 함수를 오버라이딩하였다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class AutoFitTextureView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : TextureView(context, attrs, defStyle) {
 
    private var ratioWidth = 0
    private var ratioHeight = 0
 
    /**
     * Sets the aspect ratio for this view. The size of the view will be measured based on the ratio
     * calculated from the parameters. Note that the actual sizes of parameters don't matter, that
     * is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result.
     *
     * 이 뷰의 종횡비를 설정합니다. 뷰의 크기는 매개 변수에서 계산 된 비율에 따라 측정됩니다.
     * 매개 변수의 실제 크기는 중요하지 않습니다. 즉,
     * setAspectRatio (2, 3) 및 setAspectRatio (4, 6)을 호출하면 동일한 결과가 나타납니다.
     *
     * @param width  Relative horizontal size
     * @param height Relative vertical size
     */
    fun setAspectRatio(width: Int, height: Int) {
        if (width < 0 || height < 0) {
            throw IllegalArgumentException("Size cannot be negative.")
        }
        ratioWidth = width
        ratioHeight = height
        requestLayout()
    }
 
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val width = View.MeasureSpec.getSize(widthMeasureSpec)
        val height = View.MeasureSpec.getSize(heightMeasureSpec)
        if (ratioWidth == 0 || ratioHeight == 0) {
            setMeasuredDimension(width, height)
        } else {
            if (width < ((height * ratioWidth) / ratioHeight)) {
                setMeasuredDimension(width, (width * ratioHeight) / ratioWidth)
            } else {
                setMeasuredDimension((height * ratioWidth) / ratioHeight, height)
            }
        }
    }
 
}
 

 

그리고, 카메라를 촬영하는 액티비티에 configureTransform() , chooseVideoSize(), chooseOptimalSize() 함수를 추가하여 각종 size 들의 값을 지원하는 크기형식으로 조정해주는 듯 했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/**
     * Configures the necessary [android.graphics.Matrix] transformation to `textureView`.
     * This method should not to be called until the camera preview size is determined in
     * openCamera, or until the size of `textureView` is fixed.
     *
     * @param viewWidth  The width of `textureView`
     * @param viewHeight The height of `textureView`
     */
    private fun configureTransform(viewWidth: Int, viewHeight: Int) {
        val rotation = windowManager.defaultDisplay.rotation
        val matrix = Matrix()
        val viewRect = RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat())
        val bufferRect = RectF(0f, 0f, previewSize.height.toFloat(), previewSize.width.toFloat())
        val centerX = viewRect.centerX()
        val centerY = viewRect.centerY()
 
        if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) {
            bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY())
            matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL)
            val scale = Math.max(
                viewHeight.toFloat() / previewSize.height,
                viewWidth.toFloat() / previewSize.width)
            with(matrix) {
                postScale(scale, scale, centerX, centerY)
                postRotate((90 * (rotation - 2)).toFloat(), centerX, centerY)
            }
        } else if (Surface.ROTATION_180 == rotation) {
            matrix.postRotate(180f, centerX, centerY)
        }
        texture.setTransform(matrix)
    }
 
    /**
     * In this sample, we choose a video size with 3x4 aspect ratio. Also, we don't use sizes
     * larger than 1080p, since MediaRecorder cannot handle such a high-resolution video.
     *
     * @param choices The list of available sizes
     * @return The video size
     */
    private fun chooseVideoSize(choices: Array<Size>= choices.firstOrNull {
        it.width == it.height * 4 / 3 && it.width <= 1080 } ?: choices[choices.size - 1]
 
    /**
     * Given [choices] of [Size]s supported by a camera, chooses the smallest one whose
     * width and height are at least as large as the respective requested values, and whose aspect
     * ratio matches with the specified value.
     *
     * @param choices     The list of sizes that the camera supports for the intended output class
     * @param width       The minimum desired width
     * @param height      The minimum desired height
     * @param aspectRatio The aspect ratio
     * @return The optimal [Size], or an arbitrary one if none were big enough
     */
    private fun chooseOptimalSize(
        choices: Array<Size>,
        width: Int,
        height: Int,
        aspectRatio: Size
    ): Size {
 
        // Collect the supported resolutions that are at least as big as the preview Surface
        val w = aspectRatio.width
        val h = aspectRatio.height
        val bigEnough = choices.filter {
            it.height == it.width * h / w && it.width >= width && it.height >= height }
 
        // Pick the smallest of those, assuming we found any
        return if (bigEnough.isNotEmpty()) {
        } else {
            choices[0]
        }
    }
 
    /**
     * Compare two [Size]s based on their areas.
     */
    class CompareSizesByArea : Comparator<Size> {
 
        // We cast here to ensure the multiplications won't overflow
        override fun compare(lhs: Size, rhs: Size) =
    }
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter

해당 함수들이 어떻게 기능하는지는 전부 분석하지 못했지만, 위의 함수들을 사용하여 동영상이 지원하는 형식의 크기대로 인코딩될 수 있도록 하여 문제를 해결할 수 있었다.

 


3. 느낀점 : (어떤 배움이 있었는지)

이 문제를 해결하기 위해 AutoFitTextureView 클래스와 다양한 함수들을 내 코드에 추가해야 했다. 처음에는 내 코드에서 어디서부터 어디까지가 문제와 관련있는지 전부 다 파악할 수 없었기 때문에 프로젝트를 거의 새로 팠다고 봐도 무방할 정도로 코드의 기본적인 부분부터 전부 다 고쳐야 했다.

코드를 다시 또 새로 작성하면서 내가 무심코 지나쳤거나 중요하다고 생각하지 않은 부분(여기서는 TextureView 를 임의로 설정했을때 화면이 왜곡되는 문제를 지나쳤던 부분)에서 나중에 문제가 발생할 수 있다는 사실을 배울 수 있었다. 정말 기본 중의 기본이겠지만 이렇게 몸소 체험하고 나니 감회가 새로웠다.

또한, 카메라 기능과는 상관없지만 내가 작성했던 코드를 구글 android-Camera2Video-master 예제와 비교 분석하는 과정에서 내 코드와 미세하게 다른 부분들을 찾아내면서 코드를 작성하는 프로세스와 방식에 대해 몇가지 배운 점이 있었다. 예를 들어, 함수의 기능에 대한 설명은 /** */ 으로 작성하고, 함수 내부의 코드에 대한 설명은 // 으로 작성하는 점이 새로웠다. 주석을 그런 방식으로 작성하니까 이게 함수의 주석인지 코드 한 줄에 대한 주석인지 눈에 잘 띄어서 보기 편했다. 또한, 미디어 파일을 저장할때, 나는 저장경로를 getExternalStroage 만 사용했는데, 

1
2
3
4
5
6
7
8
9
10
11
val fileName = "video${fileCount}.mp4"
   
        val dir = this.getExternalFilesDir(null)
 
        if (dir == null) {
            nextVideoAbsolutePath = fileName
        }else {
            nextVideoAbsolutePath = "${dir.absoluteFile}/$fileName"
        }
 
        val file = File(nextVideoAbsolutePath)

이런 방식으로 저장 경로를 설정하니까 훨씬 관리하기 편하고 보기 좋았던 것 같다.

Comments