Skip to content
Open
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
4 changes: 4 additions & 0 deletions packages/stream_video_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### ✅ Added

- Added `StreamPictureInPictureThemeData` on `StreamVideoTheme`, whose `StreamPictureInPictureStyle.tileStyle` restyles the participant tile the Android picture-in-picture window draws.
- Added `StreamParticipantLabelStyle.showVideoOffIcon`, to leave the camera-off icon out of the name pill.
- `StreamLayoutButton` draws the participant layout in effect and offers the rest through a `StreamAdaptiveMenuAnchor`.
- `StreamLayoutButton.defaultLayouts` is `auto` and `speakerBottom`, so the button toggles unless it is given more.
- Added layout strings to the localizations, in English and Dutch: `layoutMenuTitle`, `layoutSelectTooltip`, `layoutDefault`, `layoutGrid`, `layoutSpeakerTop`, `layoutSpeakerBottom`, `layoutSpeakerLeft`, `layoutSpeakerRight` and `layoutSpeakerOneToOne`.
Expand Down Expand Up @@ -156,6 +158,8 @@

- Fixed the participant grid rearranging itself when a participant nobody can see starts speaking. They take the place of the tile with the least claim to one — the last one on screen — instead of the first, which used to move every tile below it down one.
- Fixed a participant tile on screen being recorded as not visible, which kept it out of the running for a speaker's tile and could get its track unsubscribed. A renderer showing a participant now says so again when the call state disagrees, and the floating self-view no longer shares its visibility bookkeeping with the same participant's tile in the grid.
- The Android picture-in-picture window draws the name and the connection quality in its corners, and no other chrome.
- The name pill draws nothing at all when it has neither a name nor an indicator, instead of an empty rounded rectangle over the video.
- The floating self-view draws no name pill, whatever an app-wide participant tile theme asks for. `StreamFloatingParticipantTileStyle.tileStyle` still can.
- A participant tile keeps the name in its label at every size it draws the label at, truncating with an ellipsis.
- The participant label stops growing at 268px, set by `StreamParticipantLabelStyle.maxWidth`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ class DefaultStreamParticipantLabel extends StatelessWidget {
nameTextStyle.color ??
defaults.microphoneOffColor,
),
if (!props.isVideoEnabled)
if (!props.isVideoEnabled &&
(style?.showVideoOffIcon ?? defaults.showVideoOffIcon))
Icon(
context.streamIcons.videoOffFill,
size: style?.videoOffIconSize ?? defaults.videoOffIconSize,
Expand Down Expand Up @@ -206,6 +207,12 @@ class DefaultStreamParticipantLabel extends StatelessWidget {
StreamAudioIndicator(isSpeaking: props.isSpeaking, style: style),
];

final showsName = props.showName && props.name.isNotEmpty;

// A participant with no name set, under a style drawing none of the
// indicators, leaves an empty pill sitting on the video.
if (!showsName && indicators.isEmpty) return const SizedBox.shrink();

Widget content = Padding(
padding: style?.padding ?? defaults.padding,
child: Row(
Expand All @@ -214,7 +221,7 @@ class DefaultStreamParticipantLabel extends StatelessWidget {
// An empty name draws a zero-width Text that still claims the gap
// before the indicators, leaving the pill padded for a name it is
// not showing. A participant with no name set is not unusual.
if (props.showName && props.name.isNotEmpty)
if (showsName)
// Flexible, not Expanded: the pill is only as wide as it needs to
// be, up to whatever its parent allows. Combined with the parent's
// bound this is what makes a long name ellipsize instead of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class StreamParticipantLabelStyleDefaults extends StreamParticipantLabelStyle {
Color get backgroundColor => _colorScheme.backgroundOverlayDarkStrong;

@override
BorderRadius get borderRadius => BorderRadius.all(_radius.lg);
BorderRadiusGeometry get borderRadius => BorderRadius.all(_radius.lg);

@override
EdgeInsetsGeometry get padding => EdgeInsetsDirectional.fromSTEB(
Expand All @@ -57,6 +57,9 @@ class StreamParticipantLabelStyleDefaults extends StreamParticipantLabelStyle {
@override
bool get showAudioIndicator => true;

@override
bool get showVideoOffIcon => true;

// Whatever the sound indicator would have made it, so a pill drawing
// something shorter in its place is the size it would have been with it.
@override
Expand Down Expand Up @@ -161,7 +164,9 @@ double participantLabelMinWidth(
final indicators = <double>[
if (showMicrophoneOff)
resolved?.microphoneIconSize ?? defaults.microphoneIconSize,
if (showVideoOff) resolved?.videoOffIconSize ?? defaults.videoOffIconSize,
if (showVideoOff &&
(resolved?.showVideoOffIcon ?? defaults.showVideoOffIcon))
resolved?.videoOffIconSize ?? defaults.videoOffIconSize,
if (showVideoPaused)
resolved?.videoPausedIconSize ?? defaults.videoPausedIconSize,
// The sound indicator stands in for the microphone icon rather than
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import '../../../../stream_video_flutter.dart';
import '../../../call_participants/screen_share_call_participants_content.dart';
import 'picture_in_picture_defaults.dart';

/// A dedicated overlay widget for Android Picture-in-Picture mode.
/// This widget creates a floating overlay that shows only the video content
Expand Down Expand Up @@ -110,6 +111,9 @@ class _AndroidPipOverlayState extends State<AndroidPipOverlay>
rendererScopePrefix: 'pipVideo',
call: widget.call,
participant: pipParticipant,
style: pictureInPictureTileStyle(context).merge(
StreamPictureInPictureTheme.of(context).style?.tileStyle,
),
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import '../../../../stream_video_flutter.dart';
import '../../../call_participants/indicators/connection_quality_indicator_defaults.dart';

/// The participant tile style the picture-in-picture window draws with, before
/// [StreamPictureInPictureStyle.tileStyle] is merged over it.
///
/// Shared with the tests that assert what the window draws, so the shape they
/// check is the one it uses. Deliberately not exported.
@internal
StreamParticipantTileStyle pictureInPictureTileStyle(BuildContext context) {
final radius = context.streamRadius;

// The chrome sits in the corners of the window, so the corner each piece
// occupies is square and only the inner one is rounded. Directional: the
// toolbar puts the pill at the start and the indicator at the end, and which
// corner each of those is comes out in the layout.
final cornerRadius = radius.lg;

return StreamParticipantTileStyle(
// The window is rounded by the system, so a tile rounding itself as well
// leaves the Material behind it showing in the corners. Which also rules
// out an outline: it would be drawn square and then have its corners
// clipped away by the window.
borderRadius: BorderRadius.zero,
border: const Border(),
showSpeakerBorder: false,
showMoreButton: false,
// Flush into the window's own corners: at this size an inset costs more
// video than it buys in breathing room.
toolbarPadding: EdgeInsets.zero,
labelStyle: StreamParticipantLabelStyle(
showAudioIndicator: false,
showVideoOffIcon: false,
borderRadius: BorderRadiusDirectional.only(topEnd: cornerRadius),
),
connectionQualityIndicatorStyle: StreamConnectionQualityIndicatorStyle(
// Only the shape changes, so it is taken off the decoration the indicator
// would have drawn — resolved the way the indicator resolves it — rather
// than described again here, which would drop an app's own fill. A
// rounded rectangle where the default is a circle, and BoxDecoration
// allows a radius on neither shape but the rectangle.
decoration: _indicatorDecoration(context).copyWith(
shape: BoxShape.rectangle,
borderRadius: BorderRadiusDirectional.only(topStart: cornerRadius),
),
),
);
}

/// The decoration the connection quality indicator would draw here.
BoxDecoration _indicatorDecoration(BuildContext context) {
final style = StreamConnectionQualityIndicatorTheme.of(context).style;
return style?.decoration ??
StreamConnectionQualityIndicatorStyleDefaults(context).decoration;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export 'floating_participant_tile_theme.dart';
export 'lobby_view_theme.dart';
export 'participant_label_theme.dart';
export 'participant_tile_theme.dart';
export 'picture_in_picture_theme.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ class StreamParticipantLabelStyle with _$StreamParticipantLabelStyle {
this.microphoneIconSize,
this.microphoneOffColor,
this.showAudioIndicator,
this.showVideoOffIcon,
});

/// The pill's fill.
Expand All @@ -140,7 +141,7 @@ class StreamParticipantLabelStyle with _$StreamParticipantLabelStyle {
/// The pill's corner radius.
///
/// Defaults to `radius.lg`.
final BorderRadius? borderRadius;
final BorderRadiusGeometry? borderRadius;

/// The inset around the pill's content.
///
Expand Down Expand Up @@ -257,6 +258,13 @@ class StreamParticipantLabelStyle with _$StreamParticipantLabelStyle {
/// closed microphone for it to report.
final bool? showAudioIndicator;

/// Whether to draw the camera-off icon.
///
/// Defaults to true. Turn it off where the pill reports who a participant is
/// rather than what their devices are doing — the picture-in-picture window,
/// where the placeholder already stands in for the camera.
final bool? showVideoOffIcon;

/// Linearly interpolate between two styles.
static StreamParticipantLabelStyle? lerp(
StreamParticipantLabelStyle? a,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import 'package:flutter/widgets.dart';
import 'package:theme_extensions_builder_annotation/theme_extensions_builder_annotation.dart';

import '../../../stream_video_flutter.dart';

part 'picture_in_picture_theme.g.theme.dart';

/// Applies a picture-in-picture theme to the descendant picture-in-picture
/// window.
///
/// The window is inserted into the nearest [Overlay], above the route showing
/// the call. Set [StreamVideoTheme.pictureInPictureTheme], or wrap this above
/// the [Navigator], to reach it.
///
/// See also:
///
/// * [StreamPictureInPictureThemeData], which describes the theme.
/// * [StreamPictureInPictureStyle], the visual style it carries.
class StreamPictureInPictureTheme extends InheritedTheme {
/// Creates a picture-in-picture theme.
const StreamPictureInPictureTheme({
super.key,
required this.data,
required super.child,
});

/// The picture-in-picture theme data for descendant widgets.
final StreamPictureInPictureThemeData data;

/// Returns the [StreamPictureInPictureThemeData] merged from local and global
/// themes.
///
/// Local values from the nearest [StreamPictureInPictureTheme] ancestor take
/// precedence over the global values from
/// [StreamVideoTheme.pictureInPictureTheme].
static StreamPictureInPictureThemeData of(BuildContext context) {
final localTheme = context
.dependOnInheritedWidgetOfExactType<StreamPictureInPictureTheme>();
return StreamVideoTheme.of(
context,
).pictureInPictureTheme.merge(localTheme?.data);
}

@override
Widget wrap(BuildContext context, Widget child) {
return StreamPictureInPictureTheme(data: data, child: child);
}

@override
bool updateShouldNotify(StreamPictureInPictureTheme oldWidget) =>
data != oldWidget.data;
}

/// Theme data for customizing the picture-in-picture window.
///
/// See also:
///
/// * [StreamPictureInPictureStyle], the style embedded here.
/// * [StreamPictureInPictureTheme], for overriding it in a subtree.
@themeGen
@immutable
class StreamPictureInPictureThemeData with _$StreamPictureInPictureThemeData {
/// Creates picture-in-picture theme data.
const StreamPictureInPictureThemeData({this.style});

/// Visual styling for the picture-in-picture window.
final StreamPictureInPictureStyle? style;

/// Linearly interpolate between two theme data objects.
static StreamPictureInPictureThemeData? lerp(
StreamPictureInPictureThemeData? a,
StreamPictureInPictureThemeData? b,
double t,
) => _$StreamPictureInPictureThemeData.lerp(a, b, t);
}

/// Visual styling properties for the picture-in-picture window.
///
/// Applies to the window Android draws. The iOS window is rendered natively
/// and is configured through `IOSPictureInPictureConfiguration`.
@themeGen
@immutable
class StreamPictureInPictureStyle with _$StreamPictureInPictureStyle {
/// Creates a picture-in-picture style with optional property overrides.
const StreamPictureInPictureStyle({this.tileStyle});

/// Overrides applied to the participant tile the window renders.
///
/// Merged over the ambient [StreamParticipantTileTheme] style and over the
/// window's own choices: it draws no name pill, connection quality indicator
/// or overflow button, and this is what puts them back.
///
/// Has no effect on a window built by
/// `AndroidPictureInPictureConfiguration.callPictureInPictureWidgetBuilder`.
final StreamParticipantTileStyle? tileStyle;

/// Linearly interpolate between two styles.
static StreamPictureInPictureStyle? lerp(
StreamPictureInPictureStyle? a,
StreamPictureInPictureStyle? b,
double t,
) => _$StreamPictureInPictureStyle.lerp(a, b, t);
}
Loading