icon-fb icon-tw icon-index_hero icon-cb

iPhoneでもwebVRで全天球動画を再生する方法

iPhoneでもwebで全天球動画を再生

webVRでの全天球動画の再生は、videoタグに設定した360動画をThree.jsで球体の内側に貼り付ける、いわゆるSkySphere方式というやり方が一般的です。
動画の方式は「正距円筒図法」というもので、これはYouTubeなどで採用されているものなので、YouTubeから360度動画を落としてくれば、360度カメラを持っていなくてもwebVR全天球動画コンテンツを試作することが出来ます。

texture
正距円筒図法

 
が、この方法にはひとつ大きな問題が。

iPhoneで使えない。

iPhoneはvideoタグを再生すると強制的に全画面プレイヤーになってしまうので、ページ内でインライン再生できずThree.jsのテクスチャとして使用することが出来ません。

今回は、いくつかのTipsを組み合わせて、iPhoneでもwebVR全天球動画を実現する方法を考えてみました。

 

【追記】
今秋リリース予定のSafari10では、iOSでのインラインプレイがサポートされ、ハック的な再生なしに普通に動画を再生することができるようになるそうです!
→次期「Safari 10.0」で新しく追加される機能まとめ – GIGAZINE
となると、この記事は秋には必要無くなる情報ですが、こういう対応しなくてもよくなるのは良いことですね!!
@gtk2kさんから情報をいただきました。ありがとうございました!!

今回のゴール

まず出来上がりはこちら。

→ 360MOVIE for iOS | CardboardClub
qr_img

スマホを横持ちにして、Play/Pauseボタンを押して右下のカードボードアイコンをタップ。
(使用した動画の冒頭が白バックなので最初アイコン見えないですすみません)
ブラウザのメニューバーやボタンは上にスワイプで消えます。
止めたくなったら下に(少し長めに)スワイプでボタンを出してタップしてください。

 

参考記事

Three.jsの全天球実装も含め、参考にしたのは下記です。

・iPhoneでの動画のインライン再生
→ iPhone Safariで動画をインライン再生する方法 ー Qiita
→【音声対応】 iPhone Safariで動画をインライン再生する方法続き ー Qiita

・全天球画像貼り付け
→ Three.js / examples / panorama / equirectangular

・動画をThree.jsでテクスチャとして使用
→ Three.jsで動画をテクスチャに指定する ー Qiita

・WebVR Boilerplate(コンテンツをVR化)
→ WebVR はじめよう ー Qiita
先日ハンズオンで導入を試したWebVR Boilerplateを早速導入してみました。

 
 

ソース

で、これらを組み合わせたメインのソースはこちら

var videoFile="sample_1280.mp4";

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000 );
var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild(renderer.domElement);

//WebVR Boilerplate
var controls = new THREE.VRControls(camera);
controls.standing = true;
var effect = new THREE.VREffect(renderer);
effect.setSize(window.innerWidth, window.innerHeight);
var manager = new WebVRManager(renderer, effect);

window.addEventListener('resize', onResize, true);
window.addEventListener('vrdisplaypresentchange', onResize, true);

var btn = document.querySelector('button');
btn.disabled = true;

var audio = new Audio();
var video = document.createElement('video');
var canvas = document.createElement('canvas');
canvas.width = 1280;
canvas.height = 640;
var ctx = canvas.getContext('2d');
var togglePlay;
var ua = navigator.userAgent;
var mode = 'none';

if(/(iPhone|iPod)/.test(ua)) { // iPhoneでvideoをインライン再生
    //ctx.scale(0.5,0.5);
    var prms1 = new Promise(function(resolve, reject) {
        video.addEventListener('canplay',function(){
            resolve();
        });
        video.addEventListener('error',function(){
            reject();
            alert('failed loading video');
        });
    });
    var prms2 = new Promise(function(resolve, reject) {
        audio.addEventListener('canplay',function(){
            resolve();
        });
        audio.addEventListener('error',function(){
            reject();
            alert('failed loading audio');
        });
    });
    Promise.all([prms1,prms2]).then(function(){
        btn.disabled = false;
        mode = 'currentTime';
        makeSkybox();
    });
    video.src = videoFile;
    video.load();
    audio.src = videoFile;
    audio.load();

    togglePlay = function(){
      if(audio.paused){
        audio.play();
      } else {
        audio.pause();
      }
    };
} else { // Androidなどは素直にVideoタグで再生
    //video.style.display = 'block';
    video.src = videoFile;
    video.load();
    video.addEventListener('canplay',function(){
      btn.disabled = false;
      mode = 'defaultPlay';
      makeSkybox();
    },false);
    video.addEventListener('error',function(){
        alert('failed loading video');
    });

    togglePlay = function(){
      if(video.paused){
        video.play();
      } else {
        video.pause();
      }
    };
}
btn.addEventListener('click',togglePlay);

//生成したcanvasをtextureとしてTHREE.Textureオブジェクトを生成
var videoTexture = new THREE.Texture(canvas);
videoTexture.minFilter = THREE.LinearFilter;
videoTexture.magFilter = THREE.LinearFilter;

//生成したtextureをmapに指定し、overdrawをtureにしてマテリアルを生成
var sky;
function makeSkybox(){
  var material = new THREE.MeshBasicMaterial({map: videoTexture, overdraw: true, side:THREE.DoubleSide});
  var geometry = new THREE.SphereGeometry( 500, 20, 20 );
  geometry.scale( - 1, 1, 1 );
  sky = new THREE.Mesh( geometry, material );
  scene.add( sky );
}

function render() {
  if (video.readyState === video.HAVE_ENOUGH_DATA) {
    //videoImageContext.drawImage(video, 0, 0);
    if (videoTexture) {
      videoTexture.needsUpdate = true;
    }

    if(mode == "currentTime")video.currentTime = audio.currentTime;
    ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
  }

  requestAnimationFrame(render);
  controls.update();//step1 control

  // renderer.render(scene, camera);
  manager.render(scene, camera);//step3
}
render();

function onResize(e) {
  effect.setSize(window.innerWidth, window.innerHeight);
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
}

 

要約

ざっくり言うと、映像と音を別ファイルとして読み込んで(ファイルは同じで大丈夫です)、音のほうを再生。
ループ部分で音の再生フレームを取得し、映像のフレームを同期。(映像は再生していないので全画面プレイヤーにはなりません)
それをcanvasに書き出して、Three.jsでテクスチャーとして球体の裏面に貼り付け。
という流れですね。
繋げて読むとややこしそうですが、順を追って組んでいくと結構シンプルな作りで実現できました。

動画は、スペックによってコマ落ちなどはありますが、一応、【1920px × 960px】まで読み込むことが出来ました。(上記サンプルは1280×640を使用しています)

これで、webVRの動画再生サイトもiPhoneを推奨環境から外さないでやれそうですね!(動画サイト作る予定はまだないですが)