Using Gstreamer with wxPython – Two MainLoops and the Law of Delimiter

(Original post from 19. May 2012)

Yesterday, while writing on the new version of my super duper honking RPG MusicPlayer, I came across a very interesting problem. Interesting, because it made me learn new things about Python in particular and programming in general.

Gstreamer uses the gobject.MainLoop() to communicate events via messages to its playbin instance. The example below is directly from the tutorial. Note the bus instance and the on_message method. This all runs via the gobject.MainLoop()

        # bus is used to report messages in playtime
        # Taken from tutorial, also the methods
        self.bus = self.player.get_bus()
        self.bus.add_signal_watch()
        self.bus.enable_sync_message_emission()
        self.bus.connect("message", self.on_message)

    def on_message(self, bus, message):
        """
        Taken from gstreamer tutorial.
        http://pygstdocs.berlios.de/pygst-tutorial
        
        Used in:
        - connects to bus in class.
        
        """
        
        t = message.type
        if t == gst.MESSAGE_EOS: # track is finished
            self.player.set_state(gst.STATE_NULL)
        elif t == gst.MESSAGE_ERROR:
            self.player.set_state(gst.STATE_NULL)
            err, debug = message.parse_error()
            print "Error: %s" % err, debug

So far, I had no problems not running the gobject.MainLoop, because the wx.App.MainLoop() did all the necessary things to keep my songs running. But at one point, I noticed a bug. When the song came naturally to an end, it would just stop and not play the next song. This was, of course, due to the fact, that the End of Song Message never reached the Playbin, because there was no system to relay said message

At this point, I had the uncomfortable situation of needing two MainLoops to run at the same time. Yes, I know, I should have learned to use pyGTK instead of sticking with wxPython, but I like wxPython and I was lazy. Also, that way I learned loads more funky stuff

Anyways, so how to solve that problem… Well, I used Python’s threading module to help me out here. Without threading, I was not able to start both MainLoops at the same time, because of Python’s procedural nature. Because I had no idea about threading, I had several mishaps on the way, but I came up with a minimal example such as shown at the end. I had to look for awhile, because I found most “simple” threading tutorials a bit over the top for my liking, but taking little bits and bobs from everywhere, I was able to come up with a solution.

Most of it really is just syntactic sugar. The key part about this, is the GobjectThread class. This class starts the MainLoop in a separate thread and after the wx.App.MainLoop is finished, the thread is stopped, too. Luckily, I only have to start and stop the gobject.MainLoop. I can imagine, these sort of things can very quickly become very confusing and cluttered. The only other important thing to note in this code is the close event. It is important to set the Gstreamer elements to gst.STATE_NULL, otherwise there are several Critical Errors thrown, when terminating the MainLoop

Also, as a little side note, while discussing this in the German Python Forum, I was told about Delimiter’s Law. I very often have things like self.player.player.get_state(), which according to Delimiter’s Law is a big “NoNo!”. Essentially, when disregarding Delimiter, too closely knit classes are a consequence. This can make it hard to debug or replace certain parts of the code, because the other classes take too much of the original architecture for granted. Knowing this now, I have a lot more work ahead of me 🙂

#!usr/bin/python

import os
import urllib
from threading import Thread

import wx
import pygst
pygst.require("0.10")
import gst
import gobject


class MyFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        kwds["style"] = wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)
        
        ###############
        ## Threading ##
        ###############
        self.player = GstPlayer()
        
        # Close Event
        self.Bind(wx.EVT_CLOSE, self.on_close)

        self.__set_properties()
        self.__do_layout()

    def on_close(self, event):
        # If state not set to NULL, Critical Error occurs on gobject quit
        state = self.player.get_state()
        if state[1] == gst.STATE_PLAYING:
            self.player.set_state(gst.STATE_NULL)
        self.Destroy()

    def __set_properties(self):
        self.SetTitle("Test")

    def __do_layout(self):
        self.Layout()


class GstPlayer():
    def __init__(self):
        # This is almost straight out of the tutorial
        p = os.path.join(os.getcwd(), "tracks", "1. Shaent Blathanna.mp3")
        path = urllib.pathname2url(p)
        
        self.player = gst.element_factory_make("playbin2", "player")
        fakesink = gst.element_factory_make("fakesink", "fakesink")
        self.player.set_property("video-sink", fakesink)
        
        self.player.set_property("uri", "file:" + path)
        self.player.set_state(gst.STATE_PLAYING)
        
    def get_state(self):
        return self.player.get_state()
    
    def set_state(self, state):
        self.player.set_state(state)
        
class GobjectThread(Thread):
    def __init__(self):
        Thread.__init__(self)
        ##############################
        self.loop = gobject.MainLoop() # The mainloop to be separated from wx
        ##############################
    
    def run(self): # Called on Thread.start()
        self.loop.run()
        
    def quit(self):
        # Stops the gobject Mainloop
        self.loop.quit()

if __name__ == "__main__":
    gobject.threads_init()
    
    t = GobjectThread()
    t.start()
    
    # wxGlade's my Friend here
    # Just basics
    app = wx.PySimpleApp(0)
    wx.InitAllImageHandlers()
    frame = MyFrame(None, -1, "")
    app.SetTopWindow(frame)
    frame.Show()
    app.MainLoop()
    
    t.quit() #End the gobject.MainLoop()