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.

video_looper.py 9.4 KiB

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