Today there are so many streaming services that you can use. But they can be costly, especially if you go with lossless streaming. If you have a large musical collection, wouldn’t it be great if you could make your own private web-radio with it?
Granted, there are service like Roon that can do that. But I personally never enjoyed Roon radio as it always ends up playing something unexpected. So I decided that I should try making my own private web-radio.
This article does not cover the legal requirements about making a publicly accessible web-radio in your country of residence and I strongly advice that you read those before you do so.
I have all my music on my NAS and I wanted a stream that plays randomly among multiple playlists. I also have a VPS, you can find some pretty cheap ones for as low as 3 to 5 euros per month. No need for too much RAM or vCPUs, although I advise having at least 2 GB of RAM.
So to resume what I had at the beginning of my project:
- The sender: it’s my NAS. All my music is on it. I have it in FLAC format so I can stream lossless music. The NAS is currently running Linux. Although I could totally host an Icecast instance on my NAS, I needed to manage a dynhost solution and this has proven to be unreliable so I opted to send the stream to an external server, that I call the receiver.
- The receiver: a little inexpensive VPS, running Linux as well.
- I think this is an important prerequisite as well : a fast internet connection ideally at least 100 Mbits so that it won’t slow down your other activites.
I tried many solutions:
- Streaming music from Mixxx to an Icecast instance on my server
- Streaming music from Liquidsoap to an Icecast instance on my server
- Streaming music from Liquidsoap to another Liquidsoap instance using Harbor
But those solutions were very unstable. I needed the following constraints to be respected:
- The whole system should be stable, tolerant to internet cuts, self restartable. And I cannot emphasize enough how important this is. When you decide to
- Ideally the sender would send only one lossless stream to the server that will then transcode to different format on the fly. This is to save CPU on my NAS (or it would starts to be noisy) and also bandwidth (send one stream uses less bandwidth than four)
- It should not use too much resources both on the sender and the receiver
So here is what worked best for me:
On the sender, I have a Liquidsoap instance. It’s reading my playlists, each playlist has a weight so that I can control how often a song from a particular playlist will be played.
On the receiver, I have another Liquidsoap instance, and an Icecast instance. That Liquidsoap instance receives the audio stream, transcodes it to several formats on the fly, and sends the streams to the Icecast instance.
I chose to use the SRT transport between the two Liquidsoap instances for it’s stability and reliability as I had some resources issues with Harbor, another way to transmit the audio data.
OPTIONAL. A web-radio is usually coupled to a website, so for educational purpose I kept some optional parts that I highlighted. The sender will make some requests on the server to update metadata on the Liquidsoap instance, because metadata is currently not updated when streaming FLAC data. Also to update what’s playing now on a website. I also have a system that let me skip the track currently playing, get the current song time and metadata, However this article will focus more on getting the stream from a point A to a point B, to an Icecast instance. So feel free to remove any optional part. If you need to know, I have an express + socket.io server on both the sender and the receiver that let me communicate from the website to the sender, and vice versa. Interaction with a website could be another article.
The usernames, hosts, ports and passwords have been changed so you should replace with what works for you.
I used the rolling release of Liquidsoap 2.2.0 at the time of writing, so make sure that you use at least this version or ulterior.
Here is the script that I use on the sender:
#!/usr/bin/liquidsoap
home_path="/home/user"
log_path="#{home_path}/radio.log"
default_wav_path="#{home_path}/default.wav"
harbor_port=4444
harbor_password="hackme"
srt_port=9999
srt_password="hackme"
radio_name="My Awesome Web Radio"
radio_desc="Description of my Awesome Web Radio"
radio_genre="electronic"
radio_host="server.com"
radio_url="https://#{radio_host}"
icecast_host="localhost"
icecast_port=8887
icecast_password="hackme"
settings.harbor.bind_addrs.set(["0.0.0.0"])
log.file.path.set(log_path)
log.level.set(4)
# configure security input
security = single(
id="security_single",
default_wav_path
)
# (optional) configure live harbor input
raw_harbor_live=input.harbor(
id="raw_harbor_live",
port=harbor_port,
#transport=harbor_transport,
password=harbor_password,
buffer=2.0,
replay_metadata=true,
metadata_charset="UTF-8",
icy=true,
icy_metadata_charset="UTF-8",
"/live"
)
# process live harbor input
live_harbor=blank.strip(
id="live_harbor_blank_stripper",
stereo(
id="live_harbor_stereo",
raw_harbor_live
)
)
# configure SRT input
raw_srt_input=input.srt(
content_type="application/ffmpeg;format=s16le,channels=2,sample_rate=48000",
id="input_srt_master_stream",
passphrase=srt_password,
enforced_encryption=true,
port=srt_port
)
# process SRT input
input_srt=blank.strip(
id="input_srt_blank_stripper",
stereo(
id="input_srt_stereo",
raw_srt_input
)
)
# main source switcher
source_switcher=fallback(
id="source_switcher",
track_sensitive=false,
[
# (optional) comment "live_harbor," line if you don't need live override
live_harbor,
input_srt,
security
]
)
# function that makes a safe radio
def saferadio(radio, n) =
mksafe(
id="radio_mksafe_#{n}",
radio
)
end
radio_ogg=saferadio(
source_switcher,
"ogg"
)
radio_opus=saferadio(
source_switcher,
"opus"
)
radio_mp3=saferadio(
source_switcher,
"mp3"
)
radio_flac=saferadio(
source_switcher,
"flac"
)
output.icecast(
id="output_icecast_ogg",
%ffmpeg(
format="ogg",
%audio(
codec="libvorbis",
global_quality="9"
)
),
radio_ogg,
host=icecast_host,
port=icecast_port,
password=icecast_password,
mount="/stream.ogg",
name="#{radio_name} (OGG)",
genre=radio_genre,
description=radio_desc,
url="#{radio_url}/stream.ogg",
send_icy_metadata=true,
encoding="UTF-8",
format="audio/ogg",
start=true
)
output.icecast(
id="output_icecast_opus",
%ffmpeg(
format="opus",
%audio(
codec="libopus",
b="327680",
ar="48000"
)
),
radio_opus,
host=icecast_host,
port=icecast_port,
password=icecast_password,
mount="/stream.opus",
name="#{radio_name} (OPUS)",
genre=radio_genre,
description=radio_desc,
url="#{radio_url}/stream.opus",
send_icy_metadata=true,
encoding="UTF-8",
format="audio/ogg",
start=true
)
output.icecast(
id="output_icecast_mp3",
%ffmpeg(
format="mp3",
%audio(
codec="libmp3lame",
b="320k"
)
),
radio_mp3,
host=icecast_host,
port=icecast_port,
password=icecast_password,
mount="/stream.mp3",
name="#{radio_name} (MP3)",
genre=radio_genre,
description=radio_desc,
url="#{radio_url}/stream.mp3",
send_icy_metadata=true,
encoding="UTF-8",
format="audio/mpeg",
start=true
)
output.icecast(
id="output_icecast_flac",
%ffmpeg(
format="ogg",
%audio(
codec="flac",
ar=48000,
ac=2,
compression_level=8
)
),
radio_flac,
host=icecast_host,
port=icecast_port,
password=icecast_password,
mount="/stream.flac",
name="#{radio_name} (FLAC)",
genre=radio_genre,
description=radio_desc,
url="#{radio_url}/stream.flac",
send_icy_metadata=true,
encoding="UTF-8",
format="audio/ogg",
start=true
)
Of course you need an Icecast instance. Here is my config, stripped of all the comments:
<icecast>
<location>Earth</location>
<admin>admin@server.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>1</burst-on-connect>
<burst-size>65535</burst-size>
</limits>
<authentication>
<source-password>hackme</source-password>
<relay-password>hackme</relay-password>
<admin-user>admin</admin-user>
<admin-password>hackme</admin-password>
</authentication>
<hostname>server.com</hostname>
<listen-socket>
<port>8887</port>
</listen-socket>
<listen-socket>
<port>8888</port>
<ssl>1</ssl>
</listen-socket>
<http-headers>
<header name="Access-Control-Allow-Origin" value="*" />
</http-headers>
<relays-on-demand>1</relays-on-demand>
<mount type="default">
<public>0</public>
</mount>
<mount type="normal">
<mount-name>/live</mount-name>
<public>0</public>
<hidden>1</hidden>
</mount>
<mount type="normal">
<mount-name>/stream.ogg</mount-name>
<public>0</public>
<stream-name>My Awesome Web Radio (OGG)</stream-name>
<stream-description>Description of my Awesome Web Radio</stream-description>
<stream-url>https://server.com/stream.ogg</stream-url>
</mount>
<mount type="normal">
<mount-name>/stream.opus</mount-name>
<public>0</public>
<stream-name>My Awesome Web Radio (OPUS)</stream-name>
<stream-description>Description of my Awesome Web Radio</stream-description>
<stream-url>https://server.com/stream.opus</stream-url>
</mount>
<mount type="normal">
<mount-name>/stream.mp3</mount-name>
<public>0</public>
<stream-name>My Awesome Web Radio (MP3)</stream-name>
<stream-description>Description of my Awesome Web Radio</stream-description>
<stream-url>https://server.com/stream.mp3</stream-url>
</mount>
<mount type="normal">
<mount-name>/stream.flac</mount-name>
<public>0</public>
<stream-name>My Awesome Web Radio (FLAC)</stream-name>
<stream-description>Description of my Awesome Web Radio</stream-description>
<stream-url>https://server.com/stream.flac</stream-url>
</mount>
<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>
<ssl-certificate>/etc/icecast2/bundle.pem</ssl-certificate>
</paths>
<logging>
<accesslog>access.log</accesslog>
<errorlog>error.log</errorlog>
<loglevel>3</loglevel>
<logsize>10000</logsize>
</logging>
<security>
<chroot>0</chroot>
</security>
</icecast>
And if you want to make a reverse proxy on a stream with an Nginx server, to access the streams without ports numbers for example, here is how you do it:
server {
listen 443 ssl http2;
...
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection '';
proxy_http_version 1.1;
proxy_read_timeout 24h;
fastcgi_read_timeout 24h;
chunked_transfer_encoding off;
location /stream.ogg {
proxy_pass http://localhost:8887/stream.ogg;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header Cache-Control no-cache;
proxy_read_timeout 24h;
proxy_pass_request_headers on;
proxy_set_header Access-Control-Allow-Origin *;
proxy_set_header Range bytes=0-;
proxy_buffering off;
tcp_nodelay on;
types { }
default_type audio/ogg;
}
location /stream.opus {
proxy_pass http://localhost:8887/stream.opus;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header Cache-Control no-cache;
proxy_read_timeout 24h;
proxy_pass_request_headers on;
proxy_set_header Access-Control-Allow-Origin *;
proxy_set_header Range bytes=0-;
proxy_buffering off;
tcp_nodelay on;
types { }
default_type audio/ogg;
}
location /stream.mp3 {
proxy_pass http://localhost:8887/stream.mp3;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header Cache-Control no-cache;
proxy_read_timeout 24h;
proxy_pass_request_headers on;
proxy_set_header Access-Control-Allow-Origin *;
proxy_set_header Range bytes=0-;
proxy_buffering off;
tcp_nodelay on;
types { }
default_type audio/mpeg;
}
location /stream.flac {
proxy_pass http://localhost:8887/stream.flac;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header Cache-Control no-cache;
proxy_read_timeout 24h;
proxy_pass_request_headers on;
proxy_set_header Access-Control-Allow-Origin *;
proxy_set_header Range bytes=0-;
proxy_buffering off;
tcp_nodelay on;
types { }
default_type audio/ogg;
}
...
}
Once you got all that set up and running, there are a few things that you need to consider:
- You should run the Liquidsoap scripts in something like Screen or Tmux
- You should find a way to restart the scripts automatically when the server restarts
Usually I proceed like this on a Linux server:
Root can run a script when the server starts. For example, place this in /root/.bash_profile:
/root/start_tmux_sessions.sh
Content of /root/start_tmux_sessions.sh:
#!/usr/bin/env bash
su -c "~/start_tmux.sh" - user
Content of /home/user/start_tmux.sh:
#!/usr/bin/env bash
if tmux has-session -t liquidsoap > /dev/null 2>&1; then
echo "tmux session 'liquidsoap' already exists"
else
echo "Starting tmux session 'liquidsoap'..."
tmux new-session -d -s liquidsoap "cd /home/user/radio/ && /home/user/radio/startliquidsoap.sh"
tmux new-window -d -t liquidsoap
fi
Content of /home/user/radio/startliquidsoap.sh:
#!/usr/bin/env bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
cd $SCRIPT_DIR
liquidsoap ./radio.liq
Hopefully you can adapt all this to your needs, I hope that all this will be useful to someone!