Creating a GUI Wrapper for VLC Media Player in python/wxpython

Reverend Jim
Introduction

I have a pile of home movie videos created over more than seventy years. As with my many photos (all in digital form) they require some effort to find particular ones of interest. Typically, I name my videos (and photos) with tags. A file might look like

    2013-10-29 11-59 Tucker & Cooper playing in backyard.mp4

What I wanted was an integrated interface that would easily allow me to search and play videos. I had been using Everything for the searching, and VLC Media Player for the playback. I found this to be functional but clumsy so I decided to write my own application. I still wanted VLC as my back end because it plays just about everything. Fortunately all of the functionality is bundled in a core library, and the Python gods have seen to it that there is a Python module to interface with that library.

This tutorial will walk you through the creation of a GUI front end for VLC Media Player. It assumes that you are at least passingly familiar with Python and wxPython. If you are not I suggest that for an introduction to Python you read the excellent book Beginning Python: From Novice to Professional by Magnus Lie Hetland. For wxPython there is Creating GUI Applications with wxPython by Mike Driscoll.

Following the (hopefully) successful completion of the GUI front end I will provide the complete video library application. If you work through the tutorial you should be able to understand the workings of the final project.

If you are unfamiliar with wxPython I suggest you read through my previous tutorial on creating a [Python/wxPython Sudoku Tool]().

Quick plug - this tutorial was created using the free version of [Markdown Pad]()

Requirements

I created this application on a laptop running Windows 10 Home although, in theory, it should run on Linux and Mac systems as well with only minor modifications. Of the following packages, you will not actually need wxGlade but I highly recommend it for developing GUIs in wxPython.

Actually, any 3.x version of Python will do although there are two features of 3.8 that I use. One is the walrus operator. If you are not familiar with it, it is a special assignment operator used only within logical expressions. It allows you to do an assignment and a test in one line (something C programmers will be familiar with). It basically eliminates the necessity or priming a loop. Instead of doing

x = GetValue()
while x != someValue:
    stuff
    more stuff
    x = GetValue()

you can do

while (x := GetValue()) != someValue:
    stuff
    more stuff

It's just cleaner. The name of the operator comes from its similarity to a walrus' nose and tusks.

The other is used mainly for debug statements. In development code you will often see statements like

print("x=", x)

In 3.8 this can be shortened to

print(f'{x='})

You should use pip to install/update packages for Python. To make sure you have the latest version of pip installed, open a command shell and type

python -m pip install --upgrade pip

wxPython is a Python wrapper by Robin Dunn for the popular wxWidgets library. This library allows you to create GUI applications that render as native applications whether they run on Windows, Linux, or Mac systems. To install wxPython, open a command shell and type

pip install wxPython

If you already have wxPython installed, run the following to make sure you are using the latest version:

pip install -U wxPython

Obviously if you are building a VLC front end you will need the VLC package to build on. Make sure to download and install the latest version. You will have to explicitly tell Python where to find the core library. You'll see how to do this later.

VLC Python Interface Library

You'll need to import this Python package to control a VLC instance. Install it by:

pip install python-vlc

wxGlade is a GUI design tool that you can use to quickly build your interface. It provides a preview window so you can see your changes in real time. This is particularly useful when using wxPython sizers as the settings can be confusing. Use wxGlade to create event stubs which you can fill in later. You can also use wxGlade to add code to the events but I prefer to just create the stubs and fill in the code later using Idle or Notepad++ as my code editor. Idle is more convenient and flexible for debugging (you can run your code from within Idle) while Notepad++ supports code folding but does not allow you to execute the code directly.

wxGlade is not installed from within Python. Download it as a zip file and unzip it into a local folder. Create a shortcut to wxglade.pyw on your desktop or start menu.

I am not going to detail how to create the GUI using wxGlade here. You can simply copy/paste my code into your own py or pyw file. I've created a short, getting started tutorial on creating a GUI using wxGlade. You can find it [here]().

Some Design Notes

You will often find, at the top of my projects, a line like

DEBUG = True/False

and numerous lines in the code like

if DEBUG: print(...

Typically, a method will look like

def MyMethod(self, parm):
    if DEBUG: print(f'MyMethod {parm=}')

In the case of an event handler

def control_OnEvent(self, event):
    if DEBUG: print(f'MyMethod {event=}')

By setting DEBUG at the top I can easily enable/disable tracing of execution during debugging. The reason I print out event is because event handlers need not be triggered only by events. They can be called by other blocks of code. In the case of a button handler this means that you can simulate a button click by calling the handler like

self.btn_OnClick(None)

And because you can easily test the type of event at run time you can call the handler with any type of parameter. To borrow an example from later on, you can have a volume slider that can

  1. Respond to the user moving the slider (triggered by event)
  2. Reset the volume to a specific value (triggered from code)

This an result in fewer and more concise methods. You'll see how this works later.

Lastly, under debugging, you will see the two lines

if DEBUG: import wx.lib.mixins.inspection
if DEBUG: wx.lib.inspection.InspectionTool().Show()

The first imports a library that provides an inspection tool. The second line displays it. Using this tool you can inspect any element of your application at any time. Take the time to play with it.

The Basic Interface

The GUI that we will create in this tutorial will look like

2020-08-31_220823.jpg

In terms of the organization of the elements, the structure looks like:

Application
    Frame
        Vertical Sizer
            Video Panel
            Video Position Slider
            Horizontal Sizer
                Open button
                Play/Pause Button
                Stop Button
                Spacer
                Mute Button
                Volume Slider

Here is the code for the bare bones GUI:

TITLE = "Python vlc front end"
DEBUG = True

import os
import sys
import wx

if DEBUG: import wx.lib.mixins.inspection

class MyApp(wx.App):

    def OnInit(self):

        self.frame = MyFrame()
        self.SetTopWindow(self.frame)
        self.frame.Show()
        return True

class MyFrame(wx.Frame):

    def __init__(self):

        super().__init__(None, wx.ID_ANY)

        self.SetSize((500, 500))

        #Create display elements
        self.pnlVideo    = wx.Panel (self, wx.ID_ANY)
        self.sldPosition = wx.Slider(self, wx.ID_ANY, value=0, minValue=0, maxValue=1000)
        self.btnOpen     = wx.Button(self, wx.ID_ANY, "Open")
        self.btnPlay     = wx.Button(self, wx.ID_ANY, "Play")
        self.btnStop     = wx.Button(self, wx.ID_ANY, "Stop")
        self.btnMute     = wx.Button(self, wx.ID_ANY, "Mute")
        self.sldVolume   = wx.Slider(self, wx.ID_ANY, value=50, minValue=0, maxValue=200)

        #Set display element properties and layout
        self.__set_properties()
        self.__do_layout()

        #Create event handlers
        self.Bind(wx.EVT_BUTTON, self.btnOpen_OnClick,   self.btnOpen)
        self.Bind(wx.EVT_BUTTON, self.btnPlay_OnClick,   self.btnPlay)
        self.Bind(wx.EVT_BUTTON, self.btnStop_OnClick,   self.btnStop)
        self.Bind(wx.EVT_BUTTON, self.btnMute_OnClick,   self.btnMute)
        self.Bind(wx.EVT_SLIDER, self.sldVolume_OnSet,   self.sldVolume)
        self.Bind(wx.EVT_SLIDER, self.sldPosition_OnSet, self.sldPosition)

        self.Bind(wx.EVT_CLOSE , self.OnClose)

        if DEBUG: wx.lib.inspection.InspectionTool().Show()

    def __set_properties(self):
        if DEBUG: print("__set_properties")
        self.SetTitle(TITLE)
        self.pnlVideo.SetBackgroundColour(wx.BLACK)

    def __do_layout(self):
        if DEBUG: print("__do_layout")

        sizer_1 = wx.BoxSizer(wx.VERTICAL)
        sizer_2 = wx.BoxSizer(wx.HORIZONTAL)

        sizer_1.Add(self.pnlVideo, 1, wx.EXPAND, 0)
        sizer_1.Add(self.sldPosition, 0, wx.EXPAND, 0)
        sizer_1.Add(sizer_2, 0, wx.EXPAND, 0)
        sizer_2.Add(self.btnOpen, 0, 0, 0)
        sizer_2.Add(self.btnPlay, 0, 0, 0)
        sizer_2.Add(self.btnStop, 0, 0, 0)
        sizer_2.Add(80,23) #spacer
        sizer_2.Add(self.btnMute, 0, 0, 0)
        sizer_2.Add(self.sldVolume, 1, wx.EXPAND, 0)
        self.SetSizer(sizer_1)

        self.Layout()

    def OnClose(self, event):
        """Clean up, exit"""
        if DEBUG: print(f'OnClose {event=}')
        sys.exit()

    def btnOpen_OnClick(self, event):
        """Prompt for, load, and play a video file"""
        if DEBUG: print(f'btnOpen_OnClick {event=}')

    def btnPlay_OnClick(self, event): 
        """Play/Pause the video if media present"""
        if DEBUG: print(f'btnPlay_OnClick {event=}')

    def btnStop_OnClick(self, event):
        """Stop playback"""
        if DEBUG: print(f'btnStop_OnClick {event=}')

    def btnMute_OnClick(self, event):
        """Mute/Unmute the audio"""
        if DEBUG: print(f'btnMute_OnClick {event=}')

    def sldVolume_OnSet(self, event):
        """Adjust volume"""
        if DEBUG: print(f'sldVolume_OnSet {event=}')

    def sldPosition_OnSet(self, event):
        """Select a new position for playback"""
        if DEBUG: print(f'sldPosition_OnSet {event=}')

if __name__ == "__main__":
    app = MyApp(False)
    app.MainLoop()

Two of the methods, __set_properties and __do_layout were generated by wxGlade when I built the interface. They closely resemble the form.Designer.vb file that vb.Net uses to store the creation and setup of controls used in the associated form.vb file. If you examine the code and run the application as is you should see how the controls are put together to create the interface.

Adding Some Functionality

The first thing we are going to add is some code to the self.btnOpen handler so we can get a video loaded. Because we are going to autoplay a video on load we'll also add the code to initialize the VLC media component. The VLC core functionality is in the file libvlc.dll. This file in in the folder where VLC was installed, which on my computer is C:\Program Files\VideoLAN\VLC. We will have to tell Python where to find it before we import the vlc code. We modify our imports to look like:

import os
os.add_dll_directory(r'C:\Program Files\VideoLAN\VLC')

import sys
import wx
import vlc

import wx.lib.mixins.inspection

We create the VLC interface and player objects in MyFrame.init as follows:

self.instance = vlc.Instance()
self.player = self.instance.media_player_new()

and we tell the player that we want to render the video in self.pnlVideo by passing it a handle to the panel.

self.player.set_hwnd(self.pnlVideo.GetHandle())

Every other VLC operation will be done through the player object.

Now we can add the code to the btnOpen handler. If we are going to load a new video it makes sense to stop the current video from playing (if there is one) so we will also add code to the btnStop event handler.

def btnStop_OnClick(self, event):
    """Stop playback"""
    self.player.stop()

def btnOpen_OnClick(self, event):
    """Prompt for and load a video file"""

    #Stop any currently playing video
    self.btnStop_OnClick(None)

    #Display file dialog
    dlg = wx.FileDialog(self, "Choose a file", r'd:\my\videos', "", "*.*", 0)

    if dlg.ShowModal() == wx.ID_OK:
        dir  = dlg.GetDirectory()
        file = dlg.GetFilename()

        self.media = self.instance.media_new(os.path.join(dir, file))
        self.player.set_media(self.media)

        if (title := self.player.get_title()) == -1:
            title = file
        self.SetTitle(title)

        #Play the video
        self.btnPlay_OnClick(None)

and some code to the btnPlay handler as

def btnPlay_OnClick(self, event): 
    """Play/Pause the video if media present"""
    if DEBUG: print(f'btnPlay_OnClick {event=}')
    if self.player.get_media():
        self.player.play()

You should be able to run this and play a video. If you see any output lines like the following you can just ignore them:

[0000022c9dd12320] mmdevice audio output error: cannot initialize COM (error 0x80010106)
[0000022c9dd20940] mmdevice audio output error: cannot initialize COM (error 0x80010106)

It's nice to be able to pause and resume a video so let's modify the Play button so it acts like a toggle.

def btnPlay_OnClick(self, event): 
    """Play/Pause the video if media present"""

    if self.player.get_media():
        if self.player.get_state() == vlc.State.Playing:
            #Pause the video
            self.player.pause()
            self.btnPlay.Label = "Play"
        else:
            #Start or resume playing
            self.player.play()
            self.btnPlay.Label = "Pause"

def btnStop_OnClick(self, event):
    """Stop playback"""
    self.player.stop()
    self.btnPlay.Label = "Play"

Note that I've added a line in btnStop_OnClick to update the Play button label.

You'll probably have noticed that we have a slider to show the position of the video during playback but the slider isn't moving. In order to link the slider to the video we will create a timer object and update the slider position in the timer handler. Create the timer in the block that creates the other elements by

self.timer = wx.Timer(self)

and an associated event handler by

self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)

The event handler will look like

def OnTimer(self, event):
    """Update the position slider"""

    if self.player.get_state() == vlc.State.Playing:
        length = self.player.get_length()
        self.sldPosition.SetRange(0, length)
        time = self.player.get_time()
        self.sldPosition.SetValue(time)

If we set the slider length to the length of the video then on every timer tick we just have to copy the current position as returned by get_time to the slider position. Now all we have to do is start the timer when we start playback. Our play/pause and stop handlers will now look like

def btnPlay_OnClick(self, event): 
    """Play/Pause the video if media present"""

    if self.player.get_media():
        if self.player.get_state() == vlc.State.Playing:
            #Pause the video
            self.player.pause()
            self.btnPlay.Label = "Play"
        else:
            #Start or resume playing
            self.player.play()
            self.timer.Start()
            self.btnPlay.Label = "Pause"

def btnStop_OnClick(self, event):
    """Stop playback"""
    self.timer.Stop()
    self.player.stop()
    self.btnPlay.Label = "Play"

I want to mention here that you may notice volume problems where the volume initially is out of sync with the slider. This appears to be a timing problem with the core library where setting the volume at the start may fail. In order to avoid this we will force the volume to agree with the slider in our timer handler.

Our application so far looks like

TITLE = "Python vlc front end"
DEBUG = True

import os
os.add_dll_directory(r'C:\Program Files\VideoLAN\VLC')

import sys
import wx
import vlc

if DEBUG: import wx.lib.mixins.inspection

ROOT  = os.path.expanduser("~")

class MyApp(wx.App):

    def OnInit(self):

        self.frame = MyFrame()
        self.SetTopWindow(self.frame)
        self.frame.Show()
        return True

class MyFrame(wx.Frame):

    def __init__(self):

        super().__init__(None, wx.ID_ANY)

        self.SetSize((500, 500))

        #Create display elements
        self.pnlVideo    = wx.Panel (self, wx.ID_ANY)
        self.sldPosition = wx.Slider(self, wx.ID_ANY, value=0, minValue=0, maxValue=1000)
        self.btnOpen     = wx.Button(self, wx.ID_ANY, "Open")
        self.btnPlay     = wx.Button(self, wx.ID_ANY, "Play")
        self.btnStop     = wx.Button(self, wx.ID_ANY, "Stop")
        self.btnMute     = wx.Button(self, wx.ID_ANY, "Mute")
        self.sldVolume   = wx.Slider(self, wx.ID_ANY, value=50, minValue=0, maxValue=200)
        self.timer       = wx.Timer (self)

        #Set display element properties and layout
        self.__set_properties()
        self.__do_layout()

        #Create event handlers
        self.Bind(wx.EVT_BUTTON, self.btnOpen_OnClick,   self.btnOpen)
        self.Bind(wx.EVT_BUTTON, self.btnPlay_OnClick,   self.btnPlay)
        self.Bind(wx.EVT_BUTTON, self.btnStop_OnClick,   self.btnStop)
        self.Bind(wx.EVT_BUTTON, self.btnMute_OnClick,   self.btnMute)
        self.Bind(wx.EVT_SLIDER, self.sldVolume_OnSet,   self.sldVolume)
        self.Bind(wx.EVT_SLIDER, self.sldPosition_OnSet, self.sldPosition)
        self.Bind(wx.EVT_TIMER , self.OnTimer,           self.timer) 

        self.Bind(wx.EVT_CLOSE , self.OnClose)

        #Create vlc objects and link the player to the display panel
        self.instance = vlc.Instance()
        self.player = self.instance.media_player_new()
        self.player.set_hwnd(self.pnlVideo.GetHandle())

        if DEBUG: wx.lib.inspection.InspectionTool().Show()

    def __set_properties(self):
        if DEBUG: print("__set_properties")
        self.SetTitle(TITLE)
        self.pnlVideo.SetBackgroundColour(wx.BLACK)

    def __do_layout(self):
        if DEBUG: print("__do_layout")

        sizer_1 = wx.BoxSizer(wx.VERTICAL)
        sizer_2 = wx.BoxSizer(wx.HORIZONTAL)

        sizer_1.Add(self.pnlVideo, 1, wx.EXPAND, 0)
        sizer_1.Add(self.sldPosition, 0, wx.EXPAND, 0)
        sizer_1.Add(sizer_2, 0, wx.EXPAND, 0)
        sizer_2.Add(self.btnOpen, 0, 0, 0)
        sizer_2.Add(self.btnPlay, 0, 0, 0)
        sizer_2.Add(self.btnStop, 0, 0, 0)
        sizer_2.Add(80,23) #spacer
        sizer_2.Add(self.btnMute, 0, 0, 0)
        sizer_2.Add(self.sldVolume, 1, wx.EXPAND, 0)
        self.SetSizer(sizer_1)

        self.Layout()

    def OnClose(self, event):
        """Clean up, exit"""
        if DEBUG: print(f'OnClose {event=}')
        sys.exit()

    def btnOpen_OnClick(self, event):
        """Prompt for, load, and play a video file"""
        if DEBUG: print(f'btnOpen_OnClick {event=}')

        #Stop any currently playing video
        self.btnStop_OnClick(None)

        #Display file dialog
        dlg = wx.FileDialog(self, "Select a file", ROOT, "", "*.*", 0)

        if dlg.ShowModal() == wx.ID_OK:
            dir  = dlg.GetDirectory()
            file = dlg.GetFilename()

            self.media = self.instance.media_new(os.path.join(dir, file))
            self.player.set_media(self.media)

            if (title := self.player.get_title()) == -1:
                title = file
            self.SetTitle(title)

            #Play the video
            self.btnPlay_OnClick(None)

    def btnPlay_OnClick(self, event): 
        """Play/Pause the video if media present"""
        if DEBUG: print(f'btnPlay_OnClick {event=}')

        if self.player.get_media():            
            if self.player.get_state() == vlc.State.Playing:
                #Pause the video
                self.player.pause()
                self.btnPlay.Label = "Play"
            else:
                #Start or resume playing
                self.player.play()
                self.timer.Start()
                self.btnPlay.Label = "Pause"

    def btnStop_OnClick(self, event):
        """Stop playback"""
        if DEBUG: print(f'btnStop_OnClick {event=}')
        self.timer.Stop()
        self.player.stop()
        self.btnPlay.Label = "Play"

    def btnMute_OnClick(self, event):
        """Mute/Unmute the audio"""
        if DEBUG: print(f'btnMute_OnClick {event=}')

    def sldVolume_OnSet(self, event):
        """Adjust volume"""
        if DEBUG: print(f'sldVolume_OnSet {event=}')

    def sldPosition_OnSet(self, event):
        """Select a new position for playback"""
        if DEBUG: print(f'sldPosition_OnSet {event=}')

    def OnTimer(self, event):
        """Update the position slider"""

        if self.player.get_state() == vlc.State.Playing:
            length = self.player.get_length()
            self.sldPosition.SetRange(0, length)
            time = self.player.get_time()
            self.sldPosition.SetValue(time)
            #Force volume to slider volume
            self.player.audio_set_volume(self.sldVolume.GetValue())

if __name__ == "__main__":
    app = MyApp(False)
    app.MainLoop()

In just 162 lines of code (104 if you don't count comments, white space, and doc strings) we have a reasonably functional video player. Next we will add functionality for a Mute/Unmute button and a volume slider.

The VLC mute feature is independent of the volume setting so you can mute/unmute the audio without affecting the volume setting. In case you were wondering why I set the max value of the volume slider to 200 when I created the control, it was to match the volume range used by vlc which is 0-200. To implement a mute/unmute feature we simply have to connect a handler to the button and toggle the mute setting and update the button label to indicate the new function. To start, lets make sure that the start volume and slider position are both reasonable and consistent so in the initialization block we will add some code to set a default volume.

#Set the initial volume to 40 (vlc volume is 0-200)
self.player.audio_set_volume(40)
self.sldVolume.SetValue(40)

and the btnMute_OnClick handler will become

def btnMute_OnClick(self, event):
    """Mute/Unmute the audio"""
    self.player.audio_set_mute(not self.player.audio_get_mute())
    self.btnMute.Label = "Unute" if self.player.audio_get_mute() else "Mute"

To change the volume via the slider we can code the handler as

def sldVolume_OnSet(self, event):
    """Adjust audio volume"""
    self.player.audio_set_volume(self.sldVolume.GetValue())

or, as I mentioned earlier we could code it like

def sldVolume_OnSet(self, event):
    """Adjust volume"""
    if DEBUG: print(f'sldVolume_OnSet {event=}')

    if type(event) is int:
        #Use passed integer value as  new volume
        volume = event
        self.sldVolume.SetValue(volume)
    else:
        #Use slider value as new volume
        volume = self.sldVolume.GetValue()

This allows us to trigger the handler from an event or from code. With this form we can replace the earlier initialization of the default volume to

self.sldVolume_OnSet(40)

Before we add a seek function I want to take care of something that I've always found annoying. When I change settings I usually prefer that those settings get restored on the next run of the application. I like to implement the same two methods in most of my applications.

def LoadConfig()
def SaveConfig()

I call the LoadConfig method at the end of my init code, and SaveConfig in my OnClose handler. I don't like little files littering up my system, and I don't want to worry about dragging a config file along when I move an app to another folder so I make use of a feature of Windows NTFS called Alternate Data Streams (ADS for short). In brief when you have a file (or folder) you can create one or more ADS. Ever wonder how when you download a program, Windows knows to ask you if you really want to run it? It's because there is an ADS that tags it as a foreign file. An ADS name is simply the file (or folder) name, followed by a colon, then the stream name. In our case we will have an application file and an ADS with the respective names:

VideoLib.py
VideoLib.py:config

You can create, delete, read, and write an ADS from Python but if you want to see the contents from Windows you can use a command shell and the cat command like

cat < VideoLib.py:config

Incidentally, you can store any kind of data in an ADS. You could even store an executable. But I digress. Command line arguments are available through sys.argv, where item[0] is the name of the executing script. To get the name of the associated config ADS (which we will create) you can do

self.config = __file__ + ':config'

Now you just treat it like any other file. And because Python can happily execute code created on the fly we don't have to worry about parsing standard config file entries. We can just write Python statements and read/execute them.

Having said that, you may choose not to use this method because

  1. Your system does not support ADS (Linux, Mac, Windows non-NTFS)
  2. You just don't like it

That's fine. With just a minor modification you can use a plain old ini file. Set a flag at the top to indicate your preference

USEADS = True #or False

And code LoadConfig as follows:

def LoadConfig(self):
    """Load the settings from the previous run"""
    if DEBUG: print("LoadConfig")

    if USEADS: self.config = __file__ + ':config'
    else:      self.config = os.path.splitext(__file__)[0] + ".ini"

    try:
        with open(self.config,'r') as file:
            for line in file.read().splitlines():
                if DEBUG: print(line)
                exec(line)
    except: pass

Doing a SaveConfig, however, is a little trickier as you must ensure that you are writing out valid Python statements. We want to save the last used position, size, volume and video folder so we will use

def SaveConfig(self):
    """Save the current settings for the next run"""
    if DEBUG: print("SaveConfig")

    x,y = self.GetPosition()
    w,h = self.GetSize()
    vol = self.sldVolume.GetValue()

    with open(self.config,'w') as file:
        file.write('self.SetPosition((%d,%d))\n' % (x, y))
        file.write('self.SetSize((%d,%d))\n'     % (w, h))
        file.write('self.sldVolume_OnSet(%d)\n'  % (vol))
        file.write('self.root = "%s"' % self.root.replace("\\","/"))

We must use self.root.replace("\\","/") to prevent Python from interpreting backslash characters as escape sequences when we do the next LoadConfig. Also, because we are now maintaining a root property we add

self.root = ROOT

to self.__set_properties and change our btnOpen_OnClick code from

dlg = wx.FileDialog(self, "Select a file", ROOT, "", "*.*", 0)

to

dlg = wx.FileDialog(self, "Select a file", self.root, "", "*.*", 0)

we must also update self.root when we browse to a new folder in self.btnOpen_OnClick

self.root = dir

We will call LoadConfig at the end of our initialization and SaveConfig in our OnClose handler.

Note that we never have to change our LoadConfig code. It just merrily tries to execute whatever it reads in. Of course, if you don't like this method you are free to rewrite LoadConfig and SaveConfig or use one of the available Python libraries. Two last notes about the load/save code and ADS:

  1. Editing and saving the base file (VideoLib.py) will delete the ADS
  2. You can edit the ADS directly by notepad VideoLib.py:config

The next feature we will add is the ability to move around within the video stream. To do that, instead of updating the slider with the current playback position, we will update the playback position with the value of the slider. To do that we use the wx.EVT_SLIDER event. We already have a handler stub so let's add the code. Like the sldVolume_OnSet handler we will write it so that it can be called from other code as well as by the event subsystem.

def sldPosition_OnSet(self, event):
    """Select a new position for playback"""
    if DEBUG: print(f'sldPosition_OnSet {event=}')

    if type(event) is int:
        #Use passed value as new position (passed value = 0 to 100)
        newpos = event / 100.0
        self.sldPosition.SetValue(int(self.player.get_length() * newpos))
    else:
        #Use slider value to calculate new position from 0.0 to 1.0
        newpos = self.sldPosition.GetValue()/self.player.get_length()

    self.player.set_position(newpos)

In this case it helps to know that set_position takes a number from 0 to 1.0 where 0, naturally is the start, and 1.0 is the end. To calculate the desired value we divide the new slider position by the video length. Remember that we initially set the maximum slider value to be the video length.

Try playing a video and note that you can now seek to any position in the video.

One thing I always found annoying about VLC is that when you resize a video it doesn't automatically crop the unused space (black borders). We can easily do that with our application though by adding a few lines of code to our timer event. We'll need to get the aspect ratio of the currently playing video and we might as well just bury that in another method. Due to a timing issue which I haven't quite worked out yet, the VLC library methods that return the width and height may return 0 at the start so if that happens we'll just return a default aspect of 4/3. It may cause the video to display in the wrong size frame to start but it will be for such a short period it will likely go unnoticed.

def GetAspect(self):
    """Return the video aspect ratio w/h if available, or 4/3 if not"""
    width,height = self.player.video_get_size()
    return 4.0/3.0 if height == 0 else width/height

Now we can add the following code to the end of our timer handler

#Ensure display is same aspect as video
asp = self.GetAspect()
width,height = self.GetSize()
newheight = 75 + int(width/asp)
if newheight != height:
    self.SetSize((width,newheight))

One more little tweak handles the detection of the event when the video reaches the end. At that point we want to rest the slider to the start and make sure that play/pause button displays the correct label. I originally went through the VLC event manager to handle this but I found it problematic. After spending the better part of two days trying to debug it I gave up and just handled it in my timer code by adding

if self.player.get_state() == vlc.State.Ended:
    self.btnStop_OnClick(None)
    return

and modifying btnStop_OnClick to stop the timer.

def btnStop_OnClick(self, event):
    """Stop playback"""
    if DEBUG: print(f'btnStop_OnClick {event=}')

    self.timer.Stop()
    self.player.stop()
    self.btnPlay.Label = "Play"
    self.sldPosition_OnSet(0)

Now you see why I wrote the sldPosition_OnSet handler as dual-purpose.

If you run this application with the extension py you will see (mostly annoying) informational messages produced by VLC. You can safely ignore these. If they offend you then use the extension pyw and you won't see them again. The complete code for what we have done so far is

TITLE  = "Python vlc front end"
USEADS = False
DEBUG  = True

import os
os.add_dll_directory(r'C:\Program Files\VideoLAN\VLC')

import sys
import wx
import vlc

if DEBUG: import wx.lib.mixins.inspection

class MyApp(wx.App):

    def OnInit(self):

        self.frame = MyFrame()
        self.SetTopWindow(self.frame)
        self.frame.Show()
        return True

class MyFrame(wx.Frame):

    def __init__(self):

        super().__init__(None, wx.ID_ANY)

        self.SetSize((500, 500))

        #Create display elements
        self.pnlVideo    = wx.Panel (self, wx.ID_ANY)
        self.sldPosition = wx.Slider(self, wx.ID_ANY, value=0, minValue=0, maxValue=1000)
        self.btnOpen     = wx.Button(self, wx.ID_ANY, "Open")
        self.btnPlay     = wx.Button(self, wx.ID_ANY, "Play")
        self.btnStop     = wx.Button(self, wx.ID_ANY, "Stop")
        self.btnMute     = wx.Button(self, wx.ID_ANY, "Mute")
        self.sldVolume   = wx.Slider(self, wx.ID_ANY, value=50, minValue=0, maxValue=200)
        self.timer       = wx.Timer (self)

        #Set display element properties and layout
        self.__set_properties()
        self.__do_layout()

        #Create event handlers
        self.Bind(wx.EVT_BUTTON, self.btnOpen_OnClick,   self.btnOpen)
        self.Bind(wx.EVT_BUTTON, self.btnPlay_OnClick,   self.btnPlay)
        self.Bind(wx.EVT_BUTTON, self.btnStop_OnClick,   self.btnStop)
        self.Bind(wx.EVT_BUTTON, self.btnMute_OnClick,   self.btnMute)
        self.Bind(wx.EVT_SLIDER, self.sldVolume_OnSet,   self.sldVolume)
        self.Bind(wx.EVT_SLIDER, self.sldPosition_OnSet, self.sldPosition)
        self.Bind(wx.EVT_TIMER , self.OnTimer,           self.timer) 

        self.Bind(wx.EVT_CLOSE , self.OnClose)

        #Create vlc objects and link the player to the display panel
        self.instance = vlc.Instance()
        self.player = self.instance.media_player_new()
        self.player.set_hwnd(self.pnlVideo.GetHandle())

        self.LoadConfig()

        if DEBUG: wx.lib.inspection.InspectionTool().Show()

    def __set_properties(self):
        if DEBUG: print("__set_properties")
        self.SetTitle(TITLE)
        self.root = os.path.expanduser("~")
        self.file = ""
        self.pnlVideo.SetBackgroundColour(wx.BLACK)
        self.sldVolume_OnSet(40)

    def __do_layout(self):
        if DEBUG: print("__do_layout")

        sizer_1 = wx.BoxSizer(wx.VERTICAL)
        sizer_2 = wx.BoxSizer(wx.HORIZONTAL)

        sizer_1.Add(self.pnlVideo, 1, wx.EXPAND, 0)
        sizer_1.Add(self.sldPosition, 0, wx.EXPAND, 0)
        sizer_1.Add(sizer_2, 0, wx.EXPAND, 0)
        sizer_2.Add(self.btnOpen, 0, 0, 0)
        sizer_2.Add(self.btnPlay, 0, 0, 0)
        sizer_2.Add(self.btnStop, 0, 0, 0)
        sizer_2.Add(80,23) #spacer
        sizer_2.Add(self.btnMute, 0, 0, 0)
        sizer_2.Add(self.sldVolume, 1, wx.EXPAND, 0)
        self.SetSizer(sizer_1)

        self.Layout()

    def LoadConfig(self):
        """Load the settings from the previous run"""
        if DEBUG: print("LoadConfig")

        if USEADS: self.config = __file__ + ':config'
        else:      self.config = os.path.splitext(__file__)[0] + ".ini"

        try:
            with open(self.config,'r') as file:
                for line in file.read().splitlines():
                    if DEBUG: print(line)
                    exec(line)
        except: pass

    def SaveConfig(self):
        """Save the current settings for the next run"""
        if DEBUG: print("SaveConfig")

        x,y = self.GetPosition()
        w,h = self.GetSize()
        vol = self.sldVolume.GetValue()

        with open(self.config,'w') as file:
            file.write('self.SetPosition((%d,%d))\n' % (x, y))
            file.write('self.SetSize((%d,%d))\n'     % (w, h))
            file.write('self.sldVolume_OnSet(%d)\n'  % (vol))
            file.write('self.root = "%s"' % self.root.replace("\\","/"))

    def OnClose(self, event):
        """Clean up, save settings, and exit"""
        if DEBUG: print(f'OnClose {event=}')
        self.SaveConfig()
        sys.exit()

    def btnOpen_OnClick(self, event):
        """Prompt for, load, and play a video file"""
        if DEBUG: print(f'btnOpen_OnClick {event=}')

        #Stop any currently playing video
        self.btnStop_OnClick(None)

        #Display file dialog
        dlg = wx.FileDialog(self, "Select a file", self.root, "", "*.*", 0)

        if dlg.ShowModal() == wx.ID_OK:
            dir  = dlg.GetDirectory()
            file = dlg.GetFilename()

            self.root = dir

            self.media = self.instance.media_new(os.path.join(dir, file))
            self.player.set_media(self.media)

            if (title := self.player.get_title()) == -1:
                title = file
            self.SetTitle(title)

            #Play the video
            self.btnPlay_OnClick(None)

    def btnPlay_OnClick(self, event): 
        """Play/Pause the video if media present"""
        if DEBUG: print(f'btnPlay_OnClick {event=}')

        if self.player.get_media():            
            if self.player.get_state() == vlc.State.Playing:
                #Pause the video
                self.player.pause()
                self.btnPlay.Label = "Play"
            else:
                #Start or resume playing
                self.player.play()
                self.timer.Start()
                self.btnPlay.Label = "Pause"

    def btnStop_OnClick(self, event):
        """Stop playback"""
        if DEBUG: print(f'btnStop_OnClick {event=}')
        self.timer.Stop()
        self.player.stop()
        self.btnPlay.Label = "Play"
        self.sldPosition_OnSet(0)

    def btnMute_OnClick(self, event):
        """Mute/Unmute the audio"""
        if DEBUG: print(f'btnMute_OnClick {event=}')
        self.player.audio_set_mute(not self.player.audio_get_mute())
        self.btnMute.Label = "Unute" if self.player.audio_get_mute() else "Mute"

    def sldVolume_OnSet(self, event):
        """Adjust volume"""
        if DEBUG: print(f'sldVolume_OnSet {event=}')

        if type(event) is int:
            #Use passed value as  new volume
            volume = event
            self.sldVolume.SetValue(volume)
        else:
            #Use slider value as new volume
            volume = self.sldVolume.GetValue()

    def sldPosition_OnSet(self, event):
        """Select a new position for playback"""
        if DEBUG: print(f'sldPosition_OnSet {event=}')

        if type(event) is int:
            #Use passed value as new position (passed value = 0 to 100)
            newpos = event / 100.0
            self.sldPosition.SetValue(int(self.player.get_length() * newpos))
        else:
            #Use slider value to calculate new position from 0.0 to 1.0
            newpos = self.sldPosition.GetValue()/self.player.get_length()

        self.player.set_position(newpos)

    def GetAspect(self):
        """Return the video aspect ratio w/h if available, or 4/3 if not"""
        width,height = self.player.video_get_size()
        return 4.0/3.0 if height == 0 else width/height

    def OnTimer(self, event):
        """Update the position slider"""

        if self.player.get_state() == vlc.State.Ended:
            self.btnStop_OnClick(None)
            return

        if self.player.get_state() == vlc.State.Playing:
            length = self.player.get_length()
            self.sldPosition.SetRange(0, length)
            time = self.player.get_time()
            self.sldPosition.SetValue(time)
            #Force volume to slider volume (bug)
            self.player.audio_set_volume(self.sldVolume.GetValue())

        #Ensure display is same aspect as video
        aspect = self.GetAspect()
        width,height = self.GetSize()
        newheight = 75 + int(width/aspect)
        if newheight != height:
            self.SetSize((width,newheight))

if __name__ == "__main__":
    app = MyApp(False)
    app.MainLoop()

In my next post I am going to take this code and complete the video library project.

pritaeas commented: Nice +16
129 Views
About the Author

I completed my Computer Science degree at the University of Manitoba in 1976. I did two and a half years of programming in medical research followed by twenty-nine years at Manitoba Hydro (electric utility). Most of that was spent on doing development and maintenance on an AGC/SCADA (real-time programming/process control) system. The last ten years of that was spent doing application and infrastructure support and development. I have programmed in FORTRAN (mostly), APL, PL/1, COBOL, Lisp, SNOBOL, ALGOL, Assembler (several flavours), C, C++, Paradox, VB, vbScript and more recently, Python. I am married with two grown children of whom I am very proud, and a most beautiful wife. I am currently retired (and loving it).

Reverend Jim 3,400 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster

Sorry about the broken links. They should be good now.

Reverend Jim 3,400 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster

In the vein of "I told you that story so I could tell you this one"... I wrote the above project so that I could rebuild my VideoLib application (which I originally developed in vb.Net) in Python/wxPython. Taking the code from above, I created an expanded user interface and did some cleanup. What I ended up with was an app that I could use to browse my home movie library.

I have all of my videos tagged (keywords in the file names) and commented (expanded descriptions in :comment alternate data streams). Using the new application I can browse to a folder containing videos and filter the list of videos by entering one or more strings. With the entered strings I can restrict the list to

  1. Entries where the file or comment contains all of a set of filter strings
  2. Entries where the file or comment contains any of a set of filter strings
  3. Entries where the file or comment contains the exact filter string

Selecting a video causes it to immediately start playing. Using a slider I can seek to any part of the video. Standard controls allow me to pause/resume/stop playback and select an appropriate volume. Where possible, the display is adjusted to best-fit the video with no/minimal unused space.

Playback can also be controlled with hotkeys. Arrow left/right skips back/ahead 5 seconds. Shift left/right skips 15 seconds. Other hotkeys can be added as required.

Any comment associated with the video is displayed in a scrollable/wrapped text control. These comments can be edited. Changes will be automatically saved on new video or app close.

I added an enhancement to the timer handler to address the problem when adjusting the frame width causes part of the frame to move off screen. The new handler will attempt to shift the frame so that it is entirely on screen.

Standard disclaimer - I make no claims that this code is bug free or even an example of great Python code. However, it is mostly functional, and as uncomplicated as I can make it. Suggestions for bug fixes and improvements are always welcome.

This project (VideoLib.pyw) is attached as a zip file and includes the library routines from my D:\Include folder.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of 1.19 million developers, IT pros, digital marketers, and technology enthusiasts learning and sharing knowledge.