Merge pull request #131 from radiorabe/feature/remove-liquidsoap-lib-from-libretime
Do not bundle liquidsoap library
This commit is contained in:
commit
f32de6a024
|
@ -1,268 +0,0 @@
|
|||
# Decoders, enabled when the binary is detected and the os is not Win32.
|
||||
|
||||
%ifdef add_decoder
|
||||
# Enable external Musepack decoder. Requires the
|
||||
# mpcdec binary in the path. Does not work on
|
||||
# Win32.
|
||||
# @category Liquidsoap
|
||||
def enable_external_mpc_decoder() =
|
||||
# A list of know extensions and content-type for Musepack.
|
||||
# Values from http://en.wikipedia.org/wiki/Musepack
|
||||
mpc_mimes = [ "audio/x-musepack", "audio/musepack" ]
|
||||
mpc_filexts = [ "mpc", "mp+", "mpp" ]
|
||||
|
||||
def test_mpc(file) =
|
||||
def get_channels(file) =
|
||||
int_of_string(
|
||||
list.hd(
|
||||
get_process_lines("mpcdec -i #{quote(file)} 2>&1 \
|
||||
| grep channels | cut -d' ' -f 2")))
|
||||
end
|
||||
# Get the file's mime
|
||||
mime = get_mime(file)
|
||||
# Test mime
|
||||
if list.mem(mime,mpc_mimes) then
|
||||
get_channels(file)
|
||||
else
|
||||
# Otherwise test file extension
|
||||
ret = string.extract(pattern='\.(.+)$',file)
|
||||
if list.length(ret) != 0 then
|
||||
ext = ret["1"]
|
||||
if list.mem(ext,mpc_filexts) then
|
||||
get_channels(file)
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
get_channels(file)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if test_process("which mpcdec") then
|
||||
log(level=3,"Found mpcdec binary: enabling musepack external decoder.")
|
||||
mpcdec_p = fun(f) -> "mpcdec #{quote(f)} - 2>/dev/null"
|
||||
add_oblivious_decoder(name="MPCDEC",description="Decode files using the mpcdec \
|
||||
musepack decoder binary",test=test_mpc,mpcdec_p)
|
||||
else
|
||||
log(level=3,"Did not find mpcdec binary: musepack decoder disabled.")
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Enable external FLAC decoders. Requires flac binary
|
||||
# in the path for audio decoding and metaflac binary for
|
||||
# metadata. Does not work on Win32. Default: disabled.
|
||||
# Please note that built-in support for FLAC is available
|
||||
# in liquidsoap if compiled and should be preferred over
|
||||
# the external decoder.
|
||||
# @category Liquidsoap
|
||||
def enable_external_flac_decoder() =
|
||||
if test_process("which flac") then
|
||||
log(level=3,"Found flac binary: enabling flac external decoder.")
|
||||
flac_p = "flac -d -c - 2>/dev/null"
|
||||
def test_flac(file) =
|
||||
if test_process("which metaflac") then
|
||||
channels = list.hd(get_process_lines("metaflac \
|
||||
--show-channels #{quote(file)} \
|
||||
2>/dev/null"))
|
||||
# If the value is not an int, this returns 0 and we are ok :)
|
||||
int_of_string(channels)
|
||||
else
|
||||
if string.match(pattern="flac",file) then
|
||||
# We do not know the number of audio channels
|
||||
# so setting to -1
|
||||
(-1)
|
||||
else
|
||||
# All tests failed: no audio decodable using flac..
|
||||
0
|
||||
end
|
||||
end
|
||||
end
|
||||
add_decoder(name="EXTERNAL_FLAC",description="Decode files using the flac \
|
||||
decoder binary.", test=test_flac,flac_p)
|
||||
else
|
||||
log(level=3,"Did not find flac binary: flac decoder disabled.")
|
||||
end
|
||||
|
||||
if test_process("which metaflac") then
|
||||
log(level=3,"Found metaflac binary: enabling flac external metadata \
|
||||
resolver.")
|
||||
def flac_meta(file)
|
||||
ret = get_process_lines("metaflac --export-tags-to=- \
|
||||
#{quote(file)} 2>/dev/null")
|
||||
ret = list.map(string.split(separator="="),ret)
|
||||
# Could be made better..
|
||||
def f(l',l)=
|
||||
if list.length(l) >= 2 then
|
||||
list.append([(list.hd(l),list.nth(l,1))],l')
|
||||
else
|
||||
if list.length(l) >= 1 then
|
||||
list.append([(list.hd(l),"")],l')
|
||||
else
|
||||
l'
|
||||
end
|
||||
end
|
||||
end
|
||||
list.fold(f,[],ret)
|
||||
end
|
||||
add_metadata_resolver("EXTERNAL_FLAC",flac_meta)
|
||||
else
|
||||
log(level=3,"Did not find metaflac binary: flac metadata resolver disabled.")
|
||||
end
|
||||
end
|
||||
%endif
|
||||
|
||||
%ifdef add_oblivious_decoder
|
||||
# Enable or disable external FAAD (AAC/AAC+/M4A) decoders.
|
||||
# Requires faad binary in the path for audio decoding and
|
||||
# metaflac binary for metadata. Does not work on Win32.
|
||||
# Please note that built-in support for faad is available
|
||||
# in liquidsoap if compiled and should be preferred over
|
||||
# the external decoder.
|
||||
# @category Liquidsoap
|
||||
def enable_external_faad_decoder() =
|
||||
|
||||
# A list of know extensions and content-type for AAC.
|
||||
# Values from http://en.wikipedia.org/wiki/Advanced_Audio_Coding
|
||||
# TODO: can we register a setting for that ??
|
||||
aac_mimes =
|
||||
["audio/aac", "audio/aacp", "audio/3gpp", "audio/3gpp2", "audio/mp4",
|
||||
"audio/MP4A-LATM", "audio/mpeg4-generic", "audio/x-hx-aac-adts"]
|
||||
aac_filexts = ["m4a", "m4b", "m4p", "m4v",
|
||||
"m4r", "3gp", "mp4", "aac"]
|
||||
|
||||
# Faad is not very selective so
|
||||
# We are checking only file that
|
||||
# end with a known extension or mime type
|
||||
def faad_test(file) =
|
||||
# Get the file's mime
|
||||
mime = get_mime(file)
|
||||
# Test mime
|
||||
if list.mem(mime,aac_mimes) then
|
||||
true
|
||||
else
|
||||
# Otherwise test file extension
|
||||
ret = string.extract(pattern='\.(.+)$',file)
|
||||
if list.length(ret) != 0 then
|
||||
ext = ret["1"]
|
||||
list.mem(ext,aac_filexts)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if test_process("which faad") then
|
||||
log(level=3,"Found faad binary: enabling external faad decoder and \
|
||||
metadata resolver.")
|
||||
faad_p = (fun (f) -> "faad -w #{quote(f)} 2>/dev/null")
|
||||
def test_faad(file) =
|
||||
if faad_test(file) then
|
||||
channels = list.hd(get_process_lines("faad -i #{quote(file)} 2>&1 | \
|
||||
grep 'ch,'"))
|
||||
ret = string.extract(pattern=", (\d) ch,",channels)
|
||||
ret =
|
||||
if list.length(ret) == 0 then
|
||||
# If we pass the faad_test, chances are
|
||||
# high that the file will contain aac audio data..
|
||||
"-1"
|
||||
else
|
||||
ret["1"]
|
||||
end
|
||||
int_of_string(default=(-1),ret)
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
add_oblivious_decoder(name="EXTERNAL_FAAD",description="Decode files using \
|
||||
the faad binary.", test=test_faad, faad_p)
|
||||
def faad_meta(file) =
|
||||
if faad_test(file) then
|
||||
ret = get_process_lines("faad -i \
|
||||
#{quote(file)} 2>&1")
|
||||
# Yea, this is ugly programming (again) !
|
||||
def get_meta(l,s)=
|
||||
ret = string.extract(pattern="^(\w+):\s(.+)$",s)
|
||||
if list.length(ret) > 0 then
|
||||
list.append([(ret["1"],ret["2"])],l)
|
||||
else
|
||||
l
|
||||
end
|
||||
end
|
||||
list.fold(get_meta,[],ret)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
add_metadata_resolver("EXTERNAL_FAAD",faad_meta)
|
||||
else
|
||||
log(level=3,"Did not find faad binary: faad decoder disabled.")
|
||||
end
|
||||
end
|
||||
%endif
|
||||
|
||||
# Standard function for displaying metadata.
|
||||
# Shows artist and title, using "Unknown" when a field is empty.
|
||||
# @param m Metadata packet to be displayed.
|
||||
# @category String
|
||||
def string_of_metadata(m)
|
||||
artist = m["artist"]
|
||||
title = m["title"]
|
||||
artist = if ""==artist then "Unknown" else artist end
|
||||
title = if ""==title then "Unknown" else title end
|
||||
"#{artist} -- #{title}"
|
||||
end
|
||||
|
||||
# Use X On Screen Display to display metadata info.
|
||||
# @param ~color Color of the text.
|
||||
# @param ~position Position of the text (top|middle|bottom).
|
||||
# @param ~font Font used (xfontsel is your friend...)
|
||||
# @param ~display Function used to display a metadata packet.
|
||||
# @category Source / Track Processing
|
||||
def osd_metadata(~color="green",~position="top",
|
||||
~font="-*-courier-*-r-*-*-*-240-*-*-*-*-*-*",
|
||||
~display=string_of_metadata,
|
||||
s)
|
||||
osd = 'osd_cat -p #{position} --font #{quote(font)}'
|
||||
^ ' --color #{color}'
|
||||
def feedback(m)
|
||||
system("echo #{quote(display(m))} | #{osd} &")
|
||||
end
|
||||
on_metadata(feedback,s)
|
||||
end
|
||||
|
||||
# Use notify to display metadata info.
|
||||
# @param ~urgency Urgency (low|normal|critical).
|
||||
# @param ~icon Icon filename or stock icon to display.
|
||||
# @param ~time Timeout in milliseconds.
|
||||
# @param ~display Function used to display a metadata packet.
|
||||
# @param ~title Title of the notification message.
|
||||
# @category Source / Track Processing
|
||||
def notify_metadata(~urgency="low",~icon="stock_smiley-22",~time=3000,
|
||||
~display=string_of_metadata,
|
||||
~title="Liquidsoap: new track",s)
|
||||
send = 'notify-send -i #{icon} -u #{urgency}'
|
||||
^ ' -t #{time} #{quote(title)} '
|
||||
on_metadata(fun (m) -> system(send^quote(display(m))),s)
|
||||
end
|
||||
|
||||
%ifdef input.external
|
||||
# Stream data from mplayer
|
||||
# @category Source / Input
|
||||
# @param s data URI.
|
||||
# @param ~restart restart on exit.
|
||||
# @param ~restart_on_error restart on exit with error.
|
||||
# @param ~buffer Duration of the pre-buffered data.
|
||||
# @param ~max Maximum duration of the buffered data.
|
||||
# @category Source / Input
|
||||
def input.mplayer(~id="input.mplayer",
|
||||
~restart=true,~restart_on_error=false,
|
||||
~buffer=0.2,~max=10.,s) =
|
||||
input.external(id=id,restart=restart,
|
||||
restart_on_error=restart_on_error,
|
||||
buffer=buffer,max=max,
|
||||
"mplayer -really-quiet -ao pcm:file=/dev/stdout \
|
||||
-vc null -vo null #{quote(s)} 2>/dev/null")
|
||||
end
|
||||
%endif
|
|
@ -1,113 +0,0 @@
|
|||
# BIG TODO:
|
||||
# - Check for errors
|
||||
# - Unregister radio and streams
|
||||
|
||||
# Register a radio on Liquidsoap Flows.
|
||||
# @category Liquidsoap
|
||||
# @param ~radio Name of the radio.
|
||||
# @param ~website URL of the website of the radio.
|
||||
# @param ~description Description of the radio.
|
||||
# @param ~genre Genre of the radio (rock or rap or etc.).
|
||||
# @param ~streams List of streams for the radio described by \
|
||||
# a pair of strings consisting of the format of the stream \
|
||||
# and the url of the stream. The format should be \
|
||||
# of the form "ogg/128k" consisting of the codec and \
|
||||
# the bitrate, separated by "/".
|
||||
def register_flow(~server="",~user="default",~password="default",
|
||||
~email="",~radio,~website,~description,~genre,
|
||||
~streams,s)
|
||||
|
||||
# If the server is "", we get the server from sf.net
|
||||
server =
|
||||
if server == "" then
|
||||
server = http.get("http://savonet.sourceforge.net/flows_server")
|
||||
html_status = snd(fst(fst(fst(server))))
|
||||
if html_status == 200 then
|
||||
snd(server)
|
||||
else
|
||||
# If sf is down, we use the hardcoded server
|
||||
"http://savonet.rastageeks.org/liqflows.py"
|
||||
end
|
||||
else
|
||||
server
|
||||
end
|
||||
log(level=4,"Flows server: #{server}")
|
||||
|
||||
# Initial variables
|
||||
ping_period = 600. # Pinging period in seconds
|
||||
|
||||
# Fix default parameters
|
||||
# and set request function.
|
||||
base_params = [("v", "0.0"),
|
||||
("user",user),
|
||||
("password",password),
|
||||
("email",email),
|
||||
("radio",radio)]
|
||||
def request(~cmd,~params) =
|
||||
log = log(label=radio)
|
||||
log(level=4,"Processing command #{cmd} with arguments:")
|
||||
def log_arg(x) =
|
||||
label = fst(x)
|
||||
value = snd(x)
|
||||
log(level=4," #{label}: #{value}")
|
||||
end
|
||||
list.iter(log_arg,params)
|
||||
|
||||
cmd = url.encode(cmd)
|
||||
params = list.append(base_params,params)
|
||||
def f(z) =
|
||||
x = fst(z)
|
||||
y = url.encode(snd(z))
|
||||
"#{x}=#{y}"
|
||||
end
|
||||
params = string.concat(separator="&",list.map(f,params))
|
||||
url = "#{server}?cmd=#{cmd}&#{params}"
|
||||
|
||||
# TODO: do something with errors!
|
||||
answer = http.get(url)
|
||||
x = fst(answer)
|
||||
status = fst(x)
|
||||
y = fst(status)
|
||||
protocol = fst(y)
|
||||
code = snd(y)
|
||||
desc = snd(status)
|
||||
headers = snd(x)
|
||||
data = snd(answer)
|
||||
log(level=4,"Response status: #{protocol} #{code} #{desc}")
|
||||
log(level=4,"Response headers:")
|
||||
list.iter(log_arg,headers)
|
||||
log(level=4,"Response content: #{data}")
|
||||
end
|
||||
|
||||
# Register radio
|
||||
params = [("radio_website",website),
|
||||
("radio_description",description),
|
||||
("radio_genre",genre)]
|
||||
request(cmd="add radio",params=params)
|
||||
|
||||
# Ping
|
||||
def ping() =
|
||||
ignore(request(cmd="ping radio",params=[]))
|
||||
ping_period
|
||||
end
|
||||
add_timeout(fast=false,ping_period,ping)
|
||||
|
||||
# Register streams
|
||||
def register_stream(format_url)
|
||||
format = fst(format_url);
|
||||
url = snd(format_url);
|
||||
params = [("stream_format",format),("stream_url",url)]
|
||||
request(cmd="add stream",params=params)
|
||||
end
|
||||
request(cmd="clear streams",params=[])
|
||||
list.iter(register_stream,streams)
|
||||
|
||||
# Metadata update
|
||||
def metadata(m) =
|
||||
artist = m["artist"]
|
||||
title = m["title"]
|
||||
params = [("m_title",title),("m_artist",artist)]
|
||||
request(cmd="metadata",params=params)
|
||||
end
|
||||
on_metadata(metadata,s)
|
||||
end
|
|
@ -1,34 +0,0 @@
|
|||
%ifdef input.gstreamer.video
|
||||
# Stream from a video4linux 2 input device, such as a webcam.
|
||||
# @category Source / Input
|
||||
# @param ~id Force the value of the source ID.
|
||||
# @param ~clock_safe Force the use of the dedicated v4l clock.
|
||||
# @param ~device V4L2 device to use.
|
||||
def input.v4l2(~id="",~clock_safe=true,~device="/dev/video0")
|
||||
pipeline = "v4l2src device=#{device}"
|
||||
input.gstreamer.video(id=id, clock_safe=clock_safe, pipeline=pipeline)
|
||||
end
|
||||
|
||||
# Stream from a video4linux 2 input device, such as a webcam.
|
||||
# @category Source / Input
|
||||
# @param ~id Force the value of the source ID.
|
||||
# @param ~clock_safe Force the use of the dedicated v4l clock.
|
||||
# @param ~device V4L2 device to use.
|
||||
def input.v4l2_with_audio(~id="",~clock_safe=true,~device="/dev/video0")
|
||||
audio_pipeline = "autoaudiosrc"
|
||||
video_pipeline = "v4l2src device=#{device}"
|
||||
input.gstreamer.audio_video(id=id, clock_safe=clock_safe, audio_pipeline=audio_pipeline, video_pipeline=video_pipeline)
|
||||
end
|
||||
|
||||
def gstreamer.encode_x264_avi(fname, source)
|
||||
output.gstreamer.video(pipeline="videoconvert ! x264enc ! avimux ! filesink location=\"#{fname}\"", source)
|
||||
end
|
||||
|
||||
def gstreamer.encode_jpeg_avi(fname, source)
|
||||
output.gstreamer.video(pipeline="videoconvert ! jpegenc ! avimux ! filesink location=\"#{fname}\"", source)
|
||||
end
|
||||
|
||||
def gstreamer.encode_mp3(fname, source)
|
||||
output.gstreamer.audio(pipeline="audioconvert ! lamemp3enc ! filesink location=\"#{fname}\"", source)
|
||||
end
|
||||
%endif
|
|
@ -1,37 +0,0 @@
|
|||
# Set of HTTP utils.
|
||||
|
||||
%include "http_codes.liq"
|
||||
|
||||
# Create a HTTP response string
|
||||
# @category Interaction
|
||||
# @param ~protocol HTTP protocol used.
|
||||
# @param ~code Response code.
|
||||
# @param ~headers Response headers.
|
||||
# @param ~data Response data
|
||||
def http_response(~protocol="HTTP/1.1",
|
||||
~code=200,
|
||||
~headers=[],
|
||||
~data="") =
|
||||
status = http_codes[string_of(code)]
|
||||
# Set content-length and connection: close
|
||||
headers =
|
||||
list.append(headers,
|
||||
[("Content-Length", "#{string.length(data)}"),
|
||||
("Connection", "close")])
|
||||
|
||||
headers = list.map(fun (x) -> "#{fst(x)}: #{snd(x)}",headers)
|
||||
headers = string.concat(separator="\r\n",headers)
|
||||
# If no headers are provided, we should avoid
|
||||
# having an empty line for them. Therefore, we also
|
||||
# conditionally add the final \r\n here.
|
||||
headers =
|
||||
if headers != "" then
|
||||
"#{headers}\r\n"
|
||||
else
|
||||
headers
|
||||
end
|
||||
"#{protocol} #{code} #{status}\r\n\
|
||||
#{headers}\
|
||||
\r\n\
|
||||
#{data}"
|
||||
end
|
|
@ -1,304 +0,0 @@
|
|||
# List of HTTP codes. Stolen from en.wikipedia.org..
|
||||
|
||||
# List of HTTP response codes and statuses.
|
||||
# @category Interaction
|
||||
def http_codes = [
|
||||
("100","Continue"),
|
||||
#This means that the server has received the request headers, and that the client
|
||||
#should proceed to send the request body (in the case of a request for which a
|
||||
#body needs to be sent; for example, a POST request). If the request body is
|
||||
#large, sending it to a server when a request has already been rejected based
|
||||
#upon inappropriate headers is inefficient. To have a server check if the request
|
||||
#could be accepted based on the request's headers alone, a client must send
|
||||
#Expect: 100-continue as a header in its initial request and check if a 100
|
||||
#Continue status code is received in response before continuing (or receive 417
|
||||
#Expectation Failed and not continue).
|
||||
|
||||
("101","Switching Protocols"),
|
||||
#This means the requester has asked the server to switch protocols and the server
|
||||
#is acknowledging that it will do so.
|
||||
|
||||
("102","Processing"),
|
||||
#As a WebDAV request may contain many sub-requests involving file operations, it
|
||||
#may take a long time to complete the request. This code indicates that the
|
||||
#server has received and is processing the request, but no response is available
|
||||
#yet. This prevents the client from timing out and assuming the request was
|
||||
#lost.
|
||||
|
||||
("122","Request-URI too long"),
|
||||
#This is a non-standard IE7-only code which means the URI is longer than a
|
||||
#maximum of 2083 characters. (See code 414.),
|
||||
|
||||
#2xx Success
|
||||
#This class of status codes indicates the action requested by the client was
|
||||
#received, understood, accepted and processed successfully.
|
||||
|
||||
("200","OK"),
|
||||
#Standard response for successful HTTP requests. The actual response will depend
|
||||
#on the request method used. In a GET request, the response will contain an
|
||||
#entity corresponding to the requested resource. In a POST request the response
|
||||
#will contain an entity describing or containing the result of the action.
|
||||
|
||||
("201","Created"),
|
||||
#The request has been fulfilled and resulted in a new resource being created.
|
||||
|
||||
("202","Accepted"),
|
||||
#The request has been accepted for processing, but the processing has not been
|
||||
#completed. The request might or might not eventually be acted upon, as it might
|
||||
#be disallowed when processing actually takes place.
|
||||
|
||||
("203","Non-Authoritative Information"),
|
||||
#The server successfully processed the request, but is returning information that
|
||||
#may be from another source.
|
||||
|
||||
("204","No Content"),
|
||||
#The server successfully processed the request, but is not returning any
|
||||
#content.
|
||||
|
||||
("205","Reset Content"),
|
||||
#The server successfully processed the request, but is not returning any content.
|
||||
#Unlike a 204 response, this response requires that the requester reset the
|
||||
#document view.
|
||||
|
||||
("206","Partial Content"),
|
||||
#The server is delivering only part of the resource due to a range header sent by
|
||||
#the client. The range header is used by tools like wget to enable resuming of
|
||||
#interrupted downloads, or split a download into multiple simultaneous
|
||||
#streams.
|
||||
|
||||
("207","Multi-Status"),
|
||||
#The message body that follows is an XML message and can contain a number of
|
||||
#separate response codes, depending on how many sub-requests were made.
|
||||
|
||||
("226","IM Used"),
|
||||
#The server has fulfilled a GET request for the resource, and the response is a
|
||||
#representation of the result of one or more instance-manipulations applied to
|
||||
#the current instance.
|
||||
|
||||
#3xx Redirection
|
||||
#The client must take additional action to complete the request.
|
||||
#This class of status code indicates that further action needs to be taken by the
|
||||
#user agent in order to fulfil the request. The action required may be carried
|
||||
#out by the user agent without interaction with the user if and only if the
|
||||
#method used in the second request is GET or HEAD. A user agent should not
|
||||
#automatically redirect a request more than five times, since such redirections
|
||||
#usually indicate an infinite loop.
|
||||
|
||||
("300","Multiple Choices"),
|
||||
#Indicates multiple options for the resource that the client may follow. It, for
|
||||
#instance, could be used to present different format options for video, list
|
||||
#files with different extensions, or word sense disambiguation.
|
||||
|
||||
("301","Moved Permanently"),
|
||||
#This and all future requests should be directed to the given URI.
|
||||
|
||||
("302","Found"),
|
||||
#This is an example of industrial practice contradicting the standard.
|
||||
#HTTP/1.0 specification (RFC 1945) required the client to perform a temporary
|
||||
#redirect (the original describing phrase was "Moved Temporarily"), but
|
||||
#popular browsers implemented 302 with the functionality of a 303 See Other.
|
||||
#Therefore, HTTP/1.1 added status codes 303 and 307 to distinguish between the
|
||||
#two behaviours. However, the majority of Web applications and frameworks
|
||||
#still[as of?] use the 302 status code as if it were the 303.[citation needed]
|
||||
|
||||
("303","See Other"),
|
||||
#The response to the request can be found under another URI using a GET method.
|
||||
#When received in response to a POST (or PUT/DELETE), it should be assumed that
|
||||
#the server has received the data and the redirect should be issued with a
|
||||
#separate GET message.
|
||||
|
||||
("304","Not Modified"),
|
||||
#Indicates the resource has not been modified since last requested. Typically,
|
||||
#the HTTP client provides a header like the If-Modified-Since header to provide a
|
||||
#time against which to compare. Using this saves bandwidth and reprocessing on
|
||||
#both the server and client, as only the header data must be sent and received in
|
||||
#comparison to the entirety of the page being re-processed by the server, then
|
||||
#sent again using more bandwidth of the server and client.
|
||||
|
||||
("305","Use Proxy"),
|
||||
#Many HTTP clients (such as Mozilla and Internet Explorer) do not correctly
|
||||
#handle responses with this status code, primarily for security reasons.
|
||||
|
||||
("306","Switch Proxy"),
|
||||
#No longer used.
|
||||
|
||||
("307","Temporary Redirect"),
|
||||
#In this occasion, the request should be repeated with another URI, but future
|
||||
#requests can still use the original URI. In contrast to 303, the request
|
||||
#method should not be changed when reissuing the original request. For instance,
|
||||
#a POST request must be repeated using another POST request.
|
||||
|
||||
#4xx Client Error
|
||||
#The 4xx class of status code is intended for cases in which the client seems to
|
||||
#have erred. Except when responding to a HEAD request, the server should include
|
||||
#an entity containing an explanation of the error situation, and whether it is a
|
||||
#temporary or permanent condition. These status codes are applicable to any
|
||||
#request method. User agents should display any included entity to the user.
|
||||
#These are typically the most common error codes encountered while online.
|
||||
|
||||
("400","Bad Request"),
|
||||
#The request cannot be fulfilled due to bad syntax.
|
||||
|
||||
("401","Unauthorized"),
|
||||
#Similar to 403 Forbidden, but specifically for use when authentication is
|
||||
#possible but has failed or not yet been provided. The response must include a
|
||||
#WWW-Authenticate header field containing a challenge applicable to the requested
|
||||
#resource. See Basic access authentication and Digest access authentication.
|
||||
|
||||
("402","Payment Required"),
|
||||
#Reserved for future use. The original intention was that this code might be
|
||||
#used as part of some form of digital cash or micropayment scheme, but that has
|
||||
#not happened, and this code is not usually used. As an example of its use,
|
||||
#however, Apple's MobileMe service generates a 402 error ("httpStatusCode:402" in
|
||||
#the Mac OS X Console log) if the MobileMe account is delinquent.
|
||||
|
||||
("403","Forbidden"),
|
||||
#The request was a legal request, but the server is refusing to respond to it.
|
||||
#Unlike a 401 Unauthorized response, authenticating will make no difference.
|
||||
|
||||
("404","Not Found"),
|
||||
#The requested resource could not be found but may be available again in the
|
||||
#future. Subsequent requests by the client are permissible.
|
||||
|
||||
("405","Method Not Allowed"),
|
||||
#A request was made of a resource using a request method not supported by that
|
||||
#resource; for example, using GET on a form which requires data to be
|
||||
#presented via POST, or using PUT on a read-only resource.
|
||||
|
||||
("406","Not Acceptable"),
|
||||
#The requested resource is only capable of generating content not acceptable
|
||||
#according to the Accept headers sent in the request.
|
||||
|
||||
("407","Proxy Authentication Required"),
|
||||
|
||||
("408","Request Timeout"),
|
||||
#The server timed out waiting for the request. According to W3 HTTP
|
||||
#specifications: "The client did not produce a request within the time that the
|
||||
#server was prepared to wait. The client MAY repeat the request without
|
||||
#modifications at any later time."
|
||||
|
||||
("409","Conflict"),
|
||||
#Indicates that the request could not be processed because of conflict in the
|
||||
#request, such as an edit conflict.
|
||||
|
||||
("410","Gone"),
|
||||
#Indicates that the resource requested is no longer available and will not be
|
||||
#available again. This should be used when a resource has been intentionally
|
||||
#removed and the resource should be purged. Upon receiving a 410 status code, the
|
||||
#client should not request the resource again in the future. Clients such as
|
||||
#search engines should remove the resource from their indices. Most use cases do
|
||||
#not require clients and search engines to purge the resource, and a "404 Not
|
||||
#Found" may be used instead.
|
||||
|
||||
("411","Length Required"),
|
||||
#The request did not specify the length of its content, which is required by the
|
||||
#requested resource.
|
||||
|
||||
("412","Precondition Failed"),
|
||||
#The server does not meet one of the preconditions that the requester put on the
|
||||
#request.
|
||||
|
||||
("413","Request Entity Too Large"),
|
||||
#The request is larger than the server is willing or able to process.
|
||||
|
||||
("414","Request-URI Too Long"),
|
||||
#The URI provided was too long for the server to process.
|
||||
|
||||
("415","Unsupported Media Type"),
|
||||
#The request entity has a media type which the server or resource does not
|
||||
#support. For example, the client uploads an image as image/svg+xml, but the
|
||||
#server requires that images use a different format.
|
||||
|
||||
("416","Requested Range Not Satisfiable"),
|
||||
#The client has asked for a portion of the file, but the server cannot supply
|
||||
#that portion. For example, if the client asked for a part of the file that
|
||||
#lies beyond the end of the file.
|
||||
|
||||
("417","Expectation Failed"),
|
||||
#The server cannot meet the requirements of the Expect request-header field.
|
||||
|
||||
("418","I'm a teapot"),
|
||||
#This code was defined in 1998 as one of the traditional IETF April Fools' jokes,
|
||||
#in RFC 2324, Hyper Text Coffee Pot Control Protocol, and is not expected to be
|
||||
#implemented by actual HTTP servers.
|
||||
|
||||
("422","Unprocessable Entity"),
|
||||
#The request was well-formed but was unable to be followed due to semantic
|
||||
#errors.
|
||||
|
||||
("423","Locked"),
|
||||
#The resource that is being accessed is locked.
|
||||
|
||||
("424","Failed Dependency"),
|
||||
#The request failed due to failure of a previous request (e.g. a PROPPATCH).
|
||||
|
||||
("425","Unordered Collection"),
|
||||
#Defined in drafts of "WebDAV Advanced Collections Protocol", but not present
|
||||
#in "Web Distributed Authoring and Versioning (WebDAV) Ordered Collections
|
||||
#Protocol".
|
||||
|
||||
("426","Upgrade Required"),
|
||||
#The client should switch to a different protocol such as TLS/1.0.
|
||||
|
||||
("444","No Response"),
|
||||
#A Nginx HTTP server extension. The server returns no information to the client
|
||||
#and closes the connection (useful as a deterrent for malware).
|
||||
|
||||
("449","Retry With"),
|
||||
#A Microsoft extension. The request should be retried after performing the
|
||||
#appropriate action.
|
||||
|
||||
("450","Blocked by Windows Parental Controls"),
|
||||
#A Microsoft extension. This error is given when Windows Parental Controls are
|
||||
#turned on and are blocking access to the given webpage.
|
||||
|
||||
("499","Client Closed Request"),
|
||||
#An Nginx HTTP server extension. This code is introduced to log the case when the
|
||||
#connection is closed by client while HTTP server is processing its request,
|
||||
#making server unable to send the HTTP header back.
|
||||
|
||||
#5xx Server Error
|
||||
#The server failed to fulfill an apparently valid request.
|
||||
#Response status codes beginning with the digit "5" indicate cases in which the
|
||||
#server is aware that it has encountered an error or is otherwise incapable of
|
||||
#performing the request. Except when responding to a HEAD request, the server
|
||||
#should include an entity containing an explanation of the error situation, and
|
||||
#indicate whether it is a temporary or permanent condition. Likewise, user agents
|
||||
#should display any included entity to the user. These response codes are
|
||||
#applicable to any request method.
|
||||
|
||||
("500","Internal Server Error"),
|
||||
#A generic error message, given when no more specific message is suitable.
|
||||
|
||||
("501","Not Implemented"),
|
||||
#The server either does not recognise the request method, or it lacks the ability
|
||||
#to fulfill the request.
|
||||
|
||||
("502","Bad Gateway"),
|
||||
#The server was acting as a gateway or proxy and received an invalid response
|
||||
#from the upstream server.
|
||||
|
||||
("503","Service Unavailable"),
|
||||
#The server is currently unavailable (because it is overloaded or down for
|
||||
#maintenance). Generally, this is a temporary state.
|
||||
|
||||
("504","Gateway Timeout"),
|
||||
#The server was acting as a gateway or proxy and did not receive a timely
|
||||
#response from the upstream server.
|
||||
|
||||
("505","HTTP Version Not Supported"),
|
||||
#The server does not support the HTTP protocol version used in the request.
|
||||
|
||||
("506","Variant Also Negotiates"),
|
||||
#Transparent content negotiation for the request results in a circular
|
||||
#reference.
|
||||
|
||||
("507","Insufficient Storage"),
|
||||
|
||||
("509","Bandwidth Limit Exceeded"),
|
||||
#This status code, while used by many servers, is not specified in any RFCs.
|
||||
|
||||
("510","Not Extended")
|
||||
#Further extensions to the request are required for the server to fulfill it.
|
||||
]
|
||||
end
|
|
@ -1,125 +0,0 @@
|
|||
%ifdef input.lastfm
|
||||
# Utility to compose last.fm URIs.
|
||||
# @category String
|
||||
# @param ~user Lastfm user
|
||||
# @param ~password Lastfm password
|
||||
# @param ~discovery Allow lastfm suggestions
|
||||
# @param radio URI, e.g. user/toots5446/playlist, globaltags/rocksteady.
|
||||
def lastfm.uri(~user="",~password="",~discovery=false,
|
||||
radio="globaltags/creative-commons")
|
||||
auth = if user == "" then "" else "#{user}:#{password}@" end
|
||||
discovery = if discovery == true then "1" else "0" end
|
||||
"lastfm://#{auth}#{radio}?discovery=#{discovery}"
|
||||
end
|
||||
|
||||
# Submit metadata to libre.fm using the audioscrobbler protocol.
|
||||
# @category Interaction
|
||||
# @param ~source Source for tracks. Should be one of: "broadcast", "user", "recommendation" or "unknown". Since liquidsoap is intented for radio broadcasting, this is the default. Sources other than user don't need duration to be set.
|
||||
# @param ~length Try to submit length information. This operation can be CPU intensive. Value forced to true when used with the "user" source type.
|
||||
def librefm.submit(~user,~password,~source="broadcast",~length=false,m) =
|
||||
audioscrobbler.submit(user=user,password=password,
|
||||
source=source,length=length,
|
||||
host="turtle.libre.fm",port=80,
|
||||
m)
|
||||
end
|
||||
|
||||
# Submit metadata to lastfm.fm using the audioscrobbler protocol.
|
||||
# @category Interaction
|
||||
# @param ~source Source for tracks. Should be one of: "broadcast", "user", "recommendation" or "unknown". Since liquidsoap is intented for radio broadcasting, this is the default. Sources other than user don't need duration to be set.
|
||||
# @param ~length Try to submit length information. This operation can be CPU intensive. Value forced to true when used with the "user" source type.
|
||||
def lastfm.submit(~user,~password,~source="broadcast",~length=false,m) =
|
||||
audioscrobbler.submit(user=user,password=password,
|
||||
source=source,length=length,
|
||||
host="post.audioscrobbler.com",port=80,
|
||||
m)
|
||||
end
|
||||
|
||||
# Submit metadata to libre.fm using the audioscrobbler protocol (nowplaying mode).
|
||||
# @category Interaction
|
||||
# @param ~length Try to submit length information. This operation can be CPU intensive. Value forced to true when used with the "user" source type.
|
||||
def librefm.nowplaying(~user,~password,~length=false,m) =
|
||||
audioscrobbler.nowplaying(user=user,password=password,length=length,
|
||||
host="turtle.libre.fm",port=80,
|
||||
m)
|
||||
end
|
||||
|
||||
# Submit metadata to lastfm.fm using the audioscrobbler protocol (nowplaying mode).
|
||||
# @category Interaction
|
||||
# @param ~length Try to submit length information. This operation can be CPU intensive. Value forced to true when used with the "user" source type.
|
||||
def lastfm.nowplaying(~user,~password,~length=false,m) =
|
||||
audioscrobbler.nowplaying(user=user,password=password,length=length,
|
||||
host="post.audioscrobbler.com",port=80,
|
||||
m)
|
||||
end
|
||||
|
||||
# Submit songs using audioscrobbler, respecting the full protocol:
|
||||
# First signal song as now playing when starting, and
|
||||
# then submit song when it ends.
|
||||
# @category Interaction
|
||||
# @param ~source Source for tracks. Should be one of: "broadcast", "user", "recommendation" or "unknown". Since liquidsoap is intented for radio broadcasting, this is the default. Sources other than user don't need duration to be set.
|
||||
# @param ~length Try to submit length information. This operation can be CPU intensive. Value forced to true when used with the "user" source type.
|
||||
# @param ~delay Submit song when there is only this delay left, in seconds.
|
||||
# @param ~force If remaining time is null, the song will be assumed to be skipped or cuted, and not submitted. Set to zero to disable this behaviour.
|
||||
def audioscrobbler.submit.full(
|
||||
~user,~password,
|
||||
~host="post.audioscrobbler.com",~port=80,
|
||||
~source="broadcast",~length=false,
|
||||
~delay=10.,~force=false,s) =
|
||||
f = audioscrobbler.nowplaying(
|
||||
user=user,password=password,
|
||||
host=host,port=port,length=length)
|
||||
s = on_metadata(f,s)
|
||||
f = fun (rem,m) ->
|
||||
# Avoid skipped songs
|
||||
if rem > 0. or force then
|
||||
audioscrobbler.submit(
|
||||
user=user,password=password,
|
||||
host=host,port=port,length=length,
|
||||
source=source,m)
|
||||
else
|
||||
log(label="audioscrobbler.submit.full",
|
||||
level=4,"Remaining time null: \
|
||||
will not submit song (song skipped ?)")
|
||||
end
|
||||
on_end(delay=delay,f,s)
|
||||
end
|
||||
|
||||
# Submit songs to librefm using audioscrobbler, respecting the full protocol:
|
||||
# First signal song as now playing when starting, and
|
||||
# then submit song when it ends.
|
||||
# @category Interaction
|
||||
# @param ~source Source for tracks. Should be one of: "broadcast", "user", "recommendation" or "unknown". Since liquidsoap is intented for radio broadcasting, this is the default. Sources other than user don't need duration to be set.
|
||||
# @param ~length Try to submit length information. This operation can be CPU intensive. Value forced to true when used with the "user" source type.
|
||||
# @param ~delay Submit song when there is only this delay left, in seconds. If remaining time is less than this value, the song will be assumed to be skipped or cuted, and not submitted. Set to zero to disable this behaviour.
|
||||
# @param ~force If remaining time is null, the song will be assumed to be skipped or cuted, and not submitted. Set to zero to disable this behaviour.
|
||||
def librefm.submit.full(
|
||||
~user,~password,
|
||||
~source="broadcast",~length=false,
|
||||
~delay=10.,~force=false,s) =
|
||||
audioscrobbler.submit.full(
|
||||
user=user,password=password,
|
||||
source=source,length=length,
|
||||
host="turtle.libre.fm",port=80,
|
||||
delay=delay,force=force,s)
|
||||
end
|
||||
|
||||
# Submit songs to lastfm using audioscrobbler, respecting the full protocol:
|
||||
# First signal song as now playing when starting, and
|
||||
# then submit song when it ends.
|
||||
# @category Interaction
|
||||
# @param ~source Source for tracks. Should be one of: "broadcast", "user", "recommendation" or "unknown". Since liquidsoap is intented for radio broadcasting, this is the default. Sources other than user don't need duration to be set.
|
||||
# @param ~length Try to submit length information. This operation can be CPU intensive. Value forced to true when used with the "user" source type.
|
||||
# @param ~delay Submit song when there is only this delay left, in seconds. If remaining time is less than this value, the song will be assumed to be skipped or cuted, and not submitted. Set to zero to disable this behaviour.
|
||||
# @param ~force If remaining time is null, the song will be assumed to be skipped or cuted, and not submitted. Set to zero to disable this behaviour.
|
||||
def lastfm.submit.full(
|
||||
~user,~password,
|
||||
~source="broadcast",~length=false,
|
||||
~delay=10.,~force=false,s) =
|
||||
audioscrobbler.submit.full(
|
||||
user=user,password=password,
|
||||
source=source,length=length,
|
||||
host="post.audioscrobbler.com",port=80,
|
||||
delay=delay,force=force,s)
|
||||
end
|
||||
|
||||
%endif
|
|
@ -1,8 +0,0 @@
|
|||
%include "utils.liq"
|
||||
%include "externals.liq"
|
||||
%include "shoutcast.liq"
|
||||
%include "lastfm.liq"
|
||||
%include "flows.liq"
|
||||
%include "http.liq"
|
||||
%include "video_text.liq"
|
||||
%include "gstreamer.liq"
|
|
@ -1,46 +0,0 @@
|
|||
|
||||
%ifdef output.icecast
|
||||
# Output to shoutcast.
|
||||
# @category Source / Output
|
||||
# @param ~id Output's ID
|
||||
# @param ~start Start output threads on operator initialization.
|
||||
# @param ~user User for shout source connection. Useful only in special cases, like with per-mountpoint users.
|
||||
# @param ~icy_reset Reset shoutcast source buffer upon connecting (necessary for NSV).
|
||||
# @param ~dumpfile Dump stream to file, for debugging purpose. Disabled if empty.
|
||||
# @param ~fallible Allow the child source to fail, in which case the output will be (temporarily) stopped.
|
||||
# @param ~on_start Callback executed when outputting starts.
|
||||
# @param ~on_stop Callback executed when outputting stops.
|
||||
# @param ~on_error Callback executed when an error happens. If returned value is positive, connection wll be tried again after this amount of time (in seconds).
|
||||
# @param ~on_connect Callback executed when connection starts.
|
||||
# @param ~on_disconnect Callback executed when connection stops.
|
||||
# @param ~icy_metadata Send new metadata using the ICY protocol. One of: "guess", "true", "false"
|
||||
# @param ~format Format, e.g. "audio/ogg". When empty, the encoder is used to guess.
|
||||
# @param e Endoding format. For shoutcast, should be mp3 or AAC(+).
|
||||
# @param s The source to output
|
||||
def output.shoutcast(
|
||||
~id="output.shoutcast",~start=true,
|
||||
~host="localhost",~port=8000,
|
||||
~user="source",~password="hackme",
|
||||
~genre="Misc",~url="http://savonet.sf.net/",
|
||||
~name="Liquidsoap Radio!",~public=true, ~format="",
|
||||
~dumpfile="", ~icy_metadata="guess",
|
||||
~on_connect={()}, ~on_disconnect={()},
|
||||
~aim="",~icq="",~irc="",~icy_reset=true,
|
||||
~fallible=false,~on_start={()},~on_stop={()},
|
||||
~on_error=fun(_)->3., e,s) =
|
||||
icy_reset = if icy_reset then "1" else "0" end
|
||||
headers = [("icy-aim",aim),("icy-irc",irc),
|
||||
("icy-icq",icq),("icy-reset",icy_reset)]
|
||||
output.icecast(
|
||||
e, format=format,
|
||||
id=id, headers=headers,
|
||||
start=start,icy_metadata=icy_metadata,
|
||||
on_connect=on_connect, on_disconnect=on_disconnect,
|
||||
host=host, port=port, user=user, password=password,
|
||||
genre=genre, url=url, description="UNUSED",
|
||||
public=public, dumpfile=dumpfile,encoding="ISO-8859-1",
|
||||
name=name, mount="/", protocol="icy",on_error=on_error,
|
||||
fallible=fallible,on_start=on_start,on_stop=on_stop,
|
||||
s)
|
||||
end
|
||||
%endif
|
|
@ -1,729 +0,0 @@
|
|||
|
||||
# Turn a source into an infaillible source.
|
||||
# by adding blank when the source is not available.
|
||||
# @param s the source to turn infaillible
|
||||
# @category Source / Track Processing
|
||||
def mksafe(~id="mksafe",s)
|
||||
fallback(id=id,track_sensitive=false,[s,blank(id="safe_blank")])
|
||||
end
|
||||
|
||||
# Alias for the <code>l[k]</code> notation.
|
||||
# @category List
|
||||
# @param a Key to look for
|
||||
# @param l List of pairs (key,value)
|
||||
def list.assoc(a,l)
|
||||
l[a]
|
||||
end
|
||||
|
||||
# list.mem_assoc(key,l) returns true if l contains a pair
|
||||
# (key,value)
|
||||
# @category List
|
||||
# @param a Key to look for
|
||||
# @param l List of pairs (key,value)
|
||||
def list.mem_assoc(a,l)
|
||||
def f(cur, el) =
|
||||
if not cur then
|
||||
fst(el) == a
|
||||
else
|
||||
cur
|
||||
end
|
||||
end
|
||||
list.fold(f, false, l)
|
||||
end
|
||||
|
||||
# Remove a pair from an associative list
|
||||
# @category List
|
||||
# @param a Key of pair to be removed
|
||||
# @param l List of pairs (key,value)
|
||||
def list.remove_assoc(a,l)
|
||||
list.remove((a,list.assoc(a,l)),l)
|
||||
end
|
||||
|
||||
# Rewrite metadata on the fly using a list of (target,rules).
|
||||
# @category Source / Track Processing
|
||||
# @param l \
|
||||
# List of (target,value) rewriting rules.
|
||||
# @param ~insert_missing \
|
||||
# Treat track beginnings without metadata as having empty ones. \
|
||||
# The operational order is: \
|
||||
# create empty if needed, map and strip if enabled.
|
||||
# @param ~update \
|
||||
# Only update metadata. \
|
||||
# If false, only returned values will be set as metadata.
|
||||
# @param ~strip \
|
||||
# Completly remove empty metadata. \
|
||||
# Operates on both empty values and empty metadata chunk.
|
||||
def rewrite_metadata(l,~insert_missing=true,
|
||||
~update=true,~strip=false,
|
||||
s)
|
||||
# We don't need to return all values, since
|
||||
# map_metadata only update returned values.
|
||||
# So, we simply apply all rewrite rules !
|
||||
def map(m)
|
||||
def apply(x)
|
||||
label = fst(x)
|
||||
value = snd(x)
|
||||
(label,value % m)
|
||||
end
|
||||
list.map(apply,l)
|
||||
end
|
||||
map_metadata(map,insert_missing=insert_missing,
|
||||
update=update,strip=strip,s)
|
||||
end
|
||||
|
||||
# Add a skip function to a source
|
||||
# when it does not have one
|
||||
# by default
|
||||
# @category Interaction
|
||||
# @param s The source to attach the command to.
|
||||
def add_skip_command(s) =
|
||||
# A command to skip
|
||||
def skip(_) =
|
||||
source.skip(s)
|
||||
"Done!"
|
||||
end
|
||||
# Register the command:
|
||||
server.register(namespace="#{source.id(s)}",
|
||||
usage="skip",
|
||||
description="Skip the current song.",
|
||||
"skip",skip)
|
||||
end
|
||||
|
||||
# Removes all metadata coming from a source
|
||||
# @category Source / Track Processing
|
||||
def drop_metadata(s)
|
||||
map_metadata(fun(_)->[],update=false,strip=true,insert_missing=false,s)
|
||||
end
|
||||
|
||||
# Merge all tracks from a source, provided that it does not fail
|
||||
# @category Source / Track Processing
|
||||
def merge_tracks(s)
|
||||
sequence(merge=true,[s])
|
||||
end
|
||||
|
||||
# Default inputs and outpus
|
||||
#
|
||||
# They are called "prefered" but it's not a user preference,
|
||||
# just a view of what's generally preferable among the available
|
||||
# modules.
|
||||
# It is important that input and output preferences are in the
|
||||
# same order: the chosen I/O should work in the same clock, we don't
|
||||
# want an ALSA input and OSS output. The only exception is AO:
|
||||
# it is the default output after dummy, so the input will be a dummy
|
||||
# when AO is used for output.
|
||||
|
||||
output.prefered=output.dummy
|
||||
%ifdef output.ao
|
||||
output.prefered=output.ao
|
||||
%endif
|
||||
%ifdef output.alsa
|
||||
output.prefered=output.alsa
|
||||
%endif
|
||||
%ifdef output.oss
|
||||
output.prefered=output.oss
|
||||
%endif
|
||||
%ifdef output.portaudio
|
||||
output.prefered = output.portaudio
|
||||
%endif
|
||||
%ifdef output.pulseaudio
|
||||
output.prefered=output.pulseaudio
|
||||
%endif
|
||||
# Output to local audio card using the first available driver in
|
||||
# pulseaudio, portaudio, oss, alsa, ao, dummy.
|
||||
# @category Source / Output
|
||||
def output.prefered(~id="",~fallible=false,
|
||||
~on_start={()},~on_stop={()},~start=true,s)
|
||||
output.prefered(id=id,fallible=fallible,
|
||||
start=start,on_start=on_start,on_stop=on_stop,
|
||||
s)
|
||||
end
|
||||
|
||||
def in(~id="",~start=true,~on_start={()},~on_stop={()},~fallible=false)
|
||||
blank(id=id)
|
||||
end
|
||||
%ifdef input.alsa
|
||||
in = input.alsa
|
||||
%endif
|
||||
%ifdef input.oss
|
||||
in = input.oss
|
||||
%endif
|
||||
%ifdef input.portaudio
|
||||
in = input.portaudio
|
||||
%endif
|
||||
%ifdef input.pulseaudio
|
||||
in = input.pulseaudio
|
||||
%endif
|
||||
# Create a source from the first available input driver in
|
||||
# pulseaudio, portaudio, oss, alsa, blank.
|
||||
# @category Source / Input
|
||||
def in(~id="",~start=true,~on_start={()},~on_stop={()},~fallible=false)
|
||||
in(id=id,start=start,on_start=on_start,on_stop=on_stop,fallible=fallible)
|
||||
end
|
||||
|
||||
# Output a stream using the 'output.prefered' operator. The input source does
|
||||
# not need to be infallible, blank will just be played during failures.
|
||||
# @param s the source to output
|
||||
# @category Source / Output
|
||||
def out(s)
|
||||
output.prefered(mksafe(s))
|
||||
end
|
||||
|
||||
# Special track insensitive fallback that always skips current song before switching.
|
||||
# @category Source / Track Processing
|
||||
# @param ~input The input source
|
||||
# @param f The fallback source
|
||||
def fallback.skip(~input,f)
|
||||
def transition(a,b) =
|
||||
source.skip(a)
|
||||
# This eats the last remaining frame from a
|
||||
sequence([a,b])
|
||||
end
|
||||
fallback(track_sensitive=false,transitions=[transition,transition],[input,f])
|
||||
end
|
||||
|
||||
# Compress and normalize, producing a more uniform and "full" sound.
|
||||
# @category Source / Sound Processing
|
||||
# @param s The input source.
|
||||
def nrj(s)
|
||||
compress(threshold=-15.,ratio=3.,gain=3.,normalize(s))
|
||||
end
|
||||
|
||||
# Multiband-compression.
|
||||
# @category Source / Sound Processing
|
||||
# @param s The input source.
|
||||
def sky(s)
|
||||
# 3-band crossover
|
||||
low = filter.iir.eq.low(frequency = 168.)
|
||||
mh = filter.iir.eq.high(frequency = 100.)
|
||||
mid = filter.iir.eq.low(frequency = 1800.)
|
||||
high = filter.iir.eq.high(frequency = 1366.)
|
||||
|
||||
# Add back
|
||||
add(normalize = false,
|
||||
[ compress(attack = 100., release = 200., threshold = -20.,
|
||||
ratio = 6., gain = 6.7, knee = 0.3,
|
||||
low(s)),
|
||||
compress(attack = 100., release = 200., threshold = -20.,
|
||||
ratio = 6., gain = 6.7, knee = 0.3,
|
||||
mid(mh(s))),
|
||||
compress(attack = 100., release = 200., threshold = -20.,
|
||||
ratio = 6., gain = 6.7, knee = 0.3,
|
||||
high(s))
|
||||
])
|
||||
end
|
||||
|
||||
# Simple crossfade.
|
||||
# @category Source / Track Processing
|
||||
# @param ~start_next Duration in seconds of the crossed end of track.
|
||||
# @param ~fade_in Duration of the fade in for next track.
|
||||
# @param ~fade_out Duration of the fade out for previous track.
|
||||
# @param ~conservative Always prepare for a premature end-of-track.
|
||||
# @param s The source to use.
|
||||
def crossfade(~id="",~conservative=true,
|
||||
~start_next=5.,~fade_in=3.,~fade_out=3.,
|
||||
s)
|
||||
s = fade.in(duration=fade_in,s)
|
||||
s = fade.out(duration=fade_out,s)
|
||||
fader = fun (a,b) -> add(normalize=false,[b,a])
|
||||
cross(id=id,conservative=conservative,duration=start_next,fader,s)
|
||||
end
|
||||
|
||||
# Append speech-synthesized tracks reading the metadata.
|
||||
# @category Source / Track Processing
|
||||
# @param ~pattern Pattern to use
|
||||
# @param s The source to use
|
||||
def say_metadata
|
||||
p = 'say:$(if $(artist),"It was $(artist)$(if $(title),\", $(title)\").")'
|
||||
fun (s,~pattern=p) ->
|
||||
append(s,fun (m) -> request.queue(queue=[request.create(pattern % m)],
|
||||
interactive=false))
|
||||
end
|
||||
|
||||
%ifdef soundtouch
|
||||
# Increases the pitch, making voices sound like on helium.
|
||||
# @category Source / Sound Processing
|
||||
# @param s The input source.
|
||||
def helium(s)
|
||||
soundtouch(pitch=1.5,s)
|
||||
end
|
||||
%endif
|
||||
|
||||
# Return true if process exited with 0 code. Command should return quickly.
|
||||
# @category System
|
||||
# @param command Command to test
|
||||
def test_process(command)
|
||||
lines =
|
||||
get_process_lines("(" ^ command ^ " >/dev/null 2>&1 && echo 0) || echo 1")
|
||||
if list.length(lines) == 0 then
|
||||
false
|
||||
else
|
||||
"0" == list.hd(lines)
|
||||
end
|
||||
end
|
||||
|
||||
# Split an url of the form foo?arg=bar&arg2=bar2
|
||||
# into ("foo",[("arg","bar"),("arg2","bar2")]).
|
||||
# @category String
|
||||
# @param uri Url to split
|
||||
def url.split(uri) =
|
||||
ret = string.extract(pattern="([^\?]*)\?(.*)",uri)
|
||||
args = ret["2"]
|
||||
if args != "" then
|
||||
l = string.split(separator="&",args)
|
||||
def f(x) =
|
||||
ret = string.split(separator="=",x)
|
||||
(url.decode(list.nth(ret,0)),
|
||||
url.decode(list.nth(ret,1)))
|
||||
end
|
||||
l = list.map(f,l)
|
||||
(ret["1"],l)
|
||||
else
|
||||
(uri,[])
|
||||
end
|
||||
end
|
||||
|
||||
# Register a server/telnet command to update a source's metadata. Returns
|
||||
# a new source, which will receive the updated metadata. The command has
|
||||
# the following format: insert key1="val1",key2="val2",...
|
||||
# @category Source / Track Processing
|
||||
# @param ~id Force the value of the source ID.
|
||||
def server.insert_metadata(~id="",s) =
|
||||
x = insert_metadata(id=id,s)
|
||||
insert = fst(x)
|
||||
s = snd(x)
|
||||
def insert(s) =
|
||||
l = string.split(separator='([^=]+\s*=\s*"(\\"|[^"])*")\s*,\s*',s)
|
||||
def f(l,x) =
|
||||
sub = fun (s) -> string.replace(pattern='\\"',fun (_) -> '"',s)
|
||||
if x != "" then
|
||||
ret = string.extract(pattern='([^=]+)\s*=\s*"((?:\\"|[^"])*)"',x)
|
||||
if ret["1"] != "" then
|
||||
list.append(l,[(ret["1"],
|
||||
sub(ret["2"]))])
|
||||
else
|
||||
l
|
||||
end
|
||||
else
|
||||
l
|
||||
end
|
||||
end
|
||||
meta = list.fold(f,[],l)
|
||||
if meta != [] then
|
||||
insert(meta)
|
||||
"Done"
|
||||
else
|
||||
"Syntax error or no metadata given. \
|
||||
Use key1=\"val1\",key2=\"val2\",.."
|
||||
end
|
||||
end
|
||||
id = source.id(s)
|
||||
server.register(namespace="#{id}",
|
||||
description="Insert a metadata chunk.",
|
||||
usage="insert key1=\"val1\",key2=\"val2\",..",
|
||||
"insert",insert)
|
||||
s
|
||||
end
|
||||
|
||||
# Register a command that outputs the RMS of the returned source.
|
||||
# @category Source / Visualization
|
||||
# @param ~id Force the value of the source ID.
|
||||
def server.rms(~id="",s) =
|
||||
x = rms(id=id,s)
|
||||
rms = fst(x)
|
||||
s = snd(x)
|
||||
id = source.id(s)
|
||||
def rms(_) =
|
||||
rms = rms()
|
||||
"#{rms}"
|
||||
end
|
||||
server.register(namespace="#{id}",
|
||||
description="Return the current RMS of the source.",
|
||||
usage="rms",
|
||||
"rms",rms)
|
||||
s
|
||||
end
|
||||
|
||||
# Read some value from standard input (console).
|
||||
# @category System
|
||||
# @param ~hide Hide typed characters (for passwords).
|
||||
def read(~hide=false)
|
||||
if hide then
|
||||
system("stty -echo")
|
||||
end
|
||||
s = list.hd(get_process_lines("read BLA && echo $BLA"))
|
||||
if hide then
|
||||
system("stty echo")
|
||||
end
|
||||
print("")
|
||||
s
|
||||
end
|
||||
|
||||
# Dummy implementation of file.mime
|
||||
# @category System
|
||||
def file.mime_default(_)
|
||||
""
|
||||
end
|
||||
%ifdef file.mime
|
||||
# Alias of file.mime (because it is available)
|
||||
# @category System
|
||||
def file.mime_default(file)
|
||||
file.mime(file)
|
||||
end
|
||||
%endif
|
||||
|
||||
# Generic mime test. First try to use file.mime if it exist.
|
||||
# Otherwise try to get the value using the file binary.
|
||||
# Returns "" (empty string) if no value can be find.
|
||||
# @category System
|
||||
# @param file The file to test
|
||||
def get_mime(file) =
|
||||
def file_method(file) =
|
||||
if test_process("which file") then
|
||||
list.hd(get_process_lines("file -b --mime-type \
|
||||
#{quote(file)}"))
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
# First try mime method
|
||||
ret = file.mime_default(file)
|
||||
if ret != "" then
|
||||
ret
|
||||
else
|
||||
# Now try file method
|
||||
file_method(file)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Remove low frequencies often produced by microphones.
|
||||
# @category Source / Sound Processing
|
||||
# @param s The input source.
|
||||
def mic_filter(s)
|
||||
filter(freq=200.,q=1.,mode="high",s)
|
||||
end
|
||||
|
||||
# Creates a source that fails to produce anything.
|
||||
# @category Source / Input
|
||||
def fail(~id="")
|
||||
fallback(id=id,[])
|
||||
end
|
||||
|
||||
# Creates a source that plays only one track of the input source.
|
||||
# @category Source / Track Processing
|
||||
# @param s The input source.
|
||||
def once(s)
|
||||
sequence([s,fail()])
|
||||
end
|
||||
|
||||
# Crossfade between tracks, taking the respective volume levels into account in
|
||||
# the choice of the transition.
|
||||
# @category Source / Track Processing
|
||||
# @param ~start_next Crossing duration, if any.
|
||||
# @param ~fade_in Fade-in duration, if any.
|
||||
# @param ~fade_out Fade-out duration, if any.
|
||||
# @param ~width Width of the volume analysis window.
|
||||
# @param ~conservative Always prepare for a premature end-of-track.
|
||||
# @param ~default Transition used when no rule applies \
|
||||
# (default: sequence).
|
||||
# @param ~high Value, in dB, for loud sound level.
|
||||
# @param ~medium Value, in dB, for medium sound level.
|
||||
# @param ~margin Margin to detect sources that have too different \
|
||||
# sound level for crossing.
|
||||
# @param s The input source.
|
||||
def smart_crossfade (~start_next=5.,~fade_in=3.,~fade_out=3.,
|
||||
~default=(fun (a,b) -> sequence([a, b])),
|
||||
~high=-15., ~medium=-32., ~margin=4.,
|
||||
~width=2.,~conservative=true,s)
|
||||
fade.out = fade.out(type="sin",duration=fade_out)
|
||||
fade.in = fade.in(type="sin",duration=fade_in)
|
||||
add = fun (a,b) -> add(normalize=false,[b, a])
|
||||
log = log(label="smart_crossfade")
|
||||
|
||||
def transition(a,b,ma,mb,sa,sb)
|
||||
|
||||
list.iter(fun(x)-> log(level=4,"Before: #{x}"),ma)
|
||||
list.iter(fun(x)-> log(level=4,"After : #{x}"),mb)
|
||||
|
||||
if
|
||||
# If A and B are not too loud and close, fully cross-fade them.
|
||||
a <= medium and b <= medium and abs(a - b) <= margin
|
||||
then
|
||||
log("Old <= medium, new <= medium and |old-new| <= margin.")
|
||||
log("Old and new source are not too loud and close.")
|
||||
log("Transition: crossed, fade-in, fade-out.")
|
||||
add(fade.out(sa),fade.in(sb))
|
||||
|
||||
elsif
|
||||
# If B is significantly louder than A, only fade-out A.
|
||||
# We don't want to fade almost silent things, ask for >medium.
|
||||
b >= a + margin and a >= medium and b <= high
|
||||
then
|
||||
log("new >= old + margin, old >= medium and new <= high.")
|
||||
log("New source is significantly louder than old one.")
|
||||
log("Transition: crossed, fade-out.")
|
||||
add(fade.out(sa),sb)
|
||||
|
||||
elsif
|
||||
# Opposite as the previous one.
|
||||
a >= b + margin and b >= medium and a <= high
|
||||
then
|
||||
log("old >= new + margin, new >= medium and old <= high")
|
||||
log("Old source is significantly louder than new one.")
|
||||
log("Transition: crossed, fade-in.")
|
||||
add(sa,fade.in(sb))
|
||||
|
||||
elsif
|
||||
# Do not fade if it's already very low.
|
||||
b >= a + margin and a <= medium and b <= high
|
||||
then
|
||||
log("new >= old + margin, old <= medium and new <= high.")
|
||||
log("Do not fade if it's already very low.")
|
||||
log("Transition: crossed, no fade.")
|
||||
add(sa,sb)
|
||||
|
||||
# What to do with a loud end and a quiet beginning ?
|
||||
# A good idea is to use a jingle to separate the two tracks,
|
||||
# but that's another story.
|
||||
|
||||
else
|
||||
# Otherwise, A and B are just too loud to overlap nicely,
|
||||
# or the difference between them is too large and overlapping would
|
||||
# completely mask one of them.
|
||||
log("No transition: using default.")
|
||||
default(sa, sb)
|
||||
end
|
||||
end
|
||||
|
||||
smart_cross(width=width, duration=start_next, conservative=conservative,
|
||||
transition,s)
|
||||
end
|
||||
|
||||
# Custom playlist source written using the script language.
|
||||
# Will read directory or playlist, play all files and stop.
|
||||
# Returns a pair @(reload,source)@ where @reload@ is a function
|
||||
# of type @(?uri:string)->unit@ used to reload the source and @source@
|
||||
# is the actual source. The reload function can optionally be called
|
||||
# with a new playlist URI. Otherwise, it reloads the previous URI.
|
||||
# @category Source / Input
|
||||
# @param ~id Force the value of the source ID.
|
||||
# @param ~random Randomize playlist content
|
||||
# @param ~on_done Function to execute when the playlist is finished
|
||||
# @param uri Playlist URI
|
||||
def playlist.reloadable(~id="",~random=false,~on_done={()},uri)
|
||||
# A reference to the playlist
|
||||
playlist = ref []
|
||||
# A reference to the uri
|
||||
playlist_uri = ref uri
|
||||
# A reference to know if the source
|
||||
# has been stopped
|
||||
has_stopped = ref false
|
||||
# The next function
|
||||
def next () =
|
||||
file =
|
||||
if list.length(!playlist) > 0 then
|
||||
ret = list.hd(!playlist)
|
||||
playlist := list.tl(!playlist)
|
||||
ret
|
||||
else
|
||||
# Playlist finished
|
||||
if not !has_stopped then
|
||||
on_done ()
|
||||
end
|
||||
has_stopped := true
|
||||
""
|
||||
end
|
||||
request.create(file)
|
||||
end
|
||||
# Instanciate the source
|
||||
source = request.dynamic(id=id,next)
|
||||
# Get its id.
|
||||
id = source.id(source)
|
||||
# The load function
|
||||
def load_playlist () =
|
||||
files =
|
||||
if test_process("test -d #{quote(!playlist_uri)}") then
|
||||
log(label=id,"playlist is a directory.")
|
||||
get_process_lines("find #{quote(!playlist_uri)} -type f | sort")
|
||||
else
|
||||
playlist = request.create.raw(!playlist_uri)
|
||||
result =
|
||||
if request.resolve(playlist) then
|
||||
playlist = request.filename(playlist)
|
||||
files = playlist.parse(playlist)
|
||||
def file_request(el) =
|
||||
meta = fst(el)
|
||||
file = snd(el)
|
||||
s = list.fold(fun (cur, el) ->
|
||||
"#{cur},#{fst(el)}=#{string.escape(snd(el))}", "", meta)
|
||||
if s == "" then
|
||||
file
|
||||
else
|
||||
"annotate:#{s}:#{file}"
|
||||
end
|
||||
end
|
||||
list.map(file_request,files)
|
||||
else
|
||||
log(label=id,"Couldn't read playlist: request resolution failed.")
|
||||
[]
|
||||
end
|
||||
request.destroy(playlist)
|
||||
result
|
||||
end
|
||||
if random then
|
||||
playlist := list.sort(fun (x,y) -> int_of_float(random.float()), files)
|
||||
else
|
||||
playlist := files
|
||||
end
|
||||
end
|
||||
# The reload function
|
||||
def reload(~uri="") =
|
||||
if uri != "" then
|
||||
playlist_uri := uri
|
||||
end
|
||||
log(label=id,"Reloading playlist with URI #{!playlist_uri}")
|
||||
has_stopped := false
|
||||
load_playlist()
|
||||
end
|
||||
# Load the playlist
|
||||
load_playlist()
|
||||
# Return
|
||||
(reload,source)
|
||||
end
|
||||
|
||||
# Custom playlist source written using the script language.
|
||||
# Will read directory or playlist, play all files and stop
|
||||
# @category Source / Input
|
||||
# @param ~id Force the value of the source ID.
|
||||
# @param ~random Randomize playlist content
|
||||
# @param ~on_done Function to execute when the playlist is finished
|
||||
# @param uri Playlist URI
|
||||
def playlist.once(~id="",~random=false,~on_done={()},uri)
|
||||
snd(playlist.reloadable(id=id,random=random,on_done=on_done,uri))
|
||||
end
|
||||
|
||||
# Mixes two streams, with faded transitions between the state when only the
|
||||
# normal stream is available and when the special stream gets added on top of
|
||||
# it.
|
||||
# @category Source / Track Processing
|
||||
# @param ~delay Delay before starting the special source.
|
||||
# @param ~p Portion of amplitude of the normal source in the mix.
|
||||
# @param ~normal The normal source, which could be called the carrier too.
|
||||
# @param ~special The special source.
|
||||
def smooth_add(~delay=0.5,~p=0.2,~normal,~special)
|
||||
d = delay
|
||||
fade.final = fade.final(duration=d*2.)
|
||||
fade.initial = fade.initial(duration=d*2.)
|
||||
q = 1. - p
|
||||
c = amplify
|
||||
fallback(track_sensitive=false,
|
||||
[special,normal],
|
||||
transitions=[
|
||||
fun(normal,special)->
|
||||
add(normalize=false,
|
||||
[c(p,normal),
|
||||
c(q,fade.final(type="sin",normal)),
|
||||
sequence([blank(duration=d),c(q,special)])]),
|
||||
fun(special,normal)->
|
||||
add(normalize=false,
|
||||
[c(p,normal),
|
||||
c(q,fade.initial(type="sin",normal))])
|
||||
])
|
||||
end
|
||||
|
||||
# Restrict a source to play only when a predicate is true.
|
||||
# @category Source / Track Processing
|
||||
# @param pred The predicate, typically a time interval such as \
|
||||
# <code>{10h-10h30}</code>.
|
||||
def at(pred,s)
|
||||
switch([(pred,s)])
|
||||
end
|
||||
|
||||
# Execute a given action when a predicate is true.
|
||||
# This will be run in background.
|
||||
# @category System
|
||||
# @param ~freq Frequency for checking the predicate, in seconds.
|
||||
# @param ~pred Predicate indicating when to execute the function, \
|
||||
# typically a time interval such as <code>{10h-10h30}</code>.
|
||||
# @param f Function to execute when the predicate is true.
|
||||
def exec_at(~freq=1.,~pred,f)
|
||||
def check()
|
||||
if pred() then
|
||||
f()
|
||||
end
|
||||
freq
|
||||
end
|
||||
add_timeout(freq,check)
|
||||
end
|
||||
|
||||
# Register the replaygain protocol.
|
||||
# @category Liquidsoap
|
||||
def replaygain_protocol(arg,delay)
|
||||
# The extraction program
|
||||
extract_replaygain = "#{configure.libdir}/extract-replaygain"
|
||||
x = get_process_lines("#{extract_replaygain} #{quote(arg)}")
|
||||
if list.hd(x) != "" then
|
||||
["annotate:replay_gain=\"#{list.hd(x)}\":#{arg}"]
|
||||
else
|
||||
[arg]
|
||||
end
|
||||
end
|
||||
add_protocol("replay_gain", replaygain_protocol)
|
||||
|
||||
# Enable replay gain metadata resolver. This resolver will
|
||||
# process any file decoded by liquidsoap and add a replay_gain
|
||||
# metadata when this value could be computed. For a finer-grained
|
||||
# replay gain processing, use the replay_gain protocol.
|
||||
# @category Liquidsoap
|
||||
# @param ~extract_replaygain The extraction program
|
||||
def enable_replaygain_metadata(
|
||||
~extract_replaygain="#{configure.libdir}/extract-replaygain")
|
||||
def replaygain_metadata(file)
|
||||
x = get_process_lines("#{extract_replaygain} \
|
||||
#{quote(file)}")
|
||||
if list.hd(x) != "" then
|
||||
[("replay_gain",list.hd(x))]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
add_metadata_resolver("replay_gain", replaygain_metadata)
|
||||
end
|
||||
|
||||
# Assign a new clock to the given source (and to other time-dependent
|
||||
# sources) and return the source. It is a conveniency wrapper around
|
||||
# clock.assign_new(), allowing more concise scripts in some cases.
|
||||
# @category Liquidsoap
|
||||
# @param ~sync Do not synchronize the clock on regular wallclock time, \
|
||||
# but try to run as fast as possible (CPU burning mode).
|
||||
def clock(~sync=true,~id="",s)
|
||||
clock.assign_new(sync=sync,id=id,[s])
|
||||
s
|
||||
end
|
||||
|
||||
# Create a log of clock times for all the clocks initially present.
|
||||
# The log is in a simple format which you can directly use with gnuplot.
|
||||
# @category Liquidsoap
|
||||
# @param ~interval Polling interval.
|
||||
# @param ~delay Delay before setting up the clock logger. This should \
|
||||
# be used to ensure that the logger starts only after \
|
||||
# the clocks are created.
|
||||
# @param unlabeled Path of the log file.
|
||||
def log_clocks(~delay=0.,~interval=1.,logfile)
|
||||
# Get the current clocks
|
||||
clocks = list.map(fst,get_clock_status())
|
||||
# Column headers
|
||||
system("echo \# #{string.concat(separator=' ',clocks)} > #{(logfile:string)}")
|
||||
def report()
|
||||
status = get_clock_status()
|
||||
status = list.map(fun (x) -> (fst(x),string_of(snd(x))), status)
|
||||
status = list.map(fun (c) -> status[c], clocks)
|
||||
system("echo #{string.concat(separator=' ',status)} >> #{logfile}")
|
||||
interval
|
||||
end
|
||||
if delay<=0. then
|
||||
add_timeout(interval,report)
|
||||
else
|
||||
add_timeout(delay,{add_timeout(interval,report) (-1.)})
|
||||
end
|
||||
end
|
|
@ -1,45 +0,0 @@
|
|||
%ifdef video.add_text.gd
|
||||
# Add a scrolling line of text on video frames.
|
||||
# @category Source / Video Processing
|
||||
# @param ~id Force the value of the source ID.
|
||||
# @param ~color Text color (in 0xRRGGBB format).
|
||||
# @param ~cycle Cycle text.
|
||||
# @param ~font Path to ttf font file.
|
||||
# @param ~metadata Change text on a particular metadata \
|
||||
# (empty string means disabled).
|
||||
# @param ~size Font size.
|
||||
# @param ~speed Speed in pixels per second (0 means no scrolling).
|
||||
# @param ~x x offset (negative means from right).
|
||||
# @param ~y y offset (negative means from bottom).
|
||||
# @param text Text to display.
|
||||
def video.add_text(~id="",~color=16777215,~cycle=true,
|
||||
~font=configure.default_font,
|
||||
~metadata="",~size=18,~speed=70,~x=-1,~y=-5,
|
||||
text,source)
|
||||
video.add_text.gd(id=id,color=color,cycle=cycle,font=font,metadata=metadata,
|
||||
size=size,speed=speed,x=x,y=y,text,source)
|
||||
end
|
||||
%endif
|
||||
|
||||
%ifdef video.add_text.sdl
|
||||
# Add a scrolling line of text on video frames.
|
||||
# @category Source / Video Processing
|
||||
# @param ~id Force the value of the source ID.
|
||||
# @param ~color Text color (in 0xRRGGBB format).
|
||||
# @param ~cycle Cycle text.
|
||||
# @param ~font Path to ttf font file.
|
||||
# @param ~metadata Change text on a particular metadata \
|
||||
# (empty string means disabled).
|
||||
# @param ~size Font size.
|
||||
# @param ~speed Speed in pixels per second (0 means no scrolling).
|
||||
# @param ~x x offset (negative means from right).
|
||||
# @param ~y y offset (negative means from bottom).
|
||||
# @param text Text to display.
|
||||
def video.add_text(~id="",~color=16777215,~cycle=true,
|
||||
~font=configure.default_font,
|
||||
~metadata="",~size=18,~speed=70,~x=-1,~y=-5,
|
||||
text,source)
|
||||
video.add_text.sdl(id=id,color=color,cycle=cycle,font=font,metadata=metadata,
|
||||
size=size,speed=speed,x=x,y=y,text,source)
|
||||
end
|
||||
%endif
|
|
@ -38,7 +38,7 @@ setup(name='airtime-playout',
|
|||
author='sourcefabric',
|
||||
license='AGPLv3',
|
||||
packages=['pypo', 'pypo.media', 'pypo.media.update',
|
||||
'liquidsoap', 'liquidsoap.library'],
|
||||
'liquidsoap'],
|
||||
package_data={'': ['*.liq', '*.cfg', '*.types']},
|
||||
scripts=[
|
||||
'bin/airtime-playout',
|
||||
|
|
Loading…
Reference in New Issue