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.

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