[Flutter] 動画プレイヤーを再実装する形でTexture Widgetについて調べてみた

この記事は「[Flutter Advent Calendar 2021](https://qiita.com/advent-calendar/2021/flutter)」の17日目の記事です。

FlutterにはiOS/Androidと言った「プラットフォーム側で用意されているView」や「ネイティブ側で再生した映像」などをWidgetとして表示する仕組みとして、「**[Texture Widget](https://api.flutter.dev/flutter/widgets/Texture-class.html)**」や「**[Platform Views](https://docs.flutter.dev/development/platform-integration/platform-views)**」と言った機能が存在します。

今回、前者の`Texture Widget`について調査してみたので備忘録序に纏めてみようと思います。
もし記事中で間違いや説明不足の箇所などあれば、コメントや編集リクエストなどで教えて頂けると幸いです。

-- 

なお、調査する上では以下の記事を参考にさせて頂きました。
(特に`Texture Widget`に関しては情報が少なかった？印象があったので感謝... :bow: )

- [Flutterで動画を再生するためには](https://ca-base-next.cyberagent.co.jp/sessions/flutter-player/)
    - [「Web」「iOS」「Android」でFlutterで作った「video_player」を表示する方法](https://logmi.jp/tech/articles/324823)
- [Flutterで動画配信するプラグインを作った話](https://speakerdeck.com/espresso3389/flutterdedong-hua-pei-xin-surupuraguinwozuo-tutahua)
- [OpenGL with Texture widget](https://medium.com/@german_saprykin/opengl-with-texture-widget-f919743d25d9)
- [Flutter 实时视频渲染：Texture与PlatformView](https://zhuanlan.zhihu.com/p/62821195)


こちらの資料などを参考にさせて頂きつつ、自分なりに纏めていければと思います。

**▼ 対応プラットフォーム**

- iOS
- Android

**▼ 環境**

- [✓] Flutter (Channel stable, 2.5.3, on macOS 12.0.1 21A559 darwin-x64, locale ja-JP)
    - Dart version 2.14.4
- [✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
- [✓] Xcode - develop for iOS and macOS (version 13.0)
- [✓] Android Studio (version 2020.3)



# この記事で解説する内容について

この記事では公式の[video_player](https://pub.dev/packages/video_player)を参考に簡単な**「動画プレイヤー」**を再実装する形で`Texture Widget`に関する実装ポイントを解説していきます。

最後には持論ベースではありますが、似たような性質を持つ[Platform Views](https://docs.flutter.dev/development/platform-integration/platform-views)との使い分けについても簡単に触れていければと思います思います。

**※プラットフォーム固有の実装コードについて**

iOS及びAndroidのプラットフォーム固有の実装コード(以降、`ネイティブコード`と記載)は以下の言語で実装していきます。

- iOS: `Swift 5.0` [^1]
- Android: `Kotlin 1.6.0` [^1]

[^1]: 各バージョンとも取り敢えず「デフォルト指定のバージョン」や「新しいバージョン」を選んだだけであり、言語バージョンの選定理由にはそこまで強いこだわりはありません。

あとは解説しやすいように[pigeon](https://pub.dev/packages/pigeon)と言ったパッケージは利用せずに、`Method Channel`などは全て手動で定義していきます。


## 記事のターゲット

基本的には 「iOS/Androidアプリの開発知識 (両方 or どちらか一方)」及び「`Plugin Packages`[^2]に関する実装知識[^3]」が多少あることを前提に解説していきます。

[^2]: プラットフォーム固有の機能を呼び出すパッケージの名称 (公式ドキュメントの「[Package types](https://docs.flutter.dev/development/packages-and-plugins/developing-packages#types)」より)

[^3]: 公式ドキュメントの「[Developing packages & plugins](https://docs.flutter.dev/development/packages-and-plugins/developing-packages)」を大凡把握出来ていればOK

その上で以下の方にはリーチする内容になるかなと思います。

- 上に挙げてる知識は多少あるが、`Texture Widget`へのキャッチアップまでは行えていない
- `Texture Widget`を利用した機能の設計の一例を見てみたい

### この記事の目的

上記に該当する方が`Texture Widget`及び設計の一例までのキャッチアップを完了するところまでを目的としております。
その上で深く調べていく上での足がかりとしたり、技術選定時の一助となれば幸いです。

## 解説しない内容

「記事のターゲット」でも記載した通り、以下の前知識はある程度前提としてます。

- Flutterのアプリ開発及び`Plugin Package`に関する開発の基礎知識
    - e.g. Method Channelとは何か？ どう作り始めればよいか？など
- iOS/Androidアプリ開発の基礎知識
    - Swift/Kotlinの構文など





# 動画プレイヤーを再実装してTexture Widgetの実装ポイントを理解する

この章では`Texture Widget`を利用した簡易的な動画プレイヤーの実装解説を行っていきます。

![20211216_134623.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/b983e788-ff50-57e9-94e3-7659b5c97c28.gif)

ちなみに動画プレイヤーと言っても公式の`video_player`と全く同じものを写経して作る形ではなく、大凡の設計思想などは参考にしつつもメソッド名や機能などは独自に定義してます。

そのために全体を通してみると「`Texture Widget`の機能解説」と言うよりかは**「それを応用した設計周りの解説」**も含まれた内容となりますが、それも踏まえて実装の一例として見て頂けると幸いです。

**▼ 仕様**

- 任意の数のインスタンスを生成して配置し、それぞれ個別に操作することが可能　(↑のGIF上では3つ生成している)
- 機能としては以下を実装
    - インスタンスの生成・破棄
    - 再生するHLS形式の動画URLの設定
    - 再生
    - 一時停止
- バックグラウンド再生やエラーハンドリングは未考慮

**▼ プロジェクトについて**

解説するプロジェクトはGitHubで公開してます。

- [mao-test-h/flutter_texture_widget_sample](https://github.com/mao-test-h/flutter_texture_widget_sample)



## そもそもTexture Widgetとは何か？

具体的な解説に入る前に今回の主役である`Texture Widget`そのものについて簡単に解説していきます。
API Referenceとしては以下の`Texture class`であり、この記事中では基本的に`Texture Widget`と言う名称で記載していきます。

- [Texture class](https://api.flutter.dev/flutter/widgets/Texture-class.html)

機能自体をザックリと説明すると**「配置した`Texture Widget`の表示領域に対し、ネイティブコード側から好きな映像や描画結果などを書き込んで表示できる」**と言えそうな特殊なWidgetになります。

### 主な用途

こちらは公式の[video_player](https://pub.dev/packages/video_player)や[camera](https://pub.dev/packages/camera)と言ったパッケージで利用されており、主にネイティブ側の映像をFlutter上で表示する際に利用されています。

例えば上記に挙げたパッケージだと**「[video_player](https://pub.dev/packages/video_player)ではネイティブ側で再生した映像の表示」**に利用しており、**「[camera](https://pub.dev/packages/camera)の方はカメラ映像のプレビュー表示」**で利用されてます。

他にも以下の記事にあるように、OpenGLと言ったGraphicsApiを用いて自前でレンダリングした結果を表示するのにも使えそうです。

- [OpenGL with Texture widget](https://medium.com/@german_saprykin/opengl-with-texture-widget-f919743d25d9)




## Texture Widgetの描画の仕組みについて

`Texture Widget`の描画を行うには、**ネイティブコード側から自前で描画処理を呼び出して更新を行う必要があります。**

手順については**[Texture class](https://api.flutter.dev/flutter/widgets/Texture-class.html)**のドキュメントにそれっぽい記載があり、引用すると以下のように記載されてます。

> A rectangle upon which a backend texture is mapped.
> 
> Backend textures are images that can be applied (mapped) to an area of the Flutter view. They > are created, managed, and updated using a platform-specific texture registry. This is typically done by a plugin that integrates with host platform video player, camera, or OpenGL APIs, or similar image sources.
> 
> A texture widget refers to its backend texture using an integer ID. Texture IDs are obtained from the texture registry and are scoped to the Flutter view. Texture IDs may be reused after deregistration, at the discretion of the registry. The use of texture IDs currently unknown to the registry will silently result in a blank rectangle.
> 
> Texture widgets are repainted autonomously as dictated by the backend (e.g. on arrival of a video frame). Such repainting generally does not involve executing Dart code.
> 
> The size of the rectangle is determined by its parent widget, and the texture is automatically scaled to fit.

また、それとは別に**[TextureLayer class](https://api.flutter.dev/flutter/rendering/TextureLayer-class.html)**と言うクラスの説明には以下のように記載されてます。

> A composited layer that maps a backend texture to a rectangle.
> 
> Backend textures are images that can be applied (mapped) to an area of the Flutter view. They > are created, managed, and updated using a platform-specific texture registry. This is typically done by a plugin that integrates with host platform video player, camera, or OpenGL APIs, or similar image sources.
> 
> A texture layer refers to its backend texture using an integer ID. Texture IDs are obtained from the texture registry and are scoped to the Flutter view. Texture IDs may be reused after deregistration, at the discretion of the registry. The use of texture IDs currently unknown to the registry will silently result in a blank rectangle.
> 
> Once inserted into the layer tree, texture layers are repainted autonomously as dictated by the backend (e.g. on arrival of a video frame). Such repainting generally does not involve executing Dart code.
> 
> Texture layers are always leaves in the layer tree.

これらを見るにFlutterがプラットフォームごとに`Texture Registry`と言う実装を持っているので、こちらを利用して以下の手順で描画及び表示を行う必要があると取れそうです。[^7]

[^7]: これ以外の公式の詳細な資料が見つからなかったのもあり、ひょっとしたら説明に若干の語弊があるかもです...。(Flutter Engineのソースを追えば詳細について知ることが出来るかもしれないが...まだそこまで追えておらず...)

- **ネイティブコード**
    - 1. `TextureRegistry`に対して登録処理を呼び出して`textureId`の取得を行う
    - 2. 取得した`textureId`に紐付いている描画領域に対して**任意の描画処理を呼び出す**
        - e.g. 動画プレーヤーから得られた映像の反映など
    - 3. 不要になったら`TextureRegistry`に対し登録解除処理を呼び出すことで`texureId`を返却可能
        - ※ その上で返却(登録解除)された`textureId`はエンジン側の裁量で使い回される可能性がある
- **Flutter (Dart)**
    - 上記の1の手順で取得した`textureId`を`Texture Widget`に渡す形で任意の箇所に配置 [^6]
        - → **上記の2の手順で更新した描画結果が実際に配置したWidget内に表示される**
            - e.g. 動画プレーヤーの場合には映像がWidget内に表示される

[^6]: ちなみにここで未登録の`textureId`をTexture Widgetに渡した際にはブランクが表示されるっぽい (≒ 例外とかは投げられない模様)

これらを踏まえつつ今回実装した動画プレイヤーの具体的な解説に入っていきます。




## 全体の構成について

先ずはイメージを掴みやすいように全体構成から解説していきます。

ポイントとしてはDart側で動画プレイヤー(図で言う`SimpleVideoPlayer`クラス)を生成したら、それに合わせてネイティブ側でも「動画プレイヤーの生成」と**「`textureId`の登録」**を行い、Dart側に`textureId`を返します。

この`textureId`は配置する`Texture Widget`の引数として渡す他にも、**Dart側の動画プレイヤーのインスタンスから「対応するネイティブ側の動画プレイヤー」を参照するためのキーとしても利用してます。**

これにより`SimpleVideoPlayer`を任意の数だけ生成しつつ、それぞれを独立して管理を行えるような構成にしてます。

![スクリーンショット 2021-12-17 14.04.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/0447b053-ecb7-bd5d-c620-6d63af190f75.png)

:::note info
textureIdの本来の使い方としては「Texture Widgetへの引数としての利用」だが、今回の実装では序に「ネイティブ側の動画プレイヤーと紐付けるためのキー」としても利用する形となっている。
(参考にしたvideo_playerの実装がそうなっていた)
:::





## Flutter (Dart)側の実装

先にDart側の実装解説から入っていきます。
ネイティブコードから`textureId`を登録する手順などは次の章で解説します。

ここでは以下のトピックに分けて解説していきます。

- **動画プレイヤー本体側の実装** (Plugin Packageとして実装される箇所)
    - → `./lib`以下のコード
- **動画プレイヤーを利用したサンプルの実装**
    - → `./example/lib`以下のコード

### 動画プレイヤー本体側の実装

こちらは実際にPlugin Packageとして実装される箇所です。
要点だけ取り上げて解説していくので、ソース全体については以下を参照して下さい。

- **動画プレイヤー本体 (`SimpleVideoPlayer`)**
    - [./lib/sample_video_player.dart](https://github.com/mao-test-h/flutter_texture_widget_sample/blob/main/lib/sample_video_player.dart)
- **Method Channel定義部分** [^4]
    - [./lib/src/sample_video_player_method_channel.dart](https://github.com/mao-test-h/flutter_texture_widget_sample/blob/main/lib/src/sample_video_player_method_channel.dart)
- **[Platform Interface](https://pub.dev/packages/plugin_platform_interface)の定義クラス** [^4], [^5]
    - [./lib/src/platform_interface/sample_video_player_platform_interface.dart](https://github.com/mao-test-h/flutter_texture_widget_sample/blob/main/lib/src/platform_interface/sample_video_player_platform_interface.dart)

[^4]: ここらの設計思想や実装の詳細については「Method Channel」や「Federated plugins」の話となってしまうので、本記事では掘り下げて解説しません。

[^5]: ちなみに「sample_video_player_platform_interface.dart」のドキュメントコメントにも記載してますが、このサンプルでは便宜的に「Platform Interface」の定義を同じプロジェクトに含めています。(ちゃんとするなら別プロジェクトに分けたほうが良いかも)


### ■ インスタンスの初期化時に`textureId`を返しつつ、ネイティブ側で動画プレイヤーの生成を行う

`SampleVideoPlayer`を生成したら利用する側は`initialize()`で初期化を行うようにします。

`initialize()`ではMethod Channelで**ネイティブコード側の「動画プレイヤー生成」及び「`textureId`の登録」を呼び出しつつ**、同時にネイティブコード側で登録した`textureId`を取得して保持するようにします。

ここで`textureId`を保持することによって「`SimpleVideoPlayer`とネイティブ側の動画プレイヤー」の紐付けが成り立ちます。

```dart:./lib/sample_video_player.dart
class SampleVideoPlayer {
  int _textureId = uninitializedTextureId;

  /// textureId (再生するTextureWidgetに渡すこと)
  int get textureId => _textureId;

  /// 初期化
  Future initialize() async {
    if (_textureId != uninitializedTextureId) {
      debugPrint('textureId登録済み');
      return;
    }

    // ネイティブ側で「動画プレイヤーの生成」と「textureIdの登録」を行う
    // → `createPlayer`の戻り値には登録した`textureId`が返ってくる
    _initializeCompleter = Completer<void>();
    _textureId = await _platformInterface.createPlayer();
    _initializeCompleter!.complete(null);
  }
```

呼び出しているMethod Channelの処理は以下のような感じです。

```dart:./lib/src/sample_video_player_method_channel.dart
class SampleVideoPlayerMethodChannel implements SampleVideoPlayerPlatforminterface {

  /// ネイティブ側でプレイヤーの生成を行う
  ///
  /// 戻り値として返す[int]は「生成したプレイヤーの管理ID」及び、「TextureWidgetに渡すtextureId」として機能する
  ///
  /// NOTE: 内部的にプレイヤーの生成と共に、TextureWidgetで利用するtextureIdの登録を行う
  @override
  Future<int> createPlayer() async {
    final textureId = await _methodChannel.invokeMethod<int>(
      'createPlayer',
    );
    assert(textureId != null);
    return textureId!;
  }
```


### ■ `textureId`はインスタンスメソッドの呼び出しにも利用

保持した`textureId`は**ネイティブ側の動画プレイヤーのインスタンスメソッドを呼び出すためのキーとしても利用します。**
(詳しくは後述しますが、ネイティブコード側では渡された`textureId`を元に対応する動画プレイヤーのインスタンスを探し出してインスタンスメソッドを呼び出します)

例として「再生」と「破棄」のコードを切り出すと以下のようになります。

```dart:./lib/sample_video_player.dart
class SampleVideoPlayer {

  /// 破棄
  Future dispose() async {
    if (_textureId == uninitializedTextureId) {
      debugPrint('textureIdが未登録');
      return;
    }

    await _initializeCompleter!.future;
    await pause();
    await _platformInterface.disposePlayer(_textureId);
    _textureId = uninitializedTextureId;
    _initializeCompleter = null;
  }

  /// 再生
  Future play() async {
    if (_textureId == uninitializedTextureId) {
      debugPrint('textureIdが未登録');
      return;
    }

    await _initializeCompleter!.future;
    await _platformInterface.play(_textureId);
  }
}
```

Method Channelの実装クラスはシングルトンとなるので、ネイティブコードのインスタンスメソッドを呼び出すにはこの様に「インスタンスを参照するキー」と言ったものを渡して間接的に呼び出す必要があります。

```dart:./lib/src/sample_video_player_method_channel.dart
class SampleVideoPlayerMethodChannel implements SampleVideoPlayerPlatforminterface {

  /// ネイティブ側で生成したプレイヤーの破棄
  ///
  /// [textureId]に紐付かれたプレイヤーの破棄を行う
  ///
  /// NOTE: 内部的にプレイヤーの破棄と共に、TextureWidgetで利用するtextureIdの登録解除を行う
  @override
  Future disposePlayer(int textureId) async {
    return _methodChannel.invokeMethod(
      'disposePlayer',
      <String, dynamic>{
        'textureId': textureId,
      },
    );
  }

  /// プレイヤーの再生
  ///
  /// [textureId]に紐付かれたプレイヤーを再生する
  @override
  Future play(int textureId) {
    return _methodChannel.invokeMethod(
      'play',
      <String, dynamic>{
        'textureId': textureId,
      },
    );
  }
}
```


### 動画プレイヤーを利用したサンプルの実装

上記で実装した`SimpleVideoPlayer`を実際に利用しているサンプルについても解説します。
こちらも要点だけ取り上げて解説していくので、ソース全体については以下を参照して下さい。

- **ExampleView**
    - [./example/lib/page/view.dart](https://github.com/mao-test-h/flutter_texture_widget_sample/blob/main/example/lib/page/view.dart)
- **ExampleViewModel**
    - [./example/lib/page/view_model.dart](https://github.com/mao-test-h/flutter_texture_widget_sample/blob/main/example/lib/page/view_model.dart)


### ■ `SimpleVideoPlayer`の生成と破棄

ネイティブに関わる一通りの処理は`SimpleVideoPlayer`にカプセル化される形で実装されているので、ぶっちゃけ使う側はそこまで複雑ではありません。

今回の例だと動画プレイヤーを任意の数だけ生成して持てるようにしつつ、ViewModel側に「追加」と「削除」のメソッドを用意しているだけです。

**その上でそれぞれのプレイヤーは独立したインスタンスとなっているので、個別に「再生」「一時停止」と言った操作を行うことができます。**

```dart:./example/lib/page/view_model.dart
class ExampleViewModel extends ChangeNotifier {
  // サンプルで再生するHLSの動画URL(固定)
  final _hlsUrl =
      'http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8';

  // 生成したプレイヤー
  final createdPlayers = <SampleVideoPlayer>[];

  /// プレイヤーの追加
  Future addPlayer() async {
    // `initialize()`は必ず呼び出すこと。
    // でないと`textureId`が登録されない上に動画プレイヤーも生成されない
    final player = SampleVideoPlayer();
    await player.initialize();
    await player.setUrl(_hlsUrl);
    createdPlayers.add(player);
    notifyListeners();
  }

  /// プレイヤーの削除
  Future removePlayer() async {
    final index = createdPlayers.length - 1;
    if (index < 0) return;

    final player = createdPlayers[index];
    await player.dispose();
    createdPlayers.removeAt(index);
    notifyListeners();
  }
}
```

### ■ `Texture Widget`での表示

**実際にFlutterアプリ上に`Texture Widget`として表示している箇所は以下です。**

とは言え、こちらもそこまで複雑ではありません。
`Texture Widget`の引数`textureId:`に**`SimpleVideoPlayer`が持つ`textureId`を渡しているだけです。**

この例では`textureId`が未登録(`SimpleVideoPlayer.initialize()`が呼び出されていない)時には表示範囲を黒塗りするように実装してます。
(利便性を考慮するなら自明なエラー表示とかに切り替えても良いかも？)

```dart:./example/lib/page/view.dart
  Widget _buildVideoPlayerWidget(BuildContext context) {
    final viewModel = context.watch<ExampleViewModel>();

    // `index`には現在操作しているプレイヤーのindexが入るよ
    final textureId = viewModel.createdPlayers[index].textureId;

    // [16:9]のサイズで表示
    return SizedBox(
      width: 320 * 0.8,
      height: 180 * 0.8,
      // NOTE: textureIdが未登録状態の場合には黒塗りにしておく
      child: textureId != uninitializedTextureId
          ? Texture(textureId: textureId)    // ← SimpleVideoPlayerが持つ`textureId`
          : Container(color: Colors.black),
    );
  }
```





## ネイティブコードの実装

次にネイティブコード側に於ける以下のトピックについて解説していきます。

- **`textureId`の登録**
- **描画処理の呼び出し**
- **`textureId`の登録解除**

## iOSの実装

要点だけ取り上げて解説していくので、ソース全体については以下を参照して下さい。

- **SwiftSampleVideoPlayerPlugin.swift**
    - [./ios/Classes/SwiftSampleVideoPlayerPlugin.swift](https://github.com/mao-test-h/flutter_texture_widget_sample/blob/main/ios/Classes/SwiftSampleVideoPlayerPlugin.swift)
- **SwiftSampleVideoPlayerPlugin+FlutterPlugin.swift**
    - [./ios/Classes/SwiftSampleVideoPlayerPlugin+FlutterPlugin.swift](https://github.com/mao-test-h/flutter_texture_widget_sample/blob/main/ios/Classes/SwiftSampleVideoPlayerPlugin%2BFlutterPlugin.swift)
- **VideoPlayer.swift**
    - [./ios/Classes/VideoPlayer.swift](https://github.com/mao-test-h/flutter_texture_widget_sample/blob/main/ios/Classes/VideoPlayer.swift)

クラスとしては`SwiftSampleVideoPlayerPlugin`でMethod Channelの紐付けや処理の呼び出しを行い、`VideoPlayer`で動画再生を行ってます。


### ■ `textureId`の登録

先ず最初に`TextureRegistry`を取得する必要がありますが、iOSの場合には`FlutterPluginRegistrar`から手に入ります。

今回は`FlutterPlugin.register`が呼ばれた際に`SwiftSampleVideoPlayerPlugin`のイニシャライザを呼び出し、その中で取得して保持するようにしてます。

```swift:SwiftSampleVideoPlayerPlugin+FlutterPlugin.swif
extension SwiftSampleVideoPlayerPlugin: FlutterPlugin {

    // ※ Flutterが自動で呼び出してくれるメソッド
    public static func register(with registrar: FlutterPluginRegistrar) {
        
        // このタイミングで自身をインスタンス化
        let instance = SwiftSampleVideoPlayerPlugin(with: registrar)
    }
}
```

```swift:SwiftSampleVideoPlayerPlugin.swif
public final class SwiftSampleVideoPlayerPlugin: NSObject {

    /// FlutterPluginより取得可能なテクスチャの管理クラス
    ///
    /// NOTE: iOSはこのクラスに対して「登録」「登録解除」「更新(再描画)」などの呼び出しを行う
    private let textureRegistry: FlutterTextureRegistry

    init(with registrar: FlutterPluginRegistrar) {
        // イニシャライザで`TextureRegistry`を取得する (このメソッドから手に入る)
        textureRegistry = registrar.textures()
        super.init()
    }
```

あとはMethod Channel経由で`createPlayer`が呼び出されたら「動画プレイヤーの生成」及び「`textureId`の登録」を呼び出して、`createdPlayers`と言うDictionaryに登録します。

:::note info
ここでDictionaryにインスタンスを登録しておくことで、Dart側のSimpleVideoPlayerがtextureIdをキーに「対応する動画プレイヤー」のインスタンスメソッドを間接的に呼び出せるという仕組みになってます。
:::


```swift:SwiftSampleVideoPlayerPlugin.swif
    /// 登録したtextureId, 生成済みのプレイヤー
    private var createdPlayers = [Int64: VideoPlayer]()

    /// プレイヤーの生成
    func createPlayer() -> Int64 {
        // NOTE:
        // プレイヤー内部でテクスチャの「登録、登録解除、更新(再描画)」を呼び出せるように`textureRegistry`のインスタンスを渡しとく
        let player = VideoPlayer(with: textureRegistry)
        let textureId = player.textureId

        // 動画プレイヤーはDictionaryに保持。キーは`textureId`
        createdPlayers[textureId] = player
        return textureId
    }
```

`VideoPlayer`のイニシャライザでは「[textureRegistry.register](https://api.flutter.dev/objcdoc/Protocols/FlutterTextureRegistry.html#/c:objc(pl)FlutterTextureRegistry(im)registerTexture:)」を呼び出して**`textureId`の登録を行います。**
(この処理については続きがあり、次で解説します)

**ちなみにiOSの場合には[FlutterTextureRegistry](https://api.flutter.dev/objcdoc/Protocols/FlutterTextureRegistry.html)は「描画の更新処理」を呼び出す際にも利用するので、動画プレイヤークラスの内部でも保持しておきます。** (Androidではこの手順は不要)


```swift:VideoPlayer.swift
final class VideoPlayer: NSObject {

    /// テクスチャ未登録時の値
    private static let uninitializedTextureId: Int64 = -1

    private let textureRegistry: FlutterTextureRegistry
    private var _textureId: Int64 = uninitializedTextureId

    // AVPlayer関連
    private let avPlayer: AVPlayer
    private let playerItemVideoOutput: AVPlayerItemVideoOutput
    private var currentItem: AVPlayerItem? = nil

    /// 登録されたtextureId
    var textureId: Int64 {
        _textureId
    }

    // MARK:- Public Methods

    init(with registrar: FlutterTextureRegistry) {
        textureRegistry = registrar

        // プレイヤーの生成
        avPlayer = AVPlayer()
        playerItemVideoOutput = AVPlayerItemVideoOutput(
            pixelBufferAttributes: [
                kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
            ]
        )

        super.init()

        // textureIdの登録
        //
        // NOTE:
        // `register`に渡すクラスは`FlutterTexture`と言うprotocolを実装している必要がある。
        // → 今回の実装例ではVideoPlayer自身が`FlutterTexture`を実装している
        _textureId = textureRegistry.register(self)
    }

```

### ■ 描画処理の呼び出し

描画処理の呼び出しは`VideoPlayer`にて行ってます。

具体的に言うと[FlutterTexture](https://api.flutter.dev/objcdoc/Protocols/FlutterTexture.html)と言うプロトコルの「[copyPixelBuffer](https://api.flutter.dev/objcdoc/Protocols/FlutterTexture.html#/c:objc(pl)FlutterTexture(im)copyPixelBuffer)」がその処理に当たり、**ここで返した[CVPixelBuffer](https://developer.apple.com/documentation/corevideo/cvpixelbuffer-q2e)が`Texture Widget`上に表示されます。**
→ ここでは`playerItemVideoOutput`から再生中の動画を切り出して返している

ちなみに`textureRegistry.register`で`textureId`の登録を行うのですが、**このメソッドに渡すクラスは`FlutterTexture`プロトコルを実装している必要があり、ここで返される`texuteId`の表示結果が`copyPixelBuffer`で返した値となります。**[^9]

[^9]: コメントにも記載しているが、今回の例では`VideoPlayer`自体が`FlutterTexture`を実装しているので`self`を渡している

```swift:VideoPlayer.swift
final class VideoPlayer: NSObject {

    init(with registrar: FlutterTextureRegistry) {

        // textureIdの登録
        //
        // NOTE:
        // `register`に渡すクラスは`FlutterTexture`と言うprotocolを実装している必要がある。
        // → 今回の実装例ではVideoPlayer自身が`FlutterTexture`を実装している
        _textureId = textureRegistry.register(self)
    }
}

// `textureRegistry.register`で登録するのに必要なprotocol
extension VideoPlayer: FlutterTexture {

    // `textureRegistry.textureFrameAvailable`が呼び出されたらこちらが発火される
    // → 戻り値で返したCVPixelBufferが実際にTextureWidget上に表示される
    public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {

        // 現在のタイミングからPixelBufferを取得
        let outputItemTime = playerItemVideoOutput.itemTime(forHostTime: CACurrentMediaTime())
        guard playerItemVideoOutput.hasNewPixelBuffer(forItemTime: outputItemTime),
              let buffer = playerItemVideoOutput.copyPixelBuffer(forItemTime: outputItemTime, itemTimeForDisplay: nil) else {
            return nil
        }

        // ここで返したPixelBufferが実際にTextureWidget上に表示される
        return Unmanaged<CVPixelBuffer>.passRetained(buffer)
    }
}
```

(下手にやると重くなりそうですが)ここで`CVPixelBuffer`にフィルター処理を施すなんてことも出来そうな印象がありますね。

#### ★ 【iOSのみ】 再描画のための「更新処理」は自前で呼び出す必要あり

上記で挙げた`FlutterTexture.copyPixelBuffer`は**自動では呼び出されません。**
`copyPixelBuffer`を呼び出して再描画を行うには、こちらから「更新されたタイミング」をFlutterに伝える必要があります。

そのためのメソッドが`TextureRegistry`が持つ「[textureFrameAvailable](https://api.flutter.dev/objcdoc/Protocols/FlutterTextureRegistry.html#/c:objc(pl)FlutterTextureRegistry(im)textureFrameAvailable:)」であり、**こちらを呼び出すことでFlutterに更新された旨が通知されて`FlutterTexture.copyPixelBuffer`が呼び出されます。**

そしてこの動画プレイヤーでは[CADisplayLink](https://developer.apple.com/documentation/quartzcore/cadisplaylink)を用いて常に更新し続けることで動画再生を行うようにします。(ちなみにvideo_playerも同様のことを行っている)

```swift:VideoPlayer.swift
    // MARK:- CADisplayLink Events

    /// `CADisplayLink`よりリフレッシュレートに合わせて定期呼び出しされる処理
    @objc private func onDisplayLinkUpdate(_ displayLink: CADisplayLink) {
        // これを呼び出すと`FlutterTexture.copyPixelBuffer`が実行される
        textureRegistry.textureFrameAvailable(_textureId)
    }
```

### ■ `textureId`の登録解除

`textureId`の登録解除は`TextureRegistry`が持つ「[unregisterTexture](https://api.flutter.dev/objcdoc/Protocols/FlutterTextureRegistry.html#/c:objc(pl)FlutterTextureRegistry(im)unregisterTexture:)」と言うメソッドで行います。
この例では`VideoPlayer.dispose()`内で呼び出しを行ってます。

```swift:VideoPlayer.swift
    func dispose() {
        currentItem = nil

        displayLink.invalidate()
        displayLink = nil

        kvoObservers.forEach({ $0.invalidate() })
        kvoObservers.removeAll()

        // textureIdの登録解除
        textureRegistry.unregisterTexture(_textureId)
    }
```

↑を呼び出しているのは`SwiftSampleVideoPlayerPlugin`であり、Method Channelから`disposePlayer`が呼び出されたら該当するプレイヤーの`dispose`を呼び出します。(その上で`createdPlayers`からも消しておく)

```swift:SwiftSampleVideoPlayerPlugin.swift
    /// プレイヤーの破棄
    ///
    /// - Parameter textureId: 紐付いているtextureId
    func disposePlayer(with textureId: Int64) {
        guard let player = createdPlayers[textureId] else {
            return
        }

        player.dispose()
        createdPlayers[textureId] = nil
    }
```

:::note info
今回の実装では行っていませんが、登録解除時にはFlutterTextureプロトコルが持つ「onTextureUnregistered」が呼び出されるので、こちらから動画プレイヤー内のインスタンスの破棄などを行うことも可能です。

ただ、今回の実装では「onTextureUnregistered」を実装せずに「dispose」内で一括で破棄処理を行ってます。(それもあってか「onTextureUnregistered」自体はoptionalな定義だったりする)
:::



## Androidの実装

要点だけ取り上げて解説していくので、ソース全体については以下を参照して下さい。

- **SampleVideoPlayerPlugin.ket**
    - [./android/src/main/kotlin/com/example/sample_video_player/sample_video_player/SampleVideoPlayerPlugin.kt](https://github.com/mao-test-h/flutter_texture_widget_sample/blob/main/android/src/main/kotlin/com/example/sample_video_player/sample_video_player/SampleVideoPlayerPlugin.kt)
- **VideoPlayer.kt**
    - [./android/src/main/kotlin/com/example/sample_video_player/sample_video_player/VideoPlayer.kt](https://github.com/mao-test-h/flutter_texture_widget_sample/blob/main/android/src/main/kotlin/com/example/sample_video_player/sample_video_player/VideoPlayer.kt)

クラスの名称や役割はiOS版と同様です。(なるべく合わせる形で実装してます)


### ■ `textureId`の登録

こちらも最初に`TextureRegistry`を取得する必要がありますが、Androidの場合には`FlutterPlugin.FlutterPluginBinding`から手に入ります。

今回は`FlutterPlugin.onAttachedToEngine`が呼ばれた際に保持するようにしてます。

```kotlin:SampleVideoPlayerPlugin.kt
class SampleVideoPlayerPlugin : FlutterPlugin, MethodCallHandler {
    // FlutterPluginより取得可能なテクスチャの管理クラス
    private lateinit var textureRegistry: TextureRegistry

    // ※ Flutterが自動で呼び出してくれるメソッド
    override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        // onAttachedToEngineで`TextureRegistry`を取得する
        textureRegistry = flutterPluginBinding.textureRegistry
    }
```

あとはMethod Channel経由で`createPlayer`が呼び出されたら「動画プレイヤーの生成」及び「テクスチャの登録」を呼び出して、`createdPlayers`と言うDictionaryに登録します。

Dictionaryにインスタンスを登録する理由はiOSと同じです。
※詳しくは[iOS側の「textureIdの登録」](https://qiita.com/mao_/items/aa219925270e7dfaee39#-textureid%E3%81%AE%E7%99%BB%E9%8C%B2)にある`NOTE INFO`を参照

**その上でAndroidの場合には登録時には「Flutterによって管理されている[SurfaceTexture](https://developer.android.com/reference/android/graphics/SurfaceTexture)及びその管理インターフェースである[TextureRegistry.SurfaceTextureEntry](https://api.flutter.dev/javadoc/io/flutter/view/TextureRegistry.SurfaceTextureEntry.html)」が戻り値として返ります。**
(`textureId`は`id()`と言うメソッドから取得可能)

```kotlin:kotlin:SampleVideoPlayerPlugin.kt
    // 登録したtextureId, 生成済みのプレイヤー
    private var createPlayers: MutableMap<Long, VideoPlayer> = mutableMapOf()

    private fun onCreatePlayer(result: Result) {
        // テクスチャの登録　 (iOSと違って`textureId`が返るわけではない)
        val surfaceTextureEntry = textureRegistry.createSurfaceTexture()

        // Androidの場合は`SurfaceTextureEntry`クラスさえ渡しておけばプレイヤー側で完結可能
        // → iOSみたいに`textureRegistry`を丸ごと渡す必要はない
        val videoPlayer = VideoPlayer(applicationContext, surfaceTextureEntry)
        val textureId = surfaceTextureEntry.id()
        createPlayers[textureId] = videoPlayer
        result.success(textureId)
    }
```

これを`VideoPlayer`に渡して動画プレイヤーのセットアップを行います。

```kotlin:VideoPlayer.kt
class VideoPlayer(
    applicationContext: Context,
    private val surfaceTextureEntry: TextureRegistry.SurfaceTextureEntry,
) {
    private val exoPlayer: SimpleExoPlayer
    private val dataSourceFactory: DataSource.Factory
    private val surface: Surface

    init {
        // video_playerの実装を参考に初期化
        val player = SimpleExoPlayer.Builder(applicationContext).build()
        val factory = DefaultHttpDataSource.Factory()
            .setUserAgent("ExoPlayer")
            .setAllowCrossProtocolRedirects(true)

        // ExoPlayerに対し、テクスチャの紐付けを行う
        // NOTE: Androidの場合は簡単でPlayerにSurfaceを渡すだけで内部的によしなに更新してくれる
        val createSurface = Surface(surfaceTextureEntry.surfaceTexture())
        player.setVideoSurface(createSurface)
        player.setAudioAttributes(
            AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build(),
            false)

        exoPlayer = player
        dataSourceFactory = factory
        surface = createSurface
    }
```

### ■ 描画処理の呼び出し

Androidの場合は簡単で、`SurfaceTextureEntry`が持つ「[surfaceTexture](https://api.flutter.dev/javadoc/io/flutter/view/TextureRegistry.SurfaceTextureEntry.html#surfaceTexture())」から[Surface](https://developer.android.com/reference/android/view/Surface)を作ったら、**それを`ExoPlayer`の`setVideoSurface`に渡すだけで後は裏で勝手に更新してくれます。**

```kotlin:VideoPlayer.kt
    init {
        // ExoPlayerの初期化 (省略)

        // ExoPlayerに対し、テクスチャの紐付けを行う
        // NOTE: Androidの場合は簡単でPlayerにSurfaceを渡すだけで内部的によしなに更新してくれる
        val createSurface = Surface(surfaceTextureEntry.surfaceTexture())

        // ExoPlayerの`setVideoSurface`に渡すだけで終わり (後は裏で勝手に更新してくれる)
        player.setVideoSurface(createSurface)

    }
```

(もしネイティブコード側でフィルター処理を掛けたいとなった場合、どこをいじれば出来そうか？については勉強不足のためにイメージできず...)


### ■ `textureId`の登録解除

Androidの場合は登録解除も簡単です。

`SurfaceTextureEntry`が持つ「[release](https://api.flutter.dev/javadoc/io/flutter/view/TextureRegistry.SurfaceTextureEntry.html#release())」を呼び出すだけで終わりです。
iOSみたいに`TextureRegistry`から呼び出す必要はありません。


```kotlin:VideoPlayer.kt
    /**
     * プレイヤーの破棄
     */
    fun dispose() {
        // テクスチャの登録解除
        surfaceTextureEntry.release()

        surface.release()
        exoPlayer.release()
    }
```

あとは`SampleVideoPlayerPlugin`の方も`createPlayers`から消しておきます。

```kotlin:SampleVideoPlayerPlugin.kt
    private fun onDisposePlayer(textureId: Long, result: Result) {
        createPlayers[textureId]?.let {
            it.dispose()
            createPlayers.remove(textureId)
        }

        result.success(null)
    }
```

# 終わりに

解説としては以上となります。
結構マニュアル操作感のあるAPIではありますが、応用することでFlutterだけは完結できない描画周りの問題を解決することが出来るかもしれません。


## `Platform Views`とどう使い分けていけば良いか？

Flutterには**「ネイティブ側のViewをFlutter上に表示する」**仕組みとして`Platform Views`言う機能が提供されてます。
こちらは公式ドキュメントがしっかりと纏められているので、詳細については以下を御覧ください。

- [Hosting native Android and iOS views in your Flutter app with Platform Views](https://docs.flutter.dev/development/platform-integration/platform-views)

表題の使い分けについてですが、自分が調査した上での持論にはなりますが...以下のような使い分けが有りかな？と思ってます。

- `Platform Views`
    - 画面全体に表示を行い、操作する必要があるもの (e.g. 例えば`WebView`[^8]など)
    - **表示対象のネイティブ側のインスタンス(Viewなど)とWidgetの寿命が結びついても問題が無い場合**
- `Texture Widget`
    - **表示対象のネイティブ側のインスタンスとWidgetの寿命を分けて管理したい場合**
        - → 例えば表示しているWidgetが破棄されてもネイティブ側はインスタンスを残しておきたい時とか
    - 再描画のタイミングを制御したい

[^8]: 実際に公式のWebViewパッケージではPlatform Viewsが使われている

特に`Platform Views`は**「ネイティブ側の表示対象のインスタンス」と「Widgetの寿命」が結びついてしまいやすい印象を受けました。**
(実装側の工夫次第では回避できるかもだが...)

例えば動画プレイヤーの要件として「画面遷移しても裏で動画を再生し続けたい」と言った実装を行いたいなら、今回みたいな`Texture Widget`ベースの実装がやりやすいのかな？とは思います。
→ 動画を再生している`Texture Widget`自体が破棄されても「ネイティブ側の動画プレイヤー」自体は破棄されていないので

もし「自分はこう考えている・使い分けている」と言った例があれば教えて頂けると幸いです！ :bow: 




# 参考/関連リンク

## 参考資料

- **Texture Widget**
    - [Flutterで動画を再生するためには](https://ca-base-next.cyberagent.co.jp/sessions/flutter-player/)
        - [「Web」「iOS」「Android」でFlutterで作った「video_player」を表示する方法](https://logmi.jp/tech/articles/324823)
    - [Flutterで動画配信するプラグインを作った話](https://speakerdeck.com/espresso3389/flutterdedong-hua-pei-xin-surupuraguinwozuo-tutahua)
    - [OpenGL with Texture widget](https://medium.com/@german_saprykin/opengl-with-texture-widget-f919743d25d9)
    - [Flutter 实时视频渲染：Texture与PlatformView](https://zhuanlan.zhihu.com/p/62821195)
- **Platform Views**
    - [FlutterのPlatformViewを理解する](https://itome.team/blog/2019/12/flutter-advent-calendar-day13/)
    - [Flutterの最後の秘宝、Platform Viewの実装を調べに行く](https://qiita.com/kikuchy/items/397b7664ad32656748bf)
    - [[Flutter] Platform Viewsの仕組みとパフォーマンス影響を理解して利用する](https://qiita.com/kurun_pan/items/ec676eabac2033bbde0f)


## pub.dev

- [video_player](https://pub.dev/packages/video_player)
- [camera](https://pub.dev/packages/camera)
    - ※ 上記2点とも内部的に`Texture Widget`を利用している

## Documents

- [Texture class](https://api.flutter.dev/flutter/widgets/Texture-class.html)
    - [TextureLayer class](https://api.flutter.dev/flutter/rendering/TextureLayer-class.html)
- [Hosting native Android and iOS views in your Flutter app with Platform Views](https://docs.flutter.dev/development/platform-integration/platform-views)
- [Developing packages & plugins](https://docs.flutter.dev/development/packages-and-plugins/developing-packages)