diff --git a/docs/changelog.md b/docs/changelog.md index 1f20fb3..9180ea6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,3 +7,23 @@ add script to support local .tmpl and .dspy file translation. it can build dynam * uses weakref to collect all the i18n widgets in i18n.py * show video play position +## version 0.4.5 +* add Behavior handle when build Widget by blocks +* support following behaviors +1 ButtonBehavior +2 CodeNavigationBehavior +3 CompoundSelectionBehavior +4 CoverBehavior +5 DragBehavior +6 EmacsBehavior +7 FocusBehavior +8 ToggleButtonBehavior +9 TouchRippleBehavior +10 TouchRippleButtonBehavior +11 BGColorBehavior +12 ModalBehavior +13 swipeBehavior +14 videoBehavior +* DataGrid class add checkbox support + + diff --git a/kivyblocks/register.py b/kivyblocks/register.py index fa947f2..635c3f6 100644 --- a/kivyblocks/register.py +++ b/kivyblocks/register.py @@ -46,10 +46,12 @@ from .upload import UploadFile from .pyinterpreter import PyInterpreter from .circle_progress import CircleProgress from .modalbehavior import ModalBehavior +from .videobehavior import VideoBehavior r = Factory.register # if kivy.platform in ['win','linux', 'macosx']: # r('ScreenWithMic', ScreenWithMic) +r('VideoBehavior', VideoBehavior) r('ModalBehavior', ModalBehavior) r('CircleProgress', CircleProgress) r('PyInterpreter', PyInterpreter) diff --git a/kivyblocks/version.py b/kivyblocks/version.py index 9a8e054..68eb9b6 100644 --- a/kivyblocks/version.py +++ b/kivyblocks/version.py @@ -1 +1 @@ -__version__ = '0.4.4' +__version__ = '0.4.5' diff --git a/kivyblocks/videobehavior.py b/kivyblocks/videobehavior.py new file mode 100644 index 0000000..5a96f43 --- /dev/null +++ b/kivyblocks/videobehavior.py @@ -0,0 +1,434 @@ + +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.core.window import Window +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 +from kivy.graphics import Color, Line, Rectangle +from kivyblocks.ready import WidgetReady +from kivyblocks.baseWidget import Running + +class VideoBehavior(object): + 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(-1) + position = NumericProperty(-1) + volume = NumericProperty(-1) + timeout = NumericProperty(5) + auto_play=BooleanProperty(True) + repeat=BooleanProperty(False) + in_center_focus = BooleanProperty(False) + render_to = OptionProperty('foreground', options=['background', 'foreground', 'cover']) + + def __init__(self, **kwargs): + self._player = None + self._update_task = None + self.running = None + self.vh_task = None + self.is_black = False + self.videosize = None + self.ff_opts = { + "framedrop":True + } + self.lib_opts = {} + self.headers_pattern = {} + self.set_black() + self.start_task = None + self.block_task = None + self.register_event_type('on_frame') + self.register_event_type('on_open_failed') + self.register_event_type('on_leave_focus') + self.register_event_type('on_enter_focus') + self.register_event_type('on_load_success') + self.register_event_type('on_startplay') + self.bind(size=self.set_video_size) + self.bind(parent=self.stop_when_remove) + for k, v in kwargs.items(): + setattr(self, k, v) + + def video_blocked(self, *args): + self._play_stop() + self.on_v_src(None, None) + + def on_open_failed(self, source): + print(f'{source} open failed') + + def on_load_success(self, *args): + pass + + def on_enter_focus(self, *args): + pass + + def on_leave_focus(self, *args): + pass + + def check_focus(self,*args): + if not self.parent: + self.in_center_screen = False + return + if not self.get_root_window(): + self.in_center_screen = False + + w = self.parent + pos = w.to_widget(*Window.center) + if self.collide_point(*pos): + self.in_center_screen = True + else: + self.in_center_screen = False + + 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_header(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 = ''.join([f'{k}:{v}\r\n' for k,v in headers.items()]) + return 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: + print('no _player, do nothing') + return + + Window.allow_screensaver = True + if self.status == 'play': + Window.allow_screensaver = False + self._player.set_pause(False) + self.video_handle() + elif self.status == 'pause': + self._player.set_pause(True) + elif self.status == 'stop': + self._play_stop() + else: + pass + + def stop_when_remove(self, *args): + if self.parent: + return + self._play_stop() + + def on_startplay(self, *args): + pass + + def on_frame(self, *args): + w = self.get_root_window() + if w is None: + self.status = 'stop' + print('root is None................') + + if self._player is None: + return + if not self.playing: + self.dispatch('on_startplay') + self._player.request_channel( \ + 'audio', 'open', self.audio_id) + self.seek(self.position) + self.playing = True + + def __del__(self): + self._play_stop() + + 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): + self.vsync = False + if self.play_mode == 'preview': + return + if self._player is None: + return + if self.status != 'play': + return + self._player.seek(pts, relative=False) + self.position = self._player.get_pts() + self.last_val = None + + def mute(self, flag=None): + if self.play_mode == 'preview': + return + if self._player is None: + return + if self.status != 'play': + return + x = self._player.get_mute() + print('Video(), mute=', x) + self._player.set_mute(not x) + + 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.running = Running(self) + self.status = 'stop' + self.playing = False + + 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}) + ff_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) + if self.auto_play: + self.play() + + def open_failed(self, *args): + self.dispatch('on_open_failed', self.v_src) + + def on_open_failed(self, source): + print(f'{source} open failed') + + def file_opened(self, files): + self.v_src = files[0] + + def play(self): + if self._player is None: + return + self.status = 'play' + self.start_task = Clock.schedule_once(self.open_failed, self.timeout) + + def pause(self): + if self._player is None: + return + self.status = 'pause' + + def _get_video_info(self): + if not self.playing: + self.playing = True + meta = self._player.get_metadata() + self.duration = meta['duration'] + self._out_fmt = meta['src_pix_fmt'] + self.frame_rate = meta['frame_rate'] + self.videosize = meta['src_vid_size'] + + def _play_stop(self): + if self._player: + self._player.close_player() + self._player = None + if self._update_task: + self._update_task.cancel() + self._update_task = None + if self.vh_task: + self.vh_task.cancel() + self.vh_task = None + if self.start_task: + self.start_task.cancel() + self.start_task = None + if self.block_task: + self.block_task.cancel() + self.block_task = None + self.next_frame = None + self.duration = -1 + self.position = 0 + self.videosize = None + + def set_video_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' * int(self.width * self.height * 3) + image_texture.blit_buffer(buf, colorfmt='rgb', bufferfmt='ubyte') + self.texture = image_texture + self.is_black = True + + 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 + 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.draw_texture(texture, w, h) + # 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.draw_texture(texture, w, h) + # self.texture = texture + # print('img_size=', w, h, 'window size=', self.size) + + def draw_texture(self, texture, w, h): + if self.width != w and self.height != h: + self.set_video_size() + return + canvas = self.canvas + if self.renderto == 'background': + canvas = self.canvas.before + elif self.renderto == 'cover': + canvas = self.canvas.after + if self.duration > 0: + p = self.position / self.duration * self.width + pos = (self.width - w)/2, (self.height - h)/2 + canvas.clear() + with canvas: + Rectangle(texture=texture, pos=pos, size=(w, h)) + self.canvas.after.clear() + Color(1,1,1,1) + Line(points=[0, 1, self.width, 1], width=2) + Color(1,0,0,1) + Line(points=[0,1,p,1], width=2) + + def video_handle(self, *args): + if self._update_task: + self._update_task.cancel() + self._update_task = None + if self._player is None: + return + frame, val = self._player.get_frame() + if val == 'eof': + if self.repeat: + self.seek(0) + self.play() + return + self.status = 'stop' + self.set_black() + self.last_val = None + return + if val == 'pause': + self.status = 'pause' + self.last_val = None + return + if frame is None: + self.set_black() + self.last_val = None + self.vh_task = Clock.schedule_once(self.video_handle, 0.1) + return + + self. _get_video_info() + self.last_frame = frame + self.video_ts = val + if self.last_val is None: + self.last_val = val + self._update_task = Clock.schedule_once(self.do_update, 0) + else: + t = val - self.last_val + if t > 0: + self._update_task = Clock.schedule_once(self.do_update, t) + else: + self._update_task = Clock.schedule_once(self.do_update, 0) + + def do_update(self, *args): + if self.start_task: + self.start_task.cancel() + self.start_task = None + if self.block_task: + self.block_task.cancel() + # if self.running: + # print('runnung dismiss ........') + # self.running.dismiss() + # self.running = None + self.position = self._player.get_pts() + self.volume = self._player.get_volume() + img, t = self.last_frame + if self._out_fmt == 'yuv420p': + self.show_yuv420(img) + else: + self.show_others(img) + self.dispatch('on_frame', self.last_frame) + self.last_frame = None + self.vh_task = Clock.schedule_once(self.video_handle, 0) + self.block_task = Clock.schedule_once(self.video_blocked, self.timeout) + diff --git a/test/mixin.py b/test/mixin.py new file mode 100644 index 0000000..55cffd0 --- /dev/null +++ b/test/mixin.py @@ -0,0 +1,35 @@ + +from kivyblocks import setconfig +from kivy.app import App + +from kivy.factory import Factory +from kivyblocks import register +from kivyblocks.blocks import Blocks + +class TestApp(App): + def build(self): + desc = { + "widgettype":"VBox", + "options":{ + "TouchRippleButtonBehavior":{} + }, + "subwidgets":[ + { + "widgettype":"Text", + "options":{ + "text":"Hello" + } + } + ] + } + blocks = Blocks() + x = blocks.widgetBuild(desc) + x.bind(on_press=self.haha) + print(dir(x)) + return x + + def haha(self, *args): + print('eeeeeeeeeeeeeeeeeeee') + +if __name__ == '__main__': + TestApp().run() diff --git a/test/script/scripts/boxlayout/behavior.ui b/test/script/scripts/boxlayout/behavior.ui new file mode 100644 index 0000000..0d3d977 --- /dev/null +++ b/test/script/scripts/boxlayout/behavior.ui @@ -0,0 +1,15 @@ +{ + "widgettype":"BoxLayout", + "options":{ + }, + "subwidgets":[ + { + "widgettype":"Text", + "options":{ + "otext":"this is a button text", + "i18n":true, + "TouchRippleButtonBehavior":{} + } + } + ] +} diff --git a/test/script/scripts/boxlayout/modalbehavior.ui b/test/script/scripts/boxlayout/modalbehavior.ui new file mode 100644 index 0000000..33d9ae0 --- /dev/null +++ b/test/script/scripts/boxlayout/modalbehavior.ui @@ -0,0 +1,21 @@ +{ + "widgettype":"BoxLayout", + "options":{ + "size_hint":[0.5, 0.5], + "ModalBehavior":{ + "auto_open":true, + "position":"tr", + "auto_dismiss":true + } + }, + + "subwidgets":[ + { + "widgettype":"Text", + "options":{ + "otext":"this is a Modal Test", + "i18n":true + } + } + ] +} diff --git a/test/script/scripts/boxlayout/normal.ui b/test/script/scripts/boxlayout/normal.ui new file mode 100644 index 0000000..601f08e --- /dev/null +++ b/test/script/scripts/boxlayout/normal.ui @@ -0,0 +1,13 @@ +{ + "widgettype":"BoxLayout", + "option":{}, + "subwidgets":[ + { + "widgettype":"Text", + "options":{ + "otext":"this is a BoxLayout", + "i18n":true + } + } + ] +} diff --git a/test/script/scripts/mainmenu.ui b/test/script/scripts/mainmenu.ui index 9f0b671..e5bff03 100644 --- a/test/script/scripts/mainmenu.ui +++ b/test/script/scripts/mainmenu.ui @@ -25,6 +25,11 @@ "label":"Modal Text", "url":"{{entire_url('boxlayout/modalbehavior.ui')}}" }, + { + "id":"video", + "label":"Video test", + "url":"{{entire_url('video/behavior.ui')}}" + }, { "id":"boxlayout", "label":"WBoxLayout", diff --git a/test/script/scripts/t1.ui b/test/script/scripts/t1.ui new file mode 100644 index 0000000..ea19d55 --- /dev/null +++ b/test/script/scripts/t1.ui @@ -0,0 +1,7 @@ +{ + "widgettype":"Text", + "options":{ + "text":"To Be Implements", + "ButtonBehavior":{} + } +} diff --git a/test/script/scripts/video/behavior.ui b/test/script/scripts/video/behavior.ui new file mode 100644 index 0000000..ee6cc1b --- /dev/null +++ b/test/script/scripts/video/behavior.ui @@ -0,0 +1,20 @@ +{ + "widgettype":"BoxLayout", + "options":{ + "VideoBehavior":{ + "v_src":"/home/ymq/c/songs/alone-braver.mkv", + "renderto":"background", + "auto_play":true + } + }, + "subwidgets":[ + { + "widgettype":"Text", + "options":{ + "TouchRippleButtonBehavior":{}, + "otext":"this is a button text", + "i18n":true + } + } + ] +} diff --git a/test/script/scripts/video/behavior1.ui b/test/script/scripts/video/behavior1.ui new file mode 100644 index 0000000..0d13c27 --- /dev/null +++ b/test/script/scripts/video/behavior1.ui @@ -0,0 +1,19 @@ +{ + "widgettype":"BoxLayout", + "options":{ + "VideoBehavior":{ + "v_src":"/home/ymq/c/songs/undr-sky.mkv", + "renderto":"background", + "auto_play":true + } + }, + "subwidgets":[ + { + "widgettype":"Text", + "options":{ + "otext":"this is a button text", + "i18n":true + } + } + ] +}