Commit 80690b1b authored by David Trattnig's avatar David Trattnig
Browse files

HTTPS Stream Functionality.

parent cdae513f
......@@ -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 #
#######################
......
......@@ -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
......
......@@ -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
......
......@@ -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": [
......
......@@ -33,7 +33,10 @@ class NoActiveScheduleException(Exception):
pass
# Mixer Exceptions
# Soundsystem and Mixer Exceptions
class LoadSourceException(Exception):
pass
class InvalidChannelException(Exception):
pass
......
......@@ -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"):
......
......@@ -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.
......
......@@ -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 stream channel from A > B"
msg = "Swapped HTTP Stream 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("stream.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("stream.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("stream.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("stream.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:
......
......@@ -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!!
......
......@@ -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))
......
......@@ -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
......@@ -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)
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment