【Flutter】ダウンロードした動画をオフライン再生する方法

こんにちは、株式会社Pentagonでアプリ開発をしている山崎です!

この記事では、ネット上から動画をダウンロードし、オフラインでコントロールボタン付き動画を再生する方法を説明します。ネット上から動画をダウンロードする方法、コントロールボタン付きオフライン動画を再生する方法を知りたい人にとって役立つ内容になっています。動画のダウンロードを途中で中断して、再び途中からダウンロードを開始する方法もありますが、今回はシンプルに中断せずに一回で動画をダウンロードして再生するまでを説明します。

【こんな人に読んで欲しい】

  • Flutterで動画をダウンロードしてオフライン再生したい人
  • Flutterでコントロールボタン付き動画を再生したい人

【この記事を読むメリット】

  • ChewieControllerの使い方がわかる
  • VideoPlayerControllerの使い方がわかる

【結論】

動画をネット上からダウンロードして、コントロールボタン付きでオフライン再生をするのは、パッケージを使用すれば、難しい処理をすることなく簡単に実装することができます。みなさん、ぜひオリジナルの動画アプリを作成してみてはいかがでしょうか!

目次

動作イメージ

動画をダウンロード後、機内モードにしてオフライン状態にしてから動画を再生しています。

パッケージのインストール

最初に、pubspec.yamlに今回使用するパッケージを入力し、保存します。

dependencies:
  flutter:
    sdk: flutter
  video_player: ^2.0.0-nullsafety.3
  chewie: ^1.5.0
  dio: ^5.2.1
  path_provider: ^2.0.15
  freezed:
  build_runner:
  flutter_riverpod: ^2.2.0

ターミナルに「flutter pub get」を入力し、パッケージをインストールします。

flutter pub get

Freezedで状態クラスを作成

今回のアプリは動画のダウンロード前、ダウンロード中、ダウンロード後、再生中という4つのステップから構成されてます。それをFirstPageStapというenumで表現しています。

FirstPageState.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';

part 'FirstPageState.freezed.dart';

@freezed
class FirstPageState with _$FirstPageState {
  const factory FirstPageState({
    @Default(FirstPageStep.before) FirstPageStep currentState,
    @Default('') String progress,
    @Default(null) String? filePath,
  }) = _FirstPageState;
}

enum FirstPageStep {
  before, //ダウンロード前
  loading, //ダウンロード中
  after, //ダウンロード後
  playing, //再生中
}

コマンドラインに下記のコマンドを入力し、ファイルを生成します。

 flutter pub run build_runner build --delete-conflicting-outputs

main.dart

Riverpodを使用するのでProviderScopeでMaterialAppを囲みます。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:test01/FirstPage.dart';

void main() => runApp(
      const ProviderScope(
        child: MaterialApp(
          home: FirstPage(),
        ),
      ),
    );

ビューの作成

ビューのコードは、動画のダウンロード前、ダウンロード中、ダウンロード後、再生中の4つのステップに分かれてます。Reverpodを使用して状態を管理、更新しています。

FirstPage.dart
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:test01/FirstPageState.dart';
import 'package:test01/FirstPageViewModel.dart';

class FirstPage extends ConsumerStatefulWidget {
  const FirstPage({super.key});

  @override
  ConsumerState<FirstPage> createState() => _FirstPageState();
}

class _FirstPageState extends ConsumerState<FirstPage> {
  @override
  void dispose() {
    //コントローラーを破棄
    ref.read(firstPageProvider.notifier).deleteController();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final firstPageState = ref.watch(firstPageProvider);
    final firstPageNotifier = ref.watch(firstPageProvider.notifier);
    return Scaffold(
      appBar: AppBar(
        title: const Text('サンプルアプリ画面1'),
      ),
      body: Stack(
        children: [
          //ダウンロード前
          Visibility(
            visible: firstPageState.currentState == FirstPageStep.before,
            child: Center(
              child: TextButton(
                child: const Text(
                  'ダウンロード開始',
                  style: TextStyle(
                    fontSize: 30,
                  ),
                ),
                onPressed: () {
                  ref.read(firstPageProvider.notifier).fetchMovie();
                },
              ),
            ),
          ),
          //ダウンロード中
          Visibility(
            visible: firstPageState.currentState == FirstPageStep.loading,
            child: Center(
              child: SizedBox(
                width: 300,
                height: 300,
                child: Card(
                  color: Colors.black,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      const CircularProgressIndicator(),
                      Text(
                        firstPageState.progress,
                        style: const TextStyle(
                          color: Colors.white,
                          fontSize: 100,
                        ),
                      ),
                      const Text(
                        'ローディング中',
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: 30,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
          //ダウンロード後
          Visibility(
            visible: firstPageState.currentState == FirstPageStep.after,
            child: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Text(
                    'ダウンロード完了',
                    style: TextStyle(
                      fontSize: 30,
                    ),
                  ),
                  TextButton(
                    child: const Text(
                      '動画を再生する',
                      style: TextStyle(
                        fontSize: 30,
                      ),
                    ),
                    onPressed: () {
                      ref.read(firstPageProvider.notifier).play();
                    },
                  ),
                ],
              ),
            ),
          ),
          //再生中
          Visibility(
            visible: firstPageState.currentState == FirstPageStep.playing,
            child: SingleChildScrollView(
                child: firstPageNotifier.chewieController != null
                    ? AspectRatio(
                        //外枠の縦横比を設定
                        aspectRatio:
                            firstPageNotifier.chewieController!.aspectRatio!,
                        child: Chewie(
                          controller: firstPageNotifier.chewieController!,
                        ),
                      )
                    : Container()),
          ),
        ],
      ),
    );
  }
}

ビューモデルの作成

パッケージのDioを使用して、ネット上の動画をダウンロードし、このアプリ専用のディレクトリに動画を保存しています。
保存後、「動画を再生する」ボタンをタップすると、動画の再生が開始します。
この時、すでにアプリに動画をダウンロードしているのでオフラインの環境でも動作可能です。
VideoPlayerController:実際に動画を再生するクラスです。
ChewieController:動画にコントロールボタンを表示するクラスです。
ChewieControllerのcontrollerにVideoPlayerControllerを設定することで動画にコントロールボタンを表示して再生することができます。

FirstPageViewModel.dart
import 'dart:io';

import 'package:chewie/chewie.dart';
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path_provider/path_provider.dart';
import 'package:test01/FirstPageState.dart';
import 'package:video_player/video_player.dart';

final firstPageProvider =
    StateNotifierProvider<FirstPageViewModel, FirstPageState>(
  (ref) => FirstPageViewModel(),
);

class FirstPageViewModel extends StateNotifier<FirstPageState> {
  FirstPageViewModel({
    //初期状態を設定
    FirstPageState state =
        const FirstPageState(currentState: FirstPageStep.before, progress: '0'),
  }) : super(state);

  VideoPlayerController? _videoController;

  ChewieController? chewieController;

  Future fetchMovie() async {
    //ダウンロードしたい動画ファイルのURL
    const fileId =
        'https://xxx.xxx.com/xxxxxxxxxx';

    //Dioを使用して、ネット上の動画をダウンロード
    final dio = Dio();
    //このアプリ専用のディレクトリに動画を保存
    final dir = await getApplicationDocumentsDirectory();
    final filePath = '${dir.path}/sample.mp4';
    state = state.copyWith(
      currentState: FirstPageStep.loading,
      progress: '0%',
    );
    //ダウンロード開始
    await dio.download(
      fileId,
      filePath,
      onReceiveProgress: (count, total) {
        //ダウンロード状況を0パーセントから100パーセントになるまでの進行状況をprogressに代入している
        final progress = '${((count / total) * 100).toStringAsFixed(0)}%';
        state = state.copyWith(
          progress: progress,
        );
      },
    );
    //ダウンロード完了
    state =
        state.copyWith(currentState: FirstPageStep.after, filePath: filePath);
  }

  Future<void> play() async {
    //VideoPlayerControllerは、動画を再生するためのクラス
    _videoController = VideoPlayerController.file(File(state.filePath!));
    //ビデオの再生準備
    await _videoController?.initialize();
    //ChewieControllerは、動画にコントロールボタンを表示するクラス
    chewieController = ChewieController(
      //ChewieControllerにvideoPlayerControllerを設定
      videoPlayerController: _videoController!,
      //動画の縦横比を設定
      aspectRatio: _videoController?.value.aspectRatio,
      //自動再生するかを設定
      autoPlay: true,
    );
    state = state.copyWith(currentState: FirstPageStep.playing);
  }

  //コントローラーの破棄
  void deleteController() {
    _videoController?.dispose();
    chewieController?.dispose();
  }
}

まとめ

VideoPlayerControllerとChewieControllerを使用すれば簡単にコントロールボタン付き動画を再生することができます。この機会に自分オリジナルの動画アプリを作成してみてはいかがでしょうか?
今回は動画をダウンロードして、再生するまでの内容でしたが、次回以降は分割ダウンロードや取得動画の表示などの機能を追加した記事を書く予定です。

おまけ

Flutterに関する知識を深めたい方には、『Flutterの特徴・メリット・デメリットを徹底解説』という記事がおすすめです。

この記事では、Flutter アプリ開発の基本から、flutter とは何か、そして実際のflutter アプリ 事例を通じて、その将来性やメリット、デメリットまで詳しく解説しています。
Flutterを使ったアプリ開発に興味がある方、またはその潜在的な可能性を理解したい方にとって、必見の内容となっています。

ぜひ一度ご覧ください。

採用情報はこちら
目次