Skip to content

Commit 14722b7

Browse files
authored
[video_player_android] Migrate ExoPlayer to ExoPlayer-Media3 1.3.1 (#6535)
Resolves [#130272](flutter/flutter#130272) Migrated with https://developer.android.com/media/media3/exoplayer/migration-guide#usingscript
1 parent 8a2c4e4 commit 14722b7

File tree

5 files changed

+107
-116
lines changed

5 files changed

+107
-116
lines changed

packages/video_player/video_player_android/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.5.0
2+
3+
* Migrates ExoPlayer to Media3-ExoPlayer 1.3.1.
4+
15
## 2.4.17
26

37
* Revert Impeller support.

packages/video_player/video_player_android/android/build.gradle

+5-5
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ android {
4848
}
4949

5050
dependencies {
51-
def exoplayer_version = "2.18.7"
52-
implementation "com.google.android.exoplayer:exoplayer-core:${exoplayer_version}"
53-
implementation "com.google.android.exoplayer:exoplayer-hls:${exoplayer_version}"
54-
implementation "com.google.android.exoplayer:exoplayer-dash:${exoplayer_version}"
55-
implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:${exoplayer_version}"
51+
def exoplayer_version = "1.3.1"
52+
implementation "androidx.media3:media3-exoplayer:${exoplayer_version}"
53+
implementation "androidx.media3:media3-exoplayer-hls:${exoplayer_version}"
54+
implementation "androidx.media3:media3-exoplayer-dash:${exoplayer_version}"
55+
implementation "androidx.media3:media3-exoplayer-smoothstreaming:${exoplayer_version}"
5656
testImplementation 'junit:junit:4.13.2'
5757
testImplementation 'androidx.test:core:1.3.0'
5858
testImplementation 'org.mockito:mockito-inline:5.0.0'

packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java

+81-90
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,30 @@
44

55
package io.flutter.plugins.videoplayer;
66

7-
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
8-
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
7+
import static androidx.media3.common.Player.REPEAT_MODE_ALL;
8+
import static androidx.media3.common.Player.REPEAT_MODE_OFF;
99

1010
import android.content.Context;
11-
import android.net.Uri;
1211
import android.view.Surface;
1312
import androidx.annotation.NonNull;
13+
import androidx.annotation.Nullable;
14+
import androidx.annotation.OptIn;
1415
import androidx.annotation.VisibleForTesting;
15-
import com.google.android.exoplayer2.C;
16-
import com.google.android.exoplayer2.ExoPlayer;
17-
import com.google.android.exoplayer2.Format;
18-
import com.google.android.exoplayer2.MediaItem;
19-
import com.google.android.exoplayer2.PlaybackException;
20-
import com.google.android.exoplayer2.PlaybackParameters;
21-
import com.google.android.exoplayer2.Player;
22-
import com.google.android.exoplayer2.Player.Listener;
23-
import com.google.android.exoplayer2.audio.AudioAttributes;
24-
import com.google.android.exoplayer2.source.MediaSource;
25-
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
26-
import com.google.android.exoplayer2.source.dash.DashMediaSource;
27-
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
28-
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
29-
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
30-
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
31-
import com.google.android.exoplayer2.upstream.DataSource;
32-
import com.google.android.exoplayer2.upstream.DefaultDataSource;
33-
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
34-
import com.google.android.exoplayer2.util.Util;
16+
import androidx.media3.common.AudioAttributes;
17+
import androidx.media3.common.C;
18+
import androidx.media3.common.MediaItem;
19+
import androidx.media3.common.MimeTypes;
20+
import androidx.media3.common.PlaybackException;
21+
import androidx.media3.common.PlaybackParameters;
22+
import androidx.media3.common.Player;
23+
import androidx.media3.common.Player.Listener;
24+
import androidx.media3.common.VideoSize;
25+
import androidx.media3.common.util.UnstableApi;
26+
import androidx.media3.datasource.DataSource;
27+
import androidx.media3.datasource.DefaultDataSource;
28+
import androidx.media3.datasource.DefaultHttpDataSource;
29+
import androidx.media3.exoplayer.ExoPlayer;
30+
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
3531
import io.flutter.plugin.common.EventChannel;
3632
import io.flutter.view.TextureRegistry;
3733
import java.util.Arrays;
@@ -62,7 +58,7 @@ final class VideoPlayer {
6258

6359
private final VideoPlayerOptions options;
6460

65-
private DefaultHttpDataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory();
61+
private final DefaultHttpDataSource.Factory httpDataSourceFactory;
6662

6763
VideoPlayer(
6864
Context context,
@@ -76,16 +72,18 @@ final class VideoPlayer {
7672
this.textureEntry = textureEntry;
7773
this.options = options;
7874

79-
ExoPlayer exoPlayer = new ExoPlayer.Builder(context).build();
80-
Uri uri = Uri.parse(dataSource);
75+
MediaItem mediaItem =
76+
new MediaItem.Builder()
77+
.setUri(dataSource)
78+
.setMimeType(mimeFromFormatHint(formatHint))
79+
.build();
8180

82-
buildHttpDataSourceFactory(httpHeaders);
83-
DataSource.Factory dataSourceFactory =
84-
new DefaultDataSource.Factory(context, httpDataSourceFactory);
81+
httpDataSourceFactory = new DefaultHttpDataSource.Factory();
82+
configureHttpDataSourceFactory(httpHeaders);
8583

86-
MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint);
84+
ExoPlayer exoPlayer = buildExoPlayer(context, httpDataSourceFactory);
8785

88-
exoPlayer.setMediaSource(mediaSource);
86+
exoPlayer.setMediaItem(mediaItem);
8987
exoPlayer.prepare();
9088

9189
setUpVideoPlayer(exoPlayer, new QueuingEventSink());
@@ -109,64 +107,15 @@ final class VideoPlayer {
109107
}
110108

111109
@VisibleForTesting
112-
public void buildHttpDataSourceFactory(@NonNull Map<String, String> httpHeaders) {
110+
public void configureHttpDataSourceFactory(@NonNull Map<String, String> httpHeaders) {
113111
final boolean httpHeadersNotEmpty = !httpHeaders.isEmpty();
114112
final String userAgent =
115113
httpHeadersNotEmpty && httpHeaders.containsKey(USER_AGENT)
116114
? httpHeaders.get(USER_AGENT)
117115
: "ExoPlayer";
118116

119-
httpDataSourceFactory.setUserAgent(userAgent).setAllowCrossProtocolRedirects(true);
120-
121-
if (httpHeadersNotEmpty) {
122-
httpDataSourceFactory.setDefaultRequestProperties(httpHeaders);
123-
}
124-
}
125-
126-
private MediaSource buildMediaSource(
127-
Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint) {
128-
int type;
129-
if (formatHint == null) {
130-
type = Util.inferContentType(uri);
131-
} else {
132-
switch (formatHint) {
133-
case FORMAT_SS:
134-
type = C.CONTENT_TYPE_SS;
135-
break;
136-
case FORMAT_DASH:
137-
type = C.CONTENT_TYPE_DASH;
138-
break;
139-
case FORMAT_HLS:
140-
type = C.CONTENT_TYPE_HLS;
141-
break;
142-
case FORMAT_OTHER:
143-
type = C.CONTENT_TYPE_OTHER;
144-
break;
145-
default:
146-
type = -1;
147-
break;
148-
}
149-
}
150-
switch (type) {
151-
case C.CONTENT_TYPE_SS:
152-
return new SsMediaSource.Factory(
153-
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mediaDataSourceFactory)
154-
.createMediaSource(MediaItem.fromUri(uri));
155-
case C.CONTENT_TYPE_DASH:
156-
return new DashMediaSource.Factory(
157-
new DefaultDashChunkSource.Factory(mediaDataSourceFactory), mediaDataSourceFactory)
158-
.createMediaSource(MediaItem.fromUri(uri));
159-
case C.CONTENT_TYPE_HLS:
160-
return new HlsMediaSource.Factory(mediaDataSourceFactory)
161-
.createMediaSource(MediaItem.fromUri(uri));
162-
case C.CONTENT_TYPE_OTHER:
163-
return new ProgressiveMediaSource.Factory(mediaDataSourceFactory)
164-
.createMediaSource(MediaItem.fromUri(uri));
165-
default:
166-
{
167-
throw new IllegalStateException("Unsupported type: " + type);
168-
}
169-
}
117+
unstableUpdateDataSourceFactory(
118+
httpDataSourceFactory, httpHeaders, userAgent, httpHeadersNotEmpty);
170119
}
171120

172121
private void setUpVideoPlayer(ExoPlayer exoPlayer, QueuingEventSink eventSink) {
@@ -304,15 +253,15 @@ void sendInitialized() {
304253
event.put("event", "initialized");
305254
event.put("duration", exoPlayer.getDuration());
306255

307-
if (exoPlayer.getVideoFormat() != null) {
308-
Format videoFormat = exoPlayer.getVideoFormat();
309-
int width = videoFormat.width;
310-
int height = videoFormat.height;
311-
int rotationDegrees = videoFormat.rotationDegrees;
256+
VideoSize videoSize = exoPlayer.getVideoSize();
257+
int width = videoSize.width;
258+
int height = videoSize.height;
259+
if (width != 0 && height != 0) {
260+
int rotationDegrees = videoSize.unappliedRotationDegrees;
312261
// Switch the width/height if video was taken in portrait mode
313262
if (rotationDegrees == 90 || rotationDegrees == 270) {
314-
width = exoPlayer.getVideoFormat().height;
315-
height = exoPlayer.getVideoFormat().width;
263+
width = videoSize.height;
264+
height = videoSize.width;
316265
}
317266
event.put("width", width);
318267
event.put("height", height);
@@ -343,4 +292,46 @@ void dispose() {
343292
exoPlayer.release();
344293
}
345294
}
295+
296+
@NonNull
297+
private static ExoPlayer buildExoPlayer(
298+
Context context, DataSource.Factory baseDataSourceFactory) {
299+
DataSource.Factory dataSourceFactory =
300+
new DefaultDataSource.Factory(context, baseDataSourceFactory);
301+
DefaultMediaSourceFactory mediaSourceFactory =
302+
new DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory);
303+
return new ExoPlayer.Builder(context).setMediaSourceFactory(mediaSourceFactory).build();
304+
}
305+
306+
@Nullable
307+
private static String mimeFromFormatHint(@Nullable String formatHint) {
308+
if (formatHint == null) {
309+
return null;
310+
}
311+
switch (formatHint) {
312+
case FORMAT_SS:
313+
return MimeTypes.APPLICATION_SS;
314+
case FORMAT_DASH:
315+
return MimeTypes.APPLICATION_MPD;
316+
case FORMAT_HLS:
317+
return MimeTypes.APPLICATION_M3U8;
318+
case FORMAT_OTHER:
319+
default:
320+
return null;
321+
}
322+
}
323+
324+
// TODO: migrate to stable API, see https://github.com/flutter/flutter/issues/147039
325+
@OptIn(markerClass = UnstableApi.class)
326+
private static void unstableUpdateDataSourceFactory(
327+
DefaultHttpDataSource.Factory factory,
328+
@NonNull Map<String, String> httpHeaders,
329+
String userAgent,
330+
boolean httpHeadersNotEmpty) {
331+
factory.setUserAgent(userAgent).setAllowCrossProtocolRedirects(true);
332+
333+
if (httpHeadersNotEmpty) {
334+
factory.setDefaultRequestProperties(httpHeaders);
335+
}
336+
}
346337
}

packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java

+16-20
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
import static org.mockito.Mockito.times;
1515

1616
import android.graphics.SurfaceTexture;
17-
import com.google.android.exoplayer2.ExoPlayer;
18-
import com.google.android.exoplayer2.Format;
19-
import com.google.android.exoplayer2.PlaybackException;
20-
import com.google.android.exoplayer2.Player;
21-
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
17+
import androidx.media3.common.PlaybackException;
18+
import androidx.media3.common.Player;
19+
import androidx.media3.common.VideoSize;
20+
import androidx.media3.datasource.DefaultHttpDataSource;
21+
import androidx.media3.exoplayer.ExoPlayer;
2222
import io.flutter.plugin.common.EventChannel;
2323
import io.flutter.view.TextureRegistry;
2424
import java.util.HashMap;
@@ -71,7 +71,7 @@ public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull()
7171
fakeEventSink,
7272
httpDataSourceFactorySpy);
7373

74-
videoPlayer.buildHttpDataSourceFactory(new HashMap<>());
74+
videoPlayer.configureHttpDataSourceFactory(new HashMap<>());
7575

7676
verify(httpDataSourceFactorySpy).setUserAgent("ExoPlayer");
7777
verify(httpDataSourceFactorySpy).setAllowCrossProtocolRedirects(true);
@@ -97,7 +97,7 @@ public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull()
9797
}
9898
};
9999

100-
videoPlayer.buildHttpDataSourceFactory(httpHeaders);
100+
videoPlayer.configureHttpDataSourceFactory(httpHeaders);
101101

102102
verify(httpDataSourceFactorySpy).setUserAgent("userAgent");
103103
verify(httpDataSourceFactorySpy).setAllowCrossProtocolRedirects(true);
@@ -122,7 +122,7 @@ public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull()
122122
}
123123
};
124124

125-
videoPlayer.buildHttpDataSourceFactory(httpHeaders);
125+
videoPlayer.configureHttpDataSourceFactory(httpHeaders);
126126

127127
verify(httpDataSourceFactorySpy).setUserAgent("ExoPlayer");
128128
verify(httpDataSourceFactorySpy).setAllowCrossProtocolRedirects(true);
@@ -139,10 +139,9 @@ public void sendInitializedSendsExpectedEvent_90RotationDegrees() {
139139
fakeVideoPlayerOptions,
140140
fakeEventSink,
141141
httpDataSourceFactorySpy);
142-
Format testFormat =
143-
new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(90).build();
142+
VideoSize testVideoSize = new VideoSize(100, 200, 90, 1f);
144143

145-
when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
144+
when(fakeExoPlayer.getVideoSize()).thenReturn(testVideoSize);
146145
when(fakeExoPlayer.getDuration()).thenReturn(10L);
147146

148147
videoPlayer.isInitialized = true;
@@ -168,10 +167,9 @@ public void sendInitializedSendsExpectedEvent_270RotationDegrees() {
168167
fakeVideoPlayerOptions,
169168
fakeEventSink,
170169
httpDataSourceFactorySpy);
171-
Format testFormat =
172-
new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(270).build();
170+
VideoSize testVideoSize = new VideoSize(100, 200, 270, 1f);
173171

174-
when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
172+
when(fakeExoPlayer.getVideoSize()).thenReturn(testVideoSize);
175173
when(fakeExoPlayer.getDuration()).thenReturn(10L);
176174

177175
videoPlayer.isInitialized = true;
@@ -197,10 +195,9 @@ public void sendInitializedSendsExpectedEvent_0RotationDegrees() {
197195
fakeVideoPlayerOptions,
198196
fakeEventSink,
199197
httpDataSourceFactorySpy);
200-
Format testFormat =
201-
new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(0).build();
198+
VideoSize testVideoSize = new VideoSize(100, 200, 0, 1f);
202199

203-
when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
200+
when(fakeExoPlayer.getVideoSize()).thenReturn(testVideoSize);
204201
when(fakeExoPlayer.getDuration()).thenReturn(10L);
205202

206203
videoPlayer.isInitialized = true;
@@ -226,10 +223,9 @@ public void sendInitializedSendsExpectedEvent_180RotationDegrees() {
226223
fakeVideoPlayerOptions,
227224
fakeEventSink,
228225
httpDataSourceFactorySpy);
229-
Format testFormat =
230-
new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(180).build();
226+
VideoSize testVideoSize = new VideoSize(100, 200, 180, 1f);
231227

232-
when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
228+
when(fakeExoPlayer.getVideoSize()).thenReturn(testVideoSize);
233229
when(fakeExoPlayer.getDuration()).thenReturn(10L);
234230

235231
videoPlayer.isInitialized = true;

packages/video_player/video_player_android/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: video_player_android
22
description: Android implementation of the video_player plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_android
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
5-
version: 2.4.17
5+
version: 2.5.0
66

77
environment:
88
sdk: ^3.4.0

0 commit comments

Comments
 (0)