RadioPi
RadioPi is the web radio of the ECP (Ecole Centrale de Paris). RadioPi runs many channels. There are topical channels (Reggae, Hip-Hop, Jazz, ...) and a so-called Web channel which switches from one to another of the topical channels. On top of that, they periodically broadcast live shows, which are relayed on all channels.
We met a RadioPi manager right after having released Liquidsoap 0.2.0, and he was seduced by the system. They needed quite complex features, which they were at that time fulfilling using dirty tricks, loads of obfuscated scripts. Using Liquidsoap now allow them to do all they want in an integrated way, but also provided new features.
The migration process
Quite easy actually. They used to have many instances Ices2, each of these calling a Perl script to get the next song. The Web channel was an Icecast playback of one of the previous Ices topical streams. Other scripts were used for switching channels to live shows, switching the Web channel relay from one to another topical stream, turning on or off all these channels, etc.
Now they have this single Liquidsoap script, no more. It calls external scripts to interact with their web-based song scheduling system. And they won new features: blank detection and distributed encoding.
The first machine gets its files from a ftp server opened on the second machine. Liquidsoap handles download automatically.
Each file is given by an external script, radiopilote-getnext
,
whose answer looks as follows (except that it's on a single line):
annotate:file_id="3541",length="400.613877551",\ type="chansons",display_title="John Holt - Holigan",\ display_artist="RadioPi - Canal reggae",\ display_album="Studio One SeleKta! - Album Studio 1 12",\ canal="reggae":ftp://***:***@host/files/3541.mp3
Note that we use annotate to pass some variables to liquidsoap...
On top of that, we build a common channel, that plays each channel using a special schedule.
#!/usr/bin/liquidsoap # === Settings === set("log.file.path","/var/log/liquidsoap/pi.log") set("init.daemon",true) set("init.daemon.pidfile.path","/var/run/liquidsoap/pi.pid") set("server.telnet",true) set("harbor.bind_addr","0.0.0.0") set("harbor.port",8000) set("harbor.password","xxxxxx") set("log.level",4) set("scheduler.event_queues",4) set("scheduler.log",false) scripts = "/path/to/scripts/" pass = "xxxxxx" ice_host = "host" descr = "Radio Piston" url = "http://radiopi.org" # === Live relays === def live_start() = log("got live source, starting relays..") ignore(execute("stream_relay.start")) ignore(execute("archives.start")) end def live_stop() = log("live source has gone, stoping relays..") ignore(execute("stream_relay.stop")) ignore(execute("archives.stop")) end # Live source through harbor live = input.harbor(id="live",on_connect=live_start,on_disconnect=live_stop,"live.ogg") live_safe = mksafe(live) # Live relay to secondary encoder output.icecast.vorbis(id="stream_relay",start=false,restart=true,host="1.2.3.4",port=8005,password="xxxx",mount="live.ogg",live_safe) # File source for archiving title = '$(if $(title),"$(title)","Emission inconnue")$(if $(artist), " par $(artist)") - %m-%d-%Y, %H:%M:%S' output.file.vorbis(id="archives",start=false,reopen_on_metadata=true,"/path/to/archives/" ^ title ^ ".ogg",live_safe) # === Main script === # Specialize the output functions by partial application output.icecast = output.icecast.mp3(restart=true,description=descr, url=url) out = output.icecast(host=ice_host,port=8080,password=pass) # A file for playing during failures interlude = single("/path/to/failure.mp3") def fallback.transition(previous,next) add([fade.in(next),fade.final(duration=5.,previous)]) end # === Channels === # Lastfm submission def lastfm (m) = # Only submit songs of type "chansons" (got type using annotate..) if (m["type"] == "chansons") then canal = m["canal"] user = "radiopi-" ^ canal lastfm.submit(user=user,password="xxxxxx",m) end end # To create a channel from a basic source, add: # - a new-track notification for radiopilote # - metadata rewriting # - the live shows # - the failsafe 'interlude' source to channels # - blank detection def mklive(source) # Add lastfm submission source = on_metadata(lastfm,source) # Feedback the system source = on_metadata(fun (meta) -> system(scripts ^ "radiopilote-feedback " ^quote(meta["canal"])^" " ^quote(meta["file_id"])), source) # Rewrite metadata to add channel name and mode.. # Got data from annotate.. rewrite_metadata( [("artist",'$(if $(display_artist),"$(display_artist)","$(artist)")'), ("comment",""), ("title", '$(if $(display_title),"$(display_title)","$(title)")'), ("album", '$(if $(display_album),"$(display_album)","$(album)")')], # Default fallback fallback(track_sensitive=false,[ # Strip blank to avoid live streaming only blank.. strip_blank(live, length=10., threshold=-50.), source, interlude ]) ) end # === Basic sources === # Create a radiopilote-driven source def channel_radiopilote(~skip=true,name) log("Creating canal #{name}") # Request function def request () = log("Request for #{name}") request.create(audio=true, get_process_output(scripts ^ "radiopilote-getnext " ^ quote(name))) end # Basic source source = request.dynamic( id="dyn_"^name,request) # Add smart crossfading source = smart_crossfade(source) # Only skip some channels if skip then skip_blank(source, length=10., threshold=-40.) else source end end # Channels encoded here reggae = channel_radiopilote("reggae") jazz = channel_radiopilote("jazz") discoqueen = channel_radiopilote("discoqueen") # Avoid skiping blank with classic music !! classique = channel_radiopilote(skip=false,"classique") That70Sound = channel_radiopilote("That70Sound") # Create a channel using mklive(), encode and output it to icecast. def mkoutput(mount,source,name,genre) out(mount=mount,name=name,genre=genre, mklive(source)) end # === Outputs === # These channels are encoded on this machine reggae = mkoutput("reggae", reggae, "RadioPi - Canal Reggae","reggae") jazz = mkoutput("jazz", jazz, "RadioPi - Canal Jazz","jazz") discoqueen = mkoutput("discoqueen", discoqueen, "RadioPi - Canal DiscoQueen","discoqueen") classique = mkoutput("classique", classique, "RadioPi - Canal Classique","classique") That70Sound = mkoutput("That70Sound", That70Sound, "RadioPi - Canal That70Sound","That70Sound") # Channels from the other encoder: def get_channel(mount) input.http(autostart=true,id="input_"^mount,"http://host:8080/"^mount) end metal = get_channel("metal") electro = get_channel("electro") hiphop = get_channel("hiphop") # Finally the web channel, and its mp3 version web = out(mount="radioPi",name="RadioPi - www.radiopi.org", fallback([ switch(track_sensitive=false, [ ( { 6h-12h }, reggae ), ( { 12h-14h }, discoqueen ), ( { 14h-16h }, electro ), ( { 16h-19h }, reggae ), ( { 19h-20h }, That70Sound ), ( { 20h-22h }, metal ), ( { 22h-0h }, reggae ), ( { 0h-6h }, jazz ), ]), interlude ]))
The other machine has a similar configuration exept that files are local, but this is exactly the same for liquidsoap !
Using harbor, the live connects directly to liquidsoap, using port 8000
(icecast runs on port 8080
). Then, liquidsoap starts a relay to the other encoder, and both switch their channels to the new live.
Additionally, a file output is started upon live connection, in order to backup the stream. You could also add a relay to icecast in order to manually check what's received bythe harbor.