音楽プレイヤーMopidyをGoogle Cloud上に設置した話

[ミクシィグループアドベントカレンダー2021](https://qiita.com/advent-calendar/2021/mixi) の13日目に大遅刻しました。以下の話はすべて趣味で業務外です。

業務の話はこちら→ [【世界2位】Lookerハッカソン Hack@Home2021 参加報告](https://qiita.com/m77so/items/06c0e76ba085423b7167)

# この記事について
突然、「Google Play Music」のように、自分の所持する音源をクラウド上に置いて音楽を聞くやつをセルフホストしたくなりました。そこでMopidyを用いて構築してみました。全然使い込んでないので使い勝手はわかりませんが…。

また、この記事はどのようなものを組み合わせたのかは解説しますが、事細かに手順を解説したものではありません。参考にしたい方のために、末尾にGitHubリポジトリのリンクを貼っておきます。

# やりたいこと
- 手持ちの音源を複数端末で聞きたい
  - 自宅のPC
  - 仕事用のPC
  - Android端末
- OSSを用いて構築し、自分で管理したい
- コードは面倒なのであまり書きたくない

# この記事の特徴
Mopidyを始めとして、個人用音楽サーバはLAN内での構築を前提としている物が多いです（例: [[Question] snapcast over internet? #801 ](https://github.com/badaix/snapcast/issues/801)）。また、日本語の記事ではRasberry Pi上での構築がほとんどだと思います。

この記事ではGoogle Cloudを利用し、SSL/TLS+Basic認証でインターネット越しの利用に耐えられるようにしています。

その点以外は先発の記事やそれぞれのアプリのドキュメントが大いに参考になります。

- [Building a Personal Aggregated Music Streaming Service with Mopidy, Iris, and Icecast](https://syed-r-ali.medium.com/building-a-personal-aggregated-music-streaming-service-569c9155cedd)

# 使うもの

## Mopidy
> Mopidy is an extensible music server written in Python.
>
Mopidy plays music from local disk, Spotify, SoundCloud, TuneIn, and more. You can edit the playlist from any phone, tablet, or computer using a variety of MPD and web clients.
>
> -- https://mopidy.com/

Mopidyはローカルファイル、Spotify、SoundCloud、TuneInなど、さまざまなサービスの音源を集約して再生できる音楽サーバです。これらのサービスを集約できるならば素敵なことですが、さまざまな複雑さを避けるため今日はローカルファイルの再生についてのみ考えます。

Mopidyは、ネットワーク越しに音楽再生を制御するための機能を提供します。サーバに接続された音声インターフェイスが音を奏でますが、ネットワーク越しに音楽を送信することに関しては面倒を見ません。これもまたさまざまな拡張が提供されています。

### Mopidy-Iris
Mopidyのプラグインの一つで、Webブラウザ経由での操作インターフェイスを提供します。後述するSnapcastクライアントの機能があるため、それ自身が音楽再生プレイヤーになることができます。

### Mopidy-Local
Mopidyのプラグインの一つで、ローカルファイルを扱えるようにします。組み込みのローカルファイルを扱うためのプラグイン、Mopidy-Fileと異なるのは、事前にファイルスキャンをしてライブラリの構築ができる点です。

## Snapcast
https://github.com/badaix/snapcast
> Snapcast is a multiroom client-server audio player, where all clients are time synchronized with the server to play perfectly synced audio. It's not a standalone player, but an extension that turns your existing audio player into a Sonos-like multiroom solution.

Snapcastは音声を送信するサーバと、音声を受信するクライアントで構成された音楽プレイヤーです。Snapcastサーバから送信される音声は、ネットワークを介して複数のSnapcastクライアントで受信することができます。LAN内で動かして複数の部屋で同時に同じ音楽を流すためのものですが、今回はインターネットに載ってもらいます。

## Google Cloud

Googleのクラウドサービスです。以前はGCPと呼ばれており、未だにそう呼んでいる人も少なくありません。そもそも「Google Cloud」で検索しても、「Google Cloud Platform(GCP)」というタイトルのGoogle自身の広告が出てきました。別にAWSでもAzureでも何でもいいですが、単純に使い慣れているので利用します。

### GCE
AWSでいうEC2のようなもの。仮想マシンが建てられます。

### Cloud Run
AWSでいうApp Runnerのようなもの。HTTPサーバの機能を持つコンテナを立ち上げることができるマネージドサービス。エンドポイントにアクセスがあるときだけ起動し課金されます。Herokuから移行する人もいます。

## Docker
よく使われるコンテナのやつ。最近Desktop版の商用利用が有償化されたので、確認していない人は[確認しましょう。](https://www.docker.com/blog/updating-product-subscriptions/)

## nginx
よく使われるWebサーバのミドルウェアです。HTTPS化や認証のために使います。

### nginx-proxy
https://hub.docker.com/r/nginxproxy/nginx-proxy

`nginx.conf`すらも書くのが面倒になった人類が作り出したDockerイメージ。環境変数`VIRTUAL_HOST`を持つコンテナをnginxに接続していきます。

### nginxproxy/acme-companion
https://hub.docker.com/r/nginxproxy/acme-companion

`Let's encrypt`の設定が面倒になった人類が作り出したDockerイメージ。サーバ証明書が発行され、たちまちHTTPSになります。

### Basic認証

簡易的な認証です。パスワードが平文で送られるのでHTTPSとの併用が望ましい、ログアウトがないなど、つらい点が多いですが、簡単に設置できるのが魅力です。用法用量を守って使いましょう。

個人利用なら許されると思いたい。

# 構成

GCEで立ち上げた仮想マシンの上で、素朴にDockerコンテナ（紫の箱）を立てています。MopidyとSnapcastとnginxを押し込んだコンテナを作っています。これは元々Cloud Runで動かそうとしていたため1コンテナにしていますが、本当は分けたいです（面倒くさい）。
![無題の図形描画(1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45240/1fb1a20f-9a05-05ca-8fd9-3b57b8971738.png)

```yaml:docker-compose.yml
version: '2'
services:
  mop:
    build: . 
    image: mopidy
    environment:
      - GCS_BUC=example-bucket
      - HOSTNAME=music.example.com
      - NGINX_SERVER_NAME=music.example.com
      - VIRTUAL_HOST=music.example.com
      - LETSENCRYPT_HOST=music.example.com
      - LETSENCRYPT_EMAIL=music@example.com
      - ACME_OCSP=true
      - PORT=8080
      - VIRTUAL_PORT=8080
    ports:
        - '8080:8080'
    volumes:
        - ../music:/music

  nginx-proxy:
    image: nginxproxy/nginx-proxy
    container_name: nginx-proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - conf:/etc/nginx/conf.d
      - vhost:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
      - dhparam:/etc/nginx/dhparam
      - certs:/etc/nginx/certs:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro

  acme-companion:
    image: nginxproxy/acme-companion
    container_name: nginx-proxy-acme
    volumes_from:
      - nginx-proxy
    volumes:
      - certs:/etc/nginx/certs:rw
      - acme:/etc/acme.sh
      - /var/run/docker.sock:/var/run/docker.sock:ro

volumes:
  conf:
  vhost:
  html:
  dhparam:
  certs:
  acme:

```
## 音声の流れ
次の順に音声データが流れます。

1. Mopidy
1. `/tmp/snapfifo`ファイル
1. snapcastサーバ
1. ネットワーク
1. snapcastクライアント（Mopidy-Iris)
1. Webブラウザ
1. スピーカー

Mopidyが音源ファイルを読み取り、再生します。Mopidyは出力をGStreamerで指定します。Snapcastの設定例どおりにすると、`/tmp/snapfifo`に再生中の音声が保存されるようになります。

```
[audio]
output = audioresample ! audioconvert ! audio/x-raw,rate=48000,channels=2,format=S16LE ! filesink location=/tmp/snapfifo
```
https://github.com/badaix/snapcast/blob/master/doc/player_setup.md

snapcastサーバは次のように指定することで、`/tmp/snapfifo`を読み取ることができます。

```
[stream]
source = pipe:///tmp/snapfifo?name=mopidy
```

クライアントはsnapcastサーバにWebSocketもしくはTCPで接続して、音声を読み取り再生することができます。今回は、クライアントとして、Mopidy-Irisを用います。Mopidy-IrisはWebブラウザ上で動作するので、Webブラウザが動作する環境ならば、音楽を再生できるようになります。

## Mopidyコンテナ内のnginxの役割

MopidyとSnapcastは別のプロセスで起動し、当然それぞれ使用するポートが異なり、Mopidyは6680番、Snapcastは1780番を用います。ただ、Snapcastサーバはwebsocketで通信する場合、`/jsonrpc`と`/stream`の２つがあれば事足り、Mopidy-Irisは`/iris/`以下が中心で競合しませんので、ポートを1つにまとめています。注意点として、ちゃんとWebSocketが通れるように設定します。

また、ここでBASIC認証も設定しておきます。

```conf:nginx.conf
http{
# 略
    server {
# 略
        auth_basic "Basic Authentication";
        auth_basic_user_file /etc/nginx/.htpasswd;

        location ~ /(jsonrpc|stream)$ {
            proxy_pass http://127.0.0.1:1780;
            proxy_http_version 1.1;
# 略
        }
    
        location / {
            proxy_pass http://127.0.0.1:6680;
            proxy_http_version 1.1;
# 略
        }
    }
}
```

## Cloud Runで動かしてみたがやめた

当初、このコンテナはCloud Runで動作させる予定でした。Cloud Runには次のような利点があります。

- コンテナが立ち上がっている時間帯だけ課金されるので節約になる
- 何もしなくてもHTTPSになる
- 何もしなくても自動生成でドメインが割り当てられる

しかし、その利点を上回る面倒な点もあります。

Mopidyはホスト名（ドメイン）を指定して起動する必要がありますが、Cloud Runに起動前は割り当てられるサブドメインがわかりません。そこで、環境変数でホスト名を設定できるようにし、一度ダミーのコンテナを立ててドメインを確定させた後で、環境変数を指定して立ち上げていました。

また本来、こういったステートを持ったコンテナを動かすものではないために注意すべき点があります。使っていないときは自動でシャットダウンされデータが永続化されませんので、起動時と終了時にGCSなどの永続ストレージと同期する必要があります。複数のコンテナが立ち上がると状態が共有されないため、コンテナの同時起動数の上限を1にする必要があります。

そうして頑張ってCloud Runで安定して稼働することができました。しかしある日、ブラウザで音楽プレイヤーのページを開きっぱなしにして寝てしまいました。その結果、WebSocketの通信が張りっぱなしになり、一晩中課金が発生しました。Cloud Runは同スペックのGCEよりも時間あたりの料金が数倍高いため、使用を中止しました。そもそも音楽は長時間だらだら聞くことが多く、数秒で実行が完了するAPIのようなものとは性質が違うので不向きだったのでしょう。

こういった経緯のもと、冒頭の構成図通り、素朴にGCEを使って運用しています。


# 今後の展望

とりあえず最低限のセキュリティを確保して、インターネット上で動作させることはできました。Mopidyは拡張性が高いので、いろいろやっていけそうです。今後やっていきたいこととしては、以下のようなものでしょうか。

- 様々な音楽サービスをソースにできるらしいので試してみたい。
- Cloud StorageやS3などから音楽を再生したい。
- 音楽再生履歴をBigQueryに突っ込みたい。

# サンプル
GitHubリポジトリ https://github.com/m77so/mopidy-docker

`docker-compose.yml`の環境変数を埋めると(多分)動きます。Basic認証のユーザ名、パスワードは`user:password`です。