recorder: new input recorder class + a recorder module. Both can be used for recording touch event in a file, and replay them later.

Lot of limitations exists, but enough for simple test case or screensaver mode.
This commit is contained in:
Mathieu Virbel 2012-01-03 20:16:19 +01:00
parent 06d3a250e3
commit 7b9ee1b4db
3 changed files with 356 additions and 0 deletions

View File

@ -40,8 +40,10 @@ import kivy.modules.keybinding
import kivy.modules.monitor
import kivy.modules.touchring
import kivy.modules.inspector
import kivy.modules.recorder
import kivy.network.urlrequest
import kivy.support
import kivy.input.recorder
from kivy.factory import Factory
# force loading of all classes from factory

277
kivy/input/recorder.py Normal file
View File

@ -0,0 +1,277 @@
'''
Input recorder
==============
.. versionadded:: 1.0.10
.. warning::
This part of Kivy is still experimental, and his API is subject to change in
a future version.
This is a class that can record and replay some part of input events. This can
be used for test case, screen saver etc.
Once activated, the recorder will listen to any input event, and save some
properties in a file + the delta time. Later, you can play the input file: it
will generate fake touch with saved properties, and dispatch it to the event
loop.
By default, only the position are saved ('pos' profile and 'sx', 'sy',
attributes). Changes it only if you understand how input is working.
Recording events
----------------
The best way is to use the "recorder" module. Check the :doc:`api-kivy.modules`
documentation for learning about how to activate a module.
When activated, you can press F8 to start the recording. By default, events will
be written at `<currentpath>/recorder.kvi`. When you want to stop recording,
press F8 again.
You can replay the file by pressing F7.
Check the :doc:`api-kivy.modules.recorder` module for more information.
Manual play
-----------
You can manually open a recorder file, and play it by doing::
from kivy.input.recorder import Recorder
rec = Recorder(filename='myrecorder.kvi')
rec.play = True
If you want to loop over that file, you can do::
from kivy.input.recorder import Recorder
def recorder_loop(instance, value):
if value is False:
instance.play = True
rec = Recorder(filename='myrecorder.kvi')
rec.bind(play=recorder_loop)
rec.play = True
Recording more attributes
-------------------------
You can extend the attributes to save, at one condition: attributes values must
be simple value, not instance of complex class. Aka, saving shape will not work.
Let's say you want to save angle and pressure of the touch, if available::
from kivy.input.recorder import Recorder
rec = Recorder(filename='myrecorder.kvi',
record_attrs=['is_touch', 'sx', 'sy', 'angle', 'pressure'],
record_profile_mask=['pos', 'angle', 'pressure'])
rec.record = True
Or with modules variables::
$ python main.py -m recorder,attrs=is_touch:sx:sy:angle:pressure,profile_mask=pos:angle:pressure
Known limitations
-----------------
- Unable to save attributes with instance of complex class
- Values that represent time will be not adjusted
- Can replay only complete record, if a begin/update/end event is missing,
this could lead to ghost touch.
- Stopping the replay before the end can lead to ghost touch.
'''
__all__ = ('Recorder', )
from os.path import exists
from time import time
from kivy.event import EventDispatcher
from kivy.properties import ObjectProperty, BooleanProperty, StringProperty, \
NumericProperty, ListProperty
from kivy.input.motionevent import MotionEvent
from kivy.base import EventLoop
from kivy.logger import Logger
from ast import literal_eval
class RecorderMotionEvent(MotionEvent):
def depack(self, args):
for key, value in args.iteritems():
setattr(self, key, value)
super(RecorderMotionEvent, self).depack(args)
class Recorder(EventDispatcher):
'''Recorder class, check module documentation for more information.
'''
window = ObjectProperty(None)
'''Window instance to attach the recorder. If None set, it will use the
default one.
:data:`window` is a :class:`~kivy.properties.ObjectProperty`, default to
None.
'''
counter = NumericProperty(0)
'''Number of events recorded in the last session.
:data:`counter` is a :class:`~kivy.properties.NumericProperty`, default to
0, read-only.
'''
play = BooleanProperty(False)
'''Boolean to start/stop the replay of the current file (if exist.)
:data:`play` is a :class:`~kivy.properties.BooleanProperty`, default to
False.
'''
record = BooleanProperty(False)
'''Boolean to start/stop the recording of input events.
:data:`record` is a :class:`~kivy.properties.BooleanProperty`, default to
False.
'''
filename = StringProperty('recorder.kvi')
'''Filename to save the output of recorder.
:data:`filename` is a :class:`~kivy.properties.StringProperty`, default to
'recorder.kvi'.
'''
record_attrs = ListProperty(['is_touch', 'sx', 'sy'])
'''Attributes to record from the motion event.
:data:`record_attrs` is a :class:`~kivy.properties.ListProperty`, default to
['is_touch', 'sx', 'sy'].
'''
record_profile_mask = ListProperty(['pos'])
'''Profile to save in the fake motion event when replayed.
:data:`record_profile_mask` is a :class:`~kivy.properties.ListProperty`,
default to ['pos'].
'''
# internals
record_fd = ObjectProperty(None)
record_time = NumericProperty(0.)
def __init__(self, **kwargs):
super(Recorder, self).__init__(**kwargs)
if self.window is None:
# manually set the current window
from kivy.core.window import Window
self.window = Window
self.window.bind(on_motion=self.on_motion)
def on_motion(self, window, etype, motionevent):
if not self.record:
return
args = {}
for arg in self.record_attrs:
if hasattr(motionevent, arg):
args[arg] = getattr(motionevent, arg)
args['profile'] = [x for x in motionevent.profile if x in
self.record_profile_mask]
self.record_fd.write('%r\n' % (
(time() - self.record_time, etype, motionevent.uid, args), ))
self.counter += 1
def release(self):
self.window.unbind(on_motion=self.on_motion)
def on_record(self, instance, value):
if value:
# generate a record filename
self.counter = 0
self.record_time = time()
self.record_fd = open(self.filename, 'w')
self.record_fd.write('#RECORDER1.0\n')
Logger.info('Recorder: Recording inputs to %r' % self.filename)
else:
self.record_fd.close()
Logger.info('Recorder: Recorded %d events in %r' % (self.counter,
self.filename))
# needed for acting as an input provider
def stop(self):
pass
def start(self):
pass
def on_play(self, instance, value):
if not value:
Logger.info('Recorder: Stop playing %r' % self.filename)
EventLoop.remove_input_provider(self)
return
if not exists(self.filename):
Logger.error('Recorder: Unable to found %r file, play aborted.' % (
self.filename))
return
with open(self.filename, 'r') as fd:
data = fd.read().splitlines()
if len(data) < 2:
Logger.error('Recorder: Unable to play %r, file truncated.' % (
self.filename))
return
if data[0] != '#RECORDER1.0':
Logger.error('Recorder: Unable to play %r, invalid header.' % (
self.filename))
return
# decompile data
self.play_data = [literal_eval(x) for x in data[1:]]
self.play_time = time()
self.play_me = {}
Logger.info('Recorder: Start playing %d events from %r' %
(len(self.play_data), self.filename))
EventLoop.add_input_provider(self)
def update(self, dispatch_fn):
if len(self.play_data) == 0:
Logger.info('Recorder: Playing finished.')
self.play = False
dt = time() - self.play_time
while len(self.play_data):
event = self.play_data[0]
assert(len(event) == 4)
if event[0] > dt:
return
etype, uid, args = event[1:]
if etype == 'begin':
me = RecorderMotionEvent('recorder', uid, args)
self.play_me[uid] = me
elif etype == 'update':
me = self.play_me[uid]
me.depack(args)
elif etype == 'end':
me = self.play_me.pop(uid)
me.depack(args)
dispatch_fn(etype, me)
self.play_data.pop(0)
def start(win, ctx):
ctx.recorder = Recorder(window=win)
def stop(win, ctx):
if hasattr(ctx, 'recorder'):
ctx.recorder.release()

77
kivy/modules/recorder.py Normal file
View File

@ -0,0 +1,77 @@
'''
Recorder module
===============
.. versionadded:: 1.0.10
Create an instance of :class:`~kivy.input.recorder.Recorder`, attach to the
class, and bind some keys to record / play sequences:
- F7: read the latest recording
- F8: record input events
Configuration
-------------
:Parameters:
`attrs`: str, default to :data:`~kivy.input.recorder.Recorder.record_attrs`
value.
Attributes to record from the motion event
`profile_mask`: str, default to
:data:`~kivy.input.recorder.Recorder.record_profile_mask` value.
Mask for motion event profile. Used to filter which profile will appear
in the fake motion event when replayed.
`filename`: str, default to 'recorder.kvi'
Name of the file to record / play with
'''
from kivy.input.recorder import Recorder
from kivy.logger import Logger
from functools import partial
def on_recorder_key(recorder, window, key, *largs):
if key == 289: # F8
if recorder.play:
Logger.error('Recorder: Cannot start recording while playing.')
return
recorder.record = not recorder.record
elif key == 288: # F7
if recorder.record:
Logger.error('Recorder: Cannot start playing while recording.')
return
recorder.play = not recorder.play
def start(win, ctx):
keys = {}
# attributes
value = ctx.config.get('attrs', None)
if value is not None:
keys['record_attrs'] = value.split(':')
# profile mask
value = ctx.config.get('profile_mask', None)
if value is not None:
keys['record_profile_mask'] = value.split(':')
# filename
value = ctx.config.get('filename', None)
if value is not None:
keys['filename'] = value
ctx.recorder = Recorder(window=win, **keys)
win.bind(on_key_down=partial(on_recorder_key, ctx.recorder))
def stop(win, ctx):
if hasattr(ctx, 'recorder'):
ctx.recorder.release()