/kivy/core/video/video_ffpyplayer.py

https://github.com/yukirin/kivy · Python · 323 lines · 288 code · 17 blank · 18 comment · 27 complexity · 96236b49126a0531f7a8cc16e51ce101 MD5 · raw file

  1. '''
  2. FFmpeg based video abstraction
  3. ==============================
  4. To use, you need to install ffpyplyaer and have a compiled ffmpeg shared
  5. library.
  6. https://github.com/matham/ffpyplayer
  7. The docs there describe how to set this up. But briefly, first you need to
  8. compile ffmpeg using the shared flags while disabling the static flags (you'll
  9. probably have to set the fPIC flag, e.g. CFLAGS=-fPIC). Here's some
  10. instructions: https://trac.ffmpeg.org/wiki/CompilationGuide. For Windows, you
  11. can download compiled GPL binaries from http://ffmpeg.zeranoe.com/builds/.
  12. Similarly, you should download SDL.
  13. Now, you should a ffmpeg and sdl directory. In each, you should have a include,
  14. bin, and lib directory, where e.g. for Windows, lib contains the .dll.a files,
  15. while bin contains the actual dlls. The include directory holds the headers.
  16. The bin directory is only needed if the shared libraries are not already on
  17. the path. In the environment define FFMPEG_ROOT and SDL_ROOT, each pointing to
  18. the ffmpeg, and SDL directories, respectively. (If you're using SDL2,
  19. the include directory will contain a directory called SDL2, which then holds
  20. the headers).
  21. Once defined, download the ffpyplayer git and run
  22. python setup.py build_ext --inplace
  23. Finally, before running you need to ensure that ffpyplayer is in python's path.
  24. ..Note::
  25. When kivy exits by closing the window while the video is playing,
  26. it appears that the __del__method of VideoFFPy
  27. is not called. Because of this the VideoFFPy object is not
  28. properly deleted when kivy exits. The consequence is that because
  29. MediaPlayer creates internal threads which do not have their daemon
  30. flag set, when the main threads exists it'll hang and wait for the other
  31. MediaPlayer threads to exit. But since __del__ is not called to delete the
  32. MediaPlayer object, those threads will remain alive hanging kivy. What this
  33. means is that you have to be sure to delete the MediaPlayer object before
  34. kivy exits by setting it to None.
  35. '''
  36. __all__ = ('VideoFFPy', )
  37. try:
  38. import ffpyplayer
  39. from ffpyplayer.player import MediaPlayer
  40. from ffpyplayer.tools import set_log_callback, loglevels, get_log_callback
  41. except:
  42. raise
  43. from threading import Thread
  44. from kivy.clock import Clock, mainthread
  45. from kivy.logger import Logger
  46. from kivy.core.video import VideoBase
  47. from kivy.graphics import Rectangle, BindTexture
  48. from kivy.graphics.texture import Texture
  49. from kivy.graphics.fbo import Fbo
  50. from kivy.weakmethod import WeakMethod
  51. import time
  52. Logger.info('VideoFFPy: Using ffpyplayer {}'.format(ffpyplayer.version))
  53. logger_func = {'quiet': Logger.critical, 'panic': Logger.critical,
  54. 'fatal': Logger.critical, 'error': Logger.error,
  55. 'warning': Logger.warning, 'info': Logger.info,
  56. 'verbose': Logger.debug, 'debug': Logger.debug}
  57. def _log_callback(message, level):
  58. message = message.strip()
  59. if message:
  60. logger_func[level]('ffpyplayer: {}'.format(message))
  61. class VideoFFPy(VideoBase):
  62. YUV_RGB_FS = """
  63. $HEADER$
  64. uniform sampler2D tex_y;
  65. uniform sampler2D tex_u;
  66. uniform sampler2D tex_v;
  67. void main(void) {
  68. float y = texture2D(tex_y, tex_coord0).r;
  69. float u = texture2D(tex_u, tex_coord0).r - 0.5;
  70. float v = texture2D(tex_v, tex_coord0).r - 0.5;
  71. float r = y + 1.402 * v;
  72. float g = y - 0.344 * u - 0.714 * v;
  73. float b = y + 1.772 * u;
  74. gl_FragColor = vec4(r, g, b, 1.0);
  75. }
  76. """
  77. def __init__(self, **kwargs):
  78. self._ffplayer = None
  79. self._thread = None
  80. self._next_frame = None
  81. self._ffplayer_need_quit = False
  82. self._log_callback_set = False
  83. self._callback_ref = WeakMethod(self._player_callback)
  84. self._trigger = Clock.create_trigger(self._redraw)
  85. if not get_log_callback():
  86. set_log_callback(_log_callback)
  87. self._log_callback_set = True
  88. super(VideoFFPy, self).__init__(**kwargs)
  89. def __del__(self):
  90. self.unload()
  91. if self._log_callback_set:
  92. set_log_callback(None)
  93. def _player_callback(self, selector, value):
  94. if self._ffplayer is None:
  95. return
  96. if selector == 'quit':
  97. def close(*args):
  98. self.unload()
  99. Clock.schedule_once(close, 0)
  100. def _get_position(self):
  101. if self._ffplayer is not None:
  102. return self._ffplayer.get_pts()
  103. return 0
  104. def _set_position(self, pos):
  105. self.seek(pos)
  106. def _get_volume(self):
  107. if self._ffplayer is not None:
  108. self._volume = self._ffplayer.get_volume()
  109. return self._volume
  110. def _set_volume(self, volume):
  111. self._volume = volume
  112. if self._ffplayer is not None:
  113. self._ffplayer.set_volume(volume)
  114. def _get_duration(self):
  115. if self._ffplayer is None:
  116. return 0
  117. return self._ffplayer.get_metadata()['duration']
  118. @mainthread
  119. def _do_eos(self):
  120. if self.eos == 'pause':
  121. self.pause()
  122. elif self.eos == 'stop':
  123. self.stop()
  124. elif self.eos == 'loop':
  125. self.position = 0
  126. self.dispatch('on_eos')
  127. @mainthread
  128. def _change_state(self, state):
  129. self._state = state
  130. def _redraw(self, *args):
  131. if not self._ffplayer:
  132. return
  133. next_frame = self._next_frame
  134. if not next_frame:
  135. return
  136. img, pts = next_frame
  137. if img.get_size() != self._size or self._texture is None:
  138. self._size = w, h = img.get_size()
  139. if self._out_fmt == 'yuv420p':
  140. w2 = int(w / 2)
  141. h2 = int(h / 2)
  142. self._tex_y = Texture.create(size=(w, h), colorfmt='luminance')
  143. self._tex_u = Texture.create(size=(w2, h2), colorfmt='luminance')
  144. self._tex_v = Texture.create(size=(w2, h2), colorfmt='luminance')
  145. self._fbo = fbo = Fbo(size=self._size)
  146. with fbo:
  147. BindTexture(texture=self._tex_u, index=1)
  148. BindTexture(texture=self._tex_v, index=2)
  149. Rectangle(size=fbo.size, texture=self._tex_y)
  150. fbo.shader.fs = VideoFFPy.YUV_RGB_FS
  151. fbo['tex_y'] = 0
  152. fbo['tex_u'] = 1
  153. fbo['tex_v'] = 2
  154. self._texture = fbo.texture
  155. else:
  156. self._texture = Texture.create(size=self._size, colorfmt='rgba')
  157. # XXX FIXME
  158. #self.texture.add_reload_observer(self.reload_buffer)
  159. self._texture.flip_vertical()
  160. self.dispatch('on_load')
  161. if self._out_fmt == 'yuv420p':
  162. dy, du, dv, _ = img.to_memoryview()
  163. self._tex_y.blit_buffer(dy, colorfmt='luminance')
  164. self._tex_u.blit_buffer(du, colorfmt='luminance')
  165. self._tex_v.blit_buffer(dv, colorfmt='luminance')
  166. else:
  167. self._texture.blit_buffer(img.to_memoryview()[0], colorfmt='rgba')
  168. self._fbo.ask_update()
  169. self._fbo.draw()
  170. self.dispatch('on_frame')
  171. def _next_frame_run(self):
  172. ffplayer = self._ffplayer
  173. sleep = time.sleep
  174. trigger = self._trigger
  175. did_dispatch_eof = False
  176. # fast path, if the source video is yuv420p, we'll use a glsl shader for
  177. # buffer conversion to rgba
  178. while not self._ffplayer_need_quit:
  179. src_pix_fmt = ffplayer.get_metadata().get('src_pix_fmt')
  180. if not src_pix_fmt:
  181. sleep(0.005)
  182. continue
  183. if src_pix_fmt == 'yuv420p':
  184. self._out_fmt = 'yuv420p'
  185. ffplayer.set_output_pix_fmt(self._out_fmt)
  186. self._ffplayer.toggle_pause()
  187. break
  188. if self._ffplayer_need_quit:
  189. return
  190. # wait until loaded or failed, shouldn't take long, but just to make
  191. # sure metadata is available.
  192. s = time.clock()
  193. while not self._ffplayer_need_quit:
  194. if ffplayer.get_metadata()['src_vid_size'] != (0, 0):
  195. break
  196. # XXX if will fail later then?
  197. if time.clock() - s > 10.:
  198. break
  199. sleep(0.005)
  200. if self._ffplayer_need_quit:
  201. return
  202. # we got all the informations, now, get the frames :)
  203. self._change_state('playing')
  204. while not self._ffplayer_need_quit:
  205. t1 = time.time()
  206. frame, val = ffplayer.get_frame()
  207. t2 = time.time()
  208. if val == 'eof':
  209. sleep(0.2)
  210. if not did_dispatch_eof:
  211. self._do_eos()
  212. did_dispatch_eof = True
  213. elif val == 'paused':
  214. did_dispatch_eof = False
  215. sleep(0.2)
  216. else:
  217. did_dispatch_eof = False
  218. if frame:
  219. self._next_frame = frame
  220. trigger()
  221. else:
  222. val = val if val else (1 / 30.)
  223. sleep(val)
  224. def seek(self, percent):
  225. if self._ffplayer is None:
  226. return
  227. self._ffplayer.seek(percent * self._ffplayer.get_metadata()
  228. ['duration'], relative=False)
  229. self._next_frame = None
  230. def stop(self):
  231. self.unload()
  232. def pause(self):
  233. if self._ffplayer and self._state != 'paused':
  234. self._ffplayer.toggle_pause()
  235. self._state = 'paused'
  236. def play(self):
  237. if self._ffplayer and self._state == 'paused':
  238. self._ffplayer.toggle_pause()
  239. self._state = 'playing'
  240. return
  241. self.load()
  242. self._out_fmt = 'rgba'
  243. ff_opts = {
  244. 'paused': True,
  245. 'out_fmt': self._out_fmt
  246. }
  247. self._ffplayer = MediaPlayer(
  248. self._filename, callback=self._callback_ref,
  249. thread_lib='SDL',
  250. loglevel='info', ff_opts=ff_opts)
  251. self._thread = Thread(target=self._next_frame_run, name='Next frame')
  252. self._thread.start()
  253. def load(self):
  254. self.unload()
  255. def unload(self):
  256. Clock.unschedule(self._redraw)
  257. self._ffplayer_need_quit = True
  258. if self._thread:
  259. self._thread.join()
  260. self._thread = None
  261. if self._ffplayer:
  262. self._ffplayer = None
  263. self._next_frame = None
  264. self._size = (0, 0)
  265. self._state = ''
  266. self._ffplayer_need_quit = False