Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 5 additions & 7 deletions openadapt_capture/recorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,8 +758,6 @@ def process_events(
processing_aborted: Stop without publishing a completed journal after a
startup failure leaves a producer alive.
"""
utils.set_start_time(recording.timestamp)

logger.info("Starting")

prev_event = None
Expand Down Expand Up @@ -1691,8 +1689,6 @@ def read_screen_events(
"""
if window_scope is not None and desktop_scope is not None:
raise ValueError("screen reader cannot use both window and desktop scopes")
utils.set_start_time(recording.timestamp)

fps = config.SCREEN_CAPTURE_FPS
min_interval = 1.0 / fps if fps > 0 else 0.0

Expand Down Expand Up @@ -1870,8 +1866,6 @@ def read_window_events(
recording: The recording object.
started_event: Event to set once started.
"""
utils.set_start_time(recording.timestamp)

# Refuse at the boundary. Without this the loop below polls a backend that
# can never answer, never sets started_event, and the recording hangs in
# startup with no stated cause.
Expand Down Expand Up @@ -2206,7 +2200,6 @@ def deliver_observed(event: ObservedInput, reservation: object) -> None:
setattr(on_observed, "_openadapt_input_receipt", reserve_observed)
setattr(on_observed, "_openadapt_input_delivery", deliver_observed)

utils.set_start_time(recording.timestamp)
observer = None
started = False
observer_failed = False
Expand Down Expand Up @@ -2559,6 +2552,11 @@ def record(
)
recording_timestamp = recording.timestamp

# create_recording() established the one shared clock epoch for this
# capture. Every thread producer inherits that epoch. A thread must not
# call set_start_time() again because doing so can place a later frame
# before the retained initial frame in capture time.

event_q = OrderedEventJournal()
producers_finished = threading.Event()
input_finished = threading.Event()
Expand Down
64 changes: 64 additions & 0 deletions tests/test_desktop_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from openadapt_capture.db import create_db, crud
from openadapt_capture.desktop_capture import DesktopCaptureError, DesktopCaptureScope
from openadapt_capture.recorder import (
Event,
NativeInputFrameBoundary,
OrderedEventJournal,
create_recording,
Expand Down Expand Up @@ -165,6 +166,69 @@ def test_recording_rejects_ambiguous_coordinate_scopes(tmp_path) -> None:
)


def test_desktop_screen_reader_preserves_the_initial_frame_clock(
monkeypatch,
) -> None:
"""A later screen frame uses the initial frame's monotonic epoch."""
monkeypatch.setattr(recorder_module.config, "SCREEN_CAPTURE_FPS", 0)
terminate = threading.Event()
capture_entered = threading.Event()
release_capture = threading.Event()
monotonic_now = [10.5]
monotonic_origin = [10.0]
wall_origin = 100.0
reset_calls: list[float] = []

def get_timestamp() -> float:
return wall_origin + monotonic_now[0] - monotonic_origin[0]

def reset_clock(value: float) -> None:
reset_calls.append(value)
monotonic_origin[0] = monotonic_now[0]

def take_screenshot() -> Image.Image:
capture_entered.set()
assert release_capture.wait(timeout=5)
terminate.set()
return Image.new("RGB", (4480, 1440), "black")

monkeypatch.setattr(recorder_module.utils, "get_timestamp", get_timestamp)
monkeypatch.setattr(recorder_module.utils, "set_start_time", reset_clock)
monkeypatch.setattr(recorder_module.utils, "take_screenshot", take_screenshot)

journal = OrderedEventJournal()
journal.put(
Event(
get_timestamp(),
"screen",
Image.new("RGB", (4480, 1440), "white"),
)
)

reader = threading.Thread(
target=read_screen_events,
args=(
journal,
terminate,
SimpleNamespace(timestamp=wall_origin),
threading.Event(),
),
kwargs={"desktop_scope": _two_monitor_scope()},
)
monotonic_now[0] = 11.0
reader.start()
assert capture_entered.wait(timeout=5)
monotonic_now[0] = 11.25
release_capture.set()
reader.join(timeout=5)

assert not reader.is_alive()
frames = [journal.get_nowait(), journal.get_nowait()]
assert [frame.source_ordinal for frame in frames] == [1, 2]
assert frames[0].timestamp < frames[1].timestamp
assert reset_calls == []


def test_desktop_screen_reader_discards_a_frame_crossed_by_native_input(
monkeypatch,
) -> None:
Expand Down
7 changes: 6 additions & 1 deletion tests/test_performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,12 @@ def test_initial_frame_ready_without_input(self, capture_dir):
assert rec.screen_count >= 1

with CaptureSession.load(capture_dir) as capture:
assert capture.frames(), "capture completed without an initial frame"
frames = capture.frames()
assert len(frames) >= 2, "capture completed without a later retained frame"
timestamps = [frame.timestamp for frame in frames]
assert all(
earlier < later for earlier, later in zip(timestamps, timestamps[1:])
), "retained frame capture times did not increase"

@pytest.mark.skipif(_NO_INPUT_INJECTION, reason=_INJECTION_SKIP_REASON)
def test_record_and_load_roundtrip(self, capture_dir):
Expand Down
37 changes: 0 additions & 37 deletions tests/test_window_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,43 +69,6 @@ def test_journal_orders_concurrent_observations_by_reservation_not_timestamp():
assert (second.timestamp, second.source_ordinal) == (10.0, 2)


def test_journal_accepts_a_frame_after_a_process_clock_reset(scope):
journal = OrderedEventJournal()
first_image, _ = scope.capture_frame(publish=False)
first_generation = scope.current_generation()
journal.commit_window_frame(
Event(
20.0,
"screen",
WindowScopedFrame(
image=first_image,
window_event_data=scope.window_event_data(),
geometry_generation=first_generation,
),
),
scope,
first_generation,
)

second_image, _ = scope.capture_frame(publish=False)
second_generation = scope.current_generation()
journal.commit_window_frame(
Event(
10.0,
"screen",
WindowScopedFrame(
image=second_image,
window_event_data=scope.window_event_data(),
geometry_generation=second_generation,
),
),
scope,
second_generation,
)

assert [journal.get_nowait().source_ordinal for _ in range(2)] == [1, 2]


def test_action_reservation_cannot_bind_a_later_frame_generation(scope, fake, monkeypatch):
scope.capture_frame()
journal = OrderedEventJournal()
Expand Down