[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