A python interface to the mpv media player
—
Input event handling, custom key bindings, mouse interaction, and OSD (On-Screen Display) control. Provides comprehensive control over user input and visual feedback systems.
Send keyboard events and manage key bindings for interactive control.
def keypress(self, name: str):
"""
Send a key press event.
Parameters:
- name: Key name (e.g., 'SPACE', 'Enter', 'Esc', 'LEFT', 'a', 'ctrl+c')
"""
def keydown(self, name: str):
"""
Send a key down event (without release).
Parameters:
- name: Key name
"""
def keyup(self, name: str = None):
"""
Send a key up event.
Parameters:
- name: Key name (None for last key pressed)
"""
def keybind(self, name: str, command: str):
"""
Create a simple key binding to mpv command.
Parameters:
- name: Key combination
- command: mpv command string to execute
"""Register Python callbacks for key events with flexible binding modes.
def register_key_binding(self, keydef: str, callback_or_cmd, mode: str = 'force'):
"""
Register a key binding with callback or command.
Parameters:
- keydef: Key definition string
- callback_or_cmd: Python function or mpv command string
- mode: Binding mode ('force', 'weak')
'force': Override existing bindings
'weak': Only bind if no existing binding
"""
def unregister_key_binding(self, keydef: str):
"""
Remove a key binding.
Parameters:
- keydef: Key definition to remove
"""
def key_binding(self, keydef: str, mode: str = 'force'):
"""
Decorator for registering key binding callbacks.
Parameters:
- keydef: Key definition string
- mode: Binding mode ('force', 'weak')
Returns:
Decorator function for callback registration
"""
def on_key_press(self, keydef: str, mode: str = 'force', repetition: bool = False):
"""
Decorator for key press event handling.
Parameters:
- keydef: Key definition string
- mode: Binding mode ('force', 'weak')
- repetition: Whether to handle key repetition
Returns:
Decorator function for callback registration
"""Handle mouse events including clicks, movement, and scroll wheel.
def mouse(self, x: int, y: int, button: int = None, mode: str = 'single'):
"""
Send mouse event.
Parameters:
- x, y: Mouse coordinates
- button: Mouse button (0=left, 1=middle, 2=right)
- mode: Click mode ('single', 'double')
"""Control mpv's built-in OSD for displaying information and status.
def toggle_osd(self):
"""Toggle OSD visibility through different states."""
def print_text(self, text: str):
"""
Print text to terminal/console.
Parameters:
- text: Text to print
"""
def show_text(self, string: str, duration: str = '-1', level: int = 0):
"""
Display text on the OSD.
Parameters:
- string: Text to display (supports property expansion)
- duration: Display duration in milliseconds ('-1' for default)
- level: OSD level (0=subtitles, 1=seek bar, 2=always visible)
"""
def show_progress(self):
"""Show progress bar on OSD."""Process text templates with property expansion for dynamic content.
def expand_text(self, text: str) -> str:
"""
Expand text template with property values.
Parameters:
- text: Template text with ${property} placeholders
Returns:
Expanded text with property values substituted
"""
def expand_path(self, path: str) -> str:
"""
Expand path template with mpv path expansion.
Parameters:
- path: Path template
Returns:
Expanded path string
"""Key definitions use mpv's key naming convention:
a, b, c, etc.0, 1, 2, etc.F1, F2, F3, etc.LEFT, RIGHT, UP, DOWNSPACE, Enter, Esc, Tab, Backspacectrl+key: Control modifieralt+key: Alt modifiershift+key: Shift modifiermeta+key: Meta/Windows modifierMOUSE_BTN0: Left mouse buttonMOUSE_BTN1: Middle mouse buttonMOUSE_BTN2: Right mouse buttonWHEEL_UP, WHEEL_DOWN: Mouse wheelctrl+alt+key: Multiple modifiersshift+ctrl+F1: Function key with modifiersimport mpv
player = mpv.MPV()
# Simple command bindings
player.keybind('SPACE', 'cycle pause')
player.keybind('f', 'cycle fullscreen')
player.keybind('m', 'cycle mute')
player.keybind('LEFT', 'seek -10')
player.keybind('RIGHT', 'seek 10')
# Volume control
player.keybind('UP', 'add volume 5')
player.keybind('DOWN', 'add volume -5')
# Subtitle controls
player.keybind('s', 'cycle sub-visibility')
player.keybind('j', 'cycle sub')# Using decorator
@player.key_binding('q')
def quit_handler():
print("Quit requested")
player.quit()
@player.key_binding('i')
def info_handler():
print(f"Playing: {player.filename}")
print(f"Position: {player.time_pos}/{player.duration}")
player.show_text(f"Time: ${time-pos}/${duration}")
# Using method registration
def screenshot_handler():
filename = f"screenshot_{int(player.time_pos)}.png"
player.screenshot_to_file(filename)
player.show_text(f"Screenshot saved: {filename}", duration='2000')
player.register_key_binding('p', screenshot_handler)
# Advanced callback with key info
def debug_key_handler():
pos = player.time_pos or 0
vol = player.volume
paused = player.pause
debug_info = f"Pos:{pos:.1f} Vol:{vol} Paused:{paused}"
print(debug_info)
player.show_text(debug_info, level=1)
player.register_key_binding('d', debug_key_handler)# Mouse click handlers
def handle_mouse_click():
# Toggle pause on mouse click
player.pause = not player.pause
player.register_key_binding('MOUSE_BTN0', handle_mouse_click)
# Mouse wheel for volume
def wheel_up():
player.property_add('volume', 5)
player.show_text(f"Volume: ${volume}")
def wheel_down():
player.property_add('volume', -5)
player.show_text(f"Volume: ${volume}")
player.register_key_binding('WHEEL_UP', wheel_up)
player.register_key_binding('WHEEL_DOWN', wheel_down)
# Programmatic mouse events
def click_center():
width = player.width or 800
height = player.height or 600
player.mouse(width // 2, height // 2, button=0)
# Double-click for fullscreen
def double_click_fullscreen():
player.mouse(400, 300, mode='double')
player.fullscreen = not player.fullscreen
player.register_key_binding('ctrl+MOUSE_BTN0', double_click_fullscreen)# Context-sensitive bindings
def smart_seek():
"""Smart seeking based on current position."""
pos = player.time_pos or 0
duration = player.duration or 1
# Larger jumps near beginning/end
if pos < duration * 0.1 or pos > duration * 0.9:
seek_amount = 30
else:
seek_amount = 10
player.seek(seek_amount)
player.show_text(f"Seek +{seek_amount}s")
player.register_key_binding('ctrl+RIGHT', smart_seek)
# Multi-key sequences (custom implementation)
class MultiKeyHandler:
def __init__(self, player):
self.player = player
self.sequence = []
self.timeout = None
def handle_key(self, key):
self.sequence.append(key)
# Check for known sequences
seq_str = ''.join(self.sequence)
if seq_str == 'gg': # Go to beginning
self.player.seek(0, reference='absolute')
self.reset()
elif seq_str == 'GG': # Go to end
self.player.seek(100, reference='percent')
self.reset()
elif len(self.sequence) >= 2:
self.reset()
def reset(self):
self.sequence = []
multi_key = MultiKeyHandler(player)
@player.key_binding('g')
def handle_g():
multi_key.handle_key('g')
@player.key_binding('G')
def handle_G():
multi_key.handle_key('G')# Property-based text display
@player.key_binding('t')
def show_time():
player.show_text("Time: ${time-pos} / ${duration}")
@player.key_binding('v')
def show_volume():
player.show_text("Volume: ${volume}%", duration='1500')
@player.key_binding('r')
def show_resolution():
text = "Resolution: ${width}x${height} @ ${fps} FPS"
player.show_text(text, level=1)
# Custom OSD formatting
def format_time_display():
pos = player.time_pos or 0
dur = player.duration or 0
pos_min, pos_sec = divmod(int(pos), 60)
dur_min, dur_sec = divmod(int(dur), 60)
time_str = f"{pos_min:02d}:{pos_sec:02d} / {dur_min:02d}:{dur_sec:02d}"
return time_str
@player.key_binding('T')
def show_formatted_time():
time_display = format_time_display()
player.show_text(time_display, duration='2000', level=2)
# Progress indicator
@player.key_binding('P')
def show_progress_info():
player.show_progress()
# Additional info
percent = player.percent_pos or 0
player.show_text(f"Progress: {percent:.1f}%", level=1)class InteractivePlayer:
def __init__(self):
self.player = mpv.MPV()
self.help_visible = False
self.setup_bindings()
def setup_bindings(self):
"""Setup all key bindings."""
# Help system
@self.player.key_binding('h')
def toggle_help():
self.toggle_help_display()
# Playback controls
@self.player.key_binding('SPACE')
def toggle_pause():
self.player.pause = not self.player.pause
state = "Paused" if self.player.pause else "Playing"
self.player.show_text(state, duration='1000')
# Seeking with feedback
@self.player.key_binding('l')
def seek_forward():
self.player.seek(10)
pos = self.player.time_pos or 0
self.player.show_text(f"→ {pos:.1f}s", duration='800')
@self.player.key_binding('j')
def seek_backward():
self.player.seek(-10)
pos = self.player.time_pos or 0
self.player.show_text(f"← {pos:.1f}s", duration='800')
# Volume with visual feedback
@self.player.key_binding('+')
def volume_up():
self.player.property_add('volume', 5)
vol = self.player.volume
self.player.show_text(f"Volume: {vol:.0f}%", duration='1000')
@self.player.key_binding('-')
def volume_down():
self.player.property_add('volume', -5)
vol = self.player.volume
self.player.show_text(f"Volume: {vol:.0f}%", duration='1000')
def toggle_help_display(self):
"""Toggle help text display."""
if self.help_visible:
self.player.show_text("", duration='0') # Clear text
self.help_visible = False
else:
help_text = """
Key Bindings:
SPACE - Play/Pause h - Toggle Help
l/j - Seek ±10s +/- - Volume ±5
f - Fullscreen q - Quit
""".strip()
self.player.show_text(help_text, duration='-1', level=2)
self.help_visible = True
# Usage
interactive_player = InteractivePlayer()
interactive_player.player.play('/path/to/video.mp4')class DynamicControls:
def __init__(self, player):
self.player = player
self.mode = 'normal'
self.setup_mode_switching()
def setup_mode_switching(self):
"""Setup mode-based key bindings."""
@self.player.key_binding('ESC')
def exit_mode():
self.set_mode('normal')
@self.player.key_binding('1')
def number_mode():
self.set_mode('numbers')
@self.player.key_binding('2')
def seek_mode():
self.set_mode('seeking')
def set_mode(self, mode):
"""Change control mode and update bindings."""
self.mode = mode
# Clear existing dynamic bindings
for key in ['a', 'b', 'c', 'd']:
try:
self.player.unregister_key_binding(key)
except:
pass
# Setup mode-specific bindings
if mode == 'numbers':
self.setup_number_mode()
elif mode == 'seeking':
self.setup_seek_mode()
self.player.show_text(f"Mode: {mode}", duration='1500')
def setup_number_mode(self):
"""Number input mode bindings."""
def make_number_handler(num):
def handler():
self.player.show_text(f"Number: {num}")
return handler
for i in range(10):
self.player.register_key_binding(
str(i), make_number_handler(i))
def setup_seek_mode(self):
"""Seek mode bindings."""
@self.player.key_binding('a')
def seek_10():
self.player.seek(10)
@self.player.key_binding('b')
def seek_60():
self.player.seek(60)
@self.player.key_binding('c')
def seek_300():
self.player.seek(300)
# Usage
dynamic = DynamicControls(player)Install with Tessl CLI
npx tessl i tessl/pypi-mpv