概要
Unity + WebGL で動画をテクスチャとして使いたい場合、標準のVideoPlayerがうまく使えません。
そこで今回は動画をWebGLテクスチャとして描画しつつ、任意の時間から再生できるようにするための要点についてまとめておきます。
気をつけること
- iOSで再生するにはvideoタグに
playinline
属性が必要 - 自動再生するためには muted している必要がある
- 外部サーバーに置く場合はCORS対応すること。もしくは StreamingAssets に配置する
- Unity内でのボタンを押しても、音を出すためのブラウザの許可を取れない
- サーバーは Range Request に対応していること
- 動画ファイルはストリーム配信向けのものであること
- ローカルで試す場合は Range Request と gzip の content-type を適切に返せるように設定すること
- Unityのプロジェクトのカラースペースはリニアではなく、ガンマにしておくこと
方法1 VideoPlayerを使う場合(Safari非対応)
Unity標準の VideoPlayer
を使う場合は clip
は使えないため url
を利用します。
そして任意の時間から再生するにはUnityそのままのやり方でググれば色々と出てきます。
player.time = 30; 再生したい秒数
player.Play();
ただし、Safariで動画再生を行うには、playinline
属性が必要なことと、自動再生するにはミュートしておく必要があるため VideoPlayer はそのまま使うことが難しいです。
VideoPlayerはWebGLではvideoタグとして生成されますが、DOMツリーに追加されないため下記のように生成側のコードをいじる必要があり、保守性が下がるため今回はパスします。
方法2 : JSからレンダリングする
ではvideoタグはJavaScript側で生成してやれば細かい制御が可能です。
Unity側にレンダリングするにはWebGLから直接は難しいため、Plugins/jslib
にてマッピングします。
ここでは Unity + WebGL の基本的な構造はこちらのサイト様を参考に進めています。
なのですが、動画のテクスチャマッピングについては OpenGLのテクスチャ番号からWebGLのテクスチャ番号のマッピングがうまくできなかったため、Plugins/*.jslib
で処理をしています。
まずはUnity側は以下のように Update() のときにテクスチャを描画するようにします。
再生中は動画を描画し、停止中は元々設定しているテクスチャを描画する感じにしています。
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に対応していない場合でも一応
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();
});
};
途中から再生するには現在位置を秒数で指定するだけです。
elem.currentTime = time;
本当は読み込みタイミングによってシーク可能かどうかチェックする必要があります。
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も行けるハズです。
まだパフォーマンスについては詳しく調べられていないのでスマホなどでは結構重くなります。
WebGLの注意点
・この処理では毎回テクスチャを作成していますが、本来はこの処理は不要なはずです。(何故か自分の環境ではうまく描画されず)
・上下が反転することがあるためフリップオプションを指定しています。戻しておかないと他のテクスチャが崩れたりします。
・アスペクト比を修正する必要がある場合は cavnas にコピーしてマッピングする方法がありますが、WebGL側で処理する方法がないか探しています。
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 している必要がある
const elem = document.getElementById("video_screen");
...
elem.muted = true;
elem.play(); // muted にしてから再生する
外部サーバーに置く場合はCORS対応すること。もしくは StreamingAssets に配置する
videoタグのsrcに指定して再生するだけなら問題ないのですが、WebGLでテクスチャマッピングするときに外部サイトのリソースであればCORS
チェックが行われます。
なので動画ファイルは StreamingAssets
においておくと良いでしょう。
elem.setAttribute("src", `StreamingAssets/${file}`);
Unity内でのボタンを押しても、音を出すためのブラウザの許可を取れない
ブラウザでは音声再生にはユーザのアクションを必要としますが、Unityのボタンを押してときにJavaScript関数を呼び出してもPermission denied
エラーになります。
仕方ないので window.ontouch
などを利用して音を出すようにします。
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は途中から再生をしてくれずに頭から再生を開始します。
動画ファイルはストリーミング配信向けのものであること
詳しくデータを確認していないですが、オンデマンド用にエンコードされた長時間の動画ファイルなど、ファイルを全て読み込まないと再生開始できないようになっている場合は再生できない場合があります。
その場合はストリーミング用の設定を使って書き出すと良いでしょう。
(おそらく動画のヘッダとかフラグメント化されているかそんな奴だったと思います)
ローカルで試す場合は 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 のところのカラースペースの設定です。