How to Build Python Video Player

How to Build Python Video Player

💜 Build a Modern Python Video Player (Purple-Glass GUI)

This tutorial will guide you to create a feature-rich desktop video player in Python using CustomTkinter and python-vlc. You’ll build a stylish purple-glass themed player with playlist, mini-player, PiP, speed control, and more.

🌟 Step 1 — Installing Python Tools & Libraries You Will Need

Before coding, make sure you have:

Install Python packages:

pip install customtkinter pillow python-vlc

Optional (for drag-and-drop):

pip install tkdnd2

🏗 Step 2 — Understand the App Architecture

  • GUI Layer: Window layout, playlist, buttons, sliders, hover effects
  • Video Engine (VLC): Handles playback, speed, screenshots, PiP
  • Background Thread: Updates seek bar and time, auto-next playback
  •  Handling User Actions: 🖱️Drag-Drop, Double-Clicks, and Keyboard Tricks

🎨 Step 3 — Purple-Glass UI Design

Main colors:

  • Deep purple: #2A1F3A
  • Glass overlay: #261C33
  • Accent neon: #CFA8FF

Layout:

  • Playlist panel (left)
  • Video frame (center)
  • Control bar (bottom) with buttons, sliders, info label

🎬 Step 4 — Core Features Explained

  • Playlist: Add/remove/reorder videos, auto-next playback
  • Drag & Drop: Drop files to play instantly
  • Mini-Player: Compact floating window
  • PiP Mode: Small topmost window playing same video
  • Speed Control: 0.25x – 3x playback
  • Screenshot Capture: Saves frames to screenshots/
  • Keyboard Shortcuts:
    KeyFunction
    SpacePlay/Pause
    FFullscreen
    EscExit fullscreen
    ← / →Seek ±5s
    ↑ / ↓Volume up/down
  • Animated Theme: Subtle purple-glass color cycling

💻 Step 5 — How the Code Works

The app uses:

  • CTkTextbox for playlist
  • VLC engine to play videos inside the Tkinter frame
  • Background thread for UI updates and auto-next
  • Separate windows for PiP and mini-player

🏁 Step 6 — Run the Application

python video_player_all_features.py

You will see a purple-glass themed player with all features ready.

📦 Step 7 — Package as an EXE (Optional)

pip install pyinstaller
pyinstaller --noconsole --onefile video_player_all_features.py

EXE will be in dist/ folder.

🌈 Step 8 — Next Steps / Enhancements

  • Subtitle support
  • Playlist save/load
  • Drag-and-drop reordering
  • Custom themes and colors
  • Audio visualizers or video filters

🌟Full Code for Python Video Player

Copy the following code into vide.py:

"""
video_player_all_features.py
Feature-rich Python GUI Video Player (Purple-Glass theme) using CustomTkinter + python-vlc.
"""

import os
import sys
import time
import threading
import platform
from pathlib import Path
from tkinter import filedialog, messagebox
import customtkinter as ctk
import vlc
from PIL import Image  # pillow used for potential further image ops

# Attempt to import tkdnd for drag & drop. If not available, we'll still work.
try:
    import tkdnd
    TKDND_AVAILABLE = True
except Exception:
    TKDND_AVAILABLE = False

# ---- Purple-Glass theme colors (single-file, no external JSON) ----
PURPLE = "#8C52FF"
PURPLE_LIGHT = "#A874FF"
DEEP_PURPLE = "#2A1F3A"
DARK_PURPLE = "#1E142B"
GLASS_BG = "#261C33"
TEXT_ON_DARK = "#ECECEC"
ACCENT = "#CFA8FF"

# ---- Appearance setup ----
ctk.set_appearance_mode("dark")
# Do NOT call set_default_color_theme with "purple" (not built-in).
# We'll style widgets explicitly to produce the purple-glass look.

APP_TITLE = "🎬 Purple-Glass Video Player"

# ---- Helper functions ----
def resource_path(rel: str) -> str:
    # for frozen apps; otherwise returns rel
    base = getattr(sys, "_MEIPASS", os.path.abspath("."))
    return os.path.join(base, rel)

def human_time(ms: int) -> str:
    if ms <= 0:
        return "00:00"
    s = ms // 1000
    h, s = divmod(s, 3600)
    m, s = divmod(s, 60)
    if h:
        return f"{h:02d}:{m:02d}:{s:02d}"
    return f"{m:02d}:{s:02d}"

# ---- Main Application ----
class VideoPlayer(ctk.CTk):
    def __init__(self):
        super().__init__()
        self.title(APP_TITLE)
        self.geometry("1100x680")
        self.minsize(860, 520)

        # Apply window background color
        self.configure(fg_color=DEEP_PURPLE)

        # VLC player setup
        # On Windows, try to add default VLC path so libvlc.dll is found (common location)
        if platform.system() == "Windows":
            default_vlc = r"C:\Program Files\VideoLAN\VLC"
            if os.path.isdir(default_vlc):
                try:
                    os.add_dll_directory(default_vlc)
                except Exception:
                    pass

        self.vlc_instance = vlc.Instance()
        self.player = self.vlc_instance.media_player_new()
        self.pip_player = None  # separate VLC player for PiP, created on demand

        # Playback state
        self.current_index = None
        self.playlist = []  # list of filepaths
        self._is_fullscreen = False
        self._pip_on = False
        self._mini_mode = False
        self._animated = True

        # UI variables
        self._playback_speed = ctk.DoubleVar(value=1.0)
        self._volume = ctk.IntVar(value=60)
        self._position = ctk.DoubleVar(value=0.0)
        self._duration_ms = 0

        # Build UI
        self._build_ui()

        # Start background update thread
        self._running = True
        threading.Thread(target=self._update_thread, daemon=True).start()

        # Animated theme cycle (colors still purple-glass variants)
        self._theme_index = 0
        self._theme_colors = [GLASS_BG, "#2C1A3E", "#341944", "#3D1846"]
        self._cycle_theme()

        # keyboard shortcuts
        self.bind("", lambda e: self.toggle_play())
        self.bind("", lambda e: self.toggle_fullscreen())
        self.bind("", lambda e: self.exit_fullscreen())
        self.bind("", lambda e: self.seek_relative(-5000))
        self.bind("", lambda e: self.seek_relative(5000))
        self.protocol("WM_DELETE_WINDOW", self._on_close)

        # Try enabling drag & drop to playlist area if tkdnd present
        if TKDND_AVAILABLE:
            try:
                dnd = tkdnd.TkDND(self)
                dnd.bindtarget(self.playlist_box, 'text/uri-list', '', self._on_drop)
            except Exception:
                pass

    # ---- UI construction ----
    def _build_ui(self):
        # Main layout: top video area, bottom controls + left playlist
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(1, weight=1)

        # Playlist frame (left)
        left_frame = ctk.CTkFrame(self, corner_radius=12, fg_color=GLASS_BG)
        left_frame.grid(row=0, column=0, sticky="nsw", padx=(12, 6), pady=12)
        left_frame.grid_rowconfigure(1, weight=1)
        ctk.CTkLabel(left_frame, text="Playlist", anchor="w", font=ctk.CTkFont(size=14, weight="bold"), text_color=TEXT_ON_DARK, fg_color=GLASS_BG).grid(row=0, column=0, sticky="ew", padx=8, pady=(6, 4))

        self.playlist_box = ctk.CTkTextbox(left_frame, width=260, height=420, state="disabled", fg_color="#2b1530", text_color=TEXT_ON_DARK, corner_radius=8)
        self.playlist_box.grid(row=1, column=0, padx=8, pady=6, sticky="nsew")
        # Provide simple playlist controls
        pl_controls = ctk.CTkFrame(left_frame, fg_color="transparent")
        pl_controls.grid(row=2, column=0, padx=8, pady=6, sticky="ew")
        add_btn = ctk.CTkButton(pl_controls, text="Add", command=self.add_files, width=1, fg_color=PURPLE, hover_color=PURPLE_LIGHT, text_color="white")
        add_btn.pack(side="left", padx=6)
        remove_btn = ctk.CTkButton(pl_controls, text="Remove", command=self.remove_selected, width=1, fg_color=PURPLE, hover_color=PURPLE_LIGHT, text_color="white")
        remove_btn.pack(side="left", padx=6)
        up_btn = ctk.CTkButton(pl_controls, text="Up", command=self.move_up, width=1, fg_color=PURPLE, hover_color=PURPLE_LIGHT, text_color="white")
        up_btn.pack(side="left", padx=6)
        down_btn = ctk.CTkButton(pl_controls, text="Down", command=self.move_down, width=1, fg_color=PURPLE, hover_color=PURPLE_LIGHT, text_color="white")
        down_btn.pack(side="left", padx=6)
        clear_btn = ctk.CTkButton(pl_controls, text="Clear", command=self.clear_playlist, width=1, fg_color=PURPLE, hover_color=PURPLE_LIGHT, text_color="white")
        clear_btn.pack(side="left", padx=6)

        # Video area (center)
        self.video_frame = ctk.CTkFrame(self, corner_radius=12, fg_color=GLASS_BG)
        self.video_frame.grid(row=0, column=1, sticky="nsew", padx=(6, 12), pady=12)
        self.video_frame.grid_rowconfigure(0, weight=1)
        self.video_frame.grid_columnconfigure(0, weight=1)

        # embed area where VLC renders video
        self.embed = ctk.CTkFrame(self.video_frame, height=420, corner_radius=10, fg_color="#201026")
        self.embed.grid(row=0, column=0, sticky="nsew", padx=12, pady=12)
        # Info overlay
        self.info_label = ctk.CTkLabel(self.embed, text="Drop files here or click Add", anchor="center", text_color=ACCENT, fg_color="#201026")
        self.info_label.place(relx=0.5, rely=0.5, anchor="center")

        # Controls (below video)
        controls = ctk.CTkFrame(self, corner_radius=12, fg_color=GLASS_BG)
        controls.grid(row=1, column=0, columnspan=2, sticky="ew", padx=12, pady=(0,12))
        controls.grid_columnconfigure(4, weight=1)

        # Left control group
        left_controls = ctk.CTkFrame(controls, fg_color="transparent")
        left_controls.grid(row=0, column=0, sticky="w", padx=8, pady=8)
        self.play_btn = ctk.CTkButton(left_controls, text="Play", command=self.toggle_play, width=80, fg_color=PURPLE, hover_color=PURPLE_LIGHT, text_color="white")
        self._add_hover(self.play_btn)
        self.play_btn.pack(side="left", padx=6)
        ctk.CTkButton(left_controls, text="Stop", command=self.stop, width=80, fg_color=PURPLE, hover_color=PURPLE_LIGHT, text_color="white").pack(side="left", padx=6)
        ctk.CTkButton(left_controls, text="Prev", command=self.prev_track, width=70, fg_color=PURPLE, hover_color=PURPLE_LIGHT, text_color="white").pack(side="left", padx=6)
        ctk.CTkButton(left_controls, text="Next", command=self.next_track, width=70, fg_color=PURPLE, hover_color=PURPLE_LIGHT, text_color="white").pack(side="left", padx=6)

        # Center controls: seek and time
        center_ctrl = ctk.CTkFrame(controls, fg_color="transparent")
        center_ctrl.grid(row=0, column=1, sticky="ew", padx=8, pady=8, columnspan=1)
        self.time_label = ctk.CTkLabel(center_ctrl, text="00:00 / 00:00", text_color=TEXT_ON_DARK)
        self.time_label.pack(side="left", padx=(6,10))
        self.seek = ctk.CTkSlider(center_ctrl, from_=0, to=100, number_of_steps=100, variable=self._position, command=self._on_seek, progress_color=PURPLE, button_color=PURPLE_LIGHT)
        self.seek.pack(side="left", fill="x", expand=True, padx=6)

        # Right controls: volume, speed, PiP, screenshot, mini, theme
        right_controls = ctk.CTkFrame(controls, fg_color="transparent")
        right_controls.grid(row=0, column=2, sticky="e", padx=8, pady=8)
        ctk.CTkLabel(right_controls, text="Vol", text_color=TEXT_ON_DARK).pack(side="left", padx=(6,2))
        vol_slider = ctk.CTkSlider(right_controls, from_=0, to=100, variable=self._volume, command=self._on_volume, progress_color=PURPLE, button_color=PURPLE_LIGHT, width=120)
        vol_slider.pack(side="left", padx=6)
        ctk.CTkLabel(right_controls, text="Speed", text_color=TEXT_ON_DARK).pack(side="left", padx=(12,2))
        speed = ctk.CTkSlider(right_controls, from_=0.25, to=3.0, number_of_steps=55, variable=self._playback_speed, command=self._on_speed, progress_color=PURPLE, button_color=PURPLE_LIGHT, width=140)
        speed.pack(side="left", padx=6)

        pip_btn = ctk.CTkButton(right_controls, text="PiP", command=self.toggle_pip, fg_color=PURPLE, hover_color=PURPLE_LIGHT, text_color="white")
        pip_btn.pack(side="left", padx=6)
        self._add_hover(pip_btn)
        ctk.CTkButton(right_controls, text="Screenshot", command=self.screenshot, fg_color=PURPLE, hover_color=PURPLE_LIGHT, text_color="white").pack(side="left", padx=6)
        mini_btn = ctk.CTkButton(right_controls, text="Mini", command=self.toggle_mini, fg_color=PURPLE, hover_color=PURPLE_LIGHT, text_color="white")
        mini_btn.pack(side="left", padx=6)
        theme_btn = ctk.CTkButton(right_controls, text="Theme", command=self.toggle_animated_theme, fg_color=PURPLE, hover_color=PURPLE_LIGHT, text_color="white")
        theme_btn.pack(side="left", padx=6)

        # Bottom small area: playlist management quick info
        bottom_frame = ctk.CTkFrame(self, corner_radius=8, fg_color=GLASS_BG)
        bottom_frame.grid(row=2, column=0, columnspan=2, sticky="ew", padx=12, pady=(0,12))
        self.status_label = ctk.CTkLabel(bottom_frame, text="Ready", anchor="w", text_color=TEXT_ON_DARK, fg_color=GLASS_BG)
        self.status_label.pack(fill="x", padx=10, pady=6)

        # Right-click bindings to playlist area use textbox selection index -> we'll simulate simple selection by storing an index when clicking lines
        self.playlist_box.bind("", self._playlist_click)
        self.playlist_box.bind("", self._playlist_double_click)

        # initialize volume
        try:
            self.player.audio_set_volume(self._volume.get())
        except Exception:
            pass

    # ---- UI/UX helpers ----
    def _add_hover(self, widget):
        def on_enter(e):
            try:
                widget.configure(opacity=0.9)
            except Exception:
                pass
        def on_leave(e):
            try:
                widget.configure(opacity=1.0)
            except Exception:
                pass
        widget.bind("", on_enter)
        widget.bind("", on_leave)

    def _cycle_theme(self):
        if self._animated:
            color = self._theme_colors[self._theme_index % len(self._theme_colors)]
            try:
                self.video_frame.configure(fg_color=color)
            except Exception:
                pass
            # cycle slowly
            self._theme_index += 1
        self.after(2000, self._cycle_theme)

    def toggle_animated_theme(self):
        self._animated = not self._animated
        self.status("Animated theme: " + ("On" if self._animated else "Off"))

    # ---- Playlist management ----
    def add_files(self):
        files = filedialog.askopenfilenames(title="Select videos", filetypes=[("Video files", "*.mp4 *.mkv *.avi *.mov *.flv *.webm"), ("All files","*.*")])
        if not files:
            return
        for f in files:
            f = os.path.abspath(f)
            self.playlist.append(f)
        self._refresh_playlist_view()
        if self.current_index is None:
            self.current_index = 0
            self.load_index(0)

    def remove_selected(self):
        index = self._get_textbox_index_at_click()
        if index is None:
            self.status("No selection to remove")
            return
        try:
            del self.playlist[index]
            if self.current_index == index:
                self.stop()
                self.current_index = None
            elif self.current_index and self.current_index > index:
                self.current_index -= 1
            self._refresh_playlist_view()
        except Exception:
            pass

    def move_up(self):
        idx = self._get_textbox_index_at_click()
        if idx is None or idx == 0:
            return
        self.playlist[idx-1], self.playlist[idx] = self.playlist[idx], self.playlist[idx-1]
        if self.current_index == idx:
            self.current_index -= 1
        elif self.current_index == idx-1:
            self.current_index += 1
        self._refresh_playlist_view()

    def move_down(self):
        idx = self._get_textbox_index_at_click()
        if idx is None or idx >= len(self.playlist)-1:
            return
        self.playlist[idx+1], self.playlist[idx] = self.playlist[idx], self.playlist[idx+1]
        if self.current_index == idx:
            self.current_index += 1
        elif self.current_index == idx+1:
            self.current_index -= 1
        self._refresh_playlist_view()

    def clear_playlist(self):
        self.playlist.clear()
        self.current_index = None
        self._refresh_playlist_view()
        self.stop()

    def _refresh_playlist_view(self):
        # simple clear + write with indices
        self.playlist_box.configure(state="normal")
        self.playlist_box.delete("1.0", "end")
        for i, p in enumerate(self.playlist):
            short = os.path.basename(p)
            mark = "▶ " if i == self.current_index else "   "
            self.playlist_box.insert("end", f"{mark}{i+1}. {short}\n")
        self.playlist_box.configure(state="disabled")

    # helper: find line index clicked in playlist textbox
    def _playlist_click(self, event=None):
        # store last click index for selection-based operations
        index = self.playlist_box.index(f"@{event.x},{event.y}")
        line_no = int(str(index).split(".")[0]) - 1
        if 0 <= line_no < len(self.playlist):
            self._last_clicked_index = line_no
        else:
            self._last_clicked_index = None

    def _get_textbox_index_at_click(self):
        return getattr(self, "_last_clicked_index", None)

    def _playlist_double_click(self, event=None):
        self._playlist_click(event)
        idx = self._get_textbox_index_at_click()
        if idx is not None:
            self.load_index(idx)
            self.play()

    def _on_drop(self, event):
        # tkdnd returns a string of URIs
        data = getattr(event, 'data', None)
        if not data:
            return
        uris = data.split()
        for u in uris:
            if u.startswith("file://"):
                path = u[7:]
            else:
                path = u
            path = path.strip()
            if os.path.exists(path):
                self.playlist.append(path)
        self._refresh_playlist_view()
        if self.current_index is None and len(self.playlist) > 0:
            self.load_index(0)

    # ---- Loading & playback ----
    def load_index(self, idx: int):
        if idx < 0 or idx >= len(self.playlist):
            return
        self.current_index = idx
        path = self.playlist[idx]
        media = self.vlc_instance.media_new(path)
        self.player.set_media(media)
        self._set_video_hwnd(self.player, self.embed)
        self.status(f"Loaded: {os.path.basename(path)}")
        self._refresh_playlist_view()

    def play(self):
        if self.player.get_media() is None:
            if self.playlist:
                self.load_index(0)
            else:
                self.status("Playlist empty. Add files.")
                return
        self.player.play()
        # set speed and volume
        try:
            self.player.audio_set_volume(self._volume.get())
            self.player.set_rate(self._playback_speed.get())
        except Exception:
            pass
        self.play_btn.configure(text="Pause")

    def pause(self):
        self.player.pause()
        self.play_btn.configure(text="Play")

    def stop(self):
        try:
            self.player.stop()
        except Exception:
            pass
        self.play_btn.configure(text="Play")
        self._position.set(0.0)
        self.time_label.configure(text="00:00 / 00:00")

    def toggle_play(self):
        if self.player.is_playing():
            self.pause()
        else:
            self.play()

    def prev_track(self):
        if self.current_index is None: return
        prev = max(0, self.current_index - 1)
        self.load_index(prev)
        self.play()

    def next_track(self):
        if self.current_index is None: return
        nxt = min(len(self.playlist)-1, self.current_index + 1)
        if nxt == self.current_index:
            # try repeat
            self.player.stop()
            self.player.play()
        else:
            self.load_index(nxt)
            self.play()

    # ---- Video output handling ----
    def _set_video_hwnd(self, player_obj, tk_frame):
        # platform-specific: get window id from tk_frame.winfo_id()
        wid = tk_frame.winfo_id()
        if platform.system() == "Windows":
            player_obj.set_hwnd(wid)
        elif platform.system() == "Darwin":
            # macOS requires special handling using ctypes - but python-vlc on mac often handles automatically
            player_obj.set_nsobject(wid)
        else:
            player_obj.set_xwindow(wid)

    # ---- Seek & volume callbacks ----
    def _on_seek(self, value):
        # seek value is 0-100 percent
        if self.player.get_media() and self.player.get_length() > 0:
            length = self.player.get_length()
            new_ms = int(float(value) / 100.0 * length)
            self.player.set_time(new_ms)

    def seek_relative(self, ms_delta):
        if self.player.get_media() and self.player.get_length() > 0:
            cur = self.player.get_time()
            self.player.set_time(max(0, cur + ms_delta))

    def _on_volume(self, value):
        try:
            self.player.audio_set_volume(int(value))
        except Exception:
            pass

    def _on_speed(self, value):
        # VLC rate changes may not work on all codecs; attempt and store
        try:
            self.player.set_rate(float(value))
            self.status(f"Speed: {value:.2f}x")
        except Exception:
            self.status("Speed change not supported for this file.")

    # ---- Screenshot (uses VLC snapshot) ----
    def screenshot(self):
        if self.player.get_media() is None:
            self.status("No media to screenshot.")
            return
        out_dir = Path.cwd() / "screenshots"
        out_dir.mkdir(exist_ok=True)
        ts = int(time.time())
        filename = str(out_dir / f"snap_{ts}.png")
        # video_take_snapshot(renderer, num, filepath, width, height)
        try:
            # width/height 0 to keep source size
            self.player.video_take_snapshot(0, filename, 0, 0)
            self.status(f"Screenshot saved: {filename}")
        except Exception as e:
            self.status(f"Screenshot failed: {e}")

    # ---- PiP (Picture in Picture) ----
    def toggle_pip(self):
        if not self._pip_on:
            # create a new window and a separate VLC player that attaches to it
            self.pip_win = ctk.CTkToplevel(self, fg_color=GLASS_BG)
            self.pip_win.title("PiP")
            self.pip_win.geometry("480x270")
            self.pip_win.attributes("-topmost", True)
            self.pip_win.protocol("WM_DELETE_WINDOW", self.toggle_pip)
            pip_frame = ctk.CTkFrame(self.pip_win, corner_radius=10, fg_color="#201026")
            pip_frame.pack(fill="both", expand=True, padx=6, pady=6)
            # create new player instance
            self.pip_player = self.vlc_instance.media_player_new()
            # set same media
            if self.player.get_media():
                self.pip_player.set_media(self.player.get_media())
            self._set_video_hwnd(self.pip_player, pip_frame)
            # sync: set same time and play
            try:
                self.pip_player.play()
                time.sleep(0.05)
                self.pip_player.set_time(self.player.get_time())
            except Exception:
                pass
            self._pip_on = True
            self.status("PiP on")
        else:
            try:
                self.pip_player.stop()
                self.pip_win.destroy()
            except Exception:
                pass
            self.pip_player = None
            self._pip_on = False
            self.status("PiP off")

    # ---- Mini player mode (compact) ----
    def toggle_mini(self):
        if not self._mini_mode:
            self._prev_geometry = self.geometry()
            self._prev_widgets_state = {}  # keep if needed
            # reduce to a small compact window
            self.geometry("420x240")
            self._mini_mode = True
            self.status("Mini mode on")
        else:
            if hasattr(self, "_prev_geometry"):
                self.geometry(self._prev_geometry)
            self._mini_mode = False
            self.status("Mini mode off")

    # ---- Background update thread ----
    def _update_thread(self):
        while self._running:
            try:
                if self.player and self.player.get_media():
                    length = self.player.get_length()  # ms
                    if length > 0:
                        self._duration_ms = length
                        cur = self.player.get_time()
                        pct = (cur / length) * 100 if length > 0 else 0
                        # schedule UI update on main thread
                        self.after(0, lambda cur=cur, length=length, pct=pct: self._update_ui(cur, length, pct))
                time.sleep(0.3)
            except Exception:
                time.sleep(0.5)

    def _update_ui(self, cur, length, pct):
        try:
            self._position.set(pct)
            self.time_label.configure(text=f"{human_time(cur)} / {human_time(length)}")
            # auto next when finished
            if length > 0 and (length - cur) < 900 and not self.player.is_playing():
                # little pause before next to avoid false positives
                # if near end, advance to next track
                if self.current_index is not None and self.current_index < len(self.playlist)-1:
                    self.load_index(self.current_index + 1)
                    self.play()
        except Exception:
            pass

    # ---- Fullscreen ----
    def toggle_fullscreen(self):
        self._is_fullscreen = not self._is_fullscreen
        self.attributes("-fullscreen", self._is_fullscreen)

    def exit_fullscreen(self):
        if self._is_fullscreen:
            self._is_fullscreen = False
            self.attributes("-fullscreen", False)

    # ---- Status bar ----
    def status(self, text: str):
        self.status_label.configure(text=text)

    # ---- Close/cleanup ----
    def _on_close(self):
        self._running = False
        try:
            self.player.stop()
        except Exception:
            pass
        try:
            if self.pip_player:
                self.pip_player.stop()
        except Exception:
            pass
        self.destroy()

# ---- Run ----
if __name__ == "__main__":
    app = VideoPlayer()
    app.mainloop()

Screenshot:

Python Video Player Desktop App Screenshot 1

Python Video Player Desktop App Screenshot 2

Python Video Player Desktop App Screenshot 1

Python Video Player Desktop App Screenshot 4

Python Video Player Desktop App Screenshot 5

Python Video Player Desktop App Screenshot 6

Python Video Player Desktop App

❓ FAQ — Purple-Glass Python Video Player

Q1: Do I need VLC installed to run this player?

A: Yes. The python-vlc library requires the VLC Media Player installed on your system to handle video playback.

Q2: Can I run this on Mac or Linux?

A: Absolutely! The code is cross-platform. Some window handle assignments differ by OS, but the app is functional on Windows, macOS, and Linux.

Q3: How do I add videos to the playlist?

A: Click the Add button or drag-and-drop files (if tkdnd is installed) into the playlist panel.

Q4: Can I save my playlist?

A: Currently, the app does not auto-save playlists, but you can enhance it by using Python's pickle or json to store file paths.

Q5: How do I take a screenshot of the video?

A: Click the Screenshot button. The frame will be saved in a screenshots/ folder in your current working directory.

Q6: What is Mini-Player mode?

A: Mini-Player mode shrinks the app to a small, floating window, perfect for multitasking while watching a video.

Q7: How do I enable PiP mode?

A: Click the PiP button. A small topmost window opens and plays the same video in sync.

Q8: Can I adjust playback speed?

A: Yes! Use the speed slider (0.25x – 3x) to slow down or speed up video playback.

Q9: Why aren’t drag-and-drop files working?

A: Drag-and-drop requires the tkdnd module. Install it using pip install tkdnd2.

Q10: Can I turn off the animated theme?

A: Yes, click the Theme button to toggle animated color cycling on/off.

💻 Python Video Player Developer Worksheet

Name: ____________________    Date: ____________________

Complete the tasks below using your Python Video Player project. Write your answers in the spaces provided.

Part 1: Understanding the Code

  1. List all the main components of the video player (GUI, VLC engine, Playlist, etc.).
    Answer: _______________________________________________
  2. What variable stores the current video index in the playlist?
    Answer: _______________________________________________
  3. Explain what _update_thread does in one sentence.
    Answer: _______________________________________________

Part 2: Playlist & Event System

  1. Add a video to your playlist using code. Show the snippet.
    # Your code here
  2. How would you remove a video from the playlist programmatically?
    Answer: _______________________________________________
  3. What happens when you double-click a playlist item?
    Answer: _______________________________________________

Part 3: Mini Player & PiP

  1. Write a code snippet to toggle mini-player mode.
    # Your code here
  2. How does the PiP window synchronize with the main video player?
    Answer: _______________________________________________

Part 4: Playback & Controls

  1. How do you change volume programmatically? Provide an example.
    # Your code here
  2. Write a code snippet to seek forward 10 seconds.
    # Your code here
  3. How do you take a screenshot of the current video frame?
    Answer: _______________________________________________

Part 5: Extra Challenges

  1. Add a new keyboard shortcut to toggle PiP using the p key.
    # Your code here
  2. Save your playlist to a JSON file. Show the code snippet.
    # Your code here
  3. Implement a speed control feature to play videos at 2x.
    # Your code here

✅ Reflection Questions

  • Which feature was the most challenging to implement?
    Answer: _______________________________________________
  • Which feature would you like to add next to your player?
    Answer: _______________________________________________

📝 Answer Key: Python Video Player Developer Worksheet

Part 1: Understanding the Code

  1. Main components of the video player:
    • GUI Layer: CustomTkinter
    • Video Engine: python-vlc
    • Playlist management
    • Background update thread
    • Event system: drag & drop, double-click, keyboard shortcuts
  2. Variable storing current video index: current_index
  3. Purpose of _update_thread: Continuously updates video progress, slider, time label, and auto-plays the next video when the current one finishes.

Part 2: Playlist & Event System

  1. Add a video snippet:
    from tkinter import filedialog
    files = filedialog.askopenfilenames(filetypes=[("Video files", "*.mp4 *.mkv")])
    for f in files:
        self.playlist.append(f)
    self._refresh_playlist_view()
          
  2. Remove a video programmatically:
    index = 0  # example index
    del self.playlist[index]
    self._refresh_playlist_view()
          
  3. Double-clicking a playlist item loads and plays the selected video immediately.

Part 3: Mini Player & PiP

  1. Toggle mini-player mode:
    self.toggle_mini()
          
  2. PiP synchronization: The PiP player shares the same media as the main player and is set to the same playback time before starting, so it remains in sync.

Part 4: Playback & Controls

  1. Change volume example:
    self.player.audio_set_volume(80)  # sets volume to 80%
          
  2. Seek forward 10 seconds:
    cur = self.player.get_time()
    self.player.set_time(cur + 10000)  # 10000ms = 10s
          
  3. Take a screenshot: Uses self.player.video_take_snapshot(0, "filename.png", 0, 0) to save the current frame.

Part 5: Extra Challenges

  1. Keyboard shortcut for PiP (p key):
    self.bind("p", lambda e: self.toggle_pip())
          
  2. Save playlist to JSON:
    import json
    with open("playlist.json", "w") as f:
        json.dump(self.playlist, f)
          
  3. Speed control to play at 2x:
    self.player.set_rate(2.0)
          

Reflection Questions

  • Most challenging feature: Answers will vary (e.g., PiP sync, drag & drop, animated theme)
  • Feature to add next: Answers will vary (e.g., playlist save/load, subtitles, night mode)

✨ Final Thoughts

This project teaches practical Python GUI programming with multimedia integration. You now have a real-world, portfolio-ready application that’s visually appealing and functional!

Previous Post Next Post

Contact Form