WebView と getUserMedia との奮闘記
この記事はAizu Advent Calendar 2020の 5 日目に差し込む記事です。
今年は某 SDK を利用して WebRTC を利用しており、WebRTC を利用するにあたって カメラ・音声取得処理と WebView がかなりの障害になったのでそれに関してまとめておこうかなと思います。
TL;DR
- User Agent だけでは WebView かどうか確実にはわからない
- audio, video の取得処理が失敗した際のエラーの種類を見て対象外のブラウザであることを通知しましょう (つまるところエラーハンドリングちゃんとやろうねってこと)
カメラ・音声取得
どういう経緯で必要だったかは書きませんが、ビデオ通話ができる必要があり、そのためにもカメラと音声を取得することが必須でした。 以下、簡単な例ですが取得するために必要なコードです。
try { const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true, }); } catch (error) { console.error("getUserMedia error:", error); }
これで MediaStream
オブジェクトがstream
内に入りこれをやりとりしてビデオ通話を実現します。
WebView の何が問題なのか
こちらを見ていただければわかりますが、主要なブラウザでは getUserMedia の利用は可能です。 なので WebView に関して何を考えるでもなく開発を進めていました。 しかし、アプリケーションを運用し始めると映像が映らないという報告が飛んでくるわけです。 幸いにも Sentry でエラーログを取っていたためそちらを見てみると概ね以下のパターンに分けられました。
AbortError
を吐いていて利用できないNotAllowedError
を吐いていて利用できないAbortError
/NotAllowedError
で UserAgent に wv が含まれるAbortError
/NotAllowedError
で UserAgent に YJApp-xxxx や FBXX、line など特定の文字列が含まれる
上記のエラーを見て、後半 2 つに関して着目し、WebView であることが起因してエラーを吐き利用ができないことに気が付きました。
UserAgent 内に特定の文字列が含まれる場合アラートを出す。
一番初めにとったアプローチがこれでした。(ちゃんとエラーハンドリングしろとのツッコミはなしでお願いします。)
エラーログを見る限り利用できないパターンのうちかなりの数は Android であればアプリ内ブラウザ、iOS であればアプリ内ブラウザに加え、Chrome などの他ブラウザだったため、一見するとメジャーなアプリ内ブラウザを提供しているアプリの UA を分析して特定の文字列を弾けば良さそうに思えました。
しかし、アプローチにはいくつか問題があります。
- WebView ではあるもののそれを示す文字列が含まれない
- Twitter など特定の文字列を UA に入れていないアプリ内ブラウザを提供するアプリがあること(分類としては上記と同じ)
- そもそもメジャーどころを判定してもユーザーが何を使ってアクセスしてくるかはわからないこと
以上の 3 つに大別されました。
もちろん WebView であってもアプリ側で適切に権限が付与する実装になっていれば getUserMedia
で映像・音声を取得できると思います。が、そういったケースは少なかったです。
例えばアプリの名前がブラウザとなっていても中身自体は WebView といったこともあり、しかも UA が通常のブラウザと変わらないといったこともあるのでこのアプローチは限界があるわけです。
エラーハンドリング
パターンわけを見て気づいたとは思いますが、利用できない場合必ずエラーを吐いています。つまりエラーを吐いた場合にユーザー側に利用できないことを通知しその後のフローに進ませないような実装になっていれば良いわけです。 そして利用していただく場合には Android なら Chrome, Firefox, Edge として iOS であれば Safari とのアナウンスを出せば、ブラウザを変えてくれるユーザーも出てくはずです。
まとめ
- User Agent だけでは WebView かどうか確実にはわからない
- audio, video の取得処理が失敗した際のエラーの種類を見て対象外のブラウザであることを通知しましょう (つまるところエラーハンドリングちゃんとやろうねってこと)
もちろんこれだけでは 100 人中 100 人がちゃんと利用できるとは限りません。が、実装上問題があるといった状態ではなくなります。(例としてはユーザーがメールアプリからリンクを開いて実際はアプリ内ブラウザであるが、Chrome だと思い込んでいるなど)
ちゃんと実装していれば遠回りにならないで済むのでエラーハンドリングをちゃんと行い、どのエラーがどういった場合に起こるかちゃんと仕様やドキュメントを読んで理解しておきましょう。
おわり
ReactからVueへ移行して苦労したこと & Vueで今年知ったこと
これは Aizu Advent Calendar 2019 の 6 日目の記事です。(大遅刻してすみません 🙇♂️) 5 日目は @4rotsugd さんでC++ SSE/AVX 入門の記録、 6 日目は @acomagu さんのmakepkg-template で PKGBUILD の管理をちょっとだけ楽にするです。
去年も遅れてましたね...
(技術的に浅い話だけど許して)
React から Vue
なぜ React から Vue へ
普段私は Vue を使った Web アプリケーションを作成したりしなかったりしています。 某プロジェクトで私はとあるアプリケーションを作成することになったのですが、完全に個人開発だったのと静的ファイルを配信するだけだったため、普段とは違う React で実装を勧めました。
~某日~
私 「7 割ぐらい実装終わったぞ!(余裕の表情)」
天の声 「他で Vue 使ってるから Vue 使ってください」
私 「()」
こうして React から Vue への移行作業が始まったのです...
簡単にですが一瞬でも移行で詰まったことを少しだけ書いていきます。
子要素の定義
React では子コンポーネントを以下のようにすることで<MyComponent><div>hoge</div></MyComponent>
といったことができます。
const MyComponent = ({ children }) => { return <div>{children}</div>; };
何もおかしなことはしていませんね。特に難しくもないです。基本。
<template> <div> <slot /> </div> </template> <script> export default { name: "MyComponent" }; </script>
とすることで同じことができます。
React では Props を利用して、Vue では Slot を利用しています。
移行の際に <slot />
を思い出せなかった記憶があります...。
イベント
TypeScript を使って Vue を書いていて以下のようなコードを書いたことがある方いると思います。
methods: { handleInput(event: Event) { if (event.target instanceof HTMLInputElment) { console.log(event.target.value); }; // as 使っちゃう ver console.log(event.target as HTMLInputElment).value); } }
React だとこうなります。
const handleChange = (event: React.FormEvent<HTMLInputElement>) => { console.log(event.target.value); };
大きな違いではありませんが、React から Vue へ移行した際に便利な型生えてて欲しかった気持ちがあった気がします()
値の監視
Vue では watch
を利用して値が変更されたら実行される関数を定義できます。
data() { hoge: false }, watch: { hoge(newHoge) { console.log(newHoge) } }
React では useEffect
を利用して似たことができます。
const [hoge, setHoge] = useState(false); useEffect(() => { console.log(hoge); }, [hoge]); // hogeが変更されるような処理 ...
React から Vue へ移行した際に useEffect
を利用したコードをどうするかといったことは悩みませんでした。
ただ、本質的には watch
, useEffect
は別物なので同じことができるだけで混同しないように気をつけたいところです。
今年始めて知った/使った Vue の API など
Composition API
Vue 3.x から導入される予定の API で以下のようにすれば 今でも利用できます。
$ npm install @vue/composition-api
ここでは解説しないので気になる方はこちら を読んでください。
Vue でおなじみの this.xxx
といった書き方をせず setup
関数内に変数やイベント、ライフサイクルフックなどを書いていく感じで props
に関しても TypeScript を利用する際に type
や interface
を利用した型定義をしっかりできるのは便利。
v-slot
私が知っていたのは slot
、 slot-scope
でした。これらが非推奨になり、統合されたのが v-slot
です。
以下のように利用します。
<!-- 定義側 --> <template> <div> <div> <slot /> </div> <div> <slot name="piyo" /> </div> </div> </template> <!-- 利用する側 ---> <template> <my-layout> <div>something</div> <template v-slot:piyo> <div>piyo</div> </template> </my-layout> </template>
slot
の name
は動的に割り当てたりすることも可能ですが、スペースや記号が入っていると利用する側で素直に v-slot:xxx
と書けなくなるので個人的にはできれば避けたいです。
v-slot:[something]
といった書き方もできます(something は data)。 v-for
などを利用して子のスロットの name
に値を渡すなどする場合やスペース、記号が混入する場合などはこちらを利用しましょう。
model
v-model
がどの値をプロパティとして持ち、どのイベントを利用するか変更できます。
通常は value
とinput
イベントの組み合わせですが、change
イベントを利用したかったりする場合などに利用します。
model: { prop: "value", event: "change" }
ドキュメントの API の部分を探さないと出てこない(多分)のでchagne
イベントで呼び出した関数の中で input
を emit するか input
を emit する関数を呼んでいた人もいるはずです。(いてくれ...)
v-model を捨てる選択肢
v-model
Vue.js で便利な機能の一つに v-model
があります。
v-model
にデータを渡すと自分でフォームのイベントを利用し値を更新するということをしないで済む。
確かに便利ですが、コンポーネントがネストされていくにつれ親コンポーネントで v-model
を利用するために
<template> <input type="text" :value="value" @input="handleInput" /> </template> <script> import Vue from "vue"; export default Vue.extends({ props: { value: { type: String } }, methods: { // これより上の階層のコンポーネントの場合は // handleInput(value) { // this.$emit("input", value) // } handleInput(event) { this.$emit("input", event.currentTarget.value); } } }); </script>
というのを実際に v-model
を使用するコンポーネントまで続けることになってしまいます。
1 つ上の階層の場合 emit
で親コンポーネントにわたす値が変化します。一定以上の Vue.js の経験がある。もしくはしっかりとコンポーネントの管理がされていない場合には間違った値を渡す可能性も考えられます。(v-model
が 糖衣構文であることを知らない人も結構いる気がします。)
Props で管理する
emit
を使用するのではなく親から関数を props
で受け取り、それを子コンポーネントでは発火させることで、 emit
時のような親コンポーネントへ渡す値を考慮する必要がなくなります。
ただし、 v-model
で利用できる修飾子(.number
, .lazy
など)が利用できなくなるデメリットが存在します。
実際の子コンポーネントは以下の感じになるかと思います。
template> <input type="text" :value="value" @input="input" /> </template> <script> import Vue from "vue"; export default Vue.extends({ props: { value: { type: String }, input: { type: Function } } }); </script>
このパターンでは props
を利用していることから特に props
の命名に縛られないため柔軟に利用できるということです。
例えば input 要素を 2 つ持つようなコンポーネントを考えると v-model
を利用するためには props
として value がいなければならず、 emit もそれぞれの input 要素から取得した値をなんとかまとめて親に値を渡すなどということになります。
しかし、 props
のみで管理すればそれぞれの input 要素を利用する状況に応じた命名をしそれぞれ関数を渡すだけで良くなります。 子コンポーネントから渡ってくる値の形式に気を配らなくてよくなるのです。
まとめ
props
で関数を渡すパターンの方が大抵の場合柔軟に利用できるのではないかと思います。
もちろんシンプルなコンポーネントのみで済むのであれば、数々の修飾子を利用できる v-model
が利用可能になるパターンを使って恩恵を受けるのがいいでしょう。
変に v-model
にこだわらずに捨てる選択肢を持つことで柔軟にコンポーネントを開発していきましょう。
サンプルとして簡単なものですがリポジトリを置いておきます。
GatsbyJS触ってみた
成果物
また適当にリンク置いてるだけなのだけれど貼る
github.com
デプロイしたやつ ↓
mt-coff.me
実際に触る
環境構築
$ npm install -g gatsby-cli
$ gatsby new [プロジェクト名] [使うのであればスターターのURL]
自分はデフォルトのものを使用したが多くのスターターがあり、自分で CSS とか考えたくないならシュッといい感じのベースを生成してくれるので使う。
これで基本的な環境構築は終わり。
pages
に各ページを components
に必要なコンポーネントを定義していく感じなはず。
json からデータを取得してみる
gatsby-transformer-json
を利用する
$ npm install --save gatsby-transformer-json
依存に追加したら gatsby-config.js
に設定を追加した
plugins: [ `gatsby-transformer-json`, { resolve: `gatsby-source-filesystem`, options: { path: `${__dirname}/src/data` } } ];
src/data/
下に json
ファイルを置く。自分は以下のようなファイルを sns.json
として置いた。
[ { "name": "GitHub", "link": "https://github.com/mt-coff" }, { "name": "Blog", "link": "https://mt-coff.hatenablog.com" }, { "name": "Twitter", "link": "https://twitter.com/mt_coff" } ]
実際にコンポーネントで上記データを利用する際は以下のようなコードになると思う。
import React from "react" import { useStaticQuery, graphql } from "gatsby" const SomeComponent = () => { // all + ファイル名 + Json const { allSnsJson } = useStaticQuery( graphql` query { allSnsJson { edges { node { name link } } } } ` ) return ( <> {allSnsJson.edges.map(({ node }) => { return ( <div>{name}</div> <div>{link}</div> ) })} </> ) }
Gatsby ではいろんな値を GraphQL の query でとってくる。localhost:8000/__graphql
で query が試せるのでいろいろやってみるのがいいと思う。
ハマったところ
styled-components
を利用していて一旦デプロイをかけたら何もスタイルがあたっていない状態でデプロイされた。
びっくりして gg ったら gatsby-plugin-styled-components
というのがいたので必要な依存を追加。
$ npm install --save gatsby-plugin-styled-components\ styled-components\ babel-plugin-styled-components
設定を追加
plugins: [`gatsby-plugin-styled-components`];
これで解決した。
触ってみて
Graphql
を使う辺りがちょっととっつきにくいと感じた。それを除けばプラグインもスターターも豊富かなと感じていて凝ったことをやらない限りは良さそうなスターターでプロジェクトを生成して表示するコンテンツだけいじるのが楽。
逆に自分でコンポーネントやスタイルをゴリゴリやっていきたいならデフォルトのスターターでやっていくのがいいのかな...
自分のポートフォリオ?(mt-coff.me)は GatsbyJS
で今後いじっていくと思うのでもっと触ってなれたいと思います 💪
平成最後のハッカソンに参加してきた mt_coff版
もう令和が始まって 1 週間以上経過しましたね。おはようございます、mt_coff です。
忘れる前にハッカソンに参加したので簡単にですが参加記を書いとこうという気持ちで書きます。
平成最後のハッカソン
サイバーエージェント主催のハッカソンで参加者は平成生まれのみ、【平成生まれの私たちが、平成を楽しく振り返ることができるサービス/モノ】というテーマで開催されました。
私は参加するか悩んでいたのですが、後輩から DM でシュッと連絡が来たのでやるぞ 💪💪💪 という感じで参加を決めました。(選考が通るか不安でした。多分後輩が通ったので通った)
ハッカソン用のSlackではなにか投稿されるたび平成emojiが飛び交ってました。
チームとアイデア
id:uzimarhu0601 と id:tjmschk とチームを組んでやってました。(実は熱出したりして体調を崩し、あまり貢献できなくてすまんという感じです)
おまけで他のチームには同じ大学の Twitter芸人2人( id:flying_hato_bus, id:yt8492 ) とよく諸用があると言ってどこかに消えるプログラミングができる5才児 ( id:NoahOrberg ) がいました。他にも面識がないものの同じ大学の方がいたようです 👀👀👀
テーマ発表後のアイデア出しでは黒歴史を使う、マップに出来事をピンどめ、誕生日に何があったかわかるやつなどのアイデアが出ました。
最終的には黒歴史を投稿できるサービスというところに落ち着きました。
黒歴史という概念自体が平成になってから誕生したものであり、SNSの台頭によって黒歴史を量産する人が後をたたないのでぴったりなテーマです。
しかし.....
Twitter芸人と5才児のチームとアイデアの根本である黒歴史が被ってしまった!!!!
黒歴史大好きすぎか!?
それはさておき、成果物
私たちのチームは黒歴史を投稿し画像を生成 OGP 画像を利用して Twitter でいい感じにシェアできるといった感じのサービスです。
実際のソースこちら
github.com
動いてるサービスはこちら(Chrome 推奨です)
https://ca-dark-history.firebaseapp.com/:embed:cidte
リポジトリの名前は 実際にサービスにアクセスしてもらうとわかるんですが D-Historys
となっているんですね。
これは id:uzimaru0601 がスペルを間違ったので私がノリで それも黒歴史 といってそのままにしてあります。
使用している技術は Vue + Firebase で構想の段階で当日まで余裕で間に合うように機能を絞りました。私は主にレビューやら Xd でベースとなる UI 作成 やらチョット Vue 書けるを担当してました。
発表と結果
チームメンバーに任せて立ってるだけでしたが 動画で実際にサービスを利用しているシーンを紹介、その後使用した技術を紹介といった流れでした。
さまざまなチームがいてどのチームもアイデアがすごい...見ていて私もアイデア出せるようになりたいと感じました。 黒歴史でテーマが被りましたが私のチームが先に発表でした。やったね!
で、もう一方の黒歴史チームは
ca-dark-history.firebaseapp.com
(実際にアクセスして確かめてください)
残念ながら入賞には至らなかったですが、CSS アニメーションなど凝ってると言ってもらえたりしたので良かったです(安堵)
懇親会
前半は同年代、後半は Web フロントエンド系の方たちとグループになり、話をすることができました。
最終的に前半も後半も就活の話になり、(´;ω;`)ウッ…
ただ、普段は同じ大学の人の話ぐらいしか聞くことがないので、普段とは違った考え方や、大学の方針などなるほど!となる話を聞くことができお酒も飲めて楽しかったです。
就活?なにそれ美味しいんですか?
まとめ
平成最後のハッカソンは全体的にレベルが高く、開発するのも発表を聞くのもすごく楽しめたと思います。特別な理由がない限りハッカソンは参加しない私ですが、平成最後にハッカソンを企画してくださったサイバーエージェントの方々には感謝してもしきれません。 チームで開発を一緒に行ってくれた 2 人にも、最高の作品を作成した他のチームの皆さんも本当にありがとうございました!
近況なんですが、なにそれ美味しいの?ではなく割と就活困ってます
gRPC-Webをちょっとだけ試した
タイトル通りではあるが、gRPC-Web を試したのでほんの覚書き程度に記録。
上記を見れば概ね把握できるはず。自分はサーバー側は Go で書いた。雑コードではあるが以下にリポジトリを貼っておく。
ハマったポイントなど
はじめは proto に Timestamp を記述していた。が、TypeScript 側で const hoge = new Timestamp()
の用に書いていたが _pb_todo_pb__WEBPACK_IMPORTED_MODULE_1__.Timestamp is not a constructo
のように怒られが発生した。
定義元を見るとコンストラクタは存在していたので Timestamp を使うのを諦める選択をした。
その後 Empty でも同様の問題が発生してしまったので
const empty: Empty = { toObject: () => ({}), serializeBinary: () => new Uint8Array() };
の感じに定義することで必要なプロパティを満たし、使用することができた。
Empty も定義を見る限りではコンストラクタが存在していたのでなぜ?という感じだが生成されたコードをしっかり見ていない(もしくは JS/TS のクラスの仕様の理解が中途半端?)ので良くわからなかった。
必要なメソッドなどが自動生成されるのは非常に嬉しいと感じたものの、レスポンスは基本 200 で返ってきてエラーハンドリングはコールバック関数を用いる形だった(試してみて初めて知った)。
生成されたファイルが無限に ESlint に怒られるのでいじった気がする()
サーバー側は雑実装しかしてないのであまり感想はない。がプロキシとして使用している envoy については追々学習する必要があるなと感じた。
まとめ
proto の定義から自動でコードが生えてくるのは嬉しい。
ただし、そのへんに転がっているサンプルだと proto の中で Timestamp や Empty を使用している例がなかった(見つけられなかった)のでそれ周りでかなりハマった印象。
実家で作業してたこともあり、欠片も集中できなかったのはナイショ
ElmでQiitaの記事一覧表示するやつ作った
最近新しいことを学んでいないなと感じていたのでElmを触ってみた。
公式Docは一通り見た(つもり)状態でやっつけ感はあるしテストもまだ書いていないが、少しは理解できたんじゃないかなと思う。
作ったもの
タイトルの通りQiitaのAPIを叩いて記事一覧を表示するだけもの
一応ソースはこれ → https://github.com/mt-coff/elm-qiita-article-list
portだったりSVGを使用したかったがElm自体に全然慣れていないので一旦簡単なものにした。
詰まったところ
公式Docはサラッと確認した程度で人のコードを読んでみようと思っていくつかのリポジトリや記事なんか見た。
その結果古いバージョンを使用していることによる型だったり、引数の違いだったりではまった。ただエラーメッセージでしっかり確認できるので詰まりっぱなしにはならなかった。(当たり前)
感想
型の恩恵はかなりあると感じることができた。VSCodeの拡張を使用して書いていたが、ちょっとでも間違っているとすぐにエラーを吐いてくれるのはありがたい。
また、普段はVue.js(というかHTML,CSS?)ばかりなので関数型言語は新鮮だった(新鮮すぎた)。
やってて楽しかったもののHTMLはHTMLで書きたいなと思ってしまったところはある。なぜかといえば、イベントや属性を使用するにはそれぞれパッケージのインポートが必要であることと、文字の表示でも必ず関数を通すのが若干手間といえば手間だったから。
text
とか書くのをすぐ忘れて何度もエラーを吐いた。
気が向いたらテストの追加と試しに使った Tailwind CSS でもう少しマシなUIにするなどしたい。
普通に使っていくには理解が足りないと感じているので暇を見つけて少しずつ触ろうかなと思う。