Untitled
unknown
plain_text
3 months ago
14 kB
6
Indexable
#!/usr/bin/env python3
import sys
import os
import time
import tempfile
import subprocess
import threading
from collections import deque
import tkinter as tk
from tkinter import filedialog, messagebox
# Bootstrap pip if missing. Vibecoded so very ugly, but gets the job done, even if it takes a couple of runs to ensurepip, pip install etc.
try:
import pip
except ImportError:
try:
import ensurepip
ensurepip.bootstrap()
import pip
except Exception:
root = tk.Tk()
root.withdraw()
messagebox.showerror(
"Pip Missing",
"Failed to bootstrap pip.\n Install pip manually."
)
sys.exit(1)
from subprocess import CalledProcessError
def install_package(package):
# Try pip internal API; this usually fails on fresh Python install
try:
from pip._internal import main as pip_main
except ImportError:
try:
from pip import main as pip_main
except ImportError:
pip_main = None
if pip_main:
code = pip_main(['install', package])
if code == 0:
return True, None
return False, f"exit {code}"
# Fallback to subprocess
try:
subprocess.check_call([sys.executable, '-m', 'pip', 'install', package])
return True, None
except CalledProcessError as e:
return False, f"exit {e.returncode}"
def ensure_pynput():
try:
from pynput.keyboard import Listener, Key
return Listener, Key
except ImportError:
root = tk.Tk()
root.withdraw()
install = messagebox.askyesno(
"Missing Dependency",
"The 'pynput' library is required.\nInstall now?"
)
root.destroy()
if not install:
messagebox.showinfo(
"Cannot Continue",
"Please install 'pynput' manually and rerun."
)
sys.exit(1)
success, info = install_package('pynput')
if not success:
messagebox.showerror(
"Installation Failed",
f"Could not install 'pynput' ({info}).\nPlease install manually: pip install pynput"
)
sys.exit(1)
from pynput.keyboard import Listener, Key
return Listener, Key
Listener, Key = ensure_pynput()
# tardwrangling winapi
try:
import win32gui
import win32con
except ImportError:
win32gui = None
win32con = None
def select_region():
"""
Creates a translucent on-top window across the whole screen. No idea what it does on multi-screen setups.
"""
root = tk.Tk()
root.attributes('-fullscreen', True)
root.attributes('-alpha', 0.25)
canvas = tk.Canvas(root, cursor='cross', bg='black')
canvas.pack(fill='both', expand=True)
label = tk.Label(
root,
text="Drag a region to record.",
font=(None, 48),
fg='white',
bg='black'
)
label.place(relx=0.5, rely=0.5, anchor='center')
rect = None
start_x = start_y = 0
coords = {}
def on_press(event):
nonlocal start_x, start_y, rect #YOLO
start_x = event.x
start_y = event.y
rect = canvas.create_rectangle(
start_x, start_y, start_x, start_y,
outline='red', width=2
)
def on_drag(event):
canvas.coords(rect, start_x, start_y, event.x, event.y)
def on_release(event):
x1 = min(start_x, event.x) # this is actually clever, hats off to you Gippity
y1 = min(start_y, event.y)
x2 = max(start_x, event.x)
y2 = max(start_y, event.y)
coords['x'] = x1
coords['y'] = y1
coords['w'] = x2 - x1
coords['h'] = y2 - y1
root.destroy() # goto: return_coods
canvas.bind('<ButtonPress-1>', on_press)
canvas.bind('<B1-Motion>', on_drag)
canvas.bind('<ButtonRelease-1>', on_release)
root.mainloop()
return coords
def select_window_region():
"""
Use win32 api to pick the window under the cursor when clicked, bring that window on top, use its coords for region.
"""
if not win32gui:
messagebox.showerror("Error", "pywin32 is required for window mode.")
return None
root = tk.Tk()
root.attributes('-fullscreen', True)
root.attributes('-alpha', 0.25)
root.config(cursor='cross')
label = tk.Label(
root,
text="Click window to record",
font=(None, 48),
fg='white',
bg='black'
)
label.place(relx=0.5, rely=0.5, anchor='center')
region = {}
def on_click(event):
x_screen = event.x_root
y_screen = event.y_root
root.withdraw()
root.update_idletasks()
time.sleep(0.05)
hwnd = win32gui.WindowFromPoint((x_screen, y_screen))
parent = win32gui.GetParent(hwnd)
while parent:
hwnd = parent
parent = win32gui.GetParent(parent)
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.05)
x, y, x2, y2 = win32gui.GetWindowRect(hwnd)
region['x'] = x
region['y'] = y
region['w'] = x2 - x
region['h'] = y2 - y
print(f"[DEBUG] Window region: {region}")
root.destroy() # goto: return_region
root.bind('<Button-1>', on_click)
root.mainloop()
return region
def ask_options():
dialog = tk.Tk()
dialog.title('Recorder Options')
dialog.resizable(False, False)
mode_var = tk.StringVar(master=dialog, value='desktop')
fps_var = tk.IntVar(master=dialog, value=30)
preset_var = tk.StringVar(master=dialog, value='lossless')
tk.Label(dialog, text='Capture mode:').grid(
row=0, column=0, padx=10, pady=5, sticky='w'
)
modes = [('Desktop', 'desktop'), ('Region', 'region'), ('Window', 'window')]
for i, (text, val) in enumerate(modes, start=1):
tk.Radiobutton(
dialog, text=text, variable=mode_var, value=val
).grid(row=i, column=0, padx=20, sticky='w')
tk.Label(dialog, text='FPS:').grid(
row=0, column=1, padx=10, pady=5, sticky='w'
)
fps_options = [24, 30, 60]
for i, val in enumerate(fps_options, start=1):
tk.Radiobutton(
dialog, text=str(val), variable=fps_var, value=val
).grid(row=i, column=1, padx=20, sticky='w')
tk.Label(dialog, text='Preset:').grid(
row=0, column=2, padx=10, pady=5, sticky='w'
)
presets = [('Lossless MKV', 'lossless'), ('VP9 2-pass WebM', 'vp9')]
for i, (text, val) in enumerate(presets, start=1):
tk.Radiobutton(
dialog, text=text, variable=preset_var, value=val
).grid(row=i, column=2, padx=20, sticky='w')
def on_ok():
dialog.destroy()
def on_cancel():
mode_var.set('')
dialog.destroy()
frame = tk.Frame(dialog)
frame.grid(row=5, column=0, columnspan=3, pady=10)
tk.Button(frame, text='OK', width=10, command=on_ok).pack(side='left', padx=5)
tk.Button(frame, text='Cancel', width=10, command=on_cancel).pack(side='right', padx=5)
dialog.mainloop()
return mode_var.get(), fps_var.get(), preset_var.get()
if __name__ == '__main__':
mode, fps, preset = ask_options()
if not mode:
sys.exit(0)
if mode == 'region':
config = select_region()
if not config:
sys.exit(0)
elif mode == 'window':
config = select_window_region()
if not config:
sys.exit(0)
else:
config = {}
# Prepare temp files and FFmpeg window-hiding flags
output_ext = 'webm' if preset == 'vp9' else 'mkv'
temp_output = tempfile.NamedTemporaryFile(delete=False, suffix=f'.{output_ext}')
temp_output.close()
creationflags = 0
startupinfo = None
if os.name == 'nt':
creationflags = subprocess.CREATE_NO_WINDOW
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
# Base ffmpeg args
args_base = ['ffmpeg', '-y', '-f', 'gdigrab', '-framerate', str(fps)]
if mode in ('region', 'window'):
args_base += [
'-offset_x', str(config['x']),
'-offset_y', str(config['y']),
'-video_size', f"{config['w']}x{config['h']}",
'-i', 'desktop'
]
else:
args_base += ['-i', 'desktop']
if preset == 'lossless':
# Single-pass lossless
args = args_base + [
'-c:v', 'libx264rgb', '-preset', 'ultrafast', '-crf', '0',
temp_output.name
]
print(f"[DEBUG] Recording command: {' '.join(args)}")
proc = subprocess.Popen(
args, stdin=subprocess.PIPE,
creationflags=creationflags,
startupinfo=startupinfo
)
esc_times = deque(maxlen=5)
stop_event = threading.Event()
def on_key(key):
if key == Key.esc:
esc_times.append(time.time())
if len(esc_times) == 5 and (esc_times[-1] - esc_times[0] <= 3.0):
stop_event.set()
return False
listener = Listener(on_press=on_key)
listener.start()
stop_event.wait()
try:
proc.stdin.write(b'q')
proc.stdin.flush()
except Exception:
pass
proc.wait()
final_file = temp_output.name
else:
# Record lossless first for anon's VP9 2-pass
loss_temp = tempfile.NamedTemporaryFile(delete=False, suffix='.mkv')
loss_temp.close()
args_loss = args_base + [
'-c:v', 'libx264rgb', '-preset', 'ultrafast', '-crf', '0',
loss_temp.name
]
print(f"[DEBUG] Recording lossless for VP9 passes: {' '.join(args_loss)}")
proc = subprocess.Popen(
args_loss, stdin=subprocess.PIPE,
creationflags=creationflags,
startupinfo=startupinfo
)
esc_times = deque(maxlen=5)
stop_event = threading.Event()
def on_key_loss(key):
if key == Key.esc:
esc_times.append(time.time())
if len(esc_times) == 5 and (esc_times[-1] - esc_times[0] <= 3.0):
stop_event.set()
return False
listener = Listener(on_press=on_key_loss)
listener.start()
stop_event.wait()
try:
proc.stdin.write(b'q')
proc.stdin.flush()
except Exception:
pass
proc.wait()
print("[DEBUG] Lossless recording completed")
# Two-pass VP9 encode
print("[DEBUG] Beginning VP9 2-pass encode")
pass1 = args_loss[:]
pass1[0] = 'ffmpeg'
pass1 = [
'ffmpeg', '-y', '-i', loss_temp.name,
'-c:v', 'libvpx-vp9', '-pix_fmt', 'yuv420p', '-auto-alt-ref', '6',
'-b:v', '0', '-crf', '30', '-cpu-used', '0', '-aq-mode', '2',
'-tile-columns', '1', '-row-mt', '1',
'-pass', '1',
'-an', '-f', 'null', '-'
]
print(f"[DEBUG] VP9 Pass 1 cmd: {' '.join(pass1)}")
result1 = subprocess.run(
pass1,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
creationflags=creationflags,
startupinfo=startupinfo
)
if result1.returncode != 0:
print(f"[ERROR] Pass 1 failed (code={result1.returncode})")
print("=== FFmpeg Pass 1 stderr ===")
print(result1.stderr)
sys.exit(1)
print("[DEBUG] Pass 1 completed successfully")
pass2 = [
'ffmpeg', '-y', '-i', loss_temp.name,
'-c:v', 'libvpx-vp9', '-pix_fmt', 'yuv420p', '-auto-alt-ref', '6',
'-b:v', '0', '-crf', '30', '-cpu-used', '0', '-aq-mode', '2',
'-tile-columns', '1', '-row-mt', '1',
'-pass', '2',
'-an', temp_output.name
]
print(f"[DEBUG] VP9 Pass 2 cmd: {' '.join(pass2)}")
result2 = subprocess.run(
pass2,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
creationflags=creationflags,
startupinfo=startupinfo
)
if result2.returncode != 0:
print(f"[ERROR] Pass 2 failed (code={result2.returncode})")
print("=== FFmpeg Pass 2 stderr ===")
print(result2.stderr)
sys.exit(1)
print("[DEBUG] Pass 2 completed successfully")
os.remove(loss_temp.name)
for log in ('ffmpeg2pass-0.log', 'ffmpeg2pass-0.log.mkv'):
try:
os.remove(log)
except OSError:
pass
final_file = temp_output.name
root = tk.Tk()
root.withdraw()
save_path = filedialog.asksaveasfilename(
defaultextension=f'.{output_ext}',
filetypes=[(preset.upper(), f'*.{output_ext}'), ('All Files', '*.*')],
title="Save recording as…"
)
root.destroy()
if save_path:
os.replace(final_file, save_path)
print(f"Saved to: {save_path}")
else:
os.remove(final_file)
print("Recording discarded.")Editor is loading...
Leave a Comment