Skip to content

Commit deb14c0

Browse files
Merge pull request #899 from OliverZhaohaibin/codex/live-photo-detail-performance-fix
Fix Live Photo playback and detail view regression
2 parents d72987f + 089ef76 commit deb14c0

4 files changed

Lines changed: 93 additions & 10 deletions

File tree

src/iPhoto/gui/coordinators/playback_coordinator.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ def __init__(
237237
self._play_debounce.timeout.connect(self._execute_pending_play)
238238

239239
self._connect_signals()
240+
self._setup_zoom_handler()
241+
self._restore_filmstrip_preference()
240242

241243
def rebind_library(
242244
self,
@@ -285,8 +287,6 @@ def _is_async_token_current(self, token: PlaybackAsyncToken | None) -> bool:
285287
token == getattr(self, "_active_async_token", None)
286288
and token.library_epoch == self._current_library_epoch()
287289
)
288-
self._setup_zoom_handler()
289-
self._restore_filmstrip_preference()
290290

291291
def set_navigation_coordinator(self, nav: NavigationCoordinator) -> None:
292292
self._navigation = nav
@@ -691,6 +691,10 @@ def _render_presentation(self, presentation: DetailPresentation) -> None:
691691
self._zoom_slider.setValue(100)
692692
self._zoom_slider.blockSignals(False)
693693

694+
self._is_playing = False
695+
self._player_bar.set_playback_state(False)
696+
self._player_bar.set_position(0)
697+
694698
if presentation.is_video:
695699
self._hide_face_name_overlay(clear_annotations=True)
696700
if self._is_location_video_write_inflight(source):
@@ -725,6 +729,7 @@ def _render_presentation(self, presentation: DetailPresentation) -> None:
725729
has_trim=has_trim,
726730
)
727731
self._player_view.video_area.play()
732+
self._is_playing = True
728733
self._player_bar.setEnabled(True)
729734
self._zoom_handler.set_viewer(self._player_view.video_area)
730735
self._player_view.video_area.reset_zoom()
@@ -766,10 +771,6 @@ def _render_presentation(self, presentation: DetailPresentation) -> None:
766771
self._player_view.set_live_replay_enabled(False)
767772
self._refresh_face_name_overlay_for_presentation(presentation)
768773

769-
self._is_playing = False
770-
self._player_bar.set_playback_state(False)
771-
self._player_bar.set_position(0)
772-
773774
if self._info_panel and presentation.info_panel_visible:
774775
self._refresh_info_panel(presentation.info)
775776
self._info_panel.show()

src/iPhoto/gui/ui/controllers/player_view_controller.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,9 @@ def run(self) -> None: # pragma: no cover - executed on a worker thread
9191
try:
9292
adjustments_started = time.perf_counter()
9393
adjustments = {}
94-
self.color_stats = compute_color_statistics(image)
9594
self.source_identity = self.source_identity.repair_revision_from_stat()
9695
if self._edit_service is not None and self._edit_service.sidecar_exists(self._source):
96+
self.color_stats = compute_color_statistics(image)
9797
adjustments = self._edit_service.describe_adjustments(
9898
self._source,
9999
color_stats=self.color_stats,
@@ -109,7 +109,6 @@ def run(self) -> None: # pragma: no cover - executed on a worker thread
109109
self._signals.failed.emit(self._source, str(exc))
110110
return
111111

112-
# Pass the raw image and adjustments to the main thread. The GL viewer
113112
# Pass the raw image and adjustments to the main thread. The GL viewer
114113
# will apply the adjustments on the GPU.
115114
log_detail_profile(

tests/gui/coordinators/test_playback_coordinator.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,35 @@ def _make_presentation(
5757
)
5858

5959

60+
def test_constructor_initializes_detail_controls(qapp) -> None:
61+
location_search = SimpleNamespace(
62+
suggestionsReady=Mock(connect=Mock()),
63+
searchFailed=Mock(connect=Mock()),
64+
)
65+
arguments = [Mock() for _ in range(18)]
66+
67+
with patch.object(PlaybackCoordinator, "_connect_signals"), patch.object(
68+
PlaybackCoordinator,
69+
"_setup_zoom_handler",
70+
) as setup_zoom, patch.object(
71+
PlaybackCoordinator,
72+
"_restore_filmstrip_preference",
73+
) as restore_filmstrip, patch.object(
74+
playback_coordinator_module,
75+
"LocationSearchController",
76+
return_value=location_search,
77+
):
78+
coordinator = PlaybackCoordinator(
79+
*arguments,
80+
people_service=Mock(),
81+
pet_service=Mock(),
82+
)
83+
84+
setup_zoom.assert_called_once_with()
85+
restore_filmstrip.assert_called_once_with()
86+
coordinator.shutdown = Mock()
87+
88+
6089
def test_play_asset_dispatches_immediately_when_idle() -> None:
6190
coordinator = PlaybackCoordinator.__new__(PlaybackCoordinator)
6291
coordinator._asset_model = Mock(rowCount=Mock(return_value=3))
@@ -414,6 +443,60 @@ def test_render_presentation_stops_video_area_before_showing_still() -> None:
414443
coordinator._player_bar.setEnabled.assert_called_once_with(False)
415444

416445

446+
def test_render_live_presentation_keeps_motion_playback_active(tmp_path: Path) -> None:
447+
still = tmp_path / "photo.heic"
448+
motion = tmp_path / "motion.mov"
449+
still.write_bytes(b"still")
450+
motion.write_bytes(b"motion")
451+
presentation = replace(
452+
_make_presentation(path=str(still), is_video=False, is_live=True),
453+
live_motion_rel=Path("motion.mov"),
454+
live_motion_abs=motion,
455+
)
456+
coordinator = PlaybackCoordinator.__new__(PlaybackCoordinator)
457+
coordinator._detail_render_lifecycle = DetailRenderCoordinator()
458+
coordinator._detail_generation = 0
459+
coordinator._live_transaction = None
460+
coordinator._active_async_token = None
461+
coordinator._player_view = Mock(
462+
video_area=Mock(
463+
has_video=Mock(return_value=False),
464+
load_video=Mock(),
465+
play=Mock(),
466+
),
467+
image_viewer=Mock(reset_zoom=Mock()),
468+
)
469+
coordinator._favorite_button = Mock(setEnabled=Mock())
470+
coordinator._info_button = Mock(setEnabled=Mock())
471+
coordinator._share_button = Mock(setEnabled=Mock())
472+
coordinator._edit_button = Mock(setEnabled=Mock())
473+
coordinator._rotate_button = Mock(setEnabled=Mock())
474+
coordinator._update_favorite_icon = Mock()
475+
coordinator._zoom_slider = Mock(blockSignals=Mock(), setValue=Mock())
476+
coordinator._player_bar = Mock(
477+
setEnabled=Mock(),
478+
set_playback_state=Mock(),
479+
set_position=Mock(),
480+
)
481+
coordinator._zoom_handler = Mock(set_viewer=Mock())
482+
coordinator._zoom_widget = Mock(show=Mock())
483+
coordinator._info_panel = None
484+
coordinator._clear_play_profile = Mock()
485+
coordinator._hide_face_name_overlay = Mock()
486+
coordinator._is_playing = False
487+
488+
PlaybackCoordinator._render_presentation(coordinator, presentation)
489+
490+
coordinator._player_view.video_area.load_video.assert_called_once_with(
491+
motion,
492+
adjustments=None,
493+
trim_range_ms=None,
494+
adjusted_preview=False,
495+
)
496+
coordinator._player_view.video_area.play.assert_called_once_with()
497+
assert coordinator._is_playing is True
498+
499+
417500
def _live_lifecycle_coordinator(
418501
tmp_path: Path,
419502
) -> tuple[PlaybackCoordinator, DetailPresentation, Path, Path]:

tests/ui/controllers/test_player_view_controller_adjustments.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
)
2323

2424

25-
def test_adjusted_image_worker_collects_shared_stats_without_sidecar() -> None:
25+
def test_adjusted_image_worker_skips_color_stats_without_sidecar() -> None:
2626
source = Path("/tmp/photo.jpg")
2727
signals = Mock()
2828
edit_service = Mock()
@@ -39,7 +39,7 @@ def test_adjusted_image_worker_collects_shared_stats_without_sidecar() -> None:
3939
worker.run()
4040

4141
edit_service.describe_adjustments.assert_not_called()
42-
compute_stats.assert_called_once_with(image)
42+
compute_stats.assert_not_called()
4343
signals.completed.emit.assert_called_once_with(source, image, {})
4444

4545

0 commit comments

Comments
 (0)