Use MediaCodec in NDK

이 글은 아래 글의 후속이다.

Android MediaCodec, MediaMuxer 살펴보기

작업을 하다 보니까, 하위 버전에서 오래 걸리는 문제가 있어 고민하던 차에 ‘ndk를 이용해 cpp로 작업하면 좀 더 빠르지 않을까?’ 하는 생각이 있었다.

일단, 결론은 다음과 같다.

  • 데모용 코드로 작성했을 때 성능은 12% 정도 이득을 보임
  • API 26 이상부터 지원 가능

이러한 이유 때문에 실제로 도입은 못했지만, 그래도 어느정도 매칭은 가능해서 사용이 가능했었고, 몇 가지 겪었던 문제를 소개하고자 한다.

MediaExtractor – setDataSource 에서 FileDescriptor

MediaExtractor에 설정할 때, FileDescriptor 로 통해 설정하는 경우가 있다.

먼저, FileDescriptor를 파라미터에 선언한다.

external fun convert(input: FileDescriptor, inLength: Long, output: FileDescriptor, outLength: Long)

이에 대한 JNI Header는 다음과 같다.

extern "C"
JNIEXPORT jint JNICALL
Java_com_github_windsekirun_***_***_convert(JNIEnv *env, jobject thiz,
                                           jobject input,
                                           jlong inLength,
                                           jobject output,
                                           jlong outLength)

FileDescriptor 는 jobject 로 매칭되는데, 이를 FileDescriptor (정확히는 FileDescriptor.descriptor) 로 변환하려면 아래와 같은 코드를 사용한다.

static int jniGetFDFromFileDescriptor(JNIEnv * env, jobject fileDescriptor) {
   jint fd = -1;
   jclass fdClass = env->FindClass("java/io/FileDescriptor");

   if (fdClass != NULL) {
       jfieldID fdClassDescriptorFieldID = env->GetFieldID(fdClass, "descriptor", "I");
       if (fdClassDescriptorFieldID != NULL && fileDescriptor != NULL) {
           fd = env->GetIntField(fileDescriptor, fdClassDescriptorFieldID);
      }
  }

   return fd;
}

마지막으로, 얻은 FileDescriptor는 아래와 같이 설정한다.

AMediaExtractor *extractor = AMediaExtractor_new();
media_status_t amresult = AMediaExtractor_setDataSourceFd(extractor, inputFd, 0, inLength);
if (amresult != AMEDIA_OK) {
   LOGE("Error setting extractor data source, err %d", amresult);
   return -1;
}

여기서 media_status_t 는 NdkMediaError.h 에 선언되어 있다. (https://cs.android.com/android/platform/superproject/+/master:frameworks/av/media/ndk/include/media/NdkMediaError.h;l=43?q=NdkMediaError.h)

MediaFormat 에서 KEY_COLOR_FORMAT 설정

지난 글에서는 setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface) 로 설정했었는데, NDK에서는 아래와 같이 사용한다.

AMediaFormat *outputFormat = AMediaFormat_new();
AMediaFormat_setInt32(outputFormat, AMEDIAFORMAT_KEY_COLOR_FORMAT, 2130708361);

여기서 2130708361MediaCodecInfo.CodecCapabilities 에 선언되어 있는 COLOR_FormatSurface 의 값이다. (https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/MediaCodecInfo.java;l=218?q=CodecCapabilities)

Decoder용 MediaCodec에서 Surface 객체 관련

이 것이 도입을 하지 못하게 했었던 ‘API 26’ 문제 중 하나인데, surface = encoder.createInputSurface() 에 대응되는 아래 코드가 API 26부터 사용이 가능했다.

ANativeWindow *surface;
AMediaCodec_createInputSurface(encoder, &surface);
media_status_t decoderConfigure = AMediaCodec_configure(decoder, inFormat, surface, nullptr, 0);

Backport 되어 있는 것이 있는지 찾지는 못했지만, 위와 같이 사용할 수 있다.

Encoder에 SignalEndOfInputStream 보내기

encoder.signalEndOfInputStream() 에 대응되는 코드인 AMediaCodec_signalEndOfInputStream(encoder); 가 API 26부터 사용이 가능했다.

설명상으로는 Equivalent to submitting an empty buffer with AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM set 로 되어있어, encoder의 inputBuffer에 AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM 를 설정하면 될 것 같아서 시도해 보았지만, sf MediaCodecError -38 이라는 오류가 나오는 것 같았다.

이 것도 위와 마찬가지로 적절한 해결 방법을 찾지 못했다.

레퍼런스

‘Could not start ndk-bundle/toolchains/mips64el…’ 문제 해결 방법

도입

최근 Android SDK Manager 내부에 있는 ndk를 설치하면서, 이전에 realm 으로 만든 프로젝트 전부가 빌드가 안되는 현상이 있었다. NDK 를 설치하지 않았을 때에는 문제가 없었는데, NDK를 설치하자마자 문제가 발생했다.

빌드 오류시 뜨는 로그는 다음과 같다.

Caused by: org.gradle.process.internal.ExecException: A problem occurred starting process 'command '/home/pyxis/Android/Sdk/ndk-bundle/toolchains/mips64el-linux-android-4.9/prebuilt/linux-x86_64/bin/mips64el-linux-android-strip''
    at org.gradle.process.internal.DefaultExecHandle.execExceptionFor(DefaultExecHandle.java:220)
    at org.gradle.process.internal.DefaultExecHandle.setEndStateInfo(DefaultExecHandle.java:204)
    at org.gradle.process.internal.DefaultExecHandle.failed(DefaultExecHandle.java:340)
    at org.gradle.process.internal.ExecHandleRunner.run(ExecHandleRunner.java:86)
    at org.gradle.internal.operations.BuildOperationIdentifierPreservingRunnable.run(BuildOperationIdentifierPreservingRunnable.java:39)
    ... 6 more
Caused by: net.rubygrapefruit.platform.NativeException: Could not start '/home/pyxis/Android/Sdk/ndk-bundle/toolchains/mips64el-linux-android-4.9/prebuilt/linux-x86_64/bin/mips64el-linux-android-strip'
    at net.rubygrapefruit.platform.internal.DefaultProcessLauncher.start(DefaultProcessLauncher.java:27)
    at net.rubygrapefruit.platform.internal.WrapperProcessLauncher.start(WrapperProcessLauncher.java:36)
    at org.gradle.process.internal.ExecHandleRunner.run(ExecHandleRunner.java:68)
    ... 7 more
Caused by: java.io.IOException: Cannot run program "/home/pyxis/Android/Sdk/ndk-bundle/toolchains/mips64el-linux-android-4.9/prebuilt/linux-x86_64/bin/mips64el-linux-android-strip" (in directory "/home/pyxis/StudioProjects/Richware-KopasUser/app"): error=2, 그런 파일이나 디렉터리가 없습니다
    at java.lang.ProcessBuilder.start(ProcessBuilder.java:1048)
    at net.rubygrapefruit.platform.internal.DefaultProcessLauncher.start(DefaultProcessLauncher.java:25)
    ... 9 more
Caused by: java.io.IOException: error=2, 그런 파일이나 디렉터리가 없습니다
    at java.lang.UNIXProcess.forkAndExec(Native Method)
    at java.lang.UNIXProcess.<init>(UNIXProcess.java:247)
    at java.lang.ProcessImpl.start(ProcessImpl.java:134)
at java.lang.ProcessBuilder.start(ProcessBuilder.java:1029)

이 문제는 ndk r17 에서부터 ARMv5 (armeabi), MIPS, and MIPS64 에 대한 지원이 삭제되었기 때문이다. 관련 Changelog 는 https://github.com/android-ndk/ndk/wiki/Changelog-r17 로, 이 들 abi 로 빌드를 하는 순간 에러를 내보낸다는 것이다.

해결법

NDK Archives (https://developer.android.com/ndk/downloads/older_releases?hl=ko) 에서 자신의 OS에 맞는 r16 을 다운받은 다음, toolchain 폴더의 mips 폴더 두개를 복사하여 기존의 ndk/toolchain 폴더에 붙여넣으면 된다.

위 사진에서 4번째, 5번째 폴더가 복사할 폴더들이다.