容器化的无头 Spotify 音乐直播推流

Yelo - 2022/04/10

Spotify 客户端今年推出了「远程群组点歌房」功能,类似虾米做过的「与好友一起听」和「趴间」,但分享点歌房链接的体验做得还不是特别顺畅,好友还需要有平台账号,链接也存在时效。 现阶段相比之下,视频直播的产品体验更加完整,例如小破站就不存在这类问题。所以作为用户,我是不是也可以以直播推流的形式完成「同步听 Spotify 音乐」的效果?

📎 桌面方案

在桌面端,使用 Spotify 客户端播放音乐,使用 OBS 采集声卡输出,并推流至 RTMP 服务器即可。 在 MacOS 中,还可以通过 BlackHole 2ch 提供虚拟声卡,配合系统自带的「音频 MIDI 设置 - 多输出设备」实现一份音频同时输出至虚拟和物理声卡。

进一步,如果还想根据不同程序做更细粒度的输出管理,可以了解 Audio Hijack,这里则不过多介绍。

📎 无头方案

📎 播放 - librespot

社区中比较热门的 spotifyd 可以以 Headless 的形式使用 Spotify,但其实我们只需要其下层的 librespot 即可完成所需的 登录平台 以及 播放音乐 的能力。 librespot 能够以 Spotify Connect 的形式呈现为一台播放设备,由手机或桌面的官方客户端远程控制。

例如在 Shell 中:

cargo install librespot
librespot -u {USERNAME} -p {PASSWORD} -n "My Musicbox" --backend pipe

📎 声卡 - PulseAudio

librespot 中有多种输出方式 (即 backend 参数)。 上面示例中的管道 (pipe) 输出较为特殊,它会输出文件原始数据,接受方需要以一定速率、有节奏地的读取,否则该数据流会过快结束,librespot 收到结束信号后即开始前往下一首歌曲。例如当 stdout 为 fifo /dev/null,由于文件输出快速完成,因此最终会表现为播放一首歌曲两三秒后,librespot 即跳至下一首,并不断重复该过程。

使用其它输出方式则没有这一问题,这里我使用 PulseAudio:

# 安装
sudo apt install pulseaudio pulseaudio-utils
# 允许当前用户使用
sudo usermod -aG pulse,pulse-access $(whoami)
# 启动
pulseaudio -D

librespot 默认版本只支持了个别 backend,因此还需要 单独编译 一个支持 PulseAudio 的版本:

git clone https://github.com/librespot-org/librespot.git
cd librespot
cargo build --release --no-default-features --features pulseaudio-backend

然后准备一张虚拟声卡:

# 删除默认声卡
pactl unload-module 0
# 添加虚拟声卡
pactl load-module module-null-sink sink_name=SpotSink

便可以将音乐输出至 PulseAudio:

./target/release/librespot -u {USERNAME} -p {PASSWORD} -n {DEVICE_NAME} --backend pulseaudio

📎 推流 - FFmpeg

使用 NodeMediaServer 作为测试 RTMP 服务器:

docker run --name nms -d -p 1935:1935 -p 8001:8000 illuspas/node-media-server

使用 FFmpeg 将音频推流:

# 安装 FFmpeg
sudo apt install ffmpeg
# 采集并推流
ffmpeg -f pulse -i "SpotSink.monitor" -f flv rtmp://127.0.0.1/live/spotbox

再加入背景图片作为视频画面:

# 采集并推流
ffmpeg \
  -loop 1 -r 15 -f image2 -s 1280x720 -i ./background.jpg \
  -f pulse -i "SpotSink.monitor" \
  -c:a mp3 -c:v libx264 -preset ultrafast -vf "scale=1280:720:force_original_aspect_ratio=increase,crop=1280:720,fps=30,format=yuv420p" \
  -f flv \
  rtmp://127.0.0.1/live/spotbox

打开 http://127.0.0.1:8001/admin/ 查看 NodeMediaServer 测试流:

NodeMediaServer Screenshot

📎 容器化

最后,将整个过程容器化:

完整流程图
# Dockerfile

FROM ubuntu:20.04

# Setup APT Dependencies
RUN apt-get update -y
RUN DEBIAN_FRONTEND=nointeractive apt-get upgrade -y
RUN DEBIAN_FRONTEND=nointeractive apt-get install -y \
  curl git \
  build-essential libasound2-dev pkg-config \
  pulseaudio pulseaudio-utils \
  ffmpeg

WORKDIR /app

# Setup Rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh
RUN chmod +x rustup.sh
RUN ./rustup.sh -y

# Setup librespot
RUN git clone https://github.com/librespot-org/librespot.git
RUN cd librespot && \
  git checkout v0.3.1 && \
  ~/.cargo/bin/cargo build --release --no-default-features --features pulseaudio-backend

# Setup Script
COPY ./entrypoint.sh .

# Start
CMD ./entrypoint.sh
# entrypoint.sh

## Setup PulseAudio
rm -rf /var/run/pulse /var/lib/pulse /root/.config/pulse
usermod -aG pulse,pulse-access root
pulseaudio -D --system
pactl unload-module 0
pactl load-module module-null-sink sink_name=SpotSink

## Setup Background Image
curl -H "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36" "$BG_IMG_URI" > background.jpg

## librespot
./librespot/target/release/librespot \
  -u "$USERNAME" -p "$PASSWORD" -n "$DEVICE_NAME" \
  --backend pulseaudio | head -c 1G > librespot.log 2>&1 < /dev/null &

## ffmpeg
ffmpeg \
  -loop 1 -r 15 -f image2 -s 1280x720 -i ./background.jpg \
  -f pulse -i "SpotSink.monitor" \
  -c:a aac -c:v libx264 -preset ultrafast -vf "scale=1280:720:force_original_aspect_ratio=increase,crop=1280:720,fps=30,format=yuv420p" \
  -f flv \
  "$OUTPUT" | head -c 1G > ffmpeg.log 2>&1 < /dev/null &

## log
tail -f ./librespot.log ./ffmpeg.log

编译:

docker build . -t spotbox

运行:

docker run -d -e USERNAME="..." -e PASSWORD="..." -e DEVICE_NAME="My Spotbox" -e BG_IMG_URI="https://images.unsplash.com/photo-1608634769432-f9b6524aa2bf?fit=crop&fm=jpg&h=720&q=80&w=1280" -e OUTPUT="rtmp://172.17.0.2/live/spotbox" --name spotbox spotbox
Bilibili Screenshot

最新版本托管在 GitHub

📎 参考