[HTML5] Unity + WebGLでも動画を任意のタイミングから再生がしたい!

# 概要

Unity + WebGL で動画をテクスチャとして使いたい場合、標準のVideoPlayerがうまく使えません。
そこで今回は動画をWebGLテクスチャとして描画しつつ、任意の時間から再生できるようにするための要点についてまとめておきます。

<img width=640 alt="スクリーンショット 2023-03-29 16.00.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91710/ad9cbe70-f914-c4da-26e3-8a6e2d3b3fad.png" />


気をつけること

1. iOSで再生するにはvideoタグに `playinline` 属性が必要
1. 自動再生するためには muted している必要がある
1. 外部サーバーに置く場合はCORS対応すること。もしくは StreamingAssets に配置する
1. Unity内でのボタンを押しても、音を出すためのブラウザの許可を取れない
1. サーバーは Range Request に対応していること
1. 動画ファイルはストリーム配信向けのものであること
1. ローカルで試す場合は Range Request と gzip の content-type を適切に返せるように設定すること
1. Unityのプロジェクトのカラースペースはリニアではなく、ガンマにしておくこと

# 方法1 VideoPlayerを使う場合(Safari非対応)

Unity標準の `VideoPlayer` を使う場合は `clip` は使えないため `url` を利用します。
そして任意の時間から再生するにはUnityそのままのやり方でググれば色々と出てきます。

```cshar:Unity
    player.time = 30; 再生したい秒数
    player.Play();
```

https://qiita.com/idempotence/items/90af40866ee4cf5f246d

https://nekojara.city/unity-webgl-video-player

ただし、Safariで動画再生を行うには、`playinline` 属性が必要なことと、自動再生するにはミュートしておく必要があるため VideoPlayer はそのまま使うことが難しいです。

VideoPlayerはWebGLではvideoタグとして生成されますが、DOMツリーに追加されないため下記のように生成側のコードをいじる必要があり、保守性が下がるため今回はパスします。

https://qiita.com/kazuki_kuriyama/items/2b66c3976abdf070566d

# 方法2 : JSからレンダリングする

ではvideoタグはJavaScript側で生成してやれば細かい制御が可能です。
Unity側にレンダリングするにはWebGLから直接は難しいため、`Plugins/jslib` にてマッピングします。

ここでは Unity + WebGL の基本的な構造はこちらのサイト様を参考に進めています。

https://qiita.com/yaegaki/items/71cdebc7798784aa3464

なのですが、動画のテクスチャマッピングについては OpenGLのテクスチャ番号からWebGLのテクスチャ番号のマッピングがうまくできなかったため、`Plugins/*.jslib` で処理をしています。

まずはUnity側は以下のように Update() のときにテクスチャを描画するようにします。
再生中は動画を描画し、停止中は元々設定しているテクスチャを描画する感じにしています。

```csharp:VideoScreen.cs
public class MainScreen : MonoBehaviour
{
    // 再生を開始するための関数
    [DllImport("__Internal")]
    private static extern void VideoScreenTest(string file, int time, Action onStarted, Action onStopped);

    // 描画するための関数
    [DllImport("__Internal")]
    private static extern void UpdateMainScreenTexture(int texture);

    // 何かボタンが押されたら動画を再生する
    public void VideoTest()
    {
      // bbb.mp4 の 1:52 から再生を開始する。
      VideoScreenTest("bbb.mp4", 1 * 60 + 52, onPublished, onStopped);
    }

    // 描画
    void Update()
    {
      if (playing) {
        if (_texture)
        {
          Destroy(_texture);
        }
        _texture = new Texture2D(1, 1, TextureFormat.ARGB32, false); // jslib側で再生成されるので空で良い
        UpdateMainScreenTexture((int)_texture.GetNativeTexturePtr());
        obj3d.material.mainTexture = _texture;
      }
      else
      {
        obj3d.material.mainTexture = defaultTexture;
      }
    }
}
```

動画の再生開始は js 側で行います。
Range Requestに対応していない場合でも一応

```javascript:video.js
export const VideoScreenTest = async (file, time, _onPublished, _onStopped) => {
  const elem = document.getElementById("video_screen");
  elem.setAttribute("style", "display:none;");
  elem.setAttribute("playsinline", "");
  elem.setAttribute("src", `StreamingAssets/${file}`);
  elem.muted = true;
　 elem.play();

        // Unityの方に表示する
        elem.currentTime = time;
        if (isSafari()) {
          const unmute = () => {
            elem.muted = false;
          }
          document.getElementById("unity-canvas").addEventListener("touchstart", unmute);
          alert("画面をタップすると音声がでます。");
        } else {
          elem.muted = false;
        }

        _onPublished();

  elem.addEventListener('ended', (event) => {
    _onStopped();
  });
};
```

途中から再生するには現在位置を秒数で指定するだけです。

```javascript
        elem.currentTime = time;
```

本当は読み込みタイミングによってシーク可能かどうかチェックする必要があります。

```javascript:video.js
   const timerange = elem.seekable;
    let start;
    let end;
    for (let count = 0; count < timerange.length; count++) {
      start = timerange.start(count);
      end = timerange.end(count);
      console.log(`progress ${count}: ${start} - ${end}`);
      if (start <= time && time <= end) {
        // シークできる!
      } else {
         // まだ再生できない。Range Request非対応の場合はここでエンドまでシークして続きを読み込ませて定期的にチェックする
      }
    }
```

続いてテクスチャマッピングして描画するところです。
WebGLで検索するとだいたいこの形だと思います。
ちなみに "video_screen" という要素はhtmlにvideoタグを予め作成してあります。
DOM要素を指定できるので動画以外にもcanvasも行けるハズです。
まだパフォーマンスについては詳しく調べられていないのでスマホなどでは結構重くなります。

:::note warn
WebGLの注意点
・この処理では毎回テクスチャを作成していますが、本来はこの処理は不要なはずです。(何故か自分の環境ではうまく描画されず)
・上下が反転することがあるためフリップオプションを指定しています。戻しておかないと他のテクスチャが崩れたりします。
・アスペクト比を修正する必要がある場合は cavnas にコピーしてマッピングする方法がありますが、WebGL側で処理する方法がないか探しています。
:::


```javascript:Plugins/main.jslib
var t = null;
plugin.UpdateMainScreenTexture = function (tex) {
  // set texture
  if (!t) { 
    GLctx.deleteTexture(GL.textures[tex]);
    t = GLctx.createTexture();
    t.name = tex;
    GL.textures[tex] = t;
  }
  elem = document.getElementById("video_screen");

  // target, texture
  GLctx.bindTexture(GLctx.TEXTURE_2D, GL.textures[tex]);
  GLctx.pixelStorei(GLctx.UNPACK_FLIP_Y_WEBGL, true); // flip up down.
  GLctx.texParameteri(GLctx.TEXTURE_2D, GLctx.TEXTURE_WRAP_S, GLctx.CLAMP_TO_EDGE);
  GLctx.texParameteri(GLctx.TEXTURE_2D, GLctx.TEXTURE_WRAP_T, GLctx.CLAMP_TO_EDGE);
  GLctx.texParameteri(GLctx.TEXTURE_2D, GLctx.TEXTURE_MIN_FILTER, GLctx.LINEAR);
  GLctx.texImage2D(GLctx.TEXTURE_2D, 0, GLctx.RGBA, GLctx.RGBA, GLctx.UNSIGNED_BYTE, elem);
  GLctx.pixelStorei(GLctx.UNPACK_FLIP_Y_WEBGL, false); // <-- 使ったらちゃんと戻すこと!
}
```

以上で動画を任意の時間からテクスチャマッピングで再生することができました!
以下は色々と気をつける点が多く、忘れがちなのでそれらについて確認していきます。


#### 自動再生するためには muted している必要がある

```javascript:video.js
  const elem = document.getElementById("video_screen");
   ...

  elem.muted = true;
  elem.play(); // muted にしてから再生する
```

#### 外部サーバーに置く場合はCORS対応すること。もしくは StreamingAssets に配置する

videoタグのsrcに指定して再生するだけなら問題ないのですが、WebGLでテクスチャマッピングするときに外部サイトのリソースであれば`CORS`チェックが行われます。
なので動画ファイルは `StreamingAssets` においておくと良いでしょう。


```javascript: video.js
  elem.setAttribute("src", `StreamingAssets/${file}`);
```

#### Unity内でのボタンを押しても、音を出すためのブラウザの許可を取れない

ブラウザでは音声再生にはユーザのアクションを必要としますが、Unityのボタンを押してときにJavaScript関数を呼び出しても`Permission denied`エラーになります。
仕方ないので `window.ontouch` などを利用して音を出すようにします。

```javascript:video.js

        if (isSafari()) {
          const unmute = () => {
            elem.muted = false;
          }
          document.getElementById("unity-canvas").addEventListener("touchstart", unmute);
          alert("画面をタップすると音声がでます。");
        } else {
          elem.muted = false;
        }
```

#### サーバーは Range Request に対応していること

safariではそもそも`Range Request`の対応が必須です。
また Chromeは途中から再生をしてくれずに頭から再生を開始します。

https://qiita.com/gecko655/items/110373a6c88177d23126

#### 動画ファイルはストリーミング配信向けのものであること

詳しくデータを確認していないですが、オンデマンド用にエンコードされた長時間の動画ファイルなど、ファイルを全て読み込まないと再生開始できないようになっている場合は再生できない場合があります。
その場合はストリーミング用の設定を使って書き出すと良いでしょう。
(おそらく動画のヘッダとかフラグメント化されているかそんな奴だったと思います)

#### ローカルで試す場合は Range Request と gzip の content-type を適切に返せるように設定すること

Unityがビルドしたときに実行されるローカルサーバーはRange Requestに対応していません。
また、 `python -m http.server` や `npm -g install serve` などローカルでお手軽に動かせるhttpサーバーは多いですが、
Range Request に対応させつつ gzip ファイルの `Content-Type` をきちんと返してくれるようにするのはなかなか面倒です。

`nginx` や`S3` など動作確認用のサーバーを立てちゃった方がスマホなどからも動作確認しやすいかなと思います。


#### Unityのプロジェクトのカラースペースはリニアではなく、ガンマにしておくこと

これを設定していないと色味が少しおかしくなります。
Build Settings -> WebGL -> Player Settings -> Player -> Other Setting -> Rendering のところのカラースペースの設定です。