Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(llc): manual quality selection #791

Merged
merged 6 commits into from
Nov 6, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: Manual Video Quality Selection
slug: /manual-video-quality-selection
sidebar_position: 10
description: Learn how to manually select the incoming video quality in the Stream Video Flutter SDK.
---

By default, our SDK chooses the incoming video quality that best matches the size of a video element for a given participant. It makes less sense to waste bandwidth receiving Full HD video when it's going to be displayed in a 320 by 240 pixel rectangle.

However, it's still possible to override this behavior and manually request higher resolution video for better quality, or lower resolution to save bandwidth. It's also possible to disable incoming video altogether for an audio-only experience.

## Overriding Preferred Resolution

To override the preferred incoming video resolution, use the `call.setPreferredIncomingVideoResolution` method:

```dart
await call.setPreferredIncomingVideoResolution(VideoResolution(width: 640, height: 480));
```

:::note
Actual incoming video quality depends on a number of factors, such as the quality of the source video, and network conditions. Manual video quality selection allows you to specify your preference, while the actual resolution is automatically selected from the available resolutions to match that preference as closely as possible.
:::

It's also possible to override the incoming video resolution for only a selected subset of call participants. The `call.setPreferredIncomingVideoResolution()` method optionally takes a list of participant session identifiers as its optional argument. Session identifiers can be obtained from the call participant state:

```dart
final [first, second, ..._] = call.state.value.otherParticipants;

// Set preferred incoming video resolution for the first two participants only:
await call.setPreferredIncomingVideoResolution(
VideoResolution(width: 640, height: 480),
sessionIds: [first.sessionId, second.sessionId],
);
```

Calling this method will enable incoming video for the selected participants if it was previously disabled.

To clear a previously set preference, pass `null` instead of resolution:

```dart
// Clear resolution preference for selected participants:
await call.setPreferredIncomingVideoResolution(
null,
sessionIds: [
participant.sessionId,
],
);
// Clear resolution preference for all participants:
await call.setPreferredIncomingVideoResolution(null);
```

## Disabling Incoming Video

To completely disable incoming video (either to save data, or for an audio-only experience), use the `call.setIncomingVideoEnabled()` method:

```dart
await call.setIncomingVideoEnabled(false);
```

To enable incoming video again, pass `true` as an argument:

```dart
await call.setIncomingVideoEnabled(true);
```

Calling this method will clear the previously set resolution preferences.
47 changes: 47 additions & 0 deletions docusaurus/docs/Flutter/05-advanced/11-session-timers.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
title: Session Timers
slug: /session-timers
sidebar_position: 11
description: Learn how to limit the maximum duration of a call in the Stream Video Flutter SDK.
---

A session timer allows you to limit the maximum duration of a call. The duration [can be configured](https://getstream.io/video/docs/api/calls/#session-timers) for all calls of a certain type, or on a per-call basis. When a session timer reaches zero, the call automatically ends.

## Creating a call with a session timer

Let's see how to create a single call with a limited duration:

```dart
final call = client.makeCall(callType: StreamCallType.defaultType(), id: 'REPLACE_WITH_CALL_ID');
await call.getOrCreate(
limits: const StreamLimitsSettings(
maxDurationSeconds: 3600,
),
);
```

This code creates a call with a duration of 3600 seconds (1 hour) from the time the session is starts (a participant joins the call).

After joining the call with the specified `maxDurationSeconds`, you can examine a call state's `timerEndsAt` field, which provides the timestamp when the call will end. When a call ends, all participants are removed from the call.

```dart
await call.join();
print(call.state.value.timerEndsAt);
```

## Extending a call

​You can also extend the duration of a call, both before or during the call. To do that, you should use the `call.update` method:

```dart
final duration =
call.state.value.settings.limits.maxDurationSeconds! + 60;

call.update(
limits: StreamLimitsSettings(
maxDurationSeconds: duration,
),
);
```

If the call duration is extended, the `timerEndsAt` is updated to reflect this change. Call participants will receive the `call.updated` event to notify them about this change.
138 changes: 135 additions & 3 deletions dogfooding/lib/widgets/settings_menu.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ CallReactionData _raisedHandReaction = const CallReactionData(
icon: '✋',
);

enum IncomingVideoQuality {
auto('Auto'),
p2160('2160p'),
p1080('1080p'),
p720('720p'),
p480('480p'),
p144('144p'),
off('Off');

final String name;

const IncomingVideoQuality(this.name);

@override
String toString() => name;
}

class SettingsMenu extends StatefulWidget {
const SettingsMenu({
required this.call,
Expand Down Expand Up @@ -40,7 +57,9 @@ class _SettingsMenuState extends State<SettingsMenu> {

bool showAudioOutputs = false;
bool showAudioInputs = false;
bool get showMainSettings => !showAudioOutputs && !showAudioInputs;
bool showIncomingQuality = false;
bool get showMainSettings =>
!showAudioOutputs && !showAudioInputs && !showIncomingQuality;

@override
void initState() {
Expand Down Expand Up @@ -83,11 +102,15 @@ class _SettingsMenuState extends State<SettingsMenu> {
if (showMainSettings) ..._buildMenuItems(),
if (showAudioOutputs) ..._buildAudioOutputsMenu(),
if (showAudioInputs) ..._buildAudioInputsMenu(),
if (showIncomingQuality) ..._buildIncomingQualityMenu(),
]),
);
}

List<Widget> _buildMenuItems() {
final incomingVideoQuality = getIncomingVideoQuality(
widget.call.dynascaleManager.incomingVideoSettings);

return [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
Expand Down Expand Up @@ -155,6 +178,24 @@ class _SettingsMenuState extends State<SettingsMenu> {
showAudioInputs = true;
});
},
),
const SizedBox(height: 16),
StandardActionMenuItem(
icon: Icons.high_quality_sharp,
label: 'Incoming video quality',
trailing: Text(
incomingVideoQuality.name,
style: TextStyle(
color: incomingVideoQuality != IncomingVideoQuality.auto
? AppColorPalette.appGreen
: null,
),
),
onPressed: () {
setState(() {
showIncomingQuality = true;
});
},
)
];
}
Expand Down Expand Up @@ -229,6 +270,87 @@ class _SettingsMenuState extends State<SettingsMenu> {
.insertBetween(const SizedBox(height: 16)),
];
}

List<Widget> _buildIncomingQualityMenu() {
return [
GestureDetector(
onTap: () {
setState(() {
showIncomingQuality = false;
});
},
child: const Align(
alignment: Alignment.centerLeft,
child: Icon(Icons.arrow_back, size: 24),
),
),
const SizedBox(height: 16),
...IncomingVideoQuality.values
.map(
(quality) {
return StandardActionMenuItem(
icon: Icons.video_settings,
label: quality.name,
color: getIncomingVideoQuality(widget
.call.dynascaleManager.incomingVideoSettings) ==
quality
? AppColorPalette.appGreen
: null,
onPressed: () {
if (quality == IncomingVideoQuality.off) {
widget.call.setIncomingVideoEnabled(false);
} else {
widget.call.setPreferredIncomingVideoResolution(
getIncomingVideoResolution(quality));
}
},
);
},
)
.cast()
.insertBetween(const SizedBox(height: 16)),
];
}

VideoResolution? getIncomingVideoResolution(IncomingVideoQuality quality) {
switch (quality) {
case IncomingVideoQuality.auto:
case IncomingVideoQuality.off:
return null;
case IncomingVideoQuality.p2160:
return VideoResolution(width: 3840, height: 2160);
case IncomingVideoQuality.p1080:
return VideoResolution(width: 1920, height: 1080);
case IncomingVideoQuality.p720:
return VideoResolution(width: 1280, height: 720);
case IncomingVideoQuality.p480:
return VideoResolution(width: 640, height: 480);
case IncomingVideoQuality.p144:
return VideoResolution(width: 256, height: 144);
}
}

IncomingVideoQuality getIncomingVideoQuality(IncomingVideoSettings? setting) {
final preferredResolution = setting?.preferredResolution;
if (setting?.enabled == false) {
return IncomingVideoQuality.off;
}
if (preferredResolution == null) {
return IncomingVideoQuality.auto;
} else if (preferredResolution.height >= 2160) {
return IncomingVideoQuality.p2160;
} else if (preferredResolution.height >= 1080) {
return IncomingVideoQuality.p1080;
} else if (preferredResolution.height >= 720) {
return IncomingVideoQuality.p720;
} else if (preferredResolution.height >= 480) {
return IncomingVideoQuality.p480;
} else if (preferredResolution.height >= 144) {
return IncomingVideoQuality.p144;
} else {
return IncomingVideoQuality.auto;
}
}
}

class SettingsMenuItem extends StatelessWidget {
Expand Down Expand Up @@ -265,10 +387,12 @@ class StandardActionMenuItem extends StatelessWidget {
required this.label,
this.color,
this.onPressed,
this.trailing,
});

final IconData icon;
final String label;
final Widget? trailing;
final Color? color;
final void Function()? onPressed;

Expand All @@ -285,8 +409,16 @@ class StandardActionMenuItem extends StatelessWidget {
color: color,
),
const SizedBox(width: 8),
Text(label,
style: TextStyle(color: color, fontWeight: FontWeight.bold)),
Text(
label,
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
if (trailing != null) trailing!,
const SizedBox(width: 8),
],
),
);
Expand Down
1 change: 1 addition & 0 deletions packages/stream_video/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This release introduces a major rework of the join/reconnect flow in the Call cl
* Added `participantCount` and `anonymousParticipantCount` to `CallState` reflecting the current number of participants in the call.
* Introduced the `watch` parameter to `Call.get()` and `Call.getOrCreate()` methods (default is `true`). When set to `true`, this enables the `Call` to listen for coordinator events and update its state accordingly, even before the call is joined (`Call.join()`).
* Added support for `targetResolution` setting set on the Dashboard to determine the max resolution the video stream.
* Introduced new API methods to give greater control over incoming video quality. `Call.setPreferredIncomingVideoResolution()` allows you to manually set a preferred video resolution, while `Call.setIncomingVideoEnabled()` enables or disables incoming video. For more details, refer to the [documentation](https://getstream.io/video/docs/flutter/manual-video-quality-selection/).

🐞 Fixed
* Automatic push token registration by `StreamVideo` now stores registered token in `SharedPreferences`, performing an API call only when the token changes.
Expand Down
Loading
Loading