Skip to content

Mac

Why use the screen module?

Kivy's screen module is handy for emulating different screen sizes and dpis. This means that by using your dev machine, you can check out what your Kivy application would look like on other devices without having them. This is preferable to just guessing what your app would look like, but is in no way a replacement for actually having a test device.

Using Kivy's screen module to emulate devices

In this blog post, I will assume that you already followed the Tkinter VS Kivy Tutorial Quickstart and the How to install Kivy installation guide for your OS to install Kivy and an example project.

There is no special install or setup to use the screen module. It is built-in when you have installed Kivy.

To check it out, change directory with cd to the correct folder.

Make sure your virtual environment is setup with poetry shell!

Run this command:

$ python main.py -m screen:s3

How to the module command works:

First you call python and the script name: main.py

The -m means that you are running a Python module.

screen:s3 means that you are running the screen module with argument s3.

Examples on Windows, try them out!

Emulating an s3 screen.

Command:

$ python main.py -m screen:s3

Emulating an iphone6 screen.

Command:

$ python main.py -m screen:phone_iphone_6

How to add a custom device

To add your own device, go to the screen.py module in Kivy. If you have followed our install tutorial, you can simply to go your .venv folder, find kivy, and then go here:

.venv/lib/site-packages/kivy/modules/screen.py

I did not follow any tutorial! How do I find my environment?

If you did not follow any tutorial, you can find your environment with Python only.

Reference: Finding your Python env

Steps:

Type python in your terminal.

Type import sys. Press Enter

Type print(sys.prefix). Press Enter.

That folder will be where your Kivy install is, and subsequently your screen module as well.

Next, to add a custom device, follow the format of the devices dictionary, and add your device as follows:

$ 'fake_device_name': ('my_device_name', width, height, dpi, density),

Next, call your custom device!

Command:

$ python main.py -m screen:fake_device

Examples on MacOS, try them out!

Emulating an s3 screen.

Command:

$ python main.py -m screen:s3

Emulating an iphone6 screen.

Command:

$ python main.py -m screen:phone_iphone_6

How to add a custom device

To add your own device, go to the screen.py module in Kivy. If you have followed our install tutorial, you can simply to go your .venv folder, find kivy, and then go here:

.venv/lib/site-packages/kivy/modules/screen.py

I did not follow any tutorial! How do I find my environment?

If you did not follow any tutorial, you can find your environment with Python only.

Reference: Finding your Python env

Steps:

Type python in your terminal.

Type import sys. Press Enter

Type print(sys.prefix). Press Enter.

That folder will be where your Kivy install is, and subsequently your screen module as well.

Next, to add a custom device, follow the format of the devices dictionary, and add your device as follows:

$ 'fake_device_name': ('my_device_name', width, height, dpi, density),

Next, call your custom device!

Command:

$ python main.py -m screen:fake_device

On the Mac M1, you may need to use the scale command to get the screen to be the correct size:

Other relevant commands: portrait and scale

Other commands to try out:

Portrait mode:

$ python main.py -m screen:phone_samsung_galaxy_s5,portrait

Portrait and scale:

$ python main.py -m screen:tablet_nexus_7_13,portrait,scale=.75

Where to find screen data?

https://www.sven.de/dpi/

  • has a list of common dpis and phone resolutions

Article Error Reporting

Message @BadMetrics on the Kivy Discord.

Tkinter VS Kivy Tutorial Quickstart: Which one is right for you?

Why Use Tkinter or Kivy In the First Place?

Tkinter and Kivy are both cross platform GUIs that are commonly used by Python users. There are other frameworks out there but they are either: in another programming language, not available on all platforms, or a combination of both.

In this blog post, you will go through the hello world steps of both GUI interfaces on as much platforms as possible to get a feel for which one is right for you.

The only real test is to try them out!

Before following this guide, you need these tools and libraries:

  • Python

These are helpful, but not required for this tutorial:

  • Git
  • Pyenv
  • Poetry
  • VSCode or editor of your choice

If you do not see any of the below sections, please click to show.

main.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from kivy.app import runTouchApp
from kivy.lang import Builder
from kivy.core.window import Window

#this is to make the Kivy window always on top
Window.always_on_top = True
#set the window title
Window.set_title('Welcome to Kivy School!')

#kv language setting the main widget to be a button
kvString = '''
Button:
    text: "Hello world!"
'''

#run Kivy app
runTouchApp(Builder.load_string(kvString))

Kivy Win Step 1: Get the repo from github

You can do this in two ways: git clone https://github.com/kivy-school/kivyhw or download the zip file and unzip it somewhere.

How do I git clone?
  • Press the ⊞ Windows key and type cmd to open command prompt.

  • If git is installed, then change directory with the cd command to the correct folder you want to work in.
  • dir in Windows shows you the contents of the current directory.
  • cd changes to the directory you want.
  • cd .. goes up 1 directory level.

Then type git clone https://github.com/kivy-school/kivyhw

Then cd kivyhw to enter the project.

Kivy Win Step 2: Install libraries

If poetry is installed:
  • Type: poetry update in the kivyhw directory. Poetry will install everything for you and AUTOSOLVE VERSION CONFLICTS, plus provide you with a virtual environment so you don't break your Windows system Python.
  • Type poetry shell to enter the virtual environment and proceed to the next step.
If poetry is not installed:
  • If poetry is not installed, you can still install requirements using pip. Be warned! If you make any mistakes with any sort of pip install, you might end up breaking your Windows Python installation and have to reinstall it over again to fix your environment. Because this is just a Hello World tutorial, you do not need to worry about it now. If you have also followed other tutorials, you might get version conflicts for libraries. This is because pip normally installs every library to your system Python unless specified. That means that conflicting versions of certifi will appear and you will have to manually resolve conflicts. If so, please try installing python-poetry and use virtual environments for each project (like the one Kivy School is providing in this tutorial with the poetry shell command).

  • In the kivyhw folder (NOT kivyhw/kivyhw folder!) type: pip install -r requirements.txt

  • Proceed to the next step.

Kivy Win Step 3: Run hello world!

There are two options to run the kivy app:

From the top level kivy hw folder, run this command: python kivyhw/main.py

It is also possible to cd kivyhw and run python main.py

If poetry update was used in installation, you also have access to: task run in the top level kivyhw folder courtesy of these lines in the pyproject.toml and the taskipy library:

[tool.taskipy.tasks]
run = 'python kivyhw/main.py'

main.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import tkinter as tk

# Create the main window
root = tk.Tk()
# Set window title
root.title("Welcome to Kivy School!")
# Set min window size
root.minsize(450, 100)
# Set window to always be on top (remove this line for a regular GUI)
root.attributes('-topmost',True)

# Create label
label = tk.Label(root, text="Hello, World!")

# Lay out label
label.pack()

# Run forever!
root.mainloop()

Tkinter Win Step 1: Get the repo from github

You can do this in two ways: git clone https://github.com/kivy-school/tkinterhw or download the zip file and unzip it somewhere.

How do I git clone?
  • Press the ⊞ Windows key and type cmd to open command prompt.

  • If git is installed, then change directory with the cd command to the correct folder you want to work in.
  • dir in Windows shows you the contents of the current directory.
  • cd changes to the directory you want.
  • cd .. goes up 1 directory level.

Then type git clone https://github.com/kivy-school/tkinterhw

Then cd tkinterhw to enter the project.

Tkinter Win Step 2: Install libraries

If poetry is installed:
  • Type: poetry update in the tkinterhw directory. Poetry will install everything for you and AUTOSOLVE VERSION CONFLICTS, plus provide you with a virtual environment so you don't break your Windows system Python.
  • Type poetry shell to enter the virtual environment and proceed to the next step.
If poetry is not installed:
  • If poetry is not installed, you can still install requirements using pip. Be warned! If you make any mistakes with any sort of pip install, you might end up breaking your Windows Python installation and have to reinstall it over again to fix your environment. Because this is just a Hello World tutorial, you do not need to worry about it now. If you have also followed other tutorials, you might get version conflicts for libraries. This is because pip normally installs every library to your system Python unless specified. That means that conflicting versions of certifi will appear and you will have to manually resolve conflicts. If so, please try installing python-poetry and use virtual environments for each project (like the one Kivy School is providing in this tutorial with the poetry shell command).

  • In the tkinterhw folder (NOT tkinterhw/tkinterhw folder!) type: pip install -r requirements.txt

  • Proceed to the next step.

Tkinter Win Step 3: Run hello world!

There are two options to run the kivy app:

From the top level tkinterhw folder, run this command: python tkinterhw/main.py

It is also possible to cd tkinterhw and run python main.py

If poetry update was used in installation, you also have access to: task run in the top level tkinterhw folder courtesy of these lines in the pyproject.toml and the taskipy library:

[tool.taskipy.tasks]
run = 'python tkinterhw/main.py'

main.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from kivy.app import runTouchApp
from kivy.lang import Builder
from kivy.core.window import Window

#this is to make the Kivy window always on top
Window.always_on_top = True
#set the window title
Window.set_title('Welcome to Kivy School!')

#kv language setting the main widget to be a button
kvString = '''
Button:
    text: "Hello world!"
'''

#run Kivy app
runTouchApp(Builder.load_string(kvString))

Kivy Mac Step 1: Get the repo from github

You can do this in two ways: git clone https://github.com/kivy-school/kivyhw or download the zip file and unzip it somewhere.

How do I git clone?
  • Press Command ⌘ + Spacebar and type terminal to open terminal.app

  • If git is installed, then change directory with the cd command to the correct folder you want to work in.
  • ls -l in Mac shows you the contents of the current directory.
  • cd changes to the directory you want.
  • cd .. goes up 1 directory level.

Then type git clone https://github.com/kivy-school/kivyhw

Then cd kivyhw to enter the project.

Kivy Mac Step 2: Install libraries

If poetry is installed:
  • Type: poetry update in the kivyhw directory. Poetry will install everything for you and AUTOSOLVE VERSION CONFLICTS, plus provide you with a virtual environment so you don't break your Mac system Python.
  • Type poetry shell to enter the virtual environment and proceed to the next step.
If poetry is not installed:
  • If poetry is not installed, you can still install requirements using pip. Be warned! If you make any mistakes with any sort of pip install, you might end up breaking your Mac Python installation and have to reinstall it over again to fix your environment. Because this is just a Hello World tutorial, you do not need to worry about it now. If you have also followed other tutorials, you might get version conflicts for libraries. This is because pip normally installs every library to your system Python unless specified. That means that conflicting versions of certifi will appear and you will have to manually resolve conflicts. If so, please try installing python-poetry and use virtual environments for each project (like the one Kivy School is providing in this tutorial with the poetry shell command).

  • In the kivyhw folder (NOT kivyhw/kivyhw folder!) type: pip install -r requirements.txt

  • Proceed to the next step.
Warning

If you cannot see the .venv folder in the kivyhw, that is because your Mac Finder settings are set to hide hidden folders.

  • To toggle seeing hidden folders like .git and .venv, use the command: Command ⌘ + Shift + .

  • Without hidden folders:

  • With hidden folders:

Kivy Mac Step 3: Run hello world!

There are two options to run the kivy app:

From the top level kivy hw folder, run this command: python kivyhw/main.py

It is also possible to cd kivyhw and run python main.py

If poetry update was used in installation, you also have access to: task run in the top level kivyhw folder courtesy of these lines in the pyproject.toml and the taskipy library:

[tool.taskipy.tasks]
run = 'python kivyhw/main.py'

main.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import tkinter as tk

# Create the main window
root = tk.Tk()
# Set window title
root.title("Welcome to Kivy School!")
# Set min window size
root.minsize(450, 100)
# Set window to always be on top (remove this line for a regular GUI)
root.attributes('-topmost',True)

# Create label
label = tk.Label(root, text="Hello, World!")

# Lay out label
label.pack()

# Run forever!
root.mainloop()

TKinter Mac Step 1: Get the repo from github

You can do this in two ways: git clone https://github.com/kivy-school/tkinterhw or download the zip file and unzip it somewhere.

How do I git clone?
  • Press Command ⌘ + Spacebar and type terminal to open terminal.app

  • If git is installed, then change directory with the cd command to the correct folder you want to work in.
  • dir in Windows shows you the contents of the current directory.
  • cd changes to the directory you want.
  • cd .. goes up 1 directory level.

Then type git clone https://github.com/kivy-school/tkinterhw

Then cd tkinterhw to enter the project.

TKinter Mac Step 2: Install libraries

If poetry is installed:
  • Type: poetry update in the tkinterhw directory. Poetry will install everything for you and AUTOSOLVE VERSION CONFLICTS, plus provide you with a virtual environment so you don't break your Mac system Python.
  • Type poetry shell to enter the virtual environment and proceed to the next step.
If poetry is not installed:
  • If poetry is not installed, you can still install requirements using pip. Be warned! If you make any mistakes with any sort of pip install, you might end up breaking your Mac Python installation and have to reinstall it over again to fix your environment. Because this is just a Hello World tutorial, you do not need to worry about it now. If you have also followed other tutorials, you might get version conflicts for libraries. This is because pip normally installs every library to your system Python unless specified. That means that conflicting versions of certifi will appear and you will have to manually resolve conflicts. If so, please try installing python-poetry and use virtual environments for each project (like the one Kivy School is providing in this tutorial with the poetry shell command).

  • In the tkinterhw folder (NOT tkinterhw/tkinterhw folder!) type: pip install -r requirements.txt

  • Proceed to the next step.
Warning

If you cannot see the .venv folder in the kivyhw, that is because your Mac Finder settings are set to hide hidden folders.

  • To toggle seeing hidden folders like .git and .venv, use the command: Command ⌘ + Shift + .

  • Without hidden folders:

  • With hidden folders:

TKinter Mac Step 3: Run hello world!

There are two options to run the kivy app:

From the top level tkinterhw folder, run this command: python tkinterhw/main.py

It is also possible to cd tkinterhw and run python main.py

If poetry update was used in installation, you also have access to: task run in the top level tkinterhw folder courtesy of these lines in the pyproject.toml and the taskipy library:

[tool.taskipy.tasks]
run = 'python tkinterhw/main.py'

Why this project folder setup?

Most tutorials just give you the Python code in 1 folder.

In a real project, there are a lot of project management files that are simply not part of the main Python code and should NEVER be shipped, like hidden secrets. In this example, there are many things that should not be given to an end user, like the .git folder, .venv virtual environment provided by Poetry, .gitignore used by git, poetry.lock and pyproject.toml used by poetry, a github README.md file and requirements.txt used by pip.

Those files are for development only! That is why the management files are in the top level, and the actual Python project is inside an inner folder.

You can see for yourself by inspecting these popular Python libraries:

Kivy

numpy

fastapi

Article Error Reporting

Message @BadMetrics on the Kivy Discord.

Adding VLC to PyInstaller on Mac and Windows

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.