diff --git a/joinly/core.py b/joinly/core.py index d07f9f7..059a6dd 100644 --- a/joinly/core.py +++ b/joinly/core.py @@ -3,6 +3,7 @@ from typing import Protocol from joinly.types import ( + ActionAnimation, AudioChunk, AudioFormat, MeetingChatHistory, @@ -248,6 +249,10 @@ async def stop_sharing(self) -> None: """Stop sharing screen in the meeting.""" ... + async def set_animation(self, animation: ActionAnimation | None) -> None: + """Set an action animation on the camera feed.""" + ... + async def update_ui(self, update: UIUpdate) -> None: """Update the UI on the meeting provider. diff --git a/joinly/providers/base.py b/joinly/providers/base.py index f312492..b084d39 100644 --- a/joinly/providers/base.py +++ b/joinly/providers/base.py @@ -1,5 +1,6 @@ from joinly.core import MeetingProvider from joinly.types import ( + ActionAnimation, MeetingChatHistory, MeetingParticipant, ProviderNotSupportedError, @@ -60,5 +61,8 @@ async def stop_sharing(self) -> None: msg = "Provider does not support stopping screen share." raise ProviderNotSupportedError(msg) + async def set_animation(self, animation: ActionAnimation | None) -> None: + """Set an action animation on the camera feed.""" + async def update_ui(self, update: UIUpdate) -> None: """Update the UI on the meeting provider.""" diff --git a/joinly/providers/browser/camera_feed.py b/joinly/providers/browser/camera_feed.py index d0120ed..3b5479c 100644 --- a/joinly/providers/browser/camera_feed.py +++ b/joinly/providers/browser/camera_feed.py @@ -309,16 +309,15 @@ {fx_busy} const FX = {{ - send_chat_message: fxTyping, - get_chat_history: fxReading, - get_participants: fxReading, + typing: fxTyping, + reading: fxReading, interrupted: fxInterrupted, busy: fxBusy, }}; const FX_BG = {{ thinking: fxThinking, - share_screen: fxShare, + sharing: fxShare, }}; function _initCanvas() {{ diff --git a/joinly/providers/browser/meeting_provider.py b/joinly/providers/browser/meeting_provider.py index ad058ea..38018dd 100644 --- a/joinly/providers/browser/meeting_provider.py +++ b/joinly/providers/browser/meeting_provider.py @@ -26,11 +26,11 @@ from joinly.providers.browser.screen_share import remove_overlay, setup_content_stream from joinly.settings import get_settings from joinly.types import ( + ActionAnimation, AudioChunk, MeetingChatHistory, MeetingParticipant, ProviderNotSupportedError, - SpeechInterruptedError, UIAnimationContent, UIHtmlContent, UIUpdate, @@ -197,7 +197,6 @@ async def _action_guard( raise RuntimeError(msg) async with self._lock: - self._camera_feed.set_effect(action) try: yield self._page, self._platform_controller except Exception as e: @@ -208,18 +207,6 @@ async def _action_guard( raise RuntimeError(msg) from None else: logger.info("Successfully performed '%s'.", action) - finally: - self._camera_feed.set_effect(None) - - @asynccontextmanager - async def speech_guard(self) -> AsyncIterator[None]: - """Context manager that sets the interrupted camera status on interruption.""" - try: - yield - except SpeechInterruptedError: - self._camera_feed.set_effect("interrupted") - self._camera_feed.set_effect(None) - raise async def _get_platform_controller(self, url: str) -> BrowserPlatformController: """Get the platform-specific meeting controller based on the URL. @@ -414,6 +401,10 @@ async def stop_sharing(self) -> None: finally: await self._cleanup_content_page() + async def set_animation(self, animation: ActionAnimation | None) -> None: + """Set an action animation on the camera feed.""" + self._camera_feed.set_effect(animation) + async def update_ui(self, update: UIUpdate) -> None: """Update the UI on the camera feed.""" if isinstance(update.content, UIAnimationContent): diff --git a/joinly/session.py b/joinly/session.py index f00b107..7b3d72d 100644 --- a/joinly/session.py +++ b/joinly/session.py @@ -1,6 +1,7 @@ import contextlib import logging -from collections.abc import Callable, Coroutine +from collections.abc import AsyncIterator, Callable, Coroutine +from contextlib import asynccontextmanager from joinly.core import ( MeetingProvider, @@ -9,8 +10,10 @@ VideoReader, ) from joinly.types import ( + ActionAnimation, MeetingChatHistory, MeetingParticipant, + SpeechInterruptedError, Transcript, UIUpdate, VideoSnapshot, @@ -129,12 +132,12 @@ async def speak_text(self, text: str) -> None: Args: text (str): The text to be spoken. """ - guard = getattr(self._meeting_provider, "speech_guard", None) - if guard is not None: - async with guard(): - await self._speech_controller.speak_text(text) - else: + try: await self._speech_controller.speak_text(text) + except SpeechInterruptedError: + await self.set_animation("interrupted") + await self.set_animation(None) + raise async def send_chat_message(self, message: str) -> None: """Send a chat message in the meeting. @@ -142,7 +145,8 @@ async def send_chat_message(self, message: str) -> None: Args: message (str): The message to be sent. """ - await self._meeting_provider.send_chat_message(message) + async with self.animation("typing"): + await self._meeting_provider.send_chat_message(message) async def get_chat_history(self) -> MeetingChatHistory: """Get the chat history from the meeting. @@ -150,7 +154,8 @@ async def get_chat_history(self) -> MeetingChatHistory: Returns: MeetingChatHistory: The chat history of the meeting. """ - return await self._meeting_provider.get_chat_history() + async with self.animation("reading"): + return await self._meeting_provider.get_chat_history() async def get_participants(self) -> list[MeetingParticipant]: """Get the list of participants in the meeting. @@ -158,7 +163,8 @@ async def get_participants(self) -> list[MeetingParticipant]: Returns: list[MeetingParticipant]: A list of participants in the meeting. """ - return await self._meeting_provider.get_participants() + async with self.animation("reading"): + return await self._meeting_provider.get_participants() async def get_video_snapshot(self) -> VideoSnapshot: """Get a snapshot of the current video feed. @@ -174,7 +180,8 @@ async def share_screen(self, url: str) -> None: Args: url: URL to display while sharing. """ - await self._meeting_provider.share_screen(url) + async with self.animation("sharing"): + await self._meeting_provider.share_screen(url) async def stop_sharing(self) -> None: """Stop sharing screen in the meeting.""" @@ -188,6 +195,19 @@ async def unmute(self) -> None: """Unmute yourself in the meeting.""" await self._meeting_provider.unmute() + async def set_animation(self, animation: ActionAnimation | None) -> None: + """Set an action animation on the meeting provider.""" + await self._meeting_provider.set_animation(animation) + + @asynccontextmanager + async def animation(self, name: ActionAnimation) -> AsyncIterator[None]: + """Show an action animation for the duration of the block.""" + await self.set_animation(name) + try: + yield + finally: + await self.set_animation(None) + async def update_ui(self, update: UIUpdate) -> None: """Update the UI on the meeting provider. diff --git a/joinly/types.py b/joinly/types.py index 75987f9..55540bd 100644 --- a/joinly/types.py +++ b/joinly/types.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import Literal from joinly_common.types import ( MeetingChatHistory, @@ -16,7 +17,10 @@ VideoSnapshot, ) +ActionAnimation = Literal["typing", "reading", "interrupted", "sharing"] + __all__ = [ + "ActionAnimation", "MeetingChatHistory", "MeetingChatMessage", "MeetingParticipant",