from logging import getLogger
from time import time
from circuits import Component, Event
from path import Path
from .components.timer import Timer
from .events import (get_info, get_program, log_song, no_song_to_play, set_status)
from .players.cmus import CmusRemoteClient
from .players.events import start_player_server, play, stop
from .settings import CHECK_START_INTERVAL, SCHEDULE_NEXT_AUDIO_INTERVAL, CHECK_STATUS_INTERVAL
from .storage import ProgramData, InitialProgramData
from .utils import now, to_time, time_format
from .utils.info import PlayerInfo
from .utils.stream import MediaStream
# local events
[docs]class check_start(Event):
""" check all start conditions """
[docs]class check_players(Event):
""" check if players are running """
complete = True
[docs]class start_playing(Event):
""" start playing """
[docs]class schedule(Event):
""" get position and schedule next start """
[docs]class fade_out(Event):
""" start fading out current player until stop """
[docs]class fade_in(Event):
""" start volume fade in """
[docs]class play_next(Event):
""" play next media """
[docs]class interrupt(Event):
""" interrupt Event """
[docs]class not_allowed(Event):
""" not allowed to play """
[docs]class check_status(Event):
""" check status """
[docs]class AudioPlayer(MediaPlayer):
""" Audio player communicates with 2 cmus players, schedules switching of songs with crossfade """
channel = 'audio'
def init(self, config):
super().init(config)
self.player_channels = ('player_a', 'player_b')
self.player_a = CmusRemoteClient(data_dir=self.data_dir,
channel='player_a').register(self)
self.player_b = CmusRemoteClient(data_dir=self.data_dir,
channel='player_b').register(self)
self.schedule_timer = None
self.fadeout_timer = None
self.play_next_timer = None
self.item_timers = {}
self.current_song = None
self.next_song = None
self.has_initial_data = False
def _init_info(self):
self.fadein_offset = 0
self.fadein = self.info.cfd
self.fade_overlap = self.info.cfo
self.fadeout_offset = self.info.cof
self.fadeout = self.info.cfd
def _set_timing(self):
song = self.current_song
end_offset = getattr(song, 'end_offset', 0) if song else 0
self.fadeout_offset = end_offset
if song and song.item.type == 'mu':
self.fadeout = self.info.cfd
self.fadeout_offset += self.info.cof
else:
# for current spots and events play until the end of the spot
self.fadeout = 0
song = self.next_song
start_offset = getattr(song, 'start_offset', 0) if song else 0
self.fadein_offset = start_offset
if song and song.item.type == 'mu':
self.fadein = self.info.cfd
self.fade_overlap = self.info.cfo
else:
# for next spots and events start playing immediately at full volume
self.fadein = 0
self.fade_overlap = 0
def started(self, component):
if component.name == 'App':
self.logger.info('started: %r', self)
yield self.call(start_player_server(), *self.player_channels)
def enable_player(self):
self.enabled = True
self.logger.info('Enabled audio player')
self.fire(check_start())
def disable_player(self):
self.enabled = False
self.logger.info('Disabling audio player')
def connected(self, player_name):
self.logger.info('%s connected', player_name)
def _schedule_interrupts(self):
if self.stream:
for event_item in self.stream.events:
event_item.parse_play_at(self.info)
delta = event_item.play_at_time - now()
interval = delta.total_seconds()
if interval > 0:
name = event_item.name
tid = event_item.id
tm = event_item.play_at_time.time()
self.logger.info('Scheduling item: %s in %ss at %s',
name, round(interval), time_format(tm))
timer = self.item_timers.get(tid, None)
if timer:
timer.reset(interval)
else:
self.item_timers[tid] = Timer(interval, interrupt(event_item), self.channel).register(self)
#for tid, timer in self.item_timers.items():
# self.logger.info('Scheduled item: %s in %ss', tid, timer.remaining)
def _get_next_fadeout_start(self, duration, position):
delta = duration - position - self.fadeout - self.fadeout_offset
return max(delta, 0.1)
def _get_next_fadein_start(self, duration, position):
""" get time delta till next player start """
delta = duration - position - self.fadeout_offset - self.fade_overlap - self.fadein_offset
return max(delta, 0.1)
[docs] def start_playing(self):
""" starting audio - start scheduling """
self.logger.info('Audio player starting')
if self.stream.n_audio > 0:
self._schedule_interrupts()
if not self._is_running():
delta = 0.1
player_name = 'player_b'
self._schedule_event(delta, schedule, player_name, msg='schedule')
else:
self.logger.info('No audio files in program')
self.fire(no_song_to_play())
self.fire(set_status('no_song'))
def _get_next_song(self):
try:
song = self.stream.next_song()
except Exception as e:
self.logger.error(e)
song = None
return song
def _get_status(self):
for player_name in self.player_channels:
player = getattr(self, player_name)
self.states[player_name] = player.get_status()
[docs] def schedule(self, player_name, skip_next_song=False):
""" schedule next fade out at the end of the song, fade in of next song and checking status in between """
self.logger.info('Scheduling %s', player_name)
if not skip_next_song:
self.current_song = self.next_song
self.next_song = self._get_next_song()
self._set_timing()
self._get_status()
cs = self.states[player_name]
next_player_name = self._get_other(player_name)
ns = self.states[next_player_name]
self.logger.info(cs)
if cs.status == 'playing':
# current player playing
if ns.status == 'stopped':
# schedule fadout and next play
fo_delta = self._get_next_fadeout_start(cs.duration, cs.position)
self._schedule_event(fo_delta, fade_out, cs.player, msg='fade out')
np_delta = self._get_next_fadein_start(cs.duration, cs.position)
self._schedule_event(np_delta, play_next, next_player_name, msg='playing')
# calculate intervals for checking status until next fade out
if int(fo_delta) > CHECK_STATUS_INTERVAL:
for i in range(int(fo_delta) // CHECK_STATUS_INTERVAL):
sd = (i+1) * CHECK_STATUS_INTERVAL
self._schedule_event(sd, check_status, player_name)
# one more check after next schedule event
delta = np_delta + self.fadein_offset + self.fade_overlap + self.fadein + 2 * SCHEDULE_NEXT_AUDIO_INTERVAL
self._schedule_event(delta, check_status, next_player_name)
else:
# both players are playing: skip
self.logger.info(ns)
self.logger.warning('%s still playing', next_player_name)
self._schedule_event(SCHEDULE_NEXT_AUDIO_INTERVAL,
schedule,
player_name,
msg='schedule',
skip_next_song=True)
else:
# next player is playing: switch
self.logger.info(ns)
if ns.status == 'playing':
# reschedule with other player
self.logger.warning('%s playing instead', next_player_name)
self._schedule_event(SCHEDULE_NEXT_AUDIO_INTERVAL,
schedule,
next_player_name,
msg='schedule',
skip_next_song=True)
else:
# both players stopped: start playing
self.logger.warning('Both players stopped, restarting')
self._schedule_event(0.1, play_next, next_player_name, msg='playing')
[docs] def fade_out(self, player_name):
""" start fading out current player """
self.logger.info('%s fading out', player_name)
status = self.states.get(player_name, None)
kw = {'duration': self.fadeout}
if status:
kw['volume'] = status.volume
self.fire(stop(**kw), player_name)
[docs] def play_next(self, player_name):
""" start playing next song """
self.logger.info('%s playing next song', player_name)
if not self.enabled:
self.fire(not_allowed())
return
song = self.next_song
if song is None:
self.logger.warning('Next song is none')
self.fire(no_song_to_play())
self.fire(set_status('no_song'))
else:
self.fire(play(song, volume=0, duration=0), player_name)
delta = max(self.fadein_offset, 0.1)
self._schedule_event(delta, fade_in, player_name, msg='fade in')
# announce song to server
self.fire(log_song(song))
self.fire(set_status('playing'))
[docs] def fade_in(self, player_name):
""" fade in volume """
self.logger.info('%s fading in', player_name)
self.fire(play(None, duration=self.fadein), player_name)
delta = self.fadein + SCHEDULE_NEXT_AUDIO_INTERVAL
self._schedule_event(delta, schedule, player_name, msg='schedule')
[docs] def interrupt(self, event_item):
""" play time based Event item (not circuits event)"""
self.logger.info('Event %s timed.', event_item)
self._stop_timers()
self._get_status()
a = self.states['player_a']
b = self.states['player_b']
if a.status == 'playing' and b.status == 'playing':
# wait for transition finish
timer = self.item_timers.get(event_item.id, None)
if timer:
delta = max(self.fadein, self.fadeout, self.fade_overlap, 3)
timer.reset(delta)
return
elif a.status == 'playing' or b.status == 'playing':
player_name = [k for k,v in self.states.items() if v.status == 'playing'][0]
self.fadeout = self.info.cfd
self.fire(fade_out(player_name))
delta = self.fadeout
else:
player_name = 'player_b'
delta = 0.1
# enqueue Event item
self.stream.enqueue_event(event_item)
# and reschedule
self.next_song = self._get_next_song()
self._set_timing()
next_player_name = self._get_other(player_name)
self._schedule_event(delta, play_next, next_player_name, msg='playing')
# clean
self.item_timers.pop(event_item.id, None)
[docs] def not_allowed(self):
""" send not allowed status to web server """
self.logger.warning('Not allowed to play.')
self.fire(set_status('not_allowed'))
[docs] def reload_programs(self):
""" handle reloading of programs """
self.logger.info('reloading programs')
if self.stream:
self.stream.reload()
if self._is_running():
# schedule event items in program
self._schedule_interrupts()
else:
delta = 0.3
self.logger.info('Starting playing after programs reload in %ss', delta)
Timer(delta, start_playing(), self.channel).register(self)
def time_adjusted(self):
self.logger.info('Rescheduling events because of adjusted time')
self._schedule_interrupts()
def timezone_adjusted(self):
self.logger.info('Rescheduling events because of adjusted timezone')
self._schedule_interrupts()