diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php index f95280063..e695c404c 100644 --- a/airtime_mvc/application/models/Preference.php +++ b/airtime_mvc/application/models/Preference.php @@ -1116,7 +1116,6 @@ class Application_Model_Preference } else { /*For now we just have this hack for debugging. We should not rely on this crappy behaviour in case of failure*/ - Logging::info("Pref: $pref_param"); Logging::warn("Index $x does not exist preferences"); Logging::warn("Defaulting to identity and printing preferences"); Logging::warn($ds); diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index fa2a6c79b..f46bfedde 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -321,7 +321,7 @@ SQL; ws.description AS file_album_title, ws.length AS file_length, 't'::BOOL AS file_exists, - NULL as file_mime + ws.mime as file_mime SQL; $streamJoin = <<> /etc/apt/sources.list diff --git a/python_apps/pypo/liquidsoap_scripts/ls_lib.liq b/python_apps/pypo/liquidsoap_scripts/ls_lib.liq index 5c5919e2e..1a18fd00a 100644 --- a/python_apps/pypo/liquidsoap_scripts/ls_lib.liq +++ b/python_apps/pypo/liquidsoap_scripts/ls_lib.liq @@ -254,29 +254,15 @@ def output_to(output_type, type, bitrate, host, port, pass, mount_point, url, de if user == "" then user_ref := "source" end - - description_ref = ref description - if description == "" then - description_ref := "N/A" - end - - genre_ref = ref genre - if genre == "" then - genre_ref := "N/A" - end - - url_ref = ref url - if url == "" then - url_ref := "N/A" - end + output.shoutcast_mono = output.shoutcast(id = "shoutcast_stream_#{stream}", host = host, port = port, password = pass, fallible = true, - url = !url_ref, - genre = !genre_ref, - name = !description_ref, + url = url, + genre = genre, + name = description, user = !user_ref, on_error = on_error, on_connect = on_connect) @@ -286,9 +272,9 @@ def output_to(output_type, type, bitrate, host, port, pass, mount_point, url, de port = port, password = pass, fallible = true, - url = !url_ref, - genre = !genre_ref, - name = !description_ref, + url = url, + genre = genre, + name = description, user = !user_ref, on_error = on_error, on_connect = on_connect) @@ -390,13 +376,6 @@ def add_skip_command(s) "skip",fun(s) -> begin log("source.skip") skip(s) end) end -dyn_out = output.icecast(%wav, - host="localhost", - port=8999, - password=stream_harbor_pass, - mount="test-harbor", - fallible=true) - def set_dynamic_source_id(id) = current_dyn_id := id string_of(!current_dyn_id) @@ -406,123 +385,159 @@ def get_dynamic_source_id() = string_of(!current_dyn_id) end +#cc-4633 -# Function to create a playlist source and output it. -def create_dynamic_source(uri) = - # The playlist source - s = audio_to_stereo(input.http(buffer=2., max=12., uri)) - # The output - active_dyn_out = dyn_out(s) +# NOTE +# A few values are hardcoded and may be dependent: +# - the delay in gracetime is linked with the buffer duration of input.http +# (delay should be a bit less than buffer) +# - crossing duration should be less than buffer length +# (at best, a higher duration will be ineffective) - # We register both source and output - # in the list of sources - dyn_sources := - list.append([(!current_dyn_id, s),(!current_dyn_id, active_dyn_out)], !dyn_sources) +# HTTP input with "restart" command that waits for "stop" to be effected +# before "start" command is issued. Optionally it takes a new URL to play, +# which makes it a convenient replacement for "url". +# In the future, this may become a core feature of the HTTP input. +# TODO If we stop and restart quickly several times in a row, +# the data bursts accumulate and create buffer overflow. +# Flushing the buffer on restart could be a good idea, but +# it would also create an interruptions while the buffer is +# refilling... on the other hand, this would avoid having to +# fade using both cross() and switch(). +def input.http_restart(~id,~initial_url="http://dummy/url") + + source = input.http(buffer=5.,max=15.,id=id,autostart=false,initial_url) + + def stopped() + "stopped" == list.hd(server.execute("#{id}.status")) + end + + server.register(namespace=id, + "restart", + usage="restart [url]", + fun (url) -> begin + if url != "" then + log(string_of(server.execute("#{id}.url #{url}"))) + end + log(string_of(server.execute("#{id}.stop"))) + add_timeout(0.5, + { if stopped() then + log(string_of(server.execute("#{id}.start"))) ; + (-1.) + else 0.5 end}) + "OK" + end) + + # Dummy output should be useless if HTTP stream is meant + # to be listened to immediately. Otherwise, apply it. + # + # output.dummy(fallible=true,source) + + source - notify([("schedule_table_id", !current_dyn_id)]) - "Done!" end +# Transitions between URL changes in HTTP streams. +def cross_http(~debug=true,~http_input_id,source) -# A function to destroy a dynamic source -def destroy_dynamic_source(id) = - # We need to find the source in the list, - # remove it and destroy it. Currently, the language - # lacks some nice operators for that so we do it - # the functional way + id = http_input_id + last_url = ref "" + change = ref false - # This function is executed on every item in the list - # of dynamic sources - def parse_list(ret, current_element) = - # ret is of the form: (matching_sources, remaining_sources) - # We extract those two: - matching_sources = fst(ret) - remaining_sources = snd(ret) - - # current_element is of the form: ("uri", source) so - # we check the first element - current_id = fst(current_element) - if current_id == id then - # In this case, we add the source to the list of - # matched sources - (list.append( [snd(current_element)], - matching_sources), - remaining_sources) - else - # In this case, we put the element in the list of remaining - # sources - (matching_sources, - list.append([current_element], - remaining_sources)) + def on_m(m) + notify_stream(m) + changed = m["source_url"] != !last_url + log("URL now #{m['source_url']} (change: #{changed})") + if changed then + if !last_url != "" then change := true end + last_url := m["source_url"] end end - - # Now we execute the function: - result = list.fold(parse_list, ([], []), !dyn_sources) - matching_sources = fst(result) - remaining_sources = snd(result) - # We store the remaining sources in dyn_sources - dyn_sources := remaining_sources + # We use both metadata and status to know about the current URL. + # Using only metadata may be more precise is crazy corner cases, + # but it's also asking too much: the metadata may not pass through + # before the crosser is instantiated. + # Using only status in crosser misses some info, eg. on first URL. + source = on_metadata(on_m,source) - # If no source matched, we return an error - if list.length(matching_sources) == 0 then - "Error: no matching sources!" - else - # We stop all sources - list.iter(source.shutdown, matching_sources) - # And return - "Done!" + cross_d = 3. + + def crosser(a,b) + url = list.hd(server.execute('#{id}.url')) + status = list.hd(server.execute('#{id}.status')) + on_m([("source_url",url)]) + if debug then + log("New track inside HTTP stream") + log(" status: #{status}") + log(" need to cross: #{!change}") + log(" remaining #{source.remaining(a)} sec before, \ + #{source.remaining(b)} sec after") + end + if !change then + change := false + # In principle one should avoid crossing on a live stream + # it'd be okay to do it here (eg. use add instead of sequence) + # because it's only once per URL, but be cautious. + sequence([fade.out(duration=cross_d,a),fade.in(b)]) + else + # This is done on tracks inside a single stream. + # Do NOT cross here or you'll gradually empty the buffer! + sequence([a,b]) + end end + + # Setting conservative=true would mess with the delayed switch below + cross(duration=cross_d,conservative=false,crosser,source) + end +# Custom fallback between http and default source with fading of +# beginning and end of HTTP stream. +# It does not take potential URL changes into account, as long as +# they do not interrupt streaming (thanks to the HTTP buffer). +def http_fallback(~http_input_id,~http,~default) + id = http_input_id + # We use a custom switching predicate to trigger switching (and thus, + # transitions) before the end of a track (rather, end of HTTP stream). + # It is complexified because we don't want to trigger switching when + # HTTP disconnects for just an instant, when changing URL: for that + # we use gracetime below. -# A function to destroy a dynamic source -def destroy_dynamic_source_all() = - # We need to find the source in the list, - # remove it and destroy it. Currently, the language - # lacks some nice operators for that so we do it - # the functional way - - # This function is executed on every item in the list - # of dynamic sources - def parse_list(ret, current_element) = - # ret is of the form: (matching_sources, remaining_sources) - # We extract those two: - matching_sources = fst(ret) - remaining_sources = snd(ret) - - # current_element is of the form: ("uri", source) so - # we check the first element - current_uri = fst(current_element) - # in this case, we add the source to the list of - # matched sources - (list.append( [snd(current_element)], - matching_sources), - remaining_sources) + def gracetime(~delay=3.,f) + last_true = ref 0. + { if f() then + last_true := gettimeofday() + true + else + gettimeofday() < !last_true+delay + end } end - - # now we execute the function: - result = list.fold(parse_list, ([], []), !dyn_sources) - matching_sources = fst(result) - remaining_sources = snd(result) - # we store the remaining sources in dyn_sources - dyn_sources := remaining_sources - - # if no source matched, we return an error - if list.length(matching_sources) == 0 then - "error: no matching sources!" - else - # we stop all sources - list.iter(source.shutdown, matching_sources) - # And return - "Done!" + def connected() + status = list.hd(server.execute("#{id}.status")) + not(list.mem(status,["polling","stopped"])) end + connected = gracetime(connected) + + def to_live(a,b) = + log("TRANSITION to live") + add(normalize=false, + [fade.initial(b),fade.final(a)]) + end + def to_static(a,b) = + log("TRANSITION to static") + sequence([fade.out(a),fade.initial(b)]) + end + + switch( + track_sensitive=false, + transitions=[to_live,to_static], + [(# make sure it is connected, and not buffering + {connected() and source.is_ready(http) and !webstream_enabled}, http), + ({true},default)]) + end - - - diff --git a/python_apps/pypo/liquidsoap_scripts/ls_script.liq b/python_apps/pypo/liquidsoap_scripts/ls_script.liq index 34e56786e..962b20b2c 100644 --- a/python_apps/pypo/liquidsoap_scripts/ls_script.liq +++ b/python_apps/pypo/liquidsoap_scripts/ls_script.liq @@ -7,11 +7,11 @@ set("server.telnet", true) set("server.telnet.port", 1234) #Dynamic source list -dyn_sources = ref [] +#dyn_sources = ref [] webstream_enabled = ref false time = ref string_of(gettimeofday()) -queue = audio_to_stereo(id="queue_src", request.equeue(id="queue", length=0.5)) +queue = audio_to_stereo(id="queue_src", mksafe(request.equeue(id="queue", length=0.5))) queue = cue_cut(queue) queue = amplify(1., override="replay_gain", queue) @@ -35,15 +35,19 @@ s2_namespace = ref '' s3_namespace = ref '' just_switched = ref false -stream_harbor_pass = list.hd(get_process_lines('pwgen -s -N 1 -n 20')) +#stream_harbor_pass = list.hd(get_process_lines('pwgen -s -N 1 -n 20')) %include "ls_lib.liq" -web_stream = input.harbor("test-harbor", port=8999, password=stream_harbor_pass) -web_stream = on_metadata(notify_stream, web_stream) -output.dummy(fallible=true, web_stream) +#web_stream = input.harbor("test-harbor", port=8999, password=stream_harbor_pass) +#web_stream = on_metadata(notify_stream, web_stream) +#output.dummy(fallible=true, web_stream) +http = input.http_restart(id="http") +http = cross_http(http_input_id="http",http) +stream_queue = http_fallback(http_input_id="http",http=http,default=queue) + # the crossfade function controls fade in/out queue = crossfade_airtime(queue) queue = on_metadata(notify, queue) @@ -51,10 +55,10 @@ queue = map_metadata(update=false, append_title, queue) output.dummy(fallible=true, queue) -stream_queue = switch(id="stream_queue_switch", track_sensitive=false, - transitions=[transition, transition], - [({!webstream_enabled},web_stream), - ({true}, queue)]) +#stream_queue = switch(id="stream_queue_switch", track_sensitive=false, +# transitions=[transition, transition], +# [({!webstream_enabled},web_stream), +# ({true}, queue)]) ignore(output.dummy(stream_queue, fallible=true)) @@ -92,32 +96,33 @@ server.register(namespace="dynamic_source", fun (s) -> begin log("dynamic_source.output_stop") webstream_enabled := false "disabled" end) server.register(namespace="dynamic_source", - description="Set the cc_schedule row id", + description="Set the streams cc_schedule row id", usage="id ", "id", fun (s) -> begin log("dynamic_source.id") set_dynamic_source_id(s) end) server.register(namespace="dynamic_source", - description="Get the cc_schedule row id", + description="Get the streams cc_schedule row id", usage="get_id", "get_id", fun (s) -> begin log("dynamic_source.get_id") get_dynamic_source_id() end) -server.register(namespace="dynamic_source", - description="Start a new dynamic source.", - usage="start ", - "read_start", - fun (uri) -> begin log("dynamic_source.read_start") create_dynamic_source(uri) end) -server.register(namespace="dynamic_source", - description="Stop a dynamic source.", - usage="stop ", - "read_stop", - fun (s) -> begin log("dynamic_source.read_stop") destroy_dynamic_source(s) end) -server.register(namespace="dynamic_source", - description="Stop a dynamic source.", - usage="stop ", - "read_stop_all", - fun (s) -> begin log("dynamic_source.read_stop") destroy_dynamic_source_all() end) +#server.register(namespace="dynamic_source", +# description="Start a new dynamic source.", +# usage="start ", +# "read_start", +# fun (uri) -> begin log("dynamic_source.read_start") begin_stream_read(uri) end) +#server.register(namespace="dynamic_source", +# description="Stop a dynamic source.", +# usage="stop ", +# "read_stop", +# fun (s) -> begin log("dynamic_source.read_stop") stop_stream_read(s) end) + +#server.register(namespace="dynamic_source", +# description="Stop a dynamic source.", +# usage="stop ", +# "read_stop_all", +# fun (s) -> begin log("dynamic_source.read_stop") destroy_dynamic_source_all() end) default = amplify(id="silence_src", 0.00001, noise()) default = rewrite_metadata([("artist","Airtime"), ("title", "offline")], default) diff --git a/python_apps/pypo/pypopush.py b/python_apps/pypo/pypopush.py index 0382b5e06..c0bb36ff9 100644 --- a/python_apps/pypo/pypopush.py +++ b/python_apps/pypo/pypopush.py @@ -478,8 +478,8 @@ class PypoPush(Thread): self.logger.debug(msg) tn.write(msg) - #example: dynamic_source.read_start http://87.230.101.24:80/top100station.mp3 - msg = 'dynamic_source.read_start %s\n' % media_item['uri'].encode('latin-1') + #msg = 'dynamic_source.read_start %s\n' % media_item['uri'].encode('latin-1') + msg = 'http.restart %s\n' % media_item['uri'].encode('latin-1') self.logger.debug(msg) tn.write(msg) @@ -520,7 +520,8 @@ class PypoPush(Thread): self.telnet_lock.acquire() tn = telnetlib.Telnet(LS_HOST, LS_PORT) - msg = 'dynamic_source.read_stop_all xxx\n' + #msg = 'dynamic_source.read_stop_all xxx\n' + msg = 'http.stop\n' self.logger.debug(msg) tn.write(msg) @@ -546,7 +547,8 @@ class PypoPush(Thread): tn = telnetlib.Telnet(LS_HOST, LS_PORT) #dynamic_source.stop http://87.230.101.24:80/top100station.mp3 - msg = 'dynamic_source.read_stop %s\n' % media_item['row_id'] + #msg = 'dynamic_source.read_stop %s\n' % media_item['row_id'] + msg = 'http.stop\n' self.logger.debug(msg) tn.write(msg) diff --git a/utils/airtime-test-stream.py b/utils/airtime-test-stream.py index b7d887c0d..a2ab06982 100644 --- a/utils/airtime-test-stream.py +++ b/utils/airtime-test-stream.py @@ -27,14 +27,14 @@ def printUsage(): print " -m mount (default: test) " print " -h show help menu" - + def find_liquidsoap_binary(): """ Starting with Airtime 2.0, we don't know the exact location of the Liquidsoap binary because it may have been installed through a debian package. Let's find the location of this binary. """ - + rv = subprocess.call("which airtime-liquidsoap > /dev/null", shell=True) if rv == 0: return "airtime-liquidsoap" @@ -78,7 +78,7 @@ for o, a in optlist: mount = a try: - + print "Protocol: %s " % stream_type print "Host: %s" % host print "Port: %s" % port @@ -86,35 +86,35 @@ try: print "Password: %s" % password if stream_type == "icecast": print "Mount: %s\n" % mount - + url = "http://%s:%s/%s" % (host, port, mount) print "Outputting to %s streaming server. You should be able to hear a monotonous tone on '%s'. Press ctrl-c to quit." % (stream_type, url) - + liquidsoap_exe = find_liquidsoap_binary() - + if liquidsoap_exe is None: raise Exception("Liquidsoap not found!") - + if stream_type == "icecast": command = "%s 'output.icecast(%%vorbis, host = \"%s\", port = %s, user= \"%s\", password = \"%s\", mount=\"%s\", sine())'" % (liquidsoap_exe, host, port, user, password, mount) else: command = "%s /usr/lib/airtime/pypo/bin/liquidsoap_scripts/library/pervasives.liq 'output.shoutcast(%%mp3, host=\"%s\", port = %s, user= \"%s\", password = \"%s\", sine())'" \ % (liquidsoap_exe, host, port, user, password) - + if not verbose: command += " 2>/dev/null | grep \"failed\"" else: print command - + #print command rv = subprocess.call(command, shell=True) - + #if we reach this point, it means that our subprocess exited without the user - #doing a keyboard interrupt. This means there was a problem outputting to the + #doing a keyboard interrupt. This means there was a problem outputting to the #stream server. Print appropriate message. print "There was an error with your stream configuration. Please review your configuration " + \ "and run this program again. Use the -h option for help" - + except KeyboardInterrupt, ki: print "\nExiting" except Exception, e: