You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

271 lines
12 KiB

  1. # Copyright 2015 Adafruit Industries.
  2. # Author: Tony DiCola
  3. # License: GNU GPLv2, see LICENSE.txt
  4. import configparser
  5. import importlib
  6. import os
  7. import re
  8. import sys
  9. import signal
  10. import time
  11. import pygame
  12. from .model import Playlist
  13. # Basic video looper architecure:
  14. #
  15. # - VideoLooper class contains all the main logic for running the looper program.
  16. #
  17. # - Almost all state is configured in a .ini config file which is required for
  18. # loading and using the VideoLooper class.
  19. #
  20. # - VideoLooper has loose coupling with file reader and video player classes that
  21. # are used to find movie files and play videos respectively. The configuration
  22. # defines which file reader and video player module will be loaded.
  23. #
  24. # - A file reader module needs to define at top level create_file_reader function
  25. # that takes as a parameter a ConfigParser config object. The function should
  26. # return an instance of a file reader class. See usb_drive.py and directory.py
  27. # for the two provided file readers and their public interface.
  28. #
  29. # - Similarly a video player modules needs to define a top level create_player
  30. # function that takes in configuration. See omxplayer.py and hello_video.py
  31. # for the two provided video players and their public interface.
  32. #
  33. # - Future file readers and video players can be provided and referenced in the
  34. # config to extend the video player use to read from different file sources
  35. # or use different video players.
  36. class VideoLooper(object):
  37. def __init__(self, config_path):
  38. """Create an instance of the main video looper application class. Must
  39. pass path to a valid video looper ini configuration file.
  40. """
  41. # Load the configuration.
  42. self._config = configparser.ConfigParser()
  43. if len(self._config.read(config_path)) == 0:
  44. raise RuntimeError('Failed to find configuration file at {0}, is the application properly installed?'.format(config_path))
  45. self._console_output = self._config.getboolean('video_looper', 'console_output')
  46. # Load configured video player and file reader modules.
  47. self._player = self._load_player()
  48. self._reader = self._load_file_reader()
  49. # Load other configuration values.
  50. self._osd = self._config.getboolean('video_looper', 'osd')
  51. self._is_random = self._config.getboolean('video_looper', 'is_random')
  52. self._keyboard_control = self._config.getboolean('video_looper', 'keyboard_control')
  53. # Parse string of 3 comma separated values like "255, 255, 255" into
  54. # list of ints for colors.
  55. self._bgcolor = list(map(int, self._config.get('video_looper', 'bgcolor')
  56. .translate(str.maketrans('','', ','))
  57. .split()))
  58. self._fgcolor = list(map(int, self._config.get('video_looper', 'fgcolor')
  59. .translate(str.maketrans('','', ','))
  60. .split()))
  61. # Load sound volume file name value
  62. self._sound_vol_file = self._config.get('omxplayer', 'sound_vol_file');
  63. # default value to 0 millibels (omxplayer)
  64. self._sound_vol = 0
  65. # Initialize pygame and display a blank screen.
  66. pygame.display.init()
  67. pygame.font.init()
  68. pygame.mouse.set_visible(False)
  69. size = (pygame.display.Info().current_w, pygame.display.Info().current_h)
  70. self._screen = pygame.display.set_mode(size, pygame.FULLSCREEN)
  71. self._blank_screen()
  72. # Set other static internal state.
  73. self._extensions = self._player.supported_extensions()
  74. self._small_font = pygame.font.Font(None, 50)
  75. self._big_font = pygame.font.Font(None, 250)
  76. self._running = True
  77. def _print(self, message):
  78. """Print message to standard output if console output is enabled."""
  79. if self._console_output:
  80. print(message)
  81. def _load_player(self):
  82. """Load the configured video player and return an instance of it."""
  83. module = self._config.get('video_looper', 'video_player')
  84. return importlib.import_module('.' + module, 'Adafruit_Video_Looper') \
  85. .create_player(self._config)
  86. def _load_file_reader(self):
  87. """Load the configured file reader and return an instance of it."""
  88. module = self._config.get('video_looper', 'file_reader')
  89. return importlib.import_module('.' + module, 'Adafruit_Video_Looper') \
  90. .create_file_reader(self._config)
  91. def _is_number(iself, s):
  92. try:
  93. float(s)
  94. return True
  95. except ValueError:
  96. return False
  97. def _build_playlist(self):
  98. """Search all the file reader paths for movie files with the provided
  99. extensions.
  100. """
  101. # Get list of paths to search from the file reader.
  102. paths = self._reader.search_paths()
  103. # Enumerate all movie files inside those paths.
  104. movies = []
  105. for ex in self._extensions:
  106. for path in paths:
  107. # Skip paths that don't exist or are files.
  108. if not os.path.exists(path) or not os.path.isdir(path):
  109. continue
  110. # Ignore hidden files (useful when file loaded on usb
  111. # key from an OSX computer
  112. movies.extend(['{0}/{1}'.format(path.rstrip('/'), x) \
  113. for x in os.listdir(path) \
  114. if re.search('\.{0}$'.format(ex), x,
  115. flags=re.IGNORECASE) and \
  116. x[0] is not '.'])
  117. # Get the video volume from the file in the usb key
  118. sound_vol_file_path = '{0}/{1}'.format(path.rstrip('/'), self._sound_vol_file)
  119. if os.path.exists(sound_vol_file_path):
  120. with open(sound_vol_file_path, 'r') as sound_file:
  121. sound_vol_string = sound_file.readline()
  122. if self._is_number(sound_vol_string):
  123. self._sound_vol = int(float(sound_vol_string))
  124. # Create a playlist with the sorted list of movies.
  125. return Playlist(sorted(movies), self._is_random)
  126. def _blank_screen(self):
  127. """Render a blank screen filled with the background color."""
  128. self._screen.fill(self._bgcolor)
  129. pygame.display.update()
  130. def _render_text(self, message, font=None):
  131. """Draw the provided message and return as pygame surface of it rendered
  132. with the configured foreground and background color.
  133. """
  134. # Default to small font if not provided.
  135. if font is None:
  136. font = self._small_font
  137. return font.render(message, True, self._fgcolor, self._bgcolor)
  138. def _animate_countdown(self, playlist, seconds=10):
  139. """Print text with the number of loaded movies and a quick countdown
  140. message if the on screen display is enabled.
  141. """
  142. # Print message to console with number of movies in playlist.
  143. message = 'Found {0} movie{1}.'.format(playlist.length(),
  144. 's' if playlist.length() >= 2 else '')
  145. self._print(message)
  146. # Do nothing else if the OSD is turned off.
  147. if not self._osd:
  148. return
  149. # Draw message with number of movies loaded and animate countdown.
  150. # First render text that doesn't change and get static dimensions.
  151. label1 = self._render_text(message + ' Starting playback in:')
  152. l1w, l1h = label1.get_size()
  153. sw, sh = self._screen.get_size()
  154. for i in range(seconds, 0, -1):
  155. # Each iteration of the countdown rendering changing text.
  156. label2 = self._render_text(str(i), self._big_font)
  157. l2w, l2h = label2.get_size()
  158. # Clear screen and draw text with line1 above line2 and all
  159. # centered horizontally and vertically.
  160. self._screen.fill(self._bgcolor)
  161. self._screen.blit(label1, (sw/2-l1w/2, sh/2-l2h/2-l1h))
  162. self._screen.blit(label2, (sw/2-l2w/2, sh/2-l2h/2))
  163. pygame.display.update()
  164. # Pause for a second between each frame.
  165. time.sleep(1)
  166. def _idle_message(self):
  167. """Print idle message from file reader."""
  168. # Print message to console.
  169. message = self._reader.idle_message()
  170. self._print(message)
  171. # Do nothing else if the OSD is turned off.
  172. if not self._osd:
  173. return
  174. # Display idle message in center of screen.
  175. label = self._render_text(message)
  176. lw, lh = label.get_size()
  177. sw, sh = self._screen.get_size()
  178. self._screen.fill(self._bgcolor)
  179. self._screen.blit(label, (sw/2-lw/2, sh/2-lh/2))
  180. # If keyboard control is enabled, display message about it
  181. if self._keyboard_control:
  182. label2 = self._render_text('press ESC to quit')
  183. l2w, l2h = label2.get_size()
  184. self._screen.blit(label2, (sw/2-l2w/2, sh/2-l2h/2+lh))
  185. pygame.display.update()
  186. def _prepare_to_run_playlist(self, playlist):
  187. """Display messages when a new playlist is loaded."""
  188. # If there are movies to play show a countdown first (if OSD enabled),
  189. # or if no movies are available show the idle message.
  190. if playlist.length() > 0:
  191. self._animate_countdown(playlist)
  192. self._blank_screen()
  193. else:
  194. self._idle_message()
  195. def run(self):
  196. """Main program loop. Will never return!"""
  197. # Get playlist of movies to play from file reader.
  198. playlist = self._build_playlist()
  199. self._prepare_to_run_playlist(playlist)
  200. # Main loop to play videos in the playlist and listen for file changes.
  201. while self._running:
  202. # Load and play a new movie if nothing is playing.
  203. if not self._player.is_playing():
  204. movie = playlist.get_next()
  205. if movie is not None:
  206. # Start playing the first available movie.
  207. self._print('Playing movie: {0}'.format(movie))
  208. self._player.play(movie, loop=playlist.length() == 1, vol = self._sound_vol)
  209. # Check for changes in the file search path (like USB drives added)
  210. # and rebuild the playlist.
  211. if self._reader.is_changed():
  212. self._player.stop(3) # Up to 3 second delay waiting for old
  213. # player to stop.
  214. # Rebuild playlist and show countdown again (if OSD enabled).
  215. playlist = self._build_playlist()
  216. self._prepare_to_run_playlist(playlist)
  217. # Event handling for key press, if keyboard control is enabled
  218. if self._keyboard_control:
  219. for event in pygame.event.get():
  220. if event.type == pygame.KEYDOWN:
  221. # If pressed key is ESC quit program
  222. if event.key == pygame.K_ESCAPE:
  223. self.quit()
  224. # Give the CPU some time to do other tasks.
  225. time.sleep(0.002)
  226. def quit(self):
  227. """Shut down the program"""
  228. self._running = False
  229. if self._player is not None:
  230. self._player.stop()
  231. pygame.quit()
  232. def signal_quit(self, signal, frame):
  233. """Shut down the program, meant to by called by signal handler."""
  234. self.quit()
  235. # Main entry point.
  236. if __name__ == '__main__':
  237. print('Starting Adafruit Video Looper.')
  238. # Default config path to /boot.
  239. config_path = '/boot/video_looper.ini'
  240. # Override config path if provided as parameter.
  241. if len(sys.argv) == 2:
  242. config_path = sys.argv[1]
  243. # Create video looper.
  244. videolooper = VideoLooper(config_path)
  245. # Configure signal handlers to quit on TERM or INT signal.
  246. signal.signal(signal.SIGTERM, videolooper.signal_quit)
  247. signal.signal(signal.SIGINT, videolooper.signal_quit)
  248. # Run the main loop.
  249. videolooper.run()