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
5 changes: 5 additions & 0 deletions packages/stream_video_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## Upcoming

### ✅ Added
* Added `prioritiseScreenSharingTrack` parameter to `PictureInPictureConfiguration` to control whether screen sharing or camera track is preferred in PiP mode. When set to `false`, the camera track is preferred, but screen share will still be shown as a fallback if the camera is disabled.

## 1.0.2

🐞 Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,27 +109,36 @@ class StreamCallParticipants extends StatefulWidget {
State<StreamCallParticipants> createState() => _StreamCallParticipantsState();
}

class _StreamCallParticipantsState extends State<StreamCallParticipants> {
List<CallParticipantState> _participants = [];
CallParticipantState? _screenShareParticipant;
class _StreamCallParticipantsState extends State<StreamCallParticipants>
with CallParticipantsSortingMixin {
StreamSubscription<List<CallParticipantState>?>? _participantsSubscription;

List<String> _sortedParticipantKeys = [];
@override
Filter<CallParticipantState> get participantFilter => widget.filter;

StreamSubscription<List<CallParticipantState>?>? _participantsSubscription;
@override
Sort<CallParticipantState> get participantSort => widget.sort;

@override
void initState() {
super.initState();
_recalculateParticipants(
recalculateParticipants(
widget.participants ?? widget.call.state.value.callParticipants,
);

if (widget.participants == null) {
_participantsSubscription = widget.call
.partialState((state) => state.callParticipants)
.listen(_recalculateParticipants);
.listen(recalculateParticipants);
}
}

@override
void dispose() {
_participantsSubscription?.cancel();
super.dispose();
}

@override
void didUpdateWidget(covariant StreamCallParticipants oldWidget) {
super.didUpdateWidget(oldWidget);
Expand All @@ -141,94 +150,33 @@ class _StreamCallParticipantsState extends State<StreamCallParticipants> {
widget.participants!.toList(),
oldWidget.participants?.toList(),
)) {
_recalculateParticipants(widget.participants!);
recalculateParticipants(widget.participants!);
}
} else if (widget.call != oldWidget.call) {
_participantsSubscription?.cancel();
_participantsSubscription = widget.call
.partialState((state) => state.callParticipants)
.listen(_recalculateParticipants);
}
}

void _recalculateParticipants(List<CallParticipantState> newParticipants) {
final participants = [...newParticipants].where(widget.filter).toList();

for (final participant in participants) {
final index = _sortedParticipantKeys.indexOf(
participant.uniqueParticipantKey,
);
if (index == -1) {
_sortedParticipantKeys.add(participant.uniqueParticipantKey);
}
}

// First apply previous sorting on new participants list
participants.sort(
(a, b) => _sortedParticipantKeys
.indexOf(a.uniqueParticipantKey)
.compareTo(_sortedParticipantKeys.indexOf(b.uniqueParticipantKey)),
);

mergeSort(participants, compare: widget.sort);

final screenShareParticipant = participants.firstWhereOrNull(
(it) {
final screenShareTrack = it.screenShareTrack;
final isScreenShareEnabled = it.isScreenShareEnabled;

if (screenShareTrack == null || !isScreenShareEnabled) return false;

return true;
},
);

_sortedParticipantKeys = participants
.map((e) => e.uniqueParticipantKey)
.toList();
.listen(recalculateParticipants);

if (mounted) {
setState(() {
_participants = participants.toList();
_screenShareParticipant = screenShareParticipant;
});
recalculateParticipants(widget.call.state.value.callParticipants);
}
}

@override
Widget build(BuildContext context) {
if (_participants.isNotEmpty &&
widget.layoutMode == ParticipantLayoutMode.pictureInPicture) {
if (_screenShareParticipant != null) {
return ScreenShareContent(
key: ValueKey(
'${_screenShareParticipant!.uniqueParticipantKey} - screenShareContent',
),
call: widget.call,
participant: _screenShareParticipant!,
);
}

return widget.callParticipantBuilder(
context,
widget.call,
_participants.first,
);
}

if (_screenShareParticipant != null) {
if (screenShareParticipant != null) {
return ScreenShareCallParticipantsContent(
call: widget.call,
participants: _participants,
screenSharingParticipant: _screenShareParticipant!,
participants: sortedParticipants,
screenSharingParticipant: screenShareParticipant!,
screenShareContentBuilder: widget.screenShareContentBuilder,
screenShareParticipantBuilder: widget.screenShareParticipantBuilder,
);
}

return RegularCallParticipantsContent(
call: widget.call,
participants: _participants,
participants: sortedParticipants,
layoutMode: widget.layoutMode,
enableLocalVideo: widget.enableLocalVideo,
callParticipantBuilder: widget.callParticipantBuilder,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:stream_video/stream_video.dart';

import 'call_participants.dart';

/// A mixin that provides participant sorting and filtering logic.
mixin CallParticipantsSortingMixin<T extends StatefulWidget> on State<T> {
List<CallParticipantState> get sortedParticipants => _participants;
List<CallParticipantState> _participants = [];

CallParticipantState? get screenShareParticipant => _screenShareParticipant;
CallParticipantState? _screenShareParticipant;

/// Keys used to maintain stable participant ordering across updates.
List<String> _sortedParticipantKeys = [];

/// The filter function to apply to participants.
///
/// Override this getter to provide the filter from your widget.
Filter<CallParticipantState>? get participantFilter;

/// The sort comparator to apply to participants.
///
/// Override this getter to provide the sort from your widget.
Sort<CallParticipantState>? get participantSort;

/// Call this method whenever the participant list changes, typically from
/// a stream subscription or in [didUpdateWidget].
void recalculateParticipants(List<CallParticipantState> newParticipants) {
final participants = [
...newParticipants,
].where(participantFilter ?? (_) => true).toList();

for (final participant in participants) {
final index = _sortedParticipantKeys.indexOf(
participant.uniqueParticipantKey,
);
if (index == -1) {
_sortedParticipantKeys.add(participant.uniqueParticipantKey);
}
}

// First apply previous sorting on new participants list
participants.sort(
(a, b) => _sortedParticipantKeys
.indexOf(a.uniqueParticipantKey)
.compareTo(_sortedParticipantKeys.indexOf(b.uniqueParticipantKey)),
);

if (participantSort != null) {
mergeSort(participants, compare: participantSort);
}

final screenShareParticipant = participants.firstWhereOrNull(
(it) {
final screenShareTrack = it.screenShareTrack;
final isScreenShareEnabled = it.isScreenShareEnabled;

if (screenShareTrack == null || !isScreenShareEnabled) return false;

return true;
},
);

_sortedParticipantKeys = participants
.map((e) => e.uniqueParticipantKey)
.toList();

if (mounted) {
setState(() {
_participants = participants.toList();
_screenShareParticipant = screenShareParticipant;
});
}
}

void clearParticipantSortingCache() {
_sortedParticipantKeys = [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,8 @@ class _StreamCallContentState extends State<StreamCallContent> {
width: 300,
child: StreamPictureInPictureUiKitView(
call: call,
configuration:
widget.pictureInPictureConfiguration.iOSPiPConfiguration,
participantSort: widget.pictureInPictureConfiguration.sort,
pictureInPictureConfiguration:
widget.pictureInPictureConfiguration,
),
),
if (CurrentPlatform.isAndroid && pipEnabled)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
// ignore_for_file: deprecated_member_use_from_same_package

import 'dart:async';

import 'package:flutter/material.dart';
import '../../../../stream_video_flutter.dart';
import '../../../call_participants/screen_share_call_participants_content.dart';

/// A dedicated overlay widget for Android Picture-in-Picture mode.
/// This widget creates a floating overlay that shows only the video content
Expand All @@ -8,31 +13,106 @@ class AndroidPipOverlay extends StatefulWidget {
const AndroidPipOverlay({
super.key,
required this.call,
this.pictureInPictureConfiguration,
@Deprecated(
'Pass PictureInPictureConfiguration [pictureInPictureConfiguration] instead',
)
this.sort,
@Deprecated(
'Pass PictureInPictureConfiguration [pictureInPictureConfiguration] instead',
)
this.customBuilder,
});

final Call call;
final Comparator<CallParticipantState>? sort;
final PictureInPictureConfiguration? pictureInPictureConfiguration;
@Deprecated('Use [pictureInPictureConfiguration.sort] instead')
final Sort<CallParticipantState>? sort;
@Deprecated(
'Use [pictureInPictureConfiguration.androidPiPConfiguration.customBuilder] instead',
)
final CallWidgetBuilder? customBuilder;

@override
State<AndroidPipOverlay> createState() => _AndroidPipOverlayState();
}

class _AndroidPipOverlayState extends State<AndroidPipOverlay> {
class _AndroidPipOverlayState extends State<AndroidPipOverlay>
with CallParticipantsSortingMixin {
StreamSubscription<List<CallParticipantState>?>? _participantsSubscription;

@override
Filter<CallParticipantState>? get participantFilter => null;

@override
Sort<CallParticipantState>? get participantSort =>
widget.pictureInPictureConfiguration?.sort ?? widget.sort;

@override
void initState() {
super.initState();
recalculateParticipants(widget.call.state.value.callParticipants);

_participantsSubscription = widget.call
.partialState((state) => state.callParticipants)
.listen(recalculateParticipants);
}

@override
void dispose() {
_participantsSubscription?.cancel();
super.dispose();
}

@override
Widget build(BuildContext context) {
Widget pipBody = const SizedBox.shrink();

final customBuilder =
widget
.pictureInPictureConfiguration
?.androidPiPConfiguration
.callPictureInPictureWidgetBuilder ??
widget.customBuilder;

if (customBuilder == null && sortedParticipants.isNotEmpty) {
final pipParticipant = sortedParticipants.first;

final hasScreenShare =
pipParticipant.isScreenShareEnabled &&
pipParticipant.screenShareTrack != null;

// Show screen share if:
// 1. prioritise is true and screen share is available, OR
// 2. video is disabled but screen share is available (fallback)
final shouldShowScreenShare =
hasScreenShare &&
(widget.pictureInPictureConfiguration?.pipTrackPriority !=
PipTrackPriority.camera ||
!pipParticipant.isVideoEnabled);

if (shouldShowScreenShare) {
pipBody = ScreenShareContent(
key: ValueKey(
'${pipParticipant.uniqueParticipantKey} - screenShareContent',
),
call: widget.call,
participant: pipParticipant,
);
} else {
pipBody = StreamCallParticipant(
// We use the sessionId as the key to map the state to the participant.
key: Key(pipParticipant.uniqueParticipantKey),
call: widget.call,
participant: pipParticipant,
);
}
}

return Material(
color: Colors.black,
child: SizedBox.expand(
child:
widget.customBuilder?.call(context, widget.call) ??
StreamCallParticipants(
call: widget.call,
layoutMode: ParticipantLayoutMode.pictureInPicture,
sort: widget.sort,
),
child: customBuilder?.call(context, widget.call) ?? pipBody,
),
);
}
Expand Down
Loading