motoh's blog

主に趣味の電子工作やプログラミングについて書いていきます

Android/MobileFFmpegによる動画ファイル圧縮

スマホのカメラで撮影した動画ファイルを、スマホ内でFFmpegを使って圧縮できれば便利なアプリが作れそうだと思い、方法を調べました。MobileFFmpeg(Github)がAndroidに対応しており、使い方はWeb上に多数の情報がありますが、いざその通り使ってみると画質の劣化が気になったため、その解決策も含めて解説したいと思います。

動作環境

Android Studio 3.1.4
実機: S7-SH(Android 11)

ソースコード

以下のJavaソースコードは、実行ボタンを押すとスマホ内に保存されている特定の動画ファイルをMobileFFmpegで圧縮し、アプリ固有のフォルダ "Android/data/(パッケージ名)" に保存するという単純なアプリです。圧縮中は、ダイアログに進捗が表示されます。

■AndroidManifest.xml
スマホ内のストレージにアクセスするために以下の権限を追加します。

   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

なお、私のスマホS7-SH(Android 11)だとこれだけでは不十分で、アプリをインストールした後に、スマホの設定⇒プライバシー⇒権限マネージャーで、「ファイルとメディア」へのアクセスをアプリに許可する必要がありました。
以下の記事で解説されているように、アプリ実行時にユーザに許可を求める方法もありますが、上手くいかなかったので諦めてしまいました^^;
Androidの実行時パーミッションチェックを実装する - Qiita


■build.gradle
MobileFFmpegを使うために、dependenciesに以下の行を追加します。

    dependencies {
        implementation 'com.arthenica:mobile-ffmpeg-full-gpl:4.4'
    }

■MainActivity.java
簡単ですが、コメントで説明を記載しています。

importの部分は省略

public class MainActivity extends AppCompatActivity {
    public String path_out;
    public String path_in;
    public String filename;
    private ProgressDialog progressDialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

   }

    public void onButtonExecuteArchive(View view){
        TextView textView = findViewById(R.id.textView);

        filename = "211010_140246.mp4";     //圧縮対象のファイル名

        try {
            //圧縮対象の動画ファイルの絶対パスを取得
            // (/storage/emulated/0/DCIM/100SHARP/211010_140246.mp4のようなパスになる)
            File dcimDir = new File(Environment.getExternalStorageDirectory() + "/" + Environment.DIRECTORY_DCIM + "/100SHARP");
            path_in = dcimDir.getAbsolutePath() + "/" + filename;

            //圧縮後の動画ファイル保存先の絶対パスを取得
            // (/storage/emulated/0/Android/data/(パッケージ名)/211010_140246.mp4のようなパスになる)
            File myDir = new File(Environment.getExternalStorageDirectory() + "/Android/data/com.example.memoriesarchiver");
            path_out = myDir.getAbsolutePath() + "/" + filename;

            textView.setText("FFmpeg execution start.\n");
            textView.append("input : \n" + path_in + "\n");
            textView.append("output : \n" + path_out + "\n");

            //////動画圧縮の進捗を表示するためのprogressDialogを表示する//////
            //progressDialogのMAXを設定するために動画ファイルの情報を取得する(動画の総時間をMAXとする)
            MediaInformation info = FFprobe.getMediaInformation(path_in);

            //progressDialogを表示する
            progressDialog = new ProgressDialog(this);
            progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
            progressDialog.setTitle("FFmpeg progress");
            progressDialog.setMax((int) (Double.parseDouble(info.getDuration()) * 1000));
            progressDialog.setIndeterminate(false);
            progressDialog.setCancelable(false);
            progressDialog.show();

            //////動画圧縮を開始する(非同期で実行されるため、圧縮完了する前に処理が戻り、画面操作可能となる)//////
            long executionId = FFmpeg.executeAsync("-i " + path_in + " -crf 30 -c:v libx264 -c:a aac " + path_out, new ExecuteCallback() {

                @Override
                public void apply(final long executionId, final int returnCode) {
                    TextView textView = findViewById(R.id.textView);
                    if (returnCode == RETURN_CODE_SUCCESS) {
                        progressDialog.dismiss();
                        textView.append("FFmpeg command execution completed successfully.\n");
                    } else if (returnCode == RETURN_CODE_CANCEL) {
                        textView.append("FFmpeg command execution cancelled by user.\n");
                    } else {
                        textView.append("FFmpeg command execution failed.\n");
                    }
                }
            });

            //圧縮の進捗が更新されるたびに呼ばれる処理。ここでprogressDialogの進捗を更新する
            Config.enableStatisticsCallback(new StatisticsCallback() {
                public void apply(Statistics newStatistics) {
                    progressDialog.setProgress(newStatistics.getTime());
                }
            });
        }
        catch(Exception e){
            textView.append("Exception : " + e.toString());
        }
    }
}

スクリーンショット

①アプリ起動したときの画面
 Executeボタンで圧縮を実行

②実行ボタン押下後の画面
 動画の圧縮が始まり、進捗ダイアログが表示される

③圧縮終了後の画面
 圧縮が正常に終了すると"FFmpeg command execution completed successfully.”と表示される

f:id:mzmlab:20211018003719p:plain

圧縮後の画質について

MobileFFmpeg公式のGithubでは以下の使い方が紹介されていますが、このままでは圧縮後の画質の劣化がちょっと気になります。

    int rc = FFmpeg.execute("-i file1.mp4 -c:v mpeg4 file2.mp4");

画質の劣化を抑えるには、コーデックをH.264にすると良いみたいです。以下のように変更します。

    int rc = FFmpeg.execute("-i file1.mp4 -c:v libx264 file2.mp4");

libx264を使うには、gradleで指定するMobileFFmpegパッケージは”full-gpl”を選択する必要があります。(公式Githubの解説で、パッケージ毎の対応ライブラリが表でまとめられています。)

    dependencies {
        implementation 'com.arthenica:mobile-ffmpeg-full-gpl:4.4'
    }

※ “full-gpl”はGPLライセンスなので、ソフトウェアを頒布する場合はソースコードの公開義務が発生します。

また、以下のように-crfで圧縮の品質を変更できます。私の環境では-crf 30だと画質の劣化が見た目にはほとんどわからず、ファイルサイズは3分の1程度に圧縮できました。-crfの数字が小さいほど圧縮率は下がり、画質は良くなります。

    int rc = FFmpeg.execute("-i file1.mp4 -crf 30 -c:v libx264 file2.mp4");