自宅内でビデオ通信ができる「homelens」の開発録
はじめに
自宅内でビデオ通信ができる「homelens(β版)」を個人開発してみました。
ソースコードはGitHubにて公開しています。
(⭐をいただけると著者が喜びます)
開発に至った経緯や、このサービスの特徴、使い方についてはnoteにまとめています。
個人開発ということで自由がきくので勉強も兼ねて、私自身が使ったことのない技術や言語を積極的に採用しました。
ここでは、技術選定や実装手法など、開発に関わる細かな話をしていきます。
自宅でビデオ通信するための技術調査
ブラウザでビデオ通信するためにはWebRTCを使用することになります。
WebRTC周りは未経験の領域なのでまずは勉強。
voluntasさんのスクラップをHubにして、未知のワードを随時調べて概要を掴んでいきました。
今回作ったhomelensで扱うものに絡めて、ざっと整理します。
-
WebRTC P2Pを使う
- 1対1通信ができれば良いので、SFUは不要
- ビデオ通信を開始する前にシグナリングの手順が必要
- 「通信相手のデータを取得」し「接続を確立させるために必要なすべての状態を共有」するためにSDPを交換する
- 一般的にシグナリングサーバーを立て、WebSocketを介して通信を行う
- NAT越えをするならSTUNサーバーやTURNサーバーが必要だが、今回はLAN内で通信できればよいので不要
シグナリングをどうするか
初めはシグナリングサーバーを立てずに、直接SDPを交換できないか検討していました。
SDPのテキストデータを手入力で交換するのは論外ですが、QRコードを複数端末間で読ませあうことで簡単にSDP交換が実現できるのではと考えました。
しかし、残念ながらこのアイデアは没。
実際にSDPを生成してみると、QRコードの最大データ量2,953bytesを超えており変換できず。
可逆圧縮しても容量内には収まりませんでした。
・・・
ということで、一般的な手法に従い
シグナリングサーバーを立て、WebSocketを介してSDPを交換することにします。
Goでシグナリングサーバーを作る
バックエンドで利用するプログラミング言語はGoを選択。
選択した理由は以下です。
- メモリ等のリソース消費が少なく軽量
- インタプリタ型言語で動かすのに比べ、安いサーバーでもコスパよく動かせるはず
- 新しい言語の習得
- 気になってはいたが使ったことがなかった
構文やお作法は、とほほさんの入門ページを参照しながら書いていきます。
Echo でHTTPサーバーを立て、開発環境ではホットリロードに Air を導入しています。
準標準ライブラリ golang.org/x/net/websocket
を使いWebSocket通信にUpgradeします。
接続してきたClientの接続元IPを取得し、IPごとにRoomを作成します。
Clientには一意の名前をつけ、IPごとに作成されたRoomに所属します。
(Public IPはIPv4ではLANごとに割り当てられるので、同じLANから接続された端末は全て同じRoomに所属することになります)
Room内ではClientの一覧が参照でき、指定したClient同士でSDP交換ができるようにします。
SDP交換が完了するとシグナリングサーバーの役割は終了で、Client同士がダイレクトにP2P通信を開始しビデオ通信ができるようになります。
図にまとめるとこのようになります。
バックエンドのコードはこちらです。
(ツッコミ・プルリクお待ちしております)
シグナリングサーバーのデプロイ
開発環境をDockerで立てているので、本番サーバーもDockerだと楽です。
個人開発なので、デプロイするサーバーはランニングコストのかからない無料枠があるものが望ましいです。
先日、Herokuが有料化しましたが、無料枠を提供してくれているサービスはいくつかあります。
比較検討したところ、デプロイ用にCLIもしっかり整備されているようで気に入ったので、このfly.ioに決めました。
公式ドキュメントに沿って flyctl
をインストールし、コマンドを叩いていくとハマることなくサーバーが立ち上がります。
たまにデプロイに失敗することがありますが
$ flyctl doctor
して指示に従うか、
それでもうまくいかないときはブラウザのコンソールから手動で free builder を削除した上で deployし直すとうまくいきます。
TypeScriptでフロントエンドを作る
フロントエンドで利用する言語はTypeScript、フレームワークはNext.jsを選択。
選択した理由は以下です。
- 型安全な開発がしたい(TypeScript)
- 新しい言語・フレームワークの習得
- TypeScriptもNext.jsも、気になってはいたが使ったことがなかった
TypeScript入門には、以下のWeb本が参考になりました。
Next.jsも初めてでしたが、React・Vue・Nuxt.jsの使用経験があったので
必要に応じて公式Docを参照する程度でなんとかなりました。
画面構成はシンプルに。
同じLANにいるClient一覧が表示され、通信相手をタップするとビデオ通信が開始されます。
ビデオ通信時にはClient一覧は非表示となり、代わりに自身のビデオと相手のビデオが表示されます。
SDPのやりとりは「offer/answer model」で行います。
通信を開始したい側で「Offer用のSDP」を投げ、もう一方がOfferの内容をもとに「Answer用のSDP」を返します。
シンプルなVanilla ICEで実装したので、この1往復が完了すればWebRTCでのビデオ通信が可能になります。
(高速なTrickle ICEは、今後チャレンジしてみます)
ブラウザでWebRTCを利用するので、WebRTC APIが利用できます。
このAPIの中の RTCPeerConnection
クラスを利用することで、SDPの作成やTrackの追加ができます。
WebRTCのP2P通信は外部サーバーを介さず、LAN内のみで完結します。
フロントエンドのコードはこちらです。
(こちらもツッコミ・プルリクお待ちしております)
フロントエンドのデプロイ
フロントエンドはBuildした静的なNext.jsを配信すればよいので、無料でホストできるCloudflare Pagesを採用しました。
GitHubと連携しておくと
プルリクが送られた時にレビュー用ページに
mainブランチへマージされた時に本番ページに
自動デプロイしてくれるので便利です。
テストとコード解析
バックエンドはGoのtestingパッケージを使って単体テストを書き
フロントエンドはJestで単体テストを書いています。
GitHub Actionsを利用し、プルリクが送られた時とmainブランチへのマージ時にこれらのテストが実行されるように設定しています。
また、コードのセキュリティチェックのためにCodeQLを導入し
こちらの解析もGitHub Actionsで実行されるよう設定しています。
開発してみた所感
WebRTC・Go・TypeScript・Next.jsと、積極的に使ったことがない技術や言語を組み合わせて開発を行い、なんとかアプリケーションとして動かすことができました。
WebRTCには敷居が高いイメージがありましたが、WebRTC APIがよくできているので扱いやすかったように思います。
(シグナリング周りでの辛みが発生しない要件だったためかもしれません)
Goは言語仕様が小さく、覚えることはあまりなくとっつきやすかった印象です。
しかし「Classが書けない」「mapやfilterがない」など、RubyやJavaScriptを書きなれた身からするとしんどい点もありました。
並列処理や遅延実行が簡単に書けるのと、ビビりながらポインタ操作をしなくて良い点がGoodでした。
TypeScriptはなんといっても型がある安心感。
動的型付に比べると面倒は増えますが、コードを長くメンテナンスしていくことを考えると、有意義なコストであるように思います。
おわりに
ひとまずβ版としてリリースが完了しました。
まだまだ満足できていない挙動やリファクタリングすべき箇所、テストの不足、追加実装したい機能など課題は山積していますが、気長に改修していきます。
使っていただけると嬉しいです。
Discussion