【お詫びと訂正】iOS11でついにSafariからカメラにアクセスできるようになりました。(コピペで動くサンプルコード付き)

お詫びと訂正:(2017/06/08 23:32)
本記事内に掲載しているスクリーンショットが、
Apple.Incの開発者利用規約(APPLE BETA SOFTWARE PROGRAM AGREEMENT APPLE INC.)に抵触しているというご指摘をいただきました。
Apple.Incの開発者利用規約から、掲載内容を不適切と判断し、該当箇所を削除させていただきました。

Apple.Inc及び読者の皆様に深くお詫び申し上げます。

追記:(2019/02/17)
Navigator.getUserMediaが非推奨となったため、MediaDevices.getUserMediaを使うように修正しました。

Navigator.getUserMedia - Web API | MDN
MediaDevices.getUserMedia() - Web API | MDN

ざっくり1行でまとめると

  • iOS11のSafariがカメラにアクセスできるようになったから試しました。^ ^

f:id:kimizuka:20170608231415p:plain

はじめに

こんにちは。フロントエンジニアの @ki_230 です。
WWDC、盛り上がりましたね。寝不足の方も多いのではないでしょうか。

iMac Pro、iPad Pro 10.5インチモデル、HomePodとハードウェア系の発表もてんこ盛りでしたが、
個人的には、マークアップを担当することが多いのでiOS11のSafariの性能がとても気になっていました。

早速、手元の検証機をiOS11にしてSafariの設定画面を確認してみると。。。


f:id:kimizuka:20170608231415p:plain

むむむ。

「カメラとマイクのアクセス」という項目が!



Safari 11.0の新機能を確認してみると。。。

New in Safari 11.0 – Camera and microphone access.
 Added support for the Media Capture API.
 Websites can access camera and microphone streams from a user's device (user permission is required.)

https://developer.apple.com/library/content/releasenotes/General/WhatsNewInSafari/Safari_11_0/Safari_11_0.html より引用


むむむむむ。

どうやらカメラとマイクにアクセスできるようになった模様です!

個人的には特にカメラがうれしい。
FileAPIで撮影した写真にアクセスしたりするのではなく、
カメラにリアルタイムにアクセスできるとなると夢が広がりますね。
なので、今回は早速カメラにアクセスしてサイト上でプレビューするコードを書いてみましょう。
※ 簡単に試すためにマイクにはアクセスしていません。

ちなみに、httpsの環境でないと動作しないので注意が必要です。


ソースコード

ちなみに今回のコード(と、試行錯誤した様子のコミットログ)は こちら にアップされています。

github.com


それでは順に解説していきましょう。

1. getUserMediaでstreamを取得してvideoタグでプレビューする

まずは単純にカメラにアクセスしてブラウザでリアルタイムにプレビューすることを目指します。

HTML
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>Video</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, shrink-to-fit=no" />
  <link rel="stylesheet" href="index.css" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
</head>
<body>
  <video id="video" autoplay playsinline></video>
  <script src="index.js"></script>
</body>
</html>
JavaScript
const medias = {
  audio : false,
  video : true
};
const video = document.getElementById("video");
const promise = navigator.mediaDevices.getUserMedia(medias);

promise.then(successCallback)
       .then(errorCallback);

function successCallback(stream) {
  video.srcObject = stream;
};

function errorCallback(err) {
  alert(err);
};
CSS
body {
  margin: 0;
  background: #000;
}

#video {
  display: block;
  width: 100%;
}


なにも考えなければ、これでOKです。

サンプルURLとQRコード

https://goo.gl/ijYwQs
f:id:kimizuka:20170606161103p:plain

ちなみにgetUserMediaのmediasのvideoにtrueを渡すとフロントカメラを取得するようです。
もしもリアカメラを取得したい場合は、

const medias = {audio : false, video : true},

の部分を、

const medias = {audio : false, video : {
    facingMode : {
      exact : "environment"
    }
  }},

に変更すればリアカメラにアクセスできます。

ただし、リアカメラのストリームをvideoタグにいれるとポートフォリオで見たときに、何故か天地が逆転してしまったので、CSSでvideoタグをひっくり返しておきました。

追記:(2019/02/17)
iOSのバグだったようでいつのまにか反転しなくなってました。

Navigator.getUserMedia - Web API | MDN
MediaDevices.getUserMedia() - Web API | MDN

サンプルURLとQRコード

https://goo.gl/rQLDt9
f:id:kimizuka:20170606161304p:plain

f:id:kimizuka:20170608231415p:plain

リアルタイムプレビューできましたね。^ ^
(もっと良い動画が取れると良かったんですが。。。)

フロントカメラを起動すると自分の顔ばかりが映っておもしろくないので、
これからのサンプルはリアカメラを起動していこうと思います。

2. getUserMediaでstreamを取得してvideoタグでプレビューし、CanvasにdrawImageする

次にカメラのリアルタイムの映像をCanvasに書き出し続けてみます。
videoタグをdisplay: noneにすると動かなかったので、Canvasの上にワイプのように表示してみました。

HTML
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>Canvas</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, shrink-to-fit=no" />
  <link rel="stylesheet" href="index.css" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
</head>
<body>
  <canvas id="canvas"></canvas>
  <div id="videobox">
    <video id="video" autoplay playsinline></video>
  </div>
  <script src="index.js"></script>
</body>
</html>
JavaScript
const medias = {
  audio: false,
  video: {
    facingMode: {
      exact: "environment"
    }
  }
};
const video = document.getElementById("video");
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const promise = navigator.mediaDevices.getUserMedia(medias);

promise.then(successCallback)
       .catch(errorCallback);

function successCallback(stream) {
  video.srcObject = stream;
  requestAnimationFrame(draw);
}

function errorCallback(err) {
  console.log(err);
  alert(err);
}

function draw() {
  canvas.width  = window.innerWidth;
  canvas.height = window.innerHeight;
  ctx.drawImage(video, 0, 0);

  requestAnimationFrame(draw);
}
CSS
body {
  margin: 0;
  background: #000;
}

#videobox {
  position: absolute;
  top: 10px; left: 10px;
  transform-origin: left top;
  transform: scale(.1);
}

#video {
  display: block;
  transform: rotateZ(180deg); /* なぜか天地反転するので天地反転し返す */
}

#canvas {
  display: block;
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
}


サンプルURLとQRコード

https://goo.gl/AhKN6t
f:id:kimizuka:20170606162607p:plain

f:id:kimizuka:20170608231415p:plain

(見た目上はわからないですが)Canvasになりましたね。^ ^

3. getUserMediaでstreamを取得してvideoタグでプレビューし、CanvasにdrawImageしつつ、モノクロにする

折角Canvasにしたのでピクセルデータにアクセスして色を変更してみました。

HTML
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>Monochrome</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, shrink-to-fit=no" />
  <link rel="stylesheet" href="index.css" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
</head>
<body>
  <canvas id="canvas"></canvas>
  <div id="videobox">
    <video id="video" autoplay playsinline></video>
  </div>
  <script src="index.js"></script>
</body>
</html>
JavaScript
const medias = {
  audio: false,
  video: {
    facingMode: {
      exact: "environment"
    }
  }
};
const video  = document.getElementById("video");
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const promise = navigator.mediaDevices.getUserMedia(medias);

let imgData;
let data
let ave;

promise.then(successCallback)
       .catch(errorCallback);

function successCallback(stream) {
  video.srcObject = stream;
  requestAnimationFrame(draw);
};

function errorCallback(err) {
  alert(err);
};

function draw() {
  canvas.width  = window.innerWidth;
  canvas.height = window.innerHeight;
  ctx.drawImage(video, 0, 0);

  imgData = ctx.getImageData(0, 0, canvas.width,  canvas.height);
    data = imgData.data;

    for (let i = 0; i < data.length; i += 4) {
      ave = (data[i + 0] + data[i + 1] + data[i + 2]) / 3;

      data[i + 0] = 
      data[i + 1] = 
      data[i + 2] = (ave > 255 / 2) ? 255 : (ave > 255 / 4) ? 127 : 0;
      data[i + 3] = 255;
    }

  ctx.putImageData(imgData, 0, 0);
  requestAnimationFrame(draw);
}

CSS

body {
  margin: 0;
  background: #000;
}

#videobox {
  position: absolute;
  top: 10px; left: 10px;
  transform-origin: left top;
  transform: scale(.1);
}

#video {
  display: block;
  transform: rotateZ(180deg); /* なぜか天地反転するので天地反転し返す */
}

#canvas {
  display: block;
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
}


サンプルURLとQRコード

https://goo.gl/huZORf
f:id:kimizuka:20170606163232p:plain

f:id:kimizuka:20170608231415p:plain

モノクロになりましたね。^ ^

おわりに

今回はとにかく手っ取り早くカメラのリアルタイムプレビューを試したかったので非常に簡単なサンプルをつくるにとどまりましたが、
Canvasに取り込みさえすればこっちのものなので、簡単なARアプリぐらいはつくれるかもしれません。
iOS11のリリースまではまだ時間がありそうなので、引き続き追っていこうと思います!
(後回しにしたマイクアクセスも追っていこうと思います)

カヤックでは一緒に新しいことにチャレンジしてくれるエンジニアも募集しているので、一緒に追ってくれる方絶賛お待ちしています!
www.kayac.com
www.kayac.com