Skip to content

Conversation

@Brazol
Copy link
Contributor

@Brazol Brazol commented Dec 31, 2025

Summary by CodeRabbit

  • New Features

    • Added Picture-in-Picture mode support for Android devices.
  • Bug Fixes

    • Fixed video flickering issues on Android devices with the Skia renderer through improved video rendering stability.
  • Documentation

    • Updated changelog with details on Android rendering enhancements.

✏️ Tip: You can customize this high-level summary in your review settings.

# Conflicts:
#	packages/stream_video_flutter/lib/src/call_participants/call_participant.dart
@coderabbitai
Copy link

coderabbitai bot commented Dec 31, 2025

📝 Walkthrough

Walkthrough

This PR implements Android Picture-in-Picture (PiP) support for video calls and fixes video flickering by introducing renderer scope prefixing. It bumps the stream_webrtc_flutter dependency, adds a new AndroidPipManager class for PiP lifecycle management, extends widget constructors with a rendererScopePrefix parameter for scoped rendering, and updates widget key generation for improved tracking across different rendering contexts.

Changes

Cohort / File(s) Summary
Dependency Updates
melos.yaml, packages/stream_video/pubspec.yaml, packages/stream_video_filters/pubspec.yaml, packages/stream_video_flutter/example/pubspec.yaml, packages/stream_video_flutter/pubspec.yaml, packages/stream_video_noise_cancellation/pubspec.yaml, packages/stream_video_push_notification/pubspec.yaml
Bumped stream_webrtc_flutter from ^2.2.3 to ^2.2.4 across all configuration files.
Renderer Scope Prefix Implementation
packages/stream_video_flutter/lib/src/call_participants/call_participant.dart, packages/stream_video_flutter/lib/src/renderer/video_renderer.dart
Added optional rendererScopePrefix parameter to StreamCallParticipant and StreamVideoRenderer to enable namespaced rendering keys for different contexts (PiP, livehost, screenshare).
Android PiP Manager
packages/stream_video_flutter/lib/src/call_screen/call_content/picture_in_picture/android_pip_manager.dart
Introduced new AndroidPipManager singleton class with platform-channel integration for Android PiP control, including listener management and mode tracking.
Call Content PiP Integration
packages/stream_video_flutter/lib/src/call_screen/call_content/call_content.dart, packages/stream_video_flutter/lib/src/call_screen/call_content/picture_in_picture/stream_picture_in_picture_android_view.dart
Integrated AndroidPipManager for PiP lifecycle management and added conditional rendering logic to hide participants when in PiP mode.
PiP Overlay & Key Updates
packages/stream_video_flutter/lib/src/call_participants/livestream_hosts.dart, packages/stream_video_flutter/lib/src/call_participants/screen_share_call_participants_content.dart, packages/stream_video_flutter/lib/src/call_screen/call_content/picture_in_picture/android_pip_overlay.dart, packages/stream_video_flutter/lib/src/livestream/livestream_content.dart
Added rendererScopePrefix parameters to widgets and updated widget key generation with context-specific suffixes (livehost, screenshare, pipVideo, pipScreenShare, livecontent) for better widget identity tracking.
Exports & Documentation
packages/stream_video_flutter/lib/stream_video_flutter.dart, packages/stream_video_flutter/CHANGELOG.md
Exported new AndroidPipManager and PiP UI kit view; added changelog entry documenting Android video flickering fix.

Sequence Diagram(s)

sequenceDiagram
    participant CallContent as call_content.dart
    participant APM as AndroidPipManager
    participant Platform as Native Android
    participant UI as UI Layer

    CallContent->>APM: getInstance()
    CallContent->>APM: addOnPictureInPictureModeChangedListener()
    
    Note over Platform: User enters PiP mode
    Platform->>APM: methodChannel.invokeMethod('onPictureInPictureModeChanged')
    
    APM->>APM: _handleMethodCall()
    APM->>APM: _notifyListeners()
    APM->>CallContent: listener callback (isInPictureInPictureMode=true)
    
    CallContent->>CallContent: _handlePictureInPictureModeChanged()
    CallContent->>CallContent: setState()
    CallContent->>UI: rebuild with _isInPictureInPictureMode flag
    UI->>UI: conditionally hide participants widget if in PiP

    Note over Platform: User exits PiP mode
    Platform->>APM: methodChannel.invokeMethod('onPictureInPictureModeChanged')
    APM->>CallContent: listener callback (isInPictureInPictureMode=false)
    CallContent->>UI: rebuild and show participants widget
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • renefloor
  • xsahil03x

Poem

🐰 A scoped prefix hops through the renderer tree,
Flickering fixes and keys set free,
PiP managers listen from Android's domain,
Widget identity blooms, no more duplicate pain! 📱✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is completely empty, missing all required sections including Goal, Implementation details, UI Changes, Testing, and Contributor Checklist. Provide a comprehensive description following the template: explain the flickering issue, describe the renderer scope prefix implementation, include before/after UI comparisons, document testing approach, and complete the contributor checklist.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: fixing Skia renderer flickering on Android, which aligns with the changelog entry and the renderer scope prefix changes throughout the codebase.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

@codecov
Copy link

codecov bot commented Dec 31, 2025

Codecov Report

❌ Patch coverage is 25.75758% with 49 lines in your changes missing coverage. Please review.
✅ Project coverage is 6.47%. Comparing base (f69e458) to head (89c9eae).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...ontent/picture_in_picture/android_pip_manager.dart 40.90% 13 Missing ⚠️
...ipants/screen_share_call_participants_content.dart 0.00% 6 Missing ⚠️
...lib/src/call_screen/call_content/call_content.dart 57.14% 6 Missing ⚠️
...icture/stream_picture_in_picture_android_view.dart 0.00% 6 Missing ⚠️
...video_flutter/lib/src/renderer/video_renderer.dart 0.00% 5 Missing ⚠️
...flutter/lib/src/livestream/livestream_content.dart 0.00% 4 Missing ⚠️
...er/lib/src/call_participants/call_participant.dart 0.00% 3 Missing ⚠️
...er/lib/src/call_participants/livestream_hosts.dart 0.00% 3 Missing ⚠️
...ontent/picture_in_picture/android_pip_overlay.dart 0.00% 3 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##            main   #1147      +/-   ##
========================================
+ Coverage   6.44%   6.47%   +0.03%     
========================================
  Files        600     601       +1     
  Lines      41978   42018      +40     
========================================
+ Hits        2705    2721      +16     
- Misses     39273   39297      +24     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Brazol Brazol marked this pull request as ready for review January 29, 2026 09:22
@Brazol Brazol requested a review from a team as a code owner January 29, 2026 09:22
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

Caution

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

⚠️ Outside diff range comments (1)
packages/stream_video_flutter/lib/src/call_participants/livestream_hosts.dart (1)

99-106: Missing rendererScopePrefix in _buildScreenShareContent.

The fallback ScreenShareContent widget doesn't pass rendererScopePrefix, unlike similar usages elsewhere in this PR:

  • livestream_content.dart passes rendererScopePrefix: 'livecontent'
  • android_pip_overlay.dart passes rendererScopePrefix: 'pipScreenShare'
  • screen_share_call_participants_content.dart passes rendererScopePrefix: 'screenshareContent'

Given this PR's goal is to fix Skia flickering via renderer scope prefixing, the missing prefix here could leave this code path vulnerable to the same issue.

🐛 Suggested fix
   Widget _buildScreenShareContent(CallParticipantState host) {
     return widget.screenShareContentBuilder?.call(context, widget.call, host) ??
         ScreenShareContent(
           key: ValueKey('${host.uniqueParticipantKey}-livehost-screenshare'),
+          rendererScopePrefix: 'livehost',
           call: widget.call,
           participant: host,
         );
   }
🤖 Fix all issues with AI agents
In
`@packages/stream_video_flutter/lib/src/call_screen/call_content/picture_in_picture/android_pip_manager.dart`:
- Around line 53-61: In _handleMethodCall, avoid mutating
_onPictureInPictureModeChangedListeners while iterating (which can throw
ConcurrentModificationError) by iterating over a shallow copy of the list before
invoking each listener; keep setting _isInPictureInPictureMode from
call.arguments as before and then iterate over something like
List.from(_onPictureInPictureModeChangedListeners) so listeners can safely
remove themselves during notification.
🧹 Nitpick comments (5)
packages/stream_video_flutter/lib/src/renderer/video_renderer.dart (1)

110-156: Add a delimiter between prefix and participant key to avoid ambiguous collisions.
Without a separator, certain prefix/participant combinations could collide (e.g., prefix “live” + key “host123” vs prefix “livehost” + key “123”).

♻️ Proposed tweak (delimiter)
-    return VisibilityDetector(
-      key: Key(
-        '${widget.rendererScopePrefix ?? ''}${widget.participant.uniqueParticipantKey}${widget.videoTrackType}-visibility',
-      ),
+    final scopePrefix =
+        widget.rendererScopePrefix == null ? '' : '${widget.rendererScopePrefix}-';
+    return VisibilityDetector(
+      key: Key(
+        '$scopePrefix${widget.participant.uniqueParticipantKey}${widget.videoTrackType}-visibility',
+      ),
       onVisibilityChanged: (info) =>
           _onVisibilityChanged(info, widget.participant.userId),
       child: child,
     );
-    return VideoTrackRenderer(
-      key: Key(
-        '${widget.rendererScopePrefix ?? ''}${widget.participant.uniqueParticipantKey}-${widget.videoTrackType}-renderer',
-      ),
+    final scopePrefix =
+        widget.rendererScopePrefix == null ? '' : '${widget.rendererScopePrefix}-';
+    return VideoTrackRenderer(
+      key: Key(
+        '$scopePrefix${widget.participant.uniqueParticipantKey}-${widget.videoTrackType}-renderer',
+      ),
       videoFit: widget.videoFit,
       videoTrack: videoTrack,
       mirror: mirror,
       placeholderBuilder: widget.placeholderBuilder,
     );
packages/stream_video_flutter/lib/src/call_screen/call_content/picture_in_picture/stream_picture_in_picture_android_view.dart (1)

100-102: Consider handling potential errors from the async call.

_setPictureInPictureAllowed returns a Future<void> but is called without await in multiple places (lines 60, 75, 96). While fire-and-forget may be intentional here, unhandled errors from the platform channel could go unnoticed.

If errors are expected to be silently ignored, consider adding .ignore() or a minimal error handler to make this explicit:

_setPictureInPictureAllowed(shouldAllow).ignore();
packages/stream_video_flutter/lib/src/call_screen/call_content/picture_in_picture/android_pip_overlay.dart (1)

97-113: Key format inconsistency with other files.

The key separators here use - (space-dash-space):

  • Line 99: '${pipParticipant.uniqueParticipantKey} - pipScreenShare'
  • Line 108: '${pipParticipant.uniqueParticipantKey} - pipVideo'

However, other files in this PR use - (dash only):

  • screen_share_call_participants_content.dart: '-screenShareContent', '-screenshare-video'
  • livestream_content.dart: '-livecontent-video', '-livecontent-screenshare'
  • livestream_hosts.dart: '-livehost-video', '-livehost-screenshare'

Consider using a consistent separator format across all files for maintainability.

♻️ Suggested fix for consistency
       if (shouldShowScreenShare) {
         pipBody = ScreenShareContent(
           key: ValueKey(
-            '${pipParticipant.uniqueParticipantKey} - pipScreenShare',
+            '${pipParticipant.uniqueParticipantKey}-pipScreenShare',
           ),
           rendererScopePrefix: 'pipScreenShare',
           call: widget.call,
           participant: pipParticipant,
         );
       } else {
         pipBody = StreamCallParticipant(
           key: ValueKey(
-            '${pipParticipant.uniqueParticipantKey} - pipVideo',
+            '${pipParticipant.uniqueParticipantKey}-pipVideo',
           ),
           rendererScopePrefix: 'pipVideo',
           call: call,
           participant: pipParticipant,
         );
       }
packages/stream_video_flutter/lib/src/call_participants/screen_share_call_participants_content.dart (1)

181-185: Potential key format inconsistency when prefix is present.

When rendererScopePrefix is provided, the key becomes '{prefix}{participantKey}-screen-share' without a separator between the prefix and participant key. For example: 'screenshareContentuser123-screen-share'.

Consider adding a separator for consistency and readability:

♻️ Suggested improvement
               child: StreamVideoRenderer(
                 key: ValueKey(
-                  '${widget.rendererScopePrefix ?? ''}${widget.participant.uniqueParticipantKey}-screen-share',
+                  '${widget.rendererScopePrefix != null ? '${widget.rendererScopePrefix}-' : ''}${widget.participant.uniqueParticipantKey}-screen-share',
                 ),
                 rendererScopePrefix: widget.rendererScopePrefix,
packages/stream_video_flutter/lib/src/call_screen/call_content/call_content.dart (1)

108-110: Consider guarding AndroidPipManager listener registration with a platform check.

AndroidPipManager listeners are registered and removed unconditionally (lines 123-125, 141-143) on all platforms. While the code functions correctly since the method channel won't deliver Android callbacks on non-Android platforms, it's cleaner to explicitly guard this Android-specific functionality with a platform check, consistent with the PiP view rendering logic (lines 195, 205).

♻️ Suggested refactor
   `@override`
   void initState() {
     super.initState();
     _startListeningToCallState();

-    _androidPipManager.addOnPictureInPictureModeChangedListener(
-      _handlePictureInPictureModeChanged,
-    );
+    if (CurrentPlatform.isAndroid) {
+      _androidPipManager.addOnPictureInPictureModeChangedListener(
+        _handlePictureInPictureModeChanged,
+      );
+    }
   }
   `@override`
   void dispose() {
     _callStateSubscription?.cancel();
-    _androidPipManager.removeOnPictureInPictureModeChangedListener(
-      _handlePictureInPictureModeChanged,
-    );
+    if (CurrentPlatform.isAndroid) {
+      _androidPipManager.removeOnPictureInPictureModeChangedListener(
+        _handlePictureInPictureModeChanged,
+      );
+    }
     super.dispose();
   }

@Brazol Brazol merged commit 7d06915 into main Jan 29, 2026
16 of 18 checks passed
@Brazol Brazol deleted the fix/skia-pip-flickering branch January 29, 2026 12:17
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