class VideoPlayer: """ plays a track using omxplayer See pp_imageplayer for common software design description """ _CLOSED = "omx_closed" #probably will not exist _STARTING = "omx_starting" #track is being prepared _PLAYING = "omx_playing" #track is playing to the screen, may be paused _ENDING = "omx_ending" #track is in the process of ending due to quit or end of track # *************************************** # EXTERNAL COMMANDS # *************************************** def __init__(self, show_id, root, canvas, show_params, track_params , pp_dir, pp_home, pp_profile): self.mon=Monitor() self.mon.on() #instantiate arguments self.show_id=show_id self.root=root self.canvas = canvas self.show_params=show_params self.track_params=track_params self.pp_dir=pp_dir self.pp_home=pp_home self.pp_profile=pp_profile # get config from medialist if there. if self.track_params['omx-audio']<>"": self.omx_audio= self.track_params['omx-audio'] else: self.omx_audio= self.show_params['omx-audio'] if self.omx_audio<>"": self.omx_audio= "-o "+ self.omx_audio if self.track_params['omx-volume']<>"": self.omx_volume= self.track_params['omx-volume'] else: self.omx_volume= self.show_params['omx-volume'] if self.omx_volume<>"": self.omx_volume= "--vol "+ str(int(self.omx_volume)*100) + ' ' if self.track_params['omx-window']<>'': self.omx_window= self.track_params['omx-window'] else: self.omx_window= self.show_params['omx-window'] # get background image from profile. self.background_file='' if self.track_params['background-image']<>"": self.background_file= self.track_params['background-image'] else: if self.track_params['display-show-background']=='yes': self.background_file= self.show_params['background-image'] # get background colour from profile. if self.track_params['background-colour']<>"": self.background_colour= self.track_params['background-colour'] else: self.background_colour= self.show_params['background-colour'] self.centre_x = int(self.canvas['width'])/2 self.centre_y = int(self.canvas['height'])/2 #get animation instructions from profile self.animate_begin_text=self.track_params['animate-begin'] self.animate_end_text=self.track_params['animate-end'] # open the plugin Manager self.pim=PluginManager(self.show_id,self.root,self.canvas,self.show_params,self.track_params,self.pp_dir,self.pp_home,self.pp_profile) #create an instance of PPIO so we can create gpio events self.ppio = PPIO() # could put instance generation in play, not sure which is better. self.omx=OMXDriver(self.canvas) self.tick_timer=None self.init_play_state_machine() def play(self, track, showlist, end_callback, ready_callback, enable_menu=False): #instantiate arguments self.track=track self.showlist=showlist self.ready_callback=ready_callback #callback when ready to play self.end_callback=end_callback # callback when finished self.enable_menu = enable_menu # callback to the calling object to e.g remove egg timer and enable click areas. if self.ready_callback<>None: self.ready_callback() # create an instance of showmanager so we can control concurrent shows self.show_manager=ShowManager(self.show_id,self.showlist,self.show_params,self.root,self.canvas,self.pp_dir,self.pp_profile,self.pp_home) #set up video window reason,message,comand,has_window,x1,y1,x2,y2= self.parse_window(self.omx_window) if reason =='error': self.mon.err(self,'omx window error: ' + message + ' in ' + self.omx_window) self.end_callback(reason,message) else: if has_window==True: self.omx_window= '--win " '+ str(x1) + ' ' + str(y1) + ' ' + str(x2) + ' ' + str(y2) + ' " ' else: self.omx_window='' # Control other shows at beginning reason,message=self.show_manager.show_control(self.track_params['show-control-begin']) if reason in ('error','killed'): self.end_callback(reason,message) self=None else: #display content reason,message=self.display_content() if reason == 'error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # create animation events reason,message=self.ppio.animate(self.animate_begin_text,id(self)) if reason=='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # start playing the video. if self.play_state == VideoPlayer._CLOSED: self.mon.log(self,">play track received") self.start_play_state_machine(self.track) else: self.mon.err(self,'play track rejected') self.end_callback('error','play track rejected') self=None def terminate(self,reason): # circumvents state machine and does not wait for omxplayer to close if self.omx<>None: self.mon.log(self,"sent terminate to omxdriver") self.omx.terminate(reason) self.end('killed',' end without waiting for omxplayer to finish') # end without waiting else: self.mon.log(self,"terminate, omxdriver not running") self.end('killed','terminate, mplayerdriver not running') def input_pressed(self,symbol): if symbol[0:4]=='omx-': self.control(symbol[4]) elif symbol =='pause': self.pause() elif symbol=='stop': self.stop() else: pass def get_links(self): return self.track_params['links'] # *************************************** # INTERNAL FUNCTIONS # *************************************** # respond to normal stop def stop(self): # send signal to stop the track to the state machine self.mon.log(self,">stop received") self.quit_signal=True #toggle pause def pause(self): if self.play_state in (VideoPlayer._PLAYING,VideoPlayer._ENDING): self.omx.pause() return True else: self.mon.log(self,"!<pause rejected") return False # other control when playing def control(self,char): if self.play_state==VideoPlayer._PLAYING and char not in ('q'): self.mon.log(self,"> send control to omx: "+ char) self.omx.control(char) return True else: self.mon.log(self,"!<control rejected") return False # *********************** # sequencing # ********************** """self. play_state controls the playing sequence, it has the following values. I am not entirely sure the starting and ending states are required. - _closed - the omx process is not running, omx process can be initiated - _starting - omx process is running but is not yet able to receive controls - _playing - playing a track, controls can be sent - _ending - omx is doing its termination, controls cannot be sent """ def init_play_state_machine(self): self.quit_signal=False self.play_state=VideoPlayer._CLOSED def start_play_state_machine(self,track): #initialise all the state machine variables #self.iteration = 0 # for debugging self.quit_signal=False # signal that user has pressed stop self.play_state=VideoPlayer._STARTING #play the selected track options=self.omx_audio+ " " + self.omx_volume + ' ' + self.omx_window + ' ' + self.show_params['omx-other-options']+" " self.omx.play(track,options) self.mon.log (self,'Playing track from show Id: '+ str(self.show_id)) # and start polling for state changes self.tick_timer=self.canvas.after(50, self.play_state_machine) def play_state_machine(self): if self.play_state == VideoPlayer._CLOSED: self.mon.log(self," State machine: " + self.play_state) return elif self.play_state == VideoPlayer._STARTING: self.mon.log(self," State machine: " + self.play_state) # if omxplayer is playing the track change to play state if self.omx.start_play_signal==True: self.mon.log(self," <start play signal received from omx") self.omx.start_play_signal=False self.play_state=VideoPlayer._PLAYING self.mon.log(self," State machine: omx_playing started") self.tick_timer=self.canvas.after(50, self.play_state_machine) elif self.play_state == VideoPlayer._PLAYING: # self.mon.log(self," State machine: " + self.play_state) # service any queued stop signals if self.quit_signal==True: self.mon.log(self," Service stop required signal") self.stop_omx() self.quit_signal=False # self.play_state = VideoPlayer._ENDING # omxplayer reports it is terminating so change to ending state if self.omx.end_play_signal: self.mon.log(self," <end play signal received") self.mon.log(self," <end detected at: " + str(self.omx.video_position)) if self.omx.end_play_reason<>'nice_day': # deal with omxplayer not sending 'have a nice day' self.mon.warn(self," <end detected at: " + str(self.omx.video_position)) self.mon.warn(self," <pexpect reports: "+self.omx.end_play_reason) self.mon.warn(self,'pexpect.before is'+self.omx.xbefore) self.play_state = VideoPlayer._ENDING self.ending_count=0 self.tick_timer=self.canvas.after(200, self.play_state_machine) elif self.play_state == VideoPlayer._ENDING: self.mon.log(self," State machine: " + self.play_state) # if spawned process has closed can change to closed state self.mon.log (self," State machine : is omx process running? - " + str(self.omx.is_running())) if self.omx.is_running() ==False: self.mon.log(self," <omx process is dead") self.play_state = VideoPlayer._CLOSED self.end('normal','quit by user or system') else: self.ending_count+=1 if self.ending_count>10: # deal with omxplayer not terminating at the end of a track self.mon.warn(self," <omxplayer failed to close at: " + str(self.omx.video_position)) self.mon.warn(self,'pexpect.before is'+self.omx.xbefore) self.omx.kill() self.mon.warn(self,'omxplayer now terminated ') self.play_state = VideoPlayer._CLOSED self.end('normal','end from omxplayer failed to terminate') else: self.tick_timer=self.canvas.after(200, self.play_state_machine) def stop_omx(self): # send signal to stop the track to the state machine self.mon.log(self," >stop omx received from state machine") if self.play_state==VideoPlayer._PLAYING: self.omx.stop() return True else: self.mon.log(self,"!<stop rejected") return False # ***************** # ending the player # ***************** def end(self,reason,message): # stop the plugin if self.track_params['plugin']<>'': self.pim.stop_plugin() # os.system("xrefresh -display :0") # abort the timer if self.tick_timer<>None: self.canvas.after_cancel(self.tick_timer) self.tick_timer=None if reason in ('error','killed'): self.end_callback(reason,message) self=None else: # normal end so do show control and animation # Control concurrent shows at end reason,message=self.show_manager.show_control(self.track_params['show-control-end']) if reason =='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # clear events list for this track if self.track_params['animate-clear']=='yes': self.ppio.clear_events_list(id(self)) # create animation events for ending reason,message=self.ppio.animate(self.animate_begin_text,id(self)) if reason=='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: self.end_callback('normal',"track has terminated or quit") self=None # ***************** # displaying things # ***************** def display_content(self): #background colour if self.background_colour<>'': self.canvas.config(bg=self.background_colour) # delete previous content self.canvas.delete('pp-content') # background image if self.background_file<>'': self.background_img_file = self.complete_path(self.background_file) if not os.path.exists(self.background_img_file): self.mon.err(self,"Video background file not found: "+ self.background_img_file) self.end('error',"Video background file not found") else: pil_background_img=PIL.Image.open(self.background_img_file) self.background = PIL.ImageTk.PhotoImage(pil_background_img) self.drawn = self.canvas.create_image(int(self.canvas['width'])/2, int(self.canvas['height'])/2, image=self.background, anchor=CENTER, tag='pp-content') # execute the plugin if required if self.track_params['plugin']<>'': reason,message,self.track = self.pim.do_plugin(self.track,self.track_params['plugin'],) if reason <> 'normal': return reason,message # display show text if enabled if self.show_params['show-text']<> '' and self.track_params['display-show-text']=='yes': self.canvas.create_text(int(self.show_params['show-text-x']),int(self.show_params['show-text-y']), anchor=NW, text=self.show_params['show-text'], fill=self.show_params['show-text-colour'], font=self.show_params['show-text-font'], tag='pp-content') # display track text if enabled if self.track_params['track-text']<> '': self.canvas.create_text(int(self.track_params['track-text-x']),int(self.track_params['track-text-y']), anchor=NW, text=self.track_params['track-text'], fill=self.track_params['track-text-colour'], font=self.track_params['track-text-font'], tag='pp-content') # display instructions if enabled if self.enable_menu== True: self.canvas.create_text(int(self.show_params['hint-x']), int(self.show_params['hint-y']), text=self.show_params['hint-text'], fill=self.show_params['hint-colour'], font=self.show_params['hint-font'], anchor=NW, tag='pp-content') self.canvas.tag_raise('pp-click-area') self.canvas.update_idletasks( ) return 'normal','' # **************** # utilities # ***************** def complete_path(self,track_file): # complete path of the filename of the selected entry if track_file[0]=="+": track_file=self.pp_home+track_file[1:] self.mon.log(self,"Background image is "+ track_file) return track_file # original _ # warp _ or xy2 def parse_window(self,line): fields = line.split() # check there is a command field if len(fields) < 1: return 'error','no type field','',False,0,0,0,0 # deal with original which has 1 if fields[0]=='original': if len(fields) <> 1: return 'error','number of fields for original','',False,0,0,0,0 return 'normal','',fields[0],False,0,0,0,0 #deal with warp which has 1 or 5 arguments # check basic syntax if fields[0] <>'warp': return 'error','not a valid type','',False,0,0,0,0 if len(fields) not in (1,5): return 'error','wrong number of coordinates for warp','',False,0,0,0,0 # deal with window coordinates if len(fields) == 5: #window is specified if not (fields[1].isdigit() and fields[2].isdigit() and fields[3].isdigit() and fields[4].isdigit()): return 'error','coordinates are not positive integers','',False,0,0,0,0 has_window=True return 'normal','',fields[0],has_window,int(fields[1]),int(fields[2]),int(fields[3]),int(fields[4]) else: # fullscreen has_window=True return 'normal','',fields[0],has_window,0,0,self.canvas['width'],self.canvas['height']
class VideoPlayer: """ plays a track using omxplayer See pp_imageplayer for common software design description """ _CLOSED = "omx_closed" #probably will not exist _STARTING = "omx_starting" #track is being prepared _PLAYING = "omx_playing" #track is playing to the screen, may be paused _ENDING = "omx_ending" #track is in the process of ending due to quit or end of track # *************************************** # EXTERNAL COMMANDS # *************************************** def __init__(self, show_id, root, canvas, show_params, track_params , pp_dir, pp_home, pp_profile): self.mon=Monitor() self.mon.on() #instantiate arguments self.show_id=show_id self.root=root self.canvas = canvas self.show_params=show_params self.track_params=track_params self.pp_dir=pp_dir self.pp_home=pp_home self.pp_profile=pp_profile # get config from medialist if there. if self.track_params['omx-audio']<>"": self.omx_audio= self.track_params['omx-audio'] else: self.omx_audio= self.show_params['omx-audio'] if self.omx_audio<>"": self.omx_audio= "-o "+ self.omx_audio if self.track_params['omx-volume']<>"": self.omx_volume= self.track_params['omx-volume'] else: self.omx_volume= self.show_params['omx-volume'] if self.omx_volume<>"": self.omx_volume= "--vol "+ str(int(self.omx_volume)*100) + ' ' if self.track_params['omx-window']<>'': self.omx_window= self.track_params['omx-window'] else: self.omx_window= self.show_params['omx-window'] # get background image from profile. self.background_file='' if self.track_params['background-image']<>"": self.background_file= self.track_params['background-image'] else: if self.track_params['display-show-background']=='yes': self.background_file= self.show_params['background-image'] # get background colour from profile. if self.track_params['background-colour']<>"": self.background_colour= self.track_params['background-colour'] else: self.background_colour= self.show_params['background-colour'] self.centre_x = int(self.canvas['width'])/2 self.centre_y = int(self.canvas['height'])/2 #get animation instructions from profile self.animate_begin_text=self.track_params['animate-begin'] self.animate_end_text=self.track_params['animate-end'] # open the plugin Manager self.pim=PluginManager(self.show_id,self.root,self.canvas,self.show_params,self.track_params,self.pp_dir,self.pp_home,self.pp_profile) #create an instance of PPIO so we can create gpio events self.ppio = PPIO() # could put instance generation in play, not sure which is better. self.omx=OMXDriver(self.canvas) self.tick_timer=None self.init_play_state_machine() def play(self, track, showlist, end_callback, ready_callback, enable_menu=False): #instantiate arguments self.track=track self.showlist=showlist self.ready_callback=ready_callback #callback when ready to play self.end_callback=end_callback # callback when finished self.enable_menu = enable_menu # callback to the calling object to e.g remove egg timer and enable click areas. if self.ready_callback<>None: self.ready_callback() # create an instance of showmanager so we can control concurrent shows self.show_manager=ShowManager(self.show_id,self.showlist,self.show_params,self.root,self.canvas,self.pp_dir,self.pp_profile,self.pp_home) #set up video window reason,message,comand,has_window,x1,y1,x2,y2= self.parse_window(self.omx_window) if reason =='error': self.mon.err(self,'omx window error: ' + message + ' in ' + self.omx_window) self.end_callback(reason,message) else: if has_window==True: self.omx_window= '--win " '+ str(x1) + ' ' + str(y1) + ' ' + str(x2) + ' ' + str(y2) + ' " ' else: self.omx_window='' # Control other shows at beginning reason,message=self.show_manager.show_control(self.track_params['show-control-begin']) if reason in ('error','killed'): self.end_callback(reason,message) self=None else: #display content reason,message=self.display_content() if reason == 'error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # create animation events reason,message=self.ppio.animate(self.animate_begin_text,id(self)) if reason=='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # start playing the video. if self.play_state == VideoPlayer._CLOSED: self.mon.log(self,">play track received") self.start_play_state_machine(self.track) else: self.mon.err(self,'play track rejected') self.end_callback('error','play track rejected') self=None def terminate(self,reason): # circumvents state machine and does not wait for omxplayer to close if self.omx<>None: self.mon.log(self,"sent terminate to omxdriver") self.omx.terminate(reason) self.end('killed',' end without waiting for omxplayer to finish') # end without waiting else: self.mon.log(self,"terminate, omxdriver not running") self.end('killed','terminate, mplayerdriver not running') def input_pressed(self,symbol): if symbol[0:4]=='omx-': self.control(symbol[4]) elif symbol =='pause': self.pause() elif symbol=='stop': self.stop() else: pass def get_links(self): return self.track_params['links'] # *************************************** # INTERNAL FUNCTIONS # *************************************** # respond to normal stop def stop(self): # send signal to stop the track to the state machine self.mon.log(self,">stop received") self.quit_signal=True #toggle pause def pause(self): if self.play_state in (VideoPlayer._PLAYING,VideoPlayer._ENDING): self.omx.pause() return True #NIK # for pausing video after first frame elif self.play_state == VideoPlayer._STARTING: self.omx.delayed_pause = True return True #NIK else: self.mon.log(self,"!<pause rejected") return False # other control when playing def control(self,char): if self.play_state==VideoPlayer._PLAYING and char not in ('q'): self.mon.log(self,"> send control to omx: "+ char) self.omx.control(char) return True else: self.mon.log(self,"!<control rejected") return False # *********************** # sequencing # ********************** """self. play_state controls the playing sequence, it has the following values. I am not entirely sure the starting and ending states are required. - _closed - the omx process is not running, omx process can be initiated - _starting - omx process is running but is not yet able to receive controls - _playing - playing a track, controls can be sent - _ending - omx is doing its termination, controls cannot be sent """ def init_play_state_machine(self): self.quit_signal=False self.play_state=VideoPlayer._CLOSED def start_play_state_machine(self,track): #initialise all the state machine variables #self.iteration = 0 # for debugging self.quit_signal=False # signal that user has pressed stop self.play_state=VideoPlayer._STARTING #play the selected track options=self.omx_audio+ " " + self.omx_volume + ' ' + self.omx_window + ' ' + self.show_params['omx-other-options']+" " # NIK ADDITION # adding subtitles file for video if 'omx-subtitles' in self.track_params and self.track_params['omx-subtitles'] <> '': subtitles_full_path = self.complete_path(self.track_params['omx-subtitles']) if os.path.exists (subtitles_full_path): options += '--font-size 40 --subtitles "' + subtitles_full_path + '" ' if 'omx-subtitles-numlines' in self.track_params and self.track_params['omx-subtitles-numlines'] <> '': options += '--lines ' + self.track_params['omx-subtitles-numlines'] + ' ' # END NIK ADDITION self.omx.play(track,options) self.mon.log (self,'Playing track from show Id: '+ str(self.show_id)) # and start polling for state changes self.tick_timer=self.canvas.after(50, self.play_state_machine) def play_state_machine(self): if self.play_state == VideoPlayer._CLOSED: self.mon.log(self," State machine: " + self.play_state) return elif self.play_state == VideoPlayer._STARTING: self.mon.log(self," State machine: " + self.play_state) # if omxplayer is playing the track change to play state if self.omx.start_play_signal==True: self.mon.log(self," <start play signal received from omx") self.omx.start_play_signal=False self.play_state=VideoPlayer._PLAYING self.mon.log(self," State machine: omx_playing started") self.tick_timer=self.canvas.after(50, self.play_state_machine) elif self.play_state == VideoPlayer._PLAYING: # self.mon.log(self," State machine: " + self.play_state) # service any queued stop signals if self.quit_signal==True: self.mon.log(self," Service stop required signal") self.stop_omx() self.quit_signal=False # self.play_state = VideoPlayer._ENDING # omxplayer reports it is terminating so change to ending state if self.omx.end_play_signal: self.mon.log(self," <end play signal received") self.mon.log(self," <end detected at: " + str(self.omx.video_position)) if self.omx.end_play_reason<>'nice_day': # deal with omxplayer not sending 'have a nice day' self.mon.warn(self," <end detected at: " + str(self.omx.video_position)) self.mon.warn(self," <pexpect reports: "+self.omx.end_play_reason) self.mon.warn(self,'pexpect.before is'+self.omx.xbefore) self.play_state = VideoPlayer._ENDING self.ending_count=0 self.tick_timer=self.canvas.after(200, self.play_state_machine) elif self.play_state == VideoPlayer._ENDING: self.mon.log(self," State machine: " + self.play_state) # if spawned process has closed can change to closed state self.mon.log (self," State machine : is omx process running? - " + str(self.omx.is_running())) if self.omx.is_running() ==False: self.mon.log(self," <omx process is dead") self.play_state = VideoPlayer._CLOSED self.end('normal','quit by user or system') else: self.ending_count+=1 if self.ending_count>10: # deal with omxplayer not terminating at the end of a track self.mon.warn(self," <omxplayer failed to close at: " + str(self.omx.video_position)) self.mon.warn(self,'pexpect.before is'+self.omx.xbefore) self.omx.kill() self.mon.warn(self,'omxplayer now terminated ') self.play_state = VideoPlayer._CLOSED self.end('normal','end from omxplayer failed to terminate') else: self.tick_timer=self.canvas.after(200, self.play_state_machine) def stop_omx(self): # send signal to stop the track to the state machine self.mon.log(self," >stop omx received from state machine") if self.play_state==VideoPlayer._PLAYING: self.omx.stop() return True else: self.mon.log(self,"!<stop rejected") return False # ***************** # ending the player # ***************** def end(self,reason,message): # stop the plugin if self.track_params['plugin']<>'': self.pim.stop_plugin() # os.system("xrefresh -display :0") # abort the timer if self.tick_timer<>None: self.canvas.after_cancel(self.tick_timer) self.tick_timer=None if reason in ('error','killed'): self.end_callback(reason,message) self=None else: # normal end so do show control and animation # Control concurrent shows at end reason,message=self.show_manager.show_control(self.track_params['show-control-end']) if reason =='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # clear events list for this track if self.track_params['animate-clear']=='yes': self.ppio.clear_events_list(id(self)) # create animation events for ending reason,message=self.ppio.animate(self.animate_end_text,id(self)) if reason=='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: self.end_callback('normal',"track has terminated or quit") self=None # ***************** # displaying things # ***************** def display_content(self): #background colour if self.background_colour<>'': self.canvas.config(bg=self.background_colour) # delete previous content self.canvas.delete('pp-content') # background image if self.background_file<>'': self.background_img_file = self.complete_path(self.background_file) if not os.path.exists(self.background_img_file): self.mon.err(self,"Video background file not found: "+ self.background_img_file) self.end('error',"Video background file not found") else: pil_background_img=PIL.Image.open(self.background_img_file) self.background = PIL.ImageTk.PhotoImage(pil_background_img) self.drawn = self.canvas.create_image(int(self.canvas['width'])/2, int(self.canvas['height'])/2, image=self.background, anchor=CENTER, tag='pp-content') # execute the plugin if required if self.track_params['plugin']<>'': reason,message,self.track = self.pim.do_plugin(self.track,self.track_params['plugin'],) if reason <> 'normal': return reason,message # display show text if enabled if self.show_params['show-text']<> '' and self.track_params['display-show-text']=='yes': self.canvas.create_text(int(self.show_params['show-text-x']),int(self.show_params['show-text-y']), anchor=NW, text=self.show_params['show-text'], fill=self.show_params['show-text-colour'], font=self.show_params['show-text-font'], tag='pp-content') # display track text if enabled if self.track_params['track-text']<> '': self.canvas.create_text(int(self.track_params['track-text-x']),int(self.track_params['track-text-y']), anchor=NW, text=self.track_params['track-text'], fill=self.track_params['track-text-colour'], font=self.track_params['track-text-font'], tag='pp-content') # display instructions if enabled if self.enable_menu== True: self.canvas.create_text(int(self.show_params['hint-x']), int(self.show_params['hint-y']), text=self.show_params['hint-text'], fill=self.show_params['hint-colour'], font=self.show_params['hint-font'], anchor=NW, tag='pp-content') self.canvas.tag_raise('pp-click-area') self.canvas.update_idletasks( ) return 'normal','' # **************** # utilities # ***************** def complete_path(self,track_file): # complete path of the filename of the selected entry if track_file[0]=="+": track_file=self.pp_home+track_file[1:] self.mon.log(self,"Background image is "+ track_file) return track_file # original _ # warp _ or xy2 def parse_window(self,line): fields = line.split() # check there is a command field if len(fields) < 1: return 'error','no type field','',False,0,0,0,0 # deal with original which has 1 if fields[0]=='original': if len(fields) <> 1: return 'error','number of fields for original','',False,0,0,0,0 return 'normal','',fields[0],False,0,0,0,0 #deal with warp which has 1 or 5 arguments # check basic syntax if fields[0] <>'warp': return 'error','not a valid type','',False,0,0,0,0 if len(fields) not in (1,5): return 'error','wrong number of coordinates for warp','',False,0,0,0,0 # deal with window coordinates if len(fields) == 5: #window is specified if not (fields[1].isdigit() and fields[2].isdigit() and fields[3].isdigit() and fields[4].isdigit()): return 'error','coordinates are not positive integers','',False,0,0,0,0 has_window=True return 'normal','',fields[0],has_window,int(fields[1]),int(fields[2]),int(fields[3]),int(fields[4]) else: # fullscreen has_window=True return 'normal','',fields[0],has_window,0,0,self.canvas['width'],self.canvas['height']
class BrowserPlayer: #state constants _CLOSED = "player_closed" #probably will not exist _STARTING = "player_starting" #uzbl beinf loaded and fifo created _WAITING = "wait for timeout" # waiting for browser to appear on the screen _PLAYING = "player_playing" #track is playing to the screen _ENDING = "player_ending" #track is in the process of ending due to quit or duration exceeded # *************************************** # EXTERNAL COMMANDS # *************************************** def __init__(self, show_id, root, canvas, show_params, track_params, pp_dir, pp_home, pp_profile): self.mon=Monitor() self.mon.on() #instantiate arguments self.show_id=show_id self.root=root, self.canvas = canvas self.show_params=show_params self.track_params=track_params self.pp_dir=pp_dir self.pp_home=pp_home self.pp_profile=pp_profile # get duration limit (secs ) from profile if self.track_params['duration']<>"": self.duration= int(self.track_params['duration']) else: self.duration= int(self.show_params['duration']) self.duration_limit=20*self.duration # get background image from profile. self.background_file='' if self.track_params['background-image']<>"": self.background_file= self.track_params['background-image'] else: if self.track_params['display-show-background']=='yes': self.background_file= self.show_params['background-image'] # get background colour from profile. if self.track_params['background-colour']<>"": self.background_colour= self.track_params['background-colour'] else: self.background_colour= self.show_params['background-colour'] #get animation instructions from profile self.animate_begin_text=self.track_params['animate-begin'] self.animate_end_text=self.track_params['animate-end'] # open the plugin Manager self.pim=PluginManager(self.show_id,self.root,self.canvas,self.show_params,self.track_params,self.pp_dir,self.pp_home,self.pp_profile) #create an instance of PPIO so we can create gpio events self.ppio = PPIO() # could put instance generation in play, not sure which is better. self.bplayer=uzblDriver(self.canvas) self.command_timer=None self.tick_timer=None self.init_play_state_machine() def play(self, track, showlist, end_callback, ready_callback, enable_menu=False): #instantiate arguments self.track=track self.showlist=showlist self.end_callback=end_callback # callback when finished self.ready_callback=ready_callback #callback when ready to play self.enable_menu=enable_menu # callback to the calling object to e.g remove egg timer. if self.ready_callback<>None: self.ready_callback() # create an instance of showmanager so we can control concurrent shows self.show_manager=ShowManager(self.show_id,self.showlist,self.show_params,self.root,self.canvas,self.pp_dir,self.pp_profile,self.pp_home) #web window if self.track_params['web-window']<>'': self.web_window= self.track_params['web-window'] else: self.web_window= self.show_params['web-window'] reason,message,command,has_window,x1,y1,x2,y2=self.parse_window(self.web_window) if reason =='error': self.mon.err(self,'web window error: '+' ' + message + ' in ' + self.web_window) self.end_callback(reason,message) self=None else: #deal with web_window if has_window==False: self.geometry = ' --geometry=maximized ' else: width=x2-x1 height=y2-y1 self.geometry = "--geometry=%dx%d%+d%+d " % (width,height,x1,y1) # get browser commands reason,message=self.parse_commands(self.track_params['browser-commands']) if reason != 'normal': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # Control other shows at beginning reason,message=self.show_manager.show_control(self.track_params['show-control-begin']) if reason == 'error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: #display content reason,message=self.display_content() if reason == 'error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # create animation events reason,message=self.ppio.animate(self.animate_begin_text,id(self)) if reason=='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # start playing the track. self.start_play_state_machine() def terminate(self,reason): """ terminate the player in special circumstances normal user termination if by key_pressed 'stop' reason will be killed or error """ # circumvents state machine to terminate lower level and then itself. if self.bplayer<>None: self.mon.log(self,"sent terminate to uzbldriver") self.bplayer.terminate(reason) self.end('killed',' end without waiting for uzbl to finish') # end without waiting else: self.mon.log(self,"terminate, uzbldriver not running") self.end('killed','terminate, uzbldriver not running') def get_links(self): return self.track_params['links'] def input_pressed(self,symbol): # print symbol, symbol[0:5] if symbol[0:5]=='uzbl-': self.control(symbol[5:]) elif symbol == 'pause': self.pause() elif symbol=='stop': self.stop() # *************************************** # INTERNAL FUNCTIONS # *************************************** #browser do not do pause def pause(self): self.mon.log(self,"!<pause rejected") return False # other control when playing, not currently used def control(self,char): if self.play_state==BrowserPlayer._PLAYING and char not in ('exit'): self.mon.log(self,"> send control to uzbl:"+ char) self.bplayer.control(char) return True else: self.mon.log(self,"!<control rejected") return False # respond to normal stop def stop(self): # send signal to stop the track to the state machine self.mon.log(self,">stop received") self.quit_signal=True # *************************************** # sequencing # *************************************** """self. play_state controls the playing sequence, it has the following values. I am not entirely sure the starting and ending states are required. - _closed - the mplayer process is not running, mplayer process can be initiated - _starting - mplayer process is running but is not yet able to receive controls - _playing - playing a track, controls can be sent - _ending - mplayer is doing its termination, controls cannot be sent """ def init_play_state_machine(self): self.quit_signal=False self.play_state=BrowserPlayer._CLOSED def start_play_state_machine(self): #initialise all the state machine variables self.duration_count = 0 self.quit_signal=False # signal that user has pressed stop #play the track self.bplayer.play(self.track,self.geometry) self.mon.log (self,'Playing track from show Id: '+ str(self.show_id)) self.play_state=BrowserPlayer._STARTING # and start polling for state changes and count duration self.tick_timer=self.canvas.after(50, self.play_state_machine) def play_state_machine(self): if self.play_state == BrowserPlayer._CLOSED: self.mon.log(self," State machine: " + self.play_state) return elif self.play_state == BrowserPlayer._STARTING: # self.mon.log(self," State machine: " + self.play_state) # if uzbl fifo is available can send comands to uzbl but change to wait state to wait for it to appear on screen if self.bplayer.start_play_signal==True: self.mon.log(self," <fifo available signal received from uzbl") self.bplayer.start_play_signal=False self.play_state=BrowserPlayer._WAITING # get rid of status bar self.bplayer.control('set show_status = 0') # and get ready to wait for browser to appear self.wait_count= 50 # 10 seconds at 200mS steps self.mon.log(self," State machine: uzbl process alive") self.tick_timer=self.canvas.after(200, self.play_state_machine) elif self.play_state == BrowserPlayer._WAITING: if self.wait_count==0: # set state to playing self.play_state = BrowserPlayer._PLAYING # and start executing the browser commands self.play_commands() self.mon.log(self," State machine: uzbl_playing started") self.wait_count -=1 self.tick_timer=self.canvas.after(200, self.play_state_machine) elif self.play_state == BrowserPlayer._PLAYING: self.duration_count+=1 # self.mon.log(self," State machine: " + self.play_state) # service any queued stop signals and test duration count if self.quit_signal==True or (self.duration_limit>0 and self.duration_count>self.duration_limit): self.mon.log(self," Service stop required signal or timeout") # self.quit_signal=False self.stop_bplayer() self.play_state = BrowserPlayer._ENDING # uzbl reports it is terminating so change to ending state if self.bplayer.end_play_signal: self.mon.log(self," <end play signal received") self.play_state = BrowserPlayer._ENDING self.tick_timer=self.canvas.after(50, self.play_state_machine) elif self.play_state == BrowserPlayer._ENDING: # self.mon.log(self," State machine: " + self.play_state) # if spawned process has closed can change to closed state # self.mon.log (self," State machine : is luakit process running? - " + str(self.bplayer.is_running())) if self.bplayer.is_running() ==False: self.mon.log(self," <uzbl process is dead") if self.quit_signal==True: self.quit_signal=False self.play_state = BrowserPlayer._CLOSED self.end('normal','quit required or timeout') else: self.tick_timer=self.canvas.after(50, self.play_state_machine) def stop_bplayer(self): # send signal to stop the track to the state machine self.mon.log(self," >send stop to uzbl driver") if self.play_state==BrowserPlayer._PLAYING: self.bplayer.stop() return True else: self.mon.log(self,"!<stop rejected") return False # ***************** # ending the player # ***************** def end(self,reason,message): # stop the plugin if self.pim<>None: self.pim.stop_plugin() # abort the timers if self.tick_timer<>None: self.canvas.after_cancel(self.tick_timer) self.tick_timer=None if self.command_timer<>None: self.canvas.after_cancel(self.command_timer) self.tick_timer=None # clean up and fifos and sockets left by uzbl os.system('rm -f /tmp/uzbl_*') if reason in ('error','killed'): self.end_callback(reason,message) self=None else: # normal end so do show control and animation # Control concurrent shows at end reason,message=self.show_manager.show_control(self.track_params['show-control-end']) if reason =='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # clear events list for this track if self.track_params['animate-clear']=='yes': self.ppio.clear_events_list(id(self)) # create animation events for ending reason,message=self.ppio.animate(self.animate_end_text,id(self)) if reason=='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: self.end_callback('normal',"track has terminated or quit") self=None # ***************** # displaying things # ***************** def display_content(self): self.canvas.delete('pp-content') #background colour if self.background_colour<>'': self.canvas.config(bg=self.background_colour) if self.background_file<>'': self.background_img_file = self.complete_path(self.background_file) if not os.path.exists(self.background_img_file): self.mon.err(self,"Audio background file not found: "+ self.background_img_file) self.end('error',"Audio background file not found") else: pil_background_img=PIL.Image.open(self.background_img_file) self.background = PIL.ImageTk.PhotoImage(pil_background_img) self.drawn = self.canvas.create_image(int(self.canvas['width'])/2, int(self.canvas['height'])/2, image=self.background, anchor=CENTER, tag='pp-content') # execute the plugin if required if self.track_params['plugin']<>'': reason,message,self.track = self.pim.do_plugin(self.track,self.track_params['plugin'],) if reason <> 'normal': return reason,message # display hint text if enabled if self.enable_menu== True: self.canvas.create_text(int(self.show_params['hint-x']), int(self.show_params['hint-y']), text=self.show_params['hint-text'], fill=self.show_params['hint-colour'], font=self.show_params['hint-font'], anchor=NW, tag='pp-content') # display show text if enabled if self.show_params['show-text']<> ''and self.track_params['display-show-text']=='yes': self.canvas.create_text(int(self.show_params['show-text-x']),int(self.show_params['show-text-y']), anchor=NW, text=self.show_params['show-text'], fill=self.show_params['show-text-colour'], font=self.show_params['show-text-font'], tag='pp-content') # display track text if enabled if self.track_params['track-text']<> '': self.canvas.create_text(int(self.track_params['track-text-x']),int(self.track_params['track-text-y']), anchor=NW, text=self.track_params['track-text'], fill=self.track_params['track-text-colour'], font=self.track_params['track-text-font'], tag='pp-content') self.mon.log(self,"Displayed background and text ") self.canvas.tag_raise('pp-click-area') self.canvas.update_idletasks( ) return 'normal','' # ******************* # browser commands # *********************** def parse_commands(self,command_text): self.command_list=[] lines = command_text.split('\n') for line in lines: if line.strip()=="": continue reason,entry=self.parse_command(line) if reason != 'normal': return 'error',entry self.command_list.append(copy.deepcopy(entry)) # print self.command_list return 'normal','' def parse_command(self,line): fields = line.split() if fields[0]=='uzbl': # print fields[0], line[4:] return 'normal',[fields[0],line[4:]] if len(fields) not in (1,2): return 'error',"incorrect number of fields in command: " + line command=fields[0] arg='' if command not in ('load','refresh','wait','exit','loop'): return 'error','unknown command: '+ command if command in ('refresh','exit','loop') and len(fields)<>1: return 'error','incorrect number of fields for '+ command + 'in: ' + line if command == 'load': if len(fields)<>2: return 'error','incorrect number of fields for '+ command + 'in: ' + line else: arg = fields[1] if command == 'wait': if len(fields)<>2: return 'error','incorrect number of fields for '+ command + 'in: ' + line else: arg = fields[1] if not arg.isdigit():return 'error','Argument for Wait is not 0 or positive number in: ' + line return 'normal',[command,arg] def play_commands(self): if len(self.command_list)==0: return self.loop=0 self.command_index=0 self.canvas.after(100,self.execute_command) def execute_command(self): entry=self.command_list[self.command_index] command=entry[0] arg=entry[1] if self.command_index==len(self.command_list)-1: self.command_index=self.loop else: self.command_index+=1 # execute command if command == 'load': #self.canvas.focus_force() #self.root.lower() file=self.complete_path(arg) self.bplayer.control('uri '+ file) self.command_timer=self.canvas.after(10,self.execute_command) elif command == 'refresh': self.bplayer.control('reload_ign_cache') self.command_timer=self.canvas.after(10,self.execute_command) elif command == 'wait': self.command_timer=self.canvas.after(1000*int(arg),self.execute_command) elif command=='exit': self.quit_signal=True elif command=='loop': self.loop=self.command_index self.command_timer=self.canvas.after(10,self.execute_command) elif command=='uzbl': self.bplayer.control(arg) self.command_timer=self.canvas.after(10,self.execute_command) # ***************** # utilities # ***************** def complete_path(self,track_file): # complete path of the filename of the selected entry if track_file[0]=="+": track_file=self.pp_home+track_file[1:] self.mon.log(self,"Background image is "+ track_file) return track_file def parse_window(self,line): # parses warp _ or xy2 fields = line.split() # check there is a command field if len(fields) < 1: return 'error','no type field','',False,0,0,0,0 #deal with warp which has 1 or 5 arguments # check basic syntax if fields[0] <>'warp': return 'error','not a valid type','',False,0,0,0,0 if len(fields) not in (1,5): return 'error','wrong number of coordinates for warp','',False,0,0,0,0 # deal with window coordinates if len(fields) == 5: #window is specified if not (fields[1].isdigit() and fields[2].isdigit() and fields[3].isdigit() and fields[4].isdigit()): return 'error','coordinates are not positive integers','',False,0,0,0,0 has_window=True return 'normal','',fields[0],has_window,int(fields[1]),int(fields[2]),int(fields[3]),int(fields[4]) else: # fullscreen has_window=False return 'normal','',fields[0],has_window,0,0,0,0
class AudioPlayer: """ plays an audio track using mplayer against a coloured backgroud and image track can be paused and interrupted See pp_imageplayer for common software design description """ #state constants _CLOSED = "mplayer_closed" #probably will not exist _STARTING = "mplayer_starting" #track is being prepared _PLAYING = "mplayer_playing" #track is playing to the screen, may be paused _ENDING = "mplayer_ending" #track is in the process of ending due to quit or end of track _WAITING = "wait for timeout" # track has finished but timeout still running # audio mixer matrix settings _LEFT = "channels=2:1:0:0:1:1" _RIGHT = "channels=2:1:0:1:1:0" _STEREO = "channels=2" # *************************************** # EXTERNAL COMMANDS # *************************************** def __init__(self, show_id, root, canvas, show_params, track_params, pp_dir, pp_home, pp_profile): self.mon = Monitor() self.mon.off() #instantiate arguments self.show_id = show_id self.root = root self.canvas = canvas self.show_params = show_params self.track_params = track_params self.pp_dir = pp_dir self.pp_home = pp_home self.pp_profile = pp_profile # get duration limit (secs ) from profile if self.track_params['duration'] <> '': self.duration = int(self.track_params['duration']) self.duration_limit = 20 * self.duration else: self.duration_limit = -1 # get background image from profile. self.background_file = '' if self.track_params['background-image'] <> "": self.background_file = self.track_params['background-image'] else: if self.track_params['display-show-background'] == 'yes': self.background_file = self.show_params['background-image'] # get background colour from profile. if self.track_params['background-colour'] <> "": self.background_colour = self.track_params['background-colour'] else: self.background_colour = self.show_params['background-colour'] # get audio device from profile. if self.track_params['mplayer-audio'] <> "": self.mplayer_audio = self.track_params['mplayer-audio'] else: self.mplayer_audio = self.show_params['mplayer-audio'] # get audio volume from profile. if self.track_params['mplayer-volume'] <> "": self.mplayer_volume = self.track_params['mplayer-volume'].strip() else: self.mplayer_volume = self.show_params['mplayer-volume'].strip() self.volume_option = 'volume=' + self.mplayer_volume #get speaker from profile if self.track_params['audio-speaker'] <> "": self.audio_speaker = self.track_params['audio-speaker'] else: self.audio_speaker = self.show_params['audio-speaker'] if self.audio_speaker == 'left': self.speaker_option = AudioPlayer._LEFT elif self.audio_speaker == 'right': self.speaker_option = AudioPlayer._RIGHT else: self.speaker_option = AudioPlayer._STEREO #get animation instructions from profile self.animate_begin_text = self.track_params['animate-begin'] self.animate_end_text = self.track_params['animate-end'] # open the plugin Manager self.pim = PluginManager(self.show_id, self.root, self.canvas, self.show_params, self.track_params, self.pp_dir, self.pp_home, self.pp_profile) #create an instance of PPIO so we can create gpio events self.ppio = PPIO() # could put instance generation in play, not sure which is better. self.mplayer = mplayerDriver(self.canvas) self.tick_timer = None self.init_play_state_machine() def play(self, track, showlist, end_callback, ready_callback, enable_menu=False): #instantiate arguments self.track = track self.showlist = showlist self.end_callback = end_callback # callback when finished self.ready_callback = ready_callback #callback when ready to play self.enable_menu = enable_menu # select the sound device if self.mplayer_audio <> "": if self.mplayer_audio == 'hdmi': os.system("amixer -q -c 0 cset numid=3 2") else: os.system("amixer -q -c 0 cset numid=3 1") # callback to the calling object to e.g remove egg timer. if self.ready_callback <> None: self.ready_callback() # create an instance of showmanager so we can control concurrent shows self.show_manager = ShowManager(self.show_id, self.showlist, self.show_params, self.root, self.canvas, self.pp_dir, self.pp_profile, self.pp_home) # Control other shows at beginning reason, message = self.show_manager.show_control( self.track_params['show-control-begin']) if reason == 'error': self.mon.err(self, message) self.end_callback(reason, message) self = None else: # display image and text reason, message = self.display_content() if reason == 'error': self.mon.err(self, message) self.end_callback(reason, message) self = None else: # create animation events reason, message = self.ppio.animate(self.animate_begin_text, id(self)) if reason == 'error': self.mon.err(self, message) self.end_callback(reason, message) self = None else: # start playing the track. if self.duration_limit <> 0: self.start_play_state_machine() else: self.tick_timer = self.canvas.after(10, self.end_zero) def end_zero(self): self.end('normal', 'zero duration') def terminate(self, reason): """ terminate the player in special circumstances normal user termination if by key_pressed 'stop' reason will be killed or error """ # circumvents state machine to terminate lower level and then itself. if self.mplayer <> None: self.mon.log(self, "sent terminate to mplayerdriver") self.mplayer.terminate(reason) self.end('killed', ' end without waiting for mplayer to finish' ) # end without waiting else: self.mon.log(self, "terminate, mplayerdriver not running") self.end('killed', 'terminate, mplayerdriver not running') def get_links(self): return self.track_params['links'] def input_pressed(self, symbol): if symbol[0:6] == 'mplay-': self.control(symbol[6]) elif symbol == 'pause': self.pause() elif symbol == 'stop': self.stop() # *************************************** # INTERNAL FUNCTIONS # *************************************** #toggle pause def pause(self): if self.play_state in (AudioPlayer._PLAYING, AudioPlayer._ENDING) and self.track <> '': self.mplayer.pause() return True else: self.mon.log(self, "!<pause rejected") return False # other control when playing, not currently used def control(self, char): if self.play_state == AudioPlayer._PLAYING and self.track <> '' and char not in ( 'q'): self.mon.log(self, "> send control to mplayer: " + char) self.mplayer.control(char) return True else: self.mon.log(self, "!<control rejected") return False # respond to normal stop def stop(self): # send signal to stop the track to the state machine self.mon.log(self, ">stop received") self.quit_signal = True # *************************************** # sequencing # *************************************** """self. play_state controls the playing sequence, it has the following values. I am not entirely sure the starting and ending states are required. - _closed - the mplayer process is not running, mplayer process can be initiated - _starting - mplayer process is running but is not yet able to receive controls - _playing - playing a track, controls can be sent - _ending - mplayer is doing its termination, controls cannot be sent """ def init_play_state_machine(self): self.quit_signal = False self.play_state = AudioPlayer._CLOSED def start_play_state_machine(self): #initialise all the state machine variables self.duration_count = 0 self.quit_signal = False # signal that user has pressed stop #play the track options = self.show_params[ 'mplayer-other-options'] + '-af ' + self.speaker_option + ',' + self.volume_option + ' ' if self.track <> '': self.mplayer.play(self.track, options) self.mon.log(self, 'Playing track from show Id: ' + str(self.show_id)) self.play_state = AudioPlayer._STARTING else: self.play_state = AudioPlayer._PLAYING # and start polling for state changes and count duration self.tick_timer = self.canvas.after(50, self.play_state_machine) def play_state_machine(self): self.duration_count += 1 if self.play_state == AudioPlayer._CLOSED: self.mon.log(self, " State machine: " + self.play_state) return elif self.play_state == AudioPlayer._STARTING: self.mon.log(self, " State machine: " + self.play_state) # if mplayer is playing the track change to play state if self.mplayer.start_play_signal == True: self.mon.log( self, " <start play signal received from mplayer") self.mplayer.start_play_signal = False self.play_state = AudioPlayer._PLAYING self.mon.log(self, " State machine: mplayer_playing started") self.tick_timer = self.canvas.after(50, self.play_state_machine) elif self.play_state == AudioPlayer._PLAYING: # self.mon.log(self," State machine: " + self.play_state) # service any queued stop signals if self.quit_signal == True or ( self.duration_limit > 0 and self.duration_count > self.duration_limit): self.mon.log(self, " Service stop required signal or timeout") # self.quit_signal=False if self.track <> '': self.stop_mplayer() self.play_state = AudioPlayer._ENDING else: self.play_state = AudioPlayer._CLOSED self.end('normal', 'stop required signal or timeout') # mplayer reports it is terminating so change to ending state if self.track <> '' and self.mplayer.end_play_signal: self.mon.log(self, " <end play signal received") self.mon.log( self, " <end detected at: " + str(self.mplayer.audio_position)) self.play_state = AudioPlayer._ENDING self.tick_timer = self.canvas.after(50, self.play_state_machine) elif self.play_state == AudioPlayer._ENDING: # self.mon.log(self," State machine: " + self.play_state) # if spawned process has closed can change to closed state # self.mon.log (self," State machine : is mplayer process running? - " + str(self.mplayer.is_running())) if self.mplayer.is_running() == False: self.mon.log(self, " <mplayer process is dead") if self.quit_signal == True: self.quit_signal = False self.play_state = AudioPlayer._CLOSED self.end('normal', 'quit required or timeout') elif self.duration_limit > 0 and self.duration_count < self.duration_limit: self.play_state = AudioPlayer._WAITING self.tick_timer = self.canvas.after( 50, self.play_state_machine) else: self.play_state = AudioPlayer._CLOSED self.end('normal', 'mplayer dead') else: self.tick_timer = self.canvas.after(50, self.play_state_machine) elif self.play_state == AudioPlayer._WAITING: # self.mon.log(self," State machine: " + self.play_state) if self.quit_signal == True or ( self.duration_limit > 0 and self.duration_count > self.duration_limit): self.mon.log( self, " Service stop required signal or timeout from wait") self.quit_signal = False self.play_state = AudioPlayer._CLOSED self.end('normal', 'mplayer dead') else: self.tick_timer = self.canvas.after(50, self.play_state_machine) def stop_mplayer(self): # send signal to stop the track to the state machine self.mon.log(self, " >stop mplayer received from state machine") if self.play_state == AudioPlayer._PLAYING: self.mplayer.stop() return True else: self.mon.log(self, "!<stop rejected") return False # ***************** # ending the player # ***************** def end(self, reason, message): # stop the plugin if self.track_params['plugin'] <> '': self.pim.stop_plugin() # abort the timer if self.tick_timer <> None: self.canvas.after_cancel(self.tick_timer) self.tick_timer = None if reason in ('error', 'killed'): self.end_callback(reason, message) self = None else: # normal end so do show control and animation # Control concurrent shows at end reason, message = self.show_manager.show_control( self.track_params['show-control-end']) if reason == 'error': self.mon.err(self, message) self.end_callback(reason, message) self = None else: # clear events list for this track if self.track_params['animate-clear'] == 'yes': self.ppio.clear_events_list(id(self)) # create animation events for ending reason, message = self.ppio.animate(self.animate_end_text, id(self)) if reason == 'error': self.mon.err(self, message) self.end_callback(reason, message) self = None else: self.end_callback('normal', "track has terminated or quit") self = None # ***************** # displaying things # ***************** def display_content(self): #if self.background_file<>'' or self.show_params['show-text']<> '' or #self.track_params['track-text']<> '' or self.enable_menu== True or #self.track_params['clear-screen']=='yes': if self.track_params['clear-screen'] == 'yes': self.canvas.delete('pp-content') # self.canvas.update() #background colour if self.background_colour <> '': self.canvas.config(bg=self.background_colour) if self.background_file <> '': self.background_img_file = self.complete_path(self.background_file) if not os.path.exists(self.background_img_file): self.mon.err( self, "Audio background file not found: " + self.background_img_file) self.end('error', "Audio background file not found") else: pil_background_img = PIL.Image.open(self.background_img_file) self.background = PIL.ImageTk.PhotoImage(pil_background_img) self.drawn = self.canvas.create_image( int(self.canvas['width']) / 2, int(self.canvas['height']) / 2, image=self.background, anchor=CENTER, tag='pp-content') # execute the plugin if required if self.track_params['plugin'] <> '': reason, message, self.track = self.pim.do_plugin( self.track, self.track_params['plugin'], ) if reason <> 'normal': return reason, message # display hint text if enabled if self.enable_menu == True: self.canvas.create_text(int(self.show_params['hint-x']), int(self.show_params['hint-y']), text=self.show_params['hint-text'], fill=self.show_params['hint-colour'], font=self.show_params['hint-font'], anchor=NW, tag='pp-content') # display show text if enabled if self.show_params['show-text'] <> '' and self.track_params[ 'display-show-text'] == 'yes': self.canvas.create_text(int(self.show_params['show-text-x']), int(self.show_params['show-text-y']), anchor=NW, text=self.show_params['show-text'], fill=self.show_params['show-text-colour'], font=self.show_params['show-text-font'], tag='pp-content') # display track text if enabled if self.track_params['track-text'] <> '': self.canvas.create_text( int(self.track_params['track-text-x']), int(self.track_params['track-text-y']), anchor=NW, text=self.track_params['track-text'], fill=self.track_params['track-text-colour'], font=self.track_params['track-text-font'], tag='pp-content') self.mon.log(self, "Displayed background and text ") self.canvas.tag_raise('pp-click-area') self.canvas.update_idletasks() return 'normal', '' # ***************** # utilities # ***************** def complete_path(self, track_file): # complete path of the filename of the selected entry if track_file[0] == "+": track_file = self.pp_home + track_file[1:] self.mon.log(self, "Background image is " + track_file) return track_file
class AudioPlayer: """ plays an audio track using mplayer against a coloured backgroud and image track can be paused and interrupted See pp_imageplayer for common software design description """ # state constants _CLOSED = "mplayer_closed" # probably will not exist _STARTING = "mplayer_starting" # track is being prepared _PLAYING = "mplayer_playing" # track is playing to the screen, may be paused _ENDING = "mplayer_ending" # track is in the process of ending due to quit or end of track _WAITING = "wait for timeout" # track has finished but timeout still running # audio mixer matrix settings _LEFT = "channels=2:1:0:0:1:1" _RIGHT = "channels=2:1:0:1:1:0" _STEREO = "channels=2" # *************************************** # EXTERNAL COMMANDS # *************************************** def __init__(self, show_id, root, canvas, show_params, track_params, pp_dir, pp_home, pp_profile): self.mon = Monitor() self.mon.off() # instantiate arguments self.show_id = show_id self.root = root self.canvas = canvas self.show_params = show_params self.track_params = track_params self.pp_dir = pp_dir self.pp_home = pp_home self.pp_profile = pp_profile # get duration limit (secs ) from profile if self.track_params["duration"] <> "": self.duration = int(self.track_params["duration"]) self.duration_limit = 20 * self.duration else: self.duration_limit = -1 # get background image from profile. self.background_file = "" if self.track_params["background-image"] <> "": self.background_file = self.track_params["background-image"] else: if self.track_params["display-show-background"] == "yes": self.background_file = self.show_params["background-image"] # get background colour from profile. if self.track_params["background-colour"] <> "": self.background_colour = self.track_params["background-colour"] else: self.background_colour = self.show_params["background-colour"] # get audio device from profile. if self.track_params["mplayer-audio"] <> "": self.mplayer_audio = self.track_params["mplayer-audio"] else: self.mplayer_audio = self.show_params["mplayer-audio"] # get audio volume from profile. if self.track_params["mplayer-volume"] <> "": self.mplayer_volume = self.track_params["mplayer-volume"].strip() else: self.mplayer_volume = self.show_params["mplayer-volume"].strip() self.volume_option = "volume=" + self.mplayer_volume # get speaker from profile if self.track_params["audio-speaker"] <> "": self.audio_speaker = self.track_params["audio-speaker"] else: self.audio_speaker = self.show_params["audio-speaker"] if self.audio_speaker == "left": self.speaker_option = AudioPlayer._LEFT elif self.audio_speaker == "right": self.speaker_option = AudioPlayer._RIGHT else: self.speaker_option = AudioPlayer._STEREO # get animation instructions from profile self.animate_begin_text = self.track_params["animate-begin"] self.animate_end_text = self.track_params["animate-end"] # open the plugin Manager self.pim = PluginManager( self.show_id, self.root, self.canvas, self.show_params, self.track_params, self.pp_dir, self.pp_home, self.pp_profile, ) # create an instance of PPIO so we can create gpio events self.ppio = PPIO() # could put instance generation in play, not sure which is better. self.mplayer = mplayerDriver(self.canvas) self.tick_timer = None self.init_play_state_machine() def play(self, track, showlist, end_callback, ready_callback, enable_menu=False): # instantiate arguments self.track = track self.showlist = showlist self.end_callback = end_callback # callback when finished self.ready_callback = ready_callback # callback when ready to play self.enable_menu = enable_menu # select the sound device if self.mplayer_audio <> "": if self.mplayer_audio == "hdmi": os.system("amixer -q -c 0 cset numid=3 2") else: os.system("amixer -q -c 0 cset numid=3 1") # callback to the calling object to e.g remove egg timer. if self.ready_callback <> None: self.ready_callback() # create an instance of showmanager so we can control concurrent shows self.show_manager = ShowManager( self.show_id, self.showlist, self.show_params, self.root, self.canvas, self.pp_dir, self.pp_profile, self.pp_home, ) # Control other shows at beginning reason, message = self.show_manager.show_control(self.track_params["show-control-begin"]) if reason == "error": self.mon.err(self, message) self.end_callback(reason, message) self = None else: # display image and text reason, message = self.display_content() if reason == "error": self.mon.err(self, message) self.end_callback(reason, message) self = None else: # create animation events reason, message = self.ppio.animate(self.animate_begin_text, id(self)) if reason == "error": self.mon.err(self, message) self.end_callback(reason, message) self = None else: # start playing the track. if self.duration_limit <> 0: self.start_play_state_machine() else: self.tick_timer = self.canvas.after(10, self.end_zero) def end_zero(self): self.end("normal", "zero duration") def terminate(self, reason): """ terminate the player in special circumstances normal user termination if by key_pressed 'stop' reason will be killed or error """ # circumvents state machine to terminate lower level and then itself. if self.mplayer <> None: self.mon.log(self, "sent terminate to mplayerdriver") self.mplayer.terminate(reason) self.end("killed", " end without waiting for mplayer to finish") # end without waiting else: self.mon.log(self, "terminate, mplayerdriver not running") self.end("killed", "terminate, mplayerdriver not running") def get_links(self): return self.track_params["links"] def input_pressed(self, symbol): if symbol[0:6] == "mplay-": self.control(symbol[6]) elif symbol == "pause": self.pause() elif symbol == "stop": self.stop() # *************************************** # INTERNAL FUNCTIONS # *************************************** # toggle pause def pause(self): if self.play_state in (AudioPlayer._PLAYING, AudioPlayer._ENDING) and self.track <> "": self.mplayer.pause() return True else: self.mon.log(self, "!<pause rejected") return False # other control when playing, not currently used def control(self, char): if self.play_state == AudioPlayer._PLAYING and self.track <> "" and char not in ("q"): self.mon.log(self, "> send control to mplayer: " + char) self.mplayer.control(char) return True else: self.mon.log(self, "!<control rejected") return False # respond to normal stop def stop(self): # send signal to stop the track to the state machine self.mon.log(self, ">stop received") self.quit_signal = True # *************************************** # sequencing # *************************************** """self. play_state controls the playing sequence, it has the following values. I am not entirely sure the starting and ending states are required. - _closed - the mplayer process is not running, mplayer process can be initiated - _starting - mplayer process is running but is not yet able to receive controls - _playing - playing a track, controls can be sent - _ending - mplayer is doing its termination, controls cannot be sent """ def init_play_state_machine(self): self.quit_signal = False self.play_state = AudioPlayer._CLOSED def start_play_state_machine(self): # initialise all the state machine variables self.duration_count = 0 self.quit_signal = False # signal that user has pressed stop # play the track options = ( self.show_params["mplayer-other-options"] + "-af " + self.speaker_option + "," + self.volume_option + " " ) if self.track <> "": self.mplayer.play(self.track, options) self.mon.log(self, "Playing track from show Id: " + str(self.show_id)) self.play_state = AudioPlayer._STARTING else: self.play_state = AudioPlayer._PLAYING # and start polling for state changes and count duration self.tick_timer = self.canvas.after(50, self.play_state_machine) def play_state_machine(self): self.duration_count += 1 if self.play_state == AudioPlayer._CLOSED: self.mon.log(self, " State machine: " + self.play_state) return elif self.play_state == AudioPlayer._STARTING: self.mon.log(self, " State machine: " + self.play_state) # if mplayer is playing the track change to play state if self.mplayer.start_play_signal == True: self.mon.log(self, " <start play signal received from mplayer") self.mplayer.start_play_signal = False self.play_state = AudioPlayer._PLAYING self.mon.log(self, " State machine: mplayer_playing started") self.tick_timer = self.canvas.after(50, self.play_state_machine) elif self.play_state == AudioPlayer._PLAYING: # self.mon.log(self," State machine: " + self.play_state) # service any queued stop signals if self.quit_signal == True or (self.duration_limit > 0 and self.duration_count > self.duration_limit): self.mon.log(self, " Service stop required signal or timeout") # self.quit_signal=False if self.track <> "": self.stop_mplayer() self.play_state = AudioPlayer._ENDING else: self.play_state = AudioPlayer._CLOSED self.end("normal", "stop required signal or timeout") # mplayer reports it is terminating so change to ending state if self.track <> "" and self.mplayer.end_play_signal: self.mon.log(self, " <end play signal received") self.mon.log(self, " <end detected at: " + str(self.mplayer.audio_position)) self.play_state = AudioPlayer._ENDING self.tick_timer = self.canvas.after(50, self.play_state_machine) elif self.play_state == AudioPlayer._ENDING: # self.mon.log(self," State machine: " + self.play_state) # if spawned process has closed can change to closed state # self.mon.log (self," State machine : is mplayer process running? - " + str(self.mplayer.is_running())) if self.mplayer.is_running() == False: self.mon.log(self, " <mplayer process is dead") if self.quit_signal == True: self.quit_signal = False self.play_state = AudioPlayer._CLOSED self.end("normal", "quit required or timeout") elif self.duration_limit > 0 and self.duration_count < self.duration_limit: self.play_state = AudioPlayer._WAITING self.tick_timer = self.canvas.after(50, self.play_state_machine) else: self.play_state = AudioPlayer._CLOSED self.end("normal", "mplayer dead") else: self.tick_timer = self.canvas.after(50, self.play_state_machine) elif self.play_state == AudioPlayer._WAITING: # self.mon.log(self," State machine: " + self.play_state) if self.quit_signal == True or (self.duration_limit > 0 and self.duration_count > self.duration_limit): self.mon.log(self, " Service stop required signal or timeout from wait") self.quit_signal = False self.play_state = AudioPlayer._CLOSED self.end("normal", "mplayer dead") else: self.tick_timer = self.canvas.after(50, self.play_state_machine) def stop_mplayer(self): # send signal to stop the track to the state machine self.mon.log(self, " >stop mplayer received from state machine") if self.play_state == AudioPlayer._PLAYING: self.mplayer.stop() return True else: self.mon.log(self, "!<stop rejected") return False # ***************** # ending the player # ***************** def end(self, reason, message): # stop the plugin if self.track_params["plugin"] <> "": self.pim.stop_plugin() # abort the timer if self.tick_timer <> None: self.canvas.after_cancel(self.tick_timer) self.tick_timer = None if reason in ("error", "killed"): self.end_callback(reason, message) self = None else: # normal end so do show control and animation # Control concurrent shows at end reason, message = self.show_manager.show_control(self.track_params["show-control-end"]) if reason == "error": self.mon.err(self, message) self.end_callback(reason, message) self = None else: # clear events list for this track if self.track_params["animate-clear"] == "yes": self.ppio.clear_events_list(id(self)) # create animation events for ending reason, message = self.ppio.animate(self.animate_end_text, id(self)) if reason == "error": self.mon.err(self, message) self.end_callback(reason, message) self = None else: self.end_callback("normal", "track has terminated or quit") self = None # ***************** # displaying things # ***************** def display_content(self): # if self.background_file<>'' or self.show_params['show-text']<> '' or #self.track_params['track-text']<> '' or self.enable_menu== True or #self.track_params['clear-screen']=='yes': if self.track_params["clear-screen"] == "yes": self.canvas.delete("pp-content") # self.canvas.update() # background colour if self.background_colour <> "": self.canvas.config(bg=self.background_colour) if self.background_file <> "": self.background_img_file = self.complete_path(self.background_file) if not os.path.exists(self.background_img_file): self.mon.err(self, "Audio background file not found: " + self.background_img_file) self.end("error", "Audio background file not found") else: pil_background_img = PIL.Image.open(self.background_img_file) self.background = PIL.ImageTk.PhotoImage(pil_background_img) self.drawn = self.canvas.create_image( int(self.canvas["width"]) / 2, int(self.canvas["height"]) / 2, image=self.background, anchor=CENTER, tag="pp-content", ) # execute the plugin if required if self.track_params["plugin"] <> "": reason, message, self.track = self.pim.do_plugin(self.track, self.track_params["plugin"]) if reason <> "normal": return reason, message # display hint text if enabled if self.enable_menu == True: self.canvas.create_text( int(self.show_params["hint-x"]), int(self.show_params["hint-y"]), text=self.show_params["hint-text"], fill=self.show_params["hint-colour"], font=self.show_params["hint-font"], anchor=NW, tag="pp-content", ) # display show text if enabled if self.show_params["show-text"] <> "" and self.track_params["display-show-text"] == "yes": self.canvas.create_text( int(self.show_params["show-text-x"]), int(self.show_params["show-text-y"]), anchor=NW, text=self.show_params["show-text"], fill=self.show_params["show-text-colour"], font=self.show_params["show-text-font"], tag="pp-content", ) # display track text if enabled if self.track_params["track-text"] <> "": self.canvas.create_text( int(self.track_params["track-text-x"]), int(self.track_params["track-text-y"]), anchor=NW, text=self.track_params["track-text"], fill=self.track_params["track-text-colour"], font=self.track_params["track-text-font"], tag="pp-content", ) self.mon.log(self, "Displayed background and text ") self.canvas.tag_raise("pp-click-area") self.canvas.update_idletasks() return "normal", "" # ***************** # utilities # ***************** def complete_path(self, track_file): # complete path of the filename of the selected entry if track_file[0] == "+": track_file = self.pp_home + track_file[1:] self.mon.log(self, "Background image is " + track_file) return track_file
class AudioPlayer: _CLOSED = "mplayer_closed" #probably will not exist _STARTING = "mplayer_starting" #track is being prepared _PLAYING = "mplayer_playing" #track is playing to the screen, may be paused _ENDING = "mplayer_ending" #track is in the process of ending due to quit or end of track _WAITING = "wait for timeout" # track has finished but timeout still running #_LEFT = "-af channels=2:1:0:0:1:1,resample=48000:1 " # _RIGHT = "-af channels=2:1:0:1:1:0,resample=48000:1 " #_STEREO = "-af channels=2,resample=48000:1 " _LEFT = "channels=2:1:0:0:1:1" _RIGHT = "channels=2:1:0:1:1:0" _STEREO = "channels=2" # *************************************** # EXTERNAL COMMANDS # *************************************** def __init__(self, show_id, canvas, pp_home, show_params, track_params ): """ canvas - the canvas onto which the background image is to be drawn show_params - configuration of show playing the track track_params - config dictionary for this track overrides show_params """ self.mon=Monitor() self.mon.on() #instantiate arguments self.show_id=show_id self.show_params=show_params #configuration dictionary for the videoplayer self.canvas = canvas #canvas onto which video should be played but isn't! Use as widget for alarm self.pp_home=pp_home self.track_params=track_params # get duration (secs ) from profile self.duration= int(self.track_params['duration']) self.duration_limit=20*self.duration # get background image from profile. self.background_file = self.track_params['background-image'] # get audio sink from profile. if self.track_params['mplayer-audio']<>"": self.mplayer_audio= self.track_params['mplayer-audio'] else: self.mplayer_audio= self.show_params['mplayer-audio'] # get audio volume from profile. if self.track_params['mplayer-volume']<>"": self.mplayer_volume= self.track_params['mplayer-volume'].strip() else: self.mplayer_volume= self.show_params['mplayer-volume'].strip() self.volume_option= 'volume=' + self.mplayer_volume #get speaker from profile if self.track_params['audio-speaker']<>"": self.audio_speaker= self.track_params['audio-speaker'] else: self.audio_speaker= self.show_params['audio-speaker'] if self.audio_speaker=='left': self.speaker_option=AudioPlayer._LEFT elif self.audio_speaker=='right': self.speaker_option=AudioPlayer._RIGHT else: self.speaker_option=AudioPlayer._STEREO #get animation instructions from profile self.animate_begin_text=self.track_params['animate-begin'] self.animate_end_text=self.track_params['animate-end'] #create an instance of PPIO so we can create gpio events self.ppio = PPIO() # could put instance generation in play, not sure which is better. self.mplayer=mplayerDriver(self.canvas) self._tick_timer=None self.error=False self.terminate_me=False self._init_play_state_machine() def play(self, track, end_callback, ready_callback, enable_menu=False, starting_callback=None, playing_callback=None, ending_callback=None): """ play - plays the specified track, the first call after __init__ track - full path of track to play end_callback - callback when track ends (reason,message) reason = killed - return from a terminate with reason = killed error - return because player or lower level has generated and runtime error normal - anything else message = ant tesxt, used for debugging ready_callback - callback when the track is ready to play, use to stop eggtimer etc. enable_menu - True if the track is to have a child show starting/playing/ending callback - called repeatedly in each state for show to display status, time etc. """ #instantiate arguments self.track=track self.ready_callback=ready_callback #callback when ready to play self.enable_menu=enable_menu self.end_callback=end_callback # callback when finished self.starting_callback=starting_callback #callback during starting state self.playing_callback=playing_callback #callback during playing state self.ending_callback=ending_callback # callback during ending state # enable_menu is not used by AudioPlayer # select the sound sink if self.mplayer_audio<>"": if self.mplayer_audio=='hdmi': os.system("amixer -q -c 0 cset numid=3 2") else: os.system("amixer -q -c 0 cset numid=3 1") # callback to the calling object to e.g remove egg timer. if self.ready_callback<>None: self.ready_callback() # display image and text self.display_image() # create animation events error_text=self.ppio.animate(self.animate_begin_text,id(self)) if error_text<>'': self.mon.err(self,error_text) self.error=True self._end('error',error_text) # and start playing the track. if self.play_state == AudioPlayer._CLOSED: self.mon.log(self,">play track received") self._start_play_state_machine() return True else: self.mon.log(self,"!< play track rejected") return False def key_pressed(self,key_name): """ respond to user or system key presses """ if key_name=='': return elif key_name in ('p',' '): self._pause() return elif key_name=='escape': self._stop() return def button_pressed(self,button,edge): """ respond to user button presses """ if button =='pause': self._pause() return elif button=='stop': self._stop() return def terminate(self,reason): """ terminate the player in special circumstances normal user termination if by key_pressed 'escape' reason will be killed or error """ # circumvents state machine to terminate lower level and then itself. self.terminate_me=True if self.mplayer<>None: self.mon.log(self,"sent terminate to mplayerdriver") self.mplayer.terminate(reason) self._end('killed',' end without waiting') # end without waiting else: self.mon.log(self,"terminate, mplayerdriver not running") self._end('killed','terminate, mplayerdriver not running') # *************************************** # INTERNAL FUNCTIONS # *************************************** #toggle pause def _pause(self): if self.play_state in (AudioPlayer._PLAYING,AudioPlayer._ENDING) and self.track<>'': self.mplayer.pause() return True else: self.mon.log(self,"!<pause rejected") return False # other control when playing, not currently used def _control(self,char): if self.play_state==AudioPlayer._PLAYING and self.track<>'': self.mon.log(self,"> send control to mplayer: "+ char) self.mplayer.control(char) return True else: self.mon.log(self,"!<control rejected") return False # respond to normal stop def _stop(self): # send signal to stop the track to the state machine self.mon.log(self,">stop received") self._stop_required_signal=True #respond to internal error by setting flags to cause state machine to stop player #use this rather than end if the driver and its spawned process might still be running def _error(self): self.error=True self._stop_required_signal=True # tidy up and end AudioPlayer. def _end(self,reason,message): # self.canvas.delete(ALL) # abort the timer if self._tick_timer<>None: self.canvas.after_cancel(self._tick_timer) self._tick_timer=None if self.error==True or reason=='error': self.end_callback("error",message) self=None elif self.terminate_me==True: self.end_callback("killed",message) self=None else: # clear events list for this track if self.track_params['animate-clear']=='yes': self.ppio.clear_events_list(id(self)) # create animation events for ending error_text=self.ppio.animate(self.animate_end_text,id(self)) if error_text=='': self.end_callback('normal',"track has terminated or quit") self=None else: self.mon.err(self,error_text) self.end_callback("error",error_text) self=None # *************************************** # # PLAYING STATE MACHINE # *************************************** """self. play_state controls the playing sequence, it has the following values. I am not entirely sure the starting and ending states are required. - _closed - the mplayer process is not running, mplayer process can be initiated - _starting - mplayer process is running but is not yet able to receive controls - _playing - playing a track, controls can be sent - _ending - mplayer is doing its termination, controls cannot be sent """ def _init_play_state_machine(self): self._stop_required_signal=False self.play_state=AudioPlayer._CLOSED def _start_play_state_machine(self): #initialise all the state machine variables self.duration_count = 0 self._stop_required_signal=False # signal that user has pressed stop #play the track options = self.show_params['mplayer-other-options'] + '-af '+ self.speaker_option+','+self.volume_option + ' ' if self.track<>'': self.mplayer.play(self.track,options) self.mon.log (self,'Playing track from show Id: '+ str(self.show_id)) self.play_state=AudioPlayer._STARTING else: self.play_state=AudioPlayer._PLAYING # and start polling for state changes and count duration self._tick_timer=self.canvas.after(50, self._play_state_machine) def _play_state_machine(self): self.duration_count+=1 if self.play_state == AudioPlayer._CLOSED: self.mon.log(self," State machine: " + self.play_state) return elif self.play_state == AudioPlayer._STARTING: self.mon.log(self," State machine: " + self.play_state) # if mplayer is playing the track change to play state if self.mplayer.start_play_signal==True: self.mon.log(self," <start play signal received from mplayer") self.mplayer.start_play_signal=False self.play_state=AudioPlayer._PLAYING self.mon.log(self," State machine: mplayer_playing started") self._do_starting() self._tick_timer=self.canvas.after(50, self._play_state_machine) elif self.play_state == AudioPlayer._PLAYING: # self.mon.log(self," State machine: " + self.play_state) # service any queued stop signals if self._stop_required_signal==True or (self.duration_limit<>0 and self.duration_count>self.duration_limit): self.mon.log(self," Service stop required signa or timeout") # self._stop_required_signal=False if self.track<>'': self._stop_mplayer() self.play_state = AudioPlayer._ENDING else: self.play_state = AudioPlayer._CLOSED self._end('normal','stop required signa or timeout') # mplayer reports it is terminating so change to ending state if self.track<>'' and self.mplayer.end_play_signal: self.mon.log(self," <end play signal received") self.mon.log(self," <end detected at: " + str(self.mplayer.audio_position)) self.play_state = AudioPlayer._ENDING self._do_playing() self._tick_timer=self.canvas.after(50, self._play_state_machine) elif self.play_state == AudioPlayer._ENDING: # self.mon.log(self," State machine: " + self.play_state) self._do_ending() # if spawned process has closed can change to closed state # self.mon.log (self," State machine : is mplayer process running? - " + str(self.mplayer.is_running())) if self.mplayer.is_running() ==False: self.mon.log(self," <mplayer process is dead") if self._stop_required_signal==True: self._stop_required_signal=False self.play_state = AudioPlayer._CLOSED self._end('normal','mplayer dead') elif self.duration_limit<>0 and self.duration_count<self.duration_limit: self.play_state= AudioPlayer._WAITING self._tick_timer=self.canvas.after(50, self._play_state_machine) else: self.play_state = AudioPlayer._CLOSED self._end('normal','mplayer dead') else: self._tick_timer=self.canvas.after(50, self._play_state_machine) elif self.play_state == AudioPlayer._WAITING: # self.mon.log(self," State machine: " + self.play_state) if self._stop_required_signal==True or (self.duration_limit<>0 and self.duration_count>self.duration_limit): self.mon.log(self," Service stop required signal or timeout from wait") self._stop_required_signal=False self.play_state = AudioPlayer._CLOSED self._end('normal','mplayer dead') else: self._tick_timer=self.canvas.after(50, self._play_state_machine) # allow calling object do things in each state by calling the appropriate callback def _do_playing(self): if self.track<>'': self.audio_position=self.mplayer.audio_position if self.playing_callback<>None: self.playing_callback() def _do_starting(self): self.audio_position=0.0 if self.starting_callback<>None: self.starting_callback() def _do_ending(self): if self.ending_callback<>None: self.ending_callback() def _stop_mplayer(self): # send signal to stop the track to the state machine self.mon.log(self," >stop mplayer received from state machine") if self.play_state==AudioPlayer._PLAYING: self.mplayer.stop() return True else: self.mon.log(self,"!<stop rejected") return False # ***************** # image and text # ***************** def display_image(self): if self.background_file<>'' or self.show_params['show-text']<> '' or self.track_params['track-text']<> '' or self.enable_menu== True or self.track_params['clear-screen']=='yes': self.canvas.config(bg='black') self.canvas.delete(ALL) if self.background_file<>'': self.background_img_file = self.complete_path(self.background_file) if not os.path.exists(self.background_img_file): self.mon.err(self,"Audio background file not found: "+ self.background_img_file) self._end('error',"Audio background file not found") else: pil_background_img=PIL.Image.open(self.background_img_file) self.background = PIL.ImageTk.PhotoImage(pil_background_img) self.drawn = self.canvas.create_image(int(self.canvas['width'])/2, int(self.canvas['height'])/2, image=self.background, anchor=CENTER) # display hint text if enabled if self.enable_menu== True: self.canvas.create_text(int(self.canvas['width'])/2, int(self.canvas['height']) - int(self.show_params['hint-y']), text=self.show_params['hint-text'], fill=self.show_params['hint-colour'], font=self.show_params['hint-font']) # display show text if enabled if self.show_params['show-text']<> '': self.canvas.create_text(int(self.show_params['show-text-x']),int(self.show_params['show-text-y']), anchor=NW, text=self.show_params['show-text'], fill=self.show_params['show-text-colour'], font=self.show_params['show-text-font']) # display track text if enabled if self.track_params['track-text']<> '': self.canvas.create_text(int(self.track_params['track-text-x']),int(self.track_params['track-text-y']), anchor=NW, text=self.track_params['track-text'], fill=self.track_params['track-text-colour'], font=self.track_params['track-text-font']) self.mon.log(self,"Displayed background and text ") self.canvas.update_idletasks( ) def complete_path(self,track_file): # complete path of the filename of the selected entry if track_file[0]=="+": track_file=self.pp_home+track_file[1:] self.mon.log(self,"Background image is "+ track_file) return track_file
class VideoPlayer: _CLOSED = "omx_closed" #probably will not exist _STARTING = "omx_starting" #track is being prepared _PLAYING = "omx_playing" #track is playing to the screen, may be paused _ENDING = "omx_ending" #track is in the process of ending due to quit or end of track # *************************************** # EXTERNAL COMMANDS # *************************************** def __init__(self, show_id, canvas, pp_home, show_params, track_params ): """ canvas - the canvas onto which the video is to be drawn (not!!) cd - configuration dictionary track_params - config dictionary for this track overides cd """ self.mon=Monitor() self.mon.on() #instantiate arguments self.show_id=show_id self.show_params=show_params #configuration dictionary for the videoplayer self.pp_home=pp_home self.canvas = canvas #canvas onto which video should be played but isn't! Use as widget for alarm self.track_params=track_params # get config from medialist if there. if self.track_params['omx-audio']<>"": self.omx_audio= self.track_params['omx-audio'] else: self.omx_audio= self.show_params['omx-audio'] if self.omx_audio<>"": self.omx_audio= "-o "+ self.omx_audio if self.track_params['omx-volume']<>"": self.omx_volume= self.track_params['omx-volume'] else: self.omx_volume= self.show_params['omx-volume'] if self.omx_volume<>"": self.omx_volume= "--vol "+ str(int(self.omx_volume)*100) + ' ' self.omx_volume=' ' #get animation instructions from profile self.animate_begin_text=self.track_params['animate-begin'] self.animate_end_text=self.track_params['animate-end'] #create an instance of PPIO so we can create gpio events self.ppio = PPIO() # could put instance generation in play, not sure which is better. self.omx=OMXDriver(self.canvas) self._tick_timer=None self.error=False self.terminate_required=False self._init_play_state_machine() def play(self, track, end_callback, ready_callback, enable_menu=False, starting_callback=None, playing_callback=None, ending_callback=None): #instantiate arguments self.ready_callback=ready_callback #callback when ready to play self.end_callback=end_callback # callback when finished self.starting_callback=starting_callback #callback during starting state self.playing_callback=playing_callback #callback during playing state self.ending_callback=ending_callback # callback during ending state # enable_menu is not used by videoplayer # create animation events error_text=self.ppio.animate(self.animate_begin_text,id(self)) if error_text<>'': self.mon.err(self,error_text) self.error=True self._end() # and start playing the video. if self.play_state == VideoPlayer._CLOSED: self.mon.log(self,">play track received") self._start_play_state_machine(track) return True else: self.mon.log(self,"!< play track rejected") return False def key_pressed(self,key_name): if key_name=='': return elif key_name in ('p',' '): self._pause() return elif key_name=='escape': self._stop() return def button_pressed(self,button,edge): if button =='pause': self._pause() return elif button=='stop': self._stop() return def terminate(self,reason): # circumvents state machine self.terminate_required=True if self.omx<>None: self.mon.log(self,"sent terminate to omxdriver") self.omx.terminate(reason) else: self.mon.log(self,"terminate, omxdriver not running") self._end() # *************************************** # INTERNAL FUNCTIONS # *************************************** # respond to normal stop def _stop(self): # send signal to stop the track to the state machine self.mon.log(self,">stop received") self._stop_required_signal=True #respond to internal error def _error(self): self.error=True self._stop_required_signal=True #toggle pause def _pause(self): if self.play_state in (VideoPlayer._PLAYING,VideoPlayer._ENDING): self.omx.pause() return True else: self.mon.log(self,"!<pause rejected") return False # other control when playing, used? def _control(self,char): if self.play_state==VideoPlayer._PLAYING: self.mon.log(self,"> send control ot omx: "+ char) self.omx.control(char) return True else: self.mon.log(self,"!<control rejected") return False # called to end omxdriver def _end(self): os.system("xrefresh -display :0") if self._tick_timer<>None: self.canvas.after_cancel(self._tick_timer) self._tick_timer=None if self.error==True: self.end_callback("error",'error') self=None elif self.terminate_required==True: self.end_callback("killed",'killed') self=None else: # clear events list for this track if self.track_params['animate-clear']=='yes': self.ppio.clear_events_list(id(self)) # create animation events for ending error_text=self.ppio.animate(self.animate_end_text,id(self)) if error_text=='': self.end_callback('normal',"track has terminated or quit") self=None else: self.mon.err(self,error_text) self.end_callback("error",'error') self=None # *************************************** # # PLAYING STATE MACHINE # *************************************** """self. play_state controls the playing sequence, it has the following values. I am not entirely sure the starting and ending states are required. - _closed - the omx process is not running, omx process can be initiated - _starting - omx process is running but is not yet able to receive controls - _playing - playing a track, controls can be sent - _ending - omx is doing its termination, controls cannot be sent """ def _init_play_state_machine(self): self._stop_required_signal=False self.play_state=VideoPlayer._CLOSED def _start_play_state_machine(self,track): #initialise all the state machine variables #self.iteration = 0 # for debugging self._stop_required_signal=False # signal that user has pressed stop self.play_state=VideoPlayer._STARTING #play the selected track options=self.omx_audio+ " " + self.omx_volume + ' ' + self.show_params['omx-other-options']+" " self.omx.play(track,options) self.mon.log (self,'Playing track from show Id: '+ str(self.show_id)) # and start polling for state changes self._tick_timer=self.canvas.after(50, self._play_state_machine) def _play_state_machine(self): if self.play_state == VideoPlayer._CLOSED: self.mon.log(self," State machine: " + self.play_state) return elif self.play_state == VideoPlayer._STARTING: self.mon.log(self," State machine: " + self.play_state) # if omxplayer is playing the track change to play state if self.omx.start_play_signal==True: self.mon.log(self," <start play signal received from omx") self.omx.start_play_signal=False # callback to the calling object to e.g remove egg timer. if self.ready_callback<>None: self.ready_callback() self.canvas.config(bg='black') self.canvas.delete(ALL) self.display_image() self.play_state=VideoPlayer._PLAYING self.mon.log(self," State machine: omx_playing started") self._do_starting() self._tick_timer=self.canvas.after(50, self._play_state_machine) elif self.play_state == VideoPlayer._PLAYING: # self.mon.log(self," State machine: " + self.play_state) # service any queued stop signals if self._stop_required_signal==True: self.mon.log(self," Service stop required signal") self._stop_omx() self._stop_required_signal=False self.play_state = VideoPlayer._ENDING # omxplayer reports it is terminating so change to ending state if self.omx.end_play_signal: self.mon.log(self," <end play signal received") self.mon.log(self," <end detected at: " + str(self.omx.video_position)) self.play_state = VideoPlayer._ENDING self._do_playing() self._tick_timer=self.canvas.after(200, self._play_state_machine) elif self.play_state == VideoPlayer._ENDING: self.mon.log(self," State machine: " + self.play_state) self._do_ending() # if spawned process has closed can change to closed state # self.mon.log (self," State machine : is omx process running? - " + str(self.omx.is_running())) if self.omx.is_running() ==False: self.mon.log(self," <omx process is dead") self.play_state = VideoPlayer._CLOSED self._end() else: self._tick_timer=self.canvas.after(200, self._play_state_machine) # allow calling object do things in each state by calling the appropriate callback def _do_playing(self): self.video_position=self.omx.video_position self.audio_position=self.omx.audio_position if self.playing_callback<>None: self.playing_callback() def _do_starting(self): self.video_position=0.0 self.audio_position=0.0 if self.starting_callback<>None: self.starting_callback() def _do_ending(self): if self.ending_callback<>None: self.ending_callback() def _stop_omx(self): # send signal to stop the track to the state machine self.mon.log(self," >stop omx received from state machine") if self.play_state==VideoPlayer._PLAYING: self.omx.stop() return True else: self.mon.log(self,"!<stop rejected") return False # ***************** # image and text # ***************** def display_image(self): # display track text if enabled if self.track_params['track-text']<> '': self.canvas.create_text(int(self.track_params['track-text-x']),int(self.track_params['track-text-y']), anchor=NW, text=self.track_params['track-text'], fill=self.track_params['track-text-colour'], font=self.track_params['track-text-font']) self.mon.log(self,"Displayed text ") self.canvas.update_idletasks( )
class MessagePlayer: """ Displays lines of text in the centre of a coloured screen with background image See pp_imageplayer for common software design description """ # ******************* # external commands # ******************* def __init__(self,show_id,root,canvas,show_params,track_params,pp_dir,pp_home,pp_profile): self.mon=Monitor() self.mon.on() self.root=root self.canvas=canvas self.show_id=show_id self.track_params=track_params self.show_params=show_params self.pp_dir=pp_dir self.pp_home=pp_home self.pp_profile=pp_profile # get config from medialist if there. if 'duration' in self.track_params and self.track_params['duration']<>"": self.duration= int(self.track_params['duration']) else: self.duration= int(self.show_params['duration']) # get background image from profile. self.background_file='' if self.track_params['background-image']<>"": self.background_file= self.track_params['background-image'] else: if self.track_params['display-show-background']=='yes': self.background_file= self.show_params['background-image'] # get background colour from profile. if self.track_params['background-colour']<>"": self.background_colour= self.track_params['background-colour'] else: self.background_colour= self.show_params['background-colour'] self.centre_x = int(self.canvas['width'])/2 self.centre_y = int(self.canvas['height'])/2 # keep tick as an integer sub-multiple of 1 second self.tick = 100 # tick time for image display (milliseconds) self.dwell = 1000*self.duration #get animation instructions from profile self.animate_begin_text=self.track_params['animate-begin'] self.animate_end_text=self.track_params['animate-end'] # open the plugin Manager self.pim=PluginManager(self.show_id,self.root,self.canvas,self.show_params,self.track_params,self.pp_dir,self.pp_home,self.pp_profile) #create an instance of PPIO so we can create gpio events self.ppio = PPIO() def play(self, text, showlist, end_callback, ready_callback, enable_menu=False): # instantiate arguments self.text=text self.showlist=showlist self.end_callback=end_callback self.ready_callback=ready_callback self.enable_menu=enable_menu #init state and signals self.quit_signal=False self.tick_timer=None self.drawn=None # create an instance of showmanager so we can control concurrent shows self.show_manager=ShowManager(self.show_id,self.showlist,self.show_params,self.root,self.canvas,self.pp_dir,self.pp_profile,self.pp_home) # Control other shows at beginning reason,message=self.show_manager.show_control(self.track_params['show-control-begin']) if reason == 'error': self.end_callback(reason,message) self=None else: #display content reason,message=self.display_content() if reason == 'error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # create animation events reason,message=self.ppio.animate(self.animate_begin_text,id(self)) if reason=='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # start text display self.start_dwell() def terminate(self,reason): # no lower level things to terminate so just go to end self.end(reason,'kill or error') def get_links(self): return self.track_params['links'] def input_pressed(self,symbol): self.mon.log(self,"input received: "+symbol) if symbol=='stop': self.stop() # ******************* # internal functions # ******************* def stop(self): self.quit_signal=True # ******************* # sequencing # ******************* def start_dwell(self): self.dwell_counter=0 if self.ready_callback<>None: self.ready_callback() self.tick_timer=self.canvas.after(self.tick, self.do_dwell) def do_dwell(self): if self.quit_signal == True: self.mon.log(self,"quit received") self.end('normal','user quit') else: if self.dwell<>0: self.dwell_counter=self.dwell_counter+1 if self.dwell_counter==self.dwell/self.tick: self.end('normal','finished') else: self.tick_timer=self.canvas.after(self.tick, self.do_dwell) else: self.tick_timer=self.canvas.after(self.tick, self.do_dwell) # ***************** # ending the player # ***************** def end(self,reason,message): # stop the plugin if self.track_params['plugin']<>'': self.pim.stop_plugin() # abort the timer if self.tick_timer<>None: self.canvas.after_cancel(self.tick_timer) self.tick_timer=None if reason in ('error','killed'): self.end_callback(reason,message) self=None else: # normal end so do show control # Control concurrent shows at end reason,message=self.show_manager.show_control(self.track_params['show-control-end']) if reason =='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # clear events list for this track if self.track_params['animate-clear']=='yes': self.ppio.clear_events_list(id(self)) # create animation events for ending reason,message=self.ppio.animate(self.animate_end_text,id(self)) if reason=='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: self.end_callback('normal',"track has terminated or quit") self=None # ***************** # displaying things # ***************** def display_content(self): if self.background_colour<>'': self.canvas.config(bg=self.background_colour) self.canvas.delete('pp-content') if self.background_file<>'': self.background_img_file = self.complete_path(self.background_file) if not os.path.exists(self.background_img_file): self.mon.err(self,"Message background file not found: "+ self.background_img_file) self.end('error',"Message background file not found") else: pil_background_img=PIL.Image.open(self.background_img_file) self.background = PIL.ImageTk.PhotoImage(pil_background_img) self.drawn = self.canvas.create_image(int(self.canvas['width'])/2, int(self.canvas['height'])/2, image=self.background, anchor=CENTER, tag='pp-content') # display show text if enabled if self.show_params['show-text']<> ''and self.track_params['display-show-text']=='yes': self.canvas.create_text(int(self.show_params['show-text-x']),int(self.show_params['show-text-y']), anchor=NW, text=self.show_params['show-text'], fill=self.show_params['show-text-colour'], font=self.show_params['show-text-font'], tag='pp-content') # display track text if enabled if self.track_params['track-text']<> '': self.canvas.create_text(int(self.track_params['track-text-x']),int(self.track_params['track-text-y']), anchor=NW, text=self.track_params['track-text'], fill=self.track_params['track-text-colour'], font=self.track_params['track-text-font'], tag='pp-content') # execute the plugin if required if self.track_params['plugin']<>'': reason,message,self.text = self.pim.do_plugin(self.text,self.track_params['plugin']) if reason <> 'normal': return reason,message # display message text if self.track_params['message-x']<>'': self.canvas.create_text(int(self.track_params['message-x']), int(self.track_params['message-y']), text=self.text.rstrip('\n'), fill=self.track_params['message-colour'], font=self.track_params['message-font'], justify=self.track_params['message-justify'], anchor = 'nw', tag='pp-content') else: self.canvas.create_text(int(self.canvas['width'])/2, int(self.canvas['height'])/2, text=self.text.rstrip('\n'), fill=self.track_params['message-colour'], font=self.track_params['message-font'], justify=self.track_params['message-justify'], tag='pp-content') # display instructions (hint) if self.enable_menu==True: self.canvas.create_text(int(self.show_params['hint-x']), int(self.show_params['hint-y']), text=self.show_params['hint-text'], fill=self.show_params['hint-colour'], font=self.show_params['hint-font'], anchor=NW, tag='pp-content') self.canvas.tag_raise('pp-click-area') self.canvas.update_idletasks( ) return 'normal','' # ***************** # utilities # ***************** def complete_path(self,track_file): # complete path of the filename of the selected entry if track_file[0]=="+": track_file=self.pp_home+track_file[1:] self.mon.log(self,"Background image is "+ track_file) return track_file
class ImagePlayer: """ Displays an image on a canvas for a period of time. Image display can be paused and interrupted __init_ just makes sure that all the things the player needs are available play starts playing the track and returns immeadiately play must end up with a call to tkinter's after, the after callback will interrogae the playing state at intervals and eventually return through end_callback input-pressed receives user input while the track is playing. it might pass the input on to the driver Input-pressed must not wait, it must set a signal and return immeadiately. The signal is interrogated by the after callback. """ # slide state constants NO_SLIDE = 0 SLIDE_DWELL= 1 # ******************* # external commands # ******************* def __init__(self,show_id,root,canvas,show_params,track_params,pp_dir,pp_home,pp_profile): """ show_id - show instance that player is run from (for monitoring only) canvas - the canvas onto which the image is to be drawn show_params - dictionary of show parameters track_params - disctionary of track paramters pp_home - data home directory pp_profile - profile name """ self.mon=Monitor() self.mon.off() self.show_id=show_id self.root=root self.canvas=canvas self.show_params=show_params self.track_params=track_params self.pp_dir=pp_dir self.pp_home=pp_home self.pp_profile=pp_profile # open resources self.rr=ResourceReader() # get parameters self.animate_begin_text=self.track_params['animate-begin'] self.animate_end_text=self.track_params['animate-end'] if self.track_params['duration']<>"": self.duration= int(self.track_params['duration']) else: self.duration= int(self.show_params['duration']) #create an instance of PPIO so we can create gpio events self.ppio = PPIO() # get background image from profile. self.background_file='' if self.track_params['background-image']<>'': self.background_file= self.track_params['background-image'] else: if self.track_params['display-show-background']=='yes': self.background_file= self.show_params['background-image'] # get background colour from profile. if self.track_params['background-colour']<>"": self.background_colour= self.track_params['background-colour'] else: self.background_colour= self.show_params['background-colour'] # get image window from profile if self.track_params['image-window'].strip()<>"": self.image_window= self.track_params['image-window'].strip() else: self.image_window= self.show_params['image-window'].strip() # open the plugin Manager self.pim=PluginManager(self.show_id,self.root,self.canvas,self.show_params,self.track_params,self.pp_dir,self.pp_home,self.pp_profile) def play(self, track, showlist, end_callback, ready_callback, enable_menu=False): """ track - filename of track to be played showlist - from which track was taken end_callback - callback when player terminates ready_callback - callback just before anytthing is displayed enable_menu - there will be a child track so display the hint text """ # instantiate arguments self.track=track self.showlist=showlist self.enable_menu=enable_menu self.ready_callback=ready_callback self.end_callback=end_callback #init state and signals self.canvas_centre_x = int(self.canvas['width'])/2 self.canvas_centre_y = int(self.canvas['height'])/2 self.tick = 100 # tick time for image display (milliseconds) self.dwell = 10*self.duration self.dwell_counter=0 self.state=ImagePlayer.NO_SLIDE self.quit_signal=False self.drawn=None self.paused=False self.pause_text=None self.tick_timer=None #parse the image_window error,self.command,self.has_coords,self.image_x1,self.image_y1,self.image_x2,self.image_y2,self.filter=self.parse_window(self.image_window) if error =='error': self.mon.err(self,'image window error: '+self.image_window) self.end('error','image window error') else: # create an instance of showmanager so we can control concurrent shows self.show_manager=ShowManager(self.show_id,self.showlist,self.show_params,self.root,self.canvas,self.pp_dir,self.pp_profile,self.pp_home) # Control other shows at beginning reason,message=self.show_manager.show_control(self.track_params['show-control-begin']) if reason == 'error': self.end_callback(reason,message) self=None else: #display content reason,message=self.display_content() if reason == 'error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # create animation events reason,message=self.ppio.animate(self.animate_begin_text,id(self)) if reason=='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # start dwelling self.mon.log(self,'playing track from show Id: '+str(self.show_id)) self.start_dwell() def input_pressed(self,symbol): if symbol =='pause': self.pause() elif symbol=='stop': self.stop() return def terminate(self,reason): # no lower level things to terminate so just go to end self.end(reason,'kill or error') def get_links(self): return self.track_params['links'] # ******************* # internal functions # ******************* def pause(self): if not self.paused: self.paused = True else: self.paused=False #print "self.paused is "+str(self.paused) def stop(self): self.quit_signal=True # ****************************************** # Sequencing # ******************************************** def start_dwell(self): if self.ready_callback<>None: self.ready_callback() self.state=ImagePlayer.SLIDE_DWELL self.tick_timer=self.canvas.after(self.tick, self.do_dwell) def do_dwell(self): if self.quit_signal == True: self.mon.log(self,"quit received") self.end('normal','user quit') else: if self.paused == False: self.dwell_counter=self.dwell_counter+1 #print "self.paused is "+str(self.paused) #if self.pause_text<>None: #print "self.pause_text exists" # one time flipping of pause text #NIK if self.paused==True and self.pause_text==None: self.pause_text=self.canvas.create_text(0,900, anchor=NW, text=self.resource('imageplayer','m01'), fill="white", font="arial 25 bold") self.canvas.update_idletasks( ) #NIK if self.paused==False and self.pause_text<>None: self.canvas.delete(self.pause_text) self.pause_text=None self.canvas.update_idletasks( ) if self.dwell<>0 and self.dwell_counter==self.dwell: self.end('normal','user quit or duration exceeded') else: self.tick_timer=self.canvas.after(self.tick, self.do_dwell) # ***************** # ending the player # ***************** def end(self,reason,message): self.state=self.NO_SLIDE # stop the plugin if self.track_params['plugin']<>'': self.pim.stop_plugin() # abort the timer if self.tick_timer<>None: self.canvas.after_cancel(self.tick_timer) self.tick_timer=None if reason in ('error','killed'): self.end_callback(reason,message) self=None else: # normal end so do show control and animation # Control concurrent shows at end reason,message=self.show_manager.show_control(self.track_params['show-control-end']) if reason =='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # clear events list for this track if self.track_params['animate-clear']=='yes': self.ppio.clear_events_list(id(self)) # create animation events for ending reason,message=self.ppio.animate(self.animate_end_text,id(self)) if reason=='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: self.end_callback('normal',"track has terminated or quit") #NIK if self.pause_text<>None: self.canvas.delete(self.pause_text) self.pause_text=None self.canvas.update_idletasks( ) #NIK self=None # ********************************** # displaying things # ********************************** def display_content(self): #background colour if self.background_colour<>'': self.canvas.config(bg=self.background_colour) self.canvas.delete('pp-content') # background image if self.background_file<> '': self.background_img_file = self.complete_path(self.background_file) if not os.path.exists(self.background_img_file): self.mon.err(self,"Background file not found: "+ self.background_img_file) self.end('error',"Background file not found") else: pil_background_img=PIL.Image.open(self.background_img_file) self.background = PIL.ImageTk.PhotoImage(pil_background_img) self.drawn = self.canvas.create_image(int(self.canvas['width'])/2, int(self.canvas['height'])/2, image=self.background, anchor=CENTER, tag='pp-content') # execute the plugin if required if self.track_params['plugin']<>'': reason,message,self.track = self.pim.do_plugin(self.track,self.track_params['plugin'],) if reason <> 'normal': return reason,message #get the track to be displayed if os.path.exists(self.track)==True: self.pil_image=PIL.Image.open(self.track) else: self.pil_image=None # display track image if self.pil_image<>None: self.image_width,self.image_height=self.pil_image.size if self.command=='original': # display image at its original size if self.has_coords==False: # load and display the unmodified image in centre self.tk_img=PIL.ImageTk.PhotoImage(self.pil_image) self.drawn = self.canvas.create_image(self.canvas_centre_x, self.canvas_centre_y, image=self.tk_img, anchor=CENTER, tag='pp-content') else: # load and display the unmodified image at x1,y1 self.tk_img=PIL.ImageTk.PhotoImage(self.pil_image) self.drawn = self.canvas.create_image(self.image_x1, self.image_y1, image=self.tk_img, anchor=NW, tag='pp-content') elif self.command in ('fit','shrink'): # shrink fit the window or screen preserving aspect if self.has_coords==True: window_width=self.image_x2 - self.image_x1 window_height=self.image_y2 - self.image_y1 window_centre_x=(self.image_x2+self.image_x1)/2 window_centre_y= (self.image_y2+self.image_y1)/2 else: window_width=int(self.canvas['width']) window_height=int(self.canvas['height']) window_centre_x=self.canvas_centre_x window_centre_y=self.canvas_centre_y if (self.image_width > window_width or self.image_height > window_height and self.command=='fit') or (self.command=='shrink') : # original image is larger or , shrink it to fit the screen preserving aspect self.pil_image.thumbnail((window_width,window_height),eval(self.filter)) self.tk_img=PIL.ImageTk.PhotoImage(self.pil_image) self.drawn = self.canvas.create_image(window_centre_x, window_centre_y, image=self.tk_img, anchor=CENTER, tag='pp-content') else: # fitting and original image is smaller, expand it to fit the screen preserving aspect prop_x = float(window_width) / self.image_width prop_y = float(window_height) / self.image_height if prop_x > prop_y: prop=prop_y else: prop=prop_x increased_width=int(self.image_width * prop) increased_height=int(self.image_height * prop) # print 'result',prop, increased_width,increased_height self.pil_image=self.pil_image.resize((increased_width, increased_height),eval(self.filter)) self.tk_img=PIL.ImageTk.PhotoImage(self.pil_image) self.drawn = self.canvas.create_image(window_centre_x, window_centre_y, image=self.tk_img, anchor=CENTER, tag='pp-content') elif self.command in ('warp'): # resize to window or screen without preserving aspect if self.has_coords==True: window_width=self.image_x2 - self.image_x1 window_height=self.image_y2 - self.image_y1 window_centre_x=(self.image_x2+self.image_x1)/2 window_centre_y= (self.image_y2+self.image_y1)/2 else: window_width=int(self.canvas['width']) window_height=int(self.canvas['height']) window_centre_x=self.canvas_centre_x window_centre_y=self.canvas_centre_y self.pil_image=self.pil_image.resize((window_width, window_height),eval(self.filter)) self.tk_img=PIL.ImageTk.PhotoImage(self.pil_image) self.drawn = self.canvas.create_image(window_centre_x, window_centre_y, image=self.tk_img, anchor=CENTER, tag='pp-content') # display hint if enabled if self.enable_menu== True: self.canvas.create_text(int(self.show_params['hint-x']), int(self.show_params['hint-y']), text=self.show_params['hint-text'], fill=self.show_params['hint-colour'], font=self.show_params['hint-font'], anchor=NW, tag='pp-content') # display show text if enabled if self.show_params['show-text']<> ''and self.track_params['display-show-text']=='yes': self.canvas.create_text(int(self.show_params['show-text-x']),int(self.show_params['show-text-y']), anchor=NW, text=self.show_params['show-text'], fill=self.show_params['show-text-colour'], font=self.show_params['show-text-font'], tag='pp-content') # display track text if enabled if self.track_params['track-text']<> '': self.canvas.create_text(int(self.track_params['track-text-x']),int(self.track_params['track-text-y']), anchor=NW, text=self.track_params['track-text'], fill=self.track_params['track-text-colour'], font=self.track_params['track-text-font'], tag='pp-content') self.canvas.tag_raise('pp-click-area') self.canvas.update_idletasks( ) return 'normal','' # ********************************** # utilties # ********************************** def parse_window(self,line): fields = line.split() # check there is a command field if len(fields) < 1: return 'error','',False,0,0,0,0,'' # deal with original whch has 0 or 2 arguments filter='' if fields[0]=='original': if len(fields) not in (1,3): return 'error','',False,0,0,0,0,'' # deal with window coordinates if len(fields) == 3: #window is specified if not (fields[1].isdigit() and fields[2].isdigit()): return 'error','',False,0,0,0,0,'' has_window=True return 'normal',fields[0],has_window,int(fields[1]),int(fields[2]),0,0,filter else: # no window has_window=False return 'normal',fields[0],has_window,0,0,0,0,filter #deal with remainder which has 1, 2, 5 or 6arguments # check basic syntax if fields[0] not in ('shrink','fit','warp'): return 'error','',False,0,0,0,0,'' if len(fields) not in (1,2,5,6): return 'error','',False,0,0,0,0,'' if len(fields)==6 and fields[5] not in ('NEAREST','BILINEAR','BICUBIC','ANTIALIAS'): return 'error','',False,0,0,0,0,'' if len(fields)==2 and fields[1] not in ('NEAREST','BILINEAR','BICUBIC','ANTIALIAS'): return 'error','',False,0,0,0,0,'' # deal with window coordinates if len(fields) in (5,6): #window is specified if not (fields[1].isdigit() and fields[2].isdigit() and fields[3].isdigit() and fields[4].isdigit()): return 'error','',False,0,0,0,0,'' has_window=True if len(fields)==6: filter=fields[5] else: filter='PIL.Image.NEAREST' return 'normal',fields[0],has_window,int(fields[1]),int(fields[2]),int(fields[3]),int(fields[4]),filter else: # no window has_window=False if len(fields)==2: filter=fields[1] else: filter='PIL.Image.NEAREST' return 'normal',fields[0],has_window,0,0,0,0,filter def complete_path(self,track_file): # complete path of the filename of the selected entry if track_file[0]=="+": track_file=self.pp_home+track_file[1:] self.mon.log(self,"Background image is "+ track_file) return track_file # get a text string from resources.cfg def resource(self,section,item): value=self.rr.get(section,item) if value==False: self.mon.err(self, "resource: "+section +': '+ item + " not found" ) self.error() else: return value
class ImagePlayer: """ Displays an image on a canvas for a period of time. Image display can be interrupted Implements animation of transitions but Pi is too slow without GPU aceleration.""" # slide state constants NO_SLIDE = 0 SLIDE_IN = 1 SLIDE_DWELL= 2 SLIDE_OUT= 3 # ******************* # external commands # ******************* def __init__(self,show_id,canvas,pp_home,show_params,track_params): """ canvas - the canvas onto which the image is to be drawn cd - dictionary of show parameters track_params - disctionary of track paramters """ self.mon=Monitor() self.mon.on() self.show_id=show_id self.canvas=canvas self.pp_home=pp_home self.show_params=show_params self.track_params=track_params # open resources self.rr=ResourceReader() # get config from medialist if there. self.animate_begin_text=self.track_params['animate-begin'] self.animate_end_text=self.track_params['animate-end'] if 'duration' in self.track_params and self.track_params['duration']<>"": self.duration= int(self.track_params['duration']) else: self.duration= int(self.show_params['duration']) if 'transition' in self.track_params and self.track_params['transition']<>"": self.transition= self.track_params['transition'] else: self.transition= self.show_params['transition'] # keep dwell and porch as an integer multiple of tick self.porch = 1000 #length of pre and post porches for an image (milliseconds) self.tick = 100 # tick time for image display (milliseconds) self.dwell = (1000*self.duration)- (2*self.porch) if self.dwell<0: self.dwell=0 self.centre_x = int(self.canvas['width'])/2 self.centre_y = int(self.canvas['height'])/2 #create an instance of PPIO so we can create gpio events self.ppio = PPIO() def play(self, track, end_callback, ready_callback, enable_menu=False, starting_callback=None, playing_callback=None, ending_callback=None): # instantiate arguments self.track=track self.enable_menu=enable_menu self.ready_callback=ready_callback self.end_callback=end_callback #init state and signals self.state=ImagePlayer.NO_SLIDE self.quit_signal=False self.kill_required_signal=False self.error=False self._tick_timer=None self.drawn=None self.paused=False self.pause_text=None if os.path.exists(self.track)==True: self.pil_image=PIL.Image.open(self.track) # adjust brightness and rotate (experimental) # pil_image_enhancer=PIL.ImageEnhance.Brightness(pil_image) # pil_image=pil_image_enhancer.enhance(0.1) # pil_image=pil_image.rotate(45) # tk_image = PIL.ImageTk.PhotoImage(pil_image) else: self.pil_image=None # and start image rendering self.mon.log(self,'playing track from show Id: '+str(self.show_id)) self._start_front_porch() def key_pressed(self,key_name): if key_name=='': return elif key_name in ('p',' '): self.pause() elif key_name=='escape': self._stop() return def button_pressed(self,button,edge): if button =='pause': self.pause() elif button=='stop': self._stop() return def terminate(self,reason): if reason=='error': self.error=True self.quit_signal=True else: self.kill_required_signal=True self.quit_signal=True def pause(self): if not self.paused: self.paused = True else: self.paused=False # ******************* # internal functions # ******************* def _stop(self): self.quit_signal=True def _error(self): self.error=True self.quit_signal=True #called when back porch has completed or quit signal is received def _end(self,reason,message): if self._tick_timer<>None: self.canvas.after_cancel(self._tick_timer) self._tick_timer=None self.quit_signal=False # self.canvas.delete(ALL) self.canvas.update_idletasks( ) self.state=self.NO_SLIDE if self.error==True or reason=='error': self.end_callback("error",message) self=None elif self.kill_required_signal==True: self.end_callback("killed",message) self=None else: # clear events list for this track if self.track_params['animate-clear']=='yes': self.ppio.clear_events_list(id(self)) # create animation events for ending error_text=self.ppio.animate(self.animate_end_text,id(self)) if error_text=='': self.end_callback('normal',"track has terminated or quit") self=None else: self.mon.err(self,error_text) self.end_callback("error",'error') self=None def resource(self,section,item): value=self.rr.get(section,item) if value==False: self.mon.err(self, "resource: "+section +': '+ item + " not found" ) self._error() else: return value def _start_front_porch(self): self.state=ImagePlayer.SLIDE_IN self.porch_counter=0 if self.ready_callback<>None: self.ready_callback() if self.pil_image<>None or self.enable_menu== True or self.show_params['show-text']<> '' or self.track_params['track-text']<> '': self.canvas.delete(ALL) if self.transition=="cut": #just display the slide full brightness. No need for porch but used for symmetry if self.pil_image<>None: self.tk_img=PIL.ImageTk.PhotoImage(self.pil_image) self.drawn = self.canvas.create_image(self.centre_x, self.centre_y, image=self.tk_img, anchor=CENTER) elif self.transition=="fade": #experimental start black and increase brightness (controlled by porch_counter). self._display_image() elif self.transition == "slide": #experimental, start in middle and move to right (controlled by porch_counter) if self.pil_image<>None: self.tk_img=PIL.ImageTk.PhotoImage(self.pil_image) self.drawn = self.canvas.create_image(self.centre_x, self.centre_y, image=self.tk_img, anchor=CENTER) elif self.transition=="crop": #experimental, start in middle and crop from right (controlled by porch_counter) if self.pil_image<>None: self.tk_img=PIL.ImageTk.PhotoImage(self.pil_image) self.drawn = self.canvas.create_image(self.centre_x, self.centre_y, image=self.tk_img, anchor=CENTER) self.canvas.update_idletasks() # create animation events error_text=self.ppio.animate(self.animate_begin_text,id(self)) if error_text<>'': self.mon.err(self,error_text) self.error=True self._end() self._tick_timer=self.canvas.after(self.tick, self._do_front_porch) def _do_front_porch(self): if self.quit_signal == True: self._end('normal','user quit') else: self.porch_counter=self.porch_counter+1 # print "doing slide front porch " +str(self.porch_counter) self.canvas.config(bg='black') self._display_image() if self.porch_counter==self.porch/self.tick: self._start_dwell() else: self._tick_timer=self.canvas.after(self.tick,self._do_front_porch) def _start_dwell(self): self.state=ImagePlayer.SLIDE_DWELL self.dwell_counter=0 self._tick_timer=self.canvas.after(self.tick, self._do_dwell) def _do_dwell(self): if self.quit_signal == True: self.mon.log(self,"quit received") self._end('normal','user quit') else: if self.paused == False: self.dwell_counter=self.dwell_counter+1 # one time flipping of pause text if self.paused==True and self.pause_text==None: self.pause_text=self.canvas.create_text(100,100, anchor=NW, text=self.resource('imageplayer','m01'), fill="white", font="arial 25 bold") self.canvas.update_idletasks( ) if self.paused==False and self.pause_text<>None: self.canvas.delete(self.pause_text) self.pause_text=None self.canvas.update_idletasks( ) if self.dwell_counter==self.dwell/self.tick: self._start_back_porch() else: self._tick_timer=self.canvas.after(self.tick, self._do_dwell) def _start_back_porch(self): self.state=ImagePlayer.SLIDE_OUT self.porch_counter=self.porch/self.tick if self.transition=="cut": # just keep displaying the slide full brightness. # No need for porch but used for symmetry pass elif self.transition=="fade": #experimental start full and decrease brightness (controlled by porch_counter). self._display_image() elif self.transition== "slide": #experimental, start in middle and move to right (controlled by porch_counter) if self.pil_image<>None: self.tk_img=PIL.ImageTk.PhotoImage(self.pil_image) self.drawn = self.canvas.create_image(self.centre_x, self.centre_y, image=self.tk_img, anchor=CENTER) elif self.transition =="crop": #experimental, start in middle and crop from right (controlled by porch_counter) if self.pil_image<>None: self.tk_img=PIL.ImageTk.PhotoImage(self.pil_image) self.drawn = self.canvas.create_image(self.centre_x, self.centre_y, image=self.tk_img, anchor=CENTER) self._tick_timer=self.canvas.after(self.tick, self._do_back_porch) def _do_back_porch(self): if self.quit_signal == True: self._end('normal','user quit') else: self.porch_counter=self.porch_counter-1 self._display_image() if self.porch_counter==0: self._end('normal','finished') else: self._tick_timer=self.canvas.after(self.tick,self._do_back_porch) def _display_image(self): if self.transition=="cut": pass # all the methods below have incorrect code !!! elif self.transition=="fade": if self.pil_image<>None: self.enh=PIL.ImageEnhance.Brightness(self.pil_image) prop=float(self.porch_counter)/float(20) #???????? self.pil_img=self.enh.enhance(prop) self.tk_img=PIL.ImageTk.PhotoImage(self.pil_img) self.drawn = self.canvas.create_image(self.centre_x, self.centre_y, image=self.tk_img, anchor=CENTER) elif self.transition=="slide": if self.pil_image<>None: self.canvas.move(self.drawn,5,0) elif self.transition=="crop": if self.pil_image<>None: self.crop= 10*self.porch_counter self.pil_img=self.pil_image.crop((0,0,1000-self.crop,1080)) self.tk_img=PIL.ImageTk.PhotoImage(self.pil_img) self.drawn = self.canvas.create_image(self.centre_x, self.centre_y, image=self.tk_img, anchor=CENTER) # display instructions if enabled if self.enable_menu== True: self.canvas.create_text(self.centre_x, int(self.canvas['height']) - int(self.show_params['hint-y']), text=self.show_params['hint-text'], fill=self.show_params['hint-colour'], font=self.show_params['hint-font']) # display show text if enabled if self.show_params['show-text']<> '': self.canvas.create_text(int(self.show_params['show-text-x']),int(self.show_params['show-text-y']), anchor=NW, text=self.show_params['show-text'], fill=self.show_params['show-text-colour'], font=self.show_params['show-text-font']) # display track text if enabled if self.track_params['track-text']<> '': self.canvas.create_text(int(self.track_params['track-text-x']),int(self.track_params['track-text-y']), anchor=NW, text=self.track_params['track-text'], fill=self.track_params['track-text-colour'], font=self.track_params['track-text-font']) self.canvas.update_idletasks( )