Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
Lars Kruse
aura-engine
Commits
80690b1b
Commit
80690b1b
authored
May 15, 2020
by
David Trattnig
Browse files
HTTPS Stream Functionality.
parent
cdae513f
Changes
12
Hide whitespace changes
Inline
Side-by-side
configuration/sample-development.engine.ini
View file @
80690b1b
...
...
@@ -68,6 +68,12 @@ fetching_frequency=3600
# contents which take longer to load (big files, bad connectivity to streams etc.)
preload_offset
=
60
# Sometimes it might take longer to get a stream connect. Here you can define a viable length.
# But note, that this may affect the preload time (see `preload_offset`), hence affecting the
# overall playout, it's delays and possible fallbacks
stream_connect_retry_delay
=
1
stream_connect_max_retries
=
10
# sets the time how long we have to fade in and out, when we select another mixer input
# values are in seconds
# this is solved on engine level because it is kind of tough with liquidsoap
...
...
@@ -75,6 +81,8 @@ preload_offset=60
fade_in_time
=
"0.5"
fade_out_time
=
"2.5"
#######################
# Liquidsoap Settings #
#######################
...
...
configuration/sample-docker.engine.ini
View file @
80690b1b
...
...
@@ -68,6 +68,12 @@ fetching_frequency=3600
# contents which take longer to load (big files, bad connectivity to streams etc.)
preload_offset
=
60
# Sometimes it might take longer to get a stream connect. Here you can define a viable length.
# But note, that this may affect the preload time (see `preload_offset`), hence affecting the
# overall playout, it's delays and possible fallbacks
stream_connect_retry_delay
=
1
stream_connect_max_retries
=
10
# sets the time how long we have to fade in and out, when we select another mixer input
# values are in seconds
# this is solved on engine level because it is kind of tough with liquidsoap
...
...
configuration/sample-production.engine.ini
View file @
80690b1b
...
...
@@ -68,6 +68,12 @@ fetching_frequency=3600
# contents which take longer to load (big files, bad connectivity to streams etc.)
preload_offset
=
60
# Sometimes it might take longer to get a stream connect. Here you can define a viable length.
# But note, that this may affect the preload time (see `preload_offset`), hence affecting the
# overall playout, it's delays and possible fallbacks
stream_connect_retry_delay
=
1
stream_connect_max_retries
=
10
# sets the time how long we have to fade in and out, when we select another mixer input
# values are in seconds
# this is solved on engine level because it is kind of tough with liquidsoap
...
...
modules/base/enum.py
View file @
80690b1b
...
...
@@ -52,6 +52,8 @@ class Channel(Enum):
FILESYSTEM_B
=
"in_filesystem_1"
HTTP_A
=
"in_http_0"
HTTP_B
=
"in_http_1"
HTTPS_A
=
"in_https_0"
HTTPS_B
=
"in_https_1"
LIVE_0
=
"aura_linein_0"
LIVE_1
=
"aura_linein_1"
LIVE_2
=
"aura_linein_2"
...
...
@@ -74,6 +76,10 @@ class ChannelType(Enum):
"id"
:
"http"
,
"channels"
:
[
Channel
.
HTTP_A
,
Channel
.
HTTP_A
]
}
HTTPS
=
{
"id"
:
"https"
,
"channels"
:
[
Channel
.
HTTPS_A
,
Channel
.
HTTPS_A
]
}
LIVE
=
{
"id"
:
"live"
,
"channels"
:
[
...
...
modules/base/exceptions.py
View file @
80690b1b
...
...
@@ -33,7 +33,10 @@ class NoActiveScheduleException(Exception):
pass
# Mixer Exceptions
# Soundsystem and Mixer Exceptions
class
LoadSourceException
(
Exception
):
pass
class
InvalidChannelException
(
Exception
):
pass
...
...
modules/base/utils.py
View file @
80690b1b
...
...
@@ -47,6 +47,8 @@ class EngineUtil:
"""
if
uri
.
startswith
(
"https"
):
return
ChannelType
.
HTTPS
if
uri
.
startswith
(
"http"
):
return
ChannelType
.
HTTP
if
uri
.
startswith
(
"pool"
)
or
uri
.
startswith
(
"playlist"
)
or
uri
.
startswith
(
"file"
):
...
...
modules/communication/liquidsoap/playerclient.py
View file @
80690b1b
...
...
@@ -115,7 +115,7 @@ class LiquidSoapPlayerClient(LiquidSoapClient):
# Stream
#
def
http
_set_url
(
self
,
channel
,
url
):
def
stream
_set_url
(
self
,
channel
,
url
):
"""
Sets the URL on the given HTTP channel.
"""
...
...
@@ -123,7 +123,7 @@ class LiquidSoapPlayerClient(LiquidSoapClient):
return
self
.
message
def
http
_start
(
self
,
channel
):
def
stream
_start
(
self
,
channel
):
"""
Starts the HTTP stream set with `stream_set_url` on the given channel.
"""
...
...
@@ -131,7 +131,7 @@ class LiquidSoapPlayerClient(LiquidSoapClient):
return
self
.
message
def
http
_stop
(
self
,
channel
):
def
stream
_stop
(
self
,
channel
):
"""
Stops the HTTP stream on the given channel.
"""
...
...
@@ -139,7 +139,7 @@ class LiquidSoapPlayerClient(LiquidSoapClient):
return
self
.
message
def
http
_status
(
self
,
channel
):
def
stream
_status
(
self
,
channel
):
"""
Returns the status of the HTTP stream on the given channel.
"""
...
...
@@ -147,6 +147,10 @@ class LiquidSoapPlayerClient(LiquidSoapClient):
return
self
.
message
#
# Other
#
def
uptime
(
self
,
command
=
""
):
# no command will come
"""
Retrieves how long the engine is running already.
...
...
modules/core/engine.py
View file @
80690b1b
...
...
@@ -26,6 +26,11 @@ import time
import
logging
import
json
from
urllib.parse
import
urlparse
,
ParseResult
from
modules.base.enum
import
ChannelType
,
Channel
,
TransitionType
,
LiquidsoapResponse
,
EntryPlayState
from
modules.base.utils
import
TerminalColors
,
SimpleUtil
,
EngineUtil
from
modules.base.exceptions
import
LQConnectionError
,
InvalidChannelException
,
NoActiveEntryException
,
EngineMalfunctionException
,
LQStreamException
,
LoadSourceException
from
modules.communication.liquidsoap.playerclient
import
LiquidSoapPlayerClient
# from modules.communication.liquidsoap.recorderclient import LiquidSoapRecorderClient
from
modules.core.startup
import
StartupThread
...
...
@@ -33,10 +38,6 @@ from modules.core.state import PlayerStateService
from
modules.core.monitor
import
Monitoring
from
modules.communication.mail
import
AuraMailer
from
modules.base.enum
import
ChannelType
,
Channel
,
TransitionType
,
LiquidsoapResponse
,
EntryPlayState
from
modules.base.utils
import
TerminalColors
,
SimpleUtil
from
modules.base.exceptions
import
LQConnectionError
,
InvalidChannelException
,
NoActiveEntryException
,
EngineMalfunctionException
,
LQStreamException
class
SoundSystem
():
"""
...
...
@@ -87,6 +88,7 @@ class SoundSystem():
self
.
active_channel
=
{
ChannelType
.
FILESYSTEM
:
Channel
.
FILESYSTEM_A
,
ChannelType
.
HTTP
:
Channel
.
HTTP_A
,
ChannelType
.
HTTPS
:
Channel
.
HTTPS_A
,
ChannelType
.
LIVE
:
Channel
.
LIVE_0
}
# self.active_entries = {}
...
...
@@ -233,15 +235,8 @@ class SoundSystem():
self
.
playlist_push
(
entry
.
channel
,
entry
.
source
)
# STREAM
elif
entry
.
type
==
ChannelType
.
HTTP
:
self
.
http_load
(
entry
.
channel
,
entry
.
source
)
time
.
sleep
(
1
)
while
not
self
.
http_is_ready
(
entry
.
channel
,
entry
.
source
):
self
.
logger
.
info
(
"Loading Stream ..."
)
time
.
sleep
(
1
)
entry
.
status
=
EntryPlayState
.
READY
elif
entry
.
type
==
ChannelType
.
HTTP
or
entry
.
type
==
ChannelType
.
HTTPS
:
self
.
stream_load_entry
(
entry
)
# LIVE
else
:
...
...
@@ -357,13 +352,24 @@ class SoundSystem():
elif
channel_type
==
ChannelType
.
HTTP
:
if
active_channel
==
Channel
.
HTTP_A
:
channel
=
Channel
.
HTTP_B
msg
=
"Swapped
s
tream channel from A > B"
msg
=
"Swapped
HTTP S
tream channel from A > B"
else
:
channel
=
Channel
.
HTTP_A
msg
=
"Swapped stream channel from B > A"
msg
=
"Swapped HTTP Stream channel from B > A"
elif
channel_type
==
ChannelType
.
HTTPS
:
if
active_channel
==
Channel
.
HTTPS_A
:
channel
=
Channel
.
HTTPS_B
msg
=
"Swapped HTTPS Stream channel from A > B"
else
:
channel
=
Channel
.
HTTPS_A
msg
=
"Swapped HTTPS Stream channel from B > A"
if
msg
:
self
.
logger
.
info
(
SimpleUtil
.
pink
(
msg
))
return
channel
...
...
@@ -458,46 +464,84 @@ class SoundSystem():
# Channel Type - Stream
#
def
http_load
(
self
,
channel
,
url
):
def
stream_load_entry
(
self
,
entry
):
"""
Loads the given stream entry and updates the entries's status codes.
"""
# Hack to avoid Liquidsoap SSL Error "Connection failed:
# `SSL connection() error: error:1408F10B:SSL routines:ssl3_get_record:wrong version number`
# when passing HTTPS URLs without the port number.
# if EngineUtil.get_channel_type(entry.source) == ChannelType.HTTPS:
# old_url = urlparse(entry.source)
# new_url = ParseResult(scheme=old_url.scheme, netloc="{}:{}".format(old_url.hostname, 443),
# path=old_url.path, params=old_url.params, query=old_url.query, fragment=old_url.fragment)
# self.logger.warn("Replacing entry.source HTTPS URL '%s' with '%s'" % (str(old_url.geturl()), str(new_url.geturl())))
# entry.Source = new_url.geturl()
self
.
stream_load
(
entry
.
channel
,
entry
.
source
)
time
.
sleep
(
1
)
retry_delay
=
self
.
config
.
get
(
"input_stream_retry_delay"
)
max_retries
=
self
.
config
.
get
(
"input_stream_max_retries"
)
retries
=
0
while
not
self
.
stream_is_ready
(
entry
.
channel
,
entry
.
source
):
self
.
logger
.
info
(
"Loading Stream ..."
)
if
retries
>=
max_retries
:
raise
LoadSourceException
(
"Could not connect to stream while waiting for %s seconds!"
%
retries
*
retry_delay
)
time
.
sleep
(
retry_delay
)
retries
+=
1
entry
.
status
=
EntryPlayState
.
READY
def
stream_load
(
self
,
channel
,
url
):
"""
Preloads the stream URL on the given channel.
Preloads the stream URL on the given channel. Note this method is blocking
some serious amount of time; hence it's worth being called asynchroneously.
"""
result
=
None
self
.
enable_transaction
()
result
=
self
.
__send_lqc_command__
(
self
.
client
,
channel
,
"
http
_stop"
)
result
=
self
.
__send_lqc_command__
(
self
.
client
,
channel
,
"
stream
_stop"
)
if
result
!=
LiquidsoapResponse
.
SUCCESS
.
value
:
self
.
logger
.
error
(
"s
tream
.stop result:
"
+
result
)
self
.
logger
.
error
(
"
%
s.stop result:
%s"
%
(
channel
,
result
)
)
raise
LQStreamException
(
"Error while stopping stream!"
)
result
=
self
.
__send_lqc_command__
(
self
.
client
,
channel
,
"
http
_set_url"
,
url
)
result
=
self
.
__send_lqc_command__
(
self
.
client
,
channel
,
"
stream
_set_url"
,
url
)
if
result
!=
LiquidsoapResponse
.
SUCCESS
.
value
:
self
.
logger
.
error
(
"s
tream
.set_url result:
"
+
result
)
self
.
logger
.
error
(
"
%
s.set_url result:
%s"
%
(
channel
,
result
)
)
raise
LQStreamException
(
"Error while setting stream URL!"
)
# Liquidsoap ignores commands sent without a certain timeout
time
.
sleep
(
2
)
result
=
self
.
__send_lqc_command__
(
self
.
client
,
channel
,
"
http
_start"
)
self
.
logger
.
info
(
"s
tream
.start result:
"
+
result
)
result
=
self
.
__send_lqc_command__
(
self
.
client
,
channel
,
"
stream
_start"
)
self
.
logger
.
info
(
"
%
s.start result:
%s"
%
(
channel
,
result
)
)
self
.
disable_transaction
()
return
result
def
http
_is_ready
(
self
,
channel
,
url
):
def
stream
_is_ready
(
self
,
channel
,
url
):
"""
Checks if the stream on the given channel is ready to play.
Checks if the stream on the given channel is ready to play. Note this method is blocking
some serious amount of time even when successfull; hence it's worth being called asynchroneously.
"""
result
=
None
self
.
enable_transaction
()
result
=
self
.
__send_lqc_command__
(
self
.
client
,
channel
,
"
http
_status"
)
self
.
logger
.
info
(
"s
tream
.status result:
"
+
result
)
result
=
self
.
__send_lqc_command__
(
self
.
client
,
channel
,
"
stream
_status"
)
self
.
logger
.
info
(
"
%
s.status result:
%s"
%
(
channel
,
result
)
)
if
not
result
.
startswith
(
LiquidsoapResponse
.
STREAM_STATUS_CONNECTED
.
value
):
return
False
...
...
@@ -509,9 +553,9 @@ class SoundSystem():
self
.
disable_transaction
()
# Wait another 10 (!) seconds, because even now the old source might *still* be playing
self
.
logger
.
info
(
"Ready to play stream,
Liquidsoap wants you to wait another 10secs though..."
)
time
.
sleep
(
10
)
stream_buffer
=
self
.
config
.
get
(
"input_stream_buffer"
)
self
.
logger
.
info
(
"Ready to play stream,
but wait %s seconds until the buffer is filled..."
%
str
(
stream_buffer
)
)
time
.
sleep
(
round
(
float
(
stream_buffer
))
)
return
True
...
...
@@ -817,7 +861,7 @@ class SoundSystem():
# call wanted function ...
# FIXME REFACTOR all calls in a common way
if
command
in
[
"playlist_push"
,
"playlist_seek"
,
"playlist_clear"
,
"
http
_set_url"
,
"
http
_start"
,
"
http
_stop"
,
"
http
_status"
]:
if
command
in
[
"playlist_push"
,
"playlist_seek"
,
"playlist_clear"
,
"
stream
_set_url"
,
"
stream
_start"
,
"
stream
_stop"
,
"
stream
_status"
]:
func
=
getattr
(
lqs_instance
,
command
)
result
=
func
(
str
(
namespace
),
*
args
)
else
:
...
...
modules/database/model.py
View file @
80690b1b
...
...
@@ -746,6 +746,8 @@ class SingleEntry(DB.Model, AuraDatabaseModel):
return
Channel
.
FILESYSTEM_A
elif
type
==
ChannelType
.
HTTP
:
return
Channel
.
HTTP_A
elif
type
==
ChannelType
.
HTTPS
:
return
Channel
.
HTTPS_A
else
:
return
"foo:bar"
#FIXME Extend & finalize!!
...
...
modules/liquidsoap/engine.liq
View file @
80690b1b
...
...
@@ -50,7 +50,7 @@ inputs = ref []
%include "in_soundcard.liq"
# fill the mixer
mixer = mix(id="mixer", list.append([input_filesystem_0, input_filesystem_1, input_http_0, input_http_1], !inputs))
mixer = mix(id="mixer", list.append([input_filesystem_0, input_filesystem_1, input_http_0, input_http_1
, input_https_0, input_https_1
], !inputs))
# mixer = mix(id="mixer", list.append([input_filesystem_0, input_filesystem_1, input_filesystem_2, input_filesystem_3, input_filesystem_4, input_http_0, input_http_1], !inputs))
...
...
modules/liquidsoap/in_stream.liq
View file @
80690b1b
...
...
@@ -22,15 +22,30 @@
# along with engine. If not, see <http://www.gnu.org/licenses/>.
#
# this is overwritten as soon as a streamovertake is programmed, but liquidsoap needs it to initialize this input
#starturl = "http://stream.fro.at/fro-128.ogg"
#starturl = "http://trance.out.airtime.pro:8000/trance_a"
starturl = "http://chill.out.airtime.pro:8000/chill_a"
# Pre-population of stream input sources, as Liquidsoap needs it to initialize this input.
# This is overwritten as soon as some other Stream is scheduled.
input_http_0 = input.http(id="in_http_0", buffer=10.0, max=60.0, timeout=60.0, starturl)
input_http_1 = input.http(id="in_http_1", buffer=10.0, max=60.0, timeout=60.0, starturl)
# http_starturl = "http://stream01.kapper.net:8001/live.mp3"
# http_starturl = "http://stream.fro.at/fro-128.ogg"
http_starturl = "http://trance.out.airtime.pro:8000/trance_a"
# http_starturl = "http://chill.out.airtime.pro:8000/chill_a"
# http_starturl = "http://212.89.182.114:8008/frf"
https_starturl = "https://securestream.o94.at/live.mp3"
# https_starturl = "https://live.helsinki.at:8088/live160.ogg"
# https_starturl = "https://stream.fro.at/fro-128.ogg"
input_http_0 = input.http(id="in_http_0", buffer=3.0, max=60.0, timeout=60.0, autostart=false, http_starturl)
input_http_1 = input.http(id="in_http_1", buffer=3.0, max=60.0, timeout=60.0, autostart=false, http_starturl)
input_https_0 = input.https(id="in_https_0", buffer=3.0, max=60.0, timeout=60.0, autostart=false, https_starturl)
input_https_1 = input.https(id="in_https_1", buffer=3.0, max=60.0, timeout=60.0, autostart=false, https_starturl)
# Route input stream to an dummy output to avoid buffer-overrun messages
output.dummy(id="SPAM_STREAM_OUTPUT_0", fallible=true, input_http_0)
output.dummy(id="SPAM_STREAM_OUTPUT_1", fallible=true, input_http_1)
\ No newline at end of file
# output.dummy(id="SPAM_HTTP_OUTPUT_0", fallible=true, input_http_0)
# output.dummy(id="SPAM_HTTP_OUTPUT_1", fallible=true, input_http_1)
# output.dummy(id="SPAM_HTTPS_OUTPUT_0", fallible=true, input_https_0)
# output.dummy(id="SPAM_HTTPS_OUTPUT_1", fallible=true, input_https_1)
# output.dummy(blank())
\ No newline at end of file
modules/scheduling/scheduler.py
View file @
80690b1b
...
...
@@ -36,7 +36,7 @@ from operator import attrgetter
from
modules.database.model
import
AuraDatabaseModel
,
Schedule
,
Playlist
,
PlaylistEntry
,
PlaylistEntryMetaData
,
SingleEntry
,
SingleEntryMetaData
,
TrackService
from
modules.base.exceptions
import
NoActiveScheduleException
,
NoActiveEntryException
from
modules.base.exceptions
import
NoActiveScheduleException
,
NoActiveEntryException
,
LoadSourceException
from
modules.base.enum
import
Channel
,
ChannelType
,
TimerType
,
TransitionType
,
EntryQueueState
,
EntryPlayState
from
modules.base.utils
import
SimpleUtil
,
TerminalColors
from
modules.communication.redis.messenger
import
RedisMessenger
...
...
@@ -215,12 +215,12 @@ class AuraScheduler(threading.Thread):
self
.
soundsystem
.
disable_transaction
()
self
.
logger
.
info
(
"LiquidSoap seek response: "
+
response
)
elif
active_entry
.
type
==
ChannelType
.
HTTP
:
elif
active_entry
.
type
==
ChannelType
.
HTTP
or
active_entry
.
type
==
ChannelType
.
HTTPS
:
# Load and play active entry
self
.
soundsystem
.
load
(
active_entry
)
self
.
soundsystem
.
play
(
active_entry
,
TransitionType
.
FADE
)
self
.
queue_end_of_schedule
(
active_entry
,
True
)
elif
active_entry
.
type
==
ChannelType
.
LIVE
:
self
.
logger
.
warn
(
"LIVE ENTRIES ARE NOT YET IMPLEMENTED!"
)
...
...
@@ -776,7 +776,11 @@ class AuraScheduler(threading.Thread):
# Load function to be called by timer
def
do_load
(
entry
):
self
.
logger
.
info
(
SimpleUtil
.
cyan
(
"=== load('%s') ==="
%
entry
))
self
.
soundsystem
.
load
(
entry
)
try
:
self
.
soundsystem
.
load
(
entry
)
except
LoadSourceException
as
e
:
self
.
logger
(
"Could not load entry %s:"
%
str
(
entry
),
e
)
# TODO Fallback logic here
loader_diff
=
diff
-
self
.
config
.
get
(
"preload_offset"
)
loader
=
CallFunctionTimer
(
loader_diff
,
do_load
,
parameters
,
fadein
,
fadeout
,
switcher
)
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment