diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index e2b39f8f44f8..949ed92597e8 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 9d4d8376de2d..77f504de422f 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 @@ android { } 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 b8b781912308..4529db72d86b 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 @@ final class VideoPlayer { private final VideoPlayerOptions options; - private DefaultHttpDataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory(); + private final DefaultHttpDataSource.Factory httpDataSourceFactory; VideoPlayer( Context context, @@ -76,16 +72,18 @@ final class VideoPlayer { 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 @@ final class VideoPlayer { } @VisibleForTesting - public void buildHttpDataSourceFactory(@NonNull Map httpHeaders) { + public void configureHttpDataSourceFactory(@NonNull Map 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 @@ void sendInitialized() { 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 @@ void dispose() { 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 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 7ff5000d3cf7..a7a03e9e1ed9 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 @@ public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull() fakeEventSink, httpDataSourceFactorySpy); - videoPlayer.buildHttpDataSourceFactory(new HashMap<>()); + videoPlayer.configureHttpDataSourceFactory(new HashMap<>()); verify(httpDataSourceFactorySpy).setUserAgent("ExoPlayer"); verify(httpDataSourceFactorySpy).setAllowCrossProtocolRedirects(true); @@ -97,7 +97,7 @@ public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull() } }; - videoPlayer.buildHttpDataSourceFactory(httpHeaders); + videoPlayer.configureHttpDataSourceFactory(httpHeaders); verify(httpDataSourceFactorySpy).setUserAgent("userAgent"); verify(httpDataSourceFactorySpy).setAllowCrossProtocolRedirects(true); @@ -122,7 +122,7 @@ public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull() } }; - videoPlayer.buildHttpDataSourceFactory(httpHeaders); + videoPlayer.configureHttpDataSourceFactory(httpHeaders); verify(httpDataSourceFactorySpy).setUserAgent("ExoPlayer"); verify(httpDataSourceFactorySpy).setAllowCrossProtocolRedirects(true); @@ -139,10 +139,9 @@ public void sendInitializedSendsExpectedEvent_90RotationDegrees() { 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 @@ public void sendInitializedSendsExpectedEvent_270RotationDegrees() { 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 @@ public void sendInitializedSendsExpectedEvent_0RotationDegrees() { 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 @@ public void sendInitializedSendsExpectedEvent_180RotationDegrees() { 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 864c3ec8efac..4fbd2a88fbe2 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -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.4.17 +version: 2.5.0 environment: sdk: ^3.4.0