adding P-CERAS. A legacy python screen recorder
This commit is contained in:
4
P-CERAS/.gitignore
vendored
Normal file
4
P-CERAS/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
venv/
|
||||||
|
tests/
|
||||||
|
*.bak
|
||||||
|
*.~
|
||||||
18
P-CERAS/README.md
Normal file
18
P-CERAS/README.md
Normal 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
BIN
P-CERAS/dist/screenrecord
vendored
Executable file
Binary file not shown.
54
P-CERAS/legacy/install.sh
Executable file
54
P-CERAS/legacy/install.sh
Executable 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'."
|
||||||
282
P-CERAS/legacy/screenrecord.py.old
Normal file
282
P-CERAS/legacy/screenrecord.py.old
Normal 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()
|
||||||
13
P-CERAS/legacy/start_screenrecorder.sh
Executable file
13
P-CERAS/legacy/start_screenrecorder.sh
Executable 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
6
P-CERAS/requirements.txt
Normal 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
470
P-CERAS/screenrecord.py
Normal 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
194
P-CERAS/setup.py
Normal 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()
|
||||||
|
|
||||||
Reference in New Issue
Block a user