Skip to content
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

[video_player_android] Migrate ExoPlayer to ExoPlayer-Media3 1.3.1 #6535

Merged
merged 9 commits into from
Jun 7, 2024
4 changes: 4 additions & 0 deletions packages/video_player/video_player_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.5.0

* Migrates ExoPlayer to Media3-ExoPlayer 1.3.1.

## 2.4.17

* Revert Impeller support.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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());
Expand All @@ -109,64 +107,15 @@ final class VideoPlayer {
}

@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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<String, String> httpHeaders,
String userAgent,
boolean httpHeadersNotEmpty) {
factory.setUserAgent(userAgent).setAllowCrossProtocolRedirects(true);

if (httpHeadersNotEmpty) {
factory.setDefaultRequestProperties(httpHeaders);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -97,7 +97,7 @@ public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull()
}
};

videoPlayer.buildHttpDataSourceFactory(httpHeaders);
videoPlayer.configureHttpDataSourceFactory(httpHeaders);

verify(httpDataSourceFactorySpy).setUserAgent("userAgent");
verify(httpDataSourceFactorySpy).setAllowCrossProtocolRedirects(true);
Expand All @@ -122,7 +122,7 @@ public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull()
}
};

videoPlayer.buildHttpDataSourceFactory(httpHeaders);
videoPlayer.configureHttpDataSourceFactory(httpHeaders);

verify(httpDataSourceFactorySpy).setUserAgent("ExoPlayer");
verify(httpDataSourceFactorySpy).setAllowCrossProtocolRedirects(true);
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/video_player/video_player_android/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down