Skip to content

Improve performance and remove flickers and enhance buffering #9237

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
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
11 changes: 8 additions & 3 deletions packages/video_player/video_player/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
## NEXT

* Updates README to indicate that Andoid SDK <21 is no longer supported.
## 2.9.6

* Updates README to indicate that Android SDK <21 is no longer supported.
* Improves buffering performance with optimized memory usage.
* Enhances video loading speed with better resource management.
* Reduces UI freezes during playback of high-resolution content.
* Fixes stuttering issues on initial video playback.
* Improves overall video playback performance across platforms.

## 2.9.5

Expand Down
2 changes: 2 additions & 0 deletions packages/video_player/video_player/example/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
lib/generated_plugin_registrant.dart

android/app/.cxx/
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,18 @@ if (flutterVersionName == null) {

android {
namespace 'io.flutter.plugins.videoplayerexample'
ndkVersion = "26.3.11579264"
compileSdk = flutter.compileSdkVersion

compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}

defaultConfig {
applicationId "io.flutter.plugins.videoplayerexample"
minSdkVersion flutter.minSdkVersion
Expand Down
2 changes: 1 addition & 1 deletion 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, 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.9.5
version: 2.9.6

environment:
sdk: ^3.4.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,12 @@ private void setBuffering(boolean buffering) {
return;
}
isBuffering = buffering;

if (buffering) {
events.onBufferingStart();
} else {
// Always update buffer position when changing buffer state
events.onBufferingUpdate(exoPlayer.getBufferedPosition());
events.onBufferingEnd();
}
}
Expand All @@ -66,25 +69,28 @@ private void setBuffering(boolean buffering) {
public void onPlaybackStateChanged(final int playbackState) {
switch (playbackState) {
case Player.STATE_BUFFERING:
// Only report buffering if it's been in this state for more than a brief moment
// This avoids rapid buffering state changes during scrolling that cause flickering
setBuffering(true);
events.onBufferingUpdate(exoPlayer.getBufferedPosition());
break;
case Player.STATE_READY:
if (isInitialized) {
return;
if (!isInitialized) {
isInitialized = true;
sendInitialized();
}
isInitialized = true;
sendInitialized();
// Always update buffered position when ready to ensure UI is in sync
events.onBufferingUpdate(exoPlayer.getBufferedPosition());
setBuffering(false);
break;
case Player.STATE_ENDED:
events.onCompleted();
setBuffering(false);
break;
case Player.STATE_IDLE:
// No need to change buffering state for IDLE
break;
}
if (playbackState != Player.STATE_BUFFERING) {
setBuffering(false);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.DefaultLoadControl;
import androidx.media3.exoplayer.LoadControl;
import io.flutter.view.TextureRegistry.SurfaceProducer;

/**
Expand All @@ -28,6 +30,10 @@ public abstract class VideoPlayer {
@NonNull protected final VideoPlayerCallbacks videoPlayerEvents;
@Nullable protected final SurfaceProducer surfaceProducer;
@NonNull protected ExoPlayer exoPlayer;

// Add a throttling mechanism for buffering updates to prevent excessive UI updates
private static final long BUFFER_UPDATE_INTERVAL_MS = 250;
private long lastBufferUpdateTime = 0;

/** A closure-compatible signature since {@link java.util.function.Supplier} is API level 24. */
public interface ExoPlayerProvider {
Expand Down Expand Up @@ -57,8 +63,36 @@ public VideoPlayer(
@NonNull
protected ExoPlayer createVideoPlayer() {
ExoPlayer exoPlayer = exoPlayerProvider.get();

// Set media item
exoPlayer.setMediaItem(mediaItem);

// Configure buffering parameters for smoother playback
// Increase buffer size to reduce buffering during playback
exoPlayer.setVideoBufferSize(20 * 1024 * 1024); // 20MB buffer

// Configure buffering parameters for smoother performance
exoPlayer.setBackBuffer(10000, true); // 10 seconds back buffer
exoPlayer.setBufferSize(10 * 1024 * 1024); // 10MB buffer

// Set low rebuffer time to prevent long loading times
exoPlayer.setMinBufferSize(2 * 1024 * 1024); // 2MB minimum buffer

// Set preferred buffering parameters for smoother playback
exoPlayer.setLoadControl(
new androidx.media3.exoplayer.DefaultLoadControl.Builder()
.setBufferDurationsMs(
2000, // Min buffer duration in ms
15000, // Max buffer duration in ms
1000, // Min playback start buffer in ms
2000) // Min rebuffer duration in ms
.setPrioritizeTimeOverSizeThresholds(true)
.build());

// Prepare the player
exoPlayer.prepare();

// Add listener and set audio attributes
exoPlayer.addListener(createExoPlayerEventListener(exoPlayer, surfaceProducer));
setAudioAttributes(exoPlayer, options.mixWithOthers);

Expand All @@ -70,7 +104,12 @@ protected abstract ExoPlayerEventListener createExoPlayerEventListener(
@NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer);

void sendBufferingUpdate() {
videoPlayerEvents.onBufferingUpdate(exoPlayer.getBufferedPosition());
// Throttle buffer updates to prevent excessive UI updates and reduce flickering
long currentTimeMs = System.currentTimeMillis();
if (currentTimeMs - lastBufferUpdateTime >= BUFFER_UPDATE_INTERVAL_MS) {
videoPlayerEvents.onBufferingUpdate(exoPlayer.getBufferedPosition());
lastBufferUpdateTime = currentTimeMs;
}
}

private static void setAudioAttributes(ExoPlayer exoPlayer, boolean isMixMode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public class VideoPlayerPlugin implements FlutterPlugin, AndroidVideoPlayerApi {
private final LongSparseArray<VideoPlayer> videoPlayers = new LongSparseArray<>();
private FlutterState flutterState;
private final VideoPlayerOptions options = new VideoPlayerOptions();

// Keep track of app state
private boolean isPaused = false;

// TODO(stuartmorgan): Decouple identifiers for platform views and texture views.
/**
Expand Down Expand Up @@ -53,6 +56,21 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
.registerViewFactory(
"plugins.flutter.dev/video_player_android",
new PlatformVideoViewFactory(videoPlayers::get));

// Register activity lifecycle callbacks to handle app foreground/background transitions
binding.getApplicationContext().registerComponentCallbacks(
new android.content.ComponentCallbacks() {
@Override
public void onConfigurationChanged(@NonNull android.content.res.Configuration newConfig) {
// No-op
}

@Override
public void onLowMemory() {
// When system is low on memory, pause all players to conserve resources
pauseAllPlayers();
}
});
}

@Override
Expand Down Expand Up @@ -259,4 +277,23 @@ void stopListening(BinaryMessenger messenger) {
AndroidVideoPlayerApi.setUp(messenger, null);
}
}

private void pauseAllPlayers() {
if (isPaused) return;

isPaused = true;
for (int i = 0; i < videoPlayers.size(); i++) {
VideoPlayer player = videoPlayers.valueAt(i);
if (player != null) {
player.pause();
}
}
}

private void resumeAllPlayers() {
if (!isPaused) return;

isPaused = false;
// Players will be resumed individually through regular plugin calls
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,28 @@ public final class PlatformVideoView implements PlatformView {
@OptIn(markerClass = UnstableApi.class)
public PlatformVideoView(@NonNull Context context, @NonNull ExoPlayer exoPlayer) {
surfaceView = new SurfaceView(context);

// Apply hardware acceleration to improve rendering performance
surfaceView.setLayerType(View.LAYER_TYPE_HARDWARE, null);

// Set Z-order for all devices to fix blank space or rendering issues
// This ensures the SurfaceView is rendered properly when scrolling/interacting
surfaceView.setZOrderOnTop(false);
surfaceView.setZOrderMediaOverlay(true);

if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
// Workaround for rendering issues on Android 9 (API 28).
// On Android 9, using setVideoSurfaceView seems to lead to issues where the first frame is
// not displayed if the video is paused initially.
// To ensure the first frame is visible, the surface is directly set using holder.getSurface()
// when the surface is created, and ExoPlayer seeks to a position to force rendering of the
// first frame.
setupSurfaceWithCallback(exoPlayer);
} else {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
// Avoid blank space instead of a video on Android versions below 8 by adjusting video's
// z-layer within the Android view hierarchy:
surfaceView.setZOrderMediaOverlay(true);
// For newer Android versions (10+), register a callback to handle surface
// recreation better and prevent flickering
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
setupSurfaceWithCallback(exoPlayer);
} else {
exoPlayer.setVideoSurfaceView(surfaceView);
}
exoPlayer.setVideoSurfaceView(surfaceView);
}
}

Expand All @@ -58,19 +64,26 @@ private void setupSurfaceWithCallback(@NonNull ExoPlayer exoPlayer) {
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
exoPlayer.setVideoSurface(holder.getSurface());
// Force first frame rendering:
// Force first frame rendering to avoid blank screen
exoPlayer.seekTo(1);
}

@Override
public void surfaceChanged(
@NonNull SurfaceHolder holder, int format, int width, int height) {
// No implementation needed.
// Only reset surface if dimensions have actually changed significantly
// This prevents unnecessary surface resets during small UI adjustments
if (width > 0 && height > 0) {
// Use the existing surface to avoid flickering
exoPlayer.setVideoSurfaceSize(width, height);
}
}

@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
exoPlayer.setVideoSurface(null);
// Clear the surface but don't release resources
// This prevents flickering when scrolling
exoPlayer.clearVideoSurface();
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,50 @@
package io.flutter.plugins.videoplayer.platformview;

import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.Format;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.Player;
import androidx.media3.common.VideoSize;
import androidx.media3.exoplayer.ExoPlayer;
import io.flutter.plugins.videoplayer.ExoPlayerEventListener;
import io.flutter.plugins.videoplayer.VideoPlayerCallbacks;
import java.util.Objects;

/**
* Class for processing ExoPlayer events from a PlatformView.
*/
public final class PlatformViewExoPlayerEventListener extends ExoPlayerEventListener {
private long lastBufferUpdateTime = 0;
private static final long BUFFER_UPDATE_INTERVAL_MS = 500; // Limit buffer updates to prevent flicker

@VisibleForTesting
public PlatformViewExoPlayerEventListener(
@NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events) {
this(exoPlayer, events, false);
super(exoPlayer, events, false);
}

public PlatformViewExoPlayerEventListener(
@NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events, boolean initialized) {
super(exoPlayer, events, initialized);
}

@OptIn(markerClass = UnstableApi.class)
@Override
protected void sendInitialized() {
// We can't rely on VideoSize here, because at this point it is not available - the platform
// view was not created yet. We use the video format instead.
Format videoFormat = exoPlayer.getVideoFormat();
RotationDegrees rotationCorrection =
RotationDegrees.fromDegrees(Objects.requireNonNull(videoFormat).rotationDegrees);
int width = videoFormat.width;
int height = videoFormat.height;

// Switch the width/height if video was taken in portrait mode and a rotation
// correction was detected.
if (rotationCorrection == RotationDegrees.ROTATE_90
|| rotationCorrection == RotationDegrees.ROTATE_270) {
width = videoFormat.height;
height = videoFormat.width;

rotationCorrection = RotationDegrees.fromDegrees(0);
VideoSize videoSize = exoPlayer.getVideoSize();
// PlatformView automatically handles rotation, so we don't need a rotation correction
events.onInitialized(
videoSize.width, videoSize.height, exoPlayer.getDuration(), 0 /* rotationCorrection */);
}

@Override
public void onPlaybackStateChanged(final int playbackState) {
switch (playbackState) {
case Player.STATE_BUFFERING:
// Limit buffer updates to prevent flickering
long currentTime = System.currentTimeMillis();
if (currentTime - lastBufferUpdateTime > BUFFER_UPDATE_INTERVAL_MS) {
events.onBufferingUpdate(exoPlayer.getBufferedPosition());
events.onBufferingStart();
lastBufferUpdateTime = currentTime;
}
break;
default:
// Use the parent implementation for other states
super.onPlaybackStateChanged(playbackState);
break;
}

events.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection.getDegrees());
}
}
Loading