スマホのブラウザから加速度を取得するdevicemotionイベントが動かない問題(iOS, Android)

2020年05月18日

ブラウザから加速度センサーの値を見ることができるDeviceMotionEventを使う時に値が取得されなかったのでその解決方法です。

問題

スマホサイトでdevicemotionイベントによって加速度をを表示しようとしたらイベントが発生しませんでした。

ページ自体はReactで作っていて下のような感じです。このまま開発PCと同一ネットワーク内のスマホで192.168.100.17:3000にアクセスした時accelerationX, accelerationY, accelerationZの中身が初期値の0のまま更新されませんでした。

import React, { useEffect, useState } from 'react';

function App() {
  const [accelerationX, setAccelerationX] = useState<number>(0);
  const [accelerationY, setAccelerationY] = useState<number>(0);
  const [accelerationZ, setAccelerationZ] = useState<number>(0);

  useEffect(() => {
    window.addEventListener("devicemotion", (event: any) => {
      setAccelerationX(event.acceleration.x)
      setAccelerationY(event.acceleration.y)
      setAccelerationZ(event.acceleration.z)
    })
  })

  return (
    <div>
      <span>{accelerationX}</span>
      <span>{accelerationY}</span>
      <span>{accelerationZ}</span>
    </div>
  )
}

export default App;

解決方法(Android編)

まず使っているスマホで加速度表示サイトにアクセスして値が動くことを確認します。恐らく最近のスマホでは動かないことは無いと思いますが、動かなかったらそのブラウザは対応していません。最新版のGoogle Chromeをインストールしましょう。

そして次にHTTPSを使いましょうHTTPS=true npm startしてからhttps://192.168.100.17:3000にアクセスすると無事に表示されました。

なおこのことはW3Cにも書いてあります。

fire events only on secure browsing contexts [SECURE-CONTEXTS] ref: https://www.w3.org/TR/orientation-event/#security-and-privacy

localhostくらい許してくれよとは思います。

解決方法(iPhone編)

iPhoneの場合は素直にいってくれません。iOSのバージョンや使用するブラウザによっていろいろ変わってきます。未だに仕様策定中というのもあって動くサンプルが全然無くてちょっと厳しいです。

iOS 13からブラウザからセンサーにアクセスする場合はDeviceMotionEvent.requestPermission()関数を使って明示的に許可操作をしなければいけません。

また、罠としてDeviceMotionEvent.accelerationnullになって取得できないというものがありました。代わりに加速度を加味したDeviceMotionEvent.accelerationIncludingGravityを使用します。

実際のコードは以下のような様子となります。

import React, { useState } from 'react';

function App() {
  const [accelerationX, setAccelerationX] = useState<number>(0);
  const [accelerationY, setAccelerationY] = useState<number>(0);
  const [accelerationZ, setAccelerationZ] = useState<number>(0);

  const deviceMotionRequest = () => {
    if (DeviceMotionEvent.requestPermission) {
      DeviceMotionEvent.requestPermission()
        .then(permissionState => {
          if (permissionState === 'granted') {
            window.addEventListener("devicemotion", (event) => {
              if (!event.accelerationIncludingGravity) {
                alert('event.accelerationIncludingGravity is null');
                return;
              }
              setAccelerationX(event.accelerationIncludingGravity.x)
              setAccelerationY(event.accelerationIncludingGravity.y)
              setAccelerationZ(event.accelerationIncludingGravity.z)
            })
          }
        })
        .catch(console.error);
    } else {
      alert('DeviceMotionEvent.requestPermission is not found')
    }
  }

  return (
    <div>
      <button onClick={deviceMotionRequest} />
      <span>{accelerationX}</span>
      <span>{accelerationY}</span>
      <span>{accelerationZ}</span>
    </div>
  )
}

export default App;

ボタンをクリックしたらアクセスしていいか尋ねるダイアログボックスが出てきて許可すると値が表示されるようになります。

(おまけ) ブラウザで素直に使えるHTML/JS

上のコード片はReactですが需要ありそうなので<script>タグに直接書くタイプのコードも置いておきます。動作未検証で雰囲気で書いてます。

<html>
  <head>
    <title>DeviceMotion Test</title>
  </head>
  <body>
    <div>
      <button onClick="deviceMotionRequest()">Click</button>
      <span id="x">0</span>
      <span id="y">0</span>
      <span id="z">0</span>
    </div>
    <script>
      function deviceMotionRequest () {
        if (DeviceMotionEvent.requestPermission) {
          DeviceMotionEvent.requestPermission()
            .then(permissionState => {
              if (permissionState === 'granted') {
                window.addEventListener("devicemotion", function (event) {
                  if (!event.accelerationIncludingGravity) {
                    alert('event.accelerationIncludingGravity is null');
                    return;
                  }
                  document.getElementById('x').innerHTML = event.accelerationIncludingGravity.x;
                  document.getElementById('y').innerHTML = event.accelerationIncludingGravity.y;
                  document.getElementById('z').innerHTML = event.accelerationIncludingGravity.z;
                })
              }
            })
            .catch(console.error);
        } else {
          alert('DeviceMotionEvent.requestPermission is not found')
        }
      }
    </script>
  </body>
</html>