[Android] - 동영상에 대한 썸네일 리스트 반환하기
해당 프로젝트 관련 글
[Android] - 멀티 모듈 with Version Catalog [멀티 모듈 적용기(2)]
[Android] - 레이어 분리 [멀티 모듈 적용기(1)]
[Android] 커스텀 컨트롤러 구현 및 동영상 재생 관리
[Android] - Retrofit으로 에러 메시지 처리하기
개요
동영상을 직접 편집할 수 있고, 지도에 올리는 애플리케이션을 개발하고 있습니다.
편집 기능을 구현하기 위해 편집기 UI를 구현했고, 요구사항에 맞게 편집기 UI 안에 썸네일을 리스트형태로 보여줘야 하고,
영상에 대한 썸네일을 구하는 코드를 작성했고, 그에 대한 글을 작성할까 합니다.
MediaMetadataRetriver
동영상의 이미지를 가져오기 위해선 동영상의 데이터에 대해 접근할 수 있어야 합니다.
안드로이드에서는 MediaMetadataRetriver를 통해서 해결할 수 있습니다.
짧은 영어실력으로 해석해보자면, 인풋으로 들어온 media file에 대해서 meta dat를 뽑아낼 수 있다.
input media를 넣어주기 위해선 setDataSource
라는 함수를 이용해야 한다.
SetDataSource
setDataSource
는 위와같이 여러 가지 형태로 구현할 수 있습니다.
초기에는 동영상 파일에 대해 path를 계산하는 것보단 동영상 파일의 Uri와 context를 제공하는 것이 간편해서 이용했는데,
context를 전달하는것은 좋지 않다고 생각해서 동영상 파일의 path를 계산해서 사용하는 방향을 선택했습니다.
(저는 썸네일에 대한 작업을 처리하는 클래스를 따로 분리해서 사용했기에 context를 전달하는 것은 적절하지 않다고 생각했습니다.)
이렇게 MediaMetadataRetriver에 동영상을 넣어줬다면 뽑아낼 수 있는 정보는 정말 다양합니다.
동영상에 대한 정보를 뽑아내기 위해서는 아래 코드를 사용해야 합니다.
extractMetadata
val retriever = MediaMetadataRetriver() // MediaMetadataRetriver 객체 생성
retriver.setDataSource(videoPath) // 동영상 파일 넣어주기
val info = retriver.extractMetadata(원하는 데이터)
공식문서에 의하면 뽑아낼 수 있는 데이터는 다양합니다. 데이터에 대한 확인은 공식문서에서 constants에서 확인할 수 있습니다.
저는 동영상의 썸네일을 구간 별로 뽑아내야했기에, 동영상의 길이만을 가지고 왔습니다.
val durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
동영상의 길이에 대응해 고정된 썸네일의 개수를 리스트 형태로 반환하는 것이 이번 작업의 목표입니다.
위에서 뽑아온 durationStr의 반환타입은 String?이기에 다음과 같은 코드를 통해서 동영상의 길이를 더블체크 했습니다.
val duration = durationStr?.toLongOrNull() ?: return emptyList()
val interval = duration / thumbnailCount
toLongOrNull을 통해 Long 또는 Null을 반환하게 해서 Null 일경우 썸네일이 들어있지 않은 리스트를 반환합니다.
만약 duration이 Null이 아닐 경우 제가 정한 썸네일 개수를 나눠서 구간을 끊어줍니다.
예를 들어,
썸네일의 개수를 10개이고, 동영상이 100초로 가정할 경우 10,20,30, ~~ 100초에 해당하는 구간에 대해 썸네일을 추출해 줍니다.
val thumbnails = mutableListOf<VideoThumbnail>()
for (i in 0 until thumbnailCount) {
val timeUs = (i * interval) * 1000L
val bitmap = retriever.getFrameAtTime(timeUs, MediaMetadataRetriever.OPTION_CLOSEST)
val scaledBitmap = bitmap?.let {
Bitmap.createScaledBitmap(it, width / 10, it.height, false)
}
if (scaledBitmap != null) {
thumbnails.add(VideoThumbnail(thumbnails.size, scaledBitmap))
}
}
여기서 주의할 점은
MediaMetadataRetriver를 통해 받아온 duration의 단위는 ms이기 때문에 단위 변환을 해줘야 한다는 것입니다.
단위 변환을 하지 않으면 구한 구간이 아무 의미 없는 정보가 되어버립니다.
썸네일을 가져오기 위해선 getFrameAtTime
함수를 이용합니다.
getFrameAtTime
대다수의 함수에서 파라미터로 option을 입력받습니다.
getFrameAtTime에서 활용되는 option은 4개가 있는데요, 짧은 영어실력으로 번역을 해보면 다음과 같이 해석됩니다.
OPTION_CLOSEST
입력받은 시간(키 프레임일 필요는 없음)에 대해 가장 가까운 시간대 혹은 그에 대응되는 프레임을 반환한다.
OPTION_CLOSEST_SYNC
입력받은 시간에 대해 가장 가까운 시간대 혹은 그에 대응되는 키 프레임을 반환한다.
OPTION_NEXT_SYNC
입력받은 시간 이후에 나오는 첫 번째 키 프레임을 반환한다.
OPTION_PREVIOUS_SYNC
입력받은 시간 이전에 위치한 가장 가까운 키 프레임을 반환한다.
OPTION_NEXT_SYNC와 OPTION_PREVIOUS_SYNC는 정확한 시간에 썸네일을 뽑아내기에는 적합하지 않은 옵션이라고 생각해, OPTION_CLOSEST, OPTION_CLOSEST_SYNC를 직접 비교해서 확인해 봤습니다.
테스트 동영상 파일은 같은 파일을 사용했고, 동영상 파일와 편집 시작시간, 편집 끝나는 시간에 대해 로그를 찍었습니다.
OPTION_CLOSEST
OPTION_CLOSEST_SYNC
수치가 아닌 영상으로 확인해 보겠습니다.
첫 번째 영상이 OPTION_CLOSET을 사용한 편집이고, 두 번째 영상이 OPTION_CLOSET_SYNC를 사용한 편집입니다.
약 1시간 영상에 대해서 2초 차이가 났고, 로그로 실감하기보단 휴대폰을 실제로 보면서 느끼는 체감이 훨씬 컸습니다.
출력되는 썸네일의 이미지도 육안으로 동일한 것을 확인할 수 있었습니다.
썸네일 크기 변경
다음으로 이렇게 뽑은 썸네일들을 내가 원하는 편집기 UI에 크기를 맞춰줘야 합니다.
그렇기에 썸네일을 추출하는 함수에서 편집기 UI에 대응되는 width 값을 제공해야 했습니다.
val widthPixels = binding.recyclerViewVideoThumbnail.measuredWidth
uploadVideoViewModel.getThumbnails(width = widthPixels, path = file.path)
가로길이는 layout의 meauredWidth를 이용해서 측정할 수 있었습니다.
썸네일은 bitmap으로 반환되었고, 크기를 변경해야 했기 때문에 createdaScaledBitmap
을 사용해서 크기를 변경시켜 줬습니다.
val scaledBitmap =
bitmap?.let {
Bitmap.createScaledBitmap(it, width / 10, it.height, false)
}
if (scaledBitmap != null) {
thumbnails.add(VideoThumbnail(thumbnails.size, scaledBitmap))
}
후에 retriever를 release 해줍니다. 하지 않으면 memory leak으로 이어질 수 있기 때문에 관련 리소스를 해제하여 줍니다.
이렇게 선택한 동영상에 대해서 썸네일을 추출하는 과정을 작성해 봤습니다.
생각보다(?) 간단한 코드로 동영상에 대한 썸네일을 추출할 수 있어서 라이브러리에 대한 극찬을 계속했었습니다.
다음글은 영상 편집과정에 대해 포스팅할까 합니다.