Skils/Android

[Android] - 외부 라이브러리 없이 동영상 편집 구현하기

재한 2024. 8. 24. 21:02

해당 프로젝트 관련 글

 

[Android] - 동영상에 대한 썸네일 리스트 반환하기

[Android] - 멀티 모듈 with Version Catalog [멀티 모듈 적용기(2)]

[Android] - 레이어 분리 [멀티 모듈 적용기(1)]

[Android] 커스텀 컨트롤러 구현 및 동영상 재생 관리

[Android] 비디오 타임라인 이벤트 처리

[Android] 영상 편집 UI 구현

[Android] - Retrofit으로 에러 메시지 처리하기

[Android] DataSource 적용 및 분리

[Android] 네이버 로그인 프로필 가져오기

[Android] 자동 로그인 with DataStore(Kotlin)

[Android] 네이버 간편 로그인(Kotlin)

 

 

개요

저번 글에서는 동영상을 선택할 경우 편집기 UI에 썸네일 리스트를 반환하는 글을 작성했습니다.

이번 글에서는 동영상을 어떻게 라이브러리 없이 편집했는지에 대한 글을 작성할까 합니다.

 

MediaExtractor

동영상 편집을 구현하기 위해서 저는 MediaExtractor와 MediaMuxer를 사용했는데요,

우선적으로 MediaExtractor에 대해서 짚고 넘어가겠습니다.

https://developer.android.com/reference/android/media/MediaExtractor

 

MediaExtractor  |  Android Developers

 

developer.android.com

 

MediaExtractor는 안드로이드 프레임워크에서 멀티미디어 파일의 트랙(비디오, 오디오) 데이터를 추출할 수 있도록 도와주는 클래스입니다. 주로 비디오 또는 오디오 파일을 디코딩하거나, 재생 전 데이터를 처리할 때 사용됩니다.

MediaExtractor를 사용하여 파일에서 특정 트랙(오디오 트랙)을 선택하고, 해당 트랙에서 데이터 샘플을 추출할 수 있습니다.

 

동영상을 편집하기 위해선 동영상의 데이터를 추출하는 것이 필요한데요, 해당 작업을 MediaExtractor를 통해서 진행합니다.

동영상의 여러 정보를 가져올 수 있지만 주로 오디오와 비디오에 대한 트랙을 추출합니다.

 

오디오 트랙은 미디어 파일에서 소리를 뜻합니다.

비디오 트랙은 영상 데이터를 뜻합니다.

 

MediaExtractor를 통해서 영상에 대해 필요한 정보를 추출하고 MediaMuxer에 전달해줘야 합니다.

위 작업이 아래 코드를 통해서 구현했습니다.

val outputFilePath = video.absolutePath
val extractor = MediaExtractor() // extractor 생성
extractor.setDataSource(file.path) // 추출할 파일 지정
val muxer = MediaMuxer(outputFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) //muxer 초기 설정(경로, 타입)
val trackIndexMap = IntArray(extractor.trackCount) 
for (i in trackIndexMap.indices) {
    val format = extractor.getTrackFormat(i) // format 추출
    val mime = format.getString(MediaFormat.KEY_MIME) // 트랙의 유형을 식별해야 함.
    val trackIndex = muxer.addTrack(format) // muxer에 track를 추가함
    trackIndexMap[i] = trackIndex // 추가한 track의 인덱스를 저장
}

 

 

지금 와서 아쉬운 점은 필요없는 트랙까지 모두 추가해주고 있는 모습이 보이는데요,

필터링을 걸어서 mime 타입이 video, audio에 대한 트랙 정보만 muxer에 전달하는 것이 좋았던 거 같습니다.

 

다음과 같이 수정할 수 있을것 같습니다.

for (i in 0 until extractor.trackCount) {
    val format = extractor.getTrackFormat(i)
    val mime = format.getString(MediaFormat.KEY_MIME)
    if (mime != null && mime.startsWith("audio/") || mime.startsWith("video/")) {
        val trackIndex = muxer.addTrack(format)
        trackIndexMap[i] = trackIndex
    }
}

 

 

위 작업을 통해서 진행도는 다음과 같습니다.

 

  1. MediaExtractor에 데이터를 추출할 영상을 설정
  2. Muxer로 생성할 데이터의 타입(Mp4)과 저장될 경로를 설정
  3. 영상 파일에 대해 오디오 트랙과 영상 트랙을 추출함.
  4. Muxer에 트랙을 추가함

 

 

MediaMuxer

https://developer.android.com/reference/android/media/MediaMuxer

 

MediaMuxer  |  Android Developers

 

developer.android.com

 

MediaMuxer는 여러 개의 트랙을 합쳐 하나의 파일로 결합해 새로운 미디어 파일을 생성하는 데 사용됩니다.

앞에서 MediaExtractor를 통해 비디오 파일에서 추출한 트랙을 하나의 파일로 결합해줍니다.

 

동영상에 대한 디코딩과 인코딩은 MediaCodec이 담당하게 됩니다.

 

MediaCodec

MediaCodec은 미디어 데이터의 인코딩 및 디코딩을 수행하는 클래스입니다.

비디오와 오디오 스트림의 압축 및 압축 해제, 즉 데이터의 변환을 처리해 줍니다.

 

https://developer.android.com/reference/android/media/MediaCodec

 

MediaCodec  |  Android Developers

 

developer.android.com

 

MediaCodec과 MediaMuxer의 관계는 다음과 같습니다.

  • MediaCodec을 통해서 비디오를 인코딩 혹은 디코딩 후 처리된 데이터를 결합해서 MediaMuxer를 통해서 하나의 파일로 저장합니다.

사실 제가 작성한 코드는 인코딩과 디코딩을 처리하기보단 입력받은 시간에 대해 동영상 파일을 추출해서 저장하는 방식에 가깝습니다.

복잡한 인코딩과 디코딩을 처리할 필요가 없다고 생각해서 다음과 같이 코드를 작성했습니다.

muxer.start()
    val buffer = ByteBuffer.allocate(1024 * 1024)
    val bufferInfo = MediaCodec.BufferInfo()
    for (i in trackIndexMap.keys) {
        extractor.selectTrack(i)
        if (start != 0L) {
            extractor.seekTo(start * 1000L, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
        }
        while (true) {
            bufferInfo.size = extractor.readSampleData(buffer, 0)
            bufferInfo.presentationTimeUs = extractor.sampleTime
            if (bufferInfo.size < 0) {
                break
            }
            if (bufferInfo.presentationTimeUs > end * 1000L) {
                break
            }
            bufferInfo.flags = extractor.sampleFlags
            muxer.writeSampleData(trackIndexMap[i]!!, buffer, bufferInfo)
            extractor.advance()
        }
	    extractor.unselectTrack(i)
    }
    extractor.release()
    muxer.stop()
    muxer.release()
    return video
}

 

muxer.start()

 

muxer를 시작해 줍니다.

해당 작업을 통해 나는 이제 muxer의 등록된 트랙에 대해 데이터를 기록할 준비가 되었다는 설정을 해줍니다.

start를 해주지 않으면 아래 writeSampleData를 호출하더라도 데이터가 기록되지 않기에 필요한 작업입니다.

val buffer = ByteBuffer.allocate(1024 * 1024)
val bufferInfo = MediaCodec.BufferInfo()

 

데이터를 읽을 임시 buffer와 데이터를 저장할 bufferInfo 객체를 생성해 줍니다.

bufferInfo 객체는 후 muxer의 데이터를 기록하기 위해 필요합니다.

 

이후에는 muxer의 등록된 트랙에 대해 작업을 수행해야 합니다.

for (i in trackIndexMap.keys) {
    extractor.selectTrack(i)
    if (start != 0L) {
        extractor.seekTo(start * 1000L, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
    }
    while (true) {
        bufferInfo.size = extractor.readSampleData(buffer, 0)
        bufferInfo.presentationTimeUs = extractor.sampleTime
        if (bufferInfo.size < 0) {
            break
        }
        if (bufferInfo.presentationTimeUs > end * 1000L) {
            break
        }
        bufferInfo.flags = extractor.sampleFlags
        muxer.writeSampleData(trackIndexMap[i]!!, buffer, bufferInfo)
        extractor.advance()
    }
    extractor.unselectTrack(i)
}

 

위에서 추가한 트랙 index에 대해 트랙을 선택합니다.

저는 start와 end를 통해 동영상 편집을 해야 하기 때문에 start가 0이 아니라면 seekTo 메서드를 통해 동영상의 프레임을 뒤로 당겨줍니다.

 

다음 작업은 간단합니다. 

반복문을 통해 동영상이 끝나지 않는 시점까지 계산해서 데이터를 저장해 주는 작업인데요,

 

extractor를 통해서 데이터를 읽어오고 해당 데이터를 bufferInfo에 저장하고, 해당 값을 muxer에 저장한 후 extractor.advance 호출을 통해 데이터의 시점을 이동해 줍니다.

 

해당 과정에서 데이터를 읽을 수 없어 bufferInfo의 사이즈가 0이 되거나, 시간대가 편집이 끝나는 시점을 초과한다면 종료해 줍니다.

 

이후 트랙의 선택을 해제해 줍니다.

 

모든 트랙에 대해 작업을 완료했다면 리소소를 해제해줘야 합니다.

extractor.release()
muxer.stop()
muxer.release()

 

작업이 끝났기 때문에 extractor와 muxer를 반납하고 , 종료한 뒤 파일을 완성합니다.

 

 

 

 

편집 구간을 설정한 후 영상을 확인해보면 편집된 영상임을 확인할 수 있습니다.

 

 


글을 작성하면서 영상 처리에 대한 흐름을 완벽하게 숙지하지 않고 코드를 작성해서 불필요한 트랙의 추가와 최적화가 전혀 처리되지 않아서 실수 투성이인 코드인 것 같습니다 ㅠ

정답인 코드가 아니라 참고만 해주세요!