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
6 changes: 5 additions & 1 deletion packages/video_player/video_player/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/video_player/video_player/lib/video_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
final creationOptions = platform_interface.VideoCreationOptions(
dataSource: dataSourceDescription,
viewType: viewType,
forwardBufferDuration: videoPlayerOptions?.forwardBufferDuration,
);

if (videoPlayerOptions?.mixWithOthers != null) {
Expand Down
8 changes: 4 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,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:
Expand Down
29 changes: 29 additions & 0 deletions packages/video_player/video_player/test/video_player_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1899,6 +1926,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform {
List<String> calls = <String>[];
List<DataSource> dataSources = <DataSource>[];
List<VideoViewType> viewTypes = <VideoViewType>[];
List<VideoCreationOptions> creationOptions = <VideoCreationOptions>[];
final Map<int, StreamController<VideoEvent>> streams = <int, StreamController<VideoEvent>>{};
bool forceInitError = false;
int nextPlayerId = 0;
Expand Down Expand Up @@ -1943,6 +1971,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform {
}
dataSources.add(options.dataSource);
viewTypes.add(options.viewType);
creationOptions.add(options);
return nextPlayerId++;
}

Expand Down
5 changes: 5 additions & 0 deletions packages/video_player/video_player_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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).
*
* <p>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.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()
.setBufferDurationsMs(
bufferMs, bufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs)
.build();
}

@NonNull
protected abstract ExoPlayerEventListener createExoPlayerEventListener(
@NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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();
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,15 +300,17 @@ data class CreationOptions(
val uri: String,
val formatHint: PlatformVideoFormat? = null,
val httpHeaders: Map<String, String>,
val userAgent: String? = null
val userAgent: String? = null,
val forwardBufferDurationMs: Long? = null
) {
companion object {
fun fromList(pigeonVar_list: List<Any?>): CreationOptions {
val uri = pigeonVar_list[0] as String
val formatHint = pigeonVar_list[1] as PlatformVideoFormat?
val httpHeaders = pigeonVar_list[2] as Map<String, String>
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)
}
}

Expand All @@ -318,6 +320,7 @@ data class CreationOptions(
formatHint,
httpHeaders,
userAgent,
forwardBufferDurationMs,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,15 @@ public void createsPlatformViewVideoPlayer() throws Exception {
try (MockedStatic<PlatformViewVideoPlayer> 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 =
new CreationOptions(
"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4",
null,
new HashMap<>(),
null,
null);

final long playerId = plugin.createForPlatformView(options);
Expand All @@ -95,14 +96,15 @@ public void createsTextureVideoPlayer() throws Exception {
try (MockedStatic<TextureVideoPlayer> 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 =
new CreationOptions(
"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4",
null,
new HashMap<>(),
null,
null);

final TexturePlayerIds ids = plugin.createForTextureView(options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class AndroidVideoPlayer extends VideoPlayerPlatform {
httpHeaders: httpHeaders,
userAgent: userAgent,
formatHint: formatHint,
forwardBufferDurationMs: options.forwardBufferDuration?.inMilliseconds,
);

final int playerId;
Expand Down
14 changes: 12 additions & 2 deletions packages/video_player/video_player_android/lib/src/messages.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,13 @@ 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;

Expand All @@ -262,8 +268,11 @@ class CreationOptions {

String? userAgent;

/// Caps forward buffering (in ms); null uses ExoPlayer's default (~50s).
int? forwardBufferDurationMs;

List<Object?> _toList() {
return <Object?>[uri, formatHint, httpHeaders, userAgent];
return <Object?>[uri, formatHint, httpHeaders, userAgent, forwardBufferDurationMs];
}

Object encode() {
Expand All @@ -277,6 +286,7 @@ class CreationOptions {
formatHint: result[1] as PlatformVideoFormat?,
httpHeaders: (result[2] as Map<Object?, Object?>?)!.cast<String, String>(),
userAgent: result[3] as String?,
forwardBufferDurationMs: result[4] as int?,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ class CreationOptions {
PlatformVideoFormat? formatHint;
Map<String, String> httpHeaders;
String? userAgent;

/// Caps forward buffering (in ms); null uses ExoPlayer's default (~50s).
int? forwardBufferDurationMs;
}

class TexturePlayerIds {
Expand Down
4 changes: 2 additions & 2 deletions packages/video_player/video_player_android/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading