Androidアプリ開発

Exoplayer スライドバーの操作で
再生位置の静止画をプレビュー表示する

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

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

この記事のテーマ


スライドバーの操作で再生位置の静止画をプレビュー表示する

ポイント

Youtubeの動画再生ではスライドバーをドラッグすると再生位置の静止画がプレビュー表示します。
動画の再生位置を時間でなく、シーン(静止画)で指定できるようになります。
今回は、Exoplayerの動画再生でスライドバーで再生位置の静止画をプレビュー表示する方法を紹介します。

ExoPlayerで動画を再生する

Exoplayerにはスライドバーの操作で静止画をプレビュー表示する機能がありません。
FFmpegで動画から静止画を生成、PreviewSeekBarで静止画をスライドバーの位置に表示します。

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

PreviewSeekBar

PreviewSeekBar

PreviewSeekBarはSeekBarの操作に連動して画像を表示させるライブラリです。
PreviewSeekBarを使用するには、モジュールのbuild.gradleに追加が必要です。
Media3版のExoplayerを使用するため、PreviewSeekBarはMedia3版を追加します。

build.gradle(モジュール)

dependencies {
    implementation 'com.github.rubensousa:previewseekbar-media3:1.1.1.0'
    :
}

ExoplayerのコントローラUIを作成します。
スライドバー(media3.ui.DefaultTimeBar)をmedia3.PreviewTimeBarに変更します。
画像を表示させるフレーム(FrameLayout)とImageViewを追加します。

controller.xml

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <LinearLayout
        android:id="@+id/controller"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="4dp"
        android:layout_marginTop="8dp"
        android:layout_alignParentTop="true"
        android:gravity="center_horizontal"
        android:orientation="vertical"
        tools:ignore="UselessParent">
        <com.github.rubensousa.previewseekbar.media3.PreviewTimeBar
            android:id="@+id/exo_progress"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:previewEnabled="false"
            app:previewAnimationEnabled="true"
            app:previewFrameLayout="@id/previewFrameLayout"/>
        :
        <FrameLayout
            android:id="@+id/previewFrameLayout"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="start"
            android:visibility="invisible"
            tools:visibility="visible">
            <androidx.cardview.widget.CardView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:cardCornerRadius="8dp"
                app:cardElevation="0dp">
                <ImageView
                    android:id="@+id/imageView"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    tools:ignore="ContentDescription" />
            </androidx.cardview.widget.CardView>
        </FrameLayout>
    </LinearLayout>
</RelativeLayout>

スライドバーに画像を表示させるフレーム(previewFrameLayout)を指定します。
静止画を角丸表示するためにCardViewを使用します。

Exoplayerの動画再生UIを作成します。
動画再生ビュー(media3.ui.PlayerView)に作成したコントローラUIを指定します。

videoplayer.xml

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <RelativeLayout
        android:id="@+id/screen"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true">
        <androidx.cardview.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:cardCornerRadius="4dp"
            app:cardElevation="0dp">
            <androidx.media3.ui.PlayerView
                android:id="@+id/video"
                android:layout_width="0dp"
                android:layout_height="0dp"
                app:resize_mode="fixed_height"
                app:show_timeout="0"
                app:controller_layout_id="@layout/controller"/>
        </androidx.cardview.widget.CardView>
   </RelativeLayout>
</RelativeLayout>

再生位置の静止画をプレビュー表示

ダイアログ画面で動画を再生します。
動画再生に必要な情報(動画ファイルのUri、ファイルパス、解像度)はBundleで引き継ぎます。
動画の解像度を取得する必要がある場合はMediaMetadataRetrieverを使用します。

動画ファイルからFFmpegKitを使用して、1秒毎のサムネイル作成します。
プレビュー表示は動画の解像度の1/5サイズとします。
サムネイル作成の作成に少し時間がかかるので、UIスレッド外で実行します。
サムネイル作成が完了するまでは、PreviewTimeBarの画像表示を無効にします。
PreviewLoaderでスライドバーの位置で作成したサムネイルをImageViewに読み込むようにします。

public class VideoPlayer extends DialogFragment {
    private Context                 context;
    private ExoPlayer               exoPlayer;
    private Dialog                  dialog;
    private com.github.rubensousa.previewseekbar.media3.PreviewTimeBar
                                    previewTimeBar;
    :
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        context = requireActivity();
        dialog = new Dialog(requireActivity()) {
            @Override public void cancel() {
                dismiss();
            }};
        dialog.setContentView(R.layout.videoplayer);
        dialog.setCanceledOnTouchOutside(true);
        Uri uri = Uri.parse(requireArguments().getString("URI", ""));
        String path = requireArguments().getString("PATH", "");
        WIDTH = requireArguments().getFloat("WIDTH", 1920);
        HEIGHT = requireArguments().getFloat("HEIGHT", 1080);
        RelativeLayout relativeLayout1 = dialog.findViewById(R.id.main);
        RelativeLayout relativeLayout2 = dialog.findViewById(R.id.screen);
        PlayerView playerView = dialog.findViewById(R.id.video);
        LinearLayout controller = playerView.findViewById(R.id.controller);
        previewTimeBar = playerView.findViewById(R.id.exo_progress);
        ImageView imageView = playerView.findViewById(R.id.imageView);
        FrameLayout frameLayout = playerView.findViewById(R.id.previewFrameLayout);
        ViewGroup.LayoutParams layoutParams = frameLayout.getLayoutParams();
        layoutParams.width = (int) (WIDTH /5);
        layoutParams.height = (int) (HEIGHT /5);
        frameLayout.setLayoutParams(layoutParams);
        ExternalStorageReader externalStorageReader = new ExternalStorageReader(context);
        PreviewLoader imagePreviewLoader = (currentPosition, max) -> {
            String file = String.format(Locale.getDefault(), "thumnail%04d.jpg", (int)(currentPosition /1000));
            if (externalStorageReader.setFile(file, 1)) {
                imageView.setImageBitmap(externalStorageReader.readFileBitmap());
            }
        };
        previewTimeBar.setPreviewLoader(imagePreviewLoader);
        previewTimeBar.addOn(new PreviewBar.OnScrubListener() {
            @Override
            public void onScrubStart(PreviewBar previewBar) {
                :
            }
            @Override
            public void onScrubMove(PreviewBar previewBar, int progress, boolean fromUser) {
                :
            }
            @Override
            public void onScrubStop(PreviewBar previewBar) {
                :
            }
        });
        previewTimeBar.addOnPreviewVisibilityListener((previewBar, isPreviewShowing) -> {
                :
        });
        // サムネイル作成
        thumnail(path);
        // 画面レイアウト調整 
        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        int dialogWidth = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE ? displayMetrics.widthPixels : displayMetrics.heightPixels;
        int dialogHeight = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE ? displayMetrics.heightPixels : displayMetrics.widthPixels;
        int videoHeight = dialogHeight;
        int videoWidth = (int) (videoHeight * (WIDTH > HEIGHT? WIDTH / HEIGHT : 1));
        int height = videoHeight;
        int width = videoWidth;
        // ダイアログ幅最大化
        WindowManager.LayoutParams layoutParams1 = Objects.requireNonNull(dialog.getWindow()).getAttributes();
        layoutParams1.width = dialogWidth;
        dialog.getWindow().setAttributes(layoutParams1);
        // ダイアログ調整
        layoutParams1 = dialog.getWindow().getAttributes();
        layoutParams1.width = width;
        layoutParams1.height = height;
        dialog.getWindow().setAttributes(layoutParams1);
        // Main調整
        ViewGroup.LayoutParams params = relativeLayout1.getLayoutParams();
        params.width = width;
        params.height = height;
        relativeLayout1.setLayoutParams(params);
        // Screen調整
        params = relativeLayout2.getLayoutParams();
        params.width = videoWidth;
        params.height = videoHeight;
        relativeLayout2.setLayoutParams(params);
        // PlayerView調整
        params = playerView.getLayoutParams();
        params.width = videoWidth;
        params.height = videoHeight;
        playerView.setLayoutParams(params);
        // Controller調整
        params = controller.getLayoutParams();
        params.width = videoWidth;
        controller.setLayoutParams(params);
        exoPlayer = new ExoPlayer.Builder(context)
                .setHandleAudioBecomingNoisy(true)
                .build();
        exoPlayer.addListener(new Player.Listener() {
            @Override
            public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, int reason) {
                if (exoPlayer != null) exoPlayer.pause();
                Player.Listener.super.onPositionDiscontinuity(oldPosition, newPosition, reason);
            }
            @Override
            public void onPlaybackStateChanged(int playbackState) {
                if (playbackState == Player.STATE_ENDED) {
                    if (exoPlayer != null) exoPlayer.pause();
                    setControl(1);
                }
                Player.Listener.super.onPlaybackStateChanged(playbackState);
            }
        });
        playerView.setPlayer(exoPlayer);
        exoPlayer.setMediaItem(MediaItem.fromUri(uri), 0);
        exoPlayer.prepare();
      	return dialog;
    }

    public void thumnail(String path) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.execute(() -> {
            // サムネイルクリア
            new ExternalStorageWriter(context).initializeFiles("thumnail", 1);
            // 1秒毎のサムネイル作成
            FFmpegKit.execute(String.format(Locale.getDefault(), "-i '%s' -vf fps=1 -s %dx%d '%s'",  path, (int) (WIDTH /5), (int) (HEIGHT /5), context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) + File.separator + "thumnail%04d.jpg"));
            new Handler(Looper.getMainLooper()).post(() -> {
                if (previewTimeBar != null) {
                    previewTimeBar.setPreviewEnabled(true);
                }
            });
        });
    }

    @Override
    public void onDetach() {
        exoPlayer.stop();
        exoPlayer.release();
        previewTimeBar = null;
        super.onDetach();
    }
}

外部ストレージに静止画(サムネイル)の格納します。
外部ストレージの入出力クラスとして、ExternalStorageReaderとExternalStorageWriterを作成します。

ExternalStorageReader

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) {
        this.context = context;
        for (int i=0; i< type.length; i++) {
            path[i] = context.getExternalFilesDir(type[i]);
        }
    }

    // ファイル指定 //
    public boolean setFile(String fileName,int type) {
        file = new File(path[type], fileName);
        return file.exists();

    // ファイル読込 //
    public Bitmap readFileBitmap() {
        Bitmap bitmap = null;
        if (isExternalStorageReadable()) {
            try (FileInputStream fileinputStream =
                         new FileInputStream(file)) {
               bitmap = BitmapFactory.decodeStream(fileinputStream);
            } catch (Exception e) {
                Log.d(TAG, Objects.requireNonNull(e.getMessage()));
            }
        }
        return bitmap;
    }
}

ExternalStorageWriter

public class ExternalStorageWriter {
    private final String[]          type = new String[] {
            Environment.DIRECTORY_DOCUMENTS,
            Environment.DIRECTORY_DOWNLOADS,
            Environment.DIRECTORY_PICTURES
    };
    private final File[]            path = new File[type.length];
    private final Context           context;
    :

    public ExternalStorageWriter(Context context) {
        this.context = context;
        for (int i=0; i< type.length; i++) {
            path[i] = context.getExternalFilesDir(type[i]);
        }
    }

    // ファイル初期化 //
    public void initializeFiles(String fileName, int type) {
        File filePath = new File(path[type].getPath());
        File[] listFiles = filePath.listFiles();
        if (listFiles != null) {
            for (File file : listFiles) {
                if (!file.isDirectory() && file.getName().contains(fileName)) file.delete();
            }
        }
    }
}

今回は、ここまでです。

スライドバーの操作で再生位置の静止画をプレビュー表示しているAndroidアプリです。

誤字脱字、意味不明でわかりづらい、
もっと詳しく知りたいなどのご意見は、
このページの最後にある
コメントか、
こちらから、お願いいたします♪

ポチッとして頂けると、
次のコンテンツを作成する励みになります♪

ブログランキング・にほんブログ村へ

これからAndroidのアプリ開発やJavaでの開発を始めたい方へ

アプリケーション開発経験がない方や、アプリケーション開発経験がある方でも、JavaやC#などのオブジェクト指向言語が初めての方は、Androidのアプリ開発ができるようになるには、かなりの時間がかかります。
オンラインスクールでの習得を、強くおススメします。

未経験者からプログラマーを目指すのに最適です。まずは無料カウンセリングから♪

ゲーム系に強いスクール、UnityやUnrealEngineを習得するのに最適です。まずは無料オンライン相談から♪

参考になったら、💛をポッチとしてね♪

スポンサーリンク
シェアする
msakiをフォローする
スポンサーリンク

コメント欄

タイトルとURLをコピーしました