ド素人がicecast2とliquidsoapでストリーミングに挑戦

ラジオのパーソナリティ
©いらすとや.

icecast2のインストール・設定・起動

ストリーミングサーバとしては今回はicecast2を使う。これでなくても良いだろうけど。

$ sudo apt install icecast2
公開用のホスト名(初期値localhost)と管理用パスワードなど3つほどの入力を求められる。

icecast2設定を行う。

/etc/icecast2/icecast.xml (編集)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<icecast>
    <location>Japan</location>
    <admin>streamer@example.com</admin>

    <limits>
        <clients>100</clients>
        <sources>10</sources>
        <queue-size>524288</queue-size>
        <client-timeout>30</client-timeout>
        <header-timeout>15</header-timeout>
        <source-timeout>10</source-timeout>
        <burst-on-connect>0</burst-on-connect>
        <burst-size>0</burst-size>
    </limits>

    <authentication>
        <source-password>秘密</source-password>
        <relay-password>秘密</relay-password>
        <admin-user>admin</admin-user><!-- 変えた方が良いかも -->
        <admin-password>秘密</admin-password>
    </authentication>

    <hostname>stream.example.com</hostname>


    <!-- for liquidsoap (wo/TLS)-->
    <listen-socket>
        <port>8000</port>
    </listen-socket>

    <!-- Main Stream (w/TLS)-->
    <listen-socket>
        <port>8443</port>
        <ssl>1</ssl>
    </listen-socket>

    <http-headers>
        <header name="Access-Control-Allow-Origin" value="*" />
    </http-headers>

    <fileserve>1</fileserve>

    <paths>
        <basedir>/usr/share/icecast2</basedir>

        <logdir>/var/log/icecast2</logdir>

        <webroot>/usr/share/icecast2/web</webroot>
        <adminroot>/usr/share/icecast2/admin</adminroot>
        <alias source="/" destination="/status.xsl"/>
        <ssl-certificate>/etc/icecast2/ssl/merge.pem</ssl-certificate><!-- TLS証明書 -->
    </paths>
    
    <logging>
        <accesslog>access.log</accesslog>
        <errorlog>error.log</errorlog>
        <loglevel>3</loglevel> <!-- 4 Debug, 3 Info, 2 Warn, 1 Error -->
        <logsize>10000</logsize> <!-- Max size of a logfile -->
    </logging>

    <security>
        <chroot>0</chroot>
    </security>
</icecast>

先ほど入力した3つのパスワードはこのファイルに書き込まれている。 <limits></limits>の中。インストール直後の質問をスキップした場合は手動で入力。(なんと平文)
ウェブ管理用等のアカウント名は初期値は admin のようだが、おそらくこれも変更した方が良さげ。

TLSの証明書は公開鍵と秘密鍵を1つのファイルにマージしなければならないみたい。公開鍵(上)+秘密鍵(下)みたいな。

サンプル設定ファイルでは配信用のポートの初期値は8000だが、そのポートをlocalhost, LAN, DMZ内のliquidsoap用にして、8443 (TLS)を視聴者向けのストリーミングポートにする。なので、ファイヤウォールは外部からの8443は開けて、8000は外には開かない。

初歩的な部分で忘れがちだが、ログ置き場を適切なユーザーで作っておかないとicecast2が起動してくれない。ログ用ディレクトリをたとえば666で誰でも読み書きできるパーミッションを付けても動かないときは動かない?ユーザーicecast2、グループicecastで作った方が確実かも。ちなみにicecast2の起動スクリプトは/etc/init.d/icecast2だった。これは気に入らないので普通のsystemd用に作り直した方が良いかも。

$ sudo systemctl enable icecast2   #サービス有効化
$ sudo systemctl start icecast2    #サービス開始

icecast2はDebian/Ubuntuのパッケージにsystemdのサービス起動用ファイルが入っているので、ただ有効化・起動を実行するだけで簡単。

ブラウザでhttp://live.example.com:8000 または http://live.example.com:8000/status.xsl を開くとicecast2のステータス画面が表示される。 この設定ではicecastではストリーミングの中身が何も無い状態。次のliquidsoapで。

Liquidsoapのインストール

Debian/Ubuntuのパッケージだとliquidsoapのバージョンが古い。
しかし、2020年12月上旬現在はopamなどでビルドしようとするとliquidsoapのビルドで必要とするシェアドライブラリのバージョンが古いため苦労するかも。 libshineとかlibmad0あたりは特に。この辺り柔軟に根気強く対応できる人以外にはビルドはオススメしない。
バージョンが少し古くてもDebian/Ubuntuのパッケージでインストールする方が圧倒的に簡単。

$ sudo apt install liquidsoap
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています                
状態情報を読み取っています... 完了
以下の追加パッケージがインストールされます:

中略

提案パッケージ:
  libaudio2 libsndio6.1 libbluray-bdj libcamomile-ocaml-dev libfftw3-bin libfftw3-dev libgd-tools
  libvisual-0.4-plugins jackd2 liblo-dev opus-tools pulseaudio libraw1394-doc librsvg2-bin serdi sndiod sordi
  libsox-fmt-all festival mplayer youtube-dl opencl-icd
推奨パッケージ:
  libaacs0 libgdk-pixbuf2.0-bin gstreamer1.0-plugins-base librsvg2-common va-driver-all | va-driver vdpau-driver-all
  | vdpau-driver vorbis-tools vorbisgain
以下のパッケージが新たにインストールされます:
  fontconfig libao-common libao4 libaom0 libass9 libasyncns0 libavc1394-0 libavcodec58 libavdevice58 libavfilter7
  libavformat58 libavutil56 libbluray2 libbs2b0 libcairo-gobject2 libcamomile-ocaml-data libcdio-cdda2
  libcdio-paranoia2 libcdio18 libchromaprint1 libcodec2-0.9 libdatrie1 libdc1394-22 libdrm-amdgpu1 libdrm-common
  libdrm-nouveau2 libdrm-radeon1 libdrm2 libexif12 libfaad2 libfftw3-double3 libflac8 libflite1 libfribidi0 libgavl1
  libgd3 libgdk-pixbuf2.0-0 libgdk-pixbuf2.0-common libgif7 libgl1 libgl1-mesa-dri libglapi-mesa libglvnd0
  libglx-mesa0 libglx0 libgme0 libgraphite2-3 libgsm1 libgstreamer-plugins-base1.0-0 libharfbuzz0b libiec61883-0
  libjack-jackd2-0 libjbig0 libjpeg-turbo8 libjpeg8 liblilv-0-0 libllvm10 liblo7 libmad0 libmp3lame0 libmpg123-0
  libmysofa1 libnorm1 libnuma1 libopenal-data libopenal1 libopencore-amrnb0 libopencore-amrwb0 libopenjp2-7
  libopenmpt0 libopus0 liborc-0.4-0 libpango-1.0-0 libpangocairo-1.0-0 libpangoft2-1.0-0 libpgm-5.2-0 libportaudio2
  libpostproc55 libpulse0 libraw1394-11 librsvg2-2 librubberband2 libsdl-image1.2 libsdl-ttf2.0-0 libsdl1.2debian
  libsdl2-2.0-0 libserd-0-0 libshine3 libsnappy1v5 libsndfile1 libsndio7.0 libsodium23 libsord-0-0 libsoundtouch1
  libsox-fmt-alsa libsox-fmt-base libsox3 libsoxr0 libsratom-0-0 libssh-gcrypt-4 libswresample3 libswscale5
  libtag1v5 libtag1v5-vanilla libthai-data libthai0 libtiff5 libtwolame0 libva-drm2 libva-x11-2 libva2 libvdpau1
  libvidstab1.1 libvorbisenc2 libvpx6 libwavpack1 libwayland-client0 libwayland-cursor0 libwayland-egl1 libwebp6
  libwebpmux3 libx11-xcb1 libx264-155 libx265-179 libxcb-dri2-0 libxcb-dri3-0 libxcb-glx0 libxcb-present0
  libxcb-shape0 libxcb-sync1 libxcb-xfixes0 libxcursor1 libxdamage1 libxfixes3 libxi6 libxinerama1 libxkbcommon0
  libxpm4 libxrandr2 libxshmfence1 libxss1 libxv1 libxvidcore4 libxxf86vm1 libzmq5 libzvbi-common libzvbi0
  liquidsoap ocaml-base-nox ocl-icd-libopencl1 shared-mime-info sox x11-common
アップグレード: 0 個、新規インストール: 153 個、削除: 0 個、保留: 89 個。
71.3 MB のアーカイブを取得する必要があります。
この操作後に追加で 813 MB のディスク容量が消費されます。
続行しますか? [Y/n]

Liquidsoapをバッチ処理の簡易マネージャの類かな?くらいにあなどっていたこともあって、まさかの大量のパッケージを必要とすること愕然。まぁ要るんだと言われたら仕方ないのでこのまま[Y]でインストール。

結構重要なのにDebian/Ubuntuのパッケージのインストールでやってくれないことがある。
下の3行を実行しておかないと、liquidsoapで何かの機能を使おうとしたときに「ライブラリpervasives.liqとそれに付随するスクリプトがインストールされている必要があります。」という条件から外れるので盛大にトラブる。
liquidsoapは設定ファイルで特にインクルードしなくても /usr/local/lib/liquidsoap/<version> の pervasives.liq ファイル他を読んでそこに定義されている関数等が使えるのだけど、 Debian/Ubuntu のパッケージではなぜか /usr/share/liquidsoap/libs にインストールしちゃうのでliquidsoapが読めない状態になっている。そこで下の3行。
なお、バージョン(下の例だと1.4.1)は必要に応じて変更。

$ sudo mkdir /usr/share/liquidsoap/1.4.1
$ sudo ln -s /usr/share/liquidsoap/libs /usr/share/liquidsoap/1.4.1/libs
$ sudo ln -s /usr/share/liquidsoap/bin /usr/share/liquidsoap/1.4.1/bin

Debian/Ubuntuのliquidsoapパッケージはsystemd用のサービス起動関係が用意されていない。そこで最低限動くだけの簡易的なのを作った。

/lib/systemd/system/liquidsoap.service (新規)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[Unit]
Description=Liquidsoap daemon
Documentation=http://liquidsoap.fm/
After=network.target

[Service]
Type=simple
User=root
Group=liquidsoap
PIDFile=/etc/liquidsoap/liquidsoap.pid
ExecStart=/usr/bin/liquidsoap /etc/liquidsoap/main.liq
ExecStop=/usr/bin/killall liquidsoap

[Install]
WantedBy=multi-user.target

pidファイルは/run以下に置いたらパーミッションでエラーになってliquidsoapが落ちた。/run/liquidsoapディレクトリを作ってそのディレクトリを書き込み可にしたらliquidsoapはそのときは落ちなかったけど、システムを再起動したら /run/liquidsoap ディレクトリが消えて再びliquidsoapが落ちた。liquidsoap起動直前に/run/liquidsoapディレクトリを作るようにExecStartPre行を書けば良いのだが、そもそも/runディレクトリに拘る必要もないので/etc/liquidsoapに発生するようにした。作法的にはどうかとは思う。
liquidsoap起動に必要なメインの設定ファイルは /etc/liquidsoap/main.liq ということにした。

liquidsoapの設定

/etc/liquidsoap/main.liq (新規)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
set("init.daemon.pidfile.path", "/etc/liquidsoap/liquidsoap.pid")
set("init.daemon",true)
set("log.file.path","/var/log/liquidsoap/radioliq.log")
#set("log.level",4)


failover = single("/media/streaming/fail.mp3")
music = playlist("/etc/liquidsoap/music.pls")
news = single("/media/streaming/news.mp3")

jingle1 = single("/media/streaming/jingle1.mp3")
jingle2 = single("/media/streaming/jingle2.mp3")
jingle3 = single("/media/streaming/jingle3.mp3")

#def news98()
#    request.create("/media/streaming/news98.mp3")
#end

#news99 = request.queue(
#   queue = [
#       request.create("/media/streaming/jingle2.mp3"),
#       request.create("/media/streaming/news_open.mp3"),
#       request.create("/media/streaming/news99.mp3"),
#       request.create("/media/streaming/news_close.mp3")
#   ]
#)

stream = fallback(
    [
        #request.queue(id="request"),
        switch(
            track_sensitive=false,
            [
                ({0h0m-0h2m},   once(jingle1)),
                ({1h0m-1h2m},   once(jingle2)),
                ({2h0m-2h2m},   once(jingle3)),
                ({3h0m-3h2m},   once(jingle1)),
                ({4h0m-4h2m},   once(jingle2)),
                ({5h0m-5h2m},   once(jingle3)),
                ({6h0m-7h},     once(news)),
                ({7h0m-7h2m},   once(jingle2)),
                ({8h0m-8h2m},   once(jingle3)),
                ({9h0m-10h},    once(news)),
                ({10h0m-10h2m}, once(jingle2)),
                ({11h0m-11h2m}, once(jingle3)),
                ({12h0m-13h},   once(news)),
                ({13h0m-13h2m}, once(jingle2)),
                ({14h0m-14h2m}, once(jingle3)),
                ({15h0m-16h},   once(news)),
                ({16h0m-16h2m}, once(jingle2)),
                ({17h0m-17h2m}, once(jingle3)),
                ({18h0m-19h},   once(news)),
                ({19h0m-19h2m}, once(jingle2)),
                ({20h0m-20h2m}, once(jingle3)),
                ({21h0m-22h},   once(news)),
                ({22h0m-22h2m}, once(jingle2)),
                ({23h0m-23h2m}, once(jingle3)),
               #({23h0m-23h30m}, request.dynamic(news98)),
               #({23h0m-23h30m}, news99)),
                ({ true }, music),
            ]
        ),
    failover
    ]
)

output.icecast(
    %vorbis.cbr(
        samplerate=44100,
        channels=2,
        bitrate=128
    ),
    host = "localhost",
    port = 8000,
    password = "秘密",
    mount = "/stream", name="秘密のストリーミング",
    description = "ストリーミングの説明",
    stream
)

liquidsoapを全く理解していない素人が作った設定です。liquidsoapのドキュメントは、バカ避けになっていて「がとらぼ」の中の人の悪いアタマでは全く理解できない代物なので、もっとも簡単な関数だけを使った試行錯誤の結果がコレ。エキスパートならこんなアホなのじゃなく良いのを作ってくれる筈。

あと、この例では音量正規化の normalize() やクロスフェードの crossfade() やモノラルをステレオ化する audio_to_stereo() のような良く使うだろう関数も敢えて使ってない。効果を与えたい対象を関数に放り込んでやるだけ。

指定されている楽曲ファイルが存在しないなど、最も正常に再生できない条件のときにはfailover(名前は任意)で指定したfail.mp3ファイルが再生される。
そうでなければ通常はmusic(名前は任意)で指定した/etc/liquidsoap/music.pls (プレイリスト)が再生される。
さらに、switchで簡単なスケジュールを作っている。このスケジュールは、楽曲等を再生するだけでライブ放送の割り込み等は無し。
上の例では、6, 9, 12, 15, 18, 21時にニュースが再生される。ニュース再生のある時間を除く毎時0分にはジングル(3種の1つ)が再生される。

ジングルといえば普通は1分以内だろうと思われるが、本当に1分以内であれば、開始時間だけを書けば良いみたい。(例13h00mとか13h0sとか)
しかし、ジングルであろうと他であろうと1分を超えるなら開始時刻だけでなく終了時間も書く。 (例: 13h00m-13h30m)でないと、開始時刻だけの場合は再生開始から1分でその再生が打ち切られてしまう。このとき、終了時間については、ジングルの再生時間ぴったりがおそらくは良いのだろうけど、ぴったり以上で且つスケジュールの次の再生が始まるまでの時間であれば問題無いよう。例えば、13h0m-13h30mのように長い時間(この例では30分)を指定して1,2分の短い音声を再生してもonce()を使えば1回再生した後にswitchのデフォルト(上の例だとmusic)が再生されるので問題がないことになる。なお、once()を使わないと、指定した時間内を使って指定した1つの音声ファイルやプレイリストのループ再生になる。例えば13h0m-13h30mの30分間ずっとジングルが繰り返されるようなことになる。

簡単に考えている素人には挙動の想像がつかないのが、再生途中に再生が打ち切られた場合に、次に呼ばれたときにその続きが再生されるというところ。ヘタに打ち切りを発生させると後で影響してくるのでとても怖い。キューを停める・スキップする・リセットするというのは「がとらぼ」の中の人はまだ理解していない。キューの操作が理解できれば、もしかしたら怖くはないのかもしれないけど。

single(音声ファイル), playlist(プレイリストファイル)の単独利用だとループ再生、once(single(音声ファイル)), once(playlist(プレイリストファイル))なら音声ファイル或いはプレイリストを1回再生して停まる
そういうことであるのなら、hoge = once(single(foo)) という風にonce()指定でhogeを作っておいて、switchの中でhogeを呼ぶだけで良いのではないかと普通は考えそうな気もするが、それが罠で、この場合はhogeが1度だけ実行されるものの、別の時間にhogeを呼んでも再生してくれない。hoge = single(foo)でonce()無しのhogeを作って、switchの中でonce(hoge)であれば、別の時刻にonce(hoge)を呼んでもそれが再生される。

liquidsoapのこのような挙動はとにかくクセが強く、ドキュメントを読んだ程度では理解できず、全く思い通りに動いてくれない。1ヶ月触った程度では本当に全然ワケワカラン。
liquidsoapのことをググると簡単に番組が組めるようなことが書いてあったりするが、そんなこと書くのはロクにliquidsoapを触ってない人じゃないかしら?楽曲再生で一番最初に触れるであろうsingle(), playlist()関数1つ使うにも挙動に凄いクセがあるような代物を簡単に使いこなせるワケがない。liquidsoapで番組(スケジュール)を組んで不特定多数に聴いて貰おうなんていうことになると、liquidsoapによほど精通した人が綿密に設定を組まないとしょっちゅう放送が停まったり予定外の内容が流れたとかエライことになりそうに思う。

liquidsoapのクセのある挙動
上で書いたことを図にしてみた。

最後の方のoutput.icecast()が出力先のicecastなので、host, port, passwordの値を先に設定したicecastに合うようにする。icecast2とliquidsoapを同じホストで動かすならホスト名は localhost にする。

$ sudo systemctl enable liquidsoap   #サービス有効化
$ sudo systemctl start liquidsoap    #サービス開始
なお、systemdのサービス実行ではなく、単に手動でliquidsoapを起動するなら
$ liquidsoap /etc/liquidsoap/main.liq  (main.liqの中でdaemon稼働を指定したらこれでliquidsoapがバックグラウンドで動く)

icecastの設定(icecast.xml)でstream.example.com:8443 (TSL)でストリーミングを行う設定にした。また、liquidsoapの設定(main.liq)の59行目でマウントポイントを/streamにした。
なので、ストリーミングURLは https://stream.example.com:8443/stream になる。このURLをウェブブラウザで開くとかネットラジオのプレーヤーに登録するとストリーミングを視聴できる。(今回の設定では聴くだけだが)

ルーターが非力でかつNICが糞だとストリーミングを続けるとときどきルーター(のNIC)が落ちるのでストリーミング用のサーバよりルーターのNICに金かけた方が良いっぽい。転送量が多いとか負荷が高いとかいうわけではないと思うけどNICが落ちるのは何でかしら。

liquidsoapの設定が自由自在なエキスパートを除いてはliquidsoapを直に触って番組を組んだりするのはやめた方が無難で、 AirTime, AzuraCast, LibreTime, MSCP Pro - Media Server Control Panel(有料), OpenBroadcaster(気色が違うが), あたりのGUIのツールを利用する方が良さそう。

初稿ではonce(single())の挙動を理解できていなかったのでrequest.dynamic()でジングルを実現していたが、ようやくonce()を使う場合の挙動の謎が解り始めたのでonce(single(hoge))を使うように大幅に変更して、図も追加した。
なお、liquidsoapについてはやっぱり殆ど理解できていない。本当に難しい。