この記事は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を習得するのに最適です。まずは無料オンライン相談から♪

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





コメント欄