Skip to content

Commit 2db2f5f

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 2db2f5f

File tree

6 files changed

+648
-4
lines changed

6 files changed

+648
-4
lines changed

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

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: 5 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,12 +26,13 @@ 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:
35+
fake_async: ^1.3.0
3536
flutter_test:
3637
sdk: flutter
3738
leak_tracker_flutter_testing: any

0 commit comments

Comments
 (0)