diff --git a/P-CERAS/.gitignore b/P-CERAS/.gitignore new file mode 100644 index 0000000..b146941 --- /dev/null +++ b/P-CERAS/.gitignore @@ -0,0 +1,4 @@ +venv/ +tests/ +*.bak +*.~ diff --git a/P-CERAS/README.md b/P-CERAS/README.md new file mode 100644 index 0000000..ccd751e --- /dev/null +++ b/P-CERAS/README.md @@ -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 diff --git a/P-CERAS/dist/screenrecord b/P-CERAS/dist/screenrecord new file mode 100755 index 0000000..a9af555 Binary files /dev/null and b/P-CERAS/dist/screenrecord differ diff --git a/P-CERAS/legacy/install.sh b/P-CERAS/legacy/install.sh new file mode 100755 index 0000000..0c86ac6 --- /dev/null +++ b/P-CERAS/legacy/install.sh @@ -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'." diff --git a/P-CERAS/legacy/screenrecord.py.old b/P-CERAS/legacy/screenrecord.py.old new file mode 100644 index 0000000..988bd40 --- /dev/null +++ b/P-CERAS/legacy/screenrecord.py.old @@ -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('', quit_app) + root.bind('', quit_app) + + root.mainloop() + + +if __name__ == "__main__": + main() diff --git a/P-CERAS/legacy/start_screenrecorder.sh b/P-CERAS/legacy/start_screenrecorder.sh new file mode 100755 index 0000000..e9983af --- /dev/null +++ b/P-CERAS/legacy/start_screenrecorder.sh @@ -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 diff --git a/P-CERAS/requirements.txt b/P-CERAS/requirements.txt new file mode 100644 index 0000000..d94fc1b --- /dev/null +++ b/P-CERAS/requirements.txt @@ -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 diff --git a/P-CERAS/screenrecord.py b/P-CERAS/screenrecord.py new file mode 100644 index 0000000..d2e6d21 --- /dev/null +++ b/P-CERAS/screenrecord.py @@ -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("<>", 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('', lambda event: quit_app(event, screen_recorder, camera_recorder, root)) + root.bind('', 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() + diff --git a/P-CERAS/setup.py b/P-CERAS/setup.py new file mode 100644 index 0000000..bfe4a87 --- /dev/null +++ b/P-CERAS/setup.py @@ -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() +