반응형

안녕하세요 

디버깅 시간..? 과 비슷하지만 그 전에 동영상 좌우반전 처리와 + 관련된 버그 찾기 시간임. 

 

0. 서론

이미지는 bitmap으로 변환해서 x축을 -1f 만 해도 쉽게 좌우 반전이 되지만, 영상은 어떻게 해야할까.. 싶음

 

영상은 2가지 방법? 정도를 제안해줌

1. 처음 녹화때부터 Camera에 접근해서 좌우 반전된 카메라를 사용하기
2. 녹화된 영상을 사후에 좌우 반전 처리하기 

 

근데 Claude에 의하면, (거의 프롬프트 다루기 석사급임 ㅋㅋ) 

 

녹화 전에 Preview나 VideoCapture 설정 시점에서 미리 좌우 반전을 하는 것 ..
CameraX 최신 버전에서는 VideoCapture의 미러링 설정 방식이 좀 다릅니다. 
출처: Claude

라는데 아무리 봐도 CameraX라이브러리를 써가지고 이를 좌우반전하기에는 너무 라이브러리 깊은 부분까지 다뤄야할 것 같아서 당일 내에 구현해야하는 나로써는 failed였음.

 

근데 GPT는 이견이 있었음

FFmpeg를 직접 사용하지 않고 CameraX를 사용하여 동영상을 녹화하고 있습니다. CameraX 자체에는 좌우반전 효과를 추가할 수 있는 기능이 제한적이므로, 녹화된 영상에 FFmpeg를 활용하여 후처리로 좌우반전 효과를 적용해야 합니다.
출처: chatGPT

 

그래서 읽어보니 GPT가 정확히 아는 것 같아서 코드를 좀 읽어봤더니 

 

구현 순서는 

1. 동영상 녹화가 종료될 때를 감지
2. FFMpeg라이브러리를 사용해서 이를 좌우반전해서 저장해라

-> 어차피 나는 5초짜리 5mb가 넘지 않는 영상이라 UX측면에서 괜찮을 것 같아 결정

 

1. 본론

 

(1) 의존성 추가 

먼저 의존성을 추가해야합니다

 

// 250110 기준
implementation("com.arthenica:ffmpeg-kit-full-gpl:4.5.LTS")

중간에 내가 든 생각이..

예전 컴퓨터 비전 공부할 때 코드 좀만 틀려도 결과가 확확 바뀌고, 버전 안맞는 부분도 예민해서 동영상 처리에 대해 좀 색안경 끼고 안하면 안되나.. 싶었는데 생각보다 깔끔하게 동작해서 놀람

저는 Activity에서 사용했구요 

여기다가 전역변수가 있었구요 

    // 영상 캡처
    private var recordingJob: Job? = null
    private lateinit var videoCapture : VideoCapture<Recorder>
    private var recording: Recording? = null

 

(2) 함수 구현 

그렇게 내 프로젝트에 맞게 코드를 적어봤더니 만들어진 함수

private fun flipVideoHorizontally(inputPath: String, outputPath: String, callback: (Boolean) -> Unit) {
        val command = "-i $inputPath -vf hflip -c:a copy $outputPath"
//        val command = "-i $inputPath -vf hflip -y -c:v libx264 -crf 18 -preset veryfast -c:a copy $outputPath"
        lifecycleScope.launch(Dispatchers.Main) {
            val dialog = LoadingDialogFragment.newInstance("동영상").apply {
                show(supportFragmentManager, "LoadingDialogFragment")
            }

            withContext(Dispatchers.IO) {
                // FFmpeg 명령 실행
                FFmpegKit.executeAsync(command) { session ->
                    lifecycleScope.launch(Dispatchers.Main) {
                        val returnCode = session.returnCode
                        if (!isFinishing && !isDestroyed) {
                            dialog.dismiss()
                        }
                        if (ReturnCode.isSuccess(returnCode)) {
                            Log.d("FFMPEGAlert", "Video successfully mirrored and saved to: $outputPath")
                            callback(true)
                        } else {
                            Log.e("FFMPEGAlert", "Error occurred while mirroring video: ${session.getLogsAsString()}")
                            callback(false)
                        }
                    }
                }
            }
        }

    }


아무튼

이 함수는 내가 저장되는 곳과 저장할 곳, 그리고 영상반전 결과에 대해 callback을 받아서 넘겨주는 함수였음. 이 부분은 커스텀 되지 않을까.

 

함수 3줄 요약은

1) 일단 UI측면에서 로딩하는 dialogFragment를 하나 넣고
2) BackGround에서 FFmpegKit를 사용해서 command에 맞게 execute해주세요.
3)  그리고 결과에 대한 override 함수 처리 

 

나는 Boolean으로 성공 실패에 대해서 true / false 줌 

 

* 근데 여기서 유의할 점은 저 command 임. 세상이 참 좋아져서 gpt로 command에 대해서 그냥 쓰면 되는데 예전에 공식문서 찾아보던 시절에는 이걸 어디서 찾았을런지.. 


(3) 비디오 녹화 종료 시 함수 사용 

 

암튼 이 함수를 어디서 쓰냐 

 

VideoCapture에 대한 전역변수를 init하고 나서 녹화 시작, 녹화 종료에 대해서 적을 때

 

중요한 곳은 VideoRecordingEvent.Finalize

 

이 곳에서 프로젝트 내부에서 저장된 

val savedUri = recordEvent.outputResults.outputUri

이 uri를 토대로 플립(좌우반전을 진행 하면 됨)

 

여기서 나는 "mirrored_video.mp4"로 저장했는데 이 부분은 본인이 원하는 파일명으로 바로 저장해도 됨. 

 

그리고 flip이 성공적으로 저장됐을 때는 callback으로 다음 단계 처리 ㅎㅎ 

recording = videoCapture.output.prepareRecording(this@VideoActivity, mediaStoreOutputOptions).apply {
        if (PermissionChecker.checkSelfPermission(
                this@MeasureSkeletonActivity, Manifest.permission.RECORD_AUDIO
            ) == PermissionChecker.PERMISSION_GRANTED) {
            withAudioEnabled()
        }
    }.start(ContextCompat.getMainExecutor(this@VideoActivity)) { recordEvent ->
        when (recordEvent) {
            is VideoRecordEvent.Start -> {
                CoroutineScope(Dispatchers.Main).launch {
                    startRecording = true
                    startRecordingTimer()
                }
            }
            is VideoRecordEvent.Finalize -> {
                if (!recordEvent.hasError()) {
                    Log.v("녹화종료시점", "isRecording: $isRecording, isCapture: $isCapture")
                    isRecording = false
                    startRecording = false
                    val savedUri = recordEvent.outputResults.outputUri
                    val inputPath = getPathFromUri(this@VideoActivity, savedUri) // URI를 파일 경로로 변환
                    val outputPath = File(this@VideoActivity.cacheDir, "mirrored_video.mp4").absolutePath

                    if (inputPath != null) {
                        flipVideoHorizontally(inputPath, outputPath) { success ->
                            if (success) {
                                saveMediaToCache(this@MeasureSkeletonActivity, Uri.fromFile(File(outputPath)), videoFileName, false)
                                callback()
                            } else {
                                Log.e(TAG, "Failed to apply mirror effect to video.")
                            }
                        }
                    }
                } else {
                    CoroutineScope(Dispatchers.Main).launch  {
                        isRecording = false
                        startRecording = false
                        recording?.close()
                        recording = null
                        Log.e(TAG, "Video capture ends with error: ${recordEvent.error}")
                    }
                }
            }
        }
    }

그럼 끗 인데.


3. 예외 및 오류 상황 2가지 

 

(1)  command

먼저 command를 잘 보면 내가 주석처리 해놓은 곳이 있는데 

 

이게 가장 기본적인 command임. 이걸로 저장하면 파일 화질이 거의 tv동물농장 초창기급 640x480? 될라나 싶은 해상도의 저화질 영상이 저장됨..

 

그렇기에 위에 주석 처리 해놓은 command를 사용하면됨

일단 일반적으로 원본과 거의 비슷한 화질로 인코딩 된다는 libx264 -crf 18 을 사용하면 됨. 24도 있다는데 18써도 거의 원본(HD)랑 같아서 써보지는 않았음 

 

 

a) 기본 command

val command = "-i $inputPath -vf hflip -c:a copy $outputPath"

 

b) 화질 개선 command 

val command = "-i $inputPath -vf hflip -y -c:v libx264 -crf 18 -preset veryfast -c:a copy $outputPath"

 

(2) 같은 영상을 다시 좌우반전을 했을 때 나오는 오류 

Error occurred while mirroring video: ffmpeg version v4.5-dev-2008-g90da43557f Copyright (c) 2000-2021 the FFmpeg developers
                                                                                                      built with Android (7284624, based on r416183b) clang version 12.0.5 (https://android.googlesource.com/toolchain/llvm-project c935d99d7cf2016289302412d708641d52d2f7ee)
                                                                                                      configuration: --cross-prefix=aarch64-linux-android- --sysroot=/files/android-sdk/ndk/23.0.7599858/toolchains/llvm/prebuilt/linux-x86_64/sysroot --prefix=/storage/light/projects/ffmpeg-kit/prebuilt/android-arm64-lts/ffmpeg --pkg-config=/usr/bin/pkg-config --enable-version3 --arch=aarch64 --cpu=armv8-a --cc=aarch64-linux-android21-clang --cxx=aarch64-linux-android21-clang++ --ranlib=llvm-ranlib --strip=llvm-strip --nm=llvm-nm --extra-libs='-L/storage/light/projects/ffmpeg-kit/prebuilt/android-arm64-lts/cpu-features/lib -lndk_compat' --target-os=android --enable-neon --enable-asm --enable-inline-asm --enable-cross-compile

기존에 영상이 저장은 되는데

원하는 대로 녹화가 안됐을 때를 대비해서 되돌리기를 구현해놨었음.

 

여기서 되돌리기 후 녹화했더니 이런 오류 

 

Error occurred while mirroring video: ffmpeg version v4.5-dev-2008-g90da43557f

 

이게 도대체 뭘까.. 라는 생각했는데 

이 부분은 좌우반전된 영상인 "mirrored_video.mp4"가 있으면 좌우반전을 하다가 기존 파일이 있기 때문에 불가능하다는 로그였음.

이 부분의 해결방법은 

 

* 해결 방법 

1. command 수정
2. 기존 임시 파일을 삭제하면서 사용하기

2가지가 있는데 

1번의 command 는

ffmpeg -i input.mp4 -vf "hflip" -y output.mp4

라는데 파일 저장전에 -y 라는 강제 덮어씌우기 커맨드를 넣으면 되구요 

 

2 번은 당연히 임시 파일 .delete하면 된다는 점 ㅋㅋ

아무튼 간단했던 오류인데 구글링이 잘 안나와서 당황했다는 점

 

이 정도면 해결 

안드로이드 FFMpeg 썸네일

반응형