Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit a336b28

Browse files
author
Michael Klimushyn
authored
[video_player] Add v2 embedding support (#2226)
- Adds support in the plugin - Adds a v2 embedding to the example app - Fixes a broken remote example in the example app - Increments the Flutter SDK dependency - Increments the version - Adds e2e tests for some simple use cases of the plugin
1 parent 9ddc6c1 commit a336b28

13 files changed

Lines changed: 550 additions & 336 deletions

File tree

packages/video_player/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.10.3
2+
3+
* Add support for the v2 Android embedding. This shouldn't impact existing
4+
functionality.
5+
16
## 0.10.2+6
27

38
* Remove AndroidX warnings.

packages/video_player/android/build.gradle

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,29 @@ android {
4545
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.9.6'
4646
}
4747
}
48+
49+
// TODO(mklim): Remove this hack once androidx.lifecycle is included on stable. https://github.com/flutter/flutter/issues/42348
50+
afterEvaluate {
51+
def containsEmbeddingDependencies = false
52+
for (def configuration : configurations.all) {
53+
for (def dependency : configuration.dependencies) {
54+
if (dependency.group == 'io.flutter' &&
55+
dependency.name.startsWith('flutter_embedding') &&
56+
dependency.isTransitive())
57+
{
58+
containsEmbeddingDependencies = true
59+
break
60+
}
61+
}
62+
}
63+
if (!containsEmbeddingDependencies) {
64+
android {
65+
dependencies {
66+
def lifecycle_version = "1.1.1"
67+
compileOnly "android.arch.lifecycle:runtime:$lifecycle_version"
68+
compileOnly "android.arch.lifecycle:common:$lifecycle_version"
69+
compileOnly "android.arch.lifecycle:common-java8:$lifecycle_version"
70+
}
71+
}
72+
}
73+
}
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
package io.flutter.plugins.videoplayer;
2+
3+
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
4+
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
5+
6+
import android.content.Context;
7+
import android.net.Uri;
8+
import android.os.Build;
9+
import android.view.Surface;
10+
import com.google.android.exoplayer2.C;
11+
import com.google.android.exoplayer2.ExoPlaybackException;
12+
import com.google.android.exoplayer2.ExoPlayerFactory;
13+
import com.google.android.exoplayer2.Format;
14+
import com.google.android.exoplayer2.Player;
15+
import com.google.android.exoplayer2.Player.EventListener;
16+
import com.google.android.exoplayer2.SimpleExoPlayer;
17+
import com.google.android.exoplayer2.audio.AudioAttributes;
18+
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
19+
import com.google.android.exoplayer2.source.ExtractorMediaSource;
20+
import com.google.android.exoplayer2.source.MediaSource;
21+
import com.google.android.exoplayer2.source.dash.DashMediaSource;
22+
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
23+
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
24+
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
25+
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
26+
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
27+
import com.google.android.exoplayer2.trackselection.TrackSelector;
28+
import com.google.android.exoplayer2.upstream.DataSource;
29+
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
30+
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
31+
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
32+
import com.google.android.exoplayer2.util.Util;
33+
import io.flutter.plugin.common.EventChannel;
34+
import io.flutter.plugin.common.MethodChannel.Result;
35+
import io.flutter.view.TextureRegistry;
36+
import java.util.Arrays;
37+
import java.util.Collections;
38+
import java.util.HashMap;
39+
import java.util.List;
40+
import java.util.Map;
41+
42+
final class VideoPlayer {
43+
private static final String FORMAT_SS = "ss";
44+
private static final String FORMAT_DASH = "dash";
45+
private static final String FORMAT_HLS = "hls";
46+
private static final String FORMAT_OTHER = "other";
47+
48+
private SimpleExoPlayer exoPlayer;
49+
50+
private Surface surface;
51+
52+
private final TextureRegistry.SurfaceTextureEntry textureEntry;
53+
54+
private QueuingEventSink eventSink = new QueuingEventSink();
55+
56+
private final EventChannel eventChannel;
57+
58+
private boolean isInitialized = false;
59+
60+
VideoPlayer(
61+
Context context,
62+
EventChannel eventChannel,
63+
TextureRegistry.SurfaceTextureEntry textureEntry,
64+
String dataSource,
65+
Result result,
66+
String formatHint) {
67+
this.eventChannel = eventChannel;
68+
this.textureEntry = textureEntry;
69+
70+
TrackSelector trackSelector = new DefaultTrackSelector();
71+
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
72+
73+
Uri uri = Uri.parse(dataSource);
74+
75+
DataSource.Factory dataSourceFactory;
76+
if (isHTTP(uri)) {
77+
dataSourceFactory =
78+
new DefaultHttpDataSourceFactory(
79+
"ExoPlayer",
80+
null,
81+
DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
82+
DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
83+
true);
84+
} else {
85+
dataSourceFactory = new DefaultDataSourceFactory(context, "ExoPlayer");
86+
}
87+
88+
MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context);
89+
exoPlayer.prepare(mediaSource);
90+
91+
setupVideoPlayer(eventChannel, textureEntry, result);
92+
}
93+
94+
private static boolean isHTTP(Uri uri) {
95+
if (uri == null || uri.getScheme() == null) {
96+
return false;
97+
}
98+
String scheme = uri.getScheme();
99+
return scheme.equals("http") || scheme.equals("https");
100+
}
101+
102+
private MediaSource buildMediaSource(
103+
Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) {
104+
int type;
105+
if (formatHint == null) {
106+
type = Util.inferContentType(uri.getLastPathSegment());
107+
} else {
108+
switch (formatHint) {
109+
case FORMAT_SS:
110+
type = C.TYPE_SS;
111+
break;
112+
case FORMAT_DASH:
113+
type = C.TYPE_DASH;
114+
break;
115+
case FORMAT_HLS:
116+
type = C.TYPE_HLS;
117+
break;
118+
case FORMAT_OTHER:
119+
type = C.TYPE_OTHER;
120+
break;
121+
default:
122+
type = -1;
123+
break;
124+
}
125+
}
126+
switch (type) {
127+
case C.TYPE_SS:
128+
return new SsMediaSource.Factory(
129+
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
130+
new DefaultDataSourceFactory(context, null, mediaDataSourceFactory))
131+
.createMediaSource(uri);
132+
case C.TYPE_DASH:
133+
return new DashMediaSource.Factory(
134+
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
135+
new DefaultDataSourceFactory(context, null, mediaDataSourceFactory))
136+
.createMediaSource(uri);
137+
case C.TYPE_HLS:
138+
return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
139+
case C.TYPE_OTHER:
140+
return new ExtractorMediaSource.Factory(mediaDataSourceFactory)
141+
.setExtractorsFactory(new DefaultExtractorsFactory())
142+
.createMediaSource(uri);
143+
default:
144+
{
145+
throw new IllegalStateException("Unsupported type: " + type);
146+
}
147+
}
148+
}
149+
150+
private void setupVideoPlayer(
151+
EventChannel eventChannel, TextureRegistry.SurfaceTextureEntry textureEntry, Result result) {
152+
153+
eventChannel.setStreamHandler(
154+
new EventChannel.StreamHandler() {
155+
@Override
156+
public void onListen(Object o, EventChannel.EventSink sink) {
157+
eventSink.setDelegate(sink);
158+
}
159+
160+
@Override
161+
public void onCancel(Object o) {
162+
eventSink.setDelegate(null);
163+
}
164+
});
165+
166+
surface = new Surface(textureEntry.surfaceTexture());
167+
exoPlayer.setVideoSurface(surface);
168+
setAudioAttributes(exoPlayer);
169+
170+
exoPlayer.addListener(
171+
new EventListener() {
172+
173+
@Override
174+
public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
175+
if (playbackState == Player.STATE_BUFFERING) {
176+
sendBufferingUpdate();
177+
} else if (playbackState == Player.STATE_READY) {
178+
if (!isInitialized) {
179+
isInitialized = true;
180+
sendInitialized();
181+
}
182+
} else if (playbackState == Player.STATE_ENDED) {
183+
Map<String, Object> event = new HashMap<>();
184+
event.put("event", "completed");
185+
eventSink.success(event);
186+
}
187+
}
188+
189+
@Override
190+
public void onPlayerError(final ExoPlaybackException error) {
191+
if (eventSink != null) {
192+
eventSink.error("VideoError", "Video player had error " + error, null);
193+
}
194+
}
195+
});
196+
197+
Map<String, Object> reply = new HashMap<>();
198+
reply.put("textureId", textureEntry.id());
199+
result.success(reply);
200+
}
201+
202+
void sendBufferingUpdate() {
203+
Map<String, Object> event = new HashMap<>();
204+
event.put("event", "bufferingUpdate");
205+
List<? extends Number> range = Arrays.asList(0, exoPlayer.getBufferedPosition());
206+
// iOS supports a list of buffered ranges, so here is a list with a single range.
207+
event.put("values", Collections.singletonList(range));
208+
eventSink.success(event);
209+
}
210+
211+
@SuppressWarnings("deprecation")
212+
private static void setAudioAttributes(SimpleExoPlayer exoPlayer) {
213+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
214+
exoPlayer.setAudioAttributes(
215+
new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build());
216+
} else {
217+
exoPlayer.setAudioStreamType(C.STREAM_TYPE_MUSIC);
218+
}
219+
}
220+
221+
void play() {
222+
exoPlayer.setPlayWhenReady(true);
223+
}
224+
225+
void pause() {
226+
exoPlayer.setPlayWhenReady(false);
227+
}
228+
229+
void setLooping(boolean value) {
230+
exoPlayer.setRepeatMode(value ? REPEAT_MODE_ALL : REPEAT_MODE_OFF);
231+
}
232+
233+
void setVolume(double value) {
234+
float bracketedValue = (float) Math.max(0.0, Math.min(1.0, value));
235+
exoPlayer.setVolume(bracketedValue);
236+
}
237+
238+
void seekTo(int location) {
239+
exoPlayer.seekTo(location);
240+
}
241+
242+
long getPosition() {
243+
return exoPlayer.getCurrentPosition();
244+
}
245+
246+
@SuppressWarnings("SuspiciousNameCombination")
247+
private void sendInitialized() {
248+
if (isInitialized) {
249+
Map<String, Object> event = new HashMap<>();
250+
event.put("event", "initialized");
251+
event.put("duration", exoPlayer.getDuration());
252+
253+
if (exoPlayer.getVideoFormat() != null) {
254+
Format videoFormat = exoPlayer.getVideoFormat();
255+
int width = videoFormat.width;
256+
int height = videoFormat.height;
257+
int rotationDegrees = videoFormat.rotationDegrees;
258+
// Switch the width/height if video was taken in portrait mode
259+
if (rotationDegrees == 90 || rotationDegrees == 270) {
260+
width = exoPlayer.getVideoFormat().height;
261+
height = exoPlayer.getVideoFormat().width;
262+
}
263+
event.put("width", width);
264+
event.put("height", height);
265+
}
266+
eventSink.success(event);
267+
}
268+
}
269+
270+
void dispose() {
271+
if (isInitialized) {
272+
exoPlayer.stop();
273+
}
274+
textureEntry.release();
275+
eventChannel.setStreamHandler(null);
276+
if (surface != null) {
277+
surface.release();
278+
}
279+
if (exoPlayer != null) {
280+
exoPlayer.release();
281+
}
282+
}
283+
}

0 commit comments

Comments
 (0)