Source code for easyplayer.audio

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 MediaPlayer(Component): """ Base class for Audio and Video players containg common functionality """ def init(self, config): self.logger = getLogger(__name__) self.config = config self.data_dir = Path(config['data_dir']).expanduser() self.states = {} self.info = None self.stream = None self.enabled = False self.has_initial_data = True self.initial_data_counter = 0 def _init_info(self): pass def _check_program(self): if self.stream and self.has_initial_data: return True program_data = ProgramData(self.data_dir) initial_data = InitialProgramData(self.data_dir) if initial_data.exists(): self.has_initial_data = True self.logger.info('Loaded initial data') try: self.stream = MediaStream(program_data, initial_data) except ProgramData.DataNotPresent: return False else: self.logger.info('Loaded program data') return True def _check_info(self): if not self.info: try: self.info = PlayerInfo(self.data_dir) except PlayerInfo.DataNotPresent: return False else: if not self.info: return False self._init_info() return True def _schedule_interrupts(self): """ schedule interruption by media type Event """ pass def _show_timers(self): for c in self.components: if c.name == 'Timer' and c.remaining > 0: arg = c.event.args[0] if len(c.event.args) > 0 else '' print('Timer {} - {}: {}'.format(c.event.name, arg, c.remaining)) def _stop_timers(self): """ unregister relevant timers """ for c in self.components: if c.name == 'Timer' and c.event.name in ('play_next', 'schedule', 'fade_out', 'fade_in'): c.unregister() def _schedule_event(self, delta, event_class, player_name, msg='', **kw): """ schedule event in delta seconds """ # import sys; print('_schedule_event called from {}'.format(sys._getframe().f_back.f_code.co_name)) if msg: tm = time() + delta self.logger.info('%s next %s in %s seconds at %s.', player_name, msg, round(delta,2), time_format(to_time(tm))) event = event_class(player_name, **kw) timer = Timer(delta, event, self.channel).register(self) # self._show_timers() return timer def _get_other(self, channel): return [x for x in self.player_channels if x != channel][0] def _is_running(self): for c in self.components: if c.name == 'Timer'\ and c.event.name in ('play_next', 'schedule', 'fade_in')\ and c.remaining > 0: return True return False
[docs] def check_start(self): """ check if everything is ready to start, repeat if not """ self.logger.info('%s: Checking starting conditions', self.channel) success = True if not self._check_info(): self.logger.warning('Missing player info.') self.fire(get_info()) success = False if not self._check_program(): self.logger.warning('Missing program data.') self.fire(get_program(initial=True)) success = False elif not self.has_initial_data and self.initial_data_counter < 600: # 30s self.logger.warning('Waiting for initial data (%d).', self.initial_data_counter) self.initial_data_counter += 1 self.fire(get_program(initial=True)) success = False for channel in self.player_channels: player = getattr(self, channel) if not player.connected: self.fire(start_player_server(), channel) self.logger.info('%s not connected', channel.title()) success = False if success: self.start_playing() else: Timer(CHECK_START_INTERVAL, check_start(), self.channel).register(self)
[docs] def new_info(self): """ handle new info from server """ self.logger.info('reload info') if self.info: self.info.reload() self._init_info() self._schedule_interrupts()
[docs] def check_status(self, player_name): """ check players status """ # self.logger.info('%s check status', player_name) player = getattr(self, player_name) s = player.get_status() if s.status == 'playing': self.fire(set_status('playing')) self.logger.info('%s checking status: %s', player_name, s.status) else: self.logger.warning('%s checking status: %s', player_name, s.status) if not self._is_running(): Timer(CHECK_START_INTERVAL, check_start(), self.channel).register(self)
[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()