Skip to content

Commit 2f8c779

Browse files
author
Sailendra Bathi
committed
[video_player] Improve seek performance on Android
Adds `backBufferDurationMs` to `VideoPlayerOptions` and configures ExoPlayer's `DefaultLoadControl` to retain the back buffer from keyframes. This significantly improves seek performance and responsiveness during video playback on Android.
1 parent 6246800 commit 2f8c779

25 files changed

Lines changed: 832 additions & 346 deletions

‎packages/video_player/video_player/CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
* Updates minimum supported SDK version to Flutter 3.38/Dart 3.10.
44

5+
## 2.12.0
6+
7+
* Passes `backBufferDurationMs` from `VideoPlayerOptions` to the underlying platform interface.
8+
59
## 2.11.1
610

711
* Optimizes caption retrieval with binary search.

‎packages/video_player/video_player/example/lib/main.dart‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> {
316316
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
317317
),
318318
closedCaptionFile: _loadCaptions(),
319-
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
319+
videoPlayerOptions: const VideoPlayerOptions(mixWithOthers: true),
320320
viewType: widget.viewType,
321321
);
322322

‎packages/video_player/video_player/lib/video_player.dart‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
576576
final creationOptions = platform_interface.VideoCreationOptions(
577577
dataSource: dataSourceDescription,
578578
viewType: viewType,
579+
videoPlayerOptions: videoPlayerOptions,
579580
);
580581

581582
if (videoPlayerOptions?.mixWithOthers != null) {

‎packages/video_player/video_player/pubspec.yaml‎

Lines changed: 5 additions & 5 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,10 +26,10 @@ 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
32-
video_player_web: ^2.1.0
29+
video_player_android: ^2.10.0
30+
video_player_avfoundation: ^2.10.0
31+
video_player_platform_interface: ^6.8.0
32+
video_player_web: ^2.5.0
3333

3434
dev_dependencies:
3535
flutter_test:

‎packages/video_player/video_player/test/video_player_initialization_test.dart‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ void main() {
3636

3737
final controller = VideoPlayerController.networkUrl(
3838
Uri.parse('https://127.0.0.1'),
39-
videoPlayerOptions: VideoPlayerOptions(webOptions: expected),
39+
videoPlayerOptions: const VideoPlayerOptions(webOptions: expected),
4040
);
4141
await controller.initialize();
4242

@@ -80,4 +80,24 @@ void main() {
8080
reason: 'view type must be passed to the platform',
8181
);
8282
});
83+
84+
test('back buffer duration is forwarded to platform', () async {
85+
const expectedBackBufferDurationMs = 20000;
86+
87+
final controller = VideoPlayerController.networkUrl(
88+
Uri.parse('https://127.0.0.1'),
89+
videoPlayerOptions: const VideoPlayerOptions(
90+
backBufferDurationMs: expectedBackBufferDurationMs,
91+
),
92+
);
93+
94+
await controller.initialize();
95+
96+
expect(
97+
fakeVideoPlayerPlatform.videoPlayerOptions.last?.backBufferDurationMs,
98+
expectedBackBufferDurationMs,
99+
reason:
100+
'backBufferDurationMs must be forwarded to the platform via VideoCreationOptions.videoPlayerOptions',
101+
);
102+
});
83103
}

‎packages/video_player/video_player/test/video_player_test.dart‎

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -582,35 +582,51 @@ void main() {
582582
expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri');
583583
}, skip: kIsWeb /* Web does not support file assets. */);
584584

585-
test('file with special characters', () async {
586-
final controller = VideoPlayerController.file(File('A #1 Hit.avi'));
587-
await controller.initialize();
585+
test(
586+
'file with special characters',
587+
() async {
588+
final controller = VideoPlayerController.file(File('A #1 Hit.avi'));
589+
await controller.initialize();
588590

589-
final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!;
590-
expect(uri.startsWith('file:///'), true, reason: 'Actual string: $uri');
591-
expect(
592-
uri.endsWith('/A%20%231%20Hit.avi'),
593-
true,
594-
reason: 'Actual string: $uri',
595-
);
596-
}, skip: kIsWeb /* Web does not support file assets. */);
591+
final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!;
592+
expect(
593+
uri.startsWith('file:///'),
594+
true,
595+
reason: 'Actual string: $uri',
596+
);
597+
expect(
598+
uri.endsWith('/A%20%231%20Hit.avi'),
599+
true,
600+
reason: 'Actual string: $uri',
601+
);
602+
},
603+
skip: kIsWeb /* Web does not support file assets. */,
604+
);
597605

598-
test('file with headers (m3u8)', () async {
599-
final controller = VideoPlayerController.file(
600-
File('a.avi'),
601-
httpHeaders: <String, String>{'Authorization': 'Bearer token'},
602-
);
603-
await controller.initialize();
606+
test(
607+
'file with headers (m3u8)',
608+
() async {
609+
final controller = VideoPlayerController.file(
610+
File('a.avi'),
611+
httpHeaders: <String, String>{'Authorization': 'Bearer token'},
612+
);
613+
await controller.initialize();
604614

605-
final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!;
606-
expect(uri.startsWith('file:///'), true, reason: 'Actual string: $uri');
607-
expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri');
615+
final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!;
616+
expect(
617+
uri.startsWith('file:///'),
618+
true,
619+
reason: 'Actual string: $uri',
620+
);
621+
expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri');
608622

609-
expect(
610-
fakeVideoPlayerPlatform.dataSources[0].httpHeaders,
611-
<String, String>{'Authorization': 'Bearer token'},
612-
);
613-
}, skip: kIsWeb /* Web does not support file assets. */);
623+
expect(
624+
fakeVideoPlayerPlatform.dataSources[0].httpHeaders,
625+
<String, String>{'Authorization': 'Bearer token'},
626+
);
627+
},
628+
skip: kIsWeb /* Web does not support file assets. */,
629+
);
614630

615631
test(
616632
'successful initialize on controller with error clears error',
@@ -1151,13 +1167,11 @@ void main() {
11511167
}
11521168

11531169
expect(isSorted, false, reason: 'Expected captions to be unsorted');
1154-
expect(captions.map((Caption c) => c.text).toList(), <String>[
1155-
'one',
1156-
'two',
1157-
'three',
1158-
'five',
1159-
'four',
1160-
], reason: 'Captions should be in original unsorted order');
1170+
expect(
1171+
captions.map((Caption c) => c.text).toList(),
1172+
<String>['one', 'two', 'three', 'five', 'four'],
1173+
reason: 'Captions should be in original unsorted order',
1174+
);
11611175
});
11621176

11631177
test('works when seeking, includes all captions', () async {
@@ -2096,6 +2110,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform {
20962110
List<String> calls = <String>[];
20972111
List<DataSource> dataSources = <DataSource>[];
20982112
List<VideoViewType> viewTypes = <VideoViewType>[];
2113+
List<VideoPlayerOptions?> videoPlayerOptions = <VideoPlayerOptions?>[];
20992114
final Map<int, StreamController<VideoEvent>> streams =
21002115
<int, StreamController<VideoEvent>>{};
21012116
bool forceInitError = false;
@@ -2152,6 +2167,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform {
21522167
}
21532168
dataSources.add(options.dataSource);
21542169
viewTypes.add(options.viewType);
2170+
videoPlayerOptions.add(options.videoPlayerOptions);
21552171
return nextPlayerId++;
21562172
}
21572173

‎packages/video_player/video_player_android/CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
* Migrates to Built-in Kotlin to support AGP 9.
44
* Updates minimum supported SDK version to Flutter 3.44/Dart 3.12.
55

6+
## 2.10.0
7+
8+
* Adds `backBufferDurationMs` to `CreationOptions` to configure ExoPlayer `DefaultLoadControl` back buffer duration.
9+
610
## 2.9.5
711

812
* Updates build files from Groovy to Kotlin.

‎packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,17 @@
66

77
public class VideoPlayerOptions {
88
public boolean mixWithOthers;
9+
10+
/**
11+
* The duration of the back buffer in milliseconds, used to configure ExoPlayer's load control.
12+
*/
13+
public Long backBufferDurationMs;
14+
15+
public VideoPlayerOptions() {}
16+
17+
/** Copy constructor to ensure all options are reliably copied. */
18+
public VideoPlayerOptions(VideoPlayerOptions other) {
19+
this.mixWithOthers = other.mixWithOthers;
20+
this.backBufferDurationMs = other.backBufferDurationMs;
21+
}
922
}

‎packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,15 @@ public long createForPlatformView(@NonNull CreationOptions options) {
8787

8888
long id = nextPlayerIdentifier++;
8989
final String streamInstance = Long.toString(id);
90+
VideoPlayerOptions playerOptions = new VideoPlayerOptions(sharedOptions);
91+
playerOptions.backBufferDurationMs = options.getBackBufferDurationMs();
92+
9093
VideoPlayer videoPlayer =
9194
PlatformViewVideoPlayer.create(
9295
flutterState.applicationContext,
9396
VideoPlayerEventCallbacks.bindTo(flutterState.binaryMessenger, streamInstance),
9497
videoAsset,
95-
sharedOptions);
98+
playerOptions);
9699

97100
registerPlayerInstance(videoPlayer, id);
98101
return id;
@@ -106,13 +109,16 @@ public long createForPlatformView(@NonNull CreationOptions options) {
106109
long id = nextPlayerIdentifier++;
107110
final String streamInstance = Long.toString(id);
108111
TextureRegistry.SurfaceProducer handle = flutterState.textureRegistry.createSurfaceProducer();
112+
VideoPlayerOptions playerOptions = new VideoPlayerOptions(sharedOptions);
113+
playerOptions.backBufferDurationMs = options.getBackBufferDurationMs();
114+
109115
VideoPlayer videoPlayer =
110116
TextureVideoPlayer.create(
111117
flutterState.applicationContext,
112118
VideoPlayerEventCallbacks.bindTo(flutterState.binaryMessenger, streamInstance),
113119
handle,
114120
videoAsset,
115-
sharedOptions);
121+
playerOptions);
116122

117123
registerPlayerInstance(videoPlayer, id);
118124
return new TexturePlayerIds(id, handle.id());

‎packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import androidx.annotation.VisibleForTesting;
1111
import androidx.media3.common.MediaItem;
1212
import androidx.media3.common.util.UnstableApi;
13+
import androidx.media3.exoplayer.DefaultLoadControl;
1314
import androidx.media3.exoplayer.ExoPlayer;
1415
import io.flutter.plugins.videoplayer.ExoPlayerEventListener;
1516
import io.flutter.plugins.videoplayer.VideoAsset;
@@ -56,12 +57,23 @@ public static PlatformViewVideoPlayer create(
5657
asset.getMediaItem(),
5758
options,
5859
() -> {
60+
ExoPlayer.Builder builder = new ExoPlayer.Builder(context);
61+
if (options.backBufferDurationMs != null && options.backBufferDurationMs > 0) {
62+
// Clamp the value to ensure it fits within the int range expected by
63+
// DefaultLoadControl.
64+
int backBufferInt =
65+
(int) Math.min(options.backBufferDurationMs.longValue(), Integer.MAX_VALUE);
66+
DefaultLoadControl loadControl =
67+
new DefaultLoadControl.Builder()
68+
.setBackBuffer(backBufferInt, /* retainBackBufferFromKeyframe= */ true)
69+
.build();
70+
builder.setLoadControl(loadControl);
71+
}
5972
androidx.media3.exoplayer.trackselection.DefaultTrackSelector trackSelector =
6073
new androidx.media3.exoplayer.trackselection.DefaultTrackSelector(context);
61-
ExoPlayer.Builder builder =
62-
new ExoPlayer.Builder(context)
63-
.setTrackSelector(trackSelector)
64-
.setMediaSourceFactory(asset.getMediaSourceFactory(context));
74+
builder
75+
.setTrackSelector(trackSelector)
76+
.setMediaSourceFactory(asset.getMediaSourceFactory(context));
6577
return builder.build();
6678
});
6779
}

0 commit comments

Comments
 (0)