はじめに
なんか色々あってWebサービスをリリースすることになったのでついでに記事を書くことにしました。
何ができるのか、どんな感じに実装したか等を説明したいと思います。
作ったもの
サービス名は、midiesです。友達に考えてもらいました。
アイコンはMIDI端子とWi-Fiを組み合わせた感じで、まあ悪くはないかなと思ってます。
このサービスの特徴は、MIDIデバイス(仮想でも可)の信号をブラウザだけでライブ配信できることです。
配信機能はパソコンやAndroid端末の一部ブラウザのみ対応しており、受信と再生は多分全てのブラウザが対応してます。
ただし、IEだけは動作の保証をしません(一応再生はできたっぽい)。
送受信者のネットワークが安定していれば1秒未満の遅延で配信が可能です。
配信と再生画面のスクショです。パソコンのみですがスマホにもちゃんと対応しています。
技術的な話
通信部分の解説(というか開発の流れ)
最初は、NETDUETTOを真似できないかなとか思って作り始めました。
とりあえず何回も使ったことのある、WebSocketを使って実装しました。
東京都内同士での通信で遅延が大体40~100 msくらいでセッションはできなくもないって感じです。
欠点は短時間に大量のデータを送信するとパケットは詰まってというか、一気にデータが流れたりしたんですね(要はラグい)。
多少和音を弾くくらいだと問題はなかったですが不安定な感じでした。
次に、WebRTCに初めて挑戦してみました。まあ色々頑張ってVuexで状態管理できる感じのを実装しました。
データチャネルを使って通信したんですがこちらはWebSocketよりラグが酷かったですね。
なんか設定をミスったのか知りませんが諦めました。
最後に、FirebaseのRealtime Databaseを使いました。遅延は150~200 msって感じで安定性はWebSocketと変わらない感じでした。
まあ、通信にWebSocketを使ってるので当たり前ですね。そこで、セッションはもう諦めてライブ配信を実装することにしました。
また、サーバー使うの面倒だし全部Firebaseで実装することにしました(この時点で完成したら公開しようと思った)。
ラグの軽減のために色々試しましたが最終的に採用したのは以下の手法です。
送信側は、大量のパケットを送信しないためにまずデータを50 ms間スタックしてから送信します。
受信側は、それをsetTimeoutを使って(1000+Δt-Δt')ms後に再生するようにします。
ここでΔtは、データの前後の送信時間の差、Δt'は、データの前後の受信時間の差です。
こうすることにより、再生時のラグを緩和することが可能です(間違ってたら教えてください)。
ちなみに、送信側のスタック時間が50 msなのは30、50、100、500、1000 msの中で一番安定した気がするからです。
30 ms以上の時点で和音を1回で送信できるのでかなり効率的でした。
時間が長くなると不安定になる原因はわかりませんでした。データサイズが大きいからというわけではない気がします。
受信側の1000 msは500 ms以上で再生に問題がある場合、ネットワーク環境に問題がある気がしたので
なんとなく倍の1000 msをデフォルトに設定しました。500 msを用意したのは全体の遅延が1秒以内だと宣伝したかったからです。
この手法を実装したあとは、ユーザー、投稿等の機能を頑張って実装しました。
細かい解説はしませんが何を使って何を実装したかは説明します。
フロントエンド
みんな大好き...かは知りませんがVueを使ってます。
UIフレームワークはVuetifyを使っていてシンプルな感じに作ったつもりです。
PWAにも一応対応していますがWorkboxデフォルトのPrecacheだけですね。
音の再生にはhowler.jsを使っていてVuexで管理してます。
PWA対応したのでVuexは要らないと言えば要らないですが、それでもロードに時間がかかるのでありだと思いました(Vuexからのロードは一瞬なので)。
他にもMIDI.jsというものがありますが今回は多分使わなくていいかなと思いました、というか途中で変えたくなかった。
それと音源ファイルを後ほど説明するFirebaseのStorageで管理しようかと思ったんですがなんとなくやめました。
肝心のMIDIデバイスへのアクセスは、Web MIDI APIを使っています。
ブラウザでMIDIデバイスを操作できるって何を目指してるんですかね?
まあ、おかげでこういうサービスが作れたんですけど。
バックエンド
有名なmBaaSであるFirebaseを使っています。
実際に使うのは初めてだったので、間違った使い方もあると思いますが何をどう使ったのか軽く説明します。
Hosting
Vueでビルドしたファイルをホスティングしてます。特に言うことはないです。
Authentication
ツイッター認証を使用しています。こちらも特に言うことはないです。
Firestore
ユーザー情報と投稿詳細が保存されます。
データ構造はユーザー削除をしやすいようにするため、サブコレクションを選択しました。
ユーザードキュメントに投稿とお気に入り一覧のサブコレクションがある感じです。
こちらをかなり参考にしました。具体的な実装方法が気になる方は見てみてください。
Realtime Database
投稿のMIDIデータが保存されます。Firestoreも考えましたが遅延時間がかなり不安定だったので断念しました。
もし、このサービスが人気になればすぐストレージが足りなくなるので、ライブ配信が終了したら自動でFirestoreに移すなり方法を考える必要があります(Realtime Databaseはストレージ料金が高いので)。
また、ライブ配信中のホスト状態を共有するためにオンラインであるか、配信が終了したかのフラグも保存してます。
これらは、次に説明するFunctionsのトリガーに使用しています。
Functions
以下の4つの関数を作成しました。
- Realtime Databaseでオフラインになったら1分後にオンラインに復帰したか確認して、してなかったら終了フラグを立ててFirestoreの投稿詳細に再生時間を追加する。
- Firestoreで投稿が削除されたらRealtime Databaseも削除する。それと同時に、Firestoreで全ユーザーのお気に入り一覧からその投稿を削除する。
- ユーザーのお気に入り一覧に投稿が追加または削除されたら、その投稿のお気に入り数を変化させる。
- Authenticationでユーザーが削除されたらFirestoreでサブコレクションを含む全てのデータを削除する。
改善点
- アップロード機能の実装
- これはそんなに複雑ではないですが、とりあえずユーザーが増えたりしたら実装します。
- フォロー機能の実装
- お気に入り機能とたいして変わらないですが面倒なので、こちらもユーザーが増えたりしたら実装します。
- MIDIファイルの対応
- インポートやエクスポートができると便利そうですよね。これにより、投稿データの保存をStorageに任せることができるようになるのでユーザーが増えれば節約にもなります(JSONでも可能ではある)。また、リアルタイムでの楽譜表示なんかもできるようになります。ただ、MIDIファイル自体よくわからなかったので諦めました。バイナリはちょっと...
- 検索機能の実装
- Firestoreって全文検索できないんですよね。N-gramでの実装も考えましたがとりあえず今後も実装の予定はありません。ただ、どこかのデータベースから曲名だけ引っ張ってきてタグみたいにするのはありかなと記事を書きながら思いました。これはいつか実装するかも知れないです。
- タイトルの自動生成(実装しました)
-
SSRは無理なのでFunctions使うしかないと思います。シェアする時にタイトルがわからないのは不便なのでまあユーザーが増えたら実装します。mountedでの自動生成でも問題なく検索で表示されるみたいです。やったことあるのでそのうち対応します。
終わりに
今回初めて自分が1から作ったWebサービスを公開することになりました。
もともとは軽く作ってGitHubに公開する程度のつもりでしたが、楽しくなっちゃって1ヶ月以上かけてここまで作ってしまいました...
今までも学校のイベントで使うウェブサイトだったり、友達が運営してるSNSのフロントエンドだけだったりと色々作ってきましたが、それに比べると早く完成しました。1年以上色々作ってるしもういい加減慣れてきた感じなんですかね。
FirebaseはNoSQLに戸惑ったくらいで使いやすかったです。ただ、SQLは本当に便利なんだなとも思いました。
今のところは既存の動画投稿サイト等と比べると遅延がかなり少ないことと、音源を変更可能であるというメリットしかありませんが少しでも使っていただけると嬉しいです。また、音源の種類は増やしたいと思っているのでオススメとかあったら教えてください。
最後まで読んでいただきありがとうございました。
参考文献
setIntervalとsetTimeoutを調べた結果余分なことになった
WebRTCの簡易シグナリング
WebRTCのデータチャネル解説
Firebaseドキュメント
Cloud Firestoreで「いいね」機能を実装するときの勘所
CloudFunctionsを使ってFirestoreのサブコレクションを削除する