やったこと
- safariからiPhoneのカメラを起動する
- canvasで写真撮る
- box保存とサーバ送信する
angularでやってるけど、ほとんど標準のJS使った処理なのでtypescriptをヴァニラJSに置き換えれば何でもいける気がします。
ただし、reactだと魂を震えさせるアイツが一転して抵抗勢力となりもろもろツライ感じになるのでご注意ください。
https://github.com/facebook/react/pull/9146#issuecomment-319629271
環境
カメラついてるマシン
筆者はMBP(mac book pro)のフロントカメラでざっと作りながら、iPhone7で実機確認する手順でやっています。
mediaStream API対応 ブラウザ
中核です。カメラやスクリーンシェアリング、マイクのようなビデオやオーディオ入力装置を扱うAPIです。
safariはdesktop/mobileとも11から対応。
対応表
https://caniuse.com/#search=getusermedia
なお、APIが2つ存在するので注意です。Promiseを返すNavigator.MediaDevices.getUserMedia()
を使います。コールバック型のNavigator.getUserMedia()
はDeprecatedされてます。
APIリファレンス
https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia
angular/typescript
angular4.4.4/typescript2.3.3
どうせnativeElement(pure DOM)バリバリなのでバージョンは何でもいいと思うだよ。
バージョン低いとtypescriptの型定義でmediaDevices
あたりが出てこないかもしれない。2.3.3なら全部あったのでany
はありません。
https対策
httpsサイトからじゃないとcameraデバイスアクセスがNGになりますNotAllowedError (DOM Exception 35)
。オレオレ証明書を作り、webpack dev serverなどでhttpsのローカルサーバを起動しても、safariだとやっぱりNGです。なので、筆者はいちいちherokuにデプロイして動作確認する苦行を積んだよ。(証明書タダでできるいい方法あったら教えてください)
流れ
今回使うhtmlを示します。
<video id="video" #video autoplay playsinline></video>
<canvas id="canvas" #canvas></canvas>
<button (click)="onClick()">写真撮る</button>
angularテンプレートだが、特有の要素は#
のテンプレート変数にエクスポートしているところくらいだと思います。こいつでtypescript領域にhtmlElementを引っ張り込み、nativeElementというangularのDependencyInjection(DI)をかましつつDOM透過するための仕組みを使ってゴリゴリやっていきます。まあ、ヴァニラJSのdocument. getElementById()
などに置き換えればほぼ互換されると思います。
これを使って順番にtypescriptを実装していきます。
流れです。
- videoタグでcameraと接続してキャプチャする。
- canvasで静止画像を描画する
- videoタグでキャプチャしているcameraを止める
- ローカルにファイル保存したりサーバ送信する
1. videoタグでcameraと接続してキャプチャする
カメラを起動するところです。@ViewChild('video')
でvideoタグのhtmlElementを取得してtypescriptで操作する感じです。
export class PictureComponent {
@ViewChild('video') videoElm: ElementRef;
readonly medias: MediaStreamConstraints = {audio: false, video: {
facingMode: 'user', ・・・フロントカメラ指定
・・・リアカメラ指定するときは以下に差し替える
// facingMode: {
// exact : 'environment'
// },
}};
ngAfterViewInit() {
this.startCamera();
}
private startCamera() {
window.navigator.mediaDevices.getUserMedia(this.medias)
.then(stream => this.videoElm.nativeElement.srcObject = stream)
.catch(error => {
console.error(error);
alert(error);
});
}
}
medias変数
mediaを取得する際のconfigとなる。カメラ用の設定の肝はfacingMode
になる。
本例ではfacingMode
にuser
を指定することでフロントカメラを起動している。コメントアウトしている記述{exact : 'environment'}
に差し替えるとリアカメラが起動する。
一応、cameraの解像度を指定して、指定解像度のcamera以外はエラーにすることもできる。
完全版はこちら。
const medias: MediaStreamConstraints = {
audio: false,
video: {
facingMode: 'user',
width: { min: 1024, ideal: 1280, max: 1920 },
height: { min: 776, ideal: 720, max: 1080 },
}
}
ちなみに、audioをtrueにすると、マイクデバイスと接続する。
startCamera()
startCamera()
でcameraに接続している。mediaDevices.getUserMedia()
を呼び出すと、ブラウザがユーザーにカメラの接続許可を適宜提示する。ユーザーがOKしてmediaDevices.getUserMedia()
が成功すると、Promise型のstreamが返ってくるので、videoタグのsrcに突っ込むとキャプチャが開始される。
2. canvasで静止画像を描画する
videoタグのキャプチャをベクター画像で扱えるようにするために、PictureComponent
に以下を追加します。
private captureData: string;
onClick() {
this.captureData = this.draw();
}
private draw() {
// 写真のサイズを決める
const WIDTH = this.videoElm.nativeElement.clientWidth;
const HEIGHT = this.videoElm.nativeElement.clientHeight;
// canvasを用意する
const ctx = this.canvasElm.nativeElement.getContext('2d') as CanvasRenderingContext2D;
this.canvasElm.nativeElement.width = WIDTH;
this.canvasElm.nativeElement.height = HEIGHT;
// canvasの描画をしつつBase64データを取る
return this.canvasElm.nativeElement.toDataURL(ctx.drawImage(this.videoElm.nativeElement, 0, 0, WIDTH, HEIGHT));
}
内容はソースコメントの通り。
3. cameraを止める
後片付け大事です。放っておくとカメラと接続しっ放しになります。
止める方法が意外とサクッと出てこなくて調べるのにちょっと時間かかりましたが英語サイトで解決しました。
onClick() {
this.captureData = this.draw();
this.stopCamera();
}
private stopCamera() {
this.videoElm.nativeElement.pause(); ・・・一時停止
const track = this.videoElm.nativeElement.srcObject.getTracks()[0] as MediaStreamTrack;;
track.stop(); ・・・止める
}
trackを取得して、track毎に止める方法に変わったらしいです。
https://developers.google.com/web/updates/2015/07/mediastream-deprecations?hl=en#stop-ended-and-active
あとはcomponentのライフサイクルに任せればメモリ解放されると思います。
4. ローカルにファイル保存したりサーバ送信する
サーバ送信は、BASE64でよければcaptureData
に取得したデータをJSONに突っ込めばOKです。クラウドStorageに上げるなどは各SDKに従いましょう。
ローカル保存する場合は、iOS/Androidで振る舞いが変わるかもしれません。筆者はiPhoneしか無いので、iPhoneの挙動を記載します。aタグでダウンロードリンクを作っていますが、programmaticallyにやろうとしてもタブで画像が開いてしまうだけになります(PCなら自動ダウンロードになります)。なので、ユーザーに手作業で保存してもらうスタイルになっています。androidならこれで自動保存までできるのかもしれません。
constructor(
private renderer: Renderer2, ・・・angularのレンダラーを注入
) {}
onClick() {
this.captureData = this.draw();
this.stopCamera();
this.savePicture();
}
private savePicture() {
this.apiService.savePicture(this.captureData); ・・・サーバ送信する何か
const a = this.renderer.createElement('a') as HTMLAnchorElement;
a.href = this.captureData; ・・・キャプチャしたBase64データを突っ込む
a.setAttribute('download', 'image.png');
// a.click(); ・・・タブで開いてしまうのでコメントアウト
}
最終形
<video id="video" #video autoplay playsinline></video>
<canvas id="canvas" #canvas></canvas>
<button (click)="onClick()">写真撮る</button>
export class PictureComponent {
@ViewChild('video') videoElm: ElementRef;
@ViewChild('canvas') canvasElm: ElementRef;
readonly medias: MediaStreamConstraints = {audio: false, video: {
facingMode: 'user',
// facingMode: {
// exact : 'environment'
// },
}};
private captureData: string;
constructor(
private renderer: Renderer2,
private apiService: ApiService,
) { }
ngAfterViewInit() {
this.startCamera();
}
onClick() {
this.captureData = this.draw();
this.stopCamera();
this.savePicture();
}
private draw() {
// 写真のサイズを決める
const WIDTH = this.videoElm.nativeElement.clientWidth;
const HEIGHT = this.videoElm.nativeElement.clientHeight;
// canvasを用意する
const ctx = this.canvasElm.nativeElement.getContext('2d') as CanvasRenderingContext2D;
this.canvasElm.nativeElement.width = WIDTH;
this.canvasElm.nativeElement.height = HEIGHT;
// canvasの描画をしつつBase64データを取る
return this.canvasElm.nativeElement.toDataURL(ctx.drawImage(this.videoElm.nativeElement, 0, 0, WIDTH, HEIGHT));
}
private startCamera() {
window.navigator.mediaDevices.getUserMedia(this.medias)
.then(stream => this.videoElm.nativeElement.srcObject = stream)
.catch(error => {
console.error(error);
alert(error);
});
}
private stopCamera() {
this.videoElm.nativeElement.pause();
const track = this.videoElm.nativeElement.srcObject.getTracks()[0] as MediaStreamTrack;
track.stop();
}
private savePicture() {
this.apiService.savePicture(this.captureData);
const a = this.renderer.createElement('a') as HTMLAnchorElement;
a.href = this.captureData;
a.setAttribute('download', 'image.png');
// a.click();
}
}