Adding VLC to PyInstaller on Mac and Windows¶
Looking for PyInstaller Instructions?¶
Link here: PyInstaller Instructions
Why Use VLC In the First Place?¶
If you want to play multimedia and audio in Python, there are some options for you.
One choice is VLC: It is lightweight, free and open source. VLC also works on many devices, like Windows, Linux, Mac, Android and iOS.
In this tutorial we will:
- Set up a new project called BasicPyVLC and environment with Poetry
- Use VLC to play audio in terminal for BasicPyVLC
- Package VLC in a spec file
- Package BasicPyVLC project on Windows using PyInstaller
- Package BasicPyVLC project on Mac using PyInstaller
- Check the packaged exe works on Windows and Mac with NO VLC previously installed (by testing on a Windows guest Virtual Machine sending the exe through shared network folders, dropbox, github, etc)
Prerequisites¶
- Python is installed
- Poetry is installed
- VLC is installed
Step 1: Set up a new project (BasicPyVLC) and environment with Poetry
Goal:
Our goal is to create a new project folder and new virtual environment (virtualenv) so that we don't accidentally destroy our system's Python installation
Step 2: Create new folder and name it BasicPyVLC
How do I create a new folder?
- Open File Explorer ( ⊞ Windows key > type "file explorer")
- You can right click on an empty space in the folder of your choice and click "New > Folder"
- In VS Code, use the default shortcut to bring up a new terminal
£C:\Users\KivySchool\ CTRL + J
- Check if the terminal is in the folder you want to create your subfolder
- If you need to change directory, the command is
cd <folder location>
- In your VS Code terminal, type
£C:\Users\KivySchool\ mkdir "KivyProjects"
- You can also press ⊞ Windows key, type
cmd
and entercd "Your folder location"
to a folder of your choice - In your terminal, type
£C:\Users\KivySchool\ mkdir "KivyProjects"
Step 3: Create VirtualEnv with Poetry
Poetry configuration
If you have followed these instructions you should have:
- poetry installed
- poetry config virtualenvs.in-project true
If not, you can skip this step and proceed with a virtualenv manager of your choice. You were warned
Poetry setup your virtual environment refresher
- Open a terminal inside the BasicPyVLC folder.
How do I open a terminal in a folder?
-
Open VSCode
-
Use VSCode open folder option
The VSCode shortcut is:
CTRL + K CTRL + O
CTRL + J
-
⊞ Windows key > type
cmd
> press Enter -
Change drive if necessary (The default is
C:\
drive but if you want to go toD:\
drive just type:D:
) -
Change directory to that folder (You can get the address by copying from the address bar of File Explorer)
cd (basicpyvlc-py3.10)D:\KivySchool\CODING\BasicPyVLC
- In your terminal type:
£D:\KivySchool\CODING\BasicPyVLC poetry init
- Press enter and complete the process.
- Now you have a
pyproject.toml
file. - Next we add our dependencies, which in this case is
python-vlc
andPyInstaller
.£D:\KivySchool\CODING\BasicPyVLC poetry add python-vlc
£D:\KivySchool\CODING\BasicPyVLC poetry add pyinstaller
The current project's Python requirement...
If you get this error: The current project's Python requirement (>=3.10,<4.0) is not compatible with some of the required packages Python requirement:
pyinstaller requires Python <3.13,>=3.8, so it will not be satisfied for Python >=3.13,<4.0
You can fix it in two ways:
-
Add an upper bound to python version. This is because the upper bound is
< 4.0
as a default, and PyInstaller is asking for Python version<3.13
-
pyproject.toml line:
python = "^3.10, <3.13"
-
Set a fixed Python version in pyproject.toml using
python = "3.10.9"
-
pyproject.toml line:
python = "3.10.9"
- Now update your environment with:
£D:\KivySchool\CODING\BasicPyVLC poetry update
- Now enter your environment with:
£D:\KivySchool\CODING\BasicPyVLC poetry shell
- Now you should be in your virtual environment:
£(basicpyvlc-py3.10)D:\KivySchool\CODING\BasicPyVLC
Poetry configuration
Remember to use the correct Python version with Pyenv!
Poetry configuration pt 2
- Remember to add
.venv
folder to your.gitignore
folder so git doesn't track your environment. - Also add
build
anddist
folders (these come from PyInstaller) - Sample
.gitignore
file:.venv build dist
Step 4: Code + Explanation
basicpyvlc.py | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Basic imports
basicpyvlc.py | |
---|---|
1 2 3 |
|
basicpyvlc.py | |
---|---|
1 |
|
basicpyvlc.py | |
---|---|
1 |
|
__file__
is the location of the file basicpyvlc.py
in the filesystem.
os.path.dirname(__file__)
gets the name of the directory where basicpyvlc.py
is.
os.path.join
creates a proper file path location on any OS. This is because Windows uses \
while Linux/Mac uses /
as separators.
This approach also means it works on PyInstaller, because os.path.dirname(__file__)
will resolve correctly to be sys._MEIPASS
.
We will later add the media file to sys._MEIPASS
and package VLC using PyInstaller. This way your distributable will be packaged properly and run on target machines that don't have VLC installed.
basicpyvlc.py | |
---|---|
1 |
|
basicpyvlc.py | |
---|---|
1 2 3 |
|
time.sleep
Python will immediately exit since there is no code keeping Python open. Since Python immediately exits, it will force VLC to exit as well, meaning you see nothing or a blank cwd
window open and close.
The initial offset is to make sure VLC starts playing (since vlc_player.get_length()
will return a bad value if called while VLC has not yet loaded any media) and then newoffset is created to hold Python open for the rest of the duration of the media. Newoffset might be incorrect and cut off media on slower/older systems because it assumes VLC immediately plays the video. There is time lag between the time it takes for Python to turn on VLC and for VLC to start playing the video.
basicpyvlc.py | |
---|---|
1 2 3 4 5 |
|
What happens if you comment out time.sleep?
If you comment out the first time.sleep, Python will exit early and VLC will not play at all, or you will see a cmd window open and close.
If you comment out the second time.sleep, Python will exit early and VLC will not get to finish playing the media.
Step 5: Running basicpyvlc.py in terminal in your virtualenv
Requirements
- Make sure you are in the correct folder in your terminal (
cd BasicPyVLCfolder
), - Are in your virtualenv (
poetry shell
)
Now, type:
£(basicpyvlc-py3.10)D:\KivySchool\CODING\BasicPyVLC python basicvlc.py
You will see Python play the selected media through VLC.
If you want to exit early type, CTRL + C
in the terminal.
Step 6: Packaging with PyInstaller on Windows
Requirements
- Make sure you are in the correct folder in your terminal (
cd BasicPyVLCfolder
), - Are in your virtualenv? (
poetry shell
) - PyInstaller is in your environment (check with
pip list
) - PyInstaller only packages on that OS. This means you need a Windows machine/emulator to package to Windows, Mac machine to package to Mac, etc.
First, get a .spec file from PyInstaller with:
£(basicpyvlc-py3.10)D:\KivySchool\CODING\BasicPyVLC pyinstaller basicpyvlc.py --onefile
Example basicvlc.spec
basicvlc.spec | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
There are three things to do to add VLC to PyInstaller:
- Add VLC to pathex
- Add plugins folder
- Add
libVLC.dll
(so the executable works on Windows machines without VLC installed)
To add VLC to pathex, find the VLC directory in your machine and add to the pathex=
line in .specfile. Example:
pathex=["D:\KivySchool\VLC"],
To add the plugins folder, add everything in binaries (see example). The plugins folder should be in your VLC folder.
binaries=[("D:\KivySchool\VLC\plugins\*", "plugins")],
To add libVLC.dll
, append to binaries in the EXE (see example). The reason you need to add this is because the .exe will NOT work on a machine that does not have VLC installed. The .exe created WITHOUT this line WILL work on machines that DO have VLC installed.
a.binaries + [("libVLC.dll", "D:\KivySchool\VLC\libvlc.dll", "BINARY")],
Next, update the .spec file to package the media file.
- Add your mediafile to .spec
datas=[(medianame, ".")],
What this line does is send medianame
to the sys._MEIPASS
folder. If you wanted to send it to a "media" folder in sys._MEIPASS
, you would do datas=[(medianame, "media")]
, but then you would have to change everything else, like the mediapath in basicpyvlc.py, etc. See adding datas to specfile PyInstaller.
Since the path is os.path.dirname(__file__)
for mediapath in basicpyvlc.py, it resolves properly when packaged in PyInstaller and when run from terminal.
Step 7: Use PyInstaller to Create an Exe
Now that the .spec file is finished (make sure to save), run this command:
£(basicpyvlc-py3.10)D:\KivySchool\CODING\BasicPyVLC python -m PyInstaller basicvlc.spec --clean
dist
folder.
Step 8: Verifying EXE works on any target Windows machine
Goal:
Our goal is to make sure your .exe works on any Windows machine, regardless if VLC was previously installed. In order to do that we need two things: a virtual machine to test on and to package libVLC.dll
in PyInstaller.
This is how I discovered that you need to package libVLC.dll
to be able to run the .exe on machines that do not VLC installed. Without libVLC.dll
packaged, the .exe runs on my dev machine well, BUT on the virtual machine that has no VLC installed, it crashes, saying
Traceback (most recent call last):
File "PyInstaller\loader\pyimod03_ctypes.py", line 53, in __init__
File "ctypes\__init__.py", line 374, in __init__
FileNotFoundError: Could not find module 'C:\Users\vboxuser\Desktop\libvlc.dll' (or one of its dependencies). Try using the full path with constructor syntax.
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "basicpyvlc.py", line 1, in <module>
File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
File "PyInstaller\loader\pyimod02_importers.py", line 419, in exec_module
File "vlc.py", line 220, in <module>
File "vlc.py", line 180, in find_lib
File "PyInstaller\loader\pyimod03_ctypes.py", line 55, in __init__
pyimod03_ctypes.install.<locals>.PyInstallerImportError: Failed to load dynlib/dll '.\\libvlc.dll'. Most likely this dynlib/dll was not found when the application was frozen.
[5048] Failed to execute script 'basicpyvlc' due to unhandled exception!
By making sure your .exe runs on a fresh Windows virtual machine instance (that has no pythonenv, no special modification to PATH, no VLC installed), you should be confident your .exe works anywhere.
The test was to use shared network folders to send your .exe to the VirtualBox Windows guest OS.
All you need to do is add libvlc.dll
to the binaries line:
a.binaries + [("libVLC.dll", "D:\KivySchool\VLC\libvlc.dll", "BINARY")],
Then recreate the .exe:
£(basicpyvlc-py3.10)D:\KivySchool\CODING\BasicPyVLC python -m PyInstaller basicvlc.spec --clean
Once done, double click and run. If successful, your mediafile should play. Then copy over this .exe to your virtual machine, and it should run even without installing VLC on the virtual machine.
Step 1: Set up a new project (BasicPyVLC) and environment with Poetry
Goal:
Our goal is to create a new project folder and new virtual environment (virtualenv) so that we don't accidentally destroy our system's Python installation
Step 2: Create new folder and name it BasicPyVLC
How do I create a new folder?
- Open Finder (You can use
⌘+SPACE
, typeFinder
) - Go to the folder location
CTRL+CLICK
, then pressNew Folder
Step 3: Create VirtualEnv with Poetry
Poetry configuration
If you have followed these instructions you should have:
- poetry installed
- poetry config virtualenvs.in-project true
If not, you can skip this step and proceed with a virtualenv manager of your choice. You were warned
Poetry setup your virtual environment refresher
- Open a terminal inside the BasicPyVLC folder.
How do I open a terminal in a folder?
- Open VSCode with
⌘+spacebar
-
Type
Code
, pressEnter
-
Use VSCode open folder option There is not open folder shortcut on Mac, you gotta do some clicking. Click
File
thenOpen Folder
- This shortcut will open a terminal for you at the bottom of VSCode
⌘ + J
-
⌘+SPACE
> typeterminal
> press Enter -
Change drive if necessary. You can get a file location through Finder by
CTRL+CLICK
in a folder location >Get Info
>CTRL+Click
onWhere
, thenCopy as Pathname
.
- Change directory to that folder
cd /Users/KivySchool/CODING/BasicPyVLC
- In your terminal type:
£/Users/KivySchool/CODING/BasicPyVLC poetry init
- Press enter and complete the process.
- Now you have a
pyproject.toml
file. - Next we add our dependencies, which in this case is
python-vlc
andPyInstaller
.£/Users/KivySchool/CODING/BasicPyVLC poetry add python-vlc
£/Users/KivySchool/CODING/BasicPyVLC poetry add pyinstaller
£/Users/KivySchool/CODING/BasicPyVLC poetry add pyside6
The current project's Python requirement...
If you get this error: The current project's Python requirement (>=3.10,<4.0) is not compatible with some of the required packages Python requirement:
pyinstaller requires Python <3.13,>=3.8, so it will not be satisfied for Python >=3.13,<4.0
You can fix it in two ways:
-
Add an upper bound to python version. This is because the upper bound is
< 4.0
as a default, and PyInstaller is asking for Python version<3.13
-
pyproject.toml line:
python = "^3.10, <3.13"
-
Set a fixed Python version in pyproject.toml using
python = "3.10.9"
-
pyproject.toml line:
python = "3.10.9"
- Now update your environment with:
£/Users/KivySchool/CODING/BasicPyVLC poetry update
- Now enter your environment with:
£/Users/KivySchool/CODING/BasicPyVLC poetry shell
- Now you should be in your virtual environment:
£(basicpyvlc-py3.10)/Users/KivySchool/CODING/BasicPyVLC
Poetry configuration
Remember to use the correct Python version with Pyenv!
Poetry configuration pt 2
- Remember to add
.venv
folder to your.gitignore
folder so git doesn't track your environment. - Also add
build
anddist
folders (these come from PyInstaller) - Sample
.gitignore
file:.venv build dist
Step 4: Code + Explanation
basicpyvlc.py | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Basic imports
basicpyvlc.py | |
---|---|
1 2 3 4 |
|
basicpyvlc.py | |
---|---|
1 |
|
basicpyvlc.py | |
---|---|
1 |
|
__file__
is the location of the file basicpyvlc.py
in the filesystem.
os.path.dirname(__file__)
gets the name of the directory where basicpyvlc.py
is.
os.path.join
creates a proper file path location on any OS. This is because Windows uses \
while Linux/Mac uses /
as separators.
This approach also means it works on PyInstaller, because os.path.dirname(__file__)
will resolve correctly to be sys._MEIPASS
.
We will later add the media file to sys._MEIPASS
and package VLC using PyInstaller. This way your distributable will be packaged properly and run on target machines that don't have VLC installed.
basicpyvlc.py | |
---|---|
1 |
|
basicpyvlc.py | |
---|---|
1 2 |
|
One solution to the VLC video display bug on MAC is supplying a QtWidgets window to VLC. Here we initialize the VLC window with QtWidgets.
basicpyvlc.py | |
---|---|
1 2 3 4 |
|
Next we give the window to VLCplayer (vlc_player.set_nsobject(vlcWidget.winId())
) and then play.
basicpyvlc.py | |
---|---|
1 2 3 |
|
What happens if you vlcApp.exec() before playing with vlc_player.play() ?
If you do this, the Qtapp will just open and hold forever. Since VLC has not started, it will not recieve any audio data and you'll be forced to close the window manually, like with ⌘ + Q
.
Step 5: Running basicpyvlc.py on Mac
Requirements
- Make sure you are in the correct folder in your terminal (
cd BasicPyVLCfolder
), - Are in your virtualenv? (
poetry shell
)
Now, type:
£(basicpyvlc-py3.10)/Users/KivySchool/CODING/BasicPyVLC python basicvlc.py
You will see Python play the selected media through VLC.
If you want to exit, select the Qt window or terminal window and press ⌘ + Q
.
Step 5: Packaging with PyInstaller on Mac
Requirements
- Make sure you are in the correct folder in your terminal (
cd BasicPyVLCfolder
), - Are in your virtualenv (
poetry shell
) - PyInstaller is in your environment (check with
pip list
) - PyInstaller only packages on that OS. This means you need a Windows machine/emulator to package to Windows, Mac machine to package to Mac, etc.
First, get a .spec file from PyInstaller with:
£(basicpyvlc-py3.10)/Users/KivySchool/CODING/BasicPyVLC pyinstaller basicpyvlc.py --onefile
Example basicvlcMACBASIC.spec
basicvlcMACBASIC.spec | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
There are three things to do to add VLC to PyInstaller:
- Add VLC to pathex
- Add plugins folder
- Add
libvlccore.dylib
andlibvlc.dylib
(complicated, will be Step 8)
To add VLC to pathex, find VLC.app
in your applications folder. You can do this by opening Finder, ⌘+SHIFT+G
, typing /
, and press Enter
.
Then go to yourDrive/Applications/
pathex=["/Applications/VLC.app"],
To add the plugins folder, open up VLC.app
by CTRL+CLICK
on it, then click Open Package Contents
. Then in Contents
you will find the /MacOS/plugins/
folder.
binaries=[("/Applications/VLC.app/Contents/MacOS/plugins/*", "plugins")],
Adding libvlccore.dylib
and libvlc.dylib
is complicated as of the time of writing because of a few bugs, see Step 8. Creating an app in this easy way means that it will NOT run on a Mac does NOT have VLC installed. You can verify this by removing VLC.app from the Applications folder. You can place VLC.app back after verifying.
Next, update the .spec file to package the media file.
- Add your mediafile to .spec
datas=[(medianame, ".")],
What this line does is send medianame
to the sys._MEIPASS
folder. If you wanted to send it to a "media" folder in sys._MEIPASS
, you would do datas=[(medianame, "media")]
, but then you would have to change everything else, like the mediapath in basicpyvlc.py, etc. See adding datas to specfile PyInstaller.
Since the path is os.path.dirname(__file__)
for mediapath in basicpyvlc.py, it resolves properly when packaged in PyInstaller and when run from terminal.
If you run PyInstaller now, you will NOT get an .app file, but only a unix executable file. In order to get a .app file for Mac, you must add the bundle line:
app = BUNDLE(
exe,
name='basicpyVLC.app',
#icon="", #put your icon path here
bundle_identifier=None,
)
Step 7: Use PyInstaller to Create .app
Now that the .spec file is finished (make sure to save), run this command:
£(basicpyvlc-py3.10)/Users/KivySchool/CODING/BasicPyVLC python -m PyInstaller basicvlcMACBASIC.spec --clean
dist
folder.
Step 8: Verifying EXE works on any target Mac machine
Goal:
Our goal is to make sure your .app works on any Mac machine, regardless if VLC.app is previously installed. In order to do that we need two things: a "virtual" machine to test on and to package libvlccore.dylib
and libvlc.dylib
in PyInstaller.
There are a few problems: VirtualBox has dropped support for MAC silicon
Instead, we can simulate a target Mac machine with no VLC installed by simply removing VLC.app from the Application folder. Remember to put it back after doing this tutorial!
You can always get a 2nd Mac as well ¯\_(ツ)_/¯
, or get a friend that has one.
The harder part is that python-vlc
itself has assumed VLC is previously installed, see this issue and the source code where vlc.py assumes that VLC.app is in the applications folder.
How do we fix this? There is a series of problems, and I will provide an answer then go through the code.
Solution:
Fix the elif sys.platform.startswith('darwin'):
block to search for libvlccore.dylib
and libvlc.dylib
in the proper location.
The original goal of the elif sys.platform.startswith('darwin'):
block is to find libvlccore.dylib
, lib/libvlc.dylib
, and set the path for VLC modules
and VLC plugins
folders.
Since we are packaging to PyInstaller it should ideally look at the temp directory.
I've left the print statements to help you debug. All this does is set the base directory to PyInstaller's temp directory sys._MEIPASS
then form a correct path in line 11 to make sure that VLC can find the modules
and plugins
folders.
vlc darwin replacement code.py | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Easy Solution:
If you want to just have a quick and dirty solution, just modify vlc.py
and compile it. However, now you have to track vlc.py
in your .venv
, and if you're ignoring your .venv
folder through git, you're gonna have a BAD time when the .venv
inevitably gets lost or remade.
Correct Solution:
Make a github issue on vlc-python's github (TBD).
Interim Solution:
Monkeypatching this code. This is significantly harder because not only does it have to be fixed, it has to be fixed while frozen in PyInstaller.
- Solution #1 Attempt this solution: Modifying imported source code on the fly
def modify_and_import(module_name, package, modification_func):
spec = importlib.util.find_spec(module_name, package)
source = spec.loader.get_source(module_name)
new_source = modification_func(source)
print("new source changed?", type(source), new_source)
print("check str1 in ", str1 in new_source, str2 in new_source)
module = importlib.util.module_from_spec(spec) #this is always the killer line, because vlc runs find_lib() immediately AKA it explodes (only when packaging .pyc file with PyInstaller)
codeobj = compile(new_source, module.__spec__.origin, 'exec')
exec(codeobj, module.__dict__)
sys.modules[module_name] = module
return module
#some time later...
my_module = modify_and_import("vlc", None, lambda src: src.replace(str1, str2))
import vlc
- Problem #1
PyInstaller by default packages .pyc, and Python itself chooses .pyc when possible. This will force a few lines to fail:
source = spec.loader.get_source(module_name)
This will return either: None
, OSError: could not get source code
. This is because .pyc files don't really provide source code.
module = importlib.util.module_from_spec(spec)
vlc.pyc
will force vlc
to run the broken find_lib()
function too early, and force a crash.
- Solution #1:
Set PyInstaller's module_collection_mode
to py
in the .specfile
module_collection_mode={
'vlc': 'py',
}
a = Analysis(
, after noarchive=False,
. This will force PyInstaller to collect only vlc.py
and NOT vlc.pyc
. This will then make source = spec.loader.get_source(module_name)
return actual source code, and module = importlib.util.module_from_spec(spec)
to run without executing find_lib()
too early.
Note on src: src.replace(str1, str2)
:
This is just a string replace that replaces str1 with str2. There is room for optimization by only replacing the changed parts but dealing with matching newlines is a pain on its own. Here I just replaced the old function definition with the new function definition
str1 = r'''
def find_lib():
dll = None
plugin_path = os.environ.get('PYTHON_VLC_MODULE_PATH', None)
if 'PYTHON_VLC_LIB_PATH' in os.environ:
try:
dll = ctypes.CDLL(os.environ['PYTHON_VLC_LIB_PATH'])
except OSError:
logger.error("Cannot load lib specified by PYTHON_VLC_LIB_PATH env. variable")
sys.exit(1)
if plugin_path and not os.path.isdir(plugin_path):
logger.error("Invalid PYTHON_VLC_MODULE_PATH specified. Please fix.")
sys.exit(1)
if dll is not None:
return dll, plugin_path
if sys.platform.startswith('win'):
libname = 'libvlc.dll'
p = find_library(libname)
if p is None:
try: # some registry settings
# leaner than win32api, win32con
if PYTHON3:
import winreg as w
else:
import _winreg as w
for r in w.HKEY_LOCAL_MACHINE, w.HKEY_CURRENT_USER:
try:
r = w.OpenKey(r, 'Software\\VideoLAN\\VLC')
plugin_path, _ = w.QueryValueEx(r, 'InstallDir')
w.CloseKey(r)
break
except w.error:
pass
except ImportError: # no PyWin32
pass
if plugin_path is None:
# try some standard locations.
programfiles = os.environ["ProgramFiles"]
homedir = os.environ["HOMEDRIVE"]
for p in ('{programfiles}\\VideoLan{libname}', '{homedir}:\\VideoLan{libname}',
'{programfiles}{libname}', '{homedir}:{libname}'):
p = p.format(homedir = homedir,
programfiles = programfiles,
libname = '\\VLC\\' + libname)
if os.path.exists(p):
plugin_path = os.path.dirname(p)
break
if plugin_path is not None: # try loading
# PyInstaller Windows fix
if 'PyInstallerCDLL' in ctypes.CDLL.__name__:
ctypes.windll.kernel32.SetDllDirectoryW(None)
p = os.getcwd()
os.chdir(plugin_path)
# if chdir failed, this will raise an exception
dll = ctypes.CDLL('.\\' + libname)
# restore cwd after dll has been loaded
os.chdir(p)
else: # may fail
dll = ctypes.CDLL('.\\' + libname)
else:
plugin_path = os.path.dirname(p)
dll = ctypes.CDLL(p)
elif sys.platform.startswith('darwin'):
# FIXME: should find a means to configure path
d = '/Applications/VLC.app/Contents/MacOS/'
c = d + 'lib/libvlccore.dylib'
p = d + 'lib/libvlc.dylib'
if os.path.exists(p) and os.path.exists(c):
# pre-load libvlccore VLC 2.2.8+
ctypes.CDLL(c)
dll = ctypes.CDLL(p)
for p in ('modules', 'plugins'):
p = d + p
if os.path.isdir(p):
plugin_path = p
break
else: # hope, some [DY]LD_LIBRARY_PATH is set...
# pre-load libvlccore VLC 2.2.8+
ctypes.CDLL('libvlccore.dylib')
dll = ctypes.CDLL('libvlc.dylib')
else:
# All other OSes (linux, freebsd...)
p = find_library('vlc')
try:
dll = ctypes.CDLL(p)
except OSError: # may fail
dll = None
if dll is None:
try:
dll = ctypes.CDLL('libvlc.so.5')
except:
raise NotImplementedError('Cannot find libvlc lib')
return (dll, plugin_path)
'''
str2 = r'''
def find_lib():
dll = None
plugin_path = os.environ.get('PYTHON_VLC_MODULE_PATH', None)
if 'PYTHON_VLC_LIB_PATH' in os.environ:
try:
dll = ctypes.CDLL(os.environ['PYTHON_VLC_LIB_PATH'])
except OSError:
logger.error("Cannot load lib specified by PYTHON_VLC_LIB_PATH env. variable")
sys.exit(1)
if plugin_path and not os.path.isdir(plugin_path):
logger.error("Invalid PYTHON_VLC_MODULE_PATH specified. Please fix.")
sys.exit(1)
if dll is not None:
return dll, plugin_path
if sys.platform.startswith('win'):
libname = 'libvlc.dll'
p = find_library(libname)
if p is None:
try: # some registry settings
# leaner than win32api, win32con
if PYTHON3:
import winreg as w
else:
import _winreg as w
for r in w.HKEY_LOCAL_MACHINE, w.HKEY_CURRENT_USER:
try:
r = w.OpenKey(r, 'Software\\VideoLAN\\VLC')
plugin_path, _ = w.QueryValueEx(r, 'InstallDir')
w.CloseKey(r)
break
except w.error:
pass
except ImportError: # no PyWin32
pass
if plugin_path is None:
# try some standard locations.
programfiles = os.environ["ProgramFiles"]
homedir = os.environ["HOMEDRIVE"]
for p in ('{programfiles}\\VideoLan{libname}', '{homedir}:\\VideoLan{libname}',
'{programfiles}{libname}', '{homedir}:{libname}'):
p = p.format(homedir = homedir,
programfiles = programfiles,
libname = '\\VLC\\' + libname)
if os.path.exists(p):
plugin_path = os.path.dirname(p)
break
if plugin_path is not None: # try loading
# PyInstaller Windows fix
if 'PyInstallerCDLL' in ctypes.CDLL.__name__:
ctypes.windll.kernel32.SetDllDirectoryW(None)
p = os.getcwd()
os.chdir(plugin_path)
# if chdir failed, this will raise an exception
dll = ctypes.CDLL('.\\' + libname)
# restore cwd after dll has been loaded
os.chdir(p)
else: # may fail
dll = ctypes.CDLL('.\\' + libname)
else:
plugin_path = os.path.dirname(p)
dll = ctypes.CDLL(p)
elif sys.platform.startswith('darwin'):
d = sys._MEIPASS
c = os.path.join(d, "libvlccore.dylib")
p = os.path.join(d, "libvlc.dylib")
print("paths exists and loaded?", c, p, os.path.exists(p), os.path.exists(c))
if os.path.exists(p) and os.path.exists(c):
# pre-load libvlccore VLC 2.2.8+
ctypes.CDLL(c)
dll = ctypes.CDLL(p)
for p in ('modules', 'plugins'):
p = os.path.join(d, p)
print("newp?", p)
if os.path.isdir(p):
plugin_path = p
print("pluginpath", plugin_path, os.path.exists(plugin_path))
break
else: # hope, some [DY]LD_LIBRARY_PATH is set...
# pre-load libvlccore VLC 2.2.8+
ctypes.CDLL('libvlccore.dylib')
dll = ctypes.CDLL('libvlc.dylib')
else:
# All other OSes (linux, freebsd...)
p = find_library('vlc')
try:
dll = ctypes.CDLL(p)
except OSError: # may fail
dll = None
if dll is None:
try:
dll = ctypes.CDLL('libvlc.so.5')
except:
raise NotImplementedError('Cannot find libvlc lib')
return (dll, plugin_path)
'''
Another note: it's actually lucky that vlc.py
is a monolith py file and doesn't really have any external dependencies to package besides 2 dylib files. This approach might be harder for larger modules and/or there will be other loading issues.
Example basicpyvlc.py
#vlc has a problem with mac displaying, see
#problem description: https://github.com/PySimpleGUI/PySimpleGUI/issues/5581
#solution: https://stackoverflow.com/a/75022685
import os
import PySide6.QtWidgets as QtWidgets
import sys
str1 = r'''
def find_lib():
dll = None
plugin_path = os.environ.get('PYTHON_VLC_MODULE_PATH', None)
if 'PYTHON_VLC_LIB_PATH' in os.environ:
try:
dll = ctypes.CDLL(os.environ['PYTHON_VLC_LIB_PATH'])
except OSError:
logger.error("Cannot load lib specified by PYTHON_VLC_LIB_PATH env. variable")
sys.exit(1)
if plugin_path and not os.path.isdir(plugin_path):
logger.error("Invalid PYTHON_VLC_MODULE_PATH specified. Please fix.")
sys.exit(1)
if dll is not None:
return dll, plugin_path
if sys.platform.startswith('win'):
libname = 'libvlc.dll'
p = find_library(libname)
if p is None:
try: # some registry settings
# leaner than win32api, win32con
if PYTHON3:
import winreg as w
else:
import _winreg as w
for r in w.HKEY_LOCAL_MACHINE, w.HKEY_CURRENT_USER:
try:
r = w.OpenKey(r, 'Software\\VideoLAN\\VLC')
plugin_path, _ = w.QueryValueEx(r, 'InstallDir')
w.CloseKey(r)
break
except w.error:
pass
except ImportError: # no PyWin32
pass
if plugin_path is None:
# try some standard locations.
programfiles = os.environ["ProgramFiles"]
homedir = os.environ["HOMEDRIVE"]
for p in ('{programfiles}\\VideoLan{libname}', '{homedir}:\\VideoLan{libname}',
'{programfiles}{libname}', '{homedir}:{libname}'):
p = p.format(homedir = homedir,
programfiles = programfiles,
libname = '\\VLC\\' + libname)
if os.path.exists(p):
plugin_path = os.path.dirname(p)
break
if plugin_path is not None: # try loading
# PyInstaller Windows fix
if 'PyInstallerCDLL' in ctypes.CDLL.__name__:
ctypes.windll.kernel32.SetDllDirectoryW(None)
p = os.getcwd()
os.chdir(plugin_path)
# if chdir failed, this will raise an exception
dll = ctypes.CDLL('.\\' + libname)
# restore cwd after dll has been loaded
os.chdir(p)
else: # may fail
dll = ctypes.CDLL('.\\' + libname)
else:
plugin_path = os.path.dirname(p)
dll = ctypes.CDLL(p)
elif sys.platform.startswith('darwin'):
# FIXME: should find a means to configure path
d = '/Applications/VLC.app/Contents/MacOS/'
c = d + 'lib/libvlccore.dylib'
p = d + 'lib/libvlc.dylib'
if os.path.exists(p) and os.path.exists(c):
# pre-load libvlccore VLC 2.2.8+
ctypes.CDLL(c)
dll = ctypes.CDLL(p)
for p in ('modules', 'plugins'):
p = d + p
if os.path.isdir(p):
plugin_path = p
break
else: # hope, some [DY]LD_LIBRARY_PATH is set...
# pre-load libvlccore VLC 2.2.8+
ctypes.CDLL('libvlccore.dylib')
dll = ctypes.CDLL('libvlc.dylib')
else:
# All other OSes (linux, freebsd...)
p = find_library('vlc')
try:
dll = ctypes.CDLL(p)
except OSError: # may fail
dll = None
if dll is None:
try:
dll = ctypes.CDLL('libvlc.so.5')
except:
raise NotImplementedError('Cannot find libvlc lib')
return (dll, plugin_path)
'''
str2 = r'''
def find_lib():
dll = None
plugin_path = os.environ.get('PYTHON_VLC_MODULE_PATH', None)
if 'PYTHON_VLC_LIB_PATH' in os.environ:
try:
dll = ctypes.CDLL(os.environ['PYTHON_VLC_LIB_PATH'])
except OSError:
logger.error("Cannot load lib specified by PYTHON_VLC_LIB_PATH env. variable")
sys.exit(1)
if plugin_path and not os.path.isdir(plugin_path):
logger.error("Invalid PYTHON_VLC_MODULE_PATH specified. Please fix.")
sys.exit(1)
if dll is not None:
return dll, plugin_path
if sys.platform.startswith('win'):
libname = 'libvlc.dll'
p = find_library(libname)
if p is None:
try: # some registry settings
# leaner than win32api, win32con
if PYTHON3:
import winreg as w
else:
import _winreg as w
for r in w.HKEY_LOCAL_MACHINE, w.HKEY_CURRENT_USER:
try:
r = w.OpenKey(r, 'Software\\VideoLAN\\VLC')
plugin_path, _ = w.QueryValueEx(r, 'InstallDir')
w.CloseKey(r)
break
except w.error:
pass
except ImportError: # no PyWin32
pass
if plugin_path is None:
# try some standard locations.
programfiles = os.environ["ProgramFiles"]
homedir = os.environ["HOMEDRIVE"]
for p in ('{programfiles}\\VideoLan{libname}', '{homedir}:\\VideoLan{libname}',
'{programfiles}{libname}', '{homedir}:{libname}'):
p = p.format(homedir = homedir,
programfiles = programfiles,
libname = '\\VLC\\' + libname)
if os.path.exists(p):
plugin_path = os.path.dirname(p)
break
if plugin_path is not None: # try loading
# PyInstaller Windows fix
if 'PyInstallerCDLL' in ctypes.CDLL.__name__:
ctypes.windll.kernel32.SetDllDirectoryW(None)
p = os.getcwd()
os.chdir(plugin_path)
# if chdir failed, this will raise an exception
dll = ctypes.CDLL('.\\' + libname)
# restore cwd after dll has been loaded
os.chdir(p)
else: # may fail
dll = ctypes.CDLL('.\\' + libname)
else:
plugin_path = os.path.dirname(p)
dll = ctypes.CDLL(p)
elif sys.platform.startswith('darwin'):
d = sys._MEIPASS
c = os.path.join(d, "libvlccore.dylib")
p = os.path.join(d, "libvlc.dylib")
print("paths exists and loaded?", c, p, os.path.exists(p), os.path.exists(c))
if os.path.exists(p) and os.path.exists(c):
# pre-load libvlccore VLC 2.2.8+
ctypes.CDLL(c)
dll = ctypes.CDLL(p)
for p in ('modules', 'plugins'):
p = os.path.join(d, p)
print("newp?", p)
if os.path.isdir(p):
plugin_path = p
print("pluginpath", plugin_path, os.path.exists(plugin_path))
break
else: # hope, some [DY]LD_LIBRARY_PATH is set...
# pre-load libvlccore VLC 2.2.8+
ctypes.CDLL('libvlccore.dylib')
dll = ctypes.CDLL('libvlc.dylib')
else:
# All other OSes (linux, freebsd...)
p = find_library('vlc')
try:
dll = ctypes.CDLL(p)
except OSError: # may fail
dll = None
if dll is None:
try:
dll = ctypes.CDLL('libvlc.so.5')
except:
raise NotImplementedError('Cannot find libvlc lib')
return (dll, plugin_path)
'''
import importlib
# https://stackoverflow.com/questions/41858147/how-to-modify-imported-source-code-on-the-fly
def modify_and_import(module_name, package, modification_func):
spec = importlib.util.find_spec(module_name, package)
source = spec.loader.get_source(module_name)
new_source = modification_func(source)
print("new source changed?", type(source), new_source)
print("check str1 in ", str1 in new_source, str2 in new_source)
module = importlib.util.module_from_spec(spec) #this is always the killer line, because vlc runs find_lib() immediately AKA it explodes (only when packaging .pyc file with PyInstaller)
codeobj = compile(new_source, module.__spec__.origin, 'exec')
exec(codeobj, module.__dict__)
sys.modules[module_name] = module
return module
print("trying mod!", flush = True)
#checking to see if pyinstaller module_collection_mode py sends the py file to tmpdir
my_module = modify_and_import("vlc", None, lambda src: src.replace(str1, str2))
import vlc
vlc_player = vlc.MediaPlayer()
medianame = "bigbuckbunny x265.mp4"
mediapath = os.path.join(os.path.dirname(__file__), medianame)
media = vlc.Media(mediapath)
vlc_player.set_media(media)
vlcApp = QtWidgets.QApplication([])
vlcWidget = QtWidgets.QFrame()
vlcWidget.resize(700,700)
vlcWidget.show()
vlc_player.set_nsobject(vlcWidget.winId())
vlc_player.play() #you need to play vlc first else the qtapp will just open and hold forever
vlcApp.exec()
Example basicvlcMACVLCUninstalled.spec
basicvlcMACVLCUninstalled.spec | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
|
Now the last step is to fix the locations of pathex=
, binaries=
, a.binaries=
, and add the bundle line in the .specfile.
pathex=
wants the location of VLC.app
, it can be found by searching through Finder.
binaries=
wants the location of the plugins
folder, it can be found by opening up VLC.app
by CTRL+CLICK
on it, then click Open Package Contents
. Then in Contents
you will find the /MacOS/plugins/
folder.
a.binaries=
wants the location of two files: libvlc.dylib
and libvlccore.dylib
. Those can be found in the VLC.app/Contents/MacOS/lib/
folder after opening up VLC.app
using CTRL+CLICK
and Open Package Contents
.
Now with those changes, compile the .app with this command
£(basicpyvlc-py3.10)/Users/KivySchool/CODING/BasicPyVLC python -m PyInstaller basicvlcMACVLCUninstalled.spec --clean
Then on your machine with VLC.app removed from the Application folder, it will work:
Since this was done with VLC.app
removed from the Applications
folder, it will also work on target Mac machines that do not have VLC.app
installed.
Once done, double click and run. If successful, your mediafile should play.
After these changes, put back Vlc.app
to the Applications
folder and fix filepaths accordingly.
TIPS¶
- On Windows, you can run .exe in a
cmd
window by typing something likeBasicPyVLC.exe
when your current working directory contains BasicPyVLC.exe. That way if it shows up and disappears you can still see the logs and dont have to screen record/print screen/add time.sleep code.
Example Git repo¶
Sources¶
VLC PyInstaller for Windows/Mac
Modifying imported source code on the fly
Python itself chooses .pyc when possible
Set PyInstaller's module_collection_mode
to py
inspect.getmodule fails for pyinstaller executable
Inspect fails to retrieve source code inside frozen app (.py files included)
Problem with inspect using PyInstaller; can get source of class but not function
PyInstaller cannot find libvlc dll
EXE made with PyInstaller can't load libvlc.dll (python-vlc)
Import Error on MacOS (vlc github issue)
Article Error Reporting¶
Message @BadMetrics on the Kivy Discord.