from ffprobe import FFProbe from omxplayer.player import OMXPlayer from time import sleep import logging logging.basicConfig(level=logging.INFO) VIDEO_1_PATH = "app/static/videos/Iowa_Launch.mp4" sleep(5) player_log = logging.getLogger("Player 1") player = OMXPlayer(VIDEO_1_PATH) sleep(5) player.play() print print "Player is playing : ", player.is_playing() print "Player can play : ", player.can_play() sleep(10) player.quit()
class Player: """Main video player class""" def __init__(self): """Create an instance of the main video player application class""" print("Player: Creating instance") # First, we create a player that plays a black screen and stops. This is a little bit of a hacky way to get # a black background which we still have control over, but it works, and performance is good self.black = OMXPlayer('/home/pi/XTEC-PiPlayer/black.mp4', dbus_name='org.mpris.MediaPlayer2.omxplayerblack', \ args=['--no-osd', '--no-keys', '-b', '--end-paused', '--layer='+str(LAYER_UNMUTE)]) # Set OMX players (None until a video is loaded) self.omxplayer_playing = None self.omxplayer_loaded = None # Variables tracking videos playing and loaded self.playing_video_number = None # Playing video file number self.loaded_video_number = None # Loaded video file number self.playing_video_path = None # Playing video file path self.loaded_video_path = None # Loaded video file path self.is_playing = False # If we're currently playing something (i.e. not paused) self.is_looping = False # If we're looping self._dbus_id = 0 # Increments whenever a video is loaded, to ensure videos don't clash in dbus name self._check_end_thread = None # Thread that is used for checking end of video # Events for anyone observing playing and not playing events self.playing_observers = [] self.not_playing_observers = [] # self.playing_event = Event() # Playing event. Called when player starts playing # self.not_playing_event = Event() # Not playing event. Called when player isn't playing. # GPIO for DAC mute. Starts unmuted self.gpio_unmute = gpiozero.DigitalOutputDevice(pin="GPIO22", initial_value=True) # Setup operation status led outputs self.led_red = gpiozero.DigitalOutputDevice(pin="GPIO44", initial_value=False) self.led_green = gpiozero.DigitalOutputDevice(pin="GPIO45", initial_value=False) # Main folder where SD card is mounted, and where videos are stored self.video_folder = storage.SD_STORAGE_PATH ################################################################################ # Player command functions ################################################################################ def load_command(self, msg_data): """Load command sent to player. Loads video number sent to it. Can also be combined with seek command to seek to specific position in loaded video.""" print('Player: Load command') self.led_red.blink(on_time=0.1, off_time=0.1) self.led_green.off() return_code = '' seek = False # Check if there is also a seek command with the load if re.search(r'.*SE', msg_data): load_command = re.sub(r'.*SE', '', msg_data) seek = True else: # No seek command load_command = msg_data # Load the video load_return_code = self._load_video(load_command) if load_return_code == Load_Result.SUCCESS: # Are we also seeking when loading? if seek == True: # We are loading this file with a seek. # To do seek, we need to pass the video frames and duration. We use ffprobe to get # this info fps, duration = self._get_fps_duration_metadata( self.loaded_video_path) seek_timestamp = re.sub(r'SE.*', '', msg_data) seek_result, seek_time_secs = self._get_seek_time( seek_timestamp, fps, duration) if (seek_result == Seek_Result.SUCCESS) or ( seek_result == Seek_Result.SUCCESS_FRAME_ERROR): # Seek to correct position in the video self.omxplayer_loaded.set_position(seek_time_secs) time.sleep(0.1) self.omxplayer_loaded.step() if seek_result == Seek_Result.SUCCESS: return_code = 'OK1\r' elif seek_result == Seek_Result.SUCCESS_FRAME_ERROR: return_code = 'OK2\r' elif seek_result == Seek_Result.BAD_FORM: return_code = 'OK3\r' elif seek_result == Seek_Result.BAD_MM: return_code = 'OK4\r' elif seek_result == Seek_Result.BAD_SS: return_code = 'OK5\r' elif seek_result == Seek_Result.BAD_FF: return_code = 'OK6\r' elif seek_result == Seek_Result.TOO_LONG: return_code = 'OK7\r' else: return_code = 'OK1\r' elif load_return_code == Load_Result.NO_FILE: return_code = 'ER1\r' elif load_return_code == Load_Result.BAD_COMMAND: return_code = 'ER2\r' elif load_return_code == Load_Result.FILE_LOAD_ERROR: return_code = 'ER3\r' elif load_return_code == Load_Result.FILE_ALREADY_PLAYING: return_code = 'OK2\r' else: return_code = 'ER4\r' # Set status LED depending on operation and return the return code if re.match(r'OK', return_code): # Success. Turn on green led self.led_green.on() self.led_red.off() else: # Failure. Turn on red led self.led_red.on() return return_code def play_command(self, msg_data): """Play command sent to player. Sets loop to false, and plays video if not already playing Will try load video if video number included""" print('Player: Play command') self.led_red.blink(on_time=0.1, off_time=0.1) self.led_green.off() return_code = '' # Check if file number has been included new_video_loaded = True if msg_data != '': load_return_code = self._load_video(msg_data) if load_return_code == Load_Result.NO_FILE: return_code = 'ER1\r' elif load_return_code == Load_Result.BAD_COMMAND: return_code = 'ER2\r' elif load_return_code == Load_Result.FILE_LOAD_ERROR: return_code = 'ER3\r' elif load_return_code == Load_Result.FILE_ALREADY_PLAYING: new_video_loaded = False elif load_return_code != Load_Result.SUCCESS: return_code = 'ER4\r' # If there's been an error if return_code != '': self.led_red.on() return return_code # No file number included. Check there is a file already playing elif self.omxplayer_playing != None: # If the video has finished, restart it if self.omxplayer_playing.is_done(): self._load_video(self.playing_video_number) else: new_video_loaded = False # Check there is a file already loaded elif self.omxplayer_loaded != None: new_video_loaded = True # No file playing or loaded else: self.led_red.on() return 'ER5\r' if new_video_loaded: # New video has been loaded. Switch the players self._switch_loaded_to_playing() # Restart the thread to check for end of video self._restart_check_end() self.omxplayer_playing.play() self.is_playing = True self.is_looping = False self.omxplayer_playing.set_loop(False) self.led_red.off() self.led_green.on() # Notify observers we're playing for callback in self.playing_observers: callback(self) return 'OK1\r' def loop_command(self, msg_data): """Loop command sent to player. Sets loop to true, and plays video if not already playing. Will try load video if video number included""" print('Player: Loop command') self.led_red.blink(on_time=0.1, off_time=0.1) self.led_green.off() return_code = '' # Check if file number has been included new_video_loaded = True if msg_data != '': load_return_code = self._load_video(msg_data) if load_return_code == Load_Result.NO_FILE: return_code = 'ER1\r' elif load_return_code == Load_Result.BAD_COMMAND: return_code = 'ER2\r' elif load_return_code == Load_Result.FILE_LOAD_ERROR: return_code = 'ER3\r' elif load_return_code == Load_Result.FILE_ALREADY_PLAYING: new_video_loaded = False elif load_return_code != Load_Result.SUCCESS: return_code = 'ER4\r' # If there's been an error if return_code != '': self.led_red.on() return return_code # No file number included. Check there is a file already playing elif self.omxplayer_playing != None: # If the video has finished, restart it if self.omxplayer_playing.is_done(): self._load_video(self.playing_video_number) else: new_video_loaded = False # Check there is a file already loaded elif self.omxplayer_loaded != None: new_video_loaded = True # No file playing or loaded else: self.led_red.on() return 'ER5\r' if new_video_loaded: # New video has been loaded. Switch the players and play self._switch_loaded_to_playing() # Restart the thread to check for end of video self._restart_check_end() # Play video self.omxplayer_playing.play() self.is_looping = True self.is_playing = True self.omxplayer_playing.set_loop(True) self.led_red.off() self.led_green.on() # Notify observers we're playing for callback in self.playing_observers: callback(self) return 'OK1\r' def pause_command(self, msg_data): """Pause command sent to player. Pauses video if playing""" print('Player: Pause command') if self.omxplayer_playing == None: return 'ER1\r' self.omxplayer_playing.pause() self.is_playing = False # Notify observers we're not playing for callback in self.not_playing_observers: callback(self) self.led_green.off() return 'OK1\r' def stop_command(self, msg_data): """Stop command sent to player. Stops playback (which is same as quit)""" print('Player: Stop command') self.led_green.off() self.led_red.off() if self.omxplayer_playing == None: return 'ER1\r' if self.omxplayer_playing != None: self.omxplayer_playing.quit() self.omxplayer_playing = None self.playing_video_number = None self.is_playing = False # Notify observers we're not playing for callback in self.not_playing_observers: callback(self) return 'OK1\r' def seek_command(self, msg_data): """Seek command sent to player. Seek to time passed in with message (in ms)""" print('Player: Seek command') if self.omxplayer_playing == None: return 'ER6\r' # Get seek time in seconds and result code fps, duration = self._get_fps_duration_metadata( self.playing_video_path) seek_result_code, seek_time_secs = self._get_seek_time( msg_data, fps, duration) # Check result of getting the seek time if (seek_result_code == Seek_Result.SUCCESS) or ( seek_result_code == Seek_Result.SUCCESS_FRAME_ERROR): # Sikh self.omxplayer_playing.set_position(seek_time_secs) time.sleep(0.1) self.omxplayer_playing.step() if seek_result_code == Seek_Result.SUCCESS: return 'OK1\r' elif seek_result_code == Seek_Result.SUCCESS_FRAME_ERROR: return 'OK2\r' elif seek_result_code == Seek_Result.BAD_FORM: return 'ER1\r' elif seek_result_code == Seek_Result.BAD_MM: return 'ER2\r' elif seek_result_code == Seek_Result.BAD_SS: return 'ER3\r' elif seek_result_code == Seek_Result.BAD_FF: return 'ER4\r' elif seek_result_code == Seek_Result.TOO_LONG: return 'ER5\r' def video_mute_command(self, msg_data): """Video mute command sent to player. Will mute or unmute video depending on sent command""" print('Player: Video Mute command') mute_option = 0 try: mute_option = int(msg_data) except Exception: return 'ER1\r' # If we're unmuting if mute_option == 0: self.black.set_layer(LAYER_UNMUTE) return 'OK1\r' # If we're muting elif mute_option == 1: self.black.set_layer(LAYER_MUTE) return 'OK2\r' else: return 'ER2\r' def audio_mute_command(self, msg_data): """Audio mute command sent to player. Will mute or unmute audio with GPIO connection to DAC depending on sent command""" print('Player: Audio Mute command') mute_option = 0 try: mute_option = int(msg_data) except Exception: return 'ER1\r' # If we're unmuting if mute_option == 0: self.gpio_unmute.on() return 'OK1\r' # If we're muting elif mute_option == 1: self.gpio_unmute.off() return 'OK2\r' else: return 'ER2\r' ################################################################################ # Utility functions ################################################################################ def quit(self, *args): """Shuts down player""" print('Player: Shutdown') if self.omxplayer_playing != None: self.omxplayer_playing.quit() if self.omxplayer_loaded != None: self.omxplayer_loaded.quit() if self._check_end_thread != None: self._check_end_thread.join() # wait for check end thread self.black.quit() def bind_to_playing(self, callback): """Binds a callback to the playing event""" self.playing_observers.append(callback) def bind_to_not_playing(self, callback): """Binds a callback to the not playing event""" self.not_playing_observers.append(callback) def _load_video(self, command): """Tries to loads the video file number passed in""" # Try to convert command into a video number. If can't, throw error try: video_number = int(command) except Exception as ex: print("Player: Load exception. Can't convert into number: " + str(ex)) return Load_Result.BAD_COMMAND if video_number == self.playing_video_number: # Video already playing if not self.omxplayer_playing.is_done(): return Load_Result.FILE_ALREADY_PLAYING elif video_number == self.loaded_video_number: # Video already loaded return Load_Result.SUCCESS # Search in video folder basepath = Path(self.video_folder) # Go through every file with 5 digits at the end of file name for video_file in basepath.glob('*[0-9][0-9][0-9][0-9][0-9].*'): # Extract number. Get number at end of file name, remove the file extension part, cast into an int file_number = int( re.findall(r'\d\d\d\d\d\.', video_file.name)[0][0:5]) # Check if we have a match if file_number == video_number: # We have a match # Quit different loaded video if there is one if self.omxplayer_loaded != None: self.omxplayer_loaded.quit() self.omxplayer_loaded = None try: # Get audio setting config = configparser.ConfigParser() config.read(config_path) audio = config['MP2']['audio'] # Load video. dbus name will be appended with the video number, so every new player will have unique dbus name arguments = [ '-g', '--no-osd', '--no-keys', '--start-paused', '--end-paused', '--layer=' + str(LAYER_LOADING), '--adev=' + audio ] self.omxplayer_loaded = OMXPlayer(str(video_file.resolve()), \ dbus_name='org.mpris.MediaPlayer2.omxplayer' + str(video_number) + '_' + str(self._dbus_id), \ args=arguments) # This checks if the player has been correctly loaded. Will raise exception if error self.omxplayer_loaded.can_play() self._dbus_id += 1 # Increment dbus id except Exception as ex: # Issue loading. Most likely incorrect file print( 'Load: error loading file. Most likely incorrect file format: ' + str(ex)) self.omxplayer_loaded = None return Load_Result.FILE_LOAD_ERROR # Keep track of loaded video self.loaded_video_number = video_number self.loaded_video_path = str(video_file.resolve()) return Load_Result.SUCCESS return Load_Result.NO_FILE def _get_seek_time(self, seek_time, video_frames, video_duration): """Gets seek time passed in seconds. Returns result. Done this way because in the case of LD, we may want to do a seek for a video that isn't this one""" seek_time = seek_time.strip() seek_time = seek_time.lstrip() # Check time stamp is correct format if not re.match(r'^\d\d:\d\d:\d\d:\d\d$', seek_time): return Seek_Result.BAD_FORM, 0 timestamp_parts = seek_time.split(':') # Add hour seek time seek_hours = int(timestamp_parts[0]) seek_time_secs = seek_hours * 3600 # Add minutes seek time seek_mins = int(timestamp_parts[1]) if seek_mins > 59: return Seek_Result.BAD_MM, 0 seek_time_secs = seek_time_secs + (seek_mins * 60) # Add seconds seek time seek_seconds = int(timestamp_parts[2]) if seek_seconds > 59: return Seek_Result.BAD_SS, 0 seek_time_secs = seek_time_secs + (seek_seconds) # Add frame seek time seek_frames = int(timestamp_parts[3]) frame_error = False if (video_frames == 0) or (video_frames == None): print('Seek: frame seek error. Ignoring ff for seek') frame_error = True else: if seek_frames > video_frames: return Seek_Result.BAD_FF, 0 else: seek_time_secs = seek_time_secs + ( (1 / video_frames) * seek_frames) # Check if seek time is actually within the video duration if seek_time_secs > video_duration: return Seek_Result.TOO_LONG, 0 if frame_error: return Seek_Result.SUCCESS_FRAME_ERROR, seek_time_secs else: return Seek_Result.SUCCESS, seek_time_secs def _get_fps_duration_metadata(self, videopath): """Function to find the fps and duration of the input video file""" # try get fps and duration fps = 0 duration = 0 try: cmd = "ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1" args = shlex.split(cmd) args.append(videopath) # run the ffprobe process, decode stdout into utf-8 duration = float(subprocess.check_output(args).decode('utf-8')) # Get information from final element in probe output cmd = "ffprobe -v error -select_streams v:0 -show_entries stream=avg_frame_rate -of default=noprint_wrappers=1:nokey=1" args = shlex.split(cmd) args.append(videopath) aeval = Interpreter() fps = aeval(subprocess.check_output(args).decode('utf-8')) except Exception as ex: print('Error getting metadata: ' + str(ex)) return fps, duration def _check_end(self): """Will constantly poll currently playing media to check if it's stopped""" while True: # Wrap in try except as it is possible for omxplayer to be closed, and thus raise an exception try: while (self.omxplayer_playing.is_done() != True): time.sleep(0.1) pass self.is_playing = False self.led_green.off() self.led_red.off() # Notify observers we're not playing for callback in self.not_playing_observers: callback(self) return except Exception as ex: # Playing omxplayer has been closed print( 'Exception in check end thread. Most likely playing omxplayer has been closed: ' + str(ex)) return def _restart_check_end(self): """Restarts the check for end thread""" # Wait for old thread to exit if self._check_end_thread != None: while self._check_end_thread.is_alive(): pass # Start thread to check end of playing video self._check_end_thread = threading.Thread(target=self._check_end, daemon=True) self._check_end_thread.start() def _switch_loaded_to_playing(self): """Puts loaded player to top layer""" self.omxplayer_loaded.set_layer(LAYER_LOOP) if self.omxplayer_playing != None: self.omxplayer_playing.stop() self.omxplayer_playing.quit() self._check_end_thread.join() self.omxplayer_playing = None # When moving to playing layer, it automatically plays the video in omx self.omxplayer_loaded.set_layer(LAYER_PLAYING) self.omxplayer_playing = self.omxplayer_loaded self.omxplayer_loaded = None self.playing_video_number = self.loaded_video_number self.playing_video_path = self.loaded_video_path self.loaded_video_number = None self.loaded_video_path = None
class MusicPlayer(): def __init__(self, playlist, logger): self.playlist = playlist self.idx = 0 self.logger = logger self.omxplayer = None def get_song_real_url(self, song_url): try: htmldoc = urlopen(song_url).read().decode('utf8') except: return (None, None, 0, 0) content = json.loads(htmldoc) try: song_link = content['data']['songList'][0]['songLink'] song_name = content['data']['songList'][0]['songName'] song_size = int(content['data']['songList'][0]['size']) song_time = int(content['data']['songList'][0]['time']) except: self.logger.error('get real link failed') return (None, None, 0, 0) return song_name, song_link, song_size, song_time def play(self): self.logger.debug('MusicPlayer play') song_url = "http://music.baidu.com/data/music/fmlink?" +\ "type=mp3&rate=320&songIds=%s" % self.playlist[self.idx]['id'] song_name, song_link, song_size, song_time =\ self.get_song_real_url(song_url) self.play_mp3_by_link(song_link, song_name, song_size, song_time) def play_mp3_by_link(self, song_link, song_name, song_size, song_time): if song_link: self.omxplayer = OMXPlayer(song_link, ['-o', 'both']) else: self.omxplayer = None def pick_next(self): self.idx += 1 if self.idx > len(self.playlist) - 1: self.idx = 0 if self.omxplayer and self.omxplayer.can_play(): self.omxplayer.quit() def pick_previous(self): self.idx -= 1 if self.idx < 0: self.idx = 0 if self.omxplayer and self.omxplayer.can_play(): self.omxplayer.quit() def pause(self): if self.omxplayer and self.omxplayer.can_pause(): self.omxplayer.pause() def resume(self): if self.omxplayer and self.omxplayer.can_play(): self.omxplayer.play() def is_play_done(self): try: return self.omxplayer is None or\ self.omxplayer.can_play() is None except Exception as e: self.logger.error(e) return False def stop(self): self.playlist = [] if self.omxplayer: self.omxplayer.quit()