Asynchronous file loading...

1,352 views
Skip to first unread message

Ryan

unread,
Aug 6, 2012, 11:56:29 PM8/6/12
to kivy-...@googlegroups.com
Hi all,

I'm writing an application that involves loading content (images/videos/etc) from a networked file share.
The problem I'm running into is that file loading is not done asynchronously, and therefore, whenever I try to load content from the networked share, the application locks up and blocks user input until the loading is done (to be clear, the input gets queued ... so it's processed properly, but delayed until the loading is completed).

I've thought of using threads to accomplish this, but because threads in python go through the GIL, I believe that it wouldn't solve my problem.
I've also noticed the AsyncImage class in Kivy, but I don't think this will solve my problem either (it might solve my problem for Images only, but even still, the AsyncImage class only works for sources that are valid URIs, ie: http).

I've also found the Loader class (though I haven't tried using it yet), but I'm concerned that I'll have to do some pretty messy hacks (either to my own code, or Kivy itself) in order to get Images and Videos loading asynchronously in a neat/compact way...

Thoughts?


Thanks,
Ryan

Akshay Arora

unread,
Aug 7, 2012, 2:24:37 AM8/7/12
to kivy-...@googlegroups.com
Hello, Ryan,

    You've told us what you are doing but not how? Without knowing "how", it's kind of difficult to ascertain why/what is the reason for the network request blocking the kivy thread.

For blocking Operation threads would work but you must take care to perform any GUI operations in the main Kivy thread only or you will get a PYGAME Parachute.
A easy way is to defer GUI ops to the main thread by doing them in functions called by Clock.schedule_...

Why not use the UrlRequest package that is designed specifically for this purpose? You can use the UrlRequest to make asynchronous request on the web, and get the result
 when the request is completed. The spirit is the same as XHR object in Javascript.

Regards

--
 
 
 

Gabriel Pettier

unread,
Aug 7, 2012, 2:45:32 AM8/7/12
to kivy-...@googlegroups.com

Not sure urlRequest will work for samba share. But yeah, in any case, a correct use of threal would avoid blocking the interface.

--
 
 
 

Ryan

unread,
Aug 7, 2012, 8:41:32 PM8/7/12
to kivy-...@googlegroups.com
As an example, I'm using a modified Picture widget (as found in the Pictures example at ...\kivy\kivy\examples\demo\pictures\ ), however whenever I create the Picture widget after the canvas has been created (ie: via the click of a button/some other event), the main thread gets blocked loading the file from the network share.
I suppose I could spawn another thread to handle loading of files from the networked (samba) file share to prevent the main thread from any I/O blocking, and then have the main thread open the same image (which would ideally incur minimal blocking because the texture would already be in Kivy's cache), but this seems like a poor solution.
The reason I feel like I would need to do the above, is because setting the "source" property in the Image widget (or AsyncImage widget) automatically causes the texture to be loaded, and blocks the main OpenGL thread.

Ryan

unread,
Aug 8, 2012, 12:25:59 AM8/8/12
to kivy-...@googlegroups.com
Is there a way to make the kivy.loader.Loader class call _load_local() in an asynchronous way?
Basically, is there a way to spawn a worker thread to load the texture memory in the Kivy cache, and return when it's done (and in the process freeing the main Kivy thread to perform updates during the load from disk)?
Message has been deleted

Ryan

unread,
Aug 8, 2012, 1:12:31 AM8/8/12
to kivy-...@googlegroups.com
Here's an example of what I'm trying to do. Simply replace the contents of main.py in the Pictures demo (...\kivy\examples\demo\pictures).
However, it doesn't seem to work. The texture data ends up being lost (or at least not being displayed properly). From my perspective, I'm simply loading the texture into an Image object (which should save the data into the Kivy cache), and then calling back into the Pictures class to set the source property.

Any ideas why it doesn't work?

'''
Pictures demo
=============


This is a basic picture viewer, using the scatter widget.
'''



import kivy
kivy
.require('1.0.6')


from glob import glob
from random import randint
from os.path import join, dirname
from kivy.app import App
from kivy.logger import Logger
from kivy.uix.scatter import Scatter
from kivy.properties import StringProperty
# FIXME this shouldn't be necessary
from kivy.core.window import Window




#START CODE I MODIFIED
import threading


from kivy.core.image import Image


class ThreadedLoader(threading.Thread):
    source
= ""
    callback
= None
   
   
def __init__(self, source, callback):
        threading
.Thread.__init__ ( self )
       
       
self.source = source
       
self.callback = callback
       
   
def run(self):
       
Image.load(self.source)
       
self.callback()




class Picture(Scatter):
   
'''Picture is the class that will show the image with a white border and a
    shadow. They are nothing here because almost everything is inside the
    picture.kv. Check the rule named <Picture> inside the file, and you'
ll see
    how the
Picture() is really constructed and used.


   
The source property will be the filename to show.
   
'''


    source = StringProperty(None)
   
    def __init__(self, **kwargs):
        #Pop source property to prevent normal image loading.
        self.cached_source = kwargs.pop("source", "")
       
        super(Picture, self).__init__(**kwargs)
       
        #Instruct our threaded loader to load the content
        ThreadedLoader(self.cached_source, self.loaded).start()
       
    def loaded(self):
        #When receiving the callback, update the source property,
        # and ideally Kivy'
s normal codepath should do the rest.
       
self.source = self.cached_source


#END CODE I MODIFIED


class PicturesApp(App):


   
def build(self):


       
# the root is created in pictures.kv
        root
= self.root


       
# get any files into images directory
        curdir
= dirname(__file__)
       
for filename in glob(join(curdir, 'images', '*')):
           
try:
               
# load the image
                picture
= Picture(source=filename, rotation=randint(-30,30))
               
# add to the main field
                root
.add_widget(picture)
           
except Exception, e:
               
Logger.exception('Pictures: Unable to load <%s>' % filename)


   
def on_pause(self):
       
return True




if __name__ in ('__main__', '__android__'):
   
PicturesApp().run()

Akshay Arora

unread,
Aug 10, 2012, 6:03:08 PM8/10/12
to kivy-...@googlegroups.com
Image.load() doesn't just cache the image it also creates the textures from the data of the image, which is a OGL operation. So use clock.shcedule_once(...) .

--
 
 
 

Ryan

unread,
Aug 12, 2012, 10:52:33 PM8/12/12
to kivy-...@googlegroups.com
I'm sorry ... I don't follow you completely.
From my previous code sample, are you suggesting I replace the ThreadedLoader class with a call to Clock.schedule_once(...)?
If I do that, won't the loading be done on the main GUI thread (and therefore block UI updates until the load is done)?
Or are you suggesting something else?

Keep in mind, that I'm trying to load files into Kivy without incurring any hits on the render updates of the main Kivy thread (this is to maintain responsiveness).


Thanks for any updates,
Ryan

Akshay Arora

unread,
Aug 13, 2012, 10:01:08 PM8/13/12
to kivy-...@googlegroups.com
What I meant was to call Image.load() through Clock.schedule_once(). I'm assuming that you have completed the blocking operation before that in your thread.

Basically What I'm suggesting is to access the network share in the Thread save them files locally or in temp files and load these files from the main thread afterwords. That's what the loader does for http I think.

Something like this:: 
you will need to have pysmb to test this code. I assumed here that smb shares were being accessed, if not adjust accordingly)
replace main.py in pictures demo with this, no changes to any other file. Adjust user name/password/path for your setup.


'''
Pictures demo
=============

This is a basic picture viewer, using the scatter widget.
'''
from random import randint
from kivy.app import App

from kivy.uix.scatter import Scatter
from kivy.properties import StringProperty
from kivy.clock import Clock
from kivy.uix.videoplayer import VideoPlayer
from kivy.logger import Logger
from smb.SMBConnection import SMBConnection

userID = 'xxxxxxxx'
password = 'xxxxxxxx'
client_machine_name = ''
server_name = ''
server_ip = '192.168.11.99'
service_name = 'code'  # share name
vid_service_name = 'meditation' # share name
path = 'multitouch/MT__code/akfork/kivy/examples/demo/pictures/images'
vid_file = 'Meditation/Deep Meditation Experience.flv'

import threading
import tempfile

class M_Loader(threading.Thread):
    def __init__(self, **kwargs):
        super(M_Loader, self).__init__(**kwargs)

    def load_vid(self, fil, pic):
        conn = SMBConnection(userID, password, client_machine_name,
            server_name, use_ntlm_v2 = True)
        assert conn.connect(server_ip, 139)
        suffix = '.%s' % (fil.split('.')[-1])
        file_obj = tempfile.NamedTemporaryFile(prefix='loader', suffix=suffix, delete = False)
        servn = 'media'
        pth = fil
        file_attributes, filesize = conn.retrieveFile(servn, pth, file_obj)
        pic.vid.source = file_obj.name
        pic.vid.play = True
        conn.close()
        file_obj.close()

    def load_pix(self, fil):
        conn = SMBConnection(userID, password, client_machine_name,
            server_name, use_ntlm_v2 = True)
        assert conn.connect(server_ip, 139)
        suffix = '.%s' % (fil.filename.split('.')[-1])
        file_obj = tempfile.NamedTemporaryFile(prefix='loader', suffix=suffix,
            delete=False)

        file_attributes, filesize = conn.retrieveFile(service_name,
            path + '/' + fil.filename, file_obj)
        while filesize != fil.file_size:
            pass
        conn.close()
        file_obj.close()

        # using clock here is unnecessary cause all we are doing is changing
        # the source string property which is updated in the main thread any way.
        # Clock is used just to show a slight delay of 2 secs, one can simply do
        # fil.picture.source = file_obj.name
        Clock.schedule_once(lambda *args: fil.picture.func(file_obj.name), 2)



class Picture(Scatter):
    '''Picture is the class that will show the image with a white border and a
    shadow. They are nothing here because almost everything is inside the
    picture.kv. Check the rule named <Picture> inside the file, and you'll see
    how the Picture() is really constructed and used.

    The source property will be the filename to show.
    '''

    source = StringProperty(None)

    def func(self, filename):
       self.source = filename

    def on_touch_down(self, touch):
       if self.collide_point(*touch.pos) and touch.is_double_tap:
           if not hasattr(self, 'vid'):
               self.vid = vid = VideoPlayer()
               self.add_widget(vid)
               self.bind(
                   pos = vid.setter('pos'),
                   size = vid.setter('size'))
               M_Loader().load_vid(vid_file, self)
       return super(Picture, self).on_touch_down(touch)



class PicturesApp(App):

    def build(self):

        # the root is created in pictures.kv
        root = self.root

        # get any files into images directory
        conn = SMBConnection(userID, password, client_machine_name,
            server_name, use_ntlm_v2 = True)
        assert conn.connect(server_ip, 139)
        file_list = conn.listPath(service_name, path, search=55, pattern='*'
            ,timeout=30)
        conn.close()

        for fil in file_list:
            if fil.filename not in ('.', '..', '.empty'):

                try:
                    # load the image
                    fil.picture = picture = Picture(source=
                        'data/images/image-loading.gif', rotation=randint(-30,30))

                    # add to the main field
                    root.add_widget(picture)
                    M_Loader().load_pix(fil)

                except Exception, e:
                    Logger.exception('Pictures: Unable to load <%s>'
                        % fil.filename)

    def on_pause(self):
        return True


if __name__ == '__main__':
    PicturesApp().run()


Alternatively if Loader was modified to support smb:// protocol, one could just use AsyncImage to load images like this:;

To test checkhout the smb branch

git pull
git checkout smb

Replace main.py in pictures demo with this code. Replace `Image` in pictures.kv with `AsyncImage`

'''
Pictures demo
=============

This is a basic picture viewer, using the scatter widget.
'''

import kivy
kivy.require('1.0.6')

from glob import glob
from random import randint
from os.path import join, dirname
from kivy.app import App
from kivy.logger import Logger
from kivy.uix.scatter import Scatter
from kivy.properties import StringProperty
# FIXME this shouldn't be necessary
from kivy.core.window import Window
from smb.SMBConnection import SMBConnection

userID = 'xxxxxxxxxxxx'
password = 'xxxxxxxxxx'
client_machine_name = ''
server_name = ''
server_ip = '192.168.11.94'
service_name = 'code'
path = 'multitouch/MT__code/akfork/kivy/examples/demo/pictures/images'


class Picture(Scatter):
    '''Picture is the class that will show the image with a white border and a
    shadow. They are nothing here because almost everything is inside the
    picture.kv. Check the rule named <Picture> inside the file, and you'll see
    how the Picture() is really constructed and used.

    The source property will be the filename to show.
    '''

    source = StringProperty(None)


class PicturesApp(App):

    def build(self):

        # the root is created in pictures.kv
        root = self.root

        # get any files into images directory
        curdir = dirname(__file__)
        conn = SMBConnection(userID, password, client_machine_name,
            server_name, use_ntlm_v2 = True)
        assert conn.connect(server_ip, 139)
        file_list = conn.listPath(service_name, path, search=55, pattern='*', timeout=30)
        conn.close()

        for fil in file_list:
            if fil.filename not in ('.', '..', '.empty'):

                try:
                    # load the image
                    src = 'smb://%s:%s@%s/%s/%s/%s' %(userID,  password, server_ip, service_name, path, fil.filename)
                    picture = Picture(source=src, rotation=randint(-30,30))

                    # add to the main field
                    root.add_widget(picture)
                except Exception, e:
                    Logger.exception('Pictures: Unable to load <%s>' % fil.filename)

    def on_pause(self):
        return True


if __name__ == '__main__':
    PicturesApp().run()



I only tested these snippets with VM's don't know how it will act in normal lan situations.
However the basic premise should be pretty similar.

Hope this helps

--
 
 
 

Reply all
Reply to author
Forward
0 new messages