Skip to content

Commit b25faa4

Browse files
committed
Add adaptive bitrate streaming support to video_player
Adds setBandwidthLimit() to VideoPlayerController and an internal AdaptiveBitrateManager that automatically adjusts quality based on buffering events during HLS/DASH playback. The AdaptiveBitrateManager: - Monitors buffering events on a 3-second interval - Steps down quality (1080p -> 720p -> 480p -> 360p) as buffering increases - Recovers quality when buffering subsides (0.7x decay per cycle) - Enforces a 5-second cooldown between quality changes - Only activates for network data sources Requires video_player_platform_interface ^6.7.0, video_player_android ^2.10.0, video_player_avfoundation ^2.10.0. Fixes flutter/flutter#183941
1 parent 8dcfd11 commit b25faa4

5 files changed

Lines changed: 230 additions & 4 deletions

File tree

packages/video_player/video_player/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 2.12.0
2+
3+
* Adds `setBandwidthLimit` method for adaptive bitrate streaming control.
4+
* Adds internal `AdaptiveBitrateManager` for automatic quality adjustment based on buffering events.
5+
16
## 2.11.1
27

38
* Optimizes caption retrieval with binary search.
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import 'dart:async';
2+
3+
import 'package:video_player_platform_interface/video_player_platform_interface.dart';
4+
5+
/// Manages adaptive bitrate selection for HLS/DASH streaming.
6+
///
7+
/// Monitors buffering events and adjusts the maximum bandwidth limit
8+
/// to help the native player select appropriate quality variants.
9+
///
10+
/// This acts as a supervisory controller on top of the native player's
11+
/// own ABR logic (ExoPlayer's [AdaptiveTrackSelection] on Android,
12+
/// AVFoundation on iOS). It forces quality down via [setBandwidthLimit]
13+
/// when persistent buffering is detected, and relaxes the limit when
14+
/// playback stabilizes.
15+
class AdaptiveBitrateManager {
16+
/// Creates an [AdaptiveBitrateManager] for the given [playerId].
17+
AdaptiveBitrateManager({
18+
required this.playerId,
19+
required VideoPlayerPlatform platform,
20+
}) : _platform = platform;
21+
22+
/// The player ID this manager controls.
23+
final int playerId;
24+
final VideoPlayerPlatform _platform;
25+
26+
Timer? _monitoringTimer;
27+
int _bufferingCount = 0;
28+
int _currentBandwidthLimit = qualityUnlimited;
29+
DateTime _lastQualityChange = DateTime.now();
30+
bool _isMonitoring = false;
31+
32+
/// Bandwidth cap for 360p quality (~500 kbps).
33+
static const int quality360p = 500000;
34+
35+
/// Bandwidth cap for 480p quality (~800 kbps).
36+
static const int quality480p = 800000;
37+
38+
/// Bandwidth cap for 720p quality (~1.2 Mbps).
39+
static const int quality720p = 1200000;
40+
41+
/// Bandwidth cap for 1080p quality (~2.5 Mbps).
42+
static const int quality1080p = 2500000;
43+
44+
/// No bandwidth limit — lets the native player choose freely.
45+
static const int qualityUnlimited = 0;
46+
47+
static const Duration _monitorInterval = Duration(seconds: 3);
48+
static const Duration _qualityChangeCooldown = Duration(seconds: 5);
49+
50+
/// Buffering count decay factor applied each monitoring cycle.
51+
///
52+
/// This allows recovery to higher quality after transient buffering.
53+
static const double _bufferingDecayFactor = 0.7;
54+
55+
/// Starts automatic quality monitoring and adjustment.
56+
///
57+
/// Removes any existing bandwidth limit and begins periodic analysis.
58+
/// Safe to call multiple times — subsequent calls are no-ops.
59+
Future<void> startAutoAdaptiveQuality() async {
60+
if (_isMonitoring) {
61+
return;
62+
}
63+
_isMonitoring = true;
64+
65+
try {
66+
await _platform.setBandwidthLimit(playerId, qualityUnlimited);
67+
} catch (e) {
68+
_isMonitoring = false;
69+
return;
70+
}
71+
72+
_monitoringTimer = Timer.periodic(_monitorInterval, (_) {
73+
_analyzeAndAdjust();
74+
});
75+
}
76+
77+
/// Records a buffering event from the player.
78+
///
79+
/// Called by [VideoPlayerController] when a [bufferingStart] event occurs.
80+
void recordBufferingEvent() {
81+
if (_isMonitoring) {
82+
_bufferingCount++;
83+
}
84+
}
85+
86+
/// Analyzes recent buffering history and adjusts the bandwidth limit.
87+
Future<void> _analyzeAndAdjust() async {
88+
if (DateTime.now().difference(_lastQualityChange) <
89+
_qualityChangeCooldown) {
90+
return;
91+
}
92+
93+
final int newLimit = _selectOptimalBandwidth();
94+
95+
// Apply decay so transient buffering doesn't permanently pin quality low.
96+
_bufferingCount = (_bufferingCount * _bufferingDecayFactor).floor();
97+
98+
if (newLimit != _currentBandwidthLimit) {
99+
try {
100+
await _platform.setBandwidthLimit(playerId, newLimit);
101+
_currentBandwidthLimit = newLimit;
102+
_lastQualityChange = DateTime.now();
103+
} catch (e) {
104+
// Silently ignore errors during auto-adjustment.
105+
}
106+
}
107+
}
108+
109+
/// Selects optimal bandwidth based on recent buffering frequency.
110+
int _selectOptimalBandwidth() {
111+
if (_bufferingCount > 5) {
112+
return quality360p;
113+
}
114+
if (_bufferingCount > 2) {
115+
return quality480p;
116+
}
117+
if (_bufferingCount > 0) {
118+
return quality720p;
119+
}
120+
return qualityUnlimited;
121+
}
122+
123+
/// Stops monitoring and releases resources.
124+
void dispose() {
125+
_isMonitoring = false;
126+
_monitoringTimer?.cancel();
127+
_monitoringTimer = null;
128+
}
129+
}

packages/video_player/video_player/lib/video_player.dart

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:flutter/services.dart';
1313
import 'package:video_player_platform_interface/video_player_platform_interface.dart'
1414
as platform_interface;
1515

16+
import 'src/adaptive_bitrate_manager.dart';
1617
import 'src/closed_caption_file.dart';
1718

1819
export 'package:video_player_platform_interface/video_player_platform_interface.dart'
@@ -524,6 +525,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
524525
Completer<void>? _creatingCompleter;
525526
StreamSubscription<dynamic>? _eventSubscription;
526527
_VideoAppLifeCycleObserver? _lifeCycleObserver;
528+
AdaptiveBitrateManager? _adaptiveBitrateManager;
527529

528530
/// The id of a player that hasn't been initialized.
529531
@visibleForTesting
@@ -588,6 +590,17 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
588590
(await _videoPlayerPlatform.createWithOptions(creationOptions)) ??
589591
kUninitializedPlayerId;
590592
_creatingCompleter!.complete(null);
593+
594+
// Enable adaptive bitrate management only for network streams
595+
// (local files and assets don't use HLS/DASH adaptive streaming).
596+
if (dataSourceType == platform_interface.DataSourceType.network) {
597+
_adaptiveBitrateManager = AdaptiveBitrateManager(
598+
playerId: _playerId,
599+
platform: _videoPlayerPlatform,
600+
);
601+
await _adaptiveBitrateManager!.startAutoAdaptiveQuality();
602+
}
603+
591604
final initializingCompleter = Completer<void>();
592605

593606
// Apply the web-specific options
@@ -638,6 +651,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
638651
value = value.copyWith(buffered: event.buffered);
639652
case platform_interface.VideoEventType.bufferingStart:
640653
value = value.copyWith(isBuffering: true);
654+
_adaptiveBitrateManager?.recordBufferingEvent();
641655
case platform_interface.VideoEventType.bufferingEnd:
642656
value = value.copyWith(isBuffering: false);
643657
case platform_interface.VideoEventType.isPlayingStateUpdate:
@@ -684,6 +698,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
684698
if (!_isDisposed) {
685699
_isDisposed = true;
686700
_timer?.cancel();
701+
_adaptiveBitrateManager?.dispose();
687702
await _eventSubscription?.cancel();
688703
await _videoPlayerPlatform.dispose(_playerId);
689704
}
@@ -850,6 +865,44 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
850865
await _applyPlaybackSpeed();
851866
}
852867

868+
/// Sets the bandwidth limit for HLS adaptive bitrate streaming.
869+
///
870+
/// This method limits the maximum bandwidth used for video playback,
871+
/// which affects which HLS variant streams the player can select.
872+
///
873+
/// The native player will only select video variants with bitrate
874+
/// less than or equal to the specified [maxBandwidthBps].
875+
///
876+
/// Platforms:
877+
/// - **Android**: Uses ExoPlayer's DefaultTrackSelector.setMaxVideoBitrate()
878+
/// - **iOS/macOS**: Uses AVPlayer's preferredPeakBitRate property
879+
/// - **Web**: Not supported (no-op)
880+
///
881+
/// Parameters:
882+
/// - [maxBandwidthBps]: Maximum bandwidth in bits per second.
883+
/// * 0 or negative: No limit (player auto-selects)
884+
/// * Positive value: Player selects variants ≤ this bandwidth
885+
///
886+
/// Example:
887+
/// ```dart
888+
/// // Limit to 720p quality (~1.2 Mbps)
889+
/// await controller.setBandwidthLimit(1200000);
890+
///
891+
/// // No limit - let player decide
892+
/// await controller.setBandwidthLimit(0);
893+
/// ```
894+
///
895+
/// Note: This is useful for HLS streams where you want to control
896+
/// quality selection without reinitializing the player. The player
897+
/// will seamlessly switch to appropriate variants as bandwidth
898+
/// changes within the limit.
899+
Future<void> setBandwidthLimit(int maxBandwidthBps) async {
900+
if (_isDisposedOrNotInitialized) {
901+
return;
902+
}
903+
await _videoPlayerPlatform.setBandwidthLimit(_playerId, maxBandwidthBps);
904+
}
905+
853906
/// Sets the caption offset.
854907
///
855908
/// The [offset] will be used when getting the correct caption for a specific position.

packages/video_player/video_player/pubspec.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter
33
widgets on Android, iOS, macOS and web.
44
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player
55
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
6-
version: 2.11.1
6+
version: 2.12.0
77

88
environment:
99
sdk: ^3.10.0
@@ -26,9 +26,9 @@ dependencies:
2626
flutter:
2727
sdk: flutter
2828
html: ^0.15.0
29-
video_player_android: ^2.9.1
30-
video_player_avfoundation: ^2.9.0
31-
video_player_platform_interface: ^6.6.0
29+
video_player_android: ^2.10.0
30+
video_player_avfoundation: ^2.10.0
31+
video_player_platform_interface: ^6.7.0
3232
video_player_web: ^2.1.0
3333

3434
dev_dependencies:

packages/video_player/video_player/test/video_player_test.dart

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ class FakeController extends ValueNotifier<VideoPlayerValue>
130130
return true;
131131
}
132132

133+
@override
134+
Future<void> setBandwidthLimit(int maxBandwidthBps) async {}
135+
133136
String? selectedAudioTrackId;
134137
}
135138

@@ -1106,6 +1109,34 @@ void main() {
11061109
});
11071110
});
11081111

1112+
group('setBandwidthLimit', () {
1113+
test('delegates to platform', () async {
1114+
final controller = VideoPlayerController.networkUrl(_localhostUri);
1115+
addTearDown(controller.dispose);
1116+
await controller.initialize();
1117+
1118+
await controller.setBandwidthLimit(5000000);
1119+
1120+
expect(fakeVideoPlayerPlatform.calls, contains('setBandwidthLimit'));
1121+
expect(
1122+
fakeVideoPlayerPlatform.bandwidthLimits[controller.playerId],
1123+
5000000,
1124+
);
1125+
});
1126+
1127+
test('does nothing when not initialized', () async {
1128+
final controller = VideoPlayerController.networkUrl(_localhostUri);
1129+
addTearDown(controller.dispose);
1130+
1131+
await controller.setBandwidthLimit(5000000);
1132+
1133+
expect(
1134+
fakeVideoPlayerPlatform.calls,
1135+
isNot(contains('setBandwidthLimit')),
1136+
);
1137+
});
1138+
});
1139+
11091140
group('caption', () {
11101141
test('works when position updates', () async {
11111142
final controller = VideoPlayerController.networkUrl(
@@ -2289,4 +2320,12 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform {
22892320
}
22902321

22912322
final Map<int, String> selectedAudioTrackIds = <int, String>{};
2323+
2324+
@override
2325+
Future<void> setBandwidthLimit(int playerId, int maxBandwidthBps) async {
2326+
calls.add('setBandwidthLimit');
2327+
bandwidthLimits[playerId] = maxBandwidthBps;
2328+
}
2329+
2330+
final Map<int, int> bandwidthLimits = <int, int>{};
22922331
}

0 commit comments

Comments
 (0)