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, canvas, cd, 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.cd=cd #configuration dictionary for the videoplayer 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 'omx-audio' in self.track_params and self.track_params['omx-audio']<>"": self.omx_audio= self.track_params['omx-audio'] else: self.omx_audio= self.cd['omx-audio'] if self.omx_audio<>"": self.omx_audio= "-o "+ self.omx_audio # could put instance generation in play, not sure which is better. self.omx=OMXDriver() 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 # 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): 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: self.end_callback('normal',"track has terminated") 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.cd['omx-other-options']+" " self.omx.play(track,options) # 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.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
class VideoPlayer(Player): """ plays a track using omxplayer _init_ iniitalises state and checks resources are available. use the returned instance reference in all other calls. At the end of the path (when closed) do not re-use, make instance= None and start again. States - 'initialised' when done successfully Initialisation is immediate so just returns with error code. """ debug = False debug = True def __init__(self, show_id, showlist, root, canvas, show_params, track_params, pp_dir, pp_home, pp_profile, end_callback, command_callback): # initialise items common to all players Player.__init__(self, show_id, showlist, root, canvas, show_params, track_params, pp_dir, pp_home, pp_profile, end_callback, command_callback) # print ' !!!!!!!!!!!videoplayer init' self.mon.trace(self, '') self.video_start_timestamp = 0 self.dm = DisplayManager() # get player parameters 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 self.omx_max_volume_text = self.track_params['omx-max-volume'] if self.omx_max_volume_text != "": self.omx_max_volume = int(self.omx_max_volume_text) else: self.omx_max_volume = 0 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 = int(self.omx_volume) else: self.omx_volume = 0 self.omx_volume = min(self.omx_volume, self.omx_max_volume) if self.track_params['omx-window'] != '': self.omx_window = self.track_params['omx-window'] else: self.omx_window = self.show_params['omx-window'] if self.track_params['omx-other-options'] != '': self.omx_other_options = self.track_params['omx-other-options'] else: self.omx_other_options = self.show_params['omx-other-options'] if self.track_params['freeze-at-start'] != '': self.freeze_at_start = self.track_params['freeze-at-start'] else: self.freeze_at_start = self.show_params['freeze-at-start'] if self.track_params['freeze-at-end'] != '': freeze_at_end_text = self.track_params['freeze-at-end'] else: freeze_at_end_text = self.show_params['freeze-at-end'] if freeze_at_end_text == 'yes': self.freeze_at_end_required = True else: self.freeze_at_end_required = False if self.track_params['seamless-loop'] == 'yes': self.seamless_loop = ' --loop ' else: self.seamless_loop = '' if self.track_params['pause-timeout'] != '': pause_timeout_text = self.track_params['pause-timeout'] else: pause_timeout_text = self.show_params['pause-timeout'] if pause_timeout_text.isdigit(): self.pause_timeout = int(pause_timeout_text) else: self.pause_timeout = 0 # initialise video playing state and signals self.quit_signal = False self.unload_signal = False self.play_state = 'initialised' self.frozen_at_end = False self.pause_timer = None # LOAD - creates and omxplayer instance, loads a track and then pause def load(self, track, loaded_callback, enable_menu): # instantiate arguments self.track = track self.loaded_callback = loaded_callback #callback when loaded # print '!!!!!!!!!!! videoplayer load',self.track self.mon.log( self, "Load track received from show Id: " + str(self.show_id) + ' ' + self.track) self.mon.trace(self, '') # do common bits of load Player.pre_load(self) # set up video display if self.track_params['display-name'] != "": video_display_name = self.track_params['display-name'] else: video_display_name = self.show_canvas_display_name status, message, self.omx_display_id = self.dm.id_of_display( video_display_name) if status == 'error': self.mon.err(self, message) self.play_state = 'load-failed' if self.loaded_callback is not None: self.loaded_callback( 'error', 'Display not connected: ' + video_display_name) return # set up video window and calc orientation status, message, command, has_window, x1, y1, x2, y2 = self.parse_video_window( self.omx_window, self.omx_display_id) if status == 'error': self.mon.err( self, 'omx window error: ' + message + ' in ' + self.omx_window) self.play_state = 'load-failed' if self.loaded_callback is not None: self.loaded_callback( 'error', 'omx window error: ' + message + ' in ' + self.omx_window) return else: if has_window is True: self.omx_window_processed = '--win " ' + str(x1) + ' ' + str( y1) + ' ' + str(x2) + ' ' + str(y2) + ' " ' else: self.omx_window_processed = self.omx_aspect_mode # load the plugin, this may modify self.track and enable the plugin drawign to canvas if self.track_params['plugin'] != '': status, message = self.load_plugin() if status == 'error': self.mon.err(self, message) self.play_state = 'load-failed' if self.loaded_callback is not None: self.loaded_callback('error', message) return # load the images and text status, message = self.load_x_content(enable_menu) if status == 'error': self.mon.err(self, message) self.play_state = 'load-failed' if self.loaded_callback is not None: self.loaded_callback('error', message) return if not (track[0:3] in ('udp', 'tcp') or track[0:4] in ('rtsp', 'rtmp')): if not os.path.exists(track): self.mon.err(self, "Track file not found: " + track) self.play_state = 'load-failed' if self.loaded_callback is not None: self.loaded_callback('error', 'track file not found: ' + track) return self.omx = OMXDriver(self.canvas, self.pp_dir) self.start_state_machine_load(self.track) # SHOW - show a track def show(self, ready_callback, finished_callback, closed_callback): # print "!!!! videoplayer show" # instantiate arguments self.ready_callback = ready_callback # callback when ready to show video self.finished_callback = finished_callback # callback when finished showing self.closed_callback = closed_callback self.mon.trace(self, '') # do animation at start etc. Player.pre_show(self) # start show state machine self.start_state_machine_show() self.video_start_timestamp = self.omx.video_start_timestamp global globalVideoTimestampStart globalVideoTimestampStart = self.video_start_timestamp self.mon.log( self, "Timestamp from show starting: " + str(self.omx.video_start_timestamp)) # UNLOAD - abort a load when omplayer is loading or loaded def unload(self): self.mon.trace(self, '') self.mon.log(self, ">unload received from show Id: " + str(self.show_id)) self.start_state_machine_unload() # CLOSE - quits omxplayer from 'pause at end' state def close(self, closed_callback): self.mon.trace(self, '') self.mon.log(self, ">close received from show Id: " + str(self.show_id)) self.closed_callback = closed_callback self.start_state_machine_close() def input_pressed(self, symbol): if symbol[0:4] == 'omx-': if symbol[0:5] in ('omx-+', 'omx--', 'omx-='): if symbol[4] in ('+', '='): self.inc_volume() else: self.dec_volume() else: self.control(symbol[4]) elif symbol == 'inc-volume': self.inc_volume() elif symbol == 'dec-volume': self.dec_volume() elif symbol == 'pause': self.pause() elif symbol == 'go': self.go() elif symbol == 'unmute': self.unmute() elif symbol == 'mute': self.mute() elif symbol == 'pause-on': self.pause_on() elif symbol == 'pause-off': self.pause_off() elif symbol == 'stop': self.stop() # respond to normal stop def stop(self): self.mon.log(self, ">stop received from show Id: " + str(self.show_id)) # cancel the pause timer if self.pause_timer != None: self.canvas.after_cancel(self.pause_timer) self.pause_timer = None # send signal to freeze the track - causes either pause or quit depends on freeze at end if self.freeze_at_end_required is True: if self.play_state == 'showing' and self.frozen_at_end is False: self.frozen_at_end = True # pause the track self.omx.pause('freeze at end from user stop') self.quit_signal = True # and return to show so it can end the track and the video in track ready callback ## if self.finished_callback is not None: ## # print 'finished from stop' ## self.finished_callback('pause_at_end','pause at end') else: self.mon.log(self, "!<stop rejected") else: # freeze not required and its showing just stop the video if self.play_state == 'showing': self.quit_signal = True else: self.mon.log(self, "!<stop rejected") def inc_volume(self): self.mon.log(self, ">inc-volume received from show Id: " + str(self.show_id)) if self.play_state == 'showing' and self.frozen_at_end is False and self.omx.paused_at_start == 'done': self.omx.inc_volume() return True else: self.mon.log(self, "!<inc-volume rejected " + self.play_state) return False def dec_volume(self): self.mon.log(self, ">dec-volume received from show Id: " + str(self.show_id)) if self.play_state == 'showing' and self.frozen_at_end is False and self.omx.paused_at_start == 'done': self.omx.dec_volume() return True else: self.mon.log(self, "!<dec-volume rejected " + self.play_state) return False def mute(self): self.mon.log(self, ">mute received from show Id: " + str(self.show_id)) if self.play_state == 'showing' and self.frozen_at_end is False and self.omx.paused_at_start == 'done': self.omx.mute() return True else: self.mon.log(self, "!<mute rejected " + self.play_state) return False def unmute(self): self.mon.log(self, ">unmute received from show Id: " + str(self.show_id)) if self.play_state == 'showing' and self.frozen_at_end is False and self.omx.paused_at_start == 'done': self.omx.unmute() return True else: self.mon.log(self, "!<unmute rejected " + self.play_state) return False # toggle pause def pause(self): self.mon.log( self, ">toggle pause received from show Id: " + str(self.show_id)) if self.play_state == 'showing' and self.frozen_at_end is False and self.omx.paused_at_start == 'done': self.omx.toggle_pause('user') if self.omx.paused is True and self.pause_timeout > 0: # kick off the pause teimeout timer self.pause_timer = self.canvas.after( self.pause_timeout * 1000, self.pause_timeout_callback) else: # cancel the pause timer if self.pause_timer != None: self.canvas.after_cancel(self.pause_timer) self.pause_timer = None return True else: self.mon.log(self, "!<pause rejected " + self.play_state) return False def pause_timeout_callback(self): self.pause_off() self.pause_timer = None # pause on def pause_on(self): self.mon.log(self, ">pause on received from show Id: " + str(self.show_id)) if self.play_state == 'showing' and self.frozen_at_end is False and self.omx.paused_at_start == 'done': self.omx.pause_on() if self.omx.paused is True and self.pause_timeout > 0: # kick off the pause teimeout timer self.pause_timer = self.canvas.after( self.pause_timeout * 1000, self.pause_timeout_callback) return True else: self.mon.log(self, "!<pause on rejected " + self.play_state) return False # pause off def pause_off(self): self.mon.log(self, ">pause off received from show Id: " + str(self.show_id)) if self.play_state == 'showing' and self.frozen_at_end is False and self.omx.paused_at_start == 'done': self.omx.pause_off() if self.omx.paused is False: # cancel the pause timer if self.pause_timer != None: self.canvas.after_cancel(self.pause_timer) self.pause_timer = None return True else: self.mon.log(self, "!<pause off rejected " + self.play_state) return False # go after freeze at start def go(self): self.mon.log(self, ">go received from show Id: " + str(self.show_id)) if self.play_state == 'showing' and self.omx.paused_at_start == 'True': self.omx.go() return True else: self.mon.log(self, "!<go rejected " + self.play_state) return False # other control when playing def control(self, char): if self.play_state == 'showing' and self.frozen_at_end is False and self.omx.paused_at_start == 'done' 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 # *********************** # track showing state machine # ********************** """ STATES OF STATE MACHINE Threre are ongoing states and states that are set just before callback >init - Create an instance of the class <On return - state = initialised - - init has been completed, do not generate errors here >load Fatal errors should be detected in load. If so loaded_callback is called with 'load-failed' Ongoing - state=loading - load called, waiting for load to complete < loaded_callback with status = normal state=loaded - load has completed and video paused before first frame <loaded_callback with status=error state= load-failed - omxplayer process has been killed because of failure to load On getting the loaded_callback with status=normal the track can be shown using show Duration obtained from track should always cause pause_at_end. if not please tell me as the fudge factor may need adjusting. >show show assumes a track has been loaded and is paused. Ongoing - state=showing - video is showing <finished_callback with status = pause_at_end state=showing but frozen_at_end is True <closed_callback with status= normal state = closed - video has ended omxplayer has terminated. On getting finished_callback with status=pause_at end a new track can be shown and then use close to quit the video when new track is ready On getting closed_callback with status= nice_day omxplayer closing should not be attempted as it is already closed Do not generate user errors in Show. Only generate system erros such as illegal state abd then use end() >close Ongoing state - closing - omxplayer processes are dying due to quit sent <closed_callback with status= normal - omxplayer is dead, can close the track instance. >unload Ongoing states - start_unload and unloading - omxplayer processes are dying due to quit sent. when unloading is complete state=unloaded I have not added a callback to unload. its easy to add one if you want. closed is needed because wait_for end in pp_show polls for closed and does not use closed_callback """ def start_state_machine_load(self, track): self.track = track # initialise all the state machine variables self.loading_count = 0 # initialise loading timeout counter self.play_state = 'loading' # load the selected track # options= ' ' + self.omx_audio+ ' --vol -6000 ' + self.omx_window_processed + ' ' + self.seamless_loop + ' ' + self.omx_other_options +" " options = ' --display ' + str( self.omx_display_id ) + ' --no-osd ' + self.omx_audio + ' --vol -6000 ' + self.omx_window_processed + self.omx_rotate + ' ' + self.seamless_loop + ' ' + self.omx_other_options + " " self.omx.load(track, self.freeze_at_start, options, self.mon.pretty_inst(self), self.omx_volume, self.omx_max_volume) # self.mon.log (self,'Send load command track '+ self.track + 'with options ' + options + 'from show Id: '+ str(self.show_id)) # print 'omx.load started ',self.track # and start polling for state changes self.tick_timer = self.canvas.after(50, self.load_state_machine) def start_state_machine_unload(self): # print ('videoplayer - starting unload',self.play_state) if self.play_state in ('closed', 'initialised', 'unloaded'): # omxplayer already closed self.play_state = 'unloaded' # print ' closed so no need to unload' else: if self.play_state == 'loaded': # load already complete so set unload signal and kick off state machine self.play_state = 'start_unload' self.unloading_count = 0 self.unload_signal = True self.tick_timer = self.canvas.after(50, self.load_state_machine) elif self.play_state == 'loading': # wait for load to complete before unloading - ???? must do this because does not respond to quit when loading # state machine will go to start_unloading state and stop omxplayer self.unload_signal = True else: self.mon.err( self, 'illegal state in unload method: ' + self.play_state) self.end('error', 'illegal state in unload method: ' + self.play_state) def start_state_machine_show(self): if self.play_state == 'loaded': # print '\nstart show state machine ' + self.play_state self.play_state = 'showing' self.freeze_signal = False # signal that user has pressed stop self.must_quit_signal = False # show the track and content self.omx.show(self.freeze_at_end_required) self.mon.log(self, '>showing track from show Id: ' + str(self.show_id)) # and start polling for state changes # print 'start show state machine show' self.tick_timer = self.canvas.after(0, self.show_state_machine) # race condition don't start state machine as unload in progress elif self.play_state == 'start_unload': pass else: self.mon.fatal(self, 'illegal state in show method ' + self.play_state) self.play_state = 'show-failed' if self.finished_callback is not None: self.finished_callback( 'error', 'illegal state in show method: ' + self.play_state) def start_state_machine_close(self): self.quit_signal = True # print 'start close state machine close' self.tick_timer = self.canvas.after(0, self.show_state_machine) def load_state_machine(self): # print 'vidoeplayer state is'+self.play_state if self.play_state == 'loading': # if omxdriver says loaded then can finish normally # self.mon.log(self," State machine: " + self.play_state) if self.omx.end_play_signal is True: # got nice day before the first timestamp self.mon.warn(self, self.track) self.mon.warn( self, "loading - omxplayer ended before starting track with reason: " + self.omx.end_play_reason + ' at ' + str(self.omx.video_position)) self.omx.kill() self.mon.err(self, 'omxplayer ended before loading track') self.play_state = 'load-failed' self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) if self.loaded_callback is not None: self.loaded_callback( 'error', 'omxplayer ended before loading track') else: # end play signal false - continue waiting for first timestamp self.loading_count += 1 # video has loaded if self.omx.start_play_signal is True: self.mon.log( self, "Loading complete from show Id: " + str(self.show_id) + ' ' + self.track) self.mon.log( self, 'Got video duration from track, frezing at: ' + str(self.omx.duration) + ' microsecs.') if self.unload_signal is True: # print('unload sig=true state= start_unload') # need to unload, kick off state machine in 'start_unload' state self.play_state = 'start_unload' self.unloading_count = 0 self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) self.tick_timer = self.canvas.after( 200, self.load_state_machine) else: self.play_state = 'loaded' self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) if self.loaded_callback is not None: # print 'callback when loaded' self.loaded_callback('normal', 'video loaded') else: # start play signal false - continue to wait if self.loading_count > 400: #40 seconds # deal with omxplayer crashing while loading and hence not receive start_play_signal self.mon.warn(self, self.track) self.mon.warn( self, "loading - videoplayer counted out: " + self.omx.end_play_reason + ' at ' + str(self.omx.video_position)) self.omx.kill() self.mon.warn( self, 'videoplayer counted out when loading track ') self.play_state = 'load-failed' self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) if self.loaded_callback is not None: self.loaded_callback( 'error', 'omxplayer counted out when loading track') else: self.tick_timer = self.canvas.after( 100, self.load_state_machine) #200 elif self.play_state == 'start_unload': # omxplayer reports it is terminating # self.mon.log(self," State machine: " + self.play_state) # deal with unload signal if self.unload_signal is True: self.unload_signal = False self.omx.stop() if self.omx.end_play_signal is True: self.omx.end_play_signal = False self.mon.log( self, " <end play signal received with reason: " + self.omx.end_play_reason + ' at: ' + str(self.omx.video_position)) # omxplayer has been closed if self.omx.end_play_reason == 'nice_day': # no problem with omxplayer self.play_state = 'unloading' self.unloading_count = 0 self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) self.tick_timer = self.canvas.after( 50, self.load_state_machine) else: # unexpected reason self.mon.err( self, 'unexpected reason at unload: ' + self.omx.end_play_reason) self.end( 'error', 'unexpected reason at unload: ' + self.omx.end_play_reason) else: # end play signal false self.tick_timer = self.canvas.after(50, self.load_state_machine) elif self.play_state == 'unloading': # wait for unloading to complete self.mon.log(self, " State machine: " + self.play_state) # if spawned process has closed can change to closed state if self.omx.is_running() is False: self.mon.log(self, " <omx process is dead") self.play_state = 'unloaded' self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) else: # process still running self.unloading_count += 1 if self.unloading_count > 10: # deal with omxplayer not terminating at the end of a track self.mon.warn(self, self.track) self.mon.warn( self, " <unloading - omxplayer failed to close at: " + str(self.omx.video_position)) self.mon.warn(self, 'omxplayer should now be killed ') self.omx.kill() self.play_state = 'unloaded' self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) else: self.tick_timer = self.canvas.after( 200, self.load_state_machine) else: self.mon.err( self, 'illegal state in load state machine: ' + self.play_state) self.end('error', 'load state machine in illegal state: ' + self.play_state) def show_state_machine(self): # print self.play_state # if self.play_state != 'showing': print 'show state is '+self.play_state if self.play_state == 'showing': # service any queued stop signals by sending quit to omxplayer # self.mon.log(self," State machine: " + self.play_state) if self.quit_signal is True: self.quit_signal = False self.mon.log(self, " quit video - Send stop to omxdriver") self.omx.stop() self.tick_timer = self.canvas.after(50, self.show_state_machine) # omxplayer reports it is terminating elif self.omx.end_play_signal is True: self.omx.end_play_signal = False self.mon.log( self, "end play signal received with reason: " + self.omx.end_play_reason + ' at: ' + str(self.omx.video_position)) # paused at end of track so return so calling prog can release the pause if self.omx.end_play_reason == 'pause_at_end': self.frozen_at_end = True if self.finished_callback is not None: self.finished_callback('pause_at_end', 'pause at end') elif self.omx.end_play_reason == 'nice_day': # no problem with omxplayer self.play_state = 'closing' self.closing_count = 0 self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) self.tick_timer = self.canvas.after( 50, self.show_state_machine) else: # unexpected reason self.mon.err( self, 'unexpected reason at end of show ' + self.omx.end_play_reason) self.play_state = 'show-failed' if self.finished_callback is not None: self.finished_callback( 'error', 'unexpected reason at end of show: ' + self.omx.end_play_reason) else: # nothing to do just try again # print 'showing - try again' self.tick_timer = self.canvas.after(50, self.show_state_machine) elif self.play_state == 'closing': # wait for closing to complete self.mon.log(self, " State machine: " + self.play_state) if self.omx.is_running() is False: # if spawned process has closed can change to closed state self.mon.log(self, " <omx process is dead") self.play_state = 'closed' # print 'process dead going to closed' self.omx = None self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) if self.closed_callback is not None: self.closed_callback('normal', 'omxplayer closed') else: # process still running self.closing_count += 1 # print 'closing - waiting for process to die',self.closing_count if self.closing_count > 10: # deal with omxplayer not terminating at the end of a track # self.mon.warn(self,self.track) # self.mon.warn(self,"omxplayer failed to close at: " + str(self.omx.video_position)) self.mon.warn( self, 'failed to close - omxplayer now being killed with SIGINT' ) self.omx.kill() # print 'closing - precess will not die so ita been killed with SIGINT' self.play_state = 'closed' self.omx = None self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) if self.closed_callback is not None: self.closed_callback('normal', 'closed omxplayer after sigint') else: self.tick_timer = self.canvas.after( 200, self.show_state_machine) elif self.play_state == 'closed': # needed because wait_for_end polls the state and does not use callback self.mon.log(self, " State machine: " + self.play_state) self.tick_timer = self.canvas.after(200, self.show_state_machine) else: self.mon.err( self, 'unknown state in show/close state machine ' + self.play_state) self.play_state = 'show-failed' if self.finished_callback is not None: self.finished_callback( 'error', 'show state machine in unknown state: ' + self.play_state) def parse_video_window(self, line, display_id): # model other than 4 video is rotated by hdmi_display_rotate in config.txt if self.dm.model_of_pi() == 4: rotation = self.dm.real_display_orientation(self.omx_display_id) else: rotation = 'normal' if rotation == 'normal': self.omx_rotate = '' elif rotation == 'right': self.omx_rotate = ' --orientation 90 ' elif rotation == 'left': self.omx_rotate = ' --orientation 270 ' else: #inverted self.omx_rotate = ' --orientation 180 ' fields = line.split() # check there is a command field if len(fields) < 1: return 'error', 'no type field: ' + line, '', False, 0, 0, 0, 0 # deal with types which have no paramters if fields[0] in ('original', 'letterbox', 'fill', 'default', 'stretch'): if len(fields) != 1: return 'error', 'number of fields for original: ' + line, '', False, 0, 0, 0, 0 if fields[0] in ('letterbox', 'fill', 'stretch'): self.omx_aspect_mode = ' --aspect-mode ' + fields[0] else: self.omx_aspect_mode = '' return 'normal', '', fields[0], False, 0, 0, 0, 0 # deal with warp which has 0 or 1 or 4 parameters (1 is x+y+w*h) # check basic syntax if fields[0] != 'warp': return 'error', 'not a valid type: ' + fields[ 0], '', False, 0, 0, 0, 0 if len(fields) not in (1, 2, 5): return 'error', 'wrong number of coordinates for warp: ' + line, '', False, 0, 0, 0, 0 # deal with window coordinates, just warp if len(fields) == 1: has_window = True x1 = self.show_canvas_x1 y1 = self.show_canvas_y1 x2 = self.show_canvas_x2 y2 = self.show_canvas_y2 else: # window is specified warp + dimesions etc. status, message, x1, y1, x2, y2 = parse_rectangle(' '.join( fields[1:])) if status == 'error': return 'error', message, '', False, 0, 0, 0, 0 has_window = True x1_res, y1_res, x2_res, y2_res = self.transform_for_rotation( display_id, rotation, x1, y1, x2, y2) return 'normal', '', fields[ 0], has_window, x1_res, y1_res, x2_res, y2_res def transform_for_rotation(self, display_id, rotation, x1, y1, x2, y2): # adjust rotation of video for display rotation video_width = x2 - x1 video_height = y2 - y1 display_width, display_height = self.dm.real_display_dimensions( display_id) if rotation == 'right': x1_res = display_height - video_height - y1 x2_res = display_height - y1 y1_res = y1 y2_res = video_width + y1 elif rotation == 'normal': x1_res = x1 y1_res = y1 x2_res = x2 y2_res = y2 elif rotation == 'left': x1_res = y1 x2_res = video_height + y1 y1_res = display_width - video_width - x1 y2_res = display_width - x1 else: # inverted x2_res = display_width - x1 x1_res = display_width - video_width - x1 y2_res = display_height - y1 y1_res = display_height - video_height - y1 if VideoPlayer.debug: print('\nWarp calculation for Display Id', display_id, rotation) print('Video Window', x1, y1, x2, y2) print('video width/height', video_width, video_height) print('display width/height', display_width, display_height) print('Translated Window', x1_res, y1_res, x2_res, y2_res) return x1_res, y1_res, x2_res, y2_res
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 VideoPlayer(Player): """ plays a track using omxplayer _init_ iniitalises state and checks resources are available. use the returned instance reference in all other calls. At the end of the path (when closed) do not re-use, make instance= None and start again. States - 'initialised' when done successfully Initialisation is immediate so just returns with error code. """ def __init__(self, show_id, showlist, root, canvas, show_params, track_params, pp_dir, pp_home, pp_profile, end_callback, command_callback): # initialise items common to all players Player.__init__(self, show_id, showlist, root, canvas, show_params, track_params, pp_dir, pp_home, pp_profile, end_callback, command_callback) # print ' !!!!!!!!!!!videoplayer init' self.mon.trace(self, '') # get player parameters 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'] if self.track_params['omx-other-options'] != '': self.omx_other_options = self.track_params['omx-other-options'] else: self.omx_other_options = self.show_params['omx-other-options'] if self.track_params['freeze-at-end'] != '': freeze_at_end_text = self.track_params['freeze-at-end'] else: freeze_at_end_text = self.show_params['freeze-at-end'] if freeze_at_end_text == 'yes': self.freeze_at_end_required = True else: self.freeze_at_end_required = False if self.track_params['seamless-loop'] == 'yes': self.seamless_loop = ' --loop ' else: self.seamless_loop = '' # initialise video playing state and signals self.quit_signal = False self.unload_signal = False self.play_state = 'initialised' self.frozen_at_end = False # LOAD - creates and omxplayer instance, loads a track and then pause def load(self, track, loaded_callback, enable_menu): # instantiate arguments self.track = track self.loaded_callback = loaded_callback #callback when loaded # print '!!!!!!!!!!! videoplayer load',self.track self.mon.log( self, "Load track received from show Id: " + str(self.show_id) + ' ' + self.track) self.mon.trace(self, '') # do common bits of load Player.pre_load(self) # set up video window status, message, command, has_window, x1, y1, x2, y2 = self.parse_video_window( self.omx_window) if status == 'error': self.mon.err( self, 'omx window error: ' + message + ' in ' + self.omx_window) self.play_state = 'load-failed' if self.loaded_callback is not None: self.loaded_callback('error', message) return else: if has_window is True: self.omx_window_processed = '--win " ' + str(x1) + ' ' + str( y1) + ' ' + str(x2) + ' ' + str(y2) + ' " ' else: self.omx_window_processed = '' # load the plugin, this may modify self.track and enable the plugin drawign to canvas if self.track_params['plugin'] != '': status, message = self.load_plugin() if status == 'error': self.mon.err(self, message) self.play_state = 'load-failed' if self.loaded_callback is not None: self.loaded_callback('error', message) return # load the images and text status, message = self.load_x_content(enable_menu) if status == 'error': self.mon.err(self, message) self.play_state = 'load-failed' if self.loaded_callback is not None: self.loaded_callback('error', message) return if not os.path.exists(track): self.mon.err(self, "Track file not found: " + track) self.play_state = 'load-failed' if self.loaded_callback is not None: self.loaded_callback('error', 'track file not found') return self.omx = OMXDriver(self.canvas, self.pp_dir) self.start_state_machine_load(self.track) # SHOW - show a track def show(self, ready_callback, finished_callback, closed_callback): # print "!!!! videoplayer show" # instantiate arguments self.ready_callback = ready_callback # callback when ready to show video self.finished_callback = finished_callback # callback when finished showing self.closed_callback = closed_callback self.mon.trace(self, '') # do animation at start etc. Player.pre_show(self) # start show state machine self.start_state_machine_show() # UNLOAD - abort a load when omplayer is loading or loaded def unload(self): self.mon.trace(self, '') self.mon.log(self, ">unload received from show Id: " + str(self.show_id)) self.start_state_machine_unload() # CLOSE - quits omxplayer from 'pause at end' state def close(self, closed_callback): self.mon.trace(self, '') self.mon.log(self, ">close received from show Id: " + str(self.show_id)) self.closed_callback = closed_callback self.start_state_machine_close() def input_pressed(self, symbol): if symbol[0:4] == 'omx-': self.control(symbol[4]) elif symbol == 'pause': self.pause() elif symbol == 'stop': self.stop() # respond to normal stop def stop(self): self.mon.log(self, ">stop received from show Id: " + str(self.show_id)) # send signal to freeze the track - causes either pause or quit depends on freeze at end if self.freeze_at_end_required is True: if self.play_state == 'showing' and self.frozen_at_end is False: self.frozen_at_end = True # pause the track self.omx.pause('freeze at end from user stop') self.quit_signal = True # and return to show so it can end the track and the video in track ready callback ## if self.finished_callback is not None: ## # print 'finished from stop' ## self.finished_callback('pause_at_end','pause at end') else: self.mon.log(self, "!<stop rejected") else: # freeze not required and its showing just stop the video if self.play_state == 'showing': self.quit_signal = True else: self.mon.log(self, "!<stop rejected") # toggle pause def pause(self): self.mon.log( self, ">toggle pause received from show Id: " + str(self.show_id)) if self.play_state == 'showing' and self.frozen_at_end is False: self.omx.toggle_pause('user') return True else: self.mon.log(self, "!<pause rejected " + self.play_state) return False # other control when playing def control(self, char): if self.play_state == 'showing' and self.frozen_at_end is False 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 # *********************** # track showing state machine # ********************** """ STATES OF STATE MACHINE Threre are ongoing states and states that are set just before callback >init - Create an instance of the class <On return - state = initialised - - init has been completed, do not generate errors here >load Fatal errors should be detected in load. If so loaded_callback is called with 'load-failed' Ongoing - state=loading - load called, waiting for load to complete < loaded_callback with status = normal state=loaded - load has completed and video paused before first frame <loaded_callback with status=error state= load-failed - omxplayer process has been killed because of failure to load On getting the loaded_callback with status=normal the track can be shown using show Duration obtained from track should always cause pause_at_end. if not please tell me as the fudge factor may need adjusting. >show show assumes a track has been loaded and is paused. Ongoing - state=showing - video is showing <finished_callback with status = pause_at_end state=showing but frozen_at_end is True <closed_callback with status= normal state = closed - video has ended omxplayer has terminated. eof and timeout are error conditions and should not happen. vidoeplayer recovers from these and continues. On getting finished_callback with status=pause_at end a new track can be shown and then use close to quit the video when new track is ready On getting closed_callback with status= timeout eof or nice_day omxplayer closing should not be attempted as it is already closed Do not generate user errors in Show. Only geberate system erros such as illegal state abd then use end() >close Ongoing state - closing - omxplayer processes are dying due to quit sent <closed_callback with status= normal - omxplayer is dead, can close the track instance. >unload Ongoing states - start_unload and unloading - omxplayer processes are dying due to quit sent. when unloading is complete state=unloaded I have not added a callback to unload. its easy to add one if you want. closed is needed because wait_for end in pp_show polls for closed and does not use closed_callback """ def start_state_machine_load(self, track): self.track = track # initialise all the state machine variables self.loading_count = 0 # initialise loading timeout counter self.play_state = 'loading' # load the selected track options = ' --no-osd ' + self.omx_audio + " " + self.omx_volume + ' ' + self.omx_window_processed + ' ' + self.seamless_loop + ' ' + self.omx_other_options + " " self.omx.load(track, options, self.mon.pretty_inst(self)) # self.mon.log (self,'Send load command track '+ self.track + 'with options ' + options + 'from show Id: '+ str(self.show_id)) # print 'omx.load started ',self.track # and start polling for state changes self.tick_timer = self.canvas.after(50, self.load_state_machine) def start_state_machine_unload(self): # print 'videoplayer - starting unload',self.play_state if self.play_state in ('closed', 'initialised', 'unloaded'): # omxplayer already closed self.play_state = 'unloaded' # print ' closed so no need to unload' else: if self.play_state == 'loaded': # load already complete so set unload signal and kick off state machine self.play_state = 'start_unload' self.unloading_count = 0 self.unload_signal = True self.tick_timer = self.canvas.after(50, self.load_state_machine) elif self.play_state == 'loading': # wait for load to complete before unloading - ???? must do this because does not respond to quit when loading # state machine will go to start_unloading state and stop omxplayer self.unload_signal = True else: self.mon.err( self, 'illegal state in unload method ' + self.play_state) self.end('error', 'illegal state in unload method') def start_state_machine_show(self): if self.play_state == 'loaded': # print '\nstart show state machine ' + self.play_state self.play_state = 'showing' self.freeze_signal = False # signal that user has pressed stop self.must_quit_signal = False # show the track and content self.omx.show(self.freeze_at_end_required) self.mon.log(self, '>showing track from show Id: ' + str(self.show_id)) # and start polling for state changes # print 'start show state machine show' self.tick_timer = self.canvas.after(0, self.show_state_machine) else: self.mon.fatal(self, 'illegal state in show method ' + self.play_state) self.play_state = 'show-failed' if self.finished_callback is not None: self.finished_callback('error', 'illegal state in show method') def start_state_machine_close(self): self.quit_signal = True # print 'start close state machine close' self.tick_timer = self.canvas.after(0, self.show_state_machine) def load_state_machine(self): # print 'vidoeplayer state is'+self.play_state if self.play_state == 'loading': # if omxdriver says loaded then can finish normally # self.mon.log(self," State machine: " + self.play_state) if self.omx.end_play_signal is True: # got nice day, eof or timeout before the first timestamp self.mon.warn(self, self.track) self.mon.warn( self, "loading - omxplayer ended before starting track with reason: " + self.omx.end_play_reason + ' at ' + str(self.omx.video_position)) self.mon.warn(self, 'pexpect.before is' + self.omx.xbefore) self.omx.kill() self.mon.err(self, 'omxplayer ended before loading track') self.play_state = 'load-failed' self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) if self.loaded_callback is not None: self.loaded_callback( 'error', 'omxplayer ended before loading track') else: # end play signal false - continue waiting for first timestamp self.loading_count += 1 # video has loaded if self.omx.start_play_signal is True: self.mon.log( self, "Loading complete from show Id: " + str(self.show_id) + ' ' + self.track) self.mon.log( self, 'Got video duration from track, frezing at: ' + str(self.omx.duration) + ' microsecs.') if self.unload_signal is True: # print'unload sig=true state= start_unload' # need to unload, kick off state machine in 'start_unload' state self.play_state = 'start_unload' self.unloading_count = 0 self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) self.tick_timer = self.canvas.after( 200, self.load_state_machine) else: self.play_state = 'loaded' self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) if self.loaded_callback is not None: # print 'callback when loaded' self.loaded_callback('normal', 'video loaded') else: # start play signal false - continue to wait if self.loading_count > 400: #40 seconds # deal with omxplayer crashing while loading and hence not receive start_play_signal self.mon.warn(self, self.track) self.mon.warn( self, "loading - videoplayer counted out: " + self.omx.end_play_reason + ' at ' + str(self.omx.video_position)) self.mon.warn(self, 'pexpect.before is' + self.omx.xbefore) self.omx.kill() self.mon.warn( self, 'videoplayer counted out when loading track ') self.play_state = 'load-failed' self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) if self.loaded_callback is not None: self.loaded_callback( 'error', 'omxplayer counted out when loading track') else: self.tick_timer = self.canvas.after( 100, self.load_state_machine) #200 elif self.play_state == 'start_unload': # omxplayer reports it is terminating # self.mon.log(self," State machine: " + self.play_state) # deal with unload signal if self.unload_signal is True: self.unload_signal = False self.omx.stop() if self.omx.end_play_signal is True: self.omx.end_play_signal = False self.mon.log( self, " <end play signal received with reason: " + self.omx.end_play_reason + ' at: ' + str(self.omx.video_position)) # omxplayer has been closed if self.omx.end_play_reason == 'nice_day': # no problem with omxplayer self.play_state = 'unloading' self.unloading_count = 0 self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) self.tick_timer = self.canvas.after( 50, self.load_state_machine) else: # problem with omxplayer if self.omx.end_play_reason in ('eof', 'timeout'): self.mon.warn(self, self.track) self.mon.warn( self, " <start_unload - 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 = 'unloading' self.unloading_count = 0 self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) self.tick_timer = self.canvas.after( 50, self.load_state_machine) else: # unexpected reason self.mon.err( self, 'unexpected reason at unload ' + self.omx.end_play_reason) self.end('error', 'unexpected reason at unload') else: # end play signal false self.tick_timer = self.canvas.after(50, self.load_state_machine) elif self.play_state == 'unloading': # wait for unloading to complete self.mon.log(self, " State machine: " + self.play_state) # if spawned process has closed can change to closed state if self.omx.is_running() is False: self.mon.log(self, " <omx process is dead") self.play_state = 'unloaded' self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) else: # process still running self.unloading_count += 1 if self.unloading_count > 10: # deal with omxplayer not terminating at the end of a track self.mon.warn(self, self.track) self.mon.warn( self, " <unloading - omxplayer failed to close at: " + str(self.omx.video_position)) self.mon.warn(self, 'pexpect.before is' + self.omx.xbefore) self.mon.warn(self, 'omxplayer should now be killed ') self.omx.kill() self.play_state = 'unloaded' self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) else: self.tick_timer = self.canvas.after( 200, self.load_state_machine) else: self.mon.err( self, 'illegal state in load state machine' + self.play_state) self.end('error', 'load state machine in illegal state') def show_state_machine(self): # print self.play_state # if self.play_state != 'showing': print 'show state is '+self.play_state if self.play_state == 'showing': # service any queued stop signals by sending quit to omxplayer # self.mon.log(self," State machine: " + self.play_state) if self.quit_signal is True: self.quit_signal = False self.mon.log(self, " quit video - Send stop to omxdriver") self.omx.stop() self.tick_timer = self.canvas.after(50, self.show_state_machine) # omxplayer reports it is terminating elif self.omx.end_play_signal is True: self.omx.end_play_signal = False self.mon.log( self, "end play signal received with reason: " + self.omx.end_play_reason + ' at: ' + str(self.omx.video_position)) # paused at end of track so return so calling prog can release the pause if self.omx.end_play_reason == 'pause_at_end': self.frozen_at_end = True if self.finished_callback is not None: self.finished_callback('pause_at_end', 'pause at end') elif self.omx.end_play_reason == 'nice_day': # no problem with omxplayer self.play_state = 'closing' self.closing_count = 0 self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) self.tick_timer = self.canvas.after( 50, self.show_state_machine) elif self.omx.end_play_reason in ('eof', 'timeout'): # problem with omxplayer self.play_state = 'closing' self.closing_count = 0 # self.mon.warn(self,self.track + ' ' + self.omx.caller) self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) self.mon.warn( self, "unexpected termination - : " + self.omx.end_play_reason + ' at: ' + str(self.omx.video_position) + ' ' + self.track + ' ' + self.omx.caller) self.mon.trace(self, 'pexpect.before is' + self.omx.xbefore) # print 'showing - eof or timeout so go to closing to wait for precess to be dead' self.tick_timer = self.canvas.after( 50, self.show_state_machine) else: # unexpected reason self.mon.err( self, 'unexpected reason at end of show ' + self.omx.end_play_reason) self.play_state = 'show-failed' if self.finished_callback is not None: self.finished_callback( 'error', 'unexpected reason at end of show') else: # nothing to do just try again # print 'showing - try again' self.tick_timer = self.canvas.after(50, self.show_state_machine) elif self.play_state == 'closing': # wait for closing to complete self.mon.log(self, " State machine: " + self.play_state) if self.omx.is_running() is False: # if spawned process has closed can change to closed state self.mon.log(self, " <omx process is dead") self.play_state = 'closed' # print 'process dead going to closed' self.omx = None self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) if self.closed_callback is not None: self.closed_callback('normal', 'omxplayer closed') else: # process still running self.closing_count += 1 # print 'closing - waiting for process to die',self.closing_count if self.closing_count > 10: # deal with omxplayer not terminating at the end of a track # self.mon.warn(self,self.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.mon.warn( self, 'failed to close - omxplayer now being killed with SIGINT' ) self.omx.kill() # print 'closing - precess will not die so ita been killed with SIGINT' self.play_state = 'closed' self.omx = None self.mon.log( self, " Entering state : " + self.play_state + ' from show Id: ' + str(self.show_id)) if self.closed_callback is not None: self.closed_callback('normal', 'closed omxplayer after sigint') else: self.tick_timer = self.canvas.after( 200, self.show_state_machine) elif self.play_state == 'closed': # needed because wait_for_end polls the state and does not use callback self.mon.log(self, " State machine: " + self.play_state) self.tick_timer = self.canvas.after(200, self.show_state_machine) else: self.mon.err( self, 'unknown state in show/close state machine ' + self.play_state) self.play_state = 'show-failed' if self.finished_callback is not None: self.finished_callback('error', 'show state machine in unknown state') def parse_video_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, self.show_canvas_x1 + int( fields[1]), self.show_canvas_y1 + int( fields[2]), self.show_canvas_x1 + int( fields[3]), self.show_canvas_y1 + int(fields[4]) else: # fullscreen has_window = True return 'normal', '', fields[ 0], has_window, self.show_canvas_x1, self.show_canvas_y1, self.show_canvas_x2, self.show_canvas_y2
class VideoPlayer(Player): """ plays a track using omxplayer _init_ iniitalises state and checks resources are available. use the returned instance reference in all other calls. At the end of the path (when closed) do not re-use, make instance= None and start again. States - 'initialised' when done successfully Initialisation is immediate so just returns with error code. """ def __init__(self, show_id, showlist, root, canvas, show_params, track_params , pp_dir, pp_home, pp_profile, end_callback, command_callback): # initialise items common to all players Player.__init__( self, show_id, showlist, root, canvas, show_params, track_params , pp_dir, pp_home, pp_profile, end_callback, command_callback) # print ' !!!!!!!!!!!videoplayer init' self.mon.trace(self,'') # get player parameters 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'] if self.track_params['omx-other-options'] != '': self.omx_other_options= self.track_params['omx-other-options'] else: self.omx_other_options= self.show_params['omx-other-options'] if self.track_params['freeze-at-end'] != '': freeze_at_end_text= self.track_params['freeze-at-end'] else: freeze_at_end_text= self.show_params['freeze-at-end'] if freeze_at_end_text == 'yes': self.freeze_at_end_required=True else: self.freeze_at_end_required=False if self.track_params['seamless-loop'] == 'yes': self.seamless_loop=' --loop ' else: self.seamless_loop='' # initialise video playing state and signals self.quit_signal=False self.unload_signal=False self.play_state='initialised' self.frozen_at_end=False # LOAD - creates and omxplayer instance, loads a track and then pause def load(self,track,loaded_callback,enable_menu): # instantiate arguments self.track=track self.loaded_callback=loaded_callback #callback when loaded # print '!!!!!!!!!!! videoplayer load',self.track self.mon.log(self,"Load track received from show Id: "+ str(self.show_id) + ' ' +self.track) self.mon.trace(self,'') # do common bits of load Player.pre_load(self) # set up video window status,message,command,has_window,x1,y1,x2,y2= self.parse_video_window(self.omx_window) if status == 'error': self.mon.err(self,'omx window error: ' + message + ' in ' + self.omx_window) self.play_state='load-failed' if self.loaded_callback is not None: self.loaded_callback('error',message) return else: if has_window is True: self.omx_window_processed= '--win " '+ str(x1) + ' ' + str(y1) + ' ' + str(x2) + ' ' + str(y2) + ' " ' else: self.omx_window_processed='' # load the plugin, this may modify self.track and enable the plugin drawign to canvas if self.track_params['plugin'] != '': status,message=self.load_plugin() if status == 'error': self.mon.err(self,message) self.play_state='load-failed' if self.loaded_callback is not None: self.loaded_callback('error',message) return # load the images and text status,message=self.load_x_content(enable_menu) if status == 'error': self.mon.err(self,message) self.play_state='load-failed' if self.loaded_callback is not None: self.loaded_callback('error',message) return if not os.path.exists(track): self.mon.err(self,"Track file not found: "+ track) self.play_state='load-failed' if self.loaded_callback is not None: self.loaded_callback('error','track file not found') return self.omx=OMXDriver(self.canvas,self.pp_dir) self.start_state_machine_load(self.track) # SHOW - show a track def show(self,ready_callback,finished_callback,closed_callback): # print "!!!! videoplayer show" # instantiate arguments self.ready_callback=ready_callback # callback when ready to show video self.finished_callback=finished_callback # callback when finished showing self.closed_callback=closed_callback self.mon.trace(self,'') # do animation at start etc. Player.pre_show(self) # start show state machine self.start_state_machine_show() # UNLOAD - abort a load when omplayer is loading or loaded def unload(self): self.mon.trace(self,'') self.mon.log(self,">unload received from show Id: "+ str(self.show_id)) self.start_state_machine_unload() # CLOSE - quits omxplayer from 'pause at end' state def close(self,closed_callback): self.mon.trace(self,'') self.mon.log(self,">close received from show Id: "+ str(self.show_id)) self.closed_callback=closed_callback self.start_state_machine_close() def input_pressed(self,symbol): if symbol[0:4] == 'omx-': self.control(symbol[4]) elif symbol == 'pause': self.pause() elif symbol == 'stop': self.stop() # respond to normal stop def stop(self): self.mon.log(self,">stop received from show Id: "+ str(self.show_id)) # send signal to freeze the track - causes either pause or quit depends on freeze at end if self.freeze_at_end_required is True: if self.play_state == 'showing' and self.frozen_at_end is False: self.frozen_at_end=True # pause the track self.omx.pause('freeze at end from user stop') self.quit_signal=True # and return to show so it can end the track and the video in track ready callback ## if self.finished_callback is not None: ## # print 'finished from stop' ## self.finished_callback('pause_at_end','pause at end') else: self.mon.log(self,"!<stop rejected") else: # freeze not required and its showing just stop the video if self.play_state=='showing': self.quit_signal=True else: self.mon.log(self,"!<stop rejected") # toggle pause def pause(self): self.mon.log(self,">toggle pause received from show Id: "+ str(self.show_id)) if self.play_state == 'showing' and self.frozen_at_end is False: self.omx.toggle_pause('user') return True else: self.mon.log(self,"!<pause rejected " + self.play_state) return False # other control when playing def control(self,char): if self.play_state == 'showing' and self.frozen_at_end is False 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 # *********************** # track showing state machine # ********************** """ STATES OF STATE MACHINE Threre are ongoing states and states that are set just before callback >init - Create an instance of the class <On return - state = initialised - - init has been completed, do not generate errors here >load Fatal errors should be detected in load. If so loaded_callback is called with 'load-failed' Ongoing - state=loading - load called, waiting for load to complete < loaded_callback with status = normal state=loaded - load has completed and video paused before first frame <loaded_callback with status=error state= load-failed - omxplayer process has been killed because of failure to load On getting the loaded_callback with status=normal the track can be shown using show Duration obtained from track should always cause pause_at_end. if not please tell me as the fudge factor may need adjusting. >show show assumes a track has been loaded and is paused. Ongoing - state=showing - video is showing <finished_callback with status = pause_at_end state=showing but frozen_at_end is True <closed_callback with status= normal state = closed - video has ended omxplayer has terminated. eof and timeout are error conditions and should not happen. vidoeplayer recovers from these and continues. On getting finished_callback with status=pause_at end a new track can be shown and then use close to quit the video when new track is ready On getting closed_callback with status= timeout eof or nice_day omxplayer closing should not be attempted as it is already closed Do not generate user errors in Show. Only geberate system erros such as illegal state abd then use end() >close Ongoing state - closing - omxplayer processes are dying due to quit sent <closed_callback with status= normal - omxplayer is dead, can close the track instance. >unload Ongoing states - start_unload and unloading - omxplayer processes are dying due to quit sent. when unloading is complete state=unloaded I have not added a callback to unload. its easy to add one if you want. closed is needed because wait_for end in pp_show polls for closed and does not use closed_callback """ def start_state_machine_load(self,track): self.track=track # initialise all the state machine variables self.loading_count=0 # initialise loading timeout counter self.play_state='loading' # load the selected track options= ' --no-osd ' + self.omx_audio+ " " + self.omx_volume + ' ' + self.omx_window_processed + ' ' + self.seamless_loop + ' ' + self.omx_other_options +" " self.omx.load(track,options,self.mon.pretty_inst(self)) # self.mon.log (self,'Send load command track '+ self.track + 'with options ' + options + 'from show Id: '+ str(self.show_id)) # print 'omx.load started ',self.track # and start polling for state changes self.tick_timer=self.canvas.after(50, self.load_state_machine) def start_state_machine_unload(self): # print 'videoplayer - starting unload',self.play_state if self.play_state in('closed','initialised','unloaded'): # omxplayer already closed self.play_state='unloaded' # print ' closed so no need to unload' else: if self.play_state == 'loaded': # load already complete so set unload signal and kick off state machine self.play_state='start_unload' self.unloading_count=0 self.unload_signal=True self.tick_timer=self.canvas.after(50, self.load_state_machine) elif self.play_state == 'loading': # wait for load to complete before unloading - ???? must do this because does not respond to quit when loading # state machine will go to start_unloading state and stop omxplayer self.unload_signal=True else: self.mon.err(self,'illegal state in unload method ' + self.play_state) self.end('error','illegal state in unload method') def start_state_machine_show(self): if self.play_state == 'loaded': # print '\nstart show state machine ' + self.play_state self.play_state='showing' self.freeze_signal=False # signal that user has pressed stop self.must_quit_signal=False # show the track and content self.omx.show(self.freeze_at_end_required) self.mon.log (self,'>showing track from show Id: '+ str(self.show_id)) # and start polling for state changes # print 'start show state machine show' self.tick_timer=self.canvas.after(0, self.show_state_machine) else: self.mon.fatal(self,'illegal state in show method ' + self.play_state) self.play_state='show-failed' if self.finished_callback is not None: self.finished_callback('error','illegal state in show method') def start_state_machine_close(self): self.quit_signal=True # print 'start close state machine close' self.tick_timer=self.canvas.after(0, self.show_state_machine) def load_state_machine(self): # print 'vidoeplayer state is'+self.play_state if self.play_state == 'loading': # if omxdriver says loaded then can finish normally # self.mon.log(self," State machine: " + self.play_state) if self.omx.end_play_signal is True: # got nice day, eof or timeout before the first timestamp self.mon.warn(self,self.track) self.mon.warn(self,"loading - omxplayer ended before starting track with reason: " + self.omx.end_play_reason + ' at ' +str(self.omx.video_position)) self.mon.warn(self,'pexpect.before is'+self.omx.xbefore) self.omx.kill() self.mon.err(self,'omxplayer ended before loading track') self.play_state = 'load-failed' self.mon.log(self," Entering state : " + self.play_state + ' from show Id: '+ str(self.show_id)) if self.loaded_callback is not None: self.loaded_callback('error','omxplayer ended before loading track') else: # end play signal false - continue waiting for first timestamp self.loading_count+=1 # video has loaded if self.omx.start_play_signal is True: self.mon.log(self,"Loading complete from show Id: "+ str(self.show_id)+ ' ' +self.track) self.mon.log(self,'Got video duration from track, frezing at: '+ str(self.omx.duration)+ ' microsecs.') if self.unload_signal is True: # print'unload sig=true state= start_unload' # need to unload, kick off state machine in 'start_unload' state self.play_state='start_unload' self.unloading_count=0 self.mon.log(self," Entering state : " + self.play_state + ' from show Id: '+ str(self.show_id)) self.tick_timer=self.canvas.after(200, self.load_state_machine) else: self.play_state = 'loaded' self.mon.log(self," Entering state : " + self.play_state + ' from show Id: '+ str(self.show_id)) if self.loaded_callback is not None: # print 'callback when loaded' self.loaded_callback('normal','video loaded') else: # start play signal false - continue to wait if self.loading_count>200: #40 seconds # deal with omxplayer crashing while loading and hence not receive start_play_signal self.mon.warn(self,self.track) self.mon.warn(self,"loading - videoplayer counted out: " + self.omx.end_play_reason + ' at ' + str(self.omx.video_position)) self.mon.warn(self,'pexpect.before is'+self.omx.xbefore) self.omx.kill() self.mon.warn(self,'videoplayer counted out when loading track ') self.play_state = 'load-failed' self.mon.log(self," Entering state : " + self.play_state + ' from show Id: '+ str(self.show_id)) if self.loaded_callback is not None: self.loaded_callback('error','omxplayer counted out when loading track') else: self.tick_timer=self.canvas.after(200, self.load_state_machine) elif self.play_state == 'start_unload': # omxplayer reports it is terminating # self.mon.log(self," State machine: " + self.play_state) # deal with unload signal if self.unload_signal is True: self.unload_signal=False self.omx.stop() if self.omx.end_play_signal is True: self.omx.end_play_signal=False self.mon.log(self," <end play signal received with reason: " + self.omx.end_play_reason + ' at: ' + str(self.omx.video_position)) # omxplayer has been closed if self.omx.end_play_reason == 'nice_day': # no problem with omxplayer self.play_state='unloading' self.unloading_count=0 self.mon.log(self," Entering state : " + self.play_state + ' from show Id: '+ str(self.show_id)) self.tick_timer=self.canvas.after(50, self.load_state_machine) else: # problem with omxplayer if self.omx.end_play_reason in ('eof','timeout'): self.mon.warn(self,self.track) self.mon.warn(self," <start_unload - 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='unloading' self.unloading_count=0 self.mon.log(self," Entering state : " + self.play_state + ' from show Id: '+ str(self.show_id)) self.tick_timer=self.canvas.after(50, self.load_state_machine) else: # unexpected reason self.mon.err(self,'unexpected reason at unload '+self.omx.end_play_reason) self.end('error','unexpected reason at unload') else: # end play signal false self.tick_timer=self.canvas.after(50, self.load_state_machine) elif self.play_state == 'unloading': # wait for unloading to complete self.mon.log(self," State machine: " + self.play_state) # if spawned process has closed can change to closed state if self.omx.is_running() is False: self.mon.log(self," <omx process is dead") self.play_state='unloaded' self.mon.log(self," Entering state : " + self.play_state + ' from show Id: '+ str(self.show_id)) else: # process still running self.unloading_count+=1 if self.unloading_count>10: # deal with omxplayer not terminating at the end of a track self.mon.warn(self,self.track) self.mon.warn(self," <unloading - omxplayer failed to close at: " + str(self.omx.video_position)) self.mon.warn(self,'pexpect.before is'+self.omx.xbefore) self.mon.warn(self,'omxplayer should now be killed ') self.omx.kill() self.play_state='unloaded' self.mon.log(self," Entering state : " + self.play_state + ' from show Id: '+ str(self.show_id)) else: self.tick_timer=self.canvas.after(200, self.load_state_machine) else: self.mon.err(self,'illegal state in load state machine' + self.play_state) self.end('error','load state machine in illegal state') def show_state_machine(self): # print self.play_state # if self.play_state != 'showing': print 'show state is '+self.play_state if self.play_state == 'showing': # service any queued stop signals by sending quit to omxplayer # self.mon.log(self," State machine: " + self.play_state) if self.quit_signal is True: self.quit_signal=False self.mon.log(self," quit video - Send stop to omxdriver") self.omx.stop() self.tick_timer=self.canvas.after(50, self.show_state_machine) # omxplayer reports it is terminating elif self.omx.end_play_signal is True: self.omx.end_play_signal=False self.mon.log(self,"end play signal received with reason: " + self.omx.end_play_reason + ' at: ' + str(self.omx.video_position)) # paused at end of track so return so calling prog can release the pause if self.omx.end_play_reason == 'pause_at_end': self.frozen_at_end=True if self.finished_callback is not None: self.finished_callback('pause_at_end','pause at end') elif self.omx.end_play_reason == 'nice_day': # no problem with omxplayer self.play_state='closing' self.closing_count=0 self.mon.log(self," Entering state : " + self.play_state + ' from show Id: '+ str(self.show_id)) self.tick_timer=self.canvas.after(50, self.show_state_machine) elif self.omx.end_play_reason in ('eof','timeout'): # problem with omxplayer self.play_state='closing' self.closing_count=0 # self.mon.warn(self,self.track + ' ' + self.omx.caller) self.mon.log(self," Entering state : " + self.play_state + ' from show Id: '+ str(self.show_id)) self.mon.warn(self,"unexpected termination - : "+self.omx.end_play_reason + ' at: ' + str(self.omx.video_position) + ' ' + self.track + ' ' + self.omx.caller) self.mon.trace(self,'pexpect.before is'+self.omx.xbefore) # print 'showing - eof or timeout so go to closing to wait for precess to be dead' self.tick_timer=self.canvas.after(50, self.show_state_machine) else: # unexpected reason self.mon.err(self,'unexpected reason at end of show '+self.omx.end_play_reason) self.play_state='show-failed' if self.finished_callback is not None: self.finished_callback('error','unexpected reason at end of show') else: # nothing to do just try again # print 'showing - try again' self.tick_timer=self.canvas.after(50, self.show_state_machine) elif self.play_state == 'closing': # wait for closing to complete self.mon.log(self," State machine: " + self.play_state) if self.omx.is_running() is False: # if spawned process has closed can change to closed state self.mon.log(self," <omx process is dead") self.play_state = 'closed' # print 'process dead going to closed' self.omx=None self.mon.log(self," Entering state : " + self.play_state + ' from show Id: '+ str(self.show_id)) if self.closed_callback is not None: self.closed_callback('normal','omxplayer closed') else: # process still running self.closing_count+=1 # print 'closing - waiting for process to die',self.closing_count if self.closing_count>10: # deal with omxplayer not terminating at the end of a track # self.mon.warn(self,self.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.mon.warn(self,'failed to close - omxplayer now being killed with SIGINT') self.omx.kill() # print 'closing - precess will not die so ita been killed with SIGINT' self.play_state = 'closed' self.omx=None self.mon.log(self," Entering state : " + self.play_state + ' from show Id: '+ str(self.show_id)) if self.closed_callback is not None: self.closed_callback('normal','closed omxplayer after sigint') else: self.tick_timer=self.canvas.after(200, self.show_state_machine) elif self.play_state=='closed': # needed because wait_for_end polls the state and does not use callback self.mon.log(self," State machine: " + self.play_state) self.tick_timer=self.canvas.after(200, self.show_state_machine) else: self.mon.err(self,'unknown state in show/close state machine ' + self.play_state) self.play_state='show-failed' if self.finished_callback is not None: self.finished_callback('error','show state machine in unknown state') def parse_video_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,self.show_canvas_x1+int(fields[1]),self.show_canvas_y1+int(fields[2]),self.show_canvas_x1+int(fields[3]),self.show_canvas_y1+int(fields[4]) else: # fullscreen has_window=True return 'normal','',fields[0],has_window,self.show_canvas_x1,self.show_canvas_y1,self.show_canvas_x2,self.show_canvas_y2