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.
Python 3.8.2
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 4.1.0
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
VLC Media Player
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
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
- Respond to the user moving the slider (triggered by event)
- 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
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
- Your system does not support ADS (Linux, Mac, Windows non-NTFS)
- 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:
- Editing and saving the base file (VideoLib.py) will delete the ADS
- 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.