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
23 changes: 23 additions & 0 deletions joinly/providers/browser/camera_feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,26 @@
}
}"""

# Interrupted: dots scatter outward from center and fade
_FX_INTERRUPTED = """\
function fxInterrupted(ctx, cx, y, t, alpha) {
const N = 5, r = H * 0.007;
for (let i = 0; i < N; i++) {
const angle = (i / N) * Math.PI * 2 + t * 1.5;
const p = (t * 2.5 + i / N) % 1;
const spread = H * 0.01 + p * H * 0.04;
const dx = Math.cos(angle) * spread;
const dy = Math.sin(angle) * spread * 0.5;
const fade = (1 - p) * alpha;
if (fade < 0.01) continue;
ctx.globalAlpha = fade;
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(cx + dx, y + dy, r * (1 - p * 0.5), 0, Math.PI * 2);
ctx.fill();
}
}"""

# Thinking: rotating arc segments with soft glow around the logo
_FX_THINKING = """\
function fxThinking(ctx, cx, cy, logoW, logoH, t, alpha) {
Expand Down Expand Up @@ -284,13 +304,15 @@
{fx_typing}
{fx_share}
{fx_reading}
{fx_interrupted}
{fx_thinking}
{fx_busy}

const FX = {{
send_chat_message: fxTyping,
get_chat_history: fxReading,
get_participants: fxReading,
interrupted: fxInterrupted,
busy: fxBusy,
}};

Expand Down Expand Up @@ -484,6 +506,7 @@ async def install(self, meeting_page: Page) -> None:
fx_typing=_FX_TYPING,
fx_share=_FX_SHARE,
fx_reading=_FX_READING,
fx_interrupted=_FX_INTERRUPTED,
fx_thinking=_FX_THINKING,
fx_busy=_FX_BUSY,
)
Expand Down
11 changes: 11 additions & 0 deletions joinly/providers/browser/meeting_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
MeetingChatHistory,
MeetingParticipant,
ProviderNotSupportedError,
SpeechInterruptedError,
UIAnimationContent,
UIHtmlContent,
UIUpdate,
Expand Down Expand Up @@ -210,6 +211,16 @@ async def _action_guard(
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.

Expand Down
7 changes: 6 additions & 1 deletion joinly/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,12 @@ async def speak_text(self, text: str) -> None:
Args:
text (str): The text to be spoken.
"""
await self._speech_controller.speak_text(text)
guard = getattr(self._meeting_provider, "speech_guard", None)
if guard is not None:
async with guard():
await self._speech_controller.speak_text(text)
else:
await self._speech_controller.speak_text(text)

async def send_chat_message(self, message: str) -> None:
"""Send a chat message in the meeting.
Expand Down
Loading