[video_player_android] Migrate ExoPlayer to ExoPlayer-Media3 1.3.1 (#6535)
Resolves [#130272](https://github.com/flutter/flutter/issues/130272)
Migrated with https://developer.android.com/media/media3/exoplayer/migration-guide#usingscript
diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md
index e2b39f8..949ed92 100644
--- a/packages/video_player/video_player_android/CHANGELOG.md
+++ b/packages/video_player/video_player_android/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.5.0
+
+* Migrates ExoPlayer to Media3-ExoPlayer 1.3.1.
+
## 2.4.17
* Revert Impeller support.
diff --git a/packages/video_player/video_player_android/android/build.gradle b/packages/video_player/video_player_android/android/build.gradle
index 9d4d837..77f504d 100644
--- a/packages/video_player/video_player_android/android/build.gradle
+++ b/packages/video_player/video_player_android/android/build.gradle
@@ -48,11 +48,11 @@
}
dependencies {
- def exoplayer_version = "2.18.7"
- implementation "com.google.android.exoplayer:exoplayer-core:${exoplayer_version}"
- implementation "com.google.android.exoplayer:exoplayer-hls:${exoplayer_version}"
- implementation "com.google.android.exoplayer:exoplayer-dash:${exoplayer_version}"
- implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:${exoplayer_version}"
+ def exoplayer_version = "1.3.1"
+ implementation "androidx.media3:media3-exoplayer:${exoplayer_version}"
+ implementation "androidx.media3:media3-exoplayer-hls:${exoplayer_version}"
+ implementation "androidx.media3:media3-exoplayer-dash:${exoplayer_version}"
+ implementation "androidx.media3:media3-exoplayer-smoothstreaming:${exoplayer_version}"
testImplementation 'junit:junit:4.13.2'
testImplementation 'androidx.test:core:1.3.0'
testImplementation 'org.mockito:mockito-inline:5.0.0'
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 b8b7819..4529db7 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
@@ -4,34 +4,30 @@
package io.flutter.plugins.videoplayer;
-import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
-import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
+import static androidx.media3.common.Player.REPEAT_MODE_ALL;
+import static androidx.media3.common.Player.REPEAT_MODE_OFF;
import android.content.Context;
-import android.net.Uri;
import android.view.Surface;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.VisibleForTesting;
-import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.ExoPlayer;
-import com.google.android.exoplayer2.Format;
-import com.google.android.exoplayer2.MediaItem;
-import com.google.android.exoplayer2.PlaybackException;
-import com.google.android.exoplayer2.PlaybackParameters;
-import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.Player.Listener;
-import com.google.android.exoplayer2.audio.AudioAttributes;
-import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.source.ProgressiveMediaSource;
-import com.google.android.exoplayer2.source.dash.DashMediaSource;
-import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
-import com.google.android.exoplayer2.source.hls.HlsMediaSource;
-import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
-import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
-import com.google.android.exoplayer2.upstream.DataSource;
-import com.google.android.exoplayer2.upstream.DefaultDataSource;
-import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
-import com.google.android.exoplayer2.util.Util;
+import androidx.media3.common.AudioAttributes;
+import androidx.media3.common.C;
+import androidx.media3.common.MediaItem;
+import androidx.media3.common.MimeTypes;
+import androidx.media3.common.PlaybackException;
+import androidx.media3.common.PlaybackParameters;
+import androidx.media3.common.Player;
+import androidx.media3.common.Player.Listener;
+import androidx.media3.common.VideoSize;
+import androidx.media3.common.util.UnstableApi;
+import androidx.media3.datasource.DataSource;
+import androidx.media3.datasource.DefaultDataSource;
+import androidx.media3.datasource.DefaultHttpDataSource;
+import androidx.media3.exoplayer.ExoPlayer;
+import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import io.flutter.plugin.common.EventChannel;
import io.flutter.view.TextureRegistry;
import java.util.Arrays;
@@ -62,7 +58,7 @@
private final VideoPlayerOptions options;
- private DefaultHttpDataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory();
+ private final DefaultHttpDataSource.Factory httpDataSourceFactory;
VideoPlayer(
Context context,
@@ -76,16 +72,18 @@
this.textureEntry = textureEntry;
this.options = options;
- ExoPlayer exoPlayer = new ExoPlayer.Builder(context).build();
- Uri uri = Uri.parse(dataSource);
+ MediaItem mediaItem =
+ new MediaItem.Builder()
+ .setUri(dataSource)
+ .setMimeType(mimeFromFormatHint(formatHint))
+ .build();
- buildHttpDataSourceFactory(httpHeaders);
- DataSource.Factory dataSourceFactory =
- new DefaultDataSource.Factory(context, httpDataSourceFactory);
+ httpDataSourceFactory = new DefaultHttpDataSource.Factory();
+ configureHttpDataSourceFactory(httpHeaders);
- MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint);
+ ExoPlayer exoPlayer = buildExoPlayer(context, httpDataSourceFactory);
- exoPlayer.setMediaSource(mediaSource);
+ exoPlayer.setMediaItem(mediaItem);
exoPlayer.prepare();
setUpVideoPlayer(exoPlayer, new QueuingEventSink());
@@ -109,64 +107,15 @@
}
@VisibleForTesting
- public void buildHttpDataSourceFactory(@NonNull Map<String, String> httpHeaders) {
+ public void configureHttpDataSourceFactory(@NonNull Map<String, String> httpHeaders) {
final boolean httpHeadersNotEmpty = !httpHeaders.isEmpty();
final String userAgent =
httpHeadersNotEmpty && httpHeaders.containsKey(USER_AGENT)
? httpHeaders.get(USER_AGENT)
: "ExoPlayer";
- httpDataSourceFactory.setUserAgent(userAgent).setAllowCrossProtocolRedirects(true);
-
- if (httpHeadersNotEmpty) {
- httpDataSourceFactory.setDefaultRequestProperties(httpHeaders);
- }
- }
-
- private MediaSource buildMediaSource(
- Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint) {
- int type;
- if (formatHint == null) {
- type = Util.inferContentType(uri);
- } else {
- switch (formatHint) {
- case FORMAT_SS:
- type = C.CONTENT_TYPE_SS;
- break;
- case FORMAT_DASH:
- type = C.CONTENT_TYPE_DASH;
- break;
- case FORMAT_HLS:
- type = C.CONTENT_TYPE_HLS;
- break;
- case FORMAT_OTHER:
- type = C.CONTENT_TYPE_OTHER;
- break;
- default:
- type = -1;
- break;
- }
- }
- switch (type) {
- case C.CONTENT_TYPE_SS:
- return new SsMediaSource.Factory(
- new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mediaDataSourceFactory)
- .createMediaSource(MediaItem.fromUri(uri));
- case C.CONTENT_TYPE_DASH:
- return new DashMediaSource.Factory(
- new DefaultDashChunkSource.Factory(mediaDataSourceFactory), mediaDataSourceFactory)
- .createMediaSource(MediaItem.fromUri(uri));
- case C.CONTENT_TYPE_HLS:
- return new HlsMediaSource.Factory(mediaDataSourceFactory)
- .createMediaSource(MediaItem.fromUri(uri));
- case C.CONTENT_TYPE_OTHER:
- return new ProgressiveMediaSource.Factory(mediaDataSourceFactory)
- .createMediaSource(MediaItem.fromUri(uri));
- default:
- {
- throw new IllegalStateException("Unsupported type: " + type);
- }
- }
+ unstableUpdateDataSourceFactory(
+ httpDataSourceFactory, httpHeaders, userAgent, httpHeadersNotEmpty);
}
private void setUpVideoPlayer(ExoPlayer exoPlayer, QueuingEventSink eventSink) {
@@ -304,15 +253,15 @@
event.put("event", "initialized");
event.put("duration", exoPlayer.getDuration());
- if (exoPlayer.getVideoFormat() != null) {
- Format videoFormat = exoPlayer.getVideoFormat();
- int width = videoFormat.width;
- int height = videoFormat.height;
- int rotationDegrees = videoFormat.rotationDegrees;
+ VideoSize videoSize = exoPlayer.getVideoSize();
+ int width = videoSize.width;
+ int height = videoSize.height;
+ if (width != 0 && height != 0) {
+ int rotationDegrees = videoSize.unappliedRotationDegrees;
// Switch the width/height if video was taken in portrait mode
if (rotationDegrees == 90 || rotationDegrees == 270) {
- width = exoPlayer.getVideoFormat().height;
- height = exoPlayer.getVideoFormat().width;
+ width = videoSize.height;
+ height = videoSize.width;
}
event.put("width", width);
event.put("height", height);
@@ -343,4 +292,46 @@
exoPlayer.release();
}
}
+
+ @NonNull
+ private static ExoPlayer buildExoPlayer(
+ Context context, DataSource.Factory baseDataSourceFactory) {
+ DataSource.Factory dataSourceFactory =
+ new DefaultDataSource.Factory(context, baseDataSourceFactory);
+ DefaultMediaSourceFactory mediaSourceFactory =
+ new DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory);
+ return new ExoPlayer.Builder(context).setMediaSourceFactory(mediaSourceFactory).build();
+ }
+
+ @Nullable
+ private static String mimeFromFormatHint(@Nullable String formatHint) {
+ if (formatHint == null) {
+ return null;
+ }
+ switch (formatHint) {
+ case FORMAT_SS:
+ return MimeTypes.APPLICATION_SS;
+ case FORMAT_DASH:
+ return MimeTypes.APPLICATION_MPD;
+ case FORMAT_HLS:
+ return MimeTypes.APPLICATION_M3U8;
+ case FORMAT_OTHER:
+ default:
+ return null;
+ }
+ }
+
+ // TODO: migrate to stable API, see https://github.com/flutter/flutter/issues/147039
+ @OptIn(markerClass = UnstableApi.class)
+ private static void unstableUpdateDataSourceFactory(
+ DefaultHttpDataSource.Factory factory,
+ @NonNull Map<String, String> httpHeaders,
+ String userAgent,
+ boolean httpHeadersNotEmpty) {
+ factory.setUserAgent(userAgent).setAllowCrossProtocolRedirects(true);
+
+ if (httpHeadersNotEmpty) {
+ factory.setDefaultRequestProperties(httpHeaders);
+ }
+ }
}
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 7ff5000..a7a03e9 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
@@ -14,11 +14,11 @@
import static org.mockito.Mockito.times;
import android.graphics.SurfaceTexture;
-import com.google.android.exoplayer2.ExoPlayer;
-import com.google.android.exoplayer2.Format;
-import com.google.android.exoplayer2.PlaybackException;
-import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
+import androidx.media3.common.PlaybackException;
+import androidx.media3.common.Player;
+import androidx.media3.common.VideoSize;
+import androidx.media3.datasource.DefaultHttpDataSource;
+import androidx.media3.exoplayer.ExoPlayer;
import io.flutter.plugin.common.EventChannel;
import io.flutter.view.TextureRegistry;
import java.util.HashMap;
@@ -71,7 +71,7 @@
fakeEventSink,
httpDataSourceFactorySpy);
- videoPlayer.buildHttpDataSourceFactory(new HashMap<>());
+ videoPlayer.configureHttpDataSourceFactory(new HashMap<>());
verify(httpDataSourceFactorySpy).setUserAgent("ExoPlayer");
verify(httpDataSourceFactorySpy).setAllowCrossProtocolRedirects(true);
@@ -97,7 +97,7 @@
}
};
- videoPlayer.buildHttpDataSourceFactory(httpHeaders);
+ videoPlayer.configureHttpDataSourceFactory(httpHeaders);
verify(httpDataSourceFactorySpy).setUserAgent("userAgent");
verify(httpDataSourceFactorySpy).setAllowCrossProtocolRedirects(true);
@@ -122,7 +122,7 @@
}
};
- videoPlayer.buildHttpDataSourceFactory(httpHeaders);
+ videoPlayer.configureHttpDataSourceFactory(httpHeaders);
verify(httpDataSourceFactorySpy).setUserAgent("ExoPlayer");
verify(httpDataSourceFactorySpy).setAllowCrossProtocolRedirects(true);
@@ -139,10 +139,9 @@
fakeVideoPlayerOptions,
fakeEventSink,
httpDataSourceFactorySpy);
- Format testFormat =
- new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(90).build();
+ VideoSize testVideoSize = new VideoSize(100, 200, 90, 1f);
- when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
+ when(fakeExoPlayer.getVideoSize()).thenReturn(testVideoSize);
when(fakeExoPlayer.getDuration()).thenReturn(10L);
videoPlayer.isInitialized = true;
@@ -168,10 +167,9 @@
fakeVideoPlayerOptions,
fakeEventSink,
httpDataSourceFactorySpy);
- Format testFormat =
- new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(270).build();
+ VideoSize testVideoSize = new VideoSize(100, 200, 270, 1f);
- when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
+ when(fakeExoPlayer.getVideoSize()).thenReturn(testVideoSize);
when(fakeExoPlayer.getDuration()).thenReturn(10L);
videoPlayer.isInitialized = true;
@@ -197,10 +195,9 @@
fakeVideoPlayerOptions,
fakeEventSink,
httpDataSourceFactorySpy);
- Format testFormat =
- new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(0).build();
+ VideoSize testVideoSize = new VideoSize(100, 200, 0, 1f);
- when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
+ when(fakeExoPlayer.getVideoSize()).thenReturn(testVideoSize);
when(fakeExoPlayer.getDuration()).thenReturn(10L);
videoPlayer.isInitialized = true;
@@ -226,10 +223,9 @@
fakeVideoPlayerOptions,
fakeEventSink,
httpDataSourceFactorySpy);
- Format testFormat =
- new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(180).build();
+ VideoSize testVideoSize = new VideoSize(100, 200, 180, 1f);
- when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
+ when(fakeExoPlayer.getVideoSize()).thenReturn(testVideoSize);
when(fakeExoPlayer.getDuration()).thenReturn(10L);
videoPlayer.isInitialized = true;
diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml
index 864c3ec..4fbd2a8 100644
--- a/packages/video_player/video_player_android/pubspec.yaml
+++ b/packages/video_player/video_player_android/pubspec.yaml
@@ -2,7 +2,7 @@
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.4.17
+version: 2.5.0
environment:
sdk: ^3.4.0