Skip to content

[Suggestion] Windows API example "winapivlc.py" (for the examples folder) #305

Description

@59de44955ebd

Hi,

I realized that in the examples folder there are examples e.g. for pyobjc and gtk, but none for the (plain) Windows API, i.e. no example that only uses ctypes/libffi in Windows (without depending on third-party modules like PyWin32). In case you want to add such an example, below a 1:1 port of the PyQt5 example pyqt5vlc.py to plain Windows API. Attached a screenshot of it running in Windows 11.

Image

Example winapivlc.py

"""
A simple example for VLC python bindings using the plain Windows API.

Author: https://github.com/59de44955ebd, Berlin
Date: 25.08.2026
"""
from ctypes import *
from ctypes.wintypes import *
import sys

import vlc

########################################
# Additional used Windows API types
########################################
_is_64_bit = sys.maxsize > 2**32
LONG_PTR = c_longlong if _is_64_bit else c_long
UINT_PTR = WPARAM
WNDPROC = WINFUNCTYPE(LONG_PTR, HWND, UINT, WPARAM, LPARAM)

########################################
# Used Windows API structs
########################################
class MINMAXINFO(Structure):
    _fields_ = [
        ("ptReserved", POINT),
        ("ptMaxSize", POINT),
        ("ptMaxPosition", POINT),
        ("ptMinTrackSize", POINT),
        ("ptMaxTrackSize", POINT),
    ]

class OPENFILENAMEW(Structure):
    def __init__(self, *args, **kwargs):
        super(OPENFILENAMEW, self).__init__(*args, **kwargs)
        self.lStructSize = sizeof(OPENFILENAMEW)
    _fields_ = (
        ("lStructSize", DWORD),
        ("hwndOwner", HWND),
        ("hInstance", HINSTANCE),
        ("lpstrFilter", LPWSTR),
        ("lpstrCustomFilter", LPWSTR),
        ("nMaxCustFilter", DWORD),
        ("nFilterIndex", DWORD),
        ("lpstrFile", LPWSTR),
        ("nMaxFile", DWORD),
        ("lpstrFileTitle", LPWSTR),
        ("nMaxFileTitle", DWORD),
        ("lpstrInitialDir", LPCWSTR),
        ("lpstrTitle", LPCWSTR),
        ("Flags", DWORD),
        ("nFileOffset", WORD),
        ("nFileExtension", WORD),
        ("lpstrDefExt", LPCWSTR),
        ("lCustData", LPARAM),
        ("lpfnHook", LPVOID),  # LPOFNHOOKPROC, not used
        ("lpTemplateName", LPCWSTR),
        ("pvReserved", LPVOID),
        ("dwReserved", DWORD),
        ("FlagsEx", DWORD),
    )

class WNDCLASSEXW(Structure):
    def __init__(self, *args, **kwargs):
        super(WNDCLASSEXW, self).__init__(*args, **kwargs)
        self.cbSize = sizeof(self)
    _fields_ = [
        ("cbSize", UINT),
        ("style", UINT),
        ("lpfnWndProc", WNDPROC),
        ("cbClsExtra", INT),
        ("cbWndExtra", INT),
        ("hInstance", HANDLE),
        ("hIcon", HANDLE),
        ("hCursor", HANDLE),
        ("hbrBackground", HANDLE),
        ("lpszMenuName", LPCWSTR),
        ("lpszClassName", LPCWSTR),
        ("hIconSm", HANDLE)
    ]

########################################
# Used Windows API functions
########################################
comdlg32 = windll.comdlg32
comdlg32.GetOpenFileNameW.argtypes = (POINTER(OPENFILENAMEW),)

gdi32 = windll.gdi32
gdi32.CreateFontW.argtypes = (INT, INT, INT, INT, INT, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, LPCWSTR)
gdi32.CreateFontW.restype = HFONT
gdi32.CreateSolidBrush.argtypes = (COLORREF,)
gdi32.CreateSolidBrush.restype = HBRUSH

user32 = windll.user32
user32.AppendMenuW.argtypes = (HWND, UINT, UINT_PTR, LPCWSTR)
user32.CreateWindowExW.argtypes = (DWORD, LPCWSTR, LPCWSTR, DWORD, INT, INT, INT, INT, HWND, HMENU, HINSTANCE, LPVOID)
user32.DefWindowProcW.argtypes = (HWND, UINT, WPARAM, LPARAM)
user32.DispatchMessageW.argtypes = (LPMSG,)
user32.EnableWindow.argytpes = (HWND, BOOL)
user32.GetMessageW.argtypes = (LPMSG, HWND, UINT, UINT)
user32.GetWindowRect.argtypes = (HWND, LPRECT)
user32.InvalidateRect.argtypes = (HWND, LPRECT, BOOL)
user32.IsDialogMessageW.argtypes = (HWND, LPMSG)
user32.IsDialogMessageW.restype = BOOL
user32.SendMessageW.argtypes = (HWND, UINT, LPVOID, LPVOID)
user32.SetWindowPos.argtypes = (HWND, LONG_PTR, INT, INT, INT, INT, UINT)
user32.TranslateMessage.argtypes = (LPMSG,)

########################################
# Used Windows API constants
########################################
ANSI_CHARSET = 0
BN_CLICKED = 0
CLIP_DEFAULT_PRECIS = 0
COLOR_3DFACE = 15
CS_HREDRAW = 2
CS_VREDRAW = 1
CW_USEDEFAULT = -2147483648
DEFAULT_PITCH = 0
DEFAULT_QUALITY = 0
FF_DONTCARE = 0
FW_DONTCARE = 0
IDC_ARROW = 32512
MAX_PATH = 260
MF_POPUP = 16
MF_STRING = 0
OFN_ENABLESIZING = 8388608
OFN_PATHMUSTEXIST = 2048
OUT_TT_PRECIS = 4
SWP_NOMOVE = 2
SWP_NOSIZE = 1
SW_SHOWNORMAL = 1
TBM_SETPAGESIZE = 1045
TBM_SETPOS = 1029
TBM_SETRANGE = 1030
TBM_SETRANGEMAX = 1032
TB_ENDTRACK = 8
TB_PAGEDOWN = 3
TB_PAGEUP = 2
WC_BUTTON = "Button"
WC_TRACKBAR = "msctls_trackbar32"
WM_CLOSE = 16
WM_COMMAND = 273
WM_GETMINMAXINFO = 36
WM_HSCROLL = 276
WM_SETFONT = 48
WM_SIZE = 5
WM_TIMER = 275
WS_CHILD = 1073741824
WS_DISABLED = 134217728
WS_OVERLAPPEDWINDOW = 13565952
WS_VISIBLE = 268435456


class Player():
    """A simple Media Player using VLC and Windows API"""

    def __init__(self, master=None):

        # Create a "menubar"
        self.h_menu = user32.CreateMenu()
        hmenu_child = user32.CreateMenu()
        user32.AppendMenuW(self.h_menu, MF_POPUP, hmenu_child, "&File")
        user32.AppendMenuW(hmenu_child, MF_STRING, 1, "Load Video")
        user32.AppendMenuW(hmenu_child, MF_STRING, 2, "Close App")

        # Create the main application window

        def _window_proc_callback(hwnd, msg, wparam, lparam):

            if msg == WM_CLOSE:
                self.quit()

            elif msg == WM_TIMER:
                self.update_ui()

            elif msg == WM_SIZE:
                width, height = lparam & 0xFFFF, (lparam >> 16) & 0xFFFF
                user32.SetWindowPos(self.hwnd_video_frame, 0, 0, 0, width, height - 70, SWP_NOMOVE)
                user32.SetWindowPos(self.hwnd_button_play, 0, 10, height - 33, 0, 0, SWP_NOSIZE)
                user32.SetWindowPos(self.hwnd_button_stop, 0, 100, height - 33, 0, 0, SWP_NOSIZE)
                user32.SetWindowPos(self.hwnd_trackbar_position, 0, 7, height - 60, width - 14, 23, 0)
                user32.SetWindowPos(self.hwnd_trackbar_volume, 0, width - 107, height - 33, 0, 0, SWP_NOSIZE)

                # Our main window's class has no CS_HREDRAW/CS_VREDRAW class styles,
                # because those would VLC make flicker when the window is resized.
                # Therefor we have to invalidate the non-VLC part of the UI (bottom rect)
                # manually to avoid visual artefacts.
                user32.InvalidateRect(hwnd, byref(RECT(0, height - 70, width, height)), 1)

            elif msg == WM_GETMINMAXINFO:
                mmi = cast(lparam, POINTER(MINMAXINFO)).contents
                mmi.ptMinTrackSize = POINT(320, 128)
                return 0

            elif msg == WM_HSCROLL:
                if lparam == self.hwnd_trackbar_position:
                    lo, hi, = wparam & 0xFFFF, (wparam >> 16) & 0xFFFF
                    if lo == TB_ENDTRACK:
                        return 0
                    if lo in (TB_PAGEDOWN, TB_PAGEUP):  # Click into slider
                        pt = POINT()
                        user32.GetCursorPos(byref(pt))
                        user32.MapWindowPoints(None, self.hwnd_trackbar_position, byref(pt), 1)
                        rc = RECT()
                        user32.GetWindowRect(self.hwnd_trackbar_position, byref(rc))
                        hi = int((pt.x - 10) / (rc.right - rc.left - 20) * 2000)
                        user32.SendMessageW(self.hwnd_trackbar_position, TBM_SETPOS, 1, hi)
                    self.set_position(hi)

                elif lparam == self.hwnd_trackbar_volume:
                    lo, hi, = wparam & 0xFFFF, (wparam >> 16) & 0xFFFF
                    if lo == TB_ENDTRACK:
                        return 0
                    if lo in (TB_PAGEDOWN, TB_PAGEUP):  # Click into slider
                        pt = POINT()
                        user32.GetCursorPos(byref(pt))
                        user32.MapWindowPoints(None, self.hwnd_trackbar_volume, byref(pt), 1)
                        rc = RECT()
                        user32.GetWindowRect(self.hwnd_trackbar_volume, byref(rc))
                        hi = int((pt.x - 10) / (rc.right - rc.left - 20) * 100)
                        user32.SendMessageW(self.hwnd_trackbar_volume, TBM_SETPOS, 1, hi)
                    self.set_volume(hi)

            elif msg == WM_COMMAND:
                command_id = WORD(wparam & 0xFFFF).value  # LOWORD(wparam)
                if lparam == 0:
                    if command_id == 1:
                        self.open_file()
                    elif command_id == 2:
                        self.quit()
                elif lparam == self.hwnd_button_play:
                    if command_id == BN_CLICKED:
                        self.play_pause()
                elif lparam == self.hwnd_button_stop:
                    if command_id == BN_CLICKED:
                        self.stop()

            return user32.DefWindowProcW(hwnd, msg, wparam, lparam)

        self.windowproc = WNDPROC(_window_proc_callback)

        newclass = WNDCLASSEXW()
        newclass.lpfnWndProc = self.windowproc
        newclass.lpszClassName = "MediaPlayer"
        newclass.hbrBackground = COLOR_3DFACE + 1
        newclass.hCursor = user32.LoadCursorW(None, IDC_ARROW)
        user32.RegisterClassExW(byref(newclass))

        hwnd_desktop = user32.GetDesktopWindow()
        rc = RECT()
        user32.GetClientRect(hwnd_desktop, byref(rc))
        cx, cy = 640, 480
        x, y = (rc.right - cx) // 2, (rc.bottom - cy) // 2

        self.hwnd = user32.CreateWindowExW(
            0,
            newclass.lpszClassName,
            "Media Player",
            WS_OVERLAPPEDWINDOW,
            x, y, cx, cy,
            0,
            self.h_menu,
            None, 0
        )

        # Create a basic vlc instance
        self.instance = vlc.Instance(["--quiet", "--no-osd"])

        self.media = None

        # Create an empty vlc media player
        self.mediaplayer = self.instance.media_player_new()

        self.create_ui()

        self.is_paused = False

        # The media player has to be "connected" to our VideoFrame
        self.mediaplayer.set_hwnd(self.hwnd_video_frame)

        user32.ShowWindow(self.hwnd, SW_SHOWNORMAL)

    def run(self):
        msg = MSG()
        while user32.GetMessageW(byref(msg), None, 0, 0):
            if user32.IsDialogMessage(self.hwnd, byref(msg)):
                continue
            user32.TranslateMessage(byref(msg))
            user32.DispatchMessageW(byref(msg))
        return 0

    def quit(self):
        user32.PostQuitMessage(0)

    def timer_start(self):
        user32.SetTimer(self.hwnd, 1, 100, 0)  # 1 is our timer id

    def timer_stop(self):
        user32.KillTimer(self.hwnd, 1)

    def create_ui(self):
        """Set up the user interface"""
        self.dummy_proc = WNDPROC(user32.DefWindowProcW)
        newclass = WNDCLASSEXW()
        newclass.lpfnWndProc = self.dummy_proc
        newclass.lpszClassName = "VideoFrame"
        newclass.hbrBackground = gdi32.CreateSolidBrush(0x000000)
        newclass.hCursor = user32.LoadCursorW(None, IDC_ARROW)
        user32.RegisterClassExW(byref(newclass))

        self.hwnd_video_frame = user32.CreateWindowExW(
            0,
            newclass.lpszClassName,
            "VideoFrame",
            WS_CHILD | WS_VISIBLE,
            0, 0, 0, 0,
            self.hwnd,
            None, None, 0
        )

        h_font_shell = gdi32.CreateFontW(
            -11, 0, 0, 0, FW_DONTCARE, 0, 0, 0, ANSI_CHARSET, OUT_TT_PRECIS,
            CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, "Segoe UI"
        )

        self.hwnd_button_play = user32.CreateWindowExW(
            0,
            WC_BUTTON,
            "Play",
            WS_CHILD | WS_VISIBLE,
            0, 0, 80, 23,
            self.hwnd,
            None, None, 0
        )
        user32.SendMessageW(self.hwnd_button_play, WM_SETFONT, h_font_shell, 1)

        self.hwnd_button_stop = user32.CreateWindowExW(
            0,
            WC_BUTTON,
            "Stop",
            WS_CHILD | WS_VISIBLE,
            0, 0, 80, 23,
            self.hwnd,
            None, None, 0
        )
        user32.SendMessageW(self.hwnd_button_stop, WM_SETFONT, h_font_shell, 1)

        self.hwnd_trackbar_position = user32.CreateWindowExW(
            0,
            WC_TRACKBAR,
            "",
            WS_CHILD | WS_VISIBLE | WS_DISABLED,
            0, 0, 0, 0,
            self.hwnd,
            None, None, 0
        )
        user32.SendMessageW(self.hwnd_trackbar_position, TBM_SETRANGEMAX, 0, 2000)
        user32.SendMessageW(self.hwnd_trackbar_position, TBM_SETPAGESIZE, 0, 1)

        self.hwnd_trackbar_volume = user32.CreateWindowExW(
            0,
            WC_TRACKBAR,
            "",
            WS_CHILD | WS_VISIBLE,
            0, 0, 100, 23,
            self.hwnd,
            None, None, 0
        )
        user32.SendMessageW(self.hwnd_trackbar_volume, TBM_SETRANGEMAX, 0, 100)
        user32.SendMessageW(self.hwnd_trackbar_volume, TBM_SETPAGESIZE, 0, 1)
        user32.SendMessageW(self.hwnd_trackbar_volume, TBM_SETPOS, 1, self.mediaplayer.audio_get_volume())

    def play_pause(self):
        """Toggle play/pause status"""
        if self.mediaplayer.is_playing():
            self.mediaplayer.pause()
            user32.SetWindowTextW(self.hwnd_button_play, "Play")
            self.is_paused = True
            self.timer_stop()
        else:
            if self.mediaplayer.play() == -1:
                self.open_file()
                return

            self.mediaplayer.play()
            user32.SetWindowTextW(self.hwnd_button_play, "Pause")
            self.is_paused = False
            self.timer_start()
        user32.EnableWindow(self.hwnd_trackbar_position, 1)

    def stop(self):
        """Stop player"""
        self.mediaplayer.stop()
        user32.SetWindowTextW(self.hwnd_button_play, "Play")
        user32.EnableWindow(self.hwnd_trackbar_position, 0)

    def open_file(self):
        """Open a media file in a MediaPlayer"""
        file_buffer = create_unicode_buffer(MAX_PATH)
        ofn = OPENFILENAMEW()
        ofn.hwndOwner = self.hwnd
        ofn.lpstrTitle = "Choose Media File"
        ofn.lpstrFile = cast(file_buffer, LPWSTR)
        ofn.nMaxFile = MAX_PATH
        ofn.Flags = OFN_ENABLESIZING | OFN_PATHMUSTEXIST
        if not comdlg32.GetOpenFileNameW(byref(ofn)):
            return

        filename = file_buffer[:].split("\0", 1)[0]
        self.media = self.instance.media_new(filename)

        # Put the media in the media player
        self.mediaplayer.set_media(self.media)

        # Parse the metadata of the file
        self.media.parse()

        # Set the title of the track as window title
        user32.SetWindowTextW(self.hwnd, self.media.get_meta(0))

        self.play_pause()

    def set_volume(self, volume):
        """Set the volume"""
        self.mediaplayer.audio_set_volume(volume)

    def set_position(self, pos):
        """Set the movie position according to the position slider"""
        # Set the media position to where the slider was dragged
        self.timer_stop()
        self.mediaplayer.set_position(pos / 2000.0)
        self.timer_start()

    def update_ui(self):
        """Update the user interface"""
        # Set the slider"s position to its corresponding media position
        media_pos = int(self.mediaplayer.get_position() * 2000)
        user32.SendMessageW(self.hwnd_trackbar_position, TBM_SETPOS, 1, media_pos)

        # No need for a timer if nothing is played
        if not self.mediaplayer.is_playing():
            self.timer_stop()

            # After the video finished, the play button stills shows "Pause",
            # which is not the desired behavior of a media player.
            # This fixes that "bug".
            if not self.is_paused:
                self.stop()


def main():
    """Entry point for our simple vlc player"""
    player = Player()
    sys.exit(player.run())

if __name__ == "__main__":
    main()

Activity

  1. mrJean1 commented on Aug 26, 2026

    @mrJean1
    Collaborator

    This is a work of art ;). Tested with vlc.py 3.0.212203, libVLC 3.0.23 Vetinari and python 3.13.2 64-bit on Windows 11.

    One comment, while/after resizing the window, the aspect ratio of the video panel should be adjusted to match the aspect ration of the video.

  2. 59de44955ebd commented on Aug 26, 2026

    @59de44955ebd
    ContributorAuthor

    @mrJean1 Hi, I'm not sure if I understand your suggestion. AFAICT this simple player currently behaves like other desktop media players, e.g. MPC-HC or VLC: the video panel size is determined by the size of the application window, and VLC takes care of resizing the video inside this panel with correct aspect ratio. So why should the video panel be resized differently?

  3. mrJean1 commented on Aug 26, 2026

    @mrJean1
    Collaborator

    Yes it does and VLC fits the video inside the application window. But when the video panel's aspect ratio does not match the aspect ratio of the video, black bands show up above and below the video (or the left and right). The Big Buck Bunny snapshot above shows that.

    It may not matter, but several python_vlc/examples adjust the panel's aspect ratio to avoid such black bands. See methods Player._set_aspect_ratio and AppVLC._sizer.

  4. 59de44955ebd commented on Aug 26, 2026

    @59de44955ebd
    ContributorAuthor

    I guess this is a pure matter of taste. For me those black bands are what I expect a video player to show in this case, because the video players I use daily (MPC-HC and VLC/Windows) also show them. And also the PyQt5 example, which my example is based on. But if my example actually makes it to examples folder, I wouldn't mind at all if this behaviour (that I personally prefer) would be changed.

  5. mrJean1 commented on Aug 26, 2026

    @mrJean1
    Collaborator

    No problem, it was merely a suggestion. Leave the example as is.

  6. oaubert commented on Aug 26, 2026

    @oaubert
    Owner

    That is a great contribution, thank you. Are you ok with licensing it explicitly as GPLv2 like the source pyqt5vlc.py?

  7. 59de44955ebd commented on Aug 28, 2026

    @59de44955ebd
    ContributorAuthor

    Yes, I'm ok with any license, choose what fits best.

  8. oaubert commented on Sep 10, 2026

    @oaubert
    Owner

    Closed by #307

  9. 59de44955ebd commented on Sep 11, 2026

    @59de44955ebd
    ContributorAuthor

    PS, if anyone wonders why on earth I would deal with combining python-vlc and the native Windows API, this project was the reason:
    https://github.com/59de44955ebd/SMP

    Image
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions