adding P-CERAS. A legacy python screen recorder

This commit is contained in:
klein panic
2025-04-13 02:32:14 -04:00
parent 90b736a198
commit f30b6d462c
9 changed files with 1041 additions and 0 deletions

4
P-CERAS/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
venv/
tests/
*.bak
*.~

18
P-CERAS/README.md Normal file
View File

@@ -0,0 +1,18 @@
# Screen and Camera Recorder Program
## Overview
Provides a python-based screen and camera recording system using ALSA and FFMPEG
## Features
- Screen recording
- Camera recording
- Combined Interface
- File Information
## Requirements
- Python3
- TKinter
- OpenCV
- Pillow
- ffmpeg

BIN
P-CERAS/dist/screenrecord vendored Executable file

Binary file not shown.

54
P-CERAS/legacy/install.sh Executable file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env bash
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
VENV_PATH="$PROJECT_ROOT/screenrecordervenv"
if [ ! -d "$VENV_PATH" ]; then
echo "Creating virtual environment at $VENV_PATH..."
python3 -m venv "$VENV_PATH"
if [ $? -ne 0 ]; then
echo "Error: Failed to create virtual environment. Exiting."
exit 1
fi
fi
source "$VENV_PATH/bin/activate"
echo "Installing requirements from requirements.txt..."
pip install -r "$SCRIPT_DIR/requirements.txt"
if [ $? -ne 0 ]; then
echo "Error: Failed to install requirements. Exiting."
deactivate
exit 1
fi
if [ ! -f "$SCRIPT_DIR/start_screenrecorder.sh" ]; then
echo "Error: start_screenrecord.sh not found. Exiting."
deactivate
exit 1
fi
echo "Making start_screenrecord.sh executable..."
chmod +x "$SCRIPT_DIR/start_screenrecorder.sh"
if [ $? -ne 0 ]; then
echo "Error: Failed to make start_screenrecord.sh executable. Exiting."
deactivate
exit 1
fi
if [ ! -L /usr/local/bin/start_screenrecorder ]; then
echo "Linking start_screenrecorder.sh to /usr/local/bin/start_screenrecorder..."
sudo ln -s "$SCRIPT_DIR/start_screenrecorder.sh" /usr/local/bin/start_screenrecorder
if [ $? -ne 0 ]; then
echo "Error: Failed to create symlink. Exiting."
deactivate
exit 1
fi
fi
deactivate
echo "Installation complete. You can now run the screen recorder with 'start_screenrecorder'."

View File

@@ -0,0 +1,282 @@
import os
import subprocess
import tkinter as tk
from tkinter import simpledialog, messagebox
import time
import ffmpeg
import cv2
from PIL import Image, ImageTk
import threading
class ScreenRecorder:
def __init__(self, root, control_frame):
self.root = root
self.is_recording = False
self.start_time = None
self.output_dir = f"{os.environ['HOME']}/Videos/Screenrecords"
self.video_file = None
self.audio_file = None
self.final_file = None
self.video_proc = None
self.audio_proc = None
self.recording_thread = None
self.audio_devices = ['hw:0,7', 'hw:0,6'] # List of audio devices
self.current_device_index = 0
# Place the start/stop button in the control frame
self.toggle_button = tk.Button(control_frame, text="Start", command=self.toggle_recording, width=15, height=2)
self.toggle_button.grid(row=0, column=0, padx=10)
# Other labels and components can be packed in the main window
self.status_label = tk.Label(root, text="Status: Stopped", anchor='w')
self.status_label.pack(fill='x', padx=10, pady=5)
self.mic_label = tk.Label(root, text="Mic: ALSA (hw:0,7)", anchor='w')
self.mic_label.pack(fill='x', padx=10, pady=5)
self.time_label = tk.Label(root, text="Recording Time: 0s", anchor='w')
self.time_label.pack(fill='x', padx=10, pady=5)
self.size_label = tk.Label(root, text="File Size: 0 MB", anchor='w')
self.size_label.pack(fill='x', padx=10, pady=5)
self.duration_label = tk.Label(root, text="Duration: N/A", anchor='w')
self.duration_label.pack(fill='x', padx=10, pady=5)
self.resolution_label = tk.Label(root, text="Resolution: N/A", anchor='w')
self.resolution_label.pack(fill='x', padx=10, pady=5)
self.video_codec_label = tk.Label(root, text="Video Codec: N/A", anchor='w')
self.video_codec_label.pack(fill='x', padx=10, pady=5)
self.audio_codec_label = tk.Label(root, text="Audio Codec: N/A", anchor='w')
self.audio_codec_label.pack(fill='x', padx=10, pady=5)
self.update_info()
def toggle_recording(self):
if self.is_recording:
self.stop_recording()
else:
self.start_recording()
def start_recording(self):
# Start the recording in a separate thread
self.recording_thread = threading.Thread(target=self._start_recording)
self.recording_thread.start()
def _start_recording(self):
self.is_recording = True
self.start_time = time.time()
self.root.after(0, lambda: self.status_label.config(text="Status: Recording"))
self.root.after(0, lambda: self.toggle_button.config(text="Stop"))
# Generate temporary filenames for the recording
self.video_file = os.path.join(self.output_dir, "temp_video.mp4")
self.audio_file = os.path.join(self.output_dir, "temp_audio.wav")
# Start the video recording process
self.video_proc = subprocess.Popen([
'ffmpeg', '-f', 'x11grab', '-s', '1920x1080', '-i', os.environ['DISPLAY'],
'-c:v', 'libx264', '-r', '30', '-preset', 'ultrafast', self.video_file
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
# Try starting the audio recording process
self.start_audio_recording()
self.update_info()
def start_audio_recording(self):
success = False
for i in range(len(self.audio_devices)):
self.current_device_index = i
try:
# Try using the current audio device
self.audio_proc = subprocess.Popen([
'arecord', '-D', self.audio_devices[self.current_device_index], '-f', 'cd', '-t', 'wav', '-r', '16000', self.audio_file
], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
# Allow time for the process to initialize
time.sleep(3)
if self.audio_proc.poll() is not None:
raise Exception(f"Audio device {self.audio_devices[self.current_device_index]} failed to start.")
success = True
self.root.after(0, lambda: self.mic_label.config(text=f"Mic: ALSA ({self.audio_devices[self.current_device_index]})"))
break
except Exception as e:
print(f"Error with audio device {self.audio_devices[self.current_device_index]}: {e}")
if self.audio_proc:
self.audio_proc.terminate()
if not success:
self.root.after(0, lambda: messagebox.showerror("Audio Recording Failed", "Both audio devices failed. Exiting the program."))
self.root.quit()
def stop_recording(self):
if self.video_proc:
self.video_proc.terminate()
try:
self.video_proc.wait(timeout=5)
except subprocess.TimeoutExpired:
self.video_proc.kill()
if self.audio_proc:
self.audio_proc.terminate()
try:
self.audio_proc.wait(timeout=5)
except subprocess.TimeoutExpired:
self.audio_proc.kill()
self.is_recording = False
self.root.after(0, lambda: self.status_label.config(text="Status: Stopped"))
self.root.after(0, lambda: self.toggle_button.config(text="Start"))
if self.recording_thread and self.recording_thread.is_alive():
self.recording_thread.join()
self.combine_audio_video()
def combine_audio_video(self):
# Prompt for file name
file_name = simpledialog.askstring("Save Recording", "Enter file name (leave blank for default):")
if not file_name:
# Generate default file name
existing_files = [f for f in os.listdir(self.output_dir) if f.startswith("screenrecording") and f.endswith(".mp4")]
next_number = len(existing_files) + 1
file_name = f"screenrecording{next_number}.mp4"
else:
file_name += ".mp4"
self.final_file = os.path.join(self.output_dir, file_name)
# Combine audio and video
subprocess.run([
'ffmpeg', '-i', self.video_file, '-i', self.audio_file, '-c:v', 'copy', '-c:a', 'aac', self.final_file
], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# Remove the temporary video and audio files
if os.path.exists(self.video_file):
os.remove(self.video_file)
if os.path.exists(self.audio_file):
os.remove(self.audio_file)
self.display_mp4_info()
self.root.after(0, lambda: self.status_label.config(text=f"Saved as {file_name}"))
def display_mp4_info(self):
try:
probe = ffmpeg.probe(self.final_file)
video_info = next(stream for stream in probe['streams'] if stream['codec_type'] == 'video')
audio_info = next(stream for stream in probe['streams'] if stream['codec_type'] == 'audio')
duration = float(probe['format']['duration'])
self.root.after(0, lambda: self.duration_label.config(text=f"Duration: {duration:.2f} seconds"))
resolution = f"{video_info['width']}x{video_info['height']}"
self.root.after(0, lambda: self.resolution_label.config(text=f"Resolution: {resolution}"))
video_codec = video_info['codec_name']
self.root.after(0, lambda: self.video_codec_label.config(text=f"Video Codec: {video_codec}"))
audio_codec = audio_info['codec_name']
self.root.after(0, lambda: self.audio_codec_label.config(text=f"Audio Codec: {audio_codec}"))
except Exception as e:
print(f"Error displaying MP4 info: {e}")
def update_info(self):
if self.is_recording:
elapsed_time = int(time.time() - self.start_time)
self.root.after(0, lambda: self.time_label.config(text=f"Recording Time: {elapsed_time}s"))
if os.path.exists(self.video_file):
size_mb = os.path.getsize(self.video_file) / (1024 * 1024)
self.root.after(0, lambda: self.size_label.config(text=f"File Size: {size_mb:.2f} MB"))
else:
self.root.after(0, lambda: self.size_label.config(text="File Size: 0 MB"))
self.root.after(1000, self.update_info)
class CameraRecorder:
def __init__(self, root, control_frame):
self.root = root
self.camera_on = False
self.cap = None
# Button to start/stop camera, placed in the control frame
self.camera_button = tk.Button(control_frame, text="Start Camera", command=self.toggle_camera, width=15, height=2)
self.camera_button.grid(row=0, column=1, padx=10)
# Label to display camera feed
self.camera_frame = tk.Label(root)
self.camera_frame.pack(pady=10)
def toggle_camera(self):
if self.camera_on:
self.stop_camera()
else:
self.start_camera()
def start_camera(self):
if messagebox.askyesno("Start Camera", "Are you sure you want to turn on the camera?"):
self.cap = cv2.VideoCapture(0) # Open the default camera
if not self.cap.isOpened():
messagebox.showerror("Camera Error", "Unable to access the camera.")
return
self.camera_on = True
self.camera_button.config(text="Stop Camera")
self.update_camera()
def stop_camera(self):
if self.cap:
self.camera_on = False
self.cap.release()
self.camera_button.config(text="Start Camera")
self.camera_frame.config(image='') # Clear the camera frame
def update_camera(self):
if self.camera_on:
ret, frame = self.cap.read()
if ret:
# Convert the frame to RGB and display it
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img = Image.fromarray(cv2image)
imgtk = ImageTk.PhotoImage(image=img)
self.camera_frame.imgtk = imgtk
self.camera_frame.config(image=imgtk)
self.root.after(10, self.update_camera)
def main():
root = tk.Tk()
root.geometry("450x500") # Increase the window size to fit all elements
# Create a frame to hold the control buttons
control_frame = tk.Frame(root)
control_frame.pack(pady=10)
# Initialize ScreenRecorder and place its button in the control_frame
screen_recorder = ScreenRecorder(root, control_frame)
# Initialize CameraRecorder and place its button in the control_frame
camera_recorder = CameraRecorder(root, control_frame)
# Bind the 'Q' and 'Esc' keys to exit the application globally
def quit_app(event=None):
if not screen_recorder.is_recording and not camera_recorder.camera_on:
root.destroy()
root.bind('<Escape>', quit_app)
root.bind('<q>', quit_app)
root.mainloop()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
SCRIPT_PATH="$(readlink -f "$0")"
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
VENV_PATH="$PROJECT_ROOT/venv"
source "$VENV_PATH/bin/activate"
python3 "$PROJECT_ROOT/src/screenrecord.py" &
deactivate

6
P-CERAS/requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
ffmpeg-python==0.2.0
future==1.0.0
numpy==2.1.0
opencv-python==4.10.0.84
pillow==10.4.0
psutil==6.0.0

470
P-CERAS/screenrecord.py Normal file
View File

@@ -0,0 +1,470 @@
#!/usr/bin/env python3
import os
import re
import subprocess
import tkinter as tk
from tkinter import simpledialog, messagebox, ttk
import time
import ffmpeg
import cv2
from PIL import Image, ImageTk
import threading
import signal
import sys
def get_monitor_geometry(monitor_name):
"""
Query xrandr to get the geometry for the given monitor.
Returns a tuple (resolution, offset) where resolution is "WxH" and
offset is in the form "+X+Y". Returns (None, None) if not found.
"""
try:
output = subprocess.check_output(["xrandr", "--query"], universal_newlines=True)
for line in output.splitlines():
if " connected" in line and monitor_name.lower() in line.lower():
m = re.search(r'(\d+x\d+\+\d+\+\d+)', line)
if m:
geom = m.group(1) # e.g. "1920x1080+0+0"
parts = geom.split('+')
if len(parts) >= 3:
resolution = parts[0] # "1920x1080"
offset = f"+{parts[1]}+{parts[2]}"
return resolution, offset
return None, None
except Exception as e:
print("Error querying xrandr:", e)
return None, None
def select_window_geometry(root):
"""
Uses xwininfo so the user can click on a window.
Returns a geometry string in the format "X,Y,W,H" on success or None on error.
"""
try:
output = subprocess.check_output(["xwininfo"], universal_newlines=True)
x_match = re.search(r'Absolute upper-left X:\s+(\d+)', output)
y_match = re.search(r'Absolute upper-left Y:\s+(\d+)', output)
w_match = re.search(r'Width:\s+(\d+)', output)
h_match = re.search(r'Height:\s+(\d+)', output)
if x_match and y_match and w_match and h_match:
x = int(x_match.group(1))
y = int(y_match.group(1))
w = int(w_match.group(1))
h = int(h_match.group(1))
geometry = f"{x},{y},{w},{h}"
messagebox.showinfo("Window Selected", f"Selected window geometry: {geometry}", parent=root)
return geometry
else:
messagebox.showerror("Selection Error", "Could not parse window geometry.", parent=root)
return None
except Exception as e:
messagebox.showerror("Window Selection Error", f"Error selecting window:\n{e}", parent=root)
return None
class ScreenRecorder:
def __init__(self, root, record_btn, pause_btn, info_frame):
self.root = root
self.record_btn = record_btn # Reference to the record button (for updating text)
self.pause_btn = pause_btn # Reference to the pause/resume button
self.info_frame = info_frame
self.is_recording = False # Overall recording state
self.paused = False # Pause state within a recording
self.start_time = None # Overall start time
self.output_dir = os.path.join(os.environ['HOME'], "Videos", "Screenrecords")
os.makedirs(self.output_dir, exist_ok=True)
self.segments = [] # List of segment file paths
self.current_segment_proc = None # ffmpeg process for current segment
self.current_segment_file = None # Filename for current segment
# Fixed default audio device order.
self.audio_devices = ['hw:0,7', 'hw:0,6']
# For "Window" source selection.
self.selected_window_geometry = None
# Retrieve info labels by name.
self.status_label = info_frame.nametowidget("status_label")
self.mic_label = info_frame.nametowidget("mic_label")
self.time_label = info_frame.nametowidget("time_label")
self.size_label = info_frame.nametowidget("size_label")
self.duration_label = info_frame.nametowidget("duration_label")
self.resolution_label = info_frame.nametowidget("resolution_label")
self.video_codec_label = info_frame.nametowidget("video_codec_label")
self.audio_codec_label = info_frame.nametowidget("audio_codec_label")
self.update_info()
def toggle_recording(self):
if not self.is_recording:
# Start a new recording session.
self.segments = []
self.paused = False
self.start_time = time.time()
self.set_status("Recording")
self.record_btn.config(text="Stop Recording")
self.pause_btn.config(text="Pause Recording", state="normal")
self._start_segment()
self.is_recording = True
else:
# Stop current segment if running.
if self.current_segment_proc:
self._stop_current_segment()
self.is_recording = False
self.set_status("Stopped")
self.record_btn.config(text="Start Recording")
self.pause_btn.config(text="Pause Recording", state="disabled")
self._combine_segments()
def toggle_pause(self):
if not self.is_recording:
return
if not self.paused:
# Pause: stop the current segment.
if self.current_segment_proc:
self._stop_current_segment()
self.paused = True
self.pause_btn.config(text="Resume Recording")
self.set_status("Paused")
else:
# Resume: start a new segment.
self._start_segment()
self.paused = False
self.pause_btn.config(text="Pause Recording")
self.set_status("Recording")
def _start_segment(self):
"""
Starts a new ffmpeg process to record a segment with combined audio and video.
Uses the selected recording source and quality.
"""
# Get options from the GUI variables.
source_option = self.source_var.get() if hasattr(self, 'source_var') else "Entire Desktop"
quality_option = self.quality_var.get() if hasattr(self, 'quality_var') else "Medium"
# Determine capture settings.
if source_option == "Entire Desktop":
width = self.root.winfo_screenwidth()
height = self.root.winfo_screenheight()
resolution = f"{width}x{height}"
display_input = os.environ.get('DISPLAY', ':0.0') + "+0+0"
elif source_option in ["eDP-1", "HDMI-1"]:
res, offset = get_monitor_geometry(source_option)
if res is None:
messagebox.showerror("Monitor Error",
f"Could not get geometry for monitor {source_option}",
parent=self.root)
return
resolution = res
display_input = os.environ.get('DISPLAY', ':0.0') + offset
elif source_option == "Window":
if self.selected_window_geometry is None:
messagebox.showerror("Window Not Selected", "Please click the 'Select Window' button.", parent=self.root)
return
try:
parts = [int(x.strip()) for x in self.selected_window_geometry.split(',')]
if len(parts) != 4:
raise ValueError("Invalid format")
x, y, w, h = parts
resolution = f"{w}x{h}"
display_input = f"{os.environ.get('DISPLAY', ':0.0')}+{x}+{y}"
except Exception:
messagebox.showerror("Window Geometry Error", "Invalid window geometry.", parent=self.root)
return
else:
width = self.root.winfo_screenwidth()
height = self.root.winfo_screenheight()
resolution = f"{width}x{height}"
display_input = os.environ.get('DISPLAY', ':0.0') + "+0+0"
# Map quality setting.
if quality_option.lower() == "low":
preset = "ultrafast"
crf = "30"
elif quality_option.lower() == "high":
preset = "slow"
crf = "18"
else:
preset = "medium"
crf = "23"
# Use first audio device.
audio_dev = self.audio_devices[0]
self.resolution_value = resolution
# Create a new segment file.
seg_index = len(self.segments) + 1
seg_filename = os.path.join(self.output_dir, f"segment_{seg_index}.mp4")
self.current_segment_file = seg_filename
cmd = [
'ffmpeg',
'-f', 'x11grab',
'-video_size', resolution,
'-i', display_input,
'-f', 'alsa',
'-thread_queue_size', '512', # Helps with buffering
'-i', audio_dev,
'-c:v', 'libx264',
'-preset', preset,
'-crf', crf,
'-r', '30',
'-c:a', 'aac',
'-ar', '44100', # Set sample rate explicitly
'-ac', '2', # Force stereo audio
seg_filename
]
self.current_segment_proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def _stop_current_segment(self):
"""
Stops the current ffmpeg process (if any) and adds its file to the segments list.
"""
if self.current_segment_proc:
self.current_segment_proc.terminate()
try:
# self.current_segment_proc.wait(timeout=5)
self.current_segment_proc.communicate(input=b"q", timeout=5)
except subprocess.TimeoutExpired:
self.current_segment_proc.kill()
self.segments.append(self.current_segment_file)
self.current_segment_proc = None
self.current_segment_file = None
def _combine_segments(self):
"""
Combines all recorded segments into a final file using ffmpeg's concat demuxer
and re-encodes the result to fix timestamp issues.
"""
if not self.segments:
messagebox.showerror("No Segments", "No recording segments were recorded.", parent=self.root)
return
list_filename = os.path.join(self.output_dir, "segments.txt")
with open(list_filename, "w") as f:
for seg in self.segments:
f.write(f"file '{seg}'\n")
file_name = simpledialog.askstring("Save Recording",
"Enter file name (leave blank for default):",
parent=self.root)
if not file_name:
existing = [f for f in os.listdir(self.output_dir) if f.startswith("screenrecording") and f.endswith(".mp4")]
file_name = f"screenrecording{len(existing)+1}.mp4"
else:
file_name += ".mp4"
final_file = os.path.join(self.output_dir, file_name)
cmd = [
'ffmpeg',
'-fflags', '+genpts', # Generate new PTS for all frames
'-f', 'concat',
'-safe', '0',
'-i', list_filename,
'-vsync', '2', # Adjust video sync method
'-c:v', 'libx264',
'-preset', 'medium',
'-crf', '23',
'-c:a', 'aac',
'-af', 'aresample=async=1', # Resample audio for sync issues
'-ar', '44100', # Explicitly set sample rate
'-ac', '2', # Force stereo output
final_file
]
subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
for seg in self.segments:
if os.path.exists(seg):
os.remove(seg)
os.remove(list_filename)
self.set_status(f"Saved as {file_name}")
try:
probe = ffmpeg.probe(final_file)
video_info = next(stream for stream in probe['streams'] if stream['codec_type'] == 'video')
audio_info = next(stream for stream in probe['streams'] if stream['codec_type'] == 'audio')
duration = float(probe['format']['duration'])
resolution = f"{video_info['width']}x{video_info['height']}"
video_codec = video_info['codec_name']
audio_codec = audio_info['codec_name']
self.duration_label.config(text=f"Duration: {duration:.2f} sec")
self.resolution_label.config(text=f"Resolution: {resolution}")
self.video_codec_label.config(text=f"Video Codec: {video_codec}")
self.audio_codec_label.config(text=f"Audio Codec: {audio_codec}")
except Exception as e:
print(f"Error displaying final info: {e}")
def set_status(self, text):
self.status_label.config(text=f"Status: {text}")
def update_info(self):
if self.is_recording and not self.paused:
elapsed = int(time.time() - self.start_time)
self.time_label.config(text=f"Recording Time: {elapsed}s")
total_size = 0
for seg in self.segments:
if os.path.exists(seg):
total_size += os.path.getsize(seg)
if self.current_segment_file and os.path.exists(self.current_segment_file):
total_size += os.path.getsize(self.current_segment_file)
self.size_label.config(text=f"File Size: {total_size/1024/1024:.2f} MB")
self.root.after(1000, self.update_info)
class CameraRecorder:
def __init__(self, root, cam_btn, camera_frame):
self.root = root
self.camera_btn = cam_btn
self.camera_frame = camera_frame
self.camera_on = False
self.cap = None
self.resized = False
def toggle_camera(self):
if self.camera_on:
self.stop_camera()
else:
self.start_camera()
def start_camera(self):
if messagebox.askyesno("Start Camera", "Turn on the camera?", parent=self.root):
self.cap = cv2.VideoCapture(0)
if not self.cap.isOpened():
messagebox.showerror("Camera Error", "Unable to access the camera.", parent=self.root)
return
self.camera_on = True
self.camera_btn.config(text="Stop Camera")
self.camera_frame.pack(side="right", padx=10, pady=10)
self.update_camera()
def stop_camera(self):
if self.cap:
self.camera_on = False
self.cap.release()
self.camera_btn.config(text="Start Camera")
self.camera_frame.pack_forget()
self.resized = False
def update_camera(self):
if self.camera_on:
ret, frame = self.cap.read()
if ret:
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img = Image.fromarray(cv2image)
imgtk = ImageTk.PhotoImage(image=img)
self.camera_frame.imgtk = imgtk
self.camera_frame.config(image=imgtk)
if not self.resized:
new_size = 320
self.root.geometry(f"{new_size+400}x{new_size+150}")
self.resized = True
self.root.after(10, self.update_camera)
def quit_app(event=None, screen_recorder=None, camera_recorder=None, root=None):
if screen_recorder and camera_recorder:
if screen_recorder.is_recording or camera_recorder.camera_on:
if not messagebox.askyesno("Quit", "Recording or camera is active. Quit anyway?", parent=root):
return
root.destroy()
def main():
root = tk.Tk()
root.title("Screen Recorder")
root.geometry("600x400")
style = ttk.Style()
style.theme_use('clam')
style.configure("TFrame", background="#e0f7fa")
style.configure("TLabel", background="#e0f7fa", font=("Helvetica", 10))
style.configure("TButton", background="#00796b", foreground="white", font=("Helvetica", 10, "bold"))
main_frame = ttk.Frame(root)
main_frame.pack(fill="both", expand=True)
banner_frame = ttk.Frame(main_frame)
banner_frame.pack(side="top", fill="x", padx=10, pady=10)
banner_label = ttk.Label(banner_frame, text="Screen Recorder", font=("Helvetica", 16, "bold"))
banner_label.pack()
options_frame = ttk.Frame(main_frame)
options_frame.pack(side="top", fill="x", padx=10, pady=10)
src_label = ttk.Label(options_frame, text="Recording Source:")
src_label.grid(row=0, column=0, padx=5, pady=5, sticky="w")
source_var = tk.StringVar(value="Entire Desktop")
src_dropdown = ttk.Combobox(options_frame, textvariable=source_var,
values=["Entire Desktop", "eDP-1", "HDMI-1", "Window"],
state="readonly", width=15)
src_dropdown.grid(row=0, column=1, padx=5, pady=5)
qual_label = ttk.Label(options_frame, text="Recording Quality:")
qual_label.grid(row=1, column=0, padx=5, pady=5, sticky="w")
quality_var = tk.StringVar(value="Medium")
qual_dropdown = ttk.Combobox(options_frame, textvariable=quality_var,
values=["Low", "Medium", "High"],
state="readonly", width=15)
qual_dropdown.grid(row=1, column=1, padx=5, pady=5)
select_window_btn = ttk.Button(options_frame, text="Select Window",
command=lambda: setattr(screen_recorder, 'selected_window_geometry', select_window_geometry(root)))
select_window_btn.grid(row=2, column=0, columnspan=2, padx=5, pady=5)
select_window_btn.grid_remove()
def on_src_change(event):
if source_var.get() == "Window":
select_window_btn.grid()
else:
select_window_btn.grid_remove()
src_dropdown.bind("<<ComboboxSelected>>", on_src_change)
bottom_frame = ttk.Frame(main_frame)
bottom_frame.pack(side="bottom", fill="x", padx=10, pady=10)
info_frame = ttk.Frame(bottom_frame)
info_frame.pack(side="top", fill="x", pady=5)
status_lbl = ttk.Label(info_frame, text="Status: Stopped", name="status_label")
status_lbl.grid(row=0, column=0, sticky="w", padx=5, pady=2)
mic_lbl = ttk.Label(info_frame, text="Mic: ALSA (hw:0,7)", name="mic_label")
mic_lbl.grid(row=1, column=0, sticky="w", padx=5, pady=2)
time_lbl = ttk.Label(info_frame, text="Recording Time: 0s", name="time_label")
time_lbl.grid(row=2, column=0, sticky="w", padx=5, pady=2)
size_lbl = ttk.Label(info_frame, text="File Size: 0 MB", name="size_label")
size_lbl.grid(row=3, column=0, sticky="w", padx=5, pady=2)
dur_lbl = ttk.Label(info_frame, text="Duration: N/A", name="duration_label")
dur_lbl.grid(row=4, column=0, sticky="w", padx=5, pady=2)
res_lbl = ttk.Label(info_frame, text="Resolution: N/A", name="resolution_label")
res_lbl.grid(row=5, column=0, sticky="w", padx=5, pady=2)
vid_codec_lbl = ttk.Label(info_frame, text="Video Codec: N/A", name="video_codec_label")
vid_codec_lbl.grid(row=6, column=0, sticky="w", padx=5, pady=2)
aud_codec_lbl = ttk.Label(info_frame, text="Audio Codec: N/A", name="audio_codec_label")
aud_codec_lbl.grid(row=7, column=0, sticky="w", padx=5, pady=2)
buttons_frame = ttk.Frame(bottom_frame)
buttons_frame.pack(side="bottom", fill="x", pady=5)
record_btn = ttk.Button(buttons_frame, text="Start Recording")
record_btn.pack(side="left", padx=10, pady=5)
pause_btn = ttk.Button(buttons_frame, text="Pause Recording", state="disabled")
pause_btn.pack(side="left", padx=10, pady=5)
cam_btn = ttk.Button(buttons_frame, text="Start Camera")
cam_btn.pack(side="left", padx=10, pady=5)
cam_feed_frame = ttk.Label(main_frame)
cam_feed_frame.pack_forget()
screen_recorder = ScreenRecorder(root, record_btn, pause_btn, info_frame)
camera_recorder = CameraRecorder(root, cam_btn, cam_feed_frame)
screen_recorder.source_var = source_var
screen_recorder.quality_var = quality_var
record_btn.config(command=screen_recorder.toggle_recording)
pause_btn.config(command=screen_recorder.toggle_pause)
cam_btn.config(command=camera_recorder.toggle_camera)
root.bind('<Escape>', lambda event: quit_app(event, screen_recorder, camera_recorder, root))
root.bind('<q>', lambda event: quit_app(event, screen_recorder, camera_recorder, root))
def sigint_handler(sig, frame):
print("Caught SIGINT, exiting gracefully...")
if screen_recorder.is_recording:
screen_recorder.toggle_recording() # Stops recording.
if camera_recorder.camera_on:
camera_recorder.stop_camera()
root.quit()
sys.exit(0)
signal.signal(signal.SIGINT, sigint_handler)
root.mainloop()
if __name__ == "__main__":
main()

194
P-CERAS/setup.py Normal file
View File

@@ -0,0 +1,194 @@
#!/usr/bin/env python3
import argparse
import subprocess
import os
import shutil
import sys
import sysconfig
VERSION = "1.0.7"
HOOK_FILENAME = "hook-ffmpeg.py"
# This hook is used by PyInstaller. It tells PyInstaller to treat "ffmpeg" as a hidden import.
# (PyInstaller sometimes misses modules that are single-file or not recognized as packages.)
HOOK_CONTENT = "hiddenimports = ['ffmpeg']\n"
# Global flag to know if we created the hook file (so we can delete it later)
created_hook = False
def create_ffmpeg_hook():
"""Create a hook file for ffmpeg in the current directory if it doesn't already exist."""
global created_hook
if not os.path.exists(HOOK_FILENAME):
with open(HOOK_FILENAME, "w", encoding="utf-8") as hook_file:
hook_file.write(HOOK_CONTENT)
print(f"Created hook file: {HOOK_FILENAME}")
created_hook = True
def get_extra_pyinstaller_args():
"""
Returns a list of extra arguments for PyInstaller:
- Forces inclusion of the ffmpeg module.
- Adds our additional hooks directory.
- Adds the Python shared library using the correct INSTSONAME.
"""
extra_args = [
"--hidden-import", "ffmpeg",
"--collect-submodules", "ffmpeg",
"--additional-hooks-dir", "."
]
instsoname = sysconfig.get_config_var("INSTSONAME") # e.g., libpython3.11.so.1.0
if instsoname:
libdir = sysconfig.get_config_var("LIBDIR")
libpython_path = os.path.join(libdir, instsoname) if libdir else None
if not libpython_path or not os.path.exists(libpython_path):
fallback = os.path.join("/usr/lib/x86_64-linux-gnu", instsoname)
if os.path.exists(fallback):
libpython_path = fallback
if libpython_path and os.path.exists(libpython_path):
extra_args.extend(["--add-binary", f"{libpython_path}:."])
else:
print("Warning: Could not locate the Python shared library.")
return extra_args
def build_binary():
"""
Builds a one-file binary from screenrecord.py using PyInstaller.
Attempts:
1. A locally installed PyInstaller.
2. pipx if available.
3. Installing PyInstaller in a virtual environment (with --break-system-packages).
Returns True on success, False otherwise.
"""
create_ffmpeg_hook()
pyinstaller_args = ["--onefile"] + get_extra_pyinstaller_args() + ["screenrecord.py"]
# Try using a locally installed PyInstaller.
try:
import PyInstaller.__main__
print("Using locally installed PyInstaller.")
PyInstaller.__main__.run(pyinstaller_args)
return True
except ImportError:
pass
# Try using pipx.
if shutil.which("pipx"):
print("PyInstaller not found locally. Using pipx to run PyInstaller.")
try:
cmd = ["pipx", "run", "pyinstaller"] + pyinstaller_args
subprocess.check_call(cmd)
return True
except subprocess.CalledProcessError:
print("Error: 'pipx run pyinstaller' failed.")
return False
# If pipx isn't available, check if we're in a virtual environment.
if os.environ.get("VIRTUAL_ENV") is not None:
print("In a virtual environment. Installing PyInstaller using pip with --break-system-packages...")
try:
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "--break-system-packages", "pyinstaller"]
)
except subprocess.CalledProcessError:
print("Error: Failed to install PyInstaller in the virtual environment.")
return False
try:
import PyInstaller.__main__
PyInstaller.__main__.run(pyinstaller_args)
return True
except Exception as e:
print("Error: PyInstaller run failed after installation in venv:", e)
return False
print("Error: PyInstaller is not installed and neither pipx nor a virtual environment is available.")
print("Please install pipx or run this installer in a virtual environment, then try again.")
return False
def cleanup():
"""Remove temporary build files and our hook file if we created it."""
dirs_to_remove = ["build"]
files_to_remove = [HOOK_FILENAME, "screenrecord.spec"]
for d in dirs_to_remove:
if os.path.isdir(d):
try:
shutil.rmtree(d)
print(f"Removed directory: {d}")
except Exception as e:
print(f"Warning: Could not remove directory {d}: {e}")
for f in files_to_remove:
if os.path.exists(f):
try:
os.remove(f)
print(f"Removed file: {f}")
except Exception as e:
print(f"Warning: Could not remove file {f}: {e}")
def install():
print("Building binary with PyInstaller...")
if not build_binary():
sys.exit(1)
# Determine the binary name; PyInstaller typically creates "screenrecord" in the dist folder.
binary_name = "screenrecord"
dist_path = os.path.join("dist", binary_name)
if not os.path.exists(dist_path):
# As a fallback, check for a .py extension.
dist_path = os.path.join("dist", "screenrecord.py")
if not os.path.exists(dist_path):
print("Error: Binary not found in the 'dist' folder after build.")
sys.exit(1)
print(f"Built binary located at: {dist_path}")
confirm = input("Do you want to move the binary to ~/.local/bin for global access? [y/N]: ")
if confirm.lower() in ["y", "yes"]:
local_bin = os.path.expanduser("~/.local/bin")
os.makedirs(local_bin, exist_ok=True)
destination = os.path.join(local_bin, binary_name)
try:
shutil.move(dist_path, destination)
print(f"Installation successful! Binary moved to {destination}")
except Exception as e:
print(f"Error moving binary to {destination}: {e}")
sys.exit(1)
else:
print("Installation complete. The binary remains in the 'dist' folder.")
# Cleanup temporary files created by PyInstaller and this installer.
cleanup()
def uninstall():
local_bin = os.path.expanduser("~/.local/bin")
binary_path = os.path.join(local_bin, "screenrecord")
if os.path.exists(binary_path):
try:
os.remove(binary_path)
print(f"Uninstalled: Removed {binary_path}")
except Exception as e:
print(f"Error removing {binary_path}: {e}")
sys.exit(1)
else:
print("Uninstall: 'screenrecord' binary not found in ~/.local/bin.")
def main():
parser = argparse.ArgumentParser(description="Installer for screenrecord.py")
parser.add_argument("command", choices=["install", "uninstall", "version", "help"],
help="Command: install, uninstall, version, or help")
args = parser.parse_args()
if args.command == "install":
install()
elif args.command == "uninstall":
uninstall()
elif args.command == "version":
print(f"screenrecord installer version {VERSION}")
elif args.command == "help":
parser.print_help()
if __name__ == "__main__":
main()