Skip to content

Conversation

@maxkahan
Copy link
Contributor

@maxkahan maxkahan commented Oct 23, 2025

  1. Fixes bug with non-even numbers of pixels in screenshare causing it to fail
  2. Fixes republished tracks not being shared with the AI
  3. Tests for the above

Summary by CodeRabbit

  • New Features

    • Enhanced video track lifecycle management with support for track re-publishing and automatic fallback to alternative video sources when tracks are removed.
    • Upgraded video codec from VP8 to H.264.
  • Bug Fixes

    • Improved video frame dimension handling for encoding compatibility.
  • Tests

    • Added regression tests for track event flow and video dimension utilities.
  • Chores

    • Enhanced logging verbosity in example configurations.

@coderabbitai
Copy link

coderabbitai bot commented Oct 23, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

This PR enhances video track lifecycle management by adding TrackRemovedEvent handling, introducing per-track metadata and forwarder tracking, and implementing automatic track-switching logic. It includes a new utility for H.264-compatible even-dimension video frames and updates event payloads to remove user_metadata from track events.

Changes

Cohort / File(s) Summary
Core Agent Video Track Lifecycle
agents-core/vision_agents/core/agents/agents.py
Added TrackRemovedEvent handling with track cleanup and auto-switching. Introduced _active_video_tracks, _video_forwarders, and _current_video_track_id state. Implemented _switch_to_next_available_track() helper and revised _process_track() to manage shared VideoForwarder instances, reuse forwarders on track re-add, and integrate ensure_even_dimensions wrapper for screenshares.
Video Utilities
agents-core/vision_agents/core/utils/video_utils.py
Added ensure_even_dimensions() function to crop video frames to even dimensions for H.264 yuv420p encoding; preserves PTS and time_base metadata.
GetStream Plugin Track Events
plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py
Updated _on_track_published() and _on_track_removed() to emit events without user_metadata; added track re-publish detection with guard against agent's own tracks; broadened on_audio_received() callback signature to accept optional PcmData; changed video encoder from VP8 to H.264 in demo.
Track Republishing Tests
plugins/getstream/tests/test_getstream_plugin.py
Replaced placeholder scaffolding with TestTrackRepublishing class; added regression test verifying add–remove–add track event flow via EventManager.
Dimension Utility Tests
tests/test_utils.py
Added TestEnsureEvenDimensions suite with three cases: even dimensions unchanged, odd dimensions cropped, and metadata preservation.
Logging Configuration
examples/other_examples/openai_realtime_webrtc/openai_realtime_example.py
Changed logging level from WARNING to INFO for increased verbosity.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Agent as Agent
    participant Tracks as Track Manager
    participant Forwarder as VideoForwarder
    participant Processor as Frame Processor

    App->>Agent: on_track(video track)
    activate Agent
    Agent->>Tracks: Store in _active_video_tracks
    Agent->>Agent: _process_track()
    Note over Agent: Subscribe & wrap if screenshare
    Agent->>Forwarder: Create shared RAW forwarder
    Agent->>Agent: _current_video_track_id = track_id
    deactivate Agent

    loop Frame Processing
        Tracks->>Forwarder: Forward frame
        Forwarder->>Processor: Process RAW frame
        Processor-->>Forwarder: Return processed frame
    end

    App->>Agent: on_track_removed(video track)
    activate Agent
    Agent->>Tracks: Cancel per-track task
    Agent->>Tracks: Remove from _active_video_tracks
    Agent->>Agent: _switch_to_next_available_track()
    Note over Agent: Select next available video/screenshare
    Agent->>Agent: _current_video_track_id = new_track_id
    deactivate Agent

    App->>Agent: on_track(same video track re-added)
    activate Agent
    Agent->>Agent: Reuse existing forwarder
    Note over Agent: Realtime mode: switch forwarder
    deactivate Agent
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • agents.py: Dense state management logic with new track lifecycle handlers and forwarder integration; requires careful review of task cancellation and state cleanup
  • stream_edge_transport.py: Event payload changes affecting multiple emission points; verify user_metadata removal is consistent across all code paths
  • Integration points: Ensure VideoForwarder lifecycle aligns with track addition/removal and that screenshare wrapping doesn't interfere with non-screenshare processing

Possibly related PRs

Suggested reviewers

  • dangusev

Poem

The frame arrives, even-edged, a ghost of pixels—
trapped in the reformat's crop, time_base bleeding
through like bell-jar glass. We switch, we discard,
the forwarder holds what remains. What remains
is rarely enough.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "Fix screensharing" is directly related to the changeset's core objectives. The changes address screensharing functionality through two distinct improvements: adding the ensure_even_dimensions utility to handle non-even pixel dimensions in H.264 encoding, and enhancing track handling in the agent and stream transport to properly manage republished video tracks. While the title is concise and somewhat general, it accurately refers to the primary focus of the changeset—improving screensharing functionality—and conveys meaningful information about the intent of these changes.

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8019c14 and bfe888f.

📒 Files selected for processing (3)
  • agents-core/vision_agents/core/agents/agents.py (5 hunks)
  • plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py (4 hunks)
  • tests/test_utils.py (2 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
agents-core/vision_agents/core/agents/agents.py (2)

660-679: Nil‑safe check and fast‑path switch

Use explicit None check to avoid treating a valid enum 0 as falsy. Fast‑path logic looks good.

-            if not track_id or not track_type:
+            if not track_id or track_type is None:
                 return

866-899: Graceful cancellation in _process_track loop

Handle asyncio.CancelledError to exit cleanly when tracks are removed.

-        while True:
-            try:
+        try:
+            while True:
+                try:
@@
-            except asyncio.TimeoutError:
+                except asyncio.TimeoutError:
@@
-                await asyncio.sleep(backoff_delay)
+                await asyncio.sleep(backoff_delay)
+        except asyncio.CancelledError:
+            return
🧹 Nitpick comments (6)
plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py (1)

109-116: Consider synchronizing _track_map access

EventManager dispatches handlers as tasks; _on_track_published and _on_track_removed could race on _track_map. A simple asyncio.Lock around map writes avoids flakiness under bursty events. Optional but prudent.

tests/test_utils.py (1)

446-481: Nice coverage; add single‑odd cases and format assertion

Current tests cover even and both-odd. Add width-odd/height-even and width-even/height-odd cases, and assert pixel format preserved (yuv420p). Keeps regressions tight.

agents-core/vision_agents/core/utils/video_utils.py (1)

7-11: Docstring to Google style; pin pixel format in reformat

Improve docstring per guidelines and explicitly pass the source format to avoid any implicit format changes.

-    """
-    Ensure frame has even dimensions for H.264 yuv420p encoding.
-    
-    Crops by 1 pixel if width or height is odd.
-    """
+    """Ensure even frame dimensions for H.264 yuv420p (4:2:0).
+
+    Crops by 1 pixel on width and/or height when odd, preserving PTS/time_base.
+
+    Args:
+        frame: Input av.VideoFrame.
+
+    Returns:
+        av.VideoFrame: Original if already even; otherwise a new cropped frame.
+    """
@@
-    cropped = frame.reformat(width=new_width, height=new_height)
+    cropped = frame.reformat(width=new_width, height=new_height, format=frame.format.name)

Also applies to: 21-24

agents-core/vision_agents/core/agents/agents.py (3)

737-764: Switching logic: ensure shared_forwarder matches subscriber

You reuse a stored forwarder (from prior subscription) with a freshly added subscriber track. If Realtime._watch_video_track consumes frames exclusively from shared_forwarder, you're fine; if it also interacts with the passed track, mismatch could occur. Verify provider behavior; consider passing None/placeholder for track when using a shared forwarder only.


777-787: Even‑dimensions wrapper OK; comment says RAW but forwarder sees processed frames

You wrap screenshare frames before creating the “raw” forwarder, so it’s actually forwarding cropped frames. Adjust the comment to avoid confusion or create the forwarder from the unwrapped track if true raw is desired.

Also applies to: 788-803


841-856: Rename typo: hasImageProcessers → has_image_processors

Minor clarity/readability.

-        hasImageProcessers = len(self.image_processors) > 0
+        has_image_processors = len(self.image_processors) > 0
@@
-                    if hasImageProcessers:
+                    if has_image_processors:
@@
-        if not hasImageProcessers:
+        if not has_image_processors:

Also applies to: 876-879

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 95a03e4 and d488a0d.

📒 Files selected for processing (5)
  • agents-core/vision_agents/core/agents/agents.py (8 hunks)
  • agents-core/vision_agents/core/utils/video_utils.py (1 hunks)
  • plugins/getstream/tests/test_getstream_plugin.py (1 hunks)
  • plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py (1 hunks)
  • tests/test_utils.py (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/python.mdc)

**/*.py: Do not modify sys.path in Python code
Docstrings must follow the Google style guide

Files:

  • plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py
  • agents-core/vision_agents/core/utils/video_utils.py
  • tests/test_utils.py
  • plugins/getstream/tests/test_getstream_plugin.py
  • agents-core/vision_agents/core/agents/agents.py
tests/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/python.mdc)

tests/**/*.py: Never use mocking utilities (e.g., unittest.mock, pytest-mock) in test files
Write tests using pytest (avoid unittest.TestCase or other frameworks)
Mark integration tests with @pytest.mark.integration
Do not use @pytest.mark.asyncio; async support is automatic

Files:

  • tests/test_utils.py
🧬 Code graph analysis (4)
plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py (3)
agents-core/vision_agents/core/edge/sfu_events.py (8)
  • track_type (579-583)
  • track_type (1193-1197)
  • track_type (2254-2258)
  • participant (1495-1500)
  • participant (1537-1542)
  • participant (1610-1615)
  • participant (2078-2083)
  • participant (2127-2132)
agents-core/vision_agents/core/events/manager.py (1)
  • send (426-468)
agents-core/vision_agents/core/edge/events.py (1)
  • TrackAddedEvent (17-22)
tests/test_utils.py (1)
agents-core/vision_agents/core/utils/video_utils.py (1)
  • ensure_even_dimensions (6-26)
plugins/getstream/tests/test_getstream_plugin.py (2)
agents-core/vision_agents/core/events/manager.py (1)
  • EventManager (56-546)
agents-core/vision_agents/core/edge/events.py (2)
  • TrackAddedEvent (17-22)
  • TrackRemovedEvent (26-31)
agents-core/vision_agents/core/agents/agents.py (5)
agents-core/vision_agents/core/edge/events.py (1)
  • TrackRemovedEvent (26-31)
agents-core/vision_agents/core/utils/video_forwarder.py (1)
  • VideoForwarder (13-188)
agents-core/vision_agents/core/utils/video_utils.py (1)
  • ensure_even_dimensions (6-26)
agents-core/vision_agents/core/llm/realtime.py (2)
  • Realtime (23-201)
  • _watch_video_track (68-70)
plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py (1)
  • add_track_subscriber (312-315)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: unit / Ruff & mypy
  • GitHub Check: unit / Test "not integration"
  • GitHub Check: unit / Ruff & mypy

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
agents-core/vision_agents/core/agents/agents.py (1)

685-707: Task cancellation doesn't await cleanup.

Line 697 cancels the task but doesn't await its completion. While the task handles cleanup via CancelledError (lines 902-925), the cleanup is not guaranteed to finish before this handler returns. This creates a window where _active_video_tracks (line 701) still contains the track_id but the task is cancelling, contributing to the race condition flagged in the previous comment.

Consider awaiting cancellation to ensure deterministic cleanup ordering:

 if track_id in self._track_tasks:
-    self._track_tasks[track_id].cancel()
+    task = self._track_tasks[track_id]
+    task.cancel()
+    try:
+        await task
+    except asyncio.CancelledError:
+        pass
     self._track_tasks.pop(track_id)
🧹 Nitpick comments (2)
agents-core/vision_agents/core/agents/agents.py (2)

781-789: Consider extracting the even-dimensions wrapper.

The inline _EvenDimensionsTrack class (lines 782-789) is defined inside the method, reducing reusability. While functionally correct, extracting it to module level would improve testability and potential reuse.

Extract to module level below the imports:

class EvenDimensionsVideoTrack(VideoStreamTrack):
    """Wraps a video track to ensure frames have even dimensions for H.264 encoding."""
    
    def __init__(self, source: VideoStreamTrack):
        super().__init__()
        self.source = source
    
    async def recv(self):
        frame = await self.source.recv()
        return ensure_even_dimensions(frame)

Then use it in _process_track:

 if track_type == TrackType.TRACK_TYPE_SCREEN_SHARE:
-    class _EvenDimensionsTrack(VideoStreamTrack):
-        def __init__(self, src): 
-            super().__init__()
-            self.src = src
-        async def recv(self): 
-            return ensure_even_dimensions(await self.src.recv())
-    
-    track = _EvenDimensionsTrack(track)  # type: ignore[arg-type]
+    track = EvenDimensionsVideoTrack(track)

865-901: consecutive_errors counter is incremented but never used.

Line 867 initializes consecutive_errors, and line 892 increments it when empty frames are received, but no threshold check exists to trigger circuit-breaking or logging. Either implement a threshold check or remove the counter.

If empty frames indicate a problem worth tracking:

                else:
                    self.logger.warning("🎥VDP: Received empty frame")
                    consecutive_errors += 1
+                   if consecutive_errors >= 10:
+                       self.logger.error(
+                           f"🎥VDP: Too many consecutive empty frames ({consecutive_errors}), stopping"
+                       )
+                       break

Otherwise, remove the unused variable:

-consecutive_errors = 0
                else:
                    self.logger.warning("🎥VDP: Received empty frame")
-                   consecutive_errors += 1
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d488a0d and 3c0d4a2.

📒 Files selected for processing (1)
  • agents-core/vision_agents/core/agents/agents.py (5 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/python.mdc)

**/*.py: Do not modify sys.path in Python code
Docstrings must follow the Google style guide

Files:

  • agents-core/vision_agents/core/agents/agents.py
🧬 Code graph analysis (1)
agents-core/vision_agents/core/agents/agents.py (7)
agents-core/vision_agents/core/edge/events.py (2)
  • TrackAddedEvent (17-22)
  • TrackRemovedEvent (26-31)
agents-core/vision_agents/core/utils/video_forwarder.py (4)
  • VideoForwarder (13-188)
  • start (31-39)
  • next_frame (83-101)
  • stop (41-58)
agents-core/vision_agents/core/utils/video_utils.py (1)
  • ensure_even_dimensions (6-26)
agents-core/vision_agents/core/llm/realtime.py (2)
  • Realtime (23-201)
  • _watch_video_track (68-70)
plugins/gemini/vision_agents/plugins/gemini/gemini_realtime.py (2)
  • Realtime (52-676)
  • _watch_video_track (385-424)
plugins/openai/vision_agents/plugins/openai/openai_realtime.py (2)
  • Realtime (37-460)
  • _watch_video_track (263-267)
plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py (1)
  • add_track_subscriber (312-315)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: unit / Test "not integration"
  • GitHub Check: unit / Ruff & mypy
  • GitHub Check: unit / Test "not integration"
  • GitHub Check: unit / Ruff & mypy
🔇 Additional comments (5)
agents-core/vision_agents/core/agents/agents.py (5)

17-17: LGTM!

The new imports (TrackRemovedEvent, VideoForwarder, ensure_even_dimensions) are necessary for the track lifecycle and screensharing fixes.

Also applies to: 36-37


164-166: LGTM!

The new state variables properly track video track metadata and the currently active track. Type annotations and documentation are clear.


810-842: LGTM!

The realtime mode handling correctly distinguishes between processed frames (when a video publisher exists) and raw frames, ensuring the appropriate forwarder is shared with the LLM provider. The redundant isinstance checks (lines 829, 838) provide defense-in-depth given the property-based realtime_mode check.


902-925: CancelledError handling properly addresses the previous review.

The cleanup block correctly catches asyncio.CancelledError, stops both forwarders with proper error handling, and returns cleanly. This resolves the concern raised in the past review comment about missing cancellation guards.

Based on previous review feedback.


667-679: Race condition verified; providers lack defensive checks for stopped forwarders.

The concern is valid. The race window exists between cancelling the track task (line 697) without awaiting and removing it from _active_video_tracks (line 701). If on_track is re-added within this window, the code retrieves a forwarder that may be in the process of stopping.

VideoForwarder has no guards against reuse after stop() completes—_started becomes False but there are no checks before reusing the instance. The Realtime providers (Gemini, OpenAI) pass the forwarder to downstream consumers without validating its state, risking frame delivery failures if the forwarder is already stopped.

To fix: Either (1) await the task cancellation in on_track_removed before removing from _active_video_tracks, or (2) add a null/validity check in on_track_re-addition and create a new forwarder if the stored one is no longer started, or (3) have Realtime providers validate forwarder state before use.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
plugins/getstream/tests/test_getstream_plugin.py (1)

18-69: Well-structured regression test with proper event flow validation.

The test correctly simulates the track lifecycle (add → remove → add) and validates that all three events flow through the system. The use of await event_manager.wait() instead of sleeps makes the test deterministic, addressing previous feedback.

Consider adding cleanup to prevent potential test pollution:

     # Before the fix: The agent would never receive this third event
     assert len(events) == 3, "Republishing track should emit TrackAddedEvent"
     assert isinstance(events[2], TrackAddedEvent)
     assert events[2].track_id == track_id
+
+    # Cleanup
+    event_manager.unsubscribe(collect_track_events)
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3c0d4a2 and 8396580.

📒 Files selected for processing (1)
  • plugins/getstream/tests/test_getstream_plugin.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/python.mdc)

**/*.py: Do not modify sys.path in Python code
Docstrings must follow the Google style guide

Files:

  • plugins/getstream/tests/test_getstream_plugin.py
🧬 Code graph analysis (1)
plugins/getstream/tests/test_getstream_plugin.py (2)
agents-core/vision_agents/core/events/manager.py (2)
  • EventManager (56-546)
  • wait (470-484)
agents-core/vision_agents/core/edge/events.py (2)
  • TrackAddedEvent (17-22)
  • TrackRemovedEvent (26-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: unit / Ruff & mypy
  • GitHub Check: unit / Test "not integration"
  • GitHub Check: unit / Ruff & mypy
  • GitHub Check: unit / Test "not integration"
🔇 Additional comments (3)
plugins/getstream/tests/test_getstream_plugin.py (3)

1-4: LGTM! Previous feedback addressed.

The imports are clean and properly use the TrackType enum instead of magic numbers, addressing the prior review feedback.


7-16: Excellent documentation of the regression test.

The docstring clearly explains the bug, the fix, and the purpose of the test, making it easy for future maintainers to understand the context.


28-28: No changes required—Python 3.10+ is the project minimum.

The project explicitly requires Python 3.10+, making the | union operator syntax valid and appropriate. The code is correct as written.

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
plugins/getstream/tests/test_getstream_plugin.py (1)

7-72: Deterministic republish regression test — nice

Good use of EventManager.wait() and TrackType enum. Consider also asserting events[i].track_type == track_type for completeness. Please confirm the repo is configured to run bare async tests (no @pytest.mark.asyncio).

agents-core/vision_agents/core/agents/agents.py (5)

737-764: Snapshot dict before iterating to avoid RuntimeError on concurrent mutation

event handlers may mutate _active_video_tracks while iterating. Iterate over a snapshot.

-        for track_id, (track_type, participant, forwarder) in self._active_video_tracks.items():
+        for track_id, (track_type, participant, forwarder) in list(self._active_video_tracks.items()):

164-166: Type safety and initialization for per-track state

Initialize self._video_forwarders in init and prefer a small dataclass for track metadata for clarity.

         self._active_video_tracks: Dict[str, tuple[int, Any, Any]] = {}
         self._current_video_track_id: Optional[str] = None
+        self._video_forwarders: list[VideoForwarder] = []

Optionally:

@dataclass
class VideoTrackInfo:
    track_type: int
    participant: Any
    forwarder: VideoForwarder | None

803-806: Remove lazy hasattr init; rely on init

Now that _video_forwarders is initialized in init, drop the hasattr guard.

-            # Track forwarders for cleanup
-            if not hasattr(self, "_video_forwarders"):
-                self._video_forwarders = []
-            self._video_forwarders.append(raw_forwarder)
+            # Track forwarders for cleanup
+            self._video_forwarders.append(raw_forwarder)

667-679: Guard condition for track_type

Avoid falsy check that would drop 0-valued enums (e.g., TRACK_TYPE_UNSPECIFIED). Use explicit None check.

-            if not track_id or not track_type:
+            if not track_id or track_type is None:
                 return

765-926: General exception path should also clean forwarders

You handle asyncio.CancelledError, but other exceptions will leak running forwarders until close(). Add a generic except/finally to stop any created forwarders before exiting.

-        except asyncio.CancelledError:
+        except asyncio.CancelledError:
             # Task was cancelled (e.g., track removed)
@@
-            return
+            return
+        except Exception as e:
+            self.logger.exception(f"Error in _process_track({track_id}): {e}")
+            # Best-effort cleanup, same as on cancellation
+            for fwd in (raw_forwarder, processed_forwarder):
+                if fwd is not None and hasattr(self, "_video_forwarders"):
+                    try:
+                        await fwd.stop()
+                        if fwd in self._video_forwarders:
+                            self._video_forwarders.remove(fwd)
+                    except Exception:
+                        self.logger.error("Error stopping forwarder after exception", exc_info=True)
+            return
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8396580 and 489f574.

📒 Files selected for processing (3)
  • agents-core/vision_agents/core/agents/agents.py (5 hunks)
  • plugins/getstream/tests/test_getstream_plugin.py (1 hunks)
  • plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/python.mdc)

**/*.py: Do not modify sys.path in Python code
Docstrings must follow the Google style guide

Files:

  • plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py
  • agents-core/vision_agents/core/agents/agents.py
  • plugins/getstream/tests/test_getstream_plugin.py
🧬 Code graph analysis (3)
plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py (4)
agents-core/vision_agents/core/events/base.py (1)
  • user_id (43-46)
agents-core/vision_agents/core/edge/events.py (1)
  • TrackAddedEvent (17-22)
agents-core/vision_agents/core/agents/agents.py (1)
  • on_audio_received (647-656)
agents-core/vision_agents/core/edge/types.py (1)
  • PcmData (37-183)
agents-core/vision_agents/core/agents/agents.py (5)
agents-core/vision_agents/core/edge/events.py (2)
  • TrackAddedEvent (17-22)
  • TrackRemovedEvent (26-31)
agents-core/vision_agents/core/utils/video_forwarder.py (4)
  • VideoForwarder (13-188)
  • start (31-39)
  • next_frame (83-101)
  • stop (41-58)
agents-core/vision_agents/core/utils/video_utils.py (1)
  • ensure_even_dimensions (6-26)
agents-core/vision_agents/core/llm/realtime.py (2)
  • Realtime (23-201)
  • _watch_video_track (68-70)
plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py (1)
  • add_track_subscriber (308-311)
plugins/getstream/tests/test_getstream_plugin.py (2)
agents-core/vision_agents/core/events/manager.py (3)
  • EventManager (56-546)
  • wait (470-484)
  • unsubscribe (272-297)
agents-core/vision_agents/core/edge/events.py (2)
  • TrackAddedEvent (17-22)
  • TrackRemovedEvent (26-31)
🔇 Additional comments (2)
plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py (2)

283-283: Type-widened audio callback matches consumers

Allowing pcm: PcmData | None aligns with Agent.on_audio_received guard (skips when pcm or participant is falsy). No issues.


112-126: Republish emits TrackAddedEvent — verified clean

Re-publish correctly emits TrackAddedEvent for non-agent tracks to unblock agent switching. Verification confirms user_metadata has been fully removed from the TrackAddedEvent class and all call sites pass only supported fields (plugin_name, track_id, track_type, user). LGTM.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
agents-core/vision_agents/core/agents/agents.py (2)

690-712: Stop the VideoForwarder when removing track to prevent resource leaks.

When a track is removed, the code cancels the task and drops metadata but doesn't stop the still-running raw_forwarder. The forwarder continues pulling frames from the input track until explicitly stopped.

Apply this fix:

 # Clean up track metadata
+track_info = self._active_video_tracks.get(track_id)
+if track_info:
+    # Stop the forwarder to prevent resource leaks
+    _, _, forwarder = track_info
+    if forwarder:
+        try:
+            await forwarder.stop()
+            if hasattr(self, "_video_forwarders") and forwarder in self._video_forwarders:
+                self._video_forwarders.remove(forwarder)
+        except Exception as e:
+            self.logger.error(f"Error stopping video forwarder for {track_id}: {e}")
+
 self._active_video_tracks.pop(track_id, None)

Based on learnings (past review comments).


742-768: Snapshot dict items before iteration to prevent RuntimeError.

Line 750 iterates over self._active_video_tracks.items() directly, but concurrent calls to on_track or on_track_removed can modify this dict during iteration, raising RuntimeError. Async task switching can cause this even with the GIL.

Apply this fix:

-for track_id, (track_type, participant, forwarder) in self._active_video_tracks.items():
+for track_id, (track_type, participant, forwarder) in list(self._active_video_tracks.items()):

Based on learnings (past review comments).

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f086393 and ca62d37.

📒 Files selected for processing (6)
  • agents-core/vision_agents/core/agents/agents.py (5 hunks)
  • agents-core/vision_agents/core/utils/video_utils.py (1 hunks)
  • examples/other_examples/openai_realtime_webrtc/openai_realtime_example.py (1 hunks)
  • plugins/getstream/tests/test_getstream_plugin.py (1 hunks)
  • plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py (4 hunks)
  • tests/test_utils.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • agents-core/vision_agents/core/utils/video_utils.py
🧰 Additional context used
📓 Path-based instructions (2)
tests/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/python.mdc)

tests/**/*.py: Never use mocking utilities (e.g., unittest.mock, pytest-mock) in test files
Write tests using pytest (avoid unittest.TestCase or other frameworks)
Mark integration tests with @pytest.mark.integration
Do not use @pytest.mark.asyncio; async support is automatic

Files:

  • tests/test_utils.py
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/python.mdc)

**/*.py: Do not modify sys.path in Python code
Docstrings must follow the Google style guide

Files:

  • tests/test_utils.py
  • plugins/getstream/tests/test_getstream_plugin.py
  • examples/other_examples/openai_realtime_webrtc/openai_realtime_example.py
  • agents-core/vision_agents/core/agents/agents.py
  • plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py
🧬 Code graph analysis (4)
tests/test_utils.py (1)
agents-core/vision_agents/core/utils/video_utils.py (1)
  • ensure_even_dimensions (6-26)
plugins/getstream/tests/test_getstream_plugin.py (2)
agents-core/vision_agents/core/events/manager.py (2)
  • wait (470-484)
  • unsubscribe (272-297)
agents-core/vision_agents/core/edge/events.py (2)
  • TrackAddedEvent (17-22)
  • TrackRemovedEvent (26-31)
agents-core/vision_agents/core/agents/agents.py (8)
agents-core/vision_agents/core/edge/events.py (1)
  • TrackRemovedEvent (26-31)
agents-core/vision_agents/core/utils/video_forwarder.py (4)
  • VideoForwarder (13-188)
  • start (31-39)
  • next_frame (83-101)
  • stop (41-58)
agents-core/vision_agents/core/utils/video_utils.py (1)
  • ensure_even_dimensions (6-26)
agents-core/vision_agents/core/llm/realtime.py (1)
  • _watch_video_track (68-70)
plugins/gemini/vision_agents/plugins/gemini/gemini_realtime.py (1)
  • _watch_video_track (385-424)
plugins/openai/vision_agents/plugins/openai/openai_realtime.py (1)
  • _watch_video_track (263-267)
plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py (1)
  • add_track_subscriber (310-313)
agents-core/vision_agents/core/events/manager.py (1)
  • subscribe (299-368)
plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py (4)
agents-core/vision_agents/core/events/manager.py (1)
  • send (426-468)
agents-core/vision_agents/core/edge/events.py (1)
  • TrackAddedEvent (17-22)
agents-core/vision_agents/core/agents/agents.py (1)
  • on_audio_received (652-661)
agents-core/vision_agents/core/edge/types.py (1)
  • PcmData (37-183)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: unit / Test "not integration"
  • GitHub Check: unit / Ruff & mypy
  • GitHub Check: unit / Test "not integration"
  • GitHub Check: unit / Ruff & mypy
🔇 Additional comments (16)
examples/other_examples/openai_realtime_webrtc/openai_realtime_example.py (1)

19-19: LGTM! Enhanced logging verbosity.

Changing the log level to INFO provides better visibility into the agent's runtime behavior, which is helpful for debugging and monitoring in examples.

tests/test_utils.py (1)

446-481: LGTM! Comprehensive test coverage for ensure_even_dimensions.

The test suite properly validates:

  • Even dimensions pass through unchanged (1920×1080)
  • Odd dimensions are cropped correctly (1921×1081 → 1920×1080)
  • Frame timing metadata (pts, time_base) survives the crop

This aligns with the H.264 yuv420p encoding requirement for even dimensions mentioned in the PR objectives.

plugins/getstream/tests/test_getstream_plugin.py (1)

7-72: LGTM! Well-structured regression test for track republishing.

The test properly validates the critical fix: when a user stops and restarts screensharing, the second TrackAddedEvent is now correctly emitted (line 67 assertion). The test flow is clear:

  1. Emit initial TrackAddedEvent → verify received
  2. Emit TrackRemovedEvent → verify received
  3. Emit TrackAddedEvent again → verify the republish event is received (the bug fix)

The implementation correctly follows coding guidelines: no @pytest.mark.asyncio decorator (async support is automatic), uses event_manager.wait() for deterministic timing instead of sleeps, and uses TrackType.TRACK_TYPE_SCREEN_SHARE enum instead of magic numbers.

As per coding guidelines

plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py (5)

109-112: LGTM! Agent track filtering prevents self-subscription.

Skipping the agent's own tracks early avoids unnecessary subscription logic and potential feedback loops. The debug log provides visibility for troubleshooting.


117-130: LGTM! Core fix for track republishing.

When a track is re-published (e.g., user stops and restarts screensharing), this code path now correctly:

  1. Marks the track as published in the map
  2. Retrieves the stored track_id
  3. Emits TrackAddedEvent so the agent can switch back to the track

This directly addresses the bug described in the PR objectives where "republished tracks were not being shared with the AI."


168-176: LGTM! TrackAddedEvent emission for pending track resolution.

Correctly emits the event after successfully resolving a pending track from the WebRTC layer. The payload matches the TrackAddedEvent schema (no user_metadata field).


285-285: LGTM! Signature allows optional PCM data.

The pcm: PcmData | None type correctly reflects scenarios where audio is received but doesn't require direct processing. The maintainer confirmed this is intentional for logging received audio without necessarily acting on it.


400-403: LGTM! H.264 encoder aligns with even-dimension cropping.

Switching from VP8 to H.264 pairs well with the new ensure_even_dimensions utility introduced in this PR, since H.264 with yuv420p format requires even dimensions. The demo parameters (1920×1080) are already even, ensuring compatibility.

agents-core/vision_agents/core/agents/agents.py (8)

17-17: LGTM! Clean imports for new functionality.

The new imports support track removal events, video forwarding infrastructure, and the screenshare dimension fix.

Also applies to: 36-37


164-166: Good initialization of track metadata structures.

The instance variables for tracking active video tracks and current track ID are properly initialized in __init__ with clear type hints and documentation.


786-794: Excellent fix for screenshare dimension issues!

The wrapper class that applies ensure_even_dimensions to screenshare tracks elegantly solves the non-even pixel bug mentioned in the PR objectives. The implementation is clean and only applies the fix where needed.


849-849: Good fix: Now following Python naming conventions.

The variable now correctly uses has_image_processors (snake_case) instead of the previous camelCase hasImageProcessers.


818-868: Realtime mode forwarder logic is well-structured.

The conditional logic properly handles two scenarios:

  • When a video publisher exists: forwards processed frames via a separate forwarder
  • Otherwise: forwards raw frames directly

The early return when no image processors are present avoids unnecessary work.


907-930: Excellent CancelledError cleanup implementation.

The exception handler properly stops and removes both raw and processed forwarders when the task is cancelled. The defensive checks prevent errors if forwarders weren't created, and error handling ensures cleanup continues even if stopping one forwarder fails.


874-906: Robust frame processing loop with proper error handling.

The loop implementation includes:

  • Timeout with exponential backoff to handle temporary frame starvation
  • Error counting to track consecutive issues
  • Reset on success to avoid penalizing transient errors
  • Proper logging at appropriate levels

672-684: Verify GetStream SDK behavior for repeated add_track_subscriber calls.

The re-add logic (line 680) calls add_track_subscriber(track_id) again for a track already in _active_video_tracks, reusing the existing forwarder. However, without access to GetStream's SDK documentation or source code, it's unclear whether:

  • Calling add_track_subscriber twice on the same track_id is idempotent (safe)
  • It creates duplicate subscriptions or resource leaks
  • A new subscription replaces the old one

The codebase consistently uses this pattern (initial subscription at line 780 and re-add at line 680), but no tests validate its safety. Recommend either:

  1. Checking GetStream SDK documentation/source for subscriber_pc.add_track_subscriber idempotency guarantees
  2. Adding a clarifying comment explaining why this pattern is safe
  3. Testing with GetStream to confirm behavior

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
agents-core/vision_agents/core/agents/agents.py (2)

743-770: Snapshot items before iteration to prevent concurrent modification.

Line 751 iterates over self._active_video_tracks.items() directly, but concurrent calls to on_track or on_track_removed could modify this dictionary during iteration, raising RuntimeError. Take a snapshot first.

Based on learnings (past review comments).

Apply this diff:

-        for track_id, (track_type, participant, forwarder) in self._active_video_tracks.items():
+        for track_id, (track_type, participant, forwarder) in list(self._active_video_tracks.items()):

691-714: Stop forwarder directly to prevent leaks when _process_track exits early.

While task cancellation triggers cleanup in the CancelledError handler (lines 906-929), if _process_track returned early (e.g., at line 867 when there are no image processors), the forwarder remains running because the task is already complete. Explicitly stop the forwarder here to ensure cleanup in all cases.

Apply this diff:

         # Clean up track metadata
-        self._active_video_tracks.pop(track_id, None)
+        track_info = self._active_video_tracks.pop(track_id, None)
+        
+        # Stop the forwarder if present
+        if track_info and len(track_info) > 2:
+            forwarder = track_info[2]
+            if forwarder is not None:
+                try:
+                    await forwarder.stop()
+                    if forwarder in self._video_forwarders:
+                        self._video_forwarders.remove(forwarder)
+                except Exception as e:
+                    self.logger.error(f"Error stopping forwarder for {track_id}: {e}")
         
         # If this was the active track, switch to any other available track
🧹 Nitpick comments (1)
agents-core/vision_agents/core/agents/agents.py (1)

786-795: Consider extracting the wrapper class for clarity.

The inline _EvenDimensionsTrack class definition correctly solves the screensharing bug by ensuring even dimensions. However, defining a class inside a method is unconventional and impacts readability.

Consider extracting to a module-level helper or using a wrapper function:

Option 1: Module-level class

Add near the top of the file after imports:

class _EvenDimensionsTrack(VideoStreamTrack):
    """Wrapper that ensures video frames have even dimensions for H.264 encoding."""
    
    def __init__(self, src):
        super().__init__()
        self.src = src
    
    async def recv(self):
        return ensure_even_dimensions(await self.src.recv())

Then at line 795:

-                track = _EvenDimensionsTrack(track)  # type: ignore[arg-type]
+            track = _EvenDimensionsTrack(track)  # type: ignore[arg-type]

Option 2: Keep inline (current approach is acceptable if you prefer locality)

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ca62d37 and 8019c14.

📒 Files selected for processing (1)
  • agents-core/vision_agents/core/agents/agents.py (5 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/python.mdc)

**/*.py: Do not modify sys.path in Python code
Docstrings must follow the Google style guide

Files:

  • agents-core/vision_agents/core/agents/agents.py
🧬 Code graph analysis (1)
agents-core/vision_agents/core/agents/agents.py (7)
agents-core/vision_agents/core/edge/events.py (3)
  • AudioReceivedEvent (9-13)
  • TrackAddedEvent (17-22)
  • TrackRemovedEvent (26-31)
agents-core/vision_agents/core/utils/video_forwarder.py (4)
  • VideoForwarder (13-188)
  • start (31-39)
  • next_frame (83-101)
  • stop (41-58)
agents-core/vision_agents/core/utils/video_utils.py (1)
  • ensure_even_dimensions (6-26)
agents-core/vision_agents/core/llm/realtime.py (2)
  • Realtime (23-201)
  • _watch_video_track (68-70)
plugins/gemini/vision_agents/plugins/gemini/gemini_realtime.py (2)
  • Realtime (52-676)
  • _watch_video_track (385-424)
plugins/openai/vision_agents/plugins/openai/openai_realtime.py (2)
  • Realtime (37-460)
  • _watch_video_track (263-267)
plugins/getstream/vision_agents/plugins/getstream/stream_edge_transport.py (1)
  • add_track_subscriber (310-313)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: unit / Ruff & mypy
  • GitHub Check: unit / Test "not integration"
  • GitHub Check: unit / Ruff & mypy
  • GitHub Check: unit / Test "not integration"
🔇 Additional comments (6)
agents-core/vision_agents/core/agents/agents.py (6)

17-17: LGTM! Import additions support the new functionality.

The new imports for TrackRemovedEvent, VideoForwarder, and ensure_even_dimensions are necessary for handling track removal events, managing video frame forwarding, and fixing the screensharing pixel dimension issue.

Also applies to: 36-37


164-167: Good initialization of video track state.

The instance variables are properly initialized in __init__, addressing the past review concern about lazy initialization. The metadata structure clearly tracks the forwarder alongside track type and participant information, which will be essential for cleanup operations.


673-686: Excellent fix for republished track handling.

This logic correctly addresses the issue where republished tracks were not being shared with the AI. By reusing the existing forwarder and re-subscribing to the track, you avoid creating duplicate tasks and maintain continuity in the video processing pipeline. The early return at line 685 is crucial to prevent creating a new processing task.


797-812: Well-structured VideoForwarder creation and metadata management.

The shared VideoForwarder prevents multiple consumers from competing on track.recv(), and storing the forwarder in the metadata tuple enables proper cleanup. The pattern of creating, starting, tracking, and storing metadata is clean and maintainable.


817-847: Appropriate handling of processed vs. raw video in realtime mode.

The logic correctly distinguishes between scenarios where a video publisher exists (YOLO-processed frames) versus raw frames. Creating separate forwarders for processed video prevents interference with raw video processing, and the shared forwarder pattern ensures efficient frame delivery to the Realtime provider.


906-929: Excellent CancelledError cleanup implementation.

The cleanup handler properly stops and removes both raw_forwarder and processed_forwarder, addressing the past review concern about noisy tracebacks. The defensive checks (is not None, hasattr, membership tests) prevent errors during cleanup, and exception logging helps diagnose issues without crashing.

@tschellenbach tschellenbach merged commit 5bcffa3 into main Oct 25, 2025
2 of 5 checks passed
@tschellenbach tschellenbach deleted the fix-screensharing branch October 25, 2025 16:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants