필자가 업무를 진행하면서 미디어 파일을 공유하는 작업을 진행한 적이 있는데,
동영상 파일의 경우 높은 용량의 파일을 공유하려는 케이스가 상당히 많았다.
그래서 동영상 용량 자체를 줄인 후에, 새롭게 인코딩 된 영상 파일을 공유하면 좋겠다.라고 생각하여 찾아보다 발견했던 것이 ffmpeg이다.
이 ffmpeg를 사용하는 방법에 대해서 찾아봤는데,
ffmpeg를 사용하는 방법에 대해서는 구글링하면 쉽게 찾아서 적용할 수 있었으나, Android Studio 내부에서 사용하는 방법은 그다지 많은 정보가 있지 않았다.
그래도 발견한 몇 가지 방법에서 가장 쉽게 적용하고 사용할 수 있는 방법에 대해서 설명하고자 한다.
우선,
ffmpeg란 무엇인가?
FFmpeg은 디지털 음성 스트림과 영상 스트림에 대해서 다양한 종류의 형태로 기록하고 변환하는 컴퓨터 프로그램이다.
라고 한다.
"프로그램" 이기 때문에, ffmpeg를 "다운로드" 받은 후에, 커맨드로 명령어를 입력하게 되면 쉽게 사용이 가능하다.
하지만,
우리는 그 프로그램을 안드로이드 코드 내에서 사용하고자 한다.
그래서 안드로이드 내부에서 사용하는 방법을 찾아보다 다음과 같은 Github를 발견하였다.
README의 가장 첫 번째 내용부터 아주 마음에 들었다.
정말 다양한 플랫폼에 대해서 사용할 수 있도록 만들어둔 FFmpegKit이다.
해당 문서를 확인해보면, 사용 방법에 대하여 아주 자세하게 설명이 되어있다.
하지만 해당 문서를 정독하기에 앞서, 일단 적용해 보고 정상적으로 동작하는지 확인하는 것이 먼저인 필자와 같은 사람이 있을 것이다.
따라서, 자세한 설명은 문서를 참고하면 된다고 미리 말을 하고 아주 간단하게 사용 방법에 대해서 작성한다.
val inputFfmpegCommand = "-y -i $inputFilePath -r 20 $outPutFilePath"
FFmpegKit.executeAsync(inputFfmpegCommand) { session ->
if (ReturnCode.isSuccess(session.returnCode)) {
sharedLowQualityVideo(outputFile)
}
}
중괄호를 포함하여 단 6줄이면 손쉽게 동영상 파일을 저용량으로 인코딩하여 사용이 가능하다.
물론,
sharedLowQualityVideo를 포함한 앞 뒤로 추가적인 작업은 반드시 필요하지만 해당 library를 사용하는 부분은 저 부분이 끝이며 가장 중요한 부분이라고 볼 수 있다.
각 라인을 해석하자면 다음과 같다.
inputFfmpegCommand : inputFilePath 에 있는 video File을 주어진 설정에 맞춰 인코딩하여 outPutFilePath에 저장한다.라는 의미의 Command
FFmpegKit.executeAsync(~~~) : 입력한 Command를 ffmpeg에서 수행하여 결과를 반환한다.
if 문 : 결과가 성공적일 때, sharedLowQualityVideo 함수를 실행한다.
이 부분만 설명하고, 적용해보도록 하자.
전체 코드가 필요하면 깃허브에 들어가서 확인하면 되고, 적용에 필요한 부분만 기술하겠다.
동영상을 인코딩해서 공유하는 로직은 다음과 같은 순서로 진행된다.
- 동영상을 선택하고
- 동영상을 원하는 조건으로 인코딩하고
- 동영상을 저장하고
- 동영상을 공유한다.
이 4가지 순서로 설명하도록 하겠다.
처음으로,
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
File을 변경하여 저장하고, 공유하는 부분이기 때문에 다음과 같은 Permission이 필요하다.
해당 permission을 manifest 파일에 추가해 준다.
권한을 추가해 주었으면, 동영상을 선택해야 한다.
val contract = ActivityResultContracts.GetContent()
val callback = ActivityResultCallback<Uri?> { uri ->
uri?.let {
val inputVideoPath = getRealPathFromURI(uri, this)
inputVideoPath?.let {
this.executeCommand(inputVideoPath)
}
}
}
val launcher = registerForActivityResult(contract, callback)
...
launcher.launch("video/*")
물론, 파일 접근 권한에 대한 Permission을 직접 받아야 하지만, 해당 예제에서는 받았다고 가정하고 처리하였다.
filePicker를 열고, 선택된 동영상 파일에 대한 정보를 그 결과로 받아야 한다.
따라서, ActivityResultCallback을 사용하여 결과를 받을 수 있도록 하였고,
video/* 로 input을 제한하여 동영상 파일만 필터링해서 볼 수 있도록 하였다.
위와 같은 코드를 통해 실행시키면 다음과 같은 결과가 나오게 된다.
동영상 파일을 애뮬레이터에 넣기 귀찮아서, 개인 폰으로 캡처했기 때문에 영상에 대한 정보는 가렸다.
이와 같은 BottomSheet에서 동영상 정보를 클릭하게 되면, 동영상에 대한 Uri 데이터가 callback으로 들어오게 된다.
따라서,
val callback = ActivityResultCallback<Uri?> { uri ->
uri?.let {
val inputVideoPath = getRealPathFromURI(uri, this)
inputVideoPath?.let {
this.executeCommand(inputVideoPath)
}
}
}
이 부분이 실행되게 된다.
getRealPathFromURI 부분은 Uri 데이터를 기반으로 File에 대한 경로를 가져오는 부분인데, 해당 설명과는 무관하기 때문에 넘어가도록 하겠다.
데이터가 제대로 들어가 있으면 executeCommand라는 함수를 호출하게 되는데, 이 부분이 2번째 순서인 동영상을 원하는 조건으로 인코딩하고 3번째 순서인 동영상을 저장하는 부분이다.
fun Context.executeCommand(inputFilePath: String) {
val suffixData = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
val outputFile = File(getExternalFilesDir(null), "compose-sample-output-$suffixData.mp4")
val outPutFilePath = outputFile.absolutePath
val inputFfmpegCommand = "-y -i $inputFilePath -r 20 $outPutFilePath"
FFmpegKit.executeAsync(inputFfmpegCommand) { session ->
if (ReturnCode.isSuccess(session.returnCode)) {
sharedLowQualityVideo(outputFile)
}
}
}
코드를 보면 알 수 있듯이, 이 부분이 ffmpeg를 사용하는 유일한 부분이며 가장 중요한 부분이다.
outputFile에 대한 이름을 중복이 아닌 것으로 작성하기 위하여 현재 시간에 대한 정보를 넣어주도록 하였다.
inputFfmpegCommand와 이하의 구절은 위에 설명을 하였으므로 간단하게 설명하고 넘어가도록 한다.
-y : 이미 있으면 덮어쓰기
-i $input : 입력 파일의 경로 지정
-r xx : Frame rate를 xx로 설정
$output : 출력 파일의 경로
하지만, 필자가 사용한 명령어는 정말로 간단하게 작성한 것이므로 명령어에 대해서는 검색해 보는 것이 좋을 것이다.
공식 문서를 확인하면 정말로 많은 정보들이 나오는데, 너무 정보가 많아서 어떤 것을 사용해야 하는지 명확하게 확인하기 쉽지 않다.
그렇기 때문에, 간단하게 구글링을 해서 옵션을 찾아보거나 ffmpeg를 설치한 후에 -help를 통해 키워드를 알아보면 될 것이다.
executeAsync의 결과로 성공이 떨어졌다는 것은,
outPutFile이 정상적으로 만들어졌다는 것이며, outPutFilePath에 원하는 조건으로 인코딩 된 동영상 파일이 들어있다는 것이다.
그렇다면 마지막으로 해당 동영상을 공유하도록 해보자.
위의 코드에서 성공적으로 인코딩이 된다면 sharedLowQualityVideo 함수가 실행되게 되는데, 해당 함수는 다음과 같다.
fun Context.sharedLowQualityVideo(file: File) {
val videoUri = FileProvider.getUriForFile(this, "com.example.composesample", file)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "video/mp4"
putExtra(Intent.EXTRA_STREAM, videoUri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(shareIntent)
}
다른 앱으로 File을 전달하기 위해서는 Uri 정보를 전달해야 하고,
File 정보에서 Uri를 가져오기 위해서는 FileProvider를 이용해야 한다.
해당 함수를 통해 File에서 Uri 정보를 가져온 다음, ACTION_SEND라는 action을 가지고 있는 Intent를 실행시켜 주면 된다.
이때,
전달하는 정보가 Video Type의 Uri이기 때문에 type에 정확히 타입을 명시해 주고,
이미지, 비디오, 오디오 등 미디어 파일을 공유할 수 있는 EXTRA_STREAM을 통해 videoUri를 넣어주도록 한다.
이와 같이 선언하게 되면 정상적인 로직이라면 공유가 가능한 앱 목록이 나올 것이고, 클릭을 통해 해당 앱에 인코딩 된 동영상을 공유할 수 있게 된다.
하지만,
정상적인 로직이라면 동작하겠지만 아직 위의 로직으로는 정상적으로 동작하지 않을 것이다.
Exception thrown inside session complete callback.java.lang.IllegalArgumentException: Couldn't find meta-data for provider with authority com.example.composesample
meta-data provider를 찾을 수 없다는 오류가 발생한다.
FileProvider를 사용하여 공유할 때 권한이 없어서 발생하는 에러코드이다.
FileProvider가 파일에 대한 접근 권한을 부여하는데 별도의 경로를 지정해 주어야 지정된 경로가 허용되어 접근이 가능해진다.
FileProvider에 대한 것은 공식 문서에서 정확히 확인하길 바란다.
필요한 FileProvider의 경로를 설정해 주기 위해서는 2가지 작업이 필요하다.
첫 번째로, Manifest에 Provider를 선언해주어야 한다.
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.composesample"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
여기서 authorities에는 자신의 패키지를 넣어주어야 한다.
다음으로는, meta-data의 resource에 들어가는,
허용할 경로에 대해서 작성해주어야 한다.
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external"
path="." />
<external-files-path
name="external_files"
path="." />
<cache-path
name="cache"
path="." />
<external-cache-path
name="external_cache"
path="." />
<files-path
name="files"
path="." />
</paths>
위에 선언한 경로에 대해서 간단하게 설명하면 다음과 같다.
external-path : 외부 저장소의 루트 디렉터리에 대한 액세스를 허용
external-files-path : 외부 저장소의 getExternalFilesDir() 메서드로 얻은 디렉터리에 대한 액세스를 허용
files-path : 앱의 내부 파일 디렉터리에 대한 액세스를 허용
Cache라고 작성된 부분은 external 설명을 cache로 변경하면 된다.
이렇게까지 선언한 후에, 다시 위의 코드를 수행하게 되면 정상적으로 용량이 조절된 동영상이 공유되게 된다.
이렇게 ffmpeg를 사용한 라이브러리 ffmpeg-kit를 사용하여 동영상을 저 용량으로 인코딩하고 공유하는 로직을 구현해 보았다.
ffmpeg를 설치하여 사용해 보고, 해당 코드를 실행해 보면 알 수 있겠지만
모바일 환경에서 영상을 인코딩하는 작업과 pc에서 인코딩하는 작업은 속도 차이가 어마어마하게 발생한다.
pc에서는 영상 플레이 시간의 1/7 정도의 시간으로 인코딩이 가능하다고 하면, 모바일에서는 거의 1:1 비율로 시간이 걸리는 경우가 많았다.
하지만, pc에서는 프로그램을 설치해야 한다는 것과, 설치 없이 모바일에서 조금 기다려서 인코딩하여 사용할 수 있다는 것은 큰 메리트가 아닐까 생각한다.
물론, 실제 서비스에서 사용하려면 다양한 부분을 커스텀하거나, 여러 가지 방법을 사용하여 이 시간을 줄이거나 해야 할 것 같지만 말이다.
해당 게시글에 사용한 예제는 Github에 올려두었다.
https://github.com/HeeGyeong/ComposeSample
'Android > Utility' 카테고리의 다른 글
[Android] 정책 변경 후 구글 플레이스토어 개발자 계정 생성부터 신규 앱 배포까지 과정 정리 - 2주간 테스터 20명 유지하기 (24) | 2024.03.16 |
---|---|
[Android] 음성 녹음을 하고, 저장해보자. (2) | 2024.01.31 |
[Android] Notification UI에 대한 몇 가지 변경 방법 (0) | 2024.01.10 |
[Git] Git에서 Head에 잘못 커밋했을 때 커밋 가져오는 방법 (2) | 2023.12.27 |
[Android] RecyclerView Drag and Drop (1) | 2022.11.12 |