Androidアプリ開発

動画や音楽のファイル形式を変換する

この記事は約30分で読めます。
記事内に広告が含まれています。
スポンサーリンク

この記事は Android スマホ用のアプリ開発の中で、
今後の開発で再使用性が高いと思われるコーディングをまとめたものです。
Java での開発経験、XML 構文規則、Android のアプリ開発経験がある方を対象としています。
Android のアプリ開発でお役にたててれば、嬉しいです。
(これから Android のアプリ開発や Java での開発を始めたい方への案内は、
記事の最後で紹介します)

この記事のテーマ


 FFmpegを使用して、動画ファイルや音楽ファイルの形式を変換する

Androidのスマホは、MP4形式しか動画ファイルは再生できません。
ビデオカメラで撮影した動画ファイルは、MTS形式が多い。
MTS形式の動画ファイルをAndroidで再生するには、MP4形式に変換する必要があります。

Android13から、AIFF形式(Apple標準のオーディオ形式)の音楽ファイルが再生できなくなっています。
CDの曲をiTunesに取り込んだ音楽ファイルをAndroid13以降で再生するには、MP3形式に変換する必要があります。

FFmpegは、動画ファイルや音声ファイルを記録・変換・再生するためのフリーソフトウェアです。
Androidで対応していない形式のファイルは
FFmpegを使用して、MP4形式やMP3形式に変換すれば、Androidでも再生できるようになります。

FFmpegKitは、Androidで使用できるオープンソフトウェアライブラリです。

ffmpegkitを使用するための準備

ffmpegkitを使用するには、
プロジェクトおよび、モジュールの
build.gradleファイルに定義の追加が必要です。

◎build.gradle(プロジェクト)

:
allprojects {
    repositories {
        mavenCentral()
        :
    }
}
:

◎build.gradle(モジュール)
2024年2月現在の最新バージョンは 6.0.2 です。

dependencies {
    :
    implementation 'com.arthenica:ffmpeg-kit-full:6.0-2'
}

◎ライセンス表記
ffmpegkitは、LGPL v3.0 Licenseです。
アプリで使用する場合、ライセンス表記が必要です。

ライセンス条文の記載がある公式サイトのリンクをアプリに実装しています

動画ファイルの形式を変換する

ffmpegkitは、コマンドライン引数で動画ファイルの変換動作をパラメータで指定します。
フォーマット、コーデック、開始位置や長さ、品質指定などが指定できます。
これらパラメータ指定の組み合わせで、MP4形式変換、サムネイルの作成、動画のトリミングが可能です。

動画再生アプリ(Duel)は、MP4形式以外の動画ファイルも再生できます

メディアストア(動画フォルダ)の動画ファイルにアクセスするには、きめ細かいメディア権限を設定する必要があります。
詳細は、
Android13対応(ファイルのメディア権限)で紹介しています。

MP4形式変換

FFmpegKitは、動画ファイルの形式は拡張子で判断しています。
入力ファイルがMP4形式以外の拡張子で、出力ファイルの拡張子がMP4形式であれば、MP4形式に変換できます。

import com.arthenica.ffmpegkit.FFmpegKit;
import com.arthenica.ffmpegkit.FFmpegKitConfig;
import com.arthenica.ffmpegkit.FFmpegSession;
import com.arthenica.ffmpegkit.ReturnCode;
:
    String[] mimeList = new String[]{"video/mp4", "video/webm", "video/quicktime", "video/mp2ts"};
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    executorService.execute(() -> {
        try {
            ContentResolver resolver = context.getContentResolver();
            Cursor cursor = resolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, null, null, null, "_ID DESC");
            while (cursor.moveToNext()) {
                if (Arrays.asList(mimeList).contains(cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.MIME_TYPE)))) {
                	Uri contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, Long.parseLong(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID))));
                    String path = cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.DATA));
                    String source = cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.DISPLAY_NAME));
                    String ext = source.substring(source.lastIndexOf(".")).toLowerCase();
                    long width = Long.parseLong(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.WIDTH)));
                    long height = Long.parseLong(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.HEIGHT)));
                    // 拡張子がMP4以外はコーディック
                    if (!ext.equals(".mp4") && ext.length() > 0) {
                        // ファイル名からスペース削除と拡張子変更
                    	String file = source.toLowerCase().replace(" ", "").replace(" ", "").replace(ext, ".mp4");
                        path = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + File.separator + file;
                        // mp4ファイル変換
                        String mts = FFmpegKitConfig.getSafParameterForRead(context, contentUri);
                        String mp4 = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + File.separator + file;
                        FFmpegSession session = FFmpegKit.execute(String.format("-i '%s' -qmin %d -qmax %d -acodec libmp3lame -ab 192000 -ar 48000 -s %dx%d '%s'", mts, 16, 20, width ,height , mp4));
                        if (ReturnCode.isSuccess(session.getReturnCode())) {
                        	// 変換成功
                            :
                        } else {
                        	// 変換失敗
                            :
                        }
                    }
                }
            }
            cursor.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    });

コンテンツリゾルバを使用して、メディアストア(動画フォルダ)の動画ファイルのリストを取得しています。
また、MIMEタイプがmp4、webm、quicktime(MOV)、mp2ts(MTS)の動画ファイルか判定しています。
コンテンツリゾルバのクエリ結果より、Uri、パス、ファイル名、拡張子、サイズを取得します。
MP4形式以外の場合、MP4形式に変換します。
FFmpegKitの引数として、入力ファイルパス(-i)、品質指定(-qmin, -qmax)、音声コーディック(-acodec)、音声サンプリングレートレート(-ar)、音声ビットレート(-ab)、サイズ(-s 横*縦)、出力ファイルパスを指定しています。
動画コーディックは、デフォルト(指定なし)を使用します。
変換結果は、
FFmpegSessionで判定します。

エンコード時の出力ファイルの動画品質(q値範囲の最小と最大)を指定できます。
小さい方が高品質ですが、ファイルサイズは大きくなる傾向があります。
引数に
-c:v copyを追加することで動画コーディックをしない指定が可能です。
音声コーディックはMP3エンコーダー、サンプリングレートとビットレートは業界標準値です。

サムネイル画像の作成

動画ファイルの開始位置とフレーム数に1を指定することで、サムネイル画像ファイルを出力できます

import com.arthenica.ffmpegkit.FFmpegKit;
:
    String[] mimeList = new String[]{"video/mp4", "video/webm", "video/quicktime", "video/mp2ts"};
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    executorService.execute(() -> {
        try {
            ContentResolver resolver = context.getContentResolver();
            Cursor cursor = resolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, null, null, null, "_ID DESC");
            while (cursor.moveToNext()) {
                if (Arrays.asList(mimeList).contains(cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.MIME_TYPE)))) {
                    String path = cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.DATA));
                    long width = Long.parseLong(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.WIDTH)));
                    long height = Long.parseLong(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.HEIGHT)));
                    String jpg = String.format("%s.jpg", cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.TITLE)));
                    // サムネイル作成
                    FFmpegKit.execute(String.format("-i '%s' -ss 0 -vframes 1 -s %dx%d '%s'", path, width ,height , context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + File.separator + jpg));
                }
            }
            cursor.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    });

コンテンツリゾルバを使用して、動画フォルダにある動画ファイルのリストを取得しています。
また、MIMEタイプが。mp4、webm、quicktime(MOV)、mp2ts(MTS)を動画ファイルと判定しています。
コンテンツリゾルバのクエリ結果より、Uri、パス、ファイル名、拡張子、サイズを取得します。
FFmpegKitの引数として、入力ファイルパス(-i)、開始位置(-ss)、フレーム数-vframes)、サイズ(-s 横*縦)、出力ファイルパスを指定しています。

動画からサムネイルを作成するで、別の方法を紹介しています。
動画上でサムネイル画像の位置が指定できるか、できないかの違いがあります。

動画のトリミング

動画ファイルの開始位置(秒)と長さ(秒)を指定することで、トリミングした動画ファイルを出力できます。

import com.arthenica.ffmpegkit.FFmpegKit;
import com.arthenica.ffmpegkit.FFmpegKitConfig;
import com.arthenica.ffmpegkit.FFmpegSession;
import com.arthenica.ffmpegkit.ReturnCode;
:
    // 動画トリミング実行
    trimMp4(context, uri, String.format("%s.mp4", title), new long[]{5, 30});
    :

    // 動画トリミング //
    @SuppressLint({"Range", "DefaultLocale"})
    public void trimMp4(Context context, Uri uri, String mp4, long[] values) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.execute(() -> {
            // 内部ストレージにワークファイルを作成する
            File work = new File(context.getFilesDir(), mp4);
            work.delete();
            String in = FFmpegKitConfig.getSafParameterForRead(context, uri);
            String out = work.getAbsolutePath();
            FFmpegSession session = FFmpegKit.execute(String.format("-ss %d -i '%s' -t %d %s'", values[0], in, values[1] - values[0], out));
            if (ReturnCode.isSuccess(session.getReturnCode())) {
                OutputStream outputStream;
                try (FileInputStream fileinputStream = new FileInputStream(work)){
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                        ContentValues content = new ContentValues();
                        content.put(MediaStore.MediaColumns.DISPLAY_NAME, mp4);
                        content.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
                        content.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_MOVIES);
                        outputStream = context.getContentResolver().openOutputStream(context.getContentResolver().insert(MediaStore.Video.Media.getContentUri("external"), content));
                    } else {
                        outputStream = Files.newOutputStream(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES).toString(), mp4).toPath());
                    }
                    if (outputStream != null) {
                        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
                        byte[] buffer = new byte[1024];
                        int len;
                        while ((len = fileinputStream.read(buffer)) != -1) {
                            bufferedOutputStream.write(buffer, 0, len);
                        }
                        bufferedOutputStream.close();
                    }
                    work.delete();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else {
                // トリミング失敗
                :
            }
        });
    }

入力ファイルをトリミングした動画ファイル(ワークファイル)を内部ストレージに出力しています。
ワークファイルをコンテンツリゾルバに登録して、動画フォルダにコピーしています。
FFmpegKitの引数として、開始位置(-ss、入力ファイルパス(-i)、長さ(-t、出力ファイルパスを指定しています。
変換結果は、
FFmpegSessionで判定します。

動画のトリミングが正しく動作しない場合、以下を確認してください。
・開始地点(-ss)は、入力ファイルパス(-i)の前で指定する
・エンコードする(-c copyは指定しない)

音声ファイルの形式を変換する

ffmpegkitは、コマンドライン引数で音声ファイルの変換動作をパラメータで指定します。
フォーマット、コーデックなどが指定できます。
これらパラメータ指定の組み合わせで、MP3形式の音声ファイルに変換が可能です。

音楽再生アプリ(Assistants)は、AIFF形式の音声ファイルも再生できます

メディアストア(音声フォルダ)の音声ファイルにアクセスするには、きめ細かいメディア権限を設定する必要があります。
詳細は、
Android13対応(ファイルのメディア権限)で紹介しています。