diff --git a/kivyblocks/blocks.py b/kivyblocks/blocks.py index 8c4eb6d..30513d4 100755 --- a/kivyblocks/blocks.py +++ b/kivyblocks/blocks.py @@ -431,8 +431,11 @@ class Blocks(EventDispatcher): def urlwidgetAction(self, widget:Widget, desc, *args): target = self.get_target(widget, desc) add_mode = desc.get('mode','replace') - opts = desc.get('options').copy() - p = opts.get('params',{}).copy() + opts = desc.get('options', {}).copy() + p1 = opts.get('params',{}) + p = {} + if p1: + p.update(p1) if len(args) >= 1 and isinstance(args[0],dict): p.update(args[0]) d = self.getActionData(widget, desc, *args) diff --git a/kivyblocks/ffpyplayer_video.py b/kivyblocks/ffpyplayer_video.py new file mode 100644 index 0000000..1ff9f63 --- /dev/null +++ b/kivyblocks/ffpyplayer_video.py @@ -0,0 +1,308 @@ + +import time +import numpy as np +from ffpyplayer.player import MediaPlayer +from ffpyplayer.tools import set_log_callback + +from kivy.factory import Factory +from kivy.app import App +from kivy.uix.image import Image +from kivy.uix.widget import Widget +from kivy.clock import Clock +from kivy.properties import StringProperty, BooleanProperty, \ + OptionProperty, NumericProperty +from kivy.graphics.texture import Texture + + +class FFVideo(Image): + v_src = StringProperty(None) + status = OptionProperty('stop', \ + options=['stop', 'play', 'pause']) + play_mode = OptionProperty('normal', \ + options=['normal', 'preview', 'audioonly']) + header_callback = StringProperty(None) + audio_id = NumericProperty(None) + duration = NumericProperty(None) + position = NumericProperty(None) + volume = NumericProperty(None) + framerate = NumericProperty(180) + + def __init__(self, **kwargs): + self._player = None + self._update_task = None + self._texture = None + self.is_black = False + self.videosize = None + self.timeperiod = 1.0 / self.framerate + self.ff_opts = {} + self.lib_opts = {} + self.headers_pattern = {} + super(FFVideo, self).__init__(**kwargs) + self.register_event_type('on_start_play') + + def set_ff_opt(self, k,v): + self.ff_opts.update({k:v}) + + def set_lib_opt(self, k, v): + self.lib_opts.update({k:v}) + + def set_pattern_headers(self, pattern, k,v): + dic = self.headers_pattern.get(pattern,{}) + dic.update({k:v}) + self.headers_pattern[pattern] = dic + + def _get_spec_headers(self, filename): + for p in self.headers_pattern.keys(): + if filename.startswith(p): + headers = self.headers_pattern[p] + headers_str = '\\r\\n'.join([f'{k}:{v}' for k,v in headers.items()]) + return "$'%s'" % headers_str + return None + + def on_volume(self, *args): + if self._player is None: + return + + self._player.set_volume(self.volume) + + def on_status(self, *args): + print('on_status called, ', self.status) + if self._player is None: + return + + if self.status == 'play': + self._player.set_pause(False) + elif self.status == 'pause': + self._player.set_pause(True) + elif self.status == 'stop': + self._play_stop() + else: + pass + + def on_start_play(self, *args): + if self._player is None: + return + if self.audio_id is None: + return + self._player.request_channel(self, 'audio', 'open', self.audio_id) + + def __del__(self): + if self._update_task: + self._update_task.cancel() + self._update_task = None + if self._player: + self._player.close_player() + self._player = None + + def set_volume(self, v): + if self.play_mode == 'preview': + return + if self._player is None: + return + if self.status != 'play': + return + if v > 1.0: + v = 1.0 + if v < 0.0: + v = 0.0 + self.volume = v + + def seek(self, pts): + if self.play_mode == 'preview': + return + if self._player is None: + return + if self.status != 'play': + return + self._player.seek(pts) + self.position = self._player.get_pts() + + def mute(self, flag): + if self.play_mode == 'preview': + return + if self._player is None: + return + if self.status != 'play': + return + self._player.set_mute(flag) + + def switch_audio(self): + if self.play_mode == 'preview': + return + if self._player is None: + return + if self.status != 'play': + return + self._player.request_channel('audio', action='cycle') + + def on_v_src(self, o, src): + self._play_stop() + ff_opts = { + 'pause':False + } + + if self.play_mode == 'preview': + ff_opts['lowres'] = 2 # 1/4 size + ff_opts['an'] = True + elif self.play_mode == 'audioonly': + ff_opts['vn'] = True + ff_opts.update(self.ff_opts) + + lib_opts = { + } + lib_opts.update(self.lib_opts) + heads = self._get_spec_headers(self.v_src) + if heads: + lib_opts.update({'headers':heads}) + + print('ff_opts=', ff_opts) + print('lib_opts=', lib_opts) + # self._player = MediaPlayer(self.v_src) + self._player = MediaPlayer(self.v_src, ff_opts=ff_opts, \ + lib_opts=lib_opts) + self._play_start() + + def play(self): + if self._player is None: + return + # self._player.set_pause(False) + self.status = 'play' + + def pause(self): + if self._player is None: + return + # self._player.set_pause(True) + self.status = 'pause' + + def _play_start(self): + self.timepass = 0.0 + self.last_frame = None + self.is_black = False + self.first_play = True + self._update_task = Clock.schedule_interval(self._update, self.timeperiod) + + def _get_video_info(self): + if self.first_play: + meta = self._player.get_metadata() + self.duration = meta['duration'] + self.position = 0 + self._out_fmt = meta['src_pix_fmt'] + self.frame_rate = meta['frame_rate'] + self.videosize = meta['src_vid_size'] + self.dispatch('on_start_play', self.v_src) + + def _play_stop(self): + if self._player is None: + return + self._update_task.cancel() + self._update_task = None + self._player.close_player() + self._player = None + self.timepass = 0 + self.next_frame = None + self.duration = None + self.position = None + self.frame_rte = None + self.videosize = None + + def on_size(self, *args): + if self._player is None: + return + if self.videosize == None: + return + w, h = self.videosize + r = w / h + r1 = self.width / self.height + if r1 >= r: + self._player.set_size(-1, self.height) + else: + self._player.set_size(self.width, -1) + + def set_black(self): + if self.is_black: + return + image_texture = Texture.create( + size=self.size, colorfmt='rgb') + buf = b'\x00' * self.width * self.height * 3 + image_texture.blit_buffer(buf, colorfmt='rgb', bufferfmt='ubyte') + self.texture = image_texture + self.is_black = True + print('set black ..........................') + + def show_yuv420(self, img): + w, h = img.get_size() + 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=(w, h)) + 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 + dy, du, dv, _ = img.to_memoryview() + if dy and du and dv: + self._tex_y.blit_buffer(dy, colorfmt='luminance') + self._tex_u.blit_buffer(du, colorfmt='luminance') + self._tex_v.blit_buffer(dv, colorfmt='luminance') + self._fbo.ask_update() + self._fbo.draw() + texture.flip_vertical() + self.texture = texture + + def show_others(self, img): + w, h = img.get_size() + texture = Texture.create(size=(w, h), colorfmt='rgb') + texture.blit_buffer( + img.to_memoryview()[0], colorfmt='rgb', + bufferfmt='ubyte') + texture.flip_vertical() + self.texture = texture + # print('img_size=', w, h, 'window size=', self.size) + + def _update(self, dt): + if self.last_frame is None: + frame, val = self._player.get_frame() + if val == 'eof': + print('*****EOF******') + self.status = 'stop' + self.set_black() + return + if val == 'pause': + self.status = 'pause' + return + if frame is None: + # print('video null', time.time()) + self.set_black() + return + self.last_frame = frame + self.video_ts = val + self._get_video_info() + + self.timepass += self.timeperiod + self.position = self._player.get_pts() + if self.timepass < self.video_ts: + return + + if self.timepass > self.video_ts +0.2: + self.last_frame = None + return + + self.status = 'play' + img, t = self.last_frame + if self._out_fmt == 'yuv420p': + self.show_yuv420(img) + else: + self.show_others(img) + self.last_frame = None + +Factory.register('FFVideo', FFVideo) diff --git a/kivyblocks/hack_ffpyplayer.py b/kivyblocks/hack_ffpyplayer.py index eaefb0e..705d968 100644 --- a/kivyblocks/hack_ffpyplayer.py +++ b/kivyblocks/hack_ffpyplayer.py @@ -19,9 +19,9 @@ def MediaPlayer(filename, *args, ff_opts={}, lib_opts={}, **kw): lib_opts.update(external_lib_opts) if filename.startswith('http://') or \ filename.startswith('https://'): - headers = get_spec_headers(filename) - if headers is not None: - lib_opts['headers'] = "$'%s'" % headers + headers_str = get_spec_headers(filename) + if headers_str is not None: + lib_opts['headers'] = headers_str return OldMediaPlayer(filename, *args, ff_opts=ff_opts, lib_opts=lib_opts, **kw) @@ -38,5 +38,7 @@ def set_headers_pattern(pattern, headers_str): def get_spec_headers(filename): for p in headers_pattern.keys(): if p in filename: - return headers_pattern[p] + headers = headers_pattern[p] + headers_str = ';'.join([f'{k}={v}' for k,v in headers.items()]) + return "$'%s'" % headers_str return None