😸
[Flutter]動画からサムネイルを作成する!
好きなところをサムネにしたい!!
動画を扱っているとそう思うことありますよね!
さっそく、読み込んだ動画からサムネイルを作成する方法を記載していきます。
完成イメージ
検証環境
PC:MacBook Air (M1, 2020)
OS:macOS Monterey バージョン12.5
完成までの道のり
以下の流れでサムネイルを取得したいと思います。
この記事では2
と3
について記載するので、1
についてはこちらを参照ください!
- 動画の読込
- サムネイルにするフレームの指定
- 指定したサムネイルの表示
パッケージ
動画からサムネイルを取得するのに便利なパッケージがあります。
それがvideo_thumbnailです。
pubspec.yaml
に以下を追加してpub get
。
dependencies:
video_thumbnail: ^0.5.3
使用するDartコードでインポート。
import 'package:video_thumbnail/video_thumbnail.dart';
このパッケージでは動画ファイルデータからサムネイルを生成するメソッド
と動画のURLからサムネイルを生成するメソッド
、pubspec.yaml で宣言された動画アセットからサムネイルを生成するメソッド
がありますが、今回は動画のURLからサムネイルを生成するメソッド
を使いたいと思います。
動画からサムネイルを生成するクラスの作成
- インターフェイスは以下にします。
入力:動画のファイルパス
とフレーム位置(ms)
出力:サムネイルのファイルパス
- 動画のURLからサムネイルを生成するメソッドは
VideoThumbnail.thumbnailFile
になります。
入力する動画のファイルパス
とフレーム位置(ms)
、出力するサムネイルのファイルパス
とフォーマット
を引数に指定します。
video: [動画のファイルパス],
timeMs: [フレーム位置(ms)],
thumbnailPath: [サムネイルのファイルパス],
imageFormat: [フォーマット],
- 出力する
サムネイルのファイルパス
はデバイスの一時ディレクトリにしたいと思います。
デバイスのファイルシステムにアクセスするためにパッケージpath_providerを使用します。
getTemporaryDirectory
メソッドを使用すると一時ディレクトリへのパスが取得できます。 - Dartコードの全体はこんな感じです。
import 'package:video_thumbnail/video_thumbnail.dart';
import 'package:path_provider/path_provider.dart';
class MakeThumbnail {
Future<String> getThumbnailTwo(String videopath, int position) async {
try {
final thumbnailFile = await VideoThumbnail.thumbnailFile(
video: videopath,
timeMs: position,
thumbnailPath: (await getTemporaryDirectory()).path,
imageFormat: ImageFormat.JPEG,
);
// ignore: unnecessary_null_comparison
if (thumbnailFile != null) {
return Future<String>.value(thumbnailFile);
} else {
return Future<String>.value('');
}
} catch (e) {
return Future<String>.value('');
}
}
}
サムネイル位置を指定するViewの作成
- シークバー付きの
VideoPlayer
を用意して、シークバーの位置をサムネイルにしたいと思います。
シークバー付きのVideoPlayer
はchewie
パッケージを使用すると簡単に作れるので、こちらを利用します。詳しい使い方は公式URLを参考にしてください。 - サムネイル生成を実行するボタンを用意して、ボタンが押されたら生成を開始するようにします。ボタンが押されたときのシークバーの位置をミリ秒に変換します。
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:chewie/chewie.dart';
import 'package:videoselect/control/makethumbnail.dart';
import 'package:videoselect/thumbnail_view.dart';
// ignore: must_be_immutable
class Video extends StatefulWidget {
String videoPath;
Video({super.key, required this.videoPath});
@override
State<Video> createState() => VideoState();
}
class VideoState extends State<Video> {
late VideoPlayerController controller;
late ChewieController chewieController;
late File file;
late Uint8List bytes;
@override
void initState() {
super.initState();
controller = VideoPlayerController.file(File(widget.videoPath));
chewieController = ChewieController(
videoPlayerController: controller,
aspectRatio: 0.5625,
allowFullScreen: false,
allowMuting: false,
);
controller.addListener(() {
setState(() {});
});
controller.setLooping(true);
controller.initialize();
}
@override
void dispose() {
controller.dispose();
chewieController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
Chewie(controller: chewieController),
Padding(
padding: const EdgeInsets.all(8.0),
child: Align(
alignment: Alignment.topCenter,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange[800],
),
onPressed: () {
int timeMs = 0;
// ボタンが押されたときのシークバーの位置をミリ秒に変換
controller.value.position
.toString()
.split(':')
.asMap()
.forEach((key, value) {
if (key == 0) {
timeMs += (int.parse(value) * 60 * 60) * 1000;
} else if (key == 1) {
timeMs += (int.parse(value) * 60) * 1000;
} else {
timeMs += (double.parse(value) * 1000).toInt();
}
});
},
child: const Text('サムネイル取得'),
),
),
),
],
);
}
}
生成したサムネイルの表示
- サムネイルのパスを受け取って表示するViewを作成します。
import 'package:flutter/material.dart';
// ignore: must_be_immutable
class ViewThumbnailView extends StatefulWidget {
Image image;
ViewThumbnailView({required this.image, super.key});
@override
State<ViewThumbnailView> createState() => _ViewThumbnailViewState();
}
class _ViewThumbnailViewState extends State<ViewThumbnailView> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Container(
decoration:
BoxDecoration(border: Border.all(color: Colors.black)),
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height * 0.8,
child: widget.image),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('戻る'),
),
]),
),
);
}
}
- 先ほどのサムネイル生成を開始するボタンが押されたところ
onPressed
で、サムネイルを生成した後に、このViewを呼び出します。そうすると生成したサムネイルが表示されます。
onPressed: () {
int timeMs = 0;
// ボタンが押されたときのシークバーの位置をミリ秒に変換
controller.value.position
.toString()
.split(':')
.asMap()
.forEach((key, value) {
if (key == 0) {
timeMs += (int.parse(value) * 60 * 60) * 1000;
} else if (key == 1) {
timeMs += (int.parse(value) * 60) * 1000;
} else {
timeMs += (double.parse(value) * 1000).toInt();
}
});
// 生成したサムネイルの表示
MakeThumbnail()
.getThumbnailTwo(widget.videoPath, timeMs)
.then((path) => {
file = File(path),
bytes = file.readAsBytesSync(),
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ViewThumbnailView(
image: Image.memory(bytes)))),
});
},
完成
これで完成です。動画からサムネイルが取得できるようになっているはずです!
※UIがダサいのは許してくださいね。
全コードをGitHubに上げてあるので、よかったら参考にしてください。
Discussion