【Unity】WebGLで動画再生のフォールバックを実現する

こじゃらこじゃら

WebGL環境でWebM形式の動画再生に対応したいの…
再生できない時だけMP4動画に切り替えたりできないの?

このはこのは

JavaScriptのネイティブコードを書く事になるけどフォールバックの仕組みを実装すれば良いわ。

UnityのWebGLビルドで複数形式の動画再生をフォールバックで対応する方法の解説記事です。

本記事の内容を実践すると、ブラウザによって最適な形式の動画を選択して再生することが可能になります。

例えば、HTMLの<video>タグによるWebM形式動画の再生では、Opera miniは非対応iOS版Safariは部分的に対応というステータスになっています。

参考:“WebM” | Can I use… Support tables for HTML5, CSS3, etc

このような場合、WebM対応ブラウザではWebM形式動画非対応ブラウザではMP4形式動画フォールバックして再生させれば良いです。

これは、HTMLで言う<video>と<source>タグによるフォールバックに似ています。

<video autoplay muted playsinline>
    <source type="video/webm" src="[ストレージサーバーのURL]/movie.webm">
    <source type="video/mp4" src="[ストレージサーバーのURL]/movie.mp4">
</video>

参考:<video>: 動画埋め込み要素 – HTML: ハイパーテキストマークアップ言語 | MDN

本記事では、このように複数形式の動画再生をフォールバックで対応させる方法を解説していきます。動画はVideo Playerコンポーネントで再生するものとします。

注意

Video PlayerはUnityバージョンによっては特定プラットフォームのブラウザで再生されない不具合があることにご注意ください。

iOSやAndroidなどモバイル環境に対応する場合は、Unity 2022.1以降を使用する必要があります。

動作環境
  • Unity 2023.1.8f1

スポンサーリンク

前提条件

動画再生にはVideo Playerコンポーネントを使用して再生するものとします。

例では、Quadオブジェクトを配置し、同オブジェクトにVideo Playerコンポーネントをアタッチし、Render ModeをMaterial Overrideに設定し、マテリアルテクスチャに動画の内容を反映するものとします。

これはRender ModeをRender Textureに設定し、Render Texture経由で描画する方法でも問題ありません。

Video PlayerコンポーネントのPlay On Awakeは予めOFFにしてください。

WebGL環境におけるVideo Playerの基本的な使い方は、以下記事で解説しています。

実装の流れ

フォールバックによる動画再生の流れは次のようになります。

動画再生の流れ
  • 動画URLと動画形式のペアリストとして用意する
  • リストの先頭から順に次の処理を繰り返す
    • 動画形式ブラウザでサポートされているか判定
    • 再生可能なら再生、そうでなければ次の要素に進む
    • 正常に再生出来たら処理を終了、そうでなければ次の要素に進む
  • 全ての要素の動画再生に失敗したら、再生不可とする

上記の実装の流れについて順番に解説していきます。

動画形式がブラウザでサポートされているかチェックする

JavaScript側での実装になりますが、HTMLMediaElement.canPlayTypeメソッドで判定できます。

canPlayType(type)

引数には、「video/webm」「video/mp4」などの動画形式の文字列を指定します。

戻り値は、次の3種類です。

  • 空文字 – 再生不可
  • probably – おそらく再生可能
  • maybe – 再生可能かどうかの十分な情報なし

したがって、空文字の場合は再生不可それ以外の場合は実際にVideo Playerなどで動画再生するまでは分からないとみなせば良いでしょう。

このメソッドはJavaScriptネイティブで使う必要があるため、Unityから使うには次のようにjslibスクリプトとして実装する必要があります。

mergeInto(LibraryManager.library, {
  CanPlayType: function(format) {
    // <video>タグを作成
    var video = document.createElement('video');
    
    if (typeof video.canPlayType === 'function') {
      var result = video.canPlayType(UTF8ToString(format));
      
      // 結果をJS文字列をUTF8に変換して返す
      var bufferSize = lengthBytesUTF8(result) + 1;
      var buffer = _malloc(bufferSize);
      stringToUTF8(result, buffer, bufferSize);
      return buffer;
    }
    
    // 非対応の結果を返す
    return 0;
  }
});
注意

Unity 2021.2以降からは、C#から引数で受け取った文字列はUTF8ToString関数を使うように変更されています。

var str = UTF8ToString(arg);

参考:Interaction with browser scripting – Unity マニュアル

Pointer_stringify関数は将来的に廃止される予定なので、古いUnityバージョンからアップグレードした場合などは、この処理を置き換える必要があります。

参考:Upgrading to Unity 2021.2 – Unity マニュアル

UnityのC#スクリプトからは、次のように呼び出して使います。

// .jslibで実装したメソッドを使えるようにする
[DllImport("__Internal")]
private static extern string CanPlayType(string format);

・・・(中略)・・・

// ブラウザがサポートしている動画形式かどうかを判定
string canPlay = CanPlayType("video/webm");

Video Playerで対象動画を再生する

ブラウザで再生できる可能性がある結果が得られた場合は、Video Playerコンポーネントに対して動画を設定して再生すれば良いです。

VideoPlayer videoPlayer;

・・・(中略)・・・

videoPlayer.source = VideoSource.Url;
videoPlayer.url = "https://ドメイン/movie.webm";
videoPlayer.Play();

ここで正常再生できるとエラーにはなりませんが、失敗するとVideoPlayer.errorReceivedプロパティからエラーイベントが通知されます。

参考:Video.VideoPlayer-errorReceived – Unity スクリプトリファレンス

ただし、当環境でビルドして検証したところ、同一の動画に対して2度エラー通知が来る状況でしたので、次のように特定メッセージの時だけフォールバックの処理に進むこととします。

videoPlayer.errorReceived += (source, message) =>
{
    Debug.LogError($"VideoPlayer Error: {message}");
    
    // 2回エラー通知が来ることがあるため、URLをチェック
    if(!message.Contains(source.url))
        return;

    // 次の動画を再生

実際に拾うエラーメッセージは次の文言になっています。

VideoPlayer Error: VideoPlayer cannot play url : 動画へのURL

フォールバックの実装

前述の動画形式のサポートチェックで結果が空文字だった場合、または動画再生に失敗してエラー通知を受け取った場合は再生不可であることが確定したと判断できます。

この場合、次の動画に対して同様にサポートチェック→動画再生の順に試みます。

これを成功するまで繰り返し、再生候補の動画が無くなったら再生失敗とみなします。基本的に最終候補はすべてのブラウザで絶対再生可能な動画であることが望ましいです。

サンプルスクリプト

ここまでの流れを踏まえ、動画形式のサポートチェック、動画再生、およびフォールバックを全て実装した例を示します。

スクリプトが2つあることにご注意ください。

FallbackVideoPlayer.cs
using System;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.Video;

public class FallbackVideoPlayer : MonoBehaviour
{
    // 再生対象のVideoPlayer
    [SerializeField] private VideoPlayer _videoPlayer;

    // 動画の形式とURLの情報
    [Serializable]
    private struct VideoInfo
    {
        // 動画形式
        public string format;

        // 動画URL
        public string url;
    }

    // 動画のフォールバック情報
    [SerializeField] private VideoInfo[] _videos;

    // 起動時に再生するかどうか
    [SerializeField] private bool _playOnAwake = true;

    // ブラウザの動画サポートチェック
#if UNITY_WEBGL && !UNITY_EDITOR
    // WebGLの場合は、JavaScriptの関数を呼び出す
    [DllImport("__Internal")]
    private static extern string CanPlayType(string format);
#else
    // WebGL以外の場合は、再生するまで未確定とする
    private static string CanPlayType(string format)
    {
        return "maybe";
    }
#endif

    private int _currentVideoIndex;

    /// <summary>
    /// フォールバックを考慮して動画を再生する
    /// </summary>
    public void Play()
    {
        if (!enabled) return;

        // リソースをURLに設定
        // VideoClipはWebGLでは使用不可
        _videoPlayer.source = VideoSource.Url;

        // 動画を再生する
        _currentVideoIndex = 0;
        TryPlayVideo();
    }

    private void Awake()
    {
        if (_playOnAwake)
            Play();
    }

    private void OnEnable()
    {
        _videoPlayer.errorReceived += OnErrorReceived;
    }

    private void OnDisable()
    {
        _videoPlayer.errorReceived -= OnErrorReceived;
    }

    // 動画再生を試みる
    private void TryPlayVideo()
    {
        while (_currentVideoIndex < _videos.Length)
        {
            var video = _videos[_currentVideoIndex];

            // ブラウザで再生できるかどうかをチェック
            var canPlayType = CanPlayType(video.format);

            // 再生可能な場合は、VideoPlayerに動画を設定して再生
            if (!string.IsNullOrEmpty(canPlayType))
            {
                _videoPlayer.url = video.url;
                _videoPlayer.Play();
                break;
            }

            // 再生不可の場合は次の候補の動画に移る
            _currentVideoIndex++;
        }
    }

    // VideoPlayerの再生でエラーが発生したときに呼び出される
    private void OnErrorReceived(VideoPlayer source, string message)
    {
        // 2回エラー通知が来ることがあるため、URLが含まれるメッセージだけを通過
        if (!message.Contains(source.url))
            return;

        // 次の動画を再生
        _currentVideoIndex++;
        if (_currentVideoIndex >= _videos.Length)
            return;

        TryPlayVideo();
    }
}
CanPlayType.jslib
mergeInto(LibraryManager.library, {
  CanPlayType: function(format) {
    var video = document.createElement('video');
    
    if (typeof video.canPlayType === 'function') {
      var result = video.canPlayType(UTF8ToString(format));
      
      var bufferSize = lengthBytesUTF8(result) + 1;
      var buffer = _malloc(bufferSize);
      stringToUTF8(result, buffer, bufferSize);
      return buffer;
    }
    
    return 0;
  }
});

上記スクリプトをそれぞれ次のようなパスに保存してください。

  • FallbackVideoPlayer.cs
    • Assets配下のPlugins以外のフォルダ
    • 例 : Assets/Scripts/FallbackVideoPlayer.cs
  • CanPlayType.jslib
    • Plugins配下のフォルダ
    • 例 : Assets/Plugins/WebGL/CanPlayType.jslib

以下、配置パスの例です。

Unityプロジェクトルート
 └─Assets/
      ├─Scripts
      │   └FallbackVideoPlayer.cs
      └─Plugins
          └WebGL
             └CanPlayType.jslib

参考:プラグインのインポートと設定 – Unity マニュアル

保存したら、適当なゲームオブジェクトにFallbackVideoPlayerをアタッチし、インスペクターより次の項目を設定してください。

  • Video PlayerVideo Playerコンポーネント
  • Videos – フォールバック対象の動画情報
    • Format動画形式(video/webm、video/mp4など)
    • Url動画のURL

以下、設定例です。

チェック

設定が完了したら、一度Unityエディタで正しく再生できるか確認することをお勧めします。

本記事のサンプルはエディタ上でも再生できるように設計しています。

本記事では、WebGLビルドした後、同一ドメインのサーバーにビルドファイルを配置してプレイするものとします。

以下、サーバー側の配置例です。

path/to/server/dir
 ├─WebGLのビルドフォルダ
 └─videos
      ├─video.webm
      └─video.mp4

実行結果

URLを正しく設定し、CORSの設定も問題なくできれば、次のように動画が再生されるようになります。

例えば、最初の候補の動画(例ではWebM形式)が再生できなかった場合、次の候補の動画(例ではMP4形式)を再生するような挙動になります。

例ではわかりやすさのため、動画形式の文字列を下に表示しています。

再生されなかった場合

正しく再生されない場合ブラウザのコンソールログを確認するなどで原因を特定する必要があります。

例えば、CORS設定が正しくない場合は次のようなエラーが出ます。

Access to video at 'https://your-domain/video.mp4' from origin 'http://localhost:55546' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

URLが正しくないなどで動画が見つからない場合は次のような404エラーが出ます。

GET https://your-domain/video.mp4 404 (Not Found)

また、iOSなど主にモバイルプラットフォームでのみ再生できない場合は、Unityバージョンが2021以前であるなどの要因で、Video Player側の不具合で再生不可になっている可能性があります。

スクリプトの説明

.jslibファイル側では、ブラウザチェックのために、まず<video>タグを生成します。

var video = document.createElement('video');

Unity C#側から受け取る文字列は、UTF8ToString関数で変換する必要があります。

var result = video.canPlayType(UTF8ToString(format));

そして、得られた結果の文字列をUnity C#が解釈できる形式に変換するため、以下処理でバッファを確保して結果を格納しています。

var bufferSize = lengthBytesUTF8(result) + 1;
var buffer = _malloc(bufferSize);
stringToUTF8(result, buffer, bufferSize);
return buffer;
注意

上記処理が走る度に、タグ生成や結果の格納用バッファ確保などによりGCが発生することにご注意ください。

例えば<video>タグの生成を抑えたい場合は、初回だけタグを生成してグローバル変数に格納しておき、2回目以降はそのタグを使いまわすといった方法が考えられます。

上記関数は、Unity C#側ではDllImport属性でインポートして使います。

    // ブラウザの動画サポートチェック
#if UNITY_WEBGL && !UNITY_EDITOR
    // WebGLの場合は、JavaScriptの関数を呼び出す
    [DllImport("__Internal")]
    private static extern string CanPlayType(string format);
#else
    // WebGL以外の場合は、再生するまで未確定とする
    private static string CanPlayType(string format)
    {
        return "maybe";
    }
#endif

Unityエディタ時でも動作するように、WebGLのビルド環境以外では常に「maybe」を返すダミー処理としています。

Unity C#側では、前準備としてVideoPlayerの動画再生に失敗したときにエラーを受け取れるようにするため、次の処理でコールバックを登録しています。

private void OnEnable()
{
    _videoPlayer.errorReceived += OnErrorReceived;
}

private void OnDisable()
{
    _videoPlayer.errorReceived -= OnErrorReceived;
}

参考:Video.VideoPlayer-errorReceived – Unity スクリプトリファレンス

動画再生時では、まずブラウザでのサポートチェックを実施します。その後、再生できる可能性がある場合だけVideoPlayer.Playメソッドで再生を試みます。

再生できない場合は次の候補の動画にフォールバックします。

// 動画再生を試みる
private void TryPlayVideo()
{
    while (_currentVideoIndex < _videos.Length)
    {
        var video = _videos[_currentVideoIndex];

        // ブラウザで再生できるかどうかをチェック
        var canPlayType = CanPlayType(video.format);

        // 再生可能な場合は、VideoPlayerに動画を設定して再生
        if (!string.IsNullOrEmpty(canPlayType))
        {
            _videoPlayer.url = video.url;
            _videoPlayer.Play();
            break;
        }

        // 再生不可の場合は次の候補の動画に移る
        _currentVideoIndex++;
    }
}

VideoPlayerによる再生でエラーが発生した場合は再生不可とみなします。これは、前述のVideoPlayer.errorReceivedイベントとして受け取ります。

このコールバックでは、次の候補の動画にフォールバックする処理を行っています。

// VideoPlayerの再生でエラーが発生したときに呼び出される
private void OnErrorReceived(VideoPlayer source, string message)
{
    // 2回エラー通知が来ることがあるため、URLが含まれるメッセージだけを通過
    if (!message.Contains(source.url))
        return;

    // 次の動画を再生
    _currentVideoIndex++;
    if (_currentVideoIndex >= _videos.Length)
        return;

    TryPlayVideo();
}

1つの動画再生では複数のエラー通知が発行される可能性があるため、次の内容のエラーだけ通過させるように対策しています。

VideoPlayer cannot play url : https://your-domain/video.webm

Cannot read file.

ブラウザサポートのチェックを行う必要性

ここまで解説した方法では、ブラウザの動画形式サポートチェック→動画再生という順に処理を行っていました。

実は、動画形式のサポートチェックを行わなくてもフォールバック再生のロジックは実装可能です。

ただし、その場合は非対応動画にも関わらず動画をダウンロードする処理が走ってしまいます。これは、無駄に通信帯域を消費することに繋がります。

以下は、HLS形式動画の再生に失敗してWebM形式動画にフォールバックする例です。

事前に動画形式のサポートチェックを行う事で、もし再生不可の動画ならダウンロードせずに次の動画にフォールバックさせる事が可能になります。

HLSの場合は、動画形式に「application/vnd.apple.mpegurl」を指定すれば良いです。

WebGL以外のプラットフォームでの挙動

本記事で解説した方法は、WebGL以外の環境でも動作します。

ただし、WebGL環境とは違い、必ず動画のダウンロード処理が走ることにご注意ください。

さいごに

WebGL環境における動画のフォールバック再生は、ブラウザの動画形式サポートチェックサポートされていれば動画再生を試みるエラーなら次の候補の動画に対して前述の処理を実施するというロジックで実現できます。

JavaScriptコードを書く必要がありますが、無駄な通信帯域を消費しないためにはブラウザのサポートチェックを挟むのが望ましいです。

サポートチェックは必ず再生可能を保証するものではなく、実際に動画再生してエラーにならないかのチェックも必要な事にご注意ください。

関連記事

参考サイト

スポンサーリンク