Skip to content

Commit 0e698c2

Browse files
authored
feat(llc): manual quality selection (#791)
* manual quality selection * tweak * tweak * tweak * changelog * logger tag fix
1 parent f94fb89 commit 0e698c2

File tree

15 files changed

+708
-237
lines changed

15 files changed

+708
-237
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
title: Manual Video Quality Selection
3+
slug: /manual-video-quality-selection
4+
sidebar_position: 10
5+
description: Learn how to manually select the incoming video quality in the Stream Video Flutter SDK.
6+
---
7+
8+
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.
9+
10+
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.
11+
12+
## Overriding Preferred Resolution
13+
14+
To override the preferred incoming video resolution, use the `call.setPreferredIncomingVideoResolution` method:
15+
16+
```dart
17+
await call.setPreferredIncomingVideoResolution(VideoResolution(width: 640, height: 480));
18+
```
19+
20+
:::note
21+
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.
22+
:::
23+
24+
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:
25+
26+
```dart
27+
final [first, second, ..._] = call.state.value.otherParticipants;
28+
29+
// Set preferred incoming video resolution for the first two participants only:
30+
await call.setPreferredIncomingVideoResolution(
31+
VideoResolution(width: 640, height: 480),
32+
sessionIds: [first.sessionId, second.sessionId],
33+
);
34+
```
35+
36+
Calling this method will enable incoming video for the selected participants if it was previously disabled.
37+
38+
To clear a previously set preference, pass `null` instead of resolution:
39+
40+
```dart
41+
// Clear resolution preference for selected participants:
42+
await call.setPreferredIncomingVideoResolution(
43+
null,
44+
sessionIds: [
45+
participant.sessionId,
46+
],
47+
);
48+
// Clear resolution preference for all participants:
49+
await call.setPreferredIncomingVideoResolution(null);
50+
```
51+
52+
## Disabling Incoming Video
53+
54+
To completely disable incoming video (either to save data, or for an audio-only experience), use the `call.setIncomingVideoEnabled()` method:
55+
56+
```dart
57+
await call.setIncomingVideoEnabled(false);
58+
```
59+
60+
To enable incoming video again, pass `true` as an argument:
61+
62+
```dart
63+
await call.setIncomingVideoEnabled(true);
64+
```
65+
66+
Calling this method will clear the previously set resolution preferences.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
title: Session Timers
3+
slug: /session-timers
4+
sidebar_position: 11
5+
description: Learn how to limit the maximum duration of a call in the Stream Video Flutter SDK.
6+
---
7+
8+
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.
9+
10+
## Creating a call with a session timer
11+
12+
Let's see how to create a single call with a limited duration:
13+
14+
```dart
15+
final call = client.makeCall(callType: StreamCallType.defaultType(), id: 'REPLACE_WITH_CALL_ID');
16+
await call.getOrCreate(
17+
limits: const StreamLimitsSettings(
18+
maxDurationSeconds: 3600,
19+
),
20+
);
21+
```
22+
23+
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).
24+
25+
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.
26+
27+
```dart
28+
await call.join();
29+
print(call.state.value.timerEndsAt);
30+
```
31+
32+
## Extending a call
33+
34+
​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:
35+
36+
```dart
37+
final duration =
38+
call.state.value.settings.limits.maxDurationSeconds! + 60;
39+
40+
call.update(
41+
limits: StreamLimitsSettings(
42+
maxDurationSeconds: duration,
43+
),
44+
);
45+
```
46+
47+
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.

dogfooding/lib/widgets/settings_menu.dart

Lines changed: 135 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,23 @@ CallReactionData _raisedHandReaction = const CallReactionData(
1111
icon: '✋',
1212
);
1313

14+
enum IncomingVideoQuality {
15+
auto('Auto'),
16+
p2160('2160p'),
17+
p1080('1080p'),
18+
p720('720p'),
19+
p480('480p'),
20+
p144('144p'),
21+
off('Off');
22+
23+
final String name;
24+
25+
const IncomingVideoQuality(this.name);
26+
27+
@override
28+
String toString() => name;
29+
}
30+
1431
class SettingsMenu extends StatefulWidget {
1532
const SettingsMenu({
1633
required this.call,
@@ -40,7 +57,9 @@ class _SettingsMenuState extends State<SettingsMenu> {
4057

4158
bool showAudioOutputs = false;
4259
bool showAudioInputs = false;
43-
bool get showMainSettings => !showAudioOutputs && !showAudioInputs;
60+
bool showIncomingQuality = false;
61+
bool get showMainSettings =>
62+
!showAudioOutputs && !showAudioInputs && !showIncomingQuality;
4463

4564
@override
4665
void initState() {
@@ -83,11 +102,15 @@ class _SettingsMenuState extends State<SettingsMenu> {
83102
if (showMainSettings) ..._buildMenuItems(),
84103
if (showAudioOutputs) ..._buildAudioOutputsMenu(),
85104
if (showAudioInputs) ..._buildAudioInputsMenu(),
105+
if (showIncomingQuality) ..._buildIncomingQualityMenu(),
86106
]),
87107
);
88108
}
89109

90110
List<Widget> _buildMenuItems() {
111+
final incomingVideoQuality = getIncomingVideoQuality(
112+
widget.call.dynascaleManager.incomingVideoSettings);
113+
91114
return [
92115
Row(
93116
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
@@ -155,6 +178,24 @@ class _SettingsMenuState extends State<SettingsMenu> {
155178
showAudioInputs = true;
156179
});
157180
},
181+
),
182+
const SizedBox(height: 16),
183+
StandardActionMenuItem(
184+
icon: Icons.high_quality_sharp,
185+
label: 'Incoming video quality',
186+
trailing: Text(
187+
incomingVideoQuality.name,
188+
style: TextStyle(
189+
color: incomingVideoQuality != IncomingVideoQuality.auto
190+
? AppColorPalette.appGreen
191+
: null,
192+
),
193+
),
194+
onPressed: () {
195+
setState(() {
196+
showIncomingQuality = true;
197+
});
198+
},
158199
)
159200
];
160201
}
@@ -229,6 +270,87 @@ class _SettingsMenuState extends State<SettingsMenu> {
229270
.insertBetween(const SizedBox(height: 16)),
230271
];
231272
}
273+
274+
List<Widget> _buildIncomingQualityMenu() {
275+
return [
276+
GestureDetector(
277+
onTap: () {
278+
setState(() {
279+
showIncomingQuality = false;
280+
});
281+
},
282+
child: const Align(
283+
alignment: Alignment.centerLeft,
284+
child: Icon(Icons.arrow_back, size: 24),
285+
),
286+
),
287+
const SizedBox(height: 16),
288+
...IncomingVideoQuality.values
289+
.map(
290+
(quality) {
291+
return StandardActionMenuItem(
292+
icon: Icons.video_settings,
293+
label: quality.name,
294+
color: getIncomingVideoQuality(widget
295+
.call.dynascaleManager.incomingVideoSettings) ==
296+
quality
297+
? AppColorPalette.appGreen
298+
: null,
299+
onPressed: () {
300+
if (quality == IncomingVideoQuality.off) {
301+
widget.call.setIncomingVideoEnabled(false);
302+
} else {
303+
widget.call.setPreferredIncomingVideoResolution(
304+
getIncomingVideoResolution(quality));
305+
}
306+
},
307+
);
308+
},
309+
)
310+
.cast()
311+
.insertBetween(const SizedBox(height: 16)),
312+
];
313+
}
314+
315+
VideoResolution? getIncomingVideoResolution(IncomingVideoQuality quality) {
316+
switch (quality) {
317+
case IncomingVideoQuality.auto:
318+
case IncomingVideoQuality.off:
319+
return null;
320+
case IncomingVideoQuality.p2160:
321+
return VideoResolution(width: 3840, height: 2160);
322+
case IncomingVideoQuality.p1080:
323+
return VideoResolution(width: 1920, height: 1080);
324+
case IncomingVideoQuality.p720:
325+
return VideoResolution(width: 1280, height: 720);
326+
case IncomingVideoQuality.p480:
327+
return VideoResolution(width: 640, height: 480);
328+
case IncomingVideoQuality.p144:
329+
return VideoResolution(width: 256, height: 144);
330+
}
331+
}
332+
333+
IncomingVideoQuality getIncomingVideoQuality(IncomingVideoSettings? setting) {
334+
final preferredResolution = setting?.preferredResolution;
335+
if (setting?.enabled == false) {
336+
return IncomingVideoQuality.off;
337+
}
338+
if (preferredResolution == null) {
339+
return IncomingVideoQuality.auto;
340+
} else if (preferredResolution.height >= 2160) {
341+
return IncomingVideoQuality.p2160;
342+
} else if (preferredResolution.height >= 1080) {
343+
return IncomingVideoQuality.p1080;
344+
} else if (preferredResolution.height >= 720) {
345+
return IncomingVideoQuality.p720;
346+
} else if (preferredResolution.height >= 480) {
347+
return IncomingVideoQuality.p480;
348+
} else if (preferredResolution.height >= 144) {
349+
return IncomingVideoQuality.p144;
350+
} else {
351+
return IncomingVideoQuality.auto;
352+
}
353+
}
232354
}
233355

234356
class SettingsMenuItem extends StatelessWidget {
@@ -265,10 +387,12 @@ class StandardActionMenuItem extends StatelessWidget {
265387
required this.label,
266388
this.color,
267389
this.onPressed,
390+
this.trailing,
268391
});
269392

270393
final IconData icon;
271394
final String label;
395+
final Widget? trailing;
272396
final Color? color;
273397
final void Function()? onPressed;
274398

@@ -285,8 +409,16 @@ class StandardActionMenuItem extends StatelessWidget {
285409
color: color,
286410
),
287411
const SizedBox(width: 8),
288-
Text(label,
289-
style: TextStyle(color: color, fontWeight: FontWeight.bold)),
412+
Text(
413+
label,
414+
style: TextStyle(
415+
color: color,
416+
fontWeight: FontWeight.bold,
417+
),
418+
),
419+
const Spacer(),
420+
if (trailing != null) trailing!,
421+
const SizedBox(width: 8),
290422
],
291423
),
292424
);

packages/stream_video/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ This release introduces a major rework of the join/reconnect flow in the Call cl
1717
* Added `participantCount` and `anonymousParticipantCount` to `CallState` reflecting the current number of participants in the call.
1818
* 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()`).
1919
* Added support for `targetResolution` setting set on the Dashboard to determine the max resolution the video stream.
20+
* 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/).
2021

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

0 commit comments

Comments
 (0)