Skip to content

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 enter cd "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 
- This shortcut will open a terminal for you at the bottom of VSCode
CTRL + J

  • ⊞ Windows key > type cmd > press Enter

  • Change drive if necessary (The default is C:\ drive but if you want to go to D:\ 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 and PyInstaller.
    £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 and dist 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
import vlc
import os
import time

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)
vlc_player.play()
offset = 5
time.sleep(offset)
length = vlc_player.get_length() #time in ms (divide by 1000 to get in seconds)
newoffset = length/1000 - offset
time.sleep(newoffset)

Basic imports

basicpyvlc.py
1
2
3
import vlc
import os
import time
Initialize vlc
basicpyvlc.py
1
vlc_player = vlc.MediaPlayer() 
Set name of mediafile
basicpyvlc.py
1
medianame = "bigbuckbunny x265.mp4"
Here there are some filesystem tricks.

__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
mediapath = os.path.join(os.path.dirname(__file__), medianame)
Set media and play
basicpyvlc.py
1
2
3
media = vlc.Media(mediapath)
vlc_player.set_media(media)
vlc_player.play()
This is some timing code so that VLC plays. This is because without the initial 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
offset = 5
time.sleep(offset)
length = vlc_player.get_length() #time in ms (divide by 1000 to get in seconds)
newoffset = length/1000 - offset
time.sleep(newoffset)

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
# -*- mode: python ; coding: utf-8 -*-

medianame = "bigbuckbunny x265.mp4"

a = Analysis(
    ['basicpyvlc.py'],
    pathex=["D:\KivySchool\VLC"],
    binaries=[("D:\KivySchool\VLC\plugins\*", "plugins")],
    datas=[(medianame, ".")],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    noarchive=False,
)
pyz = PYZ(a.pure)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries + [("libVLC.dll", "D:\KivySchool\VLC\libvlc.dll", "BINARY")],
    a.datas,
    [],
    name='basicvlc',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=True,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)

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
After it's finished, check the exe out in your 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, type Finder)
  • Go to the folder location
  • CTRL+CLICK, then press New 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, press Enter

  • Use VSCode open folder option There is not open folder shortcut on Mac, you gotta do some clicking. Click File then Open Folder

  • This shortcut will open a terminal for you at the bottom of VSCode
    ⌘ + J
    
  • ⌘+SPACE > type terminal > 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 on Where, then Copy 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 and PyInstaller.
    £/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 and dist 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
#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
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()

Basic imports

basicpyvlc.py
1
2
3
4
import os
import PySide6.QtWidgets as QtWidgets
import sys
import vlc
Initialize vlc
basicpyvlc.py
1
vlc_player = vlc.MediaPlayer() 
Set name of mediafile
basicpyvlc.py
1
medianame = "bigbuckbunny x265.mp4"
Here there are some filesystem tricks.

__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
mediapath = os.path.join(os.path.dirname(__file__), medianame)
Set media.
basicpyvlc.py
1
2
media = vlc.Media(mediapath)
vlc_player.set_media(media)
There is a bug with VLC where it has an issue creating a window on MAC.

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
vlcApp = QtWidgets.QApplication([])
vlcWidget = QtWidgets.QFrame()
vlcWidget.resize(700,700)
vlcWidget.show()

Next we give the window to VLCplayer (vlc_player.set_nsobject(vlcWidget.winId())) and then play.

basicpyvlc.py
1
2
3
vlc_player.set_nsobject(vlcWidget.winId())
vlc_player.play() 
vlcApp.exec()
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
# -*- mode: python ; coding: utf-8 -*-

medianame = "bigbuckbunny x265.mp4"

a = Analysis(
    ['basicpyvlc.py'],
    pathex=["/Applications/VLC.app/Contents"], #base VLC.app/Contents path here
    binaries=[("/Applications/VLC.app/Contents/MacOS/plugins/*", "plugins")],
    datas=[(medianame, ".")],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    noarchive=False,
)
pyz = PYZ(a.pure)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.datas,
    [],
    name='basicvlc',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=True,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)

# https://pyinstaller.org/en/stable/spec-files.html#spec-file-options-for-a-macos-bundle
app = BUNDLE(
    exe,
    name='basicpyVLC.app',
    #icon="", #put your icon path here
    bundle_identifier=None,
)

There are three things to do to add VLC to PyInstaller:

  • Add VLC to pathex
  • Add plugins folder
  • Add libvlccore.dylib and libvlc.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
After it's finished, check the .app file in your 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
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

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.

basicpyvlc.py
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)
This line may also fail, this is because loading 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',
}
You add this line at the end of 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
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
# -*- mode: python ; coding: utf-8 -*-

medianame = "bigbuckbunny x265.mp4"

a = Analysis(
    ['basicpyvlc.py'],
    pathex=["/Users/KivySchool/CODING/test/BasicPyVLC/dist/VLC.app/Contents"], #base VLC.app/Contents path here
    binaries=[("/Users/KivySchool/CODING/test/BasicPyVLC/dist/VLC.app/Contents/MacOS/plugins/*", "plugins")],
    datas=[(medianame, ".")],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    noarchive=False,
    #https://github.com/pyinstaller/pyinstaller/issues/7851#issuecomment-1677986648
    module_collection_mode={
        'vlc': 'py',
    }
)
pyz = PYZ(a.pure)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries + [
        ("libvlc.dylib", "/Users/KivySchool/CODING/test/BasicPyVLC/dist/VLC.app/Contents/MacOS/lib/libvlc.dylib", "BINARY"),
        ("libvlccore.dylib", "/Users/KivySchool/CODING/test/BasicPyVLC/dist/VLC.app/Contents/MacOS/lib/libvlccore.dylib", "BINARY")
        ],
    #a.binaries,
    a.datas,
    [],
    name='MACvlcUninstalled',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=True,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)

# https://pyinstaller.org/en/stable/spec-files.html#spec-file-options-for-a-macos-bundle
app = BUNDLE(
    exe,
    name='MacpyVLCUninstalled.app',
    #icon="", #put your icon path here
    bundle_identifier=None,
)

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 like BasicPyVLC.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

Example Git Repo Link Here!

Sources

VLC bindings

VLC PyInstaller for Windows/Mac

PyInstaller Runtime Info

PyInstaller adding data files

Modifying imported source code on the fly

Python itself chooses .pyc when possible

Set PyInstaller's module_collection_mode to py

Can't get source code

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)

Packaging VLC on Windows

Article Error Reporting

Message @BadMetrics on the Kivy Discord.