/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
- '''
- FFmpeg based video abstraction
- ==============================
- To use, you need to install ffpyplyaer and have a compiled ffmpeg shared
- library.
- https://github.com/matham/ffpyplayer
- The docs there describe how to set this up. But briefly, first you need to
- compile ffmpeg using the shared flags while disabling the static flags (you'll
- probably have to set the fPIC flag, e.g. CFLAGS=-fPIC). Here's some
- instructions: https://trac.ffmpeg.org/wiki/CompilationGuide. For Windows, you
- can download compiled GPL binaries from http://ffmpeg.zeranoe.com/builds/.
- Similarly, you should download SDL.
- Now, you should a ffmpeg and sdl directory. In each, you should have a include,
- bin, and lib directory, where e.g. for Windows, lib contains the .dll.a files,
- while bin contains the actual dlls. The include directory holds the headers.
- The bin directory is only needed if the shared libraries are not already on
- the path. In the environment define FFMPEG_ROOT and SDL_ROOT, each pointing to
- the ffmpeg, and SDL directories, respectively. (If you're using SDL2,
- the include directory will contain a directory called SDL2, which then holds
- the headers).
- Once defined, download the ffpyplayer git and run
- python setup.py build_ext --inplace
- Finally, before running you need to ensure that ffpyplayer is in python's path.
- ..Note::
- When kivy exits by closing the window while the video is playing,
- it appears that the __del__method of VideoFFPy
- is not called. Because of this the VideoFFPy object is not
- properly deleted when kivy exits. The consequence is that because
- MediaPlayer creates internal threads which do not have their daemon
- flag set, when the main threads exists it'll hang and wait for the other
- MediaPlayer threads to exit. But since __del__ is not called to delete the
- MediaPlayer object, those threads will remain alive hanging kivy. What this
- means is that you have to be sure to delete the MediaPlayer object before
- kivy exits by setting it to None.
- '''
- __all__ = ('VideoFFPy', )
- try:
- import ffpyplayer
- from ffpyplayer.player import MediaPlayer
- from ffpyplayer.tools import set_log_callback, loglevels, get_log_callback
- except:
- raise
- from threading import Thread
- from kivy.clock import Clock, mainthread
- from kivy.logger import Logger
- from kivy.core.video import VideoBase
- from kivy.graphics import Rectangle, BindTexture
- from kivy.graphics.texture import Texture
- from kivy.graphics.fbo import Fbo
- from kivy.weakmethod import WeakMethod
- import time
- Logger.info('VideoFFPy: Using ffpyplayer {}'.format(ffpyplayer.version))
- logger_func = {'quiet': Logger.critical, 'panic': Logger.critical,
- 'fatal': Logger.critical, 'error': Logger.error,
- 'warning': Logger.warning, 'info': Logger.info,
- 'verbose': Logger.debug, 'debug': Logger.debug}
- def _log_callback(message, level):
- message = message.strip()
- if message:
- logger_func[level]('ffpyplayer: {}'.format(message))
- class VideoFFPy(VideoBase):
- YUV_RGB_FS = """
- $HEADER$
- uniform sampler2D tex_y;
- uniform sampler2D tex_u;
- uniform sampler2D tex_v;
- void main(void) {
- float y = texture2D(tex_y, tex_coord0).r;
- float u = texture2D(tex_u, tex_coord0).r - 0.5;
- float v = texture2D(tex_v, tex_coord0).r - 0.5;
- float r = y + 1.402 * v;
- float g = y - 0.344 * u - 0.714 * v;
- float b = y + 1.772 * u;
- gl_FragColor = vec4(r, g, b, 1.0);
- }
- """
- def __init__(self, **kwargs):
- self._ffplayer = None
- self._thread = None
- self._next_frame = None
- self._ffplayer_need_quit = False
- self._log_callback_set = False
- self._callback_ref = WeakMethod(self._player_callback)
- self._trigger = Clock.create_trigger(self._redraw)
- if not get_log_callback():
- set_log_callback(_log_callback)
- self._log_callback_set = True
- super(VideoFFPy, self).__init__(**kwargs)
- def __del__(self):
- self.unload()
- if self._log_callback_set:
- set_log_callback(None)
- def _player_callback(self, selector, value):
- if self._ffplayer is None:
- return
- if selector == 'quit':
- def close(*args):
- self.unload()
- Clock.schedule_once(close, 0)
- def _get_position(self):
- if self._ffplayer is not None:
- return self._ffplayer.get_pts()
- return 0
- def _set_position(self, pos):
- self.seek(pos)
- def _get_volume(self):
- if self._ffplayer is not None:
- self._volume = self._ffplayer.get_volume()
- return self._volume
- def _set_volume(self, volume):
- self._volume = volume
- if self._ffplayer is not None:
- self._ffplayer.set_volume(volume)
- def _get_duration(self):
- if self._ffplayer is None:
- return 0
- return self._ffplayer.get_metadata()['duration']
- @mainthread
- def _do_eos(self):
- if self.eos == 'pause':
- self.pause()
- elif self.eos == 'stop':
- self.stop()
- elif self.eos == 'loop':
- self.position = 0
- self.dispatch('on_eos')
- @mainthread
- def _change_state(self, state):
- self._state = state
- def _redraw(self, *args):
- if not self._ffplayer:
- return
- next_frame = self._next_frame
- if not next_frame:
- return
- img, pts = next_frame
- if img.get_size() != self._size or self._texture is None:
- self._size = w, h = img.get_size()
- if self._out_fmt == 'yuv420p':
- w2 = int(w / 2)
- h2 = int(h / 2)
- self._tex_y = Texture.create(size=(w, h), colorfmt='luminance')
- self._tex_u = Texture.create(size=(w2, h2), colorfmt='luminance')
- self._tex_v = Texture.create(size=(w2, h2), colorfmt='luminance')
- self._fbo = fbo = Fbo(size=self._size)
- with fbo:
- BindTexture(texture=self._tex_u, index=1)
- BindTexture(texture=self._tex_v, index=2)
- Rectangle(size=fbo.size, texture=self._tex_y)
- fbo.shader.fs = VideoFFPy.YUV_RGB_FS
- fbo['tex_y'] = 0
- fbo['tex_u'] = 1
- fbo['tex_v'] = 2
- self._texture = fbo.texture
- else:
- self._texture = Texture.create(size=self._size, colorfmt='rgba')
- # XXX FIXME
- #self.texture.add_reload_observer(self.reload_buffer)
- self._texture.flip_vertical()
- self.dispatch('on_load')
- if self._out_fmt == 'yuv420p':
- dy, du, dv, _ = img.to_memoryview()
- self._tex_y.blit_buffer(dy, colorfmt='luminance')
- self._tex_u.blit_buffer(du, colorfmt='luminance')
- self._tex_v.blit_buffer(dv, colorfmt='luminance')
- else:
- self._texture.blit_buffer(img.to_memoryview()[0], colorfmt='rgba')
- self._fbo.ask_update()
- self._fbo.draw()
- self.dispatch('on_frame')
- def _next_frame_run(self):
- ffplayer = self._ffplayer
- sleep = time.sleep
- trigger = self._trigger
- did_dispatch_eof = False
- # fast path, if the source video is yuv420p, we'll use a glsl shader for
- # buffer conversion to rgba
- while not self._ffplayer_need_quit:
- src_pix_fmt = ffplayer.get_metadata().get('src_pix_fmt')
- if not src_pix_fmt:
- sleep(0.005)
- continue
- if src_pix_fmt == 'yuv420p':
- self._out_fmt = 'yuv420p'
- ffplayer.set_output_pix_fmt(self._out_fmt)
- self._ffplayer.toggle_pause()
- break
- if self._ffplayer_need_quit:
- return
- # wait until loaded or failed, shouldn't take long, but just to make
- # sure metadata is available.
- s = time.clock()
- while not self._ffplayer_need_quit:
- if ffplayer.get_metadata()['src_vid_size'] != (0, 0):
- break
- # XXX if will fail later then?
- if time.clock() - s > 10.:
- break
- sleep(0.005)
- if self._ffplayer_need_quit:
- return
- # we got all the informations, now, get the frames :)
- self._change_state('playing')
- while not self._ffplayer_need_quit:
- t1 = time.time()
- frame, val = ffplayer.get_frame()
- t2 = time.time()
- if val == 'eof':
- sleep(0.2)
- if not did_dispatch_eof:
- self._do_eos()
- did_dispatch_eof = True
- elif val == 'paused':
- did_dispatch_eof = False
- sleep(0.2)
- else:
- did_dispatch_eof = False
- if frame:
- self._next_frame = frame
- trigger()
- else:
- val = val if val else (1 / 30.)
- sleep(val)
- def seek(self, percent):
- if self._ffplayer is None:
- return
- self._ffplayer.seek(percent * self._ffplayer.get_metadata()
- ['duration'], relative=False)
- self._next_frame = None
- def stop(self):
- self.unload()
- def pause(self):
- if self._ffplayer and self._state != 'paused':
- self._ffplayer.toggle_pause()
- self._state = 'paused'
- def play(self):
- if self._ffplayer and self._state == 'paused':
- self._ffplayer.toggle_pause()
- self._state = 'playing'
- return
- self.load()
- self._out_fmt = 'rgba'
- ff_opts = {
- 'paused': True,
- 'out_fmt': self._out_fmt
- }
- self._ffplayer = MediaPlayer(
- self._filename, callback=self._callback_ref,
- thread_lib='SDL',
- loglevel='info', ff_opts=ff_opts)
- self._thread = Thread(target=self._next_frame_run, name='Next frame')
- self._thread.start()
- def load(self):
- self.unload()
- def unload(self):
- Clock.unschedule(self._redraw)
- self._ffplayer_need_quit = True
- if self._thread:
- self._thread.join()
- self._thread = None
- if self._ffplayer:
- self._ffplayer = None
- self._next_frame = None
- self._size = (0, 0)
- self._state = ''
- self._ffplayer_need_quit = False