mirror of https://github.com/kivy/kivy.git
videoplayer: add annotation support
This commit is contained in:
parent
18f4882c78
commit
0e4c300d63
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
|
@ -0,0 +1,9 @@
|
|||
[
|
||||
{"start": 0, "duration": 2, "text": "This is an example of annotation"},
|
||||
{"start": 2, "duration": 2, "text": "You can change the background color", "bgcolor": [0.5, 0.2, 0.4, 0.5]},
|
||||
{"start": 4, "duration": 2, "text": "Or the font size", "bgcolor": [0.5, 0.2, 0.4, 0.5], "font_size": 24},
|
||||
{"start": 6, "duration": 2, "text": "Or the bold...\nAnd multiline", "bgcolor": [0.5, 0.2, 0.4, 0.5], "bold": 1},
|
||||
{"start": 8, "duration": 2, "text": "Or even\nchange the alignement!", "bgcolor": [0.5, 0.2, 0.4, 0.5], "halign": "center"},
|
||||
{"start": 10, "duration": 2, "text": "Position hint are supported too", "bgcolor": [0.5, 0.2, 0.4, 0.5], "pos_hint": {"top": 0.95, "center_x": 0.5}},
|
||||
{"start": 12, "duration": 2, "text": "[b]Text[/b] [i]Markup[/i] too.", "bgcolor": [0.5, 0.2, 0.4, 0.5], "markup": 1}
|
||||
]
|
Binary file not shown.
After Width: | Height: | Size: 249 KiB |
|
@ -0,0 +1,321 @@
|
|||
'''
|
||||
Video player
|
||||
============
|
||||
|
||||
.. versionadded:: 1.1.2
|
||||
|
||||
.. image:: images/videoplayer.jpg
|
||||
:align: right
|
||||
|
||||
The video player widget can be used to play video and let the user control the
|
||||
play/pause, volume and seek. The widget cannot be customized a lot, due to the
|
||||
complex assembly of lot of base widgets.
|
||||
'''
|
||||
|
||||
__all__ = ('VideoPlayer', )
|
||||
|
||||
from kivy.properties import ObjectProperty, StringProperty, BooleanProperty, \
|
||||
NumericProperty
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
from kivy.uix.progressbar import ProgressBar
|
||||
from kivy.uix.video import Video
|
||||
from kivy.uix.video import Image
|
||||
from kivy.factory import Factory
|
||||
|
||||
|
||||
class VideoPlayerVolume(Image):
|
||||
video = ObjectProperty(None)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if not self.collide_point(*touch.pos):
|
||||
return False
|
||||
touch.grab(self)
|
||||
# save the current volume and delta to it
|
||||
touch.ud[self.uid] = [self.video.volume, 0]
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return
|
||||
# calculate delta
|
||||
dy = abs(touch.y - touch.oy)
|
||||
if dy > 10:
|
||||
dy = min(dy - 10, 100)
|
||||
touch.ud[self.uid][1] = dy
|
||||
self.video.volume = dy / 100.
|
||||
return True
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return
|
||||
touch.ungrab(self)
|
||||
dy = abs(touch.y - touch.oy)
|
||||
if dy < 10:
|
||||
if self.video.volume > 0:
|
||||
self.video.volume = 0
|
||||
else:
|
||||
self.video.volume = 1.
|
||||
|
||||
|
||||
class VideoPlayerPlayPause(Image):
|
||||
video = ObjectProperty(None)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if self.collide_point(*touch.pos):
|
||||
self.video.play = not self.video.play
|
||||
return True
|
||||
|
||||
|
||||
class VideoPlayerProgressBar(ProgressBar):
|
||||
video = ObjectProperty(None)
|
||||
seek = NumericProperty(None, allownone=True)
|
||||
alpha = NumericProperty(1.)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(VideoPlayerProgressBar, self).__init__(**kwargs)
|
||||
self.bubble = Factory.Bubble(size=(50, 44))
|
||||
self.bubble_label = Factory.Label(text='0:00')
|
||||
self.bubble.add_widget(self.bubble_label)
|
||||
self.add_widget(self.bubble)
|
||||
self.bind(pos=self._update_bubble,
|
||||
size=self._update_bubble,
|
||||
seek=self._update_bubble)
|
||||
|
||||
def on_video(self, instance, value):
|
||||
self.video.bind(position=self._update_bubble, play=self._showhide_bubble)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if not self.collide_point(*touch.pos):
|
||||
return
|
||||
self._show_bubble()
|
||||
touch.grab(self)
|
||||
touch.ud[self.uid] = self._update_seek(touch.x)
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return
|
||||
touch.ud[self.uid] = self._update_seek(touch.x)
|
||||
return True
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is not self:
|
||||
return
|
||||
touch.ungrab(self)
|
||||
self.video.seek(self.seek)
|
||||
self.seek = None
|
||||
self._hide_bubble()
|
||||
return True
|
||||
|
||||
def _update_seek(self, x):
|
||||
if self.width == 0:
|
||||
return
|
||||
x = max(self.x, min(self.right, x)) - self.x
|
||||
self.seek = x / float(self.width)
|
||||
|
||||
def _show_bubble(self):
|
||||
self.alpha = 1
|
||||
Animation.stop_all(self, 'alpha')
|
||||
|
||||
def _hide_bubble(self):
|
||||
self.alpha = 1.
|
||||
Animation(alpha=0, d=4, t='in_out_expo').start(self)
|
||||
|
||||
def on_alpha(self, instance, value):
|
||||
self.bubble.background_color = (1, 1, 1, value)
|
||||
self.bubble_label.color = (1, 1, 1, value)
|
||||
|
||||
def _update_bubble(self, *l):
|
||||
seek = self.seek
|
||||
if self.seek is None:
|
||||
if self.video.duration == 0:
|
||||
seek = 0
|
||||
else:
|
||||
seek = self.video.position / self.video.duration
|
||||
# convert to minutes:seconds
|
||||
d = self.video.duration * seek
|
||||
minutes = int(d / 60)
|
||||
seconds = int(d - (minutes * 60))
|
||||
# fix bubble label & position
|
||||
self.bubble_label.text = '%d:%02d' % (minutes, seconds)
|
||||
self.bubble.center_x = self.x + seek * self.width
|
||||
self.bubble.y = self.top
|
||||
|
||||
def _showhide_bubble(self, instance, value):
|
||||
if value:
|
||||
self._hide_bubble()
|
||||
else:
|
||||
self._show_bubble()
|
||||
|
||||
|
||||
class VideoPlayerPreview(FloatLayout):
|
||||
source = ObjectProperty(None)
|
||||
video = ObjectProperty(None)
|
||||
click_done = BooleanProperty(False)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if self.collide_point(*touch.pos) and not self.click_done:
|
||||
self.click_done = True
|
||||
self.video.play = True
|
||||
return True
|
||||
|
||||
|
||||
class VideoPlayer(GridLayout):
|
||||
'''VideoPlayer class, see module documentation for more information.
|
||||
'''
|
||||
|
||||
source = StringProperty(None)
|
||||
'''Source of the video to read
|
||||
|
||||
:data:`source` a :class:`~kivy.properties.StringProperty`, default to None.
|
||||
'''
|
||||
|
||||
thumbnail = StringProperty(None)
|
||||
'''Thumbnail of the video to show. If None, it will try to search the thumbnail from the :data:`source` + .png.
|
||||
|
||||
:data:`thumbnail` a :class:`~kivy.properties.StringProperty`, default to None.
|
||||
'''
|
||||
|
||||
duration = NumericProperty(-1)
|
||||
'''Duration of the video. The duration is default to -1, and set to real
|
||||
duration when the video is loaded.
|
||||
|
||||
:data:`duration` is a :class:`~kivy.properties.NumericProperty`, default to
|
||||
-1.
|
||||
'''
|
||||
|
||||
position = NumericProperty(0)
|
||||
'''Position of the video between 0 and :data:`duration`. The position is
|
||||
default to -1, and set to real position when the video is loaded.
|
||||
|
||||
:data:`position` is a :class:`~kivy.properties.NumericProperty`, default to
|
||||
-1.
|
||||
'''
|
||||
|
||||
volume = NumericProperty(1.0)
|
||||
'''Volume of the video, in the range 0-1. 1 mean full volume, 0 mean mute.
|
||||
|
||||
:data:`volume` is a :class:`~kivy.properties.NumericProperty`, default to
|
||||
1.
|
||||
'''
|
||||
|
||||
play = BooleanProperty(False)
|
||||
'''Boolean, indicates if the video is playing.
|
||||
You can start/stop the video by setting this property. ::
|
||||
|
||||
# start playing the video at creation
|
||||
video = VideoPlayer(source='movie.mkv', play=True)
|
||||
|
||||
# create the video, and start later
|
||||
video = VideoPlayer(source='movie.mkv')
|
||||
# and later
|
||||
video.play = True
|
||||
|
||||
:data:`play` is a :class:`~kivy.properties.BooleanProperty`, default to
|
||||
False.
|
||||
'''
|
||||
|
||||
image_overlay_play = StringProperty('atlas://data/images/defaulttheme/player-play-overlay')
|
||||
'''Image filename used to show an "play" overlay when the video is not yet started.
|
||||
|
||||
:data:`image_overlay_play` a :class:`~kivy.properties.StringProperty`
|
||||
'''
|
||||
|
||||
image_loading = StringProperty('data/images/image-loading.gif')
|
||||
'''Image filename used when the video is loading.
|
||||
|
||||
:data:`image_loading` a :class:`~kivy.properties.StringProperty`
|
||||
'''
|
||||
|
||||
image_play = StringProperty('atlas://data/images/defaulttheme/media-playback-start')
|
||||
'''Image filename used for the "Play" button.
|
||||
|
||||
:data:`image_loading` a :class:`~kivy.properties.StringProperty`
|
||||
'''
|
||||
|
||||
image_pause = StringProperty('atlas://data/images/defaulttheme/media-playback-pause')
|
||||
'''Image filename used for the "Pause" button.
|
||||
|
||||
:data:`image_pause` a :class:`~kivy.properties.StringProperty`
|
||||
'''
|
||||
|
||||
image_volumehigh = StringProperty('atlas://data/images/defaulttheme/audio-volume-high')
|
||||
'''Image filename used for the volume icon, when the volume is high.
|
||||
|
||||
:data:`image_volumehigh` a :class:`~kivy.properties.StringProperty`
|
||||
'''
|
||||
|
||||
image_volumemedium = StringProperty('atlas://data/images/defaulttheme/audio-volume-medium')
|
||||
'''Image filename used for the volume icon, when the volume is medium.
|
||||
|
||||
:data:`image_volumemedium` a :class:`~kivy.properties.StringProperty`
|
||||
'''
|
||||
|
||||
image_volumelow = StringProperty('atlas://data/images/defaulttheme/audio-volume-low')
|
||||
'''Image filename used for the volume icon, when the volume is low.
|
||||
|
||||
:data:`image_volumelow` a :class:`~kivy.properties.StringProperty`
|
||||
'''
|
||||
|
||||
image_volumemuted = StringProperty('atlas://data/images/defaulttheme/audio-volume-muted')
|
||||
'''Image filename used for the volume icon, when the volume is muted.
|
||||
|
||||
:data:`image_volumemuted` a :class:`~kivy.properties.StringProperty`
|
||||
'''
|
||||
|
||||
# internals
|
||||
container = ObjectProperty(None)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._video = None
|
||||
self._image = None
|
||||
super(VideoPlayer, self).__init__(**kwargs)
|
||||
self._load_thumbnail()
|
||||
|
||||
def on_source(self, instance, value):
|
||||
# we got a value, try to see if we have an image for it
|
||||
self._load_thumbnail()
|
||||
|
||||
def _load_thumbnail(self):
|
||||
if not self.container:
|
||||
return
|
||||
self.container.clear_widgets()
|
||||
# get the source, remove extension, and use png
|
||||
thumbnail = self.thumbnail
|
||||
if thumbnail is None:
|
||||
filename = self.source.rsplit('.', 1)
|
||||
thumbnail = filename[0] + '.png'
|
||||
self._image = VideoPlayerPreview(source=thumbnail, video=self)
|
||||
self.container.add_widget(self._image)
|
||||
|
||||
def on_play(self, instance, value):
|
||||
if self._video is None:
|
||||
self._video = Video(source=self.source, play=True,
|
||||
volume=self.volume)
|
||||
self._video.bind(texture=self._play_started,
|
||||
duration=self.setter('duration'),
|
||||
position=self.setter('position'),
|
||||
volume=self.setter('volume'))
|
||||
self._video.play = value
|
||||
|
||||
def on_volume(self, instance, value):
|
||||
if not self._video:
|
||||
return
|
||||
self._video.volume = value
|
||||
|
||||
def seek(self, percent):
|
||||
if not self._video:
|
||||
return
|
||||
self._video.seek(percent)
|
||||
|
||||
def _play_started(self, instance, value):
|
||||
self.container.clear_widgets()
|
||||
self.container.add_widget(self._video)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
from kivy.base import runTouchApp
|
||||
runTouchApp(VideoPlayer(source=sys.argv[1]))
|
|
@ -38,9 +38,9 @@
|
|||
pos: self.pos
|
||||
|
||||
<BubbleButton>:
|
||||
background_normal: 'atlas://data/images/defaulttheme/bubble_btn'
|
||||
background_down: 'atlas://data/images/defaulttheme/bubble_btn_pressed'
|
||||
border: (0, 0, 0, 0)
|
||||
background_normal: 'atlas://data/images/defaulttheme/bubble_btn'
|
||||
background_down: 'atlas://data/images/defaulttheme/bubble_btn_pressed'
|
||||
border: (0, 0, 0, 0)
|
||||
|
||||
<Slider>:
|
||||
canvas:
|
||||
|
@ -108,17 +108,17 @@
|
|||
rgb: (0, 0, 0)
|
||||
|
||||
<TextInputCutCopyPaste>:
|
||||
size_hint: None, None
|
||||
size: 150, 50
|
||||
BubbleButton:
|
||||
text: 'Cut'
|
||||
on_release: root.do('cut')
|
||||
BubbleButton:
|
||||
text: 'Copy'
|
||||
on_release: root.do('copy')
|
||||
BubbleButton:
|
||||
text: 'Paste'
|
||||
on_release: root.do('paste')
|
||||
size_hint: None, None
|
||||
size: 150, 50
|
||||
BubbleButton:
|
||||
text: 'Cut'
|
||||
on_release: root.do('cut')
|
||||
BubbleButton:
|
||||
text: 'Copy'
|
||||
on_release: root.do('copy')
|
||||
BubbleButton:
|
||||
text: 'Paste'
|
||||
on_release: root.do('paste')
|
||||
|
||||
|
||||
<TreeViewNode>:
|
||||
|
@ -588,17 +588,17 @@
|
|||
width: sv.width
|
||||
|
||||
<ScrollView>:
|
||||
canvas.after:
|
||||
Color:
|
||||
rgba: self.bar_color[:3] + [self.bar_color[3] * self.bar_alpha if self.do_scroll_y else 0]
|
||||
Rectangle:
|
||||
pos: self.right - self.bar_width - self.bar_margin, self.y + self.height * self.vbar[0]
|
||||
size: self.bar_width, self.height * self.vbar[1]
|
||||
Color:
|
||||
rgba: self.bar_color[:3] + [self.bar_color[3] * self.bar_alpha if self.do_scroll_x else 0]
|
||||
Rectangle:
|
||||
pos: self.x + self.width * self.hbar[0], self.y + self.bar_margin
|
||||
size: self.width * self.hbar[1], self.bar_width
|
||||
canvas.after:
|
||||
Color:
|
||||
rgba: self.bar_color[:3] + [self.bar_color[3] * self.bar_alpha if self.do_scroll_y else 0]
|
||||
Rectangle:
|
||||
pos: self.right - self.bar_width - self.bar_margin, self.y + self.height * self.vbar[0]
|
||||
size: self.bar_width, self.height * self.vbar[1]
|
||||
Color:
|
||||
rgba: self.bar_color[:3] + [self.bar_color[3] * self.bar_alpha if self.do_scroll_x else 0]
|
||||
Rectangle:
|
||||
pos: self.x + self.width * self.hbar[0], self.y + self.bar_margin
|
||||
size: self.width * self.hbar[1], self.bar_width
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
@ -606,6 +606,7 @@
|
|||
# =============================================================================
|
||||
|
||||
<VideoPlayerPreview>:
|
||||
pos_hint: {'x': 0, 'y': 0}
|
||||
Image:
|
||||
source: root.source
|
||||
color: (.5, .5, .5, 1)
|
||||
|
@ -615,11 +616,24 @@
|
|||
source: 'atlas://data/images/defaulttheme/player-play-overlay' if not root.click_done else 'data/images/image-loading.gif'
|
||||
pos_hint: {'x': 0, 'y': 0}
|
||||
|
||||
<VideoPlayerAnnotation>:
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: self.annotation['bgcolor'] if 'bgcolor' in self.annotation else (0, 0, 0, 0.8)
|
||||
BorderImage:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
source: self.annotation['bgsource'] if 'bgsource' in self.annotation else None
|
||||
border: self.annotation['border'] if 'border' in self.annotation else (0, 0, 0, 0)
|
||||
size_hint: self.annotation['size_hint'] if 'size_hint' in self.annotation else (None, None)
|
||||
size: self.annotation['size'] if 'size' in self.annotation else (self.texture_size[0] + 20, self.texture_size[1] + 20)
|
||||
pos_hint: self.annotation['pos_hint'] if 'pos_hint' in self.annotation else {'center_x': .5, 'y': .05}
|
||||
|
||||
<VideoPlayer>:
|
||||
container: container
|
||||
cols: 1
|
||||
|
||||
GridLayout:
|
||||
FloatLayout:
|
||||
cols: 1
|
||||
id: container
|
||||
|
||||
|
|
|
@ -11,16 +11,48 @@ complex assembly of lot of base widgets.
|
|||
.. image:: images/videoplayer.jpg
|
||||
:align: center
|
||||
|
||||
Annotations
|
||||
-----------
|
||||
|
||||
If you want to display some texts at a specific time, duration, you might want
|
||||
to look at annotations. An annotation file have a ".jsa" extension. The player
|
||||
will automatically load the associated annotation file if exists.
|
||||
|
||||
It's a JSON based file, that provide a list of label dictionnary. The key and
|
||||
value must match one of the :class:`VideoPlayerAnnotation`. For example, here is
|
||||
a short version of a jsa that you can found in `examples/widgets/softboy.jsa`::
|
||||
|
||||
|
||||
[
|
||||
{"start": 0, "duration": 2,
|
||||
"text": "This is an example of annotation"},
|
||||
{"start": 2, "duration": 2,
|
||||
"bgcolor": [0.5, 0.2, 0.4, 0.5],
|
||||
"text": "You can change the background color"}
|
||||
]
|
||||
|
||||
On our softboy.avi example, it will look like this:
|
||||
|
||||
.. image:: images/videoplayer-annotation.jpg
|
||||
:align: center
|
||||
|
||||
If you want to test how annotations file are working, test with::
|
||||
|
||||
python -m kivy.uix.videoplayer examples/widgets/softboy.avi
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('VideoPlayer', )
|
||||
__all__ = ('VideoPlayer', 'VideoPlayerAnnotation')
|
||||
|
||||
from json import load
|
||||
from os.path import exists
|
||||
from kivy.properties import ObjectProperty, StringProperty, BooleanProperty, \
|
||||
NumericProperty
|
||||
NumericProperty, DictProperty
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
from kivy.uix.progressbar import ProgressBar
|
||||
from kivy.uix.label import Label
|
||||
from kivy.uix.video import Video
|
||||
from kivy.uix.video import Image
|
||||
from kivy.factory import Factory
|
||||
|
@ -163,6 +195,37 @@ class VideoPlayerPreview(FloatLayout):
|
|||
return True
|
||||
|
||||
|
||||
class VideoPlayerAnnotation(Label):
|
||||
'''Annotation class used for creating annotation labels.
|
||||
|
||||
Additionnals key are available:
|
||||
|
||||
* bgcolor: [r, g, b, a] - background color of the text box
|
||||
* bgsource: 'filename' - background image used for background text box
|
||||
* border: (n, e, s, w) - border used for background image
|
||||
|
||||
'''
|
||||
start = NumericProperty(0)
|
||||
'''Start time of the annotation.
|
||||
|
||||
:data:`start` is a :class:`~kivy.properties.NumericProperty`, default to
|
||||
0
|
||||
'''
|
||||
|
||||
duration = NumericProperty(1)
|
||||
'''Duration of the annotation
|
||||
|
||||
:data:`duration` is a :class:`~kivy.properties.NumericProperty`, default to
|
||||
1
|
||||
'''
|
||||
|
||||
annotation = DictProperty({})
|
||||
|
||||
def on_annotation(self, instance, ann):
|
||||
for key, value in ann.iteritems():
|
||||
setattr(self, key, value)
|
||||
|
||||
|
||||
class VideoPlayer(GridLayout):
|
||||
'''VideoPlayer class, see module documentation for more information.
|
||||
'''
|
||||
|
@ -266,18 +329,26 @@ class VideoPlayer(GridLayout):
|
|||
:data:`image_volumemuted` a :class:`~kivy.properties.StringProperty`
|
||||
'''
|
||||
|
||||
annotations = StringProperty(None)
|
||||
'''If set, it will be used for reading annotations box.
|
||||
'''
|
||||
|
||||
# internals
|
||||
container = ObjectProperty(None)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._video = None
|
||||
self._image = None
|
||||
self._annotations = None
|
||||
self._annotations_labels = []
|
||||
super(VideoPlayer, self).__init__(**kwargs)
|
||||
self._load_thumbnail()
|
||||
self._load_annotations()
|
||||
|
||||
def on_source(self, instance, value):
|
||||
# we got a value, try to see if we have an image for it
|
||||
self._load_thumbnail()
|
||||
self._load_annotations()
|
||||
|
||||
def _load_thumbnail(self):
|
||||
if not self.container:
|
||||
|
@ -291,10 +362,24 @@ class VideoPlayer(GridLayout):
|
|||
self._image = VideoPlayerPreview(source=thumbnail, video=self)
|
||||
self.container.add_widget(self._image)
|
||||
|
||||
def _load_annotations(self):
|
||||
if not self.container:
|
||||
return
|
||||
self._annotations_labels = []
|
||||
annotations = self.annotations
|
||||
if annotations is None:
|
||||
filename = self.source.rsplit('.', 1)
|
||||
annotations = filename[0] + '.jsa'
|
||||
if exists(annotations):
|
||||
with open(annotations, 'r') as fd:
|
||||
self._annotations = load(fd)
|
||||
for ann in self._annotations:
|
||||
self._annotations_labels.append(VideoPlayerAnnotation(annotation=ann))
|
||||
|
||||
def on_play(self, instance, value):
|
||||
if self._video is None:
|
||||
self._video = Video(source=self.source, play=True,
|
||||
volume=self.volume)
|
||||
volume=self.volume, pos_hint={'x': 0, 'y': 0})
|
||||
self._video.bind(texture=self._play_started,
|
||||
duration=self.setter('duration'),
|
||||
position=self.setter('position'),
|
||||
|
@ -306,6 +391,19 @@ class VideoPlayer(GridLayout):
|
|||
return
|
||||
self._video.volume = value
|
||||
|
||||
def on_position(self, instance, value):
|
||||
labels = self._annotations_labels
|
||||
if not labels:
|
||||
return
|
||||
for label in labels:
|
||||
start = label.start
|
||||
duration = label.duration
|
||||
if start > value or (start + duration) < value:
|
||||
if label.parent:
|
||||
label.parent.remove_widget(label)
|
||||
elif label.parent is None:
|
||||
self.container.add_widget(label)
|
||||
|
||||
def seek(self, percent):
|
||||
'''Change the position to a percentage of duration. Percentage must be a
|
||||
value between 0-1.
|
||||
|
|
Loading…
Reference in New Issue