Skip to main content

Overview

The desktop application (desktop_app.py) provides a native window experience by embedding the web interface in a desktop application. It supports two backend frameworks:
  • PyQt6 + QtWebEngine - Full-featured Chromium-based browser with GPU acceleration
  • pywebview - Lightweight alternative using the system’s native webview

Prerequisites

  • Python 3.8 or higher
  • Google Service Account credentials
  • One of the following UI backends:
    • PyQt6 + PyQt6-WebEngine (recommended)
    • pywebview

Installation

1

Clone the repository

git clone <repository-url>
cd platzi-viewer
2

Create virtual environment

python -m venv .venv

# Windows
.venv\Scripts\activate

# Linux/Mac
source .venv/bin/activate
3

Install dependencies

Option A: PyQt6 Backend (Recommended)
pip install -r requirements.txt
pip install PyQt6 PyQt6-WebEngine
Option B: pywebview Backend
pip install -r requirements.txt
pip install pywebview
4

Configure credentials

Place your service_account.json file in the project root:
cp /path/to/service_account.json .
5

Run the desktop app

python desktop_app.py

How It Works

The desktop application:
  1. Configures GPU acceleration - Applies Chromium flags for optimal video playback
  2. Starts embedded HTTP server - Runs server.py in a background thread
  3. Creates native window - Opens PyQt6 or pywebview window pointing to http://127.0.0.1:<port>
  4. Manages lifecycle - Shuts down server when window closes

Architecture

# Desktop app flow (desktop_app.py:118-186)
1. Configure GPU acceleration
2. Determine free port or use PORT env var
3. Set up paths (PLATZI_VIEWER_PATH, PLATZI_DATA_PATH)
4. Load service_account.json from executable directory
5. Start HTTP server in daemon thread
6. Wait for server to become ready
7. Open native window with embedded browser
8. Shutdown server on window close

Environment Variables

HOST
string
default:"127.0.0.1"
Host address for the embedded server. Desktop app defaults to localhost for security.
PORT
string
default:"auto"
Server port. If not specified, automatically finds a free port.
PORT=8080 python desktop_app.py
PLATZI_VIEWER_PATH
string
default:"script directory"
Path to application resources (HTML, CSS, JS). Automatically detected.
PLATZI_DATA_PATH
string
default:"PlatziData subdirectory"
Where user data is stored (progress, cache). Created in executable directory.
platzi-viewer/
  ├── PlatziData/
  │   ├── progress.json
  │   └── courses_cache.json
PLATZI_PREFER_DATA_CACHE
string
default:"0"
Desktop app defaults to 0 to use the bundled cache and avoid stale external caches.
GOOGLE_SERVICE_ACCOUNT_FILE
string
Path to service account credentials. Auto-detected from:
  1. service_account.json in executable directory
  2. service_account.json in bundle resources (for compiled apps)

GPU Acceleration

The desktop app automatically configures GPU acceleration for smooth video playback:

Chromium Flags (desktop_app.py:27-50)

chromium_gpu_flags = [
    "--ignore-gpu-blocklist",           # Enable GPU on unsupported hardware
    "--enable-gpu-rasterization",        # Hardware-accelerated rasterization
    "--enable-zero-copy",                # Zero-copy texture uploads
    "--enable-accelerated-video-decode", # Hardware video decoding
    "--enable-native-gpu-memory-buffers",# Native GPU memory management
]

Platform-Specific Settings

Windows:
# Applied automatically
QTWEBENGINE_CHROMIUM_FLAGS="--ignore-gpu-blocklist --enable-gpu-rasterization ..."
QT_OPENGL=desktop
QT_ANGLE_PLATFORM=d3d11
Linux:
# For better GPU support
export QT_QPA_PLATFORM=xcb
export QT_XCB_GL_INTEGRATION=xcb_egl

UI Backends

Advantages:
  • Full Chromium browser engine
  • Best video codec support
  • Hardware acceleration
  • Persistent storage and caching
  • Developer tools available
Window Configuration (desktop_app.py:82-106):
window.setWindowTitle("Platzi Viewer")
window.resize(1280, 820)
window.setMinimumSize(980, 640)

# Persistent storage in PlatziData directory
storage_path = os.path.join(exe_dir, "PlatziData")
profile.setPersistentStoragePath(storage_path)
profile.setCachePath(storage_path)

pywebview Backend

Advantages:
  • Smaller memory footprint
  • Uses system webview (EdgeHTML/WebKit)
  • Simpler installation
Window Configuration (desktop_app.py:160-178):
window = webview.create_window(
    "Platzi Viewer",
    target_url,
    width=1280,
    height=820,
    min_size=(980, 640),
    text_select=True,
)
Limitations:
  • May have codec limitations on some systems
  • Less control over rendering engine

Data Storage

The desktop app stores all user data in the PlatziData directory:
platzi-viewer/
  ├── desktop_app.py
  ├── service_account.json
  └── PlatziData/
      ├── progress.json          # User progress tracking
      ├── courses_cache.json     # Cached course structure
      └── QtWebEngine/           # Browser cache (PyQt6 only)
          ├── Default/
          └── IndexedDB/

Persistence

  • Progress tracking - Survives app restarts
  • Course cache - Reduces Drive API calls
  • Browser state - Cookies, localStorage (PyQt6 only)

Running from Source

Basic Usage

python desktop_app.py

Custom Port

PORT=3000 python desktop_app.py

Custom Data Directory

PLATZI_DATA_PATH=/custom/path python desktop_app.py

Custom Credentials

GOOGLE_SERVICE_ACCOUNT_FILE=/secure/credentials.json python desktop_app.py

Development Mode

Enable PyQt6 Developer Tools

Modify desktop_app.py to enable DevTools:
from PyQt6.QtWebEngineCore import QWebEngineSettings

# After creating browser
settings = browser.settings()
settings.setAttribute(QWebEngineSettings.WebAttribute.DeveloperExtrasEnabled, True)
Access DevTools: Right-click in window → Inspect

Enable pywebview Debug Mode

# desktop_app.py:178
webview.start(debug=True)  # Change to True

Troubleshooting

Missing UI Backend Error

RuntimeError: No se encontró backend UI. Instala pywebview o PyQt6 + PyQt6-WebEngine
Solution:
pip install PyQt6 PyQt6-WebEngine
# OR
pip install pywebview

Server Won’t Start

RuntimeError: No se pudo iniciar el servidor local en 127.0.0.1:8080
Solution:
  • Check if port is in use: netstat -ano | findstr :8080
  • Let the app auto-select a port: Unset PORT environment variable

Video Playback Issues

PyQt6:
  1. Verify GPU acceleration:
    # Check logs for GPU flags
    python desktop_app.py 2>&1 | grep QTWEBENGINE
    
  2. Try software rendering:
    QT_OPENGL=software python desktop_app.py
    
pywebview:
  • Ensure system codecs are installed (Windows Media Feature Pack on Windows N editions)

Window Won’t Close Cleanly

The app attempts to shutdown the server gracefully. If issues persist:
# Check desktop_app.py:109-113 and :170-175
# Server shutdown is handled in window close event

Credentials Not Found

The app searches for service_account.json in:
  1. Executable directory (desktop_app.py:142)
  2. Bundle resources (desktop_app.py:143)
  3. GOOGLE_SERVICE_ACCOUNT_FILE environment variable
# Verify file exists
ls -l service_account.json

# Set explicit path
export GOOGLE_SERVICE_ACCOUNT_FILE=/absolute/path/to/service_account.json

Performance Tips

Memory Usage

  • PyQt6: ~150-300 MB (Chromium engine)
  • pywebview: ~50-150 MB (system webview)

Startup Time

  • First launch: 2-5 seconds (loads course cache)
  • Subsequent launches: 1-2 seconds

Reduce Memory Footprint

# Use pywebview instead of PyQt6
pip uninstall PyQt6 PyQt6-WebEngine
pip install pywebview

Building Executables

For distributing the desktop app as a standalone executable, see:

Portable Executable

Build standalone executables with PyInstaller

Comparison with Other Deployment Methods

FeatureDesktop AppDockerPortable EXE
InstallationRequires PythonRequires DockerNo dependencies
Startup TimeFast (1-2s)Medium (5-10s)Fast (2-3s)
Memory Usage50-300 MB100-200 MB100-300 MB
Native FeelYesNo (browser)Yes
Auto-UpdateManualPull imageManual
DistributionSource codeContainerSingle .exe

Next Steps

Docker Deployment

Run in containerized environment

Build Portable EXE

Create standalone Windows executable

Build docs developers (and LLMs) love