Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
615eba2
oa_pynput -> pynput; handle linux
abrichr Dec 28, 2024
a4ae541
fix
abrichr Dec 29, 2024
ac9753f
if not impl return None
abrichr Dec 29, 2024
e6aa792
add window/_linux.py
abrichr Dec 29, 2024
c1d36d6
global X server connection
abrichr Dec 29, 2024
b87f404
byte order native
abrichr Dec 29, 2024
6ad1027
window_id_bytes
abrichr Dec 29, 2024
d47b90f
get_sct()
abrichr Dec 29, 2024
d2c4d14
get_thread_local_sct
abrichr Dec 29, 2024
6b103e3
Fallback to WM_NAME
abrichr Dec 29, 2024
34790c8
convert bytes with join
abrichr Dec 29, 2024
87e1194
task_started_events
abrichr Dec 30, 2024
4b20fcb
thread_local -> process_local
abrichr Dec 30, 2024
3ec5da6
threading.local -> multiprocessing.local
abrichr Dec 30, 2024
8dc38f6
multiprocessing_utils; xcffib
abrichr Dec 30, 2024
4fa4d4e
global monitor_width/monitor_height
abrichr Dec 30, 2024
2462941
black
abrichr Dec 30, 2024
b38df78
add capture._linux
abrichr Dec 30, 2024
1be7c88
capture._linux.get_screen_resolution
abrichr Dec 30, 2024
ee10863
cleanup
abrichr Dec 30, 2024
9a5f66e
get_double_click_interval_seconds/pixels on linux
abrichr Dec 30, 2024
c65d66f
gnome/kde cmd
abrichr Dec 30, 2024
b908ff7
black
abrichr Dec 30, 2024
02dd31c
fix gnome_cmd
abrichr Dec 30, 2024
0968b39
more robust get_double_click_distance_pixels on linux
abrichr Dec 30, 2024
4e3f05a
fix regex
abrichr Dec 30, 2024
ec90949
get_xinput_property
abrichr Dec 30, 2024
3aa844d
fix regex
abrichr Dec 30, 2024
d3b61f7
flake8
abrichr Dec 30, 2024
29df1ba
cleanup
abrichr Dec 30, 2024
34fc8ed
update todo
abrichr Dec 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions openadapt/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import urllib.request

import gradio_client
import oa_pynput
import pynput
import pycocotools
import pydicom
import pyqttoast
Expand All @@ -34,7 +34,7 @@
def build_pyinstaller() -> None:
"""Build the application using PyInstaller."""
additional_packages_to_install = [
oa_pynput,
pynput,
pydicom,
spacy_alignments,
gradio_client,
Expand Down Expand Up @@ -275,6 +275,8 @@ def main() -> None:
create_macos_dmg()
elif sys.platform == "win32":
create_windows_installer()
else:
print(f"WARNING: openadapt.build is not yet supported on {sys.platform=}")


if __name__ == "__main__":
Expand Down
4 changes: 3 additions & 1 deletion openadapt/build_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ def get_root_dir_path() -> pathlib.Path:
if not path.exists():
path.mkdir(parents=True, exist_ok=True)
return path
else:
elif sys.platform == "win32":
# if windows, get the path to the %APPDATA% directory and set the path
# for all user preferences
path = pathlib.Path.home() / "AppData" / "Roaming" / "openadapt"
if not path.exists():
path.mkdir(parents=True, exist_ok=True)
return path
else:
print(f"WARNING: openadapt.build_utils is not yet supported on {sys.platform=}")


def is_running_from_executable() -> bool:
Expand Down
2 changes: 2 additions & 0 deletions openadapt/capture/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from . import _macos as impl
elif sys.platform == "win32":
from . import _windows as impl
elif sys.platform.startswith("linux"):
from . import _linux as impl
else:
raise Exception(f"Unsupported platform: {sys.platform}")

Expand Down
140 changes: 140 additions & 0 deletions openadapt/capture/_linux.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import subprocess
import os
from datetime import datetime
from sys import platform
import pyaudio
import wave

from openadapt.config import CAPTURE_DIR_PATH


class Capture:
"""Capture the screen, audio, and camera on Linux."""

def __init__(self) -> None:
"""Initialize the capture object."""
if not platform.startswith("linux"):
assert platform == "linux", platform

self.is_recording = False
self.audio_out = None
self.video_out = None
self.audio_stream = None
self.audio_frames = []

# Initialize PyAudio
self.audio = pyaudio.PyAudio()

def get_screen_resolution(self) -> tuple:
"""Get the screen resolution dynamically using xrandr."""
try:
# Get screen resolution using xrandr
output = subprocess.check_output(
"xrandr | grep '*' | awk '{print $1}'", shell=True
)
resolution = output.decode("utf-8").strip()
width, height = resolution.split("x")
return int(width), int(height)
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Failed to get screen resolution: {e}")

def start(self, audio: bool = True, camera: bool = False) -> None:
"""Start capturing the screen, audio, and camera.

Args:
audio (bool, optional): Whether to capture audio (default: True).
camera (bool, optional): Whether to capture the camera (default: False).
"""
if self.is_recording:
raise RuntimeError("Recording is already in progress")

self.is_recording = True
capture_dir = CAPTURE_DIR_PATH
if not os.path.exists(capture_dir):
os.mkdir(capture_dir)

# Get the screen resolution dynamically
screen_width, screen_height = self.get_screen_resolution()

# Start video capture using ffmpeg
video_filename = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + ".mp4"
self.video_out = os.path.join(capture_dir, video_filename)
self._start_video_capture(screen_width, screen_height)

# Start audio capture
if audio:
audio_filename = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + ".wav"
self.audio_out = os.path.join(capture_dir, audio_filename)
self._start_audio_capture()

def _start_video_capture(self, width: int, height: int) -> None:
"""Start capturing the screen using ffmpeg with the dynamic resolution."""
cmd = [
"ffmpeg",
"-f",
"x11grab", # Capture X11 display
"-video_size",
f"{width}x{height}", # Use dynamic screen resolution
"-framerate",
"30", # Set frame rate
"-i",
":0.0", # Capture from display 0
"-c:v",
"libx264", # Video codec
"-preset",
"ultrafast", # Speed/quality tradeoff
"-y",
self.video_out, # Output file
]
self.video_proc = subprocess.Popen(cmd)

def _start_audio_capture(self) -> None:
"""Start capturing audio using PyAudio."""
self.audio_stream = self.audio.open(
format=pyaudio.paInt16,
channels=2,
rate=44100,
input=True,
frames_per_buffer=1024,
stream_callback=self._audio_callback,
)
self.audio_frames = []
self.audio_stream.start_stream()

def _audio_callback(
self, in_data: bytes, frame_count: int, time_info: dict, status: int
) -> tuple:
"""Callback function to process audio data."""
self.audio_frames.append(in_data)
return (None, pyaudio.paContinue)

def stop(self) -> None:
"""Stop capturing the screen, audio, and camera."""
if self.is_recording:
# Stop the video capture
self.video_proc.terminate()

# Stop audio capture
if self.audio_stream:
self.audio_stream.stop_stream()
self.audio_stream.close()
self.audio.terminate()
self.save_audio()

self.is_recording = False

def save_audio(self) -> None:
"""Save the captured audio to a WAV file."""
if self.audio_out:
with wave.open(self.audio_out, "wb") as wf:
wf.setnchannels(2)
wf.setsampwidth(self.audio.get_sample_size(pyaudio.paInt16))
wf.setframerate(44100)
wf.writeframes(b"".join(self.audio_frames))


if __name__ == "__main__":
capture = Capture()
capture.start(audio=True, camera=False)
input("Press enter to stop")
capture.stop()
5 changes: 1 addition & 4 deletions openadapt/capture/_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ class Capture:

def __init__(self) -> None:
"""Initialize the capture object."""
if platform != "darwin":
raise NotImplementedError(
"This is the macOS implementation, please use the Windows version"
)
assert platform == "darwin", platform

objc.options.structs_indexable = True

Expand Down
6 changes: 2 additions & 4 deletions openadapt/capture/_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,8 @@ def __init__(self, pid: int = 0) -> None:
pid (int, optional): The process ID of the window to capture.
Defaults to 0 (the entire screen)
"""
if platform != "win32":
raise NotImplementedError(
"This is the Windows implementation, please use the macOS version"
)
assert platform == "win32", platform

self.is_recording = False
self.video_out = None
self.audio_out = None
Expand Down
6 changes: 3 additions & 3 deletions openadapt/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import sys

from bs4 import BeautifulSoup
from oa_pynput import keyboard
from pynput import keyboard
from PIL import Image, ImageChops
import numpy as np
import sqlalchemy as sa
Expand Down Expand Up @@ -649,9 +649,9 @@ def to_prompt_dict(
"title",
"help",
]
if sys.platform == "win32":
if sys.platform != "darwin":
logger.warning(
"key_suffixes have not yet been defined on Windows."
"key_suffixes have not yet been defined on {sys.platform=}."
"You can help by uncommenting the lines below and pasting "
"the contents of the window_dict into a new GitHub Issue."
)
Expand Down
2 changes: 1 addition & 1 deletion openadapt/playback.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Utilities for playing back ActionEvents."""

from oa_pynput import keyboard, mouse
from pynput import keyboard, mouse

from openadapt.common import KEY_EVENTS, MOUSE_EVENTS
from openadapt.custom_logger import logger
Expand Down
4 changes: 3 additions & 1 deletion openadapt/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,8 +435,10 @@ def plot_performance(
if view_file:
if sys.platform == "darwin":
os.system(f"open {fpath}")
else:
elif sys.platform == "win32":
os.system(f"start {fpath}")
else:
os.system(f"xdg-open {fpath}")
else:
plt.savefig(BytesIO(), format="png") # save fig to void
if view_file:
Expand Down
Loading
Loading