Multimedia: musique locale et streaming

Mise à jour 2021-mai-13: le système upmpdcli+mpd se bloque parfois, et je n’ai pas trouvé la source du problème. Par contre, un contournement possible est le redémarrage du service mpd. Comme on est avec un serveur sans écran ni clavier, il faut arriver à détecter le blocage, et redémarrer le service automatiquement. Les détails

Mise à jour 2021-jan-18: si on n’a pas besoin de la partie vidéo, mais qu’on se contente de la musique (musique locale, stream de radio internet, et éventuellement stream de Qobuz ou autre fournisseur compatible UPnP), alors la partie BubbleUPnP server n’est pas nécessaire. Il suffit d’avoir upnpdcli en open-home. Je supprime donc la partir BubbleUPnP server. C’est cette réponse de l’équipe BubbleSoft qui m’a convaincu.

Rappel: du multi-média simple

Je veux qu’on puisse simplement:

  • écouter de la musique (vers la chaine hifi) à partir d’un répertoire de musique locale (rip de CDs);
  • écouter de la musique à partir d’un stream (Deezer, Qobuz …)
  • envoyer des films vers chaine hifi (son) et écran (?) à partir de fichiers
  • stream de radio internet (au moins: France Culture et KUT)

Denis propose BubbleUPnp et MinimServer. C’est parti.

L’architecture

Source de documentation: la documentation de upmpdcli et un article de HIFIZINE.

A partir de là (en oubliant le BubbleUPnP server), on va utiliser:

  • la radio de la cuisine (Libratone ZipMini) est compatible UPnP, rien à faire c’est automatique, c’est donc un renderer de prêt;
  • MPD pour jouer la musique sur la carte audio du serveur (branchée sur l’ampli de la chaîne);
  • MinimServer pour indexer la musique locale et la présenter à MPD;
  • upmpdcli qui est une interface de renderer UPnP, et va piloter MPD. En outre, on va lui préconfigurer une liste de radio à streamer;
  • l’application BubbleUpNP Android (téléphone, tablette), qui va interroger upmpdcli et piloter tout ça.

TODO: insérer un schéma

Musique locale : MinimServer

MinimServer et BubbleUpNP vont avoir besoin de Java pour s’exécuter, donc on installe.

> sudo yum install java-11-openjdk-headless.x86_64

Création d’un groupe media, ajout de l’utilisateur à ce groupe, et tous les fichiers de ce groupe seront accessibles via ce groupe.

> sudo groupadd media
> sudo usermod -a -G media fabien

Installation du software dans /home/minimserver (groupe media):

> cd /home/minimserver
> tar xzpf MinimServer-2.0.16-linux-intel.tar.gz
# configuration minimale
> minimserver/bin/setup
# démarrage au boot
> minimserver/bin/startd
# pour configurer le chemin des donnees
> minimserver/bin/startc

Bon, en fait ça ne fonctionne pas. Après quelques reboot et quelques tests, il semblerait que ça ne fonctionne pas à cause de SELinux. Pro-tip: il faut vérifier le audit.log pour commencer à comprendre le problème.

Donc on passe SELinux en permissif dans /etc/selinux/config. Et ça marche.

On crée un service systemd dans /etc/systemd/system/multi-user.target.wants/minimserver.service:

[Unit]
Description=MinimServer
After=multi-user.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/su "fabien" -c "/home/minimserver/minimserver/bin/startd init"
ExecStop=/home/minimserver/minimserver/bin/stopall

[Install]
WantedBy=multi-user.target

Une interface web de configuration est disponible sur le port 9790, pratique pour lancer un “rescan” quand on a ajouté des fichiers audio dans la base.

Renderer local: upmpdcli

Là c’est un tout petit peu plus compliqué, pas de rpm ou de package tout fait. Donc on va commencer par installer tous les packages de base pour pouvoir compiler. Note: le package llvm-toolset embarque gcc et tout un tas d’outils, donc plutôt que de les faire un à un, j’ai pris ce package. Il n’empêche qu’il y avait d’autres outils à installer. A commencer par le vénérable make, qui n’est pas dans l’installation minimale de CentOS.

Cette liste a peut-être des éléments inutiles. Et je l’ai construite au fur et à mesure de la compilation - en fonction des besoins.

sudo yum install libupnp libmpdclient expat jsoncpp libmicrohttpd
sudo yum install libcurl python-requests-futures
yum install llvm-toolset
sudo yum install libmicrohttpd-devel expat-devel make autoconf automake libtool libmpdclient-devel jsoncpp-devel

Ensuite on peut passer à la compilation:

tar xzpf libnpupnp-4.0.13.tar.gz
cd libnpupnp-4.0.13
./configure --prefix=/usr --sysconfdir=/etc
make
sudo make install
cd ..
tar xzpf libupnpp-0.19.4.tar.gz
./configure --prefix=/usr --sysconfdir=/etc
make
sudo make install
cd ..
tar xzpf upmpdcli-1.4.14.tar.gz
./configure --prefix=/usr --sysconfdir=/etc --disable-spotify
make
sudo make install

Et là si tout se passe bien on a un executable upmpdcli. Mais pas de son…

Le fichier de service est dans les sources, on le déplace dans la configuration de systemd.

[Unit]
Description=UPnP Renderer front-end to MPD
After=network-online.target mpd.service
Wants=network-online.target

[Service]
User=bubbleupnp
Group=media
Type=simple
Restart=on-failure
#Type=simple
## ExecStartPre=/bin/sleep 30
## Note: if start fails check with "systemctl status upmpdcli"
ExecStart=/usr/bin/upmpdcli -c /etc/upmpdcli.conf
## For some reason, it happens the first start of libupnp fails. Probably
## this should be started later in the start sequence, but I don't know
## how. Retry a bit later.
#Restart=always
#RestartSec=1min

[Install]
WantedBy=multi-user.target

Et on autorise le chargement au boot:

systemctl enable upmpdcli.service
systemctl start upmpdcli.service 

Music Player Daemon

Pour MPD il faut ajouter des dépôts complémentaires:

yum install https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-8.noarch.rpm 
yum install https://mirrors.rpmfusion.org/nonfree/el/rpmfusion-nonfree-release-8.noarch.rpm
yum install mpd

Ensuite ça a été la galère. J’ai voulu tenter de passer par PulseAudio, parce que c’est une solution moderne, et je n’ai rien compris. Infoutu d’avoir du son. Donc je suis retourné sur l’ancêtre ALSA et son design vintage.

yum install alsa-utils

Sélection de la bonne carte son:

aplay -L
...
surround21:CARD=Generic_1,DEV=0
    HD-Audio Generic, ALC1220 Analog
    2.1 Surround output to Front and Subwoofer speakers
...

Il y a plein de cartes listées, parce que a priori ma carte gère du mono au 7.1 en passant par le stéréo. Je n’ai qu’un ampli avec deux enceintes, donc après consultation de la documentation de ma carte, il y a une sortie à choisir qui correspond à la configuration “surround21”.

Donc on ajoute dans /etc/asound.conf:

pcm.!default {
    type hw
    card Generic_1
}

ctl.!default {
    type hw
    card Generic_1
}

Et dans /etc/mpd.conf:

audio_output {
        type            "alsa"
        name            "My ALSA Device"
        device          "surround21:CARD=Generic_1,DEV=0"       # optional
##      mixer_type      "hardware"      # optional
##      mixer_device    "default"       # optional
##      mixer_control   "PCM"           # optional
##      mixer_index     "0"             # optional
}

Un coup de alsamixer pour vérifier que les niveaux ne sont pas à 0 (touch “M” pour unmute).

On démarre le MPD et on teste (avec le player en ligne de commande mpc):

mpc add /
mpc play

Miracle, du son :)

Notes: pour le upmpdcli, il faut ouvrir un port dans nftables:

# add upmpdcli interface port
tcp dport 49152 ip saddr $SAFE_TRAFFIC_IPS accept comment "upmpdcli connection"

Bonus: une interface web pour alsamixer

Si jamais lors d’un reboot les paramètres de ALSA sont perdus, et que la carte son se retourve en mute, il faudrait pouvoir accéder à l’équivalent de alsamixer via une interface web. Cela évitera de démarrer un ordinateur, un terminal, ouvrir une session SSH sur le serveur et remettre en place les réglages ALSA.

Pour ça Alsamixer Web UI va faire l’affaire.

git clone https://github.com/JiriSko/amixer-webui.git
pip3 install --user flask
pip3 install --user argparse
# test (attention aux ports ouvert avec nftable, par défaut ici c'est 8080)
python3 alsamixer_webui.py 

La seule commande disponible pour l’installation c’est le make install. Pas de compilation (c’est un script Python). Après inspection du Makefile, il n’y a rien de bien dangereux, juste une copie dans /usr/share/amixer-webui.

sudo make install

Et on fait un service systemd pour le démarrage automatique.

[Unit]
Description=Alsamixer Web UI front end
After=network-online.target mpd.service
Wants=network-online.target

[Service]
User=fabien
Group=media
Type=simple
Restart=on-failure
ExecStart=/usr/bin/python3 /usr/share/amixer-webui/alsamixer_webui.py 

[Install]
WantedBy=multi-user.target

L’utilisateur est fabien parce que on ne veut pas tourner ça en root, et les packages flask et argparse ont été installés pour cet utilisateur.

Un petit tour sur l’adresse http:serveur:8080 et on voit une interface qui permet de régler ALSA.

Il y a même une app sur f-droid !

Done !

Bonus plus: le support de la radio

A priori, upmpdcli supporte la radio, on peut pré-configurer une liste de radio internet, et choisir ensuite via le controlleur quelle radio jouer.

Un fichier est fourni lors de l’installation, sous /usr/share/upmpdcli/radio_scripts/radiolist.conf (avec entre autres France Culture, France Inter, …)

Activation dans le fichier de configuration /etc/upmpdcli.conf :

# The name of the room where the Product is located.
ohproductroom = Salon
# Path to an external file with radio definitions.
radiolist = /usr/share/upmpdcli/radio_scripts/radiolist.conf

Puis redémarrage du service:

systemctl restart upmpdcli.service

Et … rien ne se passe, aucun accès radio sur le BubbleUPnP (Android). Quelques recherches internet, de l’expolration de logs, et finalement la solution est dans la doc de upmpdcli: pour utiliser la radio, il faut BubbleDS Next (pour Linn DS).

Installation sur Android, et à nous les radios !

Configuration de quelques radios:

# France Culture
# https://www.franceculture.fr

[radio France Culture]
url = https://stream.radiofrance.fr/franceculture/franceculture_hifi.m3u8?id=radiofrance
# France Inter
# https://www.franceinter.fr/

[radio France Inter]
url = https://stream.radiofrance.fr/franceinter/franceinter_hifi.m3u8?id=radiofrance
metaScript = radio-france-meta.py 1
preferScript = 1

# France Info
# https://www.francetvinfo.fr/

[radio France Info]
url = https://stream.radiofrance.fr/franceinfo/franceinfo_hifi.m3u8?id=radiofrance

# KUT
[radio KUT]
url = https://kut.streamguys1.com/kut-free.aac
artUrl = http://npr-brightspot.s3.amazonaws.com/7f/a4/076796614e0992199de0d50d1cb5/kut-header-logo-new2.png


[radio KUTX]
url = https://kut.streamguys1.com/kutx-free.aac
artUrl = https://yt3.ggpht.com/ytc/AAUvwniTw5DcMrGRAXj7U7vsiQRLKvRLIT22xwwkW8qwqg=s68-c-k-c0x00ffffff-no-rj

... etc ...

Les radios de France Musique

MPD autorestart

En inspectant les logs de MPD (tail /var/log/mpd/mpd.log) et les logs de upmpdcli (journactl -t upmpdcli -b) on observe deux types d’erreurs:

  • un Decoder is too slow dans les logs MPD. La meilleure corrélation que j’ai c’est en lisant certaines pistes sur Deezer, mais c’est difficile à reproduire
  • un message upmpdcli se plaignant de MPD:
May 13 09:32:26 nestor upmpdcli[2692]: :2:src/mpdcli.cxx:332::mpd_run_clear(m_conn) failed: Connection closed by the server
May 13 09:32:26 nestor upmpdcli[2692]: :3:src/mpdcli.cxx:148::MPDCli::startEventLoop: already started
May 13 09:35:03 nestor upmpdcli[2692]: :2:src/mpdcli.cxx:237::MPDCli::eventloop: mpd_run_idle_mask returned 0
May 13 09:35:03 nestor upmpdcli[2692]: :3:src/mpdcli.cxx:148::MPDCli::startEventLoop: already started

Donc on va faire un service systemctl qui surveille ces logs, et envoie un restart au service MPD en cas de détection des ces erreurs.

#! /bin/bash

while :
    do                          
        SLOW=$(tail -n 1 /var/log/mpd/mpd.log)
        if [[ $SLOW == *"Decoder is too slow"* ]]; then
            systemctl restart mpd.service
            # we add a line in the upmpdcli log to avoid another mpd restart
            echo 'MPD autorestart: decoder too slow detected: sent systemctl restart mpd.service' | systemd-cat -p info -t upmpdcli                                        
        fi

        UPMPDCLI=$(journalctl -t upmpdcli -p 6 -b -q -n 2)
        if [[ $UPMPDCLI == *"::mpd_run_clear(m_conn) failed: Connection closed by the server"*"::MPDCli::startEventLoop: already started"* ]]; then
            systemctl restart mpd.service
            # we add a line in the upmpdcli log to avoid another mpd restart
            echo 'MPD autorestart: connection closed by server detected: sent systemctl restart mpd.service' | systemd-cat -p info -t upmpdcli
        fi

        UPMPDCLIIDLE=$(journalctl -t upmpdcli -p 6 -b -q -n 2)
        if [[ $UPMPDCLIDLE == *":MPDCli::eventloop: mpd_run_idle_mask returned 0"*"::MPDCli::startEventLoop: already started"* ]]; then
            systemctl restart mpd.service
            # we add a line in the upmpdcli log to avoid another mpd restart
            echo 'MPD autorestart: mpd run idle detected: sent systemctl restart mpd.service' | systemd-cat -p info -t upmpdcli
        fi

        sleep 1
done

Quelques notes:

  • La commande de base pour redémarrer le service mpd : systemctl restart mpd.service
  • Lecture de la dernière ligne d’un fichier: tail -n 1 /path/to/file
  • Ajout d’un message dans les logs systemd: en utilisant la commande systemd-cat -p <loglevel> -t <unitname>
  • La lecture des deux dernières lignes d’un log systemd: journalctl -t upmpdcli -p 6 -b -q -n 2
    • -p 6 : indique le niveau info des logs
    • -b : depuis le dernier boot
    • -q : pas de ligne informative superflue
    • -n 2 : les deux dernières lignes

Il reste à créer un service pour tourner ce script. Dans /usr/local/lib/systemd/system/mpd_autorestart.service:

Description=Fix for MPD auto restart

[Service]
ExecStart=/root/scripts/mpd_tooslow_restart.bash

[Install]
WantedBy=multi-user.target

Voilà. A voir si tout ça fonctionne, en attendant le prochain blocage.

Précédent
Suivant