Android – Video Transcoding

Transcoding ?

Transcoding 는 영화 파일, 오디오 파일, 문자 인코딩 등 하나에서 다른 하나로의 디지털 변환 작업을 의미한다. 보통 동영상에서는 동영상의 비트열을 변환하는 뜻으로 사용된다.

기존 안드로이드에서는 FFmpeg 등을 사용했었는데, 아래와 같은 문제가 있었다.

  • mp4를 인코딩 하기 위해 libx264를 컴파일 하면 GPL에 걸림
  • mp4 인코딩에 사용되는 H.264 컨테이너를 동봉할 경우 로열티를 지급해야 함.
  • 애초 NDK를 사용한다는 것 자체가…

하지만 API 18 (4.3부터) FFmpeg 를 사용하지 않고도 Video Transcoding가 가능하게 되었다.

바로, MediaMuxer, MediaCodec, MediaExtractor 등이다.

그리고 이런 API 등을 쉽게 사용할 수 있게 만들어진 라이브러리가 android-transcoder 이다.

README에 잘 나왔긴 했지만 삽질하면서 얻은 기록을 이쪽에 정리하는 것이 좋을거라 생각해서 쓰게 되었다.

생각해야 할 것

앱 단에서 신경써야 할 것은 어느정도 있다.

인코딩은 많은 시간을 소비한다.

그래서 해당 라이브러리도 Callback 형태로 결과값을 넘겨주고 있다. 내 경우에는 영상 업로드 프로세스 전에 타야 되었기 때문에 어쩔 수 없이 UI Blocking를 해야 할 필요가 있었다.

그런고로 ProgressDialog 도입.

인수로 FileDescriptor 등을 받는다.

 private FileDescriptor getFileDescriptor() {
        File file = new File(incomePath);
        try {
            FileInputStream stream = new FileInputStream(file);
            return stream.getFD();
        } catch (IOException e) {
            return null;
        }
 }

내 경우에는 업로드 전에 시행했기 때문에 로컬 경로를 가지고 있었고, 이에 바로 FileDescriptor 로 변환이 가능했다.

16:9 영상 이외엔 받지 않는 것 같다.

16:9 이외의 영상 (960×540, iOS가 보통 이걸로 인코딩 한다) 는 다른 안드로이드 기기에서 작동을 보증하지 않는다.

안드로이드 기기들은 CTS (Compatibility Test Suite) 를 통과해야 하는데,  이 CTS는 1280×720 해상도로 지원하는 미디어 포맷 문서 에 있는 포맷들을 테스트한다.

즉 이외 해상도는 지원해야 될 의무가 없으며 이는 하드웨어 코덱의 부재 및 제한으로 이어질 수 있다.

이를 위해서는 자르거나 합쳐서 16:9로 만들어야 하는데, 이를 아직 android-transcoder 에선 지원하지 않는다.

사용할 경로와 결과 경로가 달라야 한다.

어떻게 보면, 이 문제로 2시간 이상 삽질했다.

즉, 결과 경로가 다른 파일로 저장되어야 한다. 만일 같은 파일로 할 경우에는 해상도만 변경된 채 비트레이트 등 정보가 바뀌지 않아 용량이 줄어들지 않는다.

그래서…

나중에도 활용할 수 있도록 만든 클래스가 아래와 같다.

import android.app.Activity;
import android.app.ProgressDialog;
import android.graphics.Bitmap;
import android.media.MediaFormat;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.util.Log;

import net.ypresto.androidtranscoder.MediaTranscoder;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

/**
 * VideoTranscodingUtils
 * Created by Pyxis on 2017-07-11.
 */
public class VideoTranscodingUtils implements MediaTranscoder.Listener {
    private Activity activity;
    private ProgressDialog progressDialog;
    private String incomePath;
    private String outcomePath;
    private OnResultListener listener;

    public static final int TRANSCODING_SUCCESS = 1;
    public static final int TRANSCODING_FAILED = 2;

    public VideoTranscodingUtils(Activity activity, String path) {
        this.activity = activity;
        this.incomePath = path;
        File folder = new File(Environment.getExternalStorageDirectory(), "encoding_output/");
        folder.mkdir();

        try {
            this.outcomePath = File.createTempFile("encoding", ".mp4", folder).getAbsolutePath();
        } catch (IOException e) {
            Log.d(VideoTranscodingUtils.class.getSimpleName(), "encoding failed");
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            progressDialog = new ProgressDialog(activity, android.R.style.Theme_Material_Light_Dialog);
        } else {
            progressDialog = new ProgressDialog(activity, android.R.style.Theme_Holo_Light_Dialog);
        }
        progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
        progressDialog.setMessage("Transcoding...");
        progressDialog.setCancelable(false);
    }

    public void transcode(final OnResultListener listener) {
        this.listener = listener;
        FileDescriptor descriptor = getFileDescriptor();

        MediaTranscoder.getInstance().transcodeVideo(descriptor, outcomePath,
                MediaFormatStrategyPresets.createAndroid720pStrategy(8000 * 1000, 128 * 1000, 1), this);
        
        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                progressDialog.show();
            }
        });

    }

    private FileDescriptor getFileDescriptor() {
        File file = new File(incomePath);
        try {
            FileInputStream stream = new FileInputStream(file);
            return stream.getFD();
        } catch (IOException e) {
            return null;
        }
    }

    private void dismissDialog() {
        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                try {
                    if (activity.isDestroyed() && !progressDialog.isShowing())
                        return;
                    progressDialog.dismiss();
                } catch (Exception e) {
                }
            }
        });
    }

    @Override
    public void onTranscodeProgress(final double progress) {
    }

    @Override
    public void onTranscodeCompleted() {
        dismissDialog();
        listener.onResult(TRANSCODING_SUCCESS, outcomePath);
    }

    @Override
    public void onTranscodeCanceled() {
        dismissDialog();
        listener.onResult(TRANSCODING_FAILED, incomePath);
    }

    @Override
    public void onTranscodeFailed(Exception exception) {
        dismissDialog();
        listener.onResult(TRANSCODING_FAILED, incomePath);
    }

    public interface OnResultListener {
        void onResult(int resultCode, String outPath);
    }
}

비디오 비트레이트, 오디오 비트레이트, 오디오 채널을 변경하려면 MediaFormatStrategyPresets.createAndroid720pStrategy(8000 * 1000, 128 * 1000, 1) 의 파라미터 값을 조정하면 된다.  여기서는 비디오 비트레이트 8mbps, 오디오 128kbps, 1번 오디오 채널을 사용했고, 바꿀 해상도는 720p (1280×720) 이다.

결론

깔끔하게 인코딩에 성공했다. 확실히 FFmpeg를 사용하는 것 보다는 위험 부담도 적고, 매우 쉽게 되어있었다.