😸

[Flutter]動画からサムネイルを作成する!

2023/01/28に公開

好きなところをサムネにしたい!!

動画を扱っているとそう思うことありますよね!
さっそく、読み込んだ動画からサムネイルを作成する方法を記載していきます。

完成イメージ

検証環境

PC:MacBook Air (M1, 2020)
OS:macOS Monterey バージョン12.5

完成までの道のり

以下の流れでサムネイルを取得したいと思います。
この記事では23について記載するので、についてはこちらを参照ください!

  1. 動画の読込
  2. サムネイルにするフレームの指定
  3. 指定したサムネイルの表示

パッケージ

動画からサムネイルを取得するのに便利なパッケージがあります。
それが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を用意して、シークバーの位置をサムネイルにしたいと思います。
    シークバー付きのVideoPlayerchewieパッケージを使用すると簡単に作れるので、こちらを利用します。詳しい使い方は公式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