Androidアプリ開発

動画からサムネイルを作成する

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

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

この記事のテーマ


動画ファイルからサムネイル画像を作成する

ポイント

動画ファイルは再生するまで、どのような内容の動画であるかわかりません。
再生する動画を選択する場合、どのような動画であるかサムネイルを表示する手法が一般的です。
今回は、Android アプリで動画ファイルからサムネイル用の画像ファイルを作成する方法を紹介いたします。

動画ファイルをリスト表示する場合、サムネイル用の画像が必要です。

動画からサムネイルを作成する

動画からサムネイルを作成するには、サムネイルユーティリティ(ThumbnailUtils)で動画ファイルから、サムネイル用のBitmapを取得し、画像ファイルとして出力します。
サムネイルを作成する動画ファイルが多い場合、動画はファイルサイズが大きいこともあり、処理に時間がかかりますので、作成済みの動画は再作成しないなどの工夫が必要です。

サムネイルユーティリティを使用して動画からサムネイルを作成する場合、画像の位置を指定できません。

動画ファイルや音楽ファイルの形式を変換するで、別の方法を紹介しています。
動画上でサムネイル画像の位置が指定できるか、できないかの違いがあります。

メディアストアの動画からサムネイル用のBitmapを取得する

アプリで使用する動画ですが、メディアストアでハンドリングする方法が一般的です。
メディアストアはスマホ本体の画像・音声・動画をコンテンツリゾルバを経由してアクセスができます。
下記のサンプリでは、メディアストアで管理している動画をコンテンツリゾルバにクエリを指定して、mp4形式の動画ファイルを登録の降順で取得しています。
また、取得した動画ファイルのリストから、解像度がフルHD(1920×1080px)以下の動画のサムネイルの作成対象としています。
なお、作成したサムネイルは外部共有ストレージのアプリ領域に格納し、作成済みの場合はサムネイルを作成しません。
サムネイルの作成は、
ThumbnailUtilscreateVideoThumbnailをしますが、Android10以降と前では作成するサムネイルのサイズの指定が異なります(MINI_KINDなど、定数指定は非推奨)
サンプルでは、アプリからはサムネイルを画像ファイルとしてハンドリングするのではなく、
Bitmapのまま、メタ情報と合わせて、エンティティ(VideoItem)で管理しています。

    private Map<String, VideoItem>  videoItemMap = new LinkedHashMap<>();
        …
        List<String> remains = new ArrayList<>();
        this.externalStorageReader = new ExternalStorageReader(getApplicationContext());
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.execute(() -> {
            try {
                ContentResolver resolver = context.getContentResolver();
                Cursor cursor = resolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, null, "MIME_TYPE == 'video/mp4'", null, "_ID DESC");
                while (cursor.moveToNext()) {
                    if (Long.parseLong(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.WIDTH))) <= 1920 &&
                            Long.parseLong(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.HEIGHT))) <= 1080) {
                        Uri contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, Long.parseLong(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID))));
                        String file = String.format("%s.jpg", cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.TITLE)));
                        Bitmap bitmap = null;
                        if (externalStorageReader.setFile(file, 2)) {
                            // サムネイルファイルが存在する
                            bitmap = externalStorageReader.ReadFileBitmap();
                        } else {
                            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
                                try {
                                    bitmap = ThumbnailUtils.createVideoThumbnail(new File(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.DATA))), new Size(512, 384), new CancellationSignal());
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            } else {
                                // MediaStore.Images.Thumbnails.MINI_KIND = 1 : 512×384
                                bitmap = ThumbnailUtils.createVideoThumbnail(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.DATA)), 1);
                            }
                            // サムネイルファイルを作成する
                            if (bitmap != null) externalStorageReader.writeFileBitmap(bitmap);
                        }
                        if (bitmap != null) {
                            videoItemMap.put(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.DISPLAY_NAME)),
                                    new VideoItem(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.TITLE)),
                                            Long.parseLong(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.DURATION))),
                                            contentUri, bitmap, cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.DISPLAY_NAME))));
                        }
                        remains.add(file);
                    }
                }
                cursor.close();
                // 不要なサムネイルの削除
                for (String file : externalStorageReader.getFileList(2)) {
                    if (!remains.contains(file) && file.toLowerCase().contains(".jpg")) {
                        externalStorageReader.initializeFile(file, 2);
                    }
                }
            } catch (NumberFormatException ne) {
                ne.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        …

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

◎エンティティ(VideoItem

public class VideoItem {
    public String   title;      // タイトル(xxx)
    public long     duration;   // 再生時間(ms)
    public Uri      videoUri;   // 動画ファイルUri
    public Bitmap   bitmap;     // サムネイル(xxx.jpg)
    public String   source;     // 動画ファイル(xxx.mp4)
    public VideoItem(String title, long duration, Uri videoUri, Bitmap bitmap, String source) {
        this.title  = title;
        this.duration = duration;
        this.videoUri = videoUri;
        this.bitmap = bitmap;
        this.source = source;
    }
}

Bitmapから画像ファイルを出力する

外部共有ストレージのアプリ領域にサムネイル画像を作成するクラスです。
writeFileBitmapは、
ThumbnailUtilscreateVideoThumbnailで取得したBitmapをファイル出力します。
ReadFileBitmapは、作成済みのサムネイルから
Bitmapを取得します。

public class ExternalStorageReader  {
    private final String[]          type = new String[] {
            Environment.DIRECTORY_DOCUMENTS,
            Environment.DIRECTORY_DOWNLOADS,
            Environment.DIRECTORY_PICTURES
    };
    private final File[]            path = new File[type.length];
    private File                    file;

    // コンストラクタ //
    public ExternalStorageReader(Context context) {
        for (int i=0; i< type.length; i++) {
            path[i] = context.getExternalFilesDir(type[i]);
        }
    }

    // ファイル一覧取得 //
    public List<String> getFileList(int type) {
        ArrayList<String> fileList = new ArrayList<>();
        File filePath = new File(path[type].getPath());
        File[] listFiles = filePath.listFiles();
        if (listFiles != null) {
            for (File file : listFiles) {
                if (!file.isDirectory()) fileList.add(file.getName());
            }
        }
        return fileList;
    }
    // ファイル指定 //
    public boolean setFile(String fileName,int type) {
        file = new File(path[type], fileName);
        return file.exists();
    }
    // ファイル初期化 //
    public boolean initializeFile(String fileName, int type) {
        file = new File(path[type], fileName);
        return (!file.exists() || file.delete());
    }
    // ファイル読込 //
    public Bitmap ReadFileBitmap() {
        Bitmap bitmap = null;
        if (isExternalStorageReadable()) {
            try (FileInputStream fileinputStream =
                         new FileInputStream(file)) {
               bitmap = BitmapFactory.decodeStream(fileinputStream);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return bitmap;
    }
    // ファイル書込可能判定 //
    private boolean isExternalStorageWritable() {
        String state = Environment.getExternalStorageState();
        return (Environment.MEDIA_MOUNTED.equals(state));
    }
    // ファイル書込(bitmap) //
    public void writeFileBitmap(Bitmap bitmap) {
        if (isExternalStorageWritable()) {
            try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    // ファイル読込可能判定 //
    private boolean isExternalStorageReadable() {
            String state = Environment.getExternalStorageState();
            return (Environment.MEDIA_MOUNTED.equals(state) ||
                    Environment.MEDIA_MOUNTED_READ_ONLY.equals(state));
    }
}

今回は、ここまでです。

動画からサムネイルを作成しているAndroidアプリです。
動画選択で動画から作成したサムネイルを一覧表示しています。