VLC — Python Manipulation Guide
SkillAI & modelsHow to programmatically configure, control, and verify VLC media player state using Python (vlcrc config, HTTP interface, media comparison). For setup-gen and reward-gen agents.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the VLC — Python Manipulation Guide skill
What this skill tells your AI
The instructions your AI receives, as published by xlang-ai/cua-gym in .claude/skills/vlc/SKILL.md and read by ahel’s review.
This skill teaches setup-gen (configure/launch VLC) and reward-gen (verify playback/config/media) how to work with VLC using Python.
- Libraries:
subprocess,requests,xml.etree.ElementTree,librosa,cv2,imagehash,Pillow - Install:
pip3 install requests librosa fastdtw scipy scikit-image opencv-python imagehash Pillow - Config path (Linux):
~/.config/vlc/vlcrc
0. GUI Startup on VM (for setup-gen)
After preparing media/config, setup-gen should open VLC (and target media if required) for the GUI agent.
CRITICAL VM LIMIT: GUI launches must set DISPLAY=:0.
import os
import shlex
import subprocess
import time
def launch_gui(command: str, delay_sec: float = 1.0):
env = os.environ.copy()
env["DISPLAY"] = ":0"
subprocess.Popen(
shlex.split(command),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=env,
)
time.sleep(delay_sec)
# Launch VLC with a target file
launch_gui('vlc "/home/user/Desktop/video.mp4"', delay_sec=2.0)
Guidelines:
- Use non-blocking launch (
Popen) so script exits cleanly. - Keep startup deterministic with short sleeps.
0.5. Media Asset Library (setup-gen)
Use the real video/audio library at assets/media/vlc/ instead of generating synthetic test patterns.
Available Categories
| Category | Count | Description | Good for |
|---|---|---|---|
videos | ~30 | Real short clips: nature, city, cooking, aerial, rain, sunset | Playback, format conversion, trimming, effects |
subtitles | 5 | Sample .srt files (English dialogue, tutorial, movie, cooking, multilingual) | Subtitle loading, track switching, delay settings |
Audio files can be extracted from videos on the VM using FFmpeg (see Section 1).
Upload Pattern
# Upload video to BOTH VMs
python3 scripts/env_cli.py -c "<workdir>/env_config_initial.json" upload \
"assets/media/vlc/videos/nature_timelapse_001.mp4" "/home/user/<task_id>.mp4"
python3 scripts/env_cli.py -c "<workdir>/env_config_golden.json" upload \
"assets/media/vlc/videos/nature_timelapse_001.mp4" "/home/user/<task_id>.mp4"
# Upload subtitle if needed
python3 scripts/env_cli.py -c "<workdir>/env_config_initial.json" upload \
"assets/media/vlc/subtitles/english_dialogue.srt" "/home/user/<task_id>.srt"
Then in initial_setup.py:
import shutil
shutil.copy(f'{WORKDIR}/{TASK_ID}.mp4', f'{WORKDIR}/{TASK_ID}_initial_reference.mp4')
launch_gui(f'vlc "{WORKDIR}/{TASK_ID}.mp4"', delay_sec=2.0)
Picking Assets from Manifest
import json, random
manifest = json.load(open("assets/media/manifest.json"))
vlc_videos = [a for a in manifest["assets"] if a["domain"] == "vlc" and a["category"] == "videos"]
# Pick a nature video
nature = [v for v in vlc_videos if "nature" in v.get("tags", [])]
asset = random.choice(nature)
1. Creating & Configuring (setup-gen)
vlcrc Configuration File
The vlcrc file uses key=value format. Commented lines start with #. A commented-out key means "use default".
import os, re, shutil, subprocess
VLCRC_PATH = os.path.expanduser("~/.config/vlc/vlcrc")
def read_vlcrc() -> str:
with open(VLCRC_PATH, "r") as f:
return f.read()
def set_vlcrc_option(key: str, value: str):
"""Set a vlcrc option. Uncomments the key if commented out."""
content = read_vlcrc()
# Pattern: match both commented (#key=...) and uncommented (key=...)
pattern = re.compile(rf'^(#?\s*){re.escape(key)}=.*$', re.MULTILINE)
replacement = f'{key}={value}'
if pattern.search(content):
content = pattern.sub(replacement, content)
else:
content += f'\n{key}={value}\n'
with open(VLCRC_PATH, "w") as f:
f.write(content)
# Examples — all config keys the evaluator checks:
set_vlcrc_option("qt-bgcone", "0") # splash screen cone: 0=off, 1=on (default: 1)
set_vlcrc_option("qt-max-volume", "200") # max volume: integer (default: 125)
set_vlcrc_option("qt-minimal-view", "1") # minimal interface: 0=normal, 1=minimal (default: 0)
set_vlcrc_option("qt-slider-colours", "10;10;10;10;10;10;10;10;10;10;10;10") # R;G;B groups
set_vlcrc_option("global-key-play-pause", "Space") # global hotkey: empty=""=disabled, key=enabled
set_vlcrc_option("one-instance-when-started-from-file", "0") # single instance: 0=off, 1=on (default: 1)
set_vlcrc_option("play-and-exit", "0") # auto-close after playback: 0=off, 1=on (default: 0)
set_vlcrc_option("input-record-path", "/home/user/Desktop") # recording output path
Launching VLC
# Play a local file
subprocess.Popen(["vlc", "/home/user/Desktop/video.mp4"])
# Play with specific options
subprocess.Popen([
"vlc",
"--no-audio", # mute audio
"--no-video-title-show", # hide title overlay
"--start-time=10", # start at 10 seconds
"--stop-time=30", # stop at 30 seconds
"--play-and-pause", # pause when reaching stop time
"/home/user/Desktop/video.mp4"
])
# Play with HTTP interface enabled (for status monitoring)
subprocess.Popen([
"vlc",
"--extraintf=http",
"--http-password=password",
"--http-port=8080",
"/home/user/Desktop/video.mp4"
])
# Stream an HLS URL
subprocess.Popen(["vlc", "https://example.com/stream/master.m3u8"])
# Suppress verbose output
env = os.environ.copy()
env["VLC_VERBOSE"] = "-1"
subprocess.Popen(["vlc", "/home/user/Desktop/video.mp4"], env=env)
Killing and Restarting VLC
# Kill VLC (needed before modifying vlcrc)
subprocess.run(["pkill", "-f", "vlc"], capture_output=True)
import time; time.sleep(2)
# Restart VLC to pick up config changes
subprocess.Popen(["vlc", "--extraintf=http", "--http-password=password"])
Creating Media Files for Testing
# Generate silent audio with ffmpeg
subprocess.run([
"ffmpeg", "-f", "lavfi", "-i", "anullsrc=r=44100:cl=mono",
"-t", "5", "-q:a", "9", "/home/user/Desktop/silence.mp3"
], check=True)
# Generate test video (color bars)
subprocess.run([
"ffmpeg", "-f", "lavfi", "-i", "testsrc=duration=5:size=640x480:rate=30",
"-pix_fmt", "yuv420p", "/home/user/Desktop/test.mp4"
], check=True)
# Extract audio from video
subprocess.run([
"ffmpeg", "-i", "/home/user/Desktop/video.mp4",
"-vn", "-acodec", "libmp3lame", "/home/user/Desktop/audio.mp3"
], check=True)
# Capture a frame as screenshot
subprocess.run([
"ffmpeg", "-i", "/home/user/Desktop/video.mp4",
"-ss", "00:00:05", "-frames:v", "1", "/home/user/Desktop/frame.png"
], check=True)
# Rotate video
subprocess.run([
"ffmpeg", "-i", "/home/user/Desktop/input.mp4",
"-vf", "transpose=1", "/home/user/Desktop/rotated.mp4"
], check=True)
2. Reading & Verifying (reward-gen)
Verifying vlcrc Config Options
def get_vlcrc_option(key: str, default: str = None) -> str:
"""Read a vlcrc option value. Returns default if key is commented or missing."""
content = read_vlcrc()
for line in content.split("\n"):
stripped = line.strip()
if stripped.startswith("#") or not stripped:
continue
if f"{key}=" in stripped:
return stripped.split("=", 1)[-1].strip()
return default
# Config verification helpers with correct defaults
CONFIG_DEFAULTS = {
"qt-bgcone": "1",
"qt-max-volume": "125",
"qt-minimal-view": "0",
"qt-slider-colours": "153;210;153;20;210;20;255;199;15;245;39;29",
"one-instance-when-started-from-file": "1",
"play-and-exit": "0",
}
def verify_vlcrc_option(key: str, expected: str) -> bool:
actual = get_vlcrc_option(key, CONFIG_DEFAULTS.get(key))
return str(actual) == str(expected)
# Verify global-key-play-pause (special: empty = disabled, non-empty = enabled)
def verify_global_key_enabled(expected_enabled: bool) -> bool:
val = get_vlcrc_option("global-key-play-pause", "")
is_enabled = val != ""
return is_enabled == expected_enabled
# Verify slider colors are "blackish" (all RGB values < 100)
def verify_slider_blackish() -> bool:
colours = get_vlcrc_option("qt-slider-colours", CONFIG_DEFAULTS["qt-slider-colours"])
values = [int(x) for x in colours.split(";")]
return all(v < 100 for v in values)
# Verify recording path
def verify_recording_path(expected_path: str) -> bool:
return get_vlcrc_option("input-record-path") == expected_path
Verifying VLC Playback Status (HTTP Interface)
import requests
from xml.etree import ElementTree
def get_vlc_status(host: str = "localhost", port: int = 8080, password: str = "password") -> ElementTree.Element:
"""Fetch VLC status XML via HTTP interface."""
resp = requests.get(f"http://{host}:{port}/requests/status.xml", auth=("", password), timeout=5)
return ElementTree.fromstring(resp.content)
def verify_vlc_playing(expected_filename: str = None, expected_url: str = None,
host: str = "localhost", port: int = 8080) -> bool:
tree = get_vlc_status(host, port)
state = tree.find("state").text
if state != "playing":
return False
if expected_filename:
# Check multiple XML paths for filename
for xpath in [
'information/category[@name="meta"]/info[@name="filename"]',
'information/category[@name="meta"]/info[@name="title"]',
'information/category[@name="meta"]/info[@name="uri"]',
'information/category[@name="meta"]/info[@name="name"]',
]:
elem = tree.find(xpath)
if elem is not None and elem.text:
if os.path.basename(elem.text) == expected_filename or elem.text.endswith(expected_filename):
return True
return False
if expected_url:
from urllib.parse import urlparse
for xpath in [
'information/category[@name="meta"]/info[@name="url"]',
'information/category[@name="meta"]/info[@name="filename"]',
'information/category[@name="meta"]/info[@name="title"]',
]:
elem = tree.find(xpath)
if elem is not None and elem.text:
if expected_url in elem.text:
return True
# HLS streams may show just the filename
if elem.text == os.path.basename(urlparse(expected_url).path):
return True
return False
return True # just verify playing state
Verifying Fullscreen Mode
def verify_fullscreen(window_size: dict, screen_size: dict) -> bool:
"""Compare window size to screen size."""
if not window_size or not screen_size:
return False
return (window_size["width"] == screen_size["width"] and
window_size["height"] == screen_size["height"])
Comparing Audio Files (MFCC + DTW)
import librosa
import numpy as np
from fastdtw import fastdtw
from scipy.spatial.distance import cosine
def compare_audios(audio1: str, audio2: str) -> float:
"""Compare two audio files using MFCC features + DTW. Returns 0.0-1.0."""
try:
y1, sr1 = librosa.load(audio1)
y2, sr2 = librosa.load(audio2)
except Exception:
return 0.0
if y1.shape[0] == 0 or y2.shape[0] == 0:
return 0.0
mfcc1 = librosa.util.normalize(librosa.feature.mfcc(y=y1, sr=sr1), axis=1)
mfcc2 = librosa.util.normalize(librosa.feature.mfcc(y=y2, sr=sr2), axis=1)
distance, path = fastdtw(mfcc1.T, mfcc2.T, dist=lambda x, y: cosine(x, y))
normalized = distance / len(path) if path else float("inf")
return float(np.exp(-normalized))
Comparing Video Files (Perceptual Hashing)
import cv2, imagehash
from PIL import Image
def compare_videos(video1: str, video2: str, max_frames: int = 100, threshold: int = 5) -> float:
"""Compare two videos frame-by-frame using perceptual hashing. Returns 0.0-1.0."""
cap1, cap2 = cv2.VideoCapture(video1), cv2.VideoCapture(video2)
total, matches = 0, 0
for _ in range(max_frames):
ret1, f1 = cap1.read()
ret2, f2 = cap2.read()
if not ret1 or not ret2:
if ret1 == ret2: # both ended
break
cap1.release(); cap2.release()
return matches / max(total, 1)
h1 = imagehash.phash(Image.fromarray(cv2.cvtColor(f1, cv2.COLOR_BGR2RGB)))
h2 = imagehash.phash(Image.fromarray(cv2.cvtColor(f2, cv2.COLOR_BGR2RGB)))
total += 1
if h1 - h2 <= threshold:
matches += 1
cap1.release(); cap2.release()
return matches / max(total, 1)
Comparing Images (SSIM)
from skimage.metrics import structural_similarity as ssim
def compare_images(img1_path: str, img2_path: str, base_score: float = None) -> float:
"""Compare two images using SSIM. Returns 0.0-1.0."""
img1 = Image.open(img1_path).convert("L")
img2 = Image.open(img2_path).convert("L")
# Resize both to the smaller width and height independently
new_size = (min(img1.size[0], img2.size[0]), min(img1.size[1], img2.size[1]))
img1 = img1.resize(new_size, Image.Resampling.LANCZOS)
img2 = img2.resize(new_size, Image.Resampling.LANCZOS)
score = ssim(np.array(img1), np.array(img2))
if base_score is not None:
return max(0, (score - base_score) / (1 - base_score)) if score >= base_score + 0.01 else 0
return score
3. Playlist Operations (setup-gen & reward-gen)
Reading Playlist via HTTP Interface
def get_vlc_playlist(host: str = "localhost", port: int = 8080, password: str = "password") -> list:
"""Fetch VLC playlist as list of dicts with id, name, uri, duration."""
resp = requests.get(f"http://{host}:{port}/requests/playlist.xml", auth=("", password), timeout=5)
tree = ElementTree.fromstring(resp.content)
items = []
for leaf in tree.iter("leaf"):
items.append({
"id": leaf.get("id"),
"name": leaf.get("name", ""),
"uri": leaf.get("uri", ""),
"duration": int(leaf.get("duration", 0)),
})
return items
def verify_playlist_count(expected_count: int, host: str = "localhost", port: int = 8080) -> bool:
"""Verify the number of items in the playlist."""
items = get_vlc_playlist(host, port)
return len(items) == expected_count
def verify_playlist_order(expected_names: list, host: str = "localhost", port: int = 8080) -> bool:
"""Verify playlist item order by filename substrings."""
items = get_vlc_playlist(host, port)
if len(items) != len(expected_names):
return False
for item, expected in zip(items, expected_names):
if expected.lower() not in item["name"].lower() and expected.lower() not in item["uri"].lower():
return False
return True
def verify_playlist_contains(filename: str, host: str = "localhost", port: int = 8080) -> bool:
"""Check if a specific file is in the playlist."""
items = get_vlc_playlist(host, port)
return any(filename.lower() in item["name"].lower() or filename.lower() in item["uri"].lower()
for item in items)
Adding Items to Playlist via HTTP
def vlc_playlist_add(uri: str, host: str = "localhost", port: int = 8080, password: str = "password"):
"""Add a media file to VLC playlist via HTTP interface."""
from urllib.parse import quote
requests.get(
f"http://{host}:{port}/requests/status.xml?command=in_enqueue&input={quote(uri)}",
auth=("", password), timeout=5
)
def vlc_playlist_clear(host: str = "localhost", port: int = 8080, password: str = "password"):
"""Clear the VLC playlist."""
requests.get(
f"http://{host}:{port}/requests/status.xml?command=pl_empty",
auth=("", password), timeout=5
)
4. Subtitle Operations (setup-gen & reward-gen)
Creating Subtitle Files
def create_srt_file(output_path: str, entries: list):
"""Create an SRT subtitle file.
entries: list of (index, start_time, end_time, text) tuples
start_time/end_time format: "HH:MM:SS,mmm"
"""
with open(output_path, "w", encoding="utf-8") as f:
for idx, start, end, text in entries:
f.write(f"{idx}\n{start} --> {end}\n{text}\n\n")
# Example
create_srt_file("/home/user/Desktop/subs.srt", [
(1, "00:00:01,000", "00:00:04,000", "Hello, welcome!"),
(2, "00:00:05,000", "00:00:08,000", "This is a subtitle test."),
(3, "00:00:09,000", "00:00:12,000", "Goodbye!"),
])
VLC Subtitle Configuration (vlcrc)
# Subtitle-related vlcrc options
set_vlcrc_option("sub-auto-detect-file", "1") # auto-detect subtitle files: 0=off, 1=on
set_vlcrc_option("sub-autodetect-path", ".") # subtitle search paths
set_vlcrc_option("sub-language", "en") # preferred subtitle language
set_vlcrc_option("freetype-fontsize", "24") # subtitle font size
set_vlcrc_option("freetype-color", "16777215") # subtitle color (decimal RGB, 16777215 = white)
set_vlcrc_option("freetype-rel-fontsize", "16") # relative font size: 10=smaller, 20=small, 16=normal
set_vlcrc_option("sub-margin", "50") # subtitle margin from bottom (pixels)
Verifying Subtitle State via HTTP
def verify_subtitle_track(expected_track_id: int = None,
host: str = "localhost", port: int = 8080) -> bool:
"""Verify current subtitle track. track_id=-1 means subtitles disabled."""
tree = get_vlc_status(host, port)
for cat in tree.findall('information/category'):
if "subtitle" in cat.get("name", "").lower() or cat.get("name") == "Subtitle":
return True # subtitle track exists
# Check via streams info
for info in tree.findall('.//info[@name="Type"]'):
if info.text and "subtitle" in info.text.lower():
return True
return expected_track_id == -1 # if expecting disabled, return True when not found
def verify_srt_content(srt_path: str, expected_entries: int = None,
contains_text: str = None) -> bool:
"""Verify SRT file content."""
if not os.path.isfile(srt_path):
return False
with open(srt_path, "r", encoding="utf-8") as f:
content = f.read()
blocks = [b.strip() for b in content.strip().split("\n\n") if b.strip()]
if expected_entries is not None and len(blocks) != expected_entries:
return False
if contains_text is not None and contains_text not in content:
return False
return True
5. Audio Metadata Verification
import json, subprocess
def get_audio_info(audio_path: str) -> dict:
"""Extract audio metadata using FFprobe."""
result = subprocess.run([
"ffprobe", "-v", "quiet", "-print_format", "json",
"-show_format", "-show_streams", audio_path
], capture_output=True, text=True)
return json.loads(result.stdout) if result.stdout else {}
def verify_audio_sample_rate(audio_path: str, expected_rate: int) -> bool:
"""Verify audio sample rate (e.g., 44100, 48000)."""
info = get_audio_info(audio_path)
for stream in info.get("streams", []):
if stream.get("codec_type") == "audio":
return int(stream.get("sample_rate", 0)) == expected_rate
return False
def verify_audio_channels(audio_path: str, expected_channels: int) -> bool:
"""Verify audio channel count (1=mono, 2=stereo)."""
info = get_audio_info(audio_path)
for stream in info.get("streams", []):
if stream.get("codec_type") == "audio":
return stream.get("channels") == expected_channels
return False
def verify_audio_bitrate(audio_path: str, min_bitrate: int = None, max_bitrate: int = None) -> bool:
"""Verify audio bitrate (bits/sec). Use format-level bitrate as fallback."""
info = get_audio_info(audio_path)
bitrate = None
for stream in info.get("streams", []):
if stream.get("codec_type") == "audio" and stream.get("bit_rate"):
bitrate = int(stream["bit_rate"])
break
if bitrate is None:
bitrate = int(info.get("format", {}).get("bit_rate", 0))
if bitrate == 0:
return False
if min_bitrate is not None and bitrate < min_bitrate:
return False
if max_bitrate is not None and bitrate > max_bitrate:
return False
return True
def verify_audio_duration(audio_path: str, expected_duration: float, tolerance: float = 1.0) -> bool:
"""Verify audio file duration in seconds."""
info = get_audio_info(audio_path)
duration = float(info.get("format", {}).get("duration", 0))
return abs(duration - expected_duration) <= tolerance
def verify_audio_codec(audio_path: str, expected_codec: str) -> bool:
"""Verify audio codec (e.g., 'mp3', 'aac', 'vorbis', 'pcm_s16le')."""
info = get_audio_info(audio_path)
for stream in info.get("streams", []):
if stream.get("codec_type") == "audio":
return expected_codec.lower() in stream.get("codec_name", "").lower()
return False
6. Gradual Scoring Pattern (reward-gen)
def compute_reward(task_type: str = "config", config_checks: list = None,
media_src: str = None, media_golden: str = None,
playback_checks: list = None) -> float:
"""Compute a 0.0-1.0 reward score for VLC tasks.
task_type: "config" (vlcrc settings), "media" (audio/video manipulation),
"playback" (player state), "mixed" (combination)
config_checks: list of (check_fn, weight) tuples for vlcrc verification
playback_checks: list of (check_fn, weight) tuples for HTTP status checks
"""
score = 0.0
total_weight = 0.0
# Dimension 1: Config verification (weight: 0.4 for config tasks)
if config_checks:
w = 0.4 if task_type == "config" else 0.2
total_weight += w
passed = sum(1 for fn, _ in config_checks if fn())
score += w * (passed / len(config_checks))
# Dimension 2: Media file comparison (weight: 0.4 for media tasks)
if media_src and media_golden:
w = 0.4 if task_type == "media" else 0.25
total_weight += w
sub_score = 0.0
# Check file existence
if os.path.isfile(media_src):
sub_score += 0.2
# Duration match
src_info = get_audio_info(media_src) if media_src.endswith((".mp3", ".wav", ".ogg")) else {}
gld_info = get_audio_info(media_golden) if media_golden.endswith((".mp3", ".wav", ".ogg")) else {}
src_dur = float(src_info.get("format", {}).get("duration", 0))
gld_dur = float(gld_info.get("format", {}).get("duration", 0))
if gld_dur > 0 and abs(src_dur - gld_dur) < 2.0:
sub_score += 0.3
# Content similarity (audio or video)
try:
if media_src.endswith((".mp3", ".wav", ".ogg", ".flac")):
similarity = compare_audios(media_src, media_golden)
else:
similarity = compare_videos(media_src, media_golden)
sub_score += 0.5 * similarity
except Exception:
pass
score += w * sub_score
# Dimension 3: Playback state (weight: 0.3 for playback tasks)
if playback_checks:
w = 0.3 if task_type == "playback" else 0.15
total_weight += w
passed = sum(1 for fn, _ in playback_checks if fn())
score += w * (passed / len(playback_checks))
# Dimension 4: VLC process is running (weight: 0.1 — sanity check)
w = 0.1
total_weight += w
result = subprocess.run(["pgrep", "-f", "vlc"], capture_output=True)
if result.returncode == 0:
score += w
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 197
- Forks
- 18
- Last commit
- Aug 2026
ahel review
K1binfo
installs-packages
Automated review, not a security audit. Ruleset v1+k2.
Advanced
- Catalog kind
- skill
- Gateway key
vlc- Source
- github.com/xlang-ai/cua-gym