From 954df4987093e1d4ceae63d6ed538ee3b7b2d4af Mon Sep 17 00:00:00 2001 From: Mohamed Fat-hy Date: Sun, 21 Jun 2026 05:38:48 +0300 Subject: [PATCH 1/4] [video_player] Add forwardBufferDuration to cap network read-ahead Adds an optional VideoPlayerOptions.forwardBufferDuration so apps can limit how far ahead of the playhead the player buffers from the network. Upstream defaults to aggressive read-ahead (ExoPlayer ~50s; AVPlayer may fetch most of the file), which wastes bandwidth when a user abandons or seeks away. Requested in flutter/flutter#40931, #141511, #163918. null preserves current behavior. Threaded through VideoCreationOptions and the pigeon CreationOptions to: - Android: ExoPlayer DefaultLoadControl (VideoPlayer.cappedLoadControl, clamped playback-start thresholds). - iOS/macOS: AVPlayerItem.preferredForwardBufferDuration. --- .../video_player/video_player/CHANGELOG.md | 6 ++++- .../video_player/lib/video_player.dart | 1 + .../video_player/video_player/pubspec.yaml | 8 +++--- .../video_player_android/CHANGELOG.md | 5 ++++ .../plugins/videoplayer/VideoPlayer.java | 25 +++++++++++++++++++ .../videoplayer/VideoPlayerPlugin.java | 6 +++-- .../platformview/PlatformViewVideoPlayer.java | 8 +++++- .../texture/TextureVideoPlayer.java | 8 +++++- .../flutter/plugins/videoplayer/Messages.kt | 7 ++++-- .../lib/src/android_video_player.dart | 1 + .../lib/src/messages.g.dart | 7 ++++-- .../pigeons/messages.dart | 3 +++ .../video_player_android/pubspec.yaml | 4 +-- .../video_player_avfoundation/CHANGELOG.md | 5 ++++ .../VideoPlayerPlugin.swift | 9 ++++++- .../VideoPlayerPluginMessages.g.swift | 8 +++++- .../lib/src/avfoundation_video_player.dart | 6 ++++- .../src/video_player_plugin_messages.g.dart | 7 ++++-- .../pigeons/video_player_plugin_messages.dart | 3 +++ .../video_player_avfoundation/pubspec.yaml | 4 +-- .../CHANGELOG.md | 5 +++- .../lib/video_player_platform_interface.dart | 21 +++++++++++++++- .../pubspec.yaml | 2 +- 23 files changed, 134 insertions(+), 25 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 4e4c13b0a0f2..c2a623f5070f 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,5 +1,9 @@ -## NEXT +## 2.12.0 +* Adds `VideoPlayerOptions.forwardBufferDuration` to cap how far ahead of the + playhead the player buffers from the network, reducing wasted bandwidth when a + video is abandoned or seeked away. Defaults to the platform's automatic + buffering when unset. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 2.11.1 diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index b287a39fed24..cf3d34704301 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -560,6 +560,7 @@ class VideoPlayerController extends ValueNotifier { final creationOptions = platform_interface.VideoCreationOptions( dataSource: dataSourceDescription, viewType: viewType, + forwardBufferDuration: videoPlayerOptions?.forwardBufferDuration, ); if (videoPlayerOptions?.mixWithOthers != null) { diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index c55f4e823c4e..151f49419af2 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -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 @@ -26,9 +26,9 @@ 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.8.0 video_player_web: ^2.1.0 dev_dependencies: diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index 99cda569f5c2..7c45d02525b6 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.10.0 + +* Adds support for `VideoPlayerOptions.forwardBufferDuration`, capping ExoPlayer's + forward buffering via `DefaultLoadControl` (default behavior is unchanged when null). + ## 2.9.6 * Migrates to Built-in Kotlin to support AGP 9. diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index ef661f7e1e97..842e4944bc4a 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -18,7 +18,9 @@ import androidx.media3.common.TrackSelectionOverride; import androidx.media3.common.Tracks; import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.DefaultLoadControl; import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.exoplayer.LoadControl; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import io.flutter.view.TextureRegistry.SurfaceProducer; import java.util.ArrayList; @@ -85,6 +87,29 @@ public void setDisposeHandler(@Nullable DisposeHandler handler) { disposeHandler = handler; } + /** + * Builds a {@link LoadControl} that caps forward buffering at {@code forwardBufferDurationMs}, or + * returns null to keep ExoPlayer's default (~50s). + * + *

Backs {@code VideoPlayerOptions.forwardBufferDuration} so abandoned/seek-away playback + * doesn't keep downloading. Playback-start thresholds are clamped so they never exceed the cap (a + * requirement of {@link DefaultLoadControl}). + */ + @UnstableApi + @Nullable + public static LoadControl cappedLoadControl(@Nullable Long forwardBufferDurationMs) { + if (forwardBufferDurationMs == null) { + return null; + } + int bufferMs = (int) Math.max(1L, forwardBufferDurationMs); + int bufferForPlaybackMs = Math.min(2_500, bufferMs); + int bufferForPlaybackAfterRebufferMs = Math.min(5_000, bufferMs); + return new DefaultLoadControl.Builder() + .setBufferDurationsMs( + bufferMs, bufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs) + .build(); + } + @NonNull protected abstract ExoPlayerEventListener createExoPlayerEventListener( @NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer); diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index 49adaf4b7b33..21f5ab9a31f6 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -92,7 +92,8 @@ public long createForPlatformView(@NonNull CreationOptions options) { flutterState.applicationContext, VideoPlayerEventCallbacks.bindTo(flutterState.binaryMessenger, streamInstance), videoAsset, - sharedOptions); + sharedOptions, + options.getForwardBufferDurationMs()); registerPlayerInstance(videoPlayer, id); return id; @@ -112,7 +113,8 @@ public long createForPlatformView(@NonNull CreationOptions options) { VideoPlayerEventCallbacks.bindTo(flutterState.binaryMessenger, streamInstance), handle, videoAsset, - sharedOptions); + sharedOptions, + options.getForwardBufferDurationMs()); registerPlayerInstance(videoPlayer, id); return new TexturePlayerIds(id, handle.id()); diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java index a7c079773b58..ef00ebe6af42 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java @@ -50,7 +50,8 @@ public static PlatformViewVideoPlayer create( @NonNull Context context, @NonNull VideoPlayerCallbacks events, @NonNull VideoAsset asset, - @NonNull VideoPlayerOptions options) { + @NonNull VideoPlayerOptions options, + @Nullable Long forwardBufferDurationMs) { return new PlatformViewVideoPlayer( events, asset.getMediaItem(), @@ -62,6 +63,11 @@ public static PlatformViewVideoPlayer create( new ExoPlayer.Builder(context) .setTrackSelector(trackSelector) .setMediaSourceFactory(asset.getMediaSourceFactory(context)); + androidx.media3.exoplayer.LoadControl loadControl = + VideoPlayer.cappedLoadControl(forwardBufferDurationMs); + if (loadControl != null) { + builder.setLoadControl(loadControl); + } return builder.build(); }); } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java index d623ddc88608..38fac00b0f16 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java @@ -49,7 +49,8 @@ public static TextureVideoPlayer create( @NonNull VideoPlayerCallbacks events, @NonNull SurfaceProducer surfaceProducer, @NonNull VideoAsset asset, - @NonNull VideoPlayerOptions options) { + @NonNull VideoPlayerOptions options, + @Nullable Long forwardBufferDurationMs) { return new TextureVideoPlayer( events, surfaceProducer, @@ -62,6 +63,11 @@ public static TextureVideoPlayer create( new ExoPlayer.Builder(context) .setTrackSelector(trackSelector) .setMediaSourceFactory(asset.getMediaSourceFactory(context)); + androidx.media3.exoplayer.LoadControl loadControl = + VideoPlayer.cappedLoadControl(forwardBufferDurationMs); + if (loadControl != null) { + builder.setLoadControl(loadControl); + } return builder.build(); }); } diff --git a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt index e546c744e561..9c8593bfd7c2 100644 --- a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt +++ b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt @@ -300,7 +300,8 @@ data class CreationOptions( val uri: String, val formatHint: PlatformVideoFormat? = null, val httpHeaders: Map, - val userAgent: String? = null + val userAgent: String? = null, + val forwardBufferDurationMs: Long? = null ) { companion object { fun fromList(pigeonVar_list: List): CreationOptions { @@ -308,7 +309,8 @@ data class CreationOptions( val formatHint = pigeonVar_list[1] as PlatformVideoFormat? val httpHeaders = pigeonVar_list[2] as Map val userAgent = pigeonVar_list[3] as String? - return CreationOptions(uri, formatHint, httpHeaders, userAgent) + val forwardBufferDurationMs = pigeonVar_list[4] as Long? + return CreationOptions(uri, formatHint, httpHeaders, userAgent, forwardBufferDurationMs) } } @@ -318,6 +320,7 @@ data class CreationOptions( formatHint, httpHeaders, userAgent, + forwardBufferDurationMs, ) } diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart index 5ecf673a7aec..728394a24419 100644 --- a/packages/video_player/video_player_android/lib/src/android_video_player.dart +++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart @@ -103,6 +103,7 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { httpHeaders: httpHeaders, userAgent: userAgent, formatHint: formatHint, + forwardBufferDurationMs: options.forwardBufferDuration?.inMilliseconds, ); final int playerId; diff --git a/packages/video_player/video_player_android/lib/src/messages.g.dart b/packages/video_player/video_player_android/lib/src/messages.g.dart index 2782e80e8c14..22dcfdbe2b91 100644 --- a/packages/video_player/video_player_android/lib/src/messages.g.dart +++ b/packages/video_player/video_player_android/lib/src/messages.g.dart @@ -252,7 +252,7 @@ class PlatformVideoViewCreationParams { } class CreationOptions { - CreationOptions({required this.uri, this.formatHint, required this.httpHeaders, this.userAgent}); + CreationOptions({required this.uri, this.formatHint, required this.httpHeaders, this.userAgent, this.forwardBufferDurationMs}); String uri; @@ -262,8 +262,10 @@ class CreationOptions { String? userAgent; + int? forwardBufferDurationMs; + List _toList() { - return [uri, formatHint, httpHeaders, userAgent]; + return [uri, formatHint, httpHeaders, userAgent, forwardBufferDurationMs]; } Object encode() { @@ -277,6 +279,7 @@ class CreationOptions { formatHint: result[1] as PlatformVideoFormat?, httpHeaders: (result[2] as Map?)!.cast(), userAgent: result[3] as String?, + forwardBufferDurationMs: result[4] as int?, ); } diff --git a/packages/video_player/video_player_android/pigeons/messages.dart b/packages/video_player/video_player_android/pigeons/messages.dart index 5b67adb40fad..3f86bad17d96 100644 --- a/packages/video_player/video_player_android/pigeons/messages.dart +++ b/packages/video_player/video_player_android/pigeons/messages.dart @@ -72,6 +72,9 @@ class CreationOptions { PlatformVideoFormat? formatHint; Map httpHeaders; String? userAgent; + + /// Caps forward buffering (in ms); null uses ExoPlayer's default (~50s). + int? forwardBufferDurationMs; } class TexturePlayerIds { diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index d2cdfc394bc2..fa3b9bd3f2d7 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_android description: Android implementation of the video_player plugin. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.9.6 +version: 2.10.0 environment: sdk: ^3.12.0 @@ -20,7 +20,7 @@ flutter: dependencies: flutter: sdk: flutter - video_player_platform_interface: ^6.6.0 + video_player_platform_interface: ^6.8.0 dev_dependencies: build_runner: ^2.3.3 diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index 8ef74b3c4417..c2c9bb53b21c 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.10.0 + +* Adds support for `VideoPlayerOptions.forwardBufferDuration`, setting + `AVPlayerItem.preferredForwardBufferDuration` (default behavior is unchanged when null). + ## 2.9.7 * Forces tone-mapping to SDR on iOS to prevent washed-out HDR video playback. diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPlugin.swift b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPlugin.swift index 951f3bcfed67..3c357c2d94eb 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPlugin.swift +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPlugin.swift @@ -277,7 +277,14 @@ public final class VideoPlayerPlugin: NSObject, FlutterPlugin, AVFoundationVideo throw PigeonError(code: "video_player", message: "Invalid URI", details: nil) } let asset = avFactory.urlAsset(with: url, options: itemOptions) - return avFactory.playerItem(with: asset) + let item = avFactory.playerItem(with: asset) + // Optional forward-buffer cap (see VideoPlayerOptions.forwardBufferDuration). + // nil leaves AVPlayer's automatic (aggressive) buffering; a value (seconds) + // limits read-ahead to save bandwidth. + if let forwardBufferDurationMs = options.forwardBufferDurationMs { + item.preferredForwardBufferDuration = Double(forwardBufferDurationMs) / 1000.0 + } + return item } } diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPluginMessages.g.swift b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPluginMessages.g.swift index 3cd23143abc7..b07569eb3d28 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPluginMessages.g.swift +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPluginMessages.g.swift @@ -214,21 +214,25 @@ struct PlatformVideoViewCreationParams: Hashable { struct CreationOptions: Hashable { var uri: String var httpHeaders: [String: String] + var forwardBufferDurationMs: Int64? = nil // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> CreationOptions? { let uri = pigeonVar_list[0] as! String let httpHeaders = pigeonVar_list[1] as! [String: String] + let forwardBufferDurationMs: Int64? = nilOrValue(pigeonVar_list[2]) return CreationOptions( uri: uri, - httpHeaders: httpHeaders + httpHeaders: httpHeaders, + forwardBufferDurationMs: forwardBufferDurationMs ) } func toList() -> [Any?] { return [ uri, httpHeaders, + forwardBufferDurationMs, ] } static func == (lhs: CreationOptions, rhs: CreationOptions) -> Bool { @@ -237,12 +241,14 @@ struct CreationOptions: Hashable { } return deepEqualsVideoPlayerPluginMessages(lhs.uri, rhs.uri) && deepEqualsVideoPlayerPluginMessages(lhs.httpHeaders, rhs.httpHeaders) + && deepEqualsVideoPlayerPluginMessages(lhs.forwardBufferDurationMs, rhs.forwardBufferDurationMs) } func hash(into hasher: inout Hasher) { hasher.combine("CreationOptions") deepHashVideoPlayerPluginMessages(value: uri, hasher: &hasher) deepHashVideoPlayerPluginMessages(value: httpHeaders, hasher: &hasher) + deepHashVideoPlayerPluginMessages(value: forwardBufferDurationMs, hasher: &hasher) } } diff --git a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart index ea94c1f8fc1b..01727a1b6b9f 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart @@ -90,7 +90,11 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { if (uri == null) { throw ArgumentError('Unable to construct a video asset from $options'); } - final pigeonCreationOptions = CreationOptions(uri: uri, httpHeaders: dataSource.httpHeaders); + final pigeonCreationOptions = CreationOptions( + uri: uri, + httpHeaders: dataSource.httpHeaders, + forwardBufferDurationMs: options.forwardBufferDuration?.inMilliseconds, + ); final int playerId; final VideoPlayerViewState state; diff --git a/packages/video_player/video_player_avfoundation/lib/src/video_player_plugin_messages.g.dart b/packages/video_player/video_player_avfoundation/lib/src/video_player_plugin_messages.g.dart index cca6f571848d..191077d45bdc 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/video_player_plugin_messages.g.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/video_player_plugin_messages.g.dart @@ -135,14 +135,16 @@ class PlatformVideoViewCreationParams { } class CreationOptions { - CreationOptions({required this.uri, required this.httpHeaders}); + CreationOptions({required this.uri, required this.httpHeaders, this.forwardBufferDurationMs}); String uri; Map httpHeaders; + int? forwardBufferDurationMs; + List _toList() { - return [uri, httpHeaders]; + return [uri, httpHeaders, forwardBufferDurationMs]; } Object encode() { @@ -154,6 +156,7 @@ class CreationOptions { return CreationOptions( uri: result[0]! as String, httpHeaders: (result[1]! as Map).cast(), + forwardBufferDurationMs: result[2] as int?, ); } diff --git a/packages/video_player/video_player_avfoundation/pigeons/video_player_plugin_messages.dart b/packages/video_player/video_player_avfoundation/pigeons/video_player_plugin_messages.dart index 91f4fc80cbda..59ab861dcc11 100644 --- a/packages/video_player/video_player_avfoundation/pigeons/video_player_plugin_messages.dart +++ b/packages/video_player/video_player_avfoundation/pigeons/video_player_plugin_messages.dart @@ -26,6 +26,9 @@ class CreationOptions { String uri; Map httpHeaders; + + /// Caps forward buffering (in ms); null uses AVPlayer's automatic buffering. + int? forwardBufferDurationMs; } class TexturePlayerIds { diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index b60381dc3c85..da9ef4a0fc28 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_avfoundation description: iOS and macOS implementation of the video_player plugin. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.9.7 +version: 2.10.0 environment: sdk: ^3.10.0 @@ -25,7 +25,7 @@ dependencies: flutter: sdk: flutter meta: ^1.10.0 - video_player_platform_interface: ^6.6.0 + video_player_platform_interface: ^6.8.0 dev_dependencies: build_runner: ^2.3.3 diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index 203f913e937c..24a07b917334 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,5 +1,8 @@ -## NEXT +## 6.8.0 +* Adds `forwardBufferDuration` to `VideoPlayerOptions` and `VideoCreationOptions`, + allowing apps to cap how far ahead of the playhead the player buffers from the + network. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 6.7.0 diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index 16c83ed35018..9d6ac7ae1365 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -462,12 +462,22 @@ class VideoPlayerOptions { this.mixWithOthers = false, this.allowBackgroundPlayback = false, this.webOptions, + this.forwardBufferDuration, }); /// Set this to true to keep playing video in background, when app goes in background. /// The default value is false. final bool allowBackgroundPlayback; + /// Caps how far ahead of the playhead the player buffers from the network. + /// + /// When null (the default) the platform's automatic buffering is used, which + /// can read far ahead (ExoPlayer ~50s; AVPlayer may fetch most of the file). + /// Setting a small value (e.g. 10-15s) avoids wasting bandwidth when a user + /// abandons or seeks away, at the cost of a higher re-buffer risk on jittery + /// networks. Applied at player creation. Has no effect on the web platform. + final Duration? forwardBufferDuration; + /// Set this to true to mix the video players audio with other audio sources. /// The default value is false /// @@ -576,13 +586,22 @@ class VideoViewOptions { @immutable class VideoCreationOptions { /// Constructs an instance of [VideoCreationOptions]. - const VideoCreationOptions({required this.dataSource, required this.viewType}); + const VideoCreationOptions({ + required this.dataSource, + required this.viewType, + this.forwardBufferDuration, + }); /// The data source used to create the player. final DataSource dataSource; /// The type of view to be used for displaying the video player final VideoViewType viewType; + + /// Caps how far ahead of the playhead the player buffers from the network. + /// + /// See [VideoPlayerOptions.forwardBufferDuration]. Null uses platform default. + final Duration? forwardBufferDuration; } /// Represents an audio track in a video with its metadata. diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index 1742dec5a53c..c8d03f31704d 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/video_player/ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 6.7.0 +version: 6.8.0 environment: sdk: ^3.10.0 From d4421a4ce24afec7a4b37846677ceb14ca554187 Mon Sep 17 00:00:00 2001 From: Mohamed Fat-hy Date: Sun, 21 Jun 2026 09:03:25 +0300 Subject: [PATCH 2/4] [video_player] Add tests for forwardBufferDuration - Dart: umbrella passes the option into VideoCreationOptions; android & avfoundation pass forwardBufferDurationMs (and default to null). - Android native: VideoPlayer.cappedLoadControl null/non-null + threshold clamping; fixes existing VideoPlayerPluginTest for the new create() arity. - iOS native: forwardBufferDuration applied to the player item / left untouched when null. Also exposes preferredForwardBufferDuration on the FVPAVPlayerItem abstraction (protocol + default impl) so the Swift plugin can set it and tests can verify it. --- .../video_player/test/video_player_test.dart | 29 +++++++++++ .../videoplayer/VideoPlayerPluginTest.java | 6 ++- .../plugins/videoplayer/VideoPlayerTest.java | 26 ++++++++++ .../test/android_video_player_test.dart | 49 +++++++++++++++++++ .../darwin/RunnerTests/TestClasses.swift | 1 + .../darwin/RunnerTests/VideoPlayerTests.swift | 24 +++++++++ .../FVPAVFactory.m | 8 +++ .../FVPAVFactory.h | 3 ++ .../test/avfoundation_video_player_test.dart | 45 +++++++++++++++++ 9 files changed, 189 insertions(+), 2 deletions(-) diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 56cd402f1228..5c944e189afe 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -1754,6 +1754,33 @@ void main() { expect(controller.videoPlayerOptions!.mixWithOthers, true); }); + test('forwardBufferDuration is passed to the platform', () async { + final controller = VideoPlayerController.networkUrl( + _localhostUri, + videoPlayerOptions: VideoPlayerOptions( + forwardBufferDuration: const Duration(seconds: 10), + ), + ); + addTearDown(controller.dispose); + + await controller.initialize(); + expect( + fakeVideoPlayerPlatform.creationOptions.last.forwardBufferDuration, + const Duration(seconds: 10), + ); + }); + + test('forwardBufferDuration defaults to null', () async { + final controller = VideoPlayerController.networkUrl(_localhostUri); + addTearDown(controller.dispose); + + await controller.initialize(); + expect( + fakeVideoPlayerPlatform.creationOptions.last.forwardBufferDuration, + isNull, + ); + }); + test('true allowBackgroundPlayback continues playback', () async { final controller = VideoPlayerController.networkUrl( _localhostUri, @@ -1899,6 +1926,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { List calls = []; List dataSources = []; List viewTypes = []; + List creationOptions = []; final Map> streams = >{}; bool forceInitError = false; int nextPlayerId = 0; @@ -1943,6 +1971,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { } dataSources.add(options.dataSource); viewTypes.add(options.viewType); + creationOptions.add(options); return nextPlayerId++; } diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java index 6093dc86573c..5e54603406a1 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java @@ -73,7 +73,7 @@ public void createsPlatformViewVideoPlayer() throws Exception { try (MockedStatic mockedPlatformViewVideoPlayerStatic = mockStatic(PlatformViewVideoPlayer.class)) { mockedPlatformViewVideoPlayerStatic - .when(() -> PlatformViewVideoPlayer.create(any(), any(), any(), any())) + .when(() -> PlatformViewVideoPlayer.create(any(), any(), any(), any(), any())) .thenReturn(mock(PlatformViewVideoPlayer.class)); final CreationOptions options = @@ -81,6 +81,7 @@ public void createsPlatformViewVideoPlayer() throws Exception { "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", null, new HashMap<>(), + null, null); final long playerId = plugin.createForPlatformView(options); @@ -95,7 +96,7 @@ public void createsTextureVideoPlayer() throws Exception { try (MockedStatic mockedTextureVideoPlayerStatic = mockStatic(TextureVideoPlayer.class)) { mockedTextureVideoPlayerStatic - .when(() -> TextureVideoPlayer.create(any(), any(), any(), any(), any())) + .when(() -> TextureVideoPlayer.create(any(), any(), any(), any(), any(), any())) .thenReturn(mock(TextureVideoPlayer.class)); final CreationOptions options = @@ -103,6 +104,7 @@ public void createsTextureVideoPlayer() throws Exception { "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", null, new HashMap<>(), + null, null); final TexturePlayerIds ids = plugin.createForTextureView(options); diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java index 92c2ff5f1566..cbdf70a46f2f 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java @@ -24,7 +24,10 @@ import androidx.media3.common.TrackGroup; import androidx.media3.common.TrackSelectionOverride; import androidx.media3.common.Tracks; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.DefaultLoadControl; import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.exoplayer.LoadControl; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import com.google.common.collect.ImmutableList; import io.flutter.plugins.videoplayer.platformview.PlatformViewExoPlayerEventListener; @@ -91,6 +94,29 @@ public void setUp() { fakeVideoAsset = new FakeVideoAsset(FAKE_ASSET_URL); } + @Test + @UnstableApi + public void cappedLoadControlReturnsNullForNullDuration() { + assertNull(VideoPlayer.cappedLoadControl(null)); + } + + @Test + @UnstableApi + public void cappedLoadControlReturnsLoadControlForDuration() { + LoadControl loadControl = VideoPlayer.cappedLoadControl(10_000L); + assertNotNull(loadControl); + assertTrue(loadControl instanceof DefaultLoadControl); + } + + @Test + @UnstableApi + public void cappedLoadControlClampsThresholdsForSmallDuration() { + // A 1s cap is smaller than the default playback-start thresholds (2.5s/5s). + // DefaultLoadControl.build() throws if those exceed the buffer duration, so a + // successful build proves the thresholds were clamped to the cap. + assertNotNull(VideoPlayer.cappedLoadControl(1_000L)); + } + private VideoPlayer createVideoPlayer() { return createVideoPlayer(new VideoPlayerOptions()); } diff --git a/packages/video_player/video_player_android/test/android_video_player_test.dart b/packages/video_player/video_player_android/test/android_video_player_test.dart index 84239afc78c5..7ddec558a6da 100644 --- a/packages/video_player/video_player_android/test/android_video_player_test.dart +++ b/packages/video_player/video_player_android/test/android_video_player_test.dart @@ -311,6 +311,55 @@ void main() { expect(player.buildViewWithOptions(VideoViewOptions(playerId: playerId!)), isA()); }); + test('createWithOptions passes forwardBufferDuration in ms', () async { + final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = setUpMockPlayer( + playerId: 1, + textureId: 100, + ); + when( + api.createForTextureView(any), + ).thenAnswer((_) async => TexturePlayerIds(playerId: 2, textureId: 100)); + + await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource( + sourceType: DataSourceType.network, + uri: 'https://example.com', + ), + viewType: VideoViewType.textureView, + forwardBufferDuration: const Duration(seconds: 10), + ), + ); + + final VerificationResult verification = verify(api.createForTextureView(captureAny)); + final creationOptions = verification.captured[0] as CreationOptions; + expect(creationOptions.forwardBufferDurationMs, 10000); + }); + + test('createWithOptions defaults forwardBufferDuration to null', () async { + final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = setUpMockPlayer( + playerId: 1, + textureId: 100, + ); + when( + api.createForTextureView(any), + ).thenAnswer((_) async => TexturePlayerIds(playerId: 2, textureId: 100)); + + await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource( + sourceType: DataSourceType.network, + uri: 'https://example.com', + ), + viewType: VideoViewType.textureView, + ), + ); + + final VerificationResult verification = verify(api.createForTextureView(captureAny)); + final creationOptions = verification.captured[0] as CreationOptions; + expect(creationOptions.forwardBufferDurationMs, isNull); + }); + test('createWithOptions with network passes headers', () async { final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = setUpMockPlayer( playerId: 1, diff --git a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift index 24fcc1ca38f3..80161628302b 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift @@ -74,6 +74,7 @@ final class TestAsset: NSObject, FVPAVAsset { final class StubPlayerItem: NSObject, FVPAVPlayerItem { let asset: FVPAVAsset var videoComposition: AVVideoComposition? + var preferredForwardBufferDuration: TimeInterval = 0 init(asset: FVPAVAsset = TestAsset()) { self.asset = asset diff --git a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift index 5d0784d636ad..8c9e41411679 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift @@ -777,6 +777,30 @@ private let hlsAudioTestURI = colorProperties[AVVideoYCbCrMatrixKey] as? String == AVVideoYCbCrMatrix_ITU_R_709_2) } + @Test func forwardBufferDurationIsAppliedToPlayerItem() throws { + let stubPlayerItem = StubPlayerItem() + let videoPlayerPlugin = try createInitializedPlugin( + avFactory: StubFVPAVFactory(playerItem: stubPlayerItem)) + + _ = try videoPlayerPlugin.createTexturePlayer( + options: CreationOptions( + uri: mp4TestURI, httpHeaders: [:], forwardBufferDurationMs: 10000)) + + #expect(stubPlayerItem.preferredForwardBufferDuration == 10.0) + } + + @Test func forwardBufferDurationDefaultsToUnset() throws { + let stubPlayerItem = StubPlayerItem() + let videoPlayerPlugin = try createInitializedPlugin( + avFactory: StubFVPAVFactory(playerItem: stubPlayerItem)) + + _ = try videoPlayerPlugin.createTexturePlayer( + options: CreationOptions(uri: mp4TestURI, httpHeaders: [:])) + + // Untouched when the option is null (AVPlayer's automatic buffering). + #expect(stubPlayerItem.preferredForwardBufferDuration == 0) + } + // MARK: - Helper Methods /// Creates a plugin with the given dependencies, and default stubs for any that aren't provided, diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPAVFactory.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPAVFactory.m index d87a7c46c1b1..e12463a16b28 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPAVFactory.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPAVFactory.m @@ -75,6 +75,14 @@ - (AVVideoComposition *)videoComposition { - (void)setVideoComposition:(AVVideoComposition *)videoComposition { self.playerItem.videoComposition = videoComposition; } + +- (NSTimeInterval)preferredForwardBufferDuration { + return self.playerItem.preferredForwardBufferDuration; +} + +- (void)setPreferredForwardBufferDuration:(NSTimeInterval)preferredForwardBufferDuration { + self.playerItem.preferredForwardBufferDuration = preferredForwardBufferDuration; +} @end #pragma mark - diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/include/video_player_avfoundation_objc/FVPAVFactory.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/include/video_player_avfoundation_objc/FVPAVFactory.h index 6e80b8a5b67f..8bfa6a2675b9 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/include/video_player_avfoundation_objc/FVPAVFactory.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/include/video_player_avfoundation_objc/FVPAVFactory.h @@ -62,6 +62,9 @@ NS_ASSUME_NONNULL_BEGIN /// Wraps the underlying playerItem's videoComposition property. @property(nonatomic, copy, nullable) AVVideoComposition *videoComposition; + +/// Wraps the underlying playerItem's preferredForwardBufferDuration property. +@property(nonatomic) NSTimeInterval preferredForwardBufferDuration; @end #if TARGET_OS_IOS diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart index 5ea15303e635..9d7fbc106a25 100644 --- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart @@ -212,6 +212,51 @@ void main() { expect(player.buildViewWithOptions(VideoViewOptions(playerId: playerId!)), isA()); }); + test('createWithOptions passes forwardBufferDuration in ms', () async { + final (AVFoundationVideoPlayer player, MockAVFoundationVideoPlayerApi api, _) = + setUpMockPlayer(playerId: 1, textureId: 101); + when( + api.createForTextureView(any), + ).thenAnswer((_) async => TexturePlayerIds(playerId: 2, textureId: 102)); + + await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource( + sourceType: DataSourceType.network, + uri: 'https://example.com', + ), + viewType: VideoViewType.textureView, + forwardBufferDuration: const Duration(seconds: 10), + ), + ); + + final VerificationResult verification = verify(api.createForTextureView(captureAny)); + final creationOptions = verification.captured[0] as CreationOptions; + expect(creationOptions.forwardBufferDurationMs, 10000); + }); + + test('createWithOptions defaults forwardBufferDuration to null', () async { + final (AVFoundationVideoPlayer player, MockAVFoundationVideoPlayerApi api, _) = + setUpMockPlayer(playerId: 1, textureId: 101); + when( + api.createForTextureView(any), + ).thenAnswer((_) async => TexturePlayerIds(playerId: 2, textureId: 102)); + + await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource( + sourceType: DataSourceType.network, + uri: 'https://example.com', + ), + viewType: VideoViewType.textureView, + ), + ); + + final VerificationResult verification = verify(api.createForTextureView(captureAny)); + final creationOptions = verification.captured[0] as CreationOptions; + expect(creationOptions.forwardBufferDurationMs, isNull); + }); + test('createWithOptions with network passes headers', () async { final (AVFoundationVideoPlayer player, MockAVFoundationVideoPlayerApi api, _) = setUpMockPlayer(playerId: 1, textureId: 101); From b93f9daafc7aa99860eb2c1c0a019a6a9e59e3f6 Mon Sep 17 00:00:00 2001 From: Mohamed Fat-hy Date: Sun, 21 Jun 2026 18:26:09 +0300 Subject: [PATCH 3/4] [video_player] Regenerate pigeon Dart with the repo toolchain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the hand-edited generated Dart with proper `dart run pigeon` output (android: pigeon 26.1.5, avfoundation: 26.3.4 — matching each package's committed header) formatted at the repo's page width 100, so the generated-code CI check matches. Native generated files (Messages.kt / VideoPlayerPluginMessages.g.swift) are unchanged; their single added field already matches ktfmt/swift-format style. --- .../video_player_android/lib/src/messages.g.dart | 9 ++++++++- .../lib/src/video_player_plugin_messages.g.dart | 5 ++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/video_player/video_player_android/lib/src/messages.g.dart b/packages/video_player/video_player_android/lib/src/messages.g.dart index 22dcfdbe2b91..c9a479533b46 100644 --- a/packages/video_player/video_player_android/lib/src/messages.g.dart +++ b/packages/video_player/video_player_android/lib/src/messages.g.dart @@ -252,7 +252,13 @@ class PlatformVideoViewCreationParams { } class CreationOptions { - CreationOptions({required this.uri, this.formatHint, required this.httpHeaders, this.userAgent, this.forwardBufferDurationMs}); + CreationOptions({ + required this.uri, + this.formatHint, + required this.httpHeaders, + this.userAgent, + this.forwardBufferDurationMs, + }); String uri; @@ -262,6 +268,7 @@ class CreationOptions { String? userAgent; + /// Caps forward buffering (in ms); null uses ExoPlayer's default (~50s). int? forwardBufferDurationMs; List _toList() { diff --git a/packages/video_player/video_player_avfoundation/lib/src/video_player_plugin_messages.g.dart b/packages/video_player/video_player_avfoundation/lib/src/video_player_plugin_messages.g.dart index 191077d45bdc..c4c886779b60 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/video_player_plugin_messages.g.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/video_player_plugin_messages.g.dart @@ -141,6 +141,7 @@ class CreationOptions { Map httpHeaders; + /// Caps forward buffering (in ms); null uses AVPlayer's automatic buffering. int? forwardBufferDurationMs; List _toList() { @@ -169,7 +170,9 @@ class CreationOptions { if (identical(this, other)) { return true; } - return _deepEquals(uri, other.uri) && _deepEquals(httpHeaders, other.httpHeaders); + return _deepEquals(uri, other.uri) && + _deepEquals(httpHeaders, other.httpHeaders) && + _deepEquals(forwardBufferDurationMs, other.forwardBufferDurationMs); } @override From fd7120c1d9419cd693cb97c4fa0633347b2d555e Mon Sep 17 00:00:00 2001 From: Mohamed Fat-hy Date: Sun, 21 Jun 2026 18:46:23 +0300 Subject: [PATCH 4/4] [video_player] Address review: guard against negative/overflow buffer values - platform_interface: assert forwardBufferDuration is non-negative in VideoPlayerOptions (the user-facing entry point). - Android: clamp to Integer.MAX_VALUE before the int cast in cappedLoadControl. - iOS/macOS: clamp preferredForwardBufferDuration to >= 0 (a negative value raises an exception per Apple's docs). VideoCreationOptions keeps its const constructor (an isNegative assert isn't const-evaluable); it's only built internally from an already-validated VideoPlayerOptions. --- .../java/io/flutter/plugins/videoplayer/VideoPlayer.java | 2 +- .../video_player_avfoundation/VideoPlayerPlugin.swift | 3 ++- .../lib/video_player_platform_interface.dart | 5 ++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 842e4944bc4a..1c4badeb4f67 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -101,7 +101,7 @@ public static LoadControl cappedLoadControl(@Nullable Long forwardBufferDuration if (forwardBufferDurationMs == null) { return null; } - int bufferMs = (int) Math.max(1L, forwardBufferDurationMs); + int bufferMs = (int) Math.min((long) Integer.MAX_VALUE, Math.max(1L, forwardBufferDurationMs)); int bufferForPlaybackMs = Math.min(2_500, bufferMs); int bufferForPlaybackAfterRebufferMs = Math.min(5_000, bufferMs); return new DefaultLoadControl.Builder() diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPlugin.swift b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPlugin.swift index 3c357c2d94eb..767e4330bbe3 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPlugin.swift +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPlugin.swift @@ -282,7 +282,8 @@ public final class VideoPlayerPlugin: NSObject, FlutterPlugin, AVFoundationVideo // nil leaves AVPlayer's automatic (aggressive) buffering; a value (seconds) // limits read-ahead to save bandwidth. if let forwardBufferDurationMs = options.forwardBufferDurationMs { - item.preferredForwardBufferDuration = Double(forwardBufferDurationMs) / 1000.0 + // Clamp to >= 0; a negative preferredForwardBufferDuration raises an exception. + item.preferredForwardBufferDuration = max(0.0, Double(forwardBufferDurationMs) / 1000.0) } return item } diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index 9d6ac7ae1365..55914a1884bf 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -463,7 +463,10 @@ class VideoPlayerOptions { this.allowBackgroundPlayback = false, this.webOptions, this.forwardBufferDuration, - }); + }) : assert( + forwardBufferDuration == null || !forwardBufferDuration.isNegative, + 'forwardBufferDuration must be non-negative.', + ); /// Set this to true to keep playing video in background, when app goes in background. /// The default value is false.