Skip to content
Draft
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
5 changes: 5 additions & 0 deletions packages/video_player/video_player/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 2.12.0

* Adds `setBandwidthLimit` method for adaptive bitrate streaming control.
* Adds internal `AdaptiveBitrateManager` for automatic quality adjustment based on buffering events.

## 2.11.1

* Optimizes caption retrieval with binary search.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import 'dart:async';

import 'package:flutter/foundation.dart' show visibleForTesting;
import 'package:video_player_platform_interface/video_player_platform_interface.dart';

/// Manages adaptive bitrate selection for HLS/DASH streaming.
///
/// Monitors buffering events and adjusts the maximum bandwidth limit
/// to help the native player select appropriate quality variants.
///
/// This acts as a supervisory controller on top of the native player's
/// own ABR logic (ExoPlayer's [AdaptiveTrackSelection] on Android,
/// AVFoundation on iOS). It forces quality down via [setBandwidthLimit]
/// when persistent buffering is detected, and relaxes the limit when
/// playback stabilizes.
class AdaptiveBitrateManager {
/// Creates an [AdaptiveBitrateManager] for the given [playerId].
///
/// The optional [clock] parameter is exposed for testing and defaults
/// to [DateTime.now].
AdaptiveBitrateManager({
required this.playerId,
required VideoPlayerPlatform platform,
@visibleForTesting DateTime Function()? clock,
}) : _platform = platform,
_clock = clock ?? DateTime.now;

/// The player ID this manager controls.
final int playerId;
final VideoPlayerPlatform _platform;
final DateTime Function() _clock;

Timer? _monitoringTimer;
int _bufferingCount = 0;
int _currentBandwidthLimit = qualityUnlimited;
late DateTime _lastQualityChange = _clock();
bool _isMonitoring = false;

/// Bandwidth cap for 360p quality (~500 kbps).
static const int quality360p = 500000;

/// Bandwidth cap for 480p quality (~800 kbps).
static const int quality480p = 800000;

/// Bandwidth cap for 720p quality (~1.2 Mbps).
static const int quality720p = 1200000;

/// Bandwidth cap for 1080p quality (~2.5 Mbps).
static const int quality1080p = 2500000;

/// No bandwidth limit — lets the native player choose freely.
static const int qualityUnlimited = 0;

static const Duration _monitorInterval = Duration(seconds: 3);
static const Duration _qualityChangeCooldown = Duration(seconds: 5);

/// Buffering count decay factor applied each monitoring cycle.
///
/// This allows recovery to higher quality after transient buffering.
static const double _bufferingDecayFactor = 0.7;

/// Starts automatic quality monitoring and adjustment.
///
/// Removes any existing bandwidth limit and begins periodic analysis.
/// Safe to call multiple times — subsequent calls are no-ops.
Future<void> startAutoAdaptiveQuality() async {
if (_isMonitoring) {
return;
}
_isMonitoring = true;

try {
await _platform.setBandwidthLimit(playerId, qualityUnlimited);
} catch (e) {
_isMonitoring = false;
return;
}

_monitoringTimer = Timer.periodic(_monitorInterval, (_) {
_analyzeAndAdjust();
});
}

/// Records a buffering event from the player.
///
/// Called by [VideoPlayerController] when a [bufferingStart] event occurs.
void recordBufferingEvent() {
if (_isMonitoring) {
_bufferingCount++;
}
}

/// Analyzes recent buffering history and adjusts the bandwidth limit.
Future<void> _analyzeAndAdjust() async {
if (_clock().difference(_lastQualityChange) < _qualityChangeCooldown) {
return;
}

final int newLimit = _selectOptimalBandwidth();

// Apply decay so transient buffering doesn't permanently pin quality low.
_bufferingCount = (_bufferingCount * _bufferingDecayFactor).floor();

if (newLimit != _currentBandwidthLimit) {
try {
await _platform.setBandwidthLimit(playerId, newLimit);
_currentBandwidthLimit = newLimit;
_lastQualityChange = _clock();
} catch (e) {
// Silently ignore errors during auto-adjustment.
}
}
}

/// Selects optimal bandwidth based on recent buffering frequency.
int _selectOptimalBandwidth() {
if (_bufferingCount > 5) {
return quality360p;
}
if (_bufferingCount > 3) {
return quality480p;
}
if (_bufferingCount > 1) {
return quality720p;
}
if (_bufferingCount > 0) {
return quality1080p;
}
return qualityUnlimited;
}
Comment on lines +116 to +130

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The _selectOptimalBandwidth method doesn't use the quality1080p constant. This causes the adaptive bitrate logic to step down from unlimited quality directly to 720p after a single buffering event, which is more aggressive than the gradual step-down described in the PR description ("unlimited → 1080p → 720p ...") and may lead to a suboptimal user experience.

I suggest adjusting the thresholds to include a step for 1080p to provide a smoother quality transition.

  int _selectOptimalBandwidth() {
    if (_bufferingCount > 5) {
      return quality360p;
    }
    if (_bufferingCount > 3) {
      return quality480p;
    }
    if (_bufferingCount > 1) {
      return quality720p;
    }
    if (_bufferingCount > 0) {
      return quality1080p;
    }
    return qualityUnlimited;
  }


/// Stops monitoring and releases resources.
void dispose() {
_isMonitoring = false;
_monitoringTimer?.cancel();
_monitoringTimer = null;
}
}
Comment on lines +16 to +138

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The new AdaptiveBitrateManager class contains significant logic for automatic quality adjustment, including state management, timers, and quality selection based on buffering events. However, there are no unit tests for this class. Adding unit tests would help ensure the correctness of this complex logic and prevent future regressions.

You could use the fake_async package to test the timer-based behavior and mock VideoPlayerPlatform to verify interactions.

53 changes: 53 additions & 0 deletions packages/video_player/video_player/lib/video_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:flutter/services.dart';
import 'package:video_player_platform_interface/video_player_platform_interface.dart'
as platform_interface;

import 'src/adaptive_bitrate_manager.dart';
import 'src/closed_caption_file.dart';

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

/// The id of a player that hasn't been initialized.
@visibleForTesting
Expand Down Expand Up @@ -588,6 +590,17 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
(await _videoPlayerPlatform.createWithOptions(creationOptions)) ??
kUninitializedPlayerId;
_creatingCompleter!.complete(null);

// Enable adaptive bitrate management only for network streams
// (local files and assets don't use HLS/DASH adaptive streaming).
if (dataSourceType == platform_interface.DataSourceType.network) {
_adaptiveBitrateManager = AdaptiveBitrateManager(
playerId: _playerId,
platform: _videoPlayerPlatform,
);
await _adaptiveBitrateManager!.startAutoAdaptiveQuality();
}

final initializingCompleter = Completer<void>();

// Apply the web-specific options
Expand Down Expand Up @@ -638,6 +651,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
value = value.copyWith(buffered: event.buffered);
case platform_interface.VideoEventType.bufferingStart:
value = value.copyWith(isBuffering: true);
_adaptiveBitrateManager?.recordBufferingEvent();
case platform_interface.VideoEventType.bufferingEnd:
value = value.copyWith(isBuffering: false);
case platform_interface.VideoEventType.isPlayingStateUpdate:
Expand Down Expand Up @@ -684,6 +698,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
if (!_isDisposed) {
_isDisposed = true;
_timer?.cancel();
_adaptiveBitrateManager?.dispose();
await _eventSubscription?.cancel();
await _videoPlayerPlatform.dispose(_playerId);
}
Expand Down Expand Up @@ -850,6 +865,44 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
await _applyPlaybackSpeed();
}

/// Sets the bandwidth limit for HLS adaptive bitrate streaming.
///
/// This method limits the maximum bandwidth used for video playback,
/// which affects which HLS variant streams the player can select.
///
/// The native player will only select video variants with bitrate
/// less than or equal to the specified [maxBandwidthBps].
///
/// Platforms:
/// - **Android**: Uses ExoPlayer's DefaultTrackSelector.setMaxVideoBitrate()
/// - **iOS/macOS**: Uses AVPlayer's preferredPeakBitRate property
/// - **Web**: Not supported (no-op)
///
/// Parameters:
/// - [maxBandwidthBps]: Maximum bandwidth in bits per second.
/// * 0 or negative: No limit (player auto-selects)
/// * Positive value: Player selects variants ≤ this bandwidth
///
/// Example:
/// ```dart
/// // Limit to 720p quality (~1.2 Mbps)
/// await controller.setBandwidthLimit(1200000);
///
/// // No limit - let player decide
/// await controller.setBandwidthLimit(0);
/// ```
///
/// Note: This is useful for HLS streams where you want to control
/// quality selection without reinitializing the player. The player
/// will seamlessly switch to appropriate variants as bandwidth
/// changes within the limit.
Future<void> setBandwidthLimit(int maxBandwidthBps) async {
if (_isDisposedOrNotInitialized) {
return;
}
await _videoPlayerPlatform.setBandwidthLimit(_playerId, maxBandwidthBps);
}

/// Sets the caption offset.
///
/// The [offset] will be used when getting the correct caption for a specific position.
Expand Down
9 changes: 5 additions & 4 deletions packages/video_player/video_player/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter
widgets on Android, iOS, macOS and web.
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
version: 2.11.1
version: 2.12.0

environment:
sdk: ^3.10.0
Expand All @@ -26,12 +26,13 @@ dependencies:
flutter:
sdk: flutter
html: ^0.15.0
video_player_android: ^2.9.1
video_player_avfoundation: ^2.9.0
video_player_platform_interface: ^6.6.0
video_player_android: ^2.10.0
video_player_avfoundation: ^2.10.0
video_player_platform_interface: ^6.7.0
video_player_web: ^2.1.0

dev_dependencies:
fake_async: ^1.3.0
flutter_test:
sdk: flutter
leak_tracker_flutter_testing: any
Expand Down
Loading