blob: 65657509b49f7bdb2a8c8d664b49fdb8b2718b28 [file] [log] [blame]
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 android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.view.Surface;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.EventListener;
import com.google.android.exoplayer2.SimpleExoPlayer;
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.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
import io.flutter.plugin.common.EventChannel;
import io.flutter.view.TextureRegistry;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
final class VideoPlayer {
private static final String FORMAT_SS = "ss";
private static final String FORMAT_DASH = "dash";
private static final String FORMAT_HLS = "hls";
private static final String FORMAT_OTHER = "other";
private SimpleExoPlayer exoPlayer;
private Surface surface;
private final TextureRegistry.SurfaceTextureEntry textureEntry;
private QueuingEventSink eventSink = new QueuingEventSink();
private final EventChannel eventChannel;
private boolean isInitialized = false;
private final VideoPlayerOptions options;
VideoPlayer(
Context context,
EventChannel eventChannel,
TextureRegistry.SurfaceTextureEntry textureEntry,
String dataSource,
String formatHint,
VideoPlayerOptions options) {
this.eventChannel = eventChannel;
this.textureEntry = textureEntry;
this.options = options;
exoPlayer = new SimpleExoPlayer.Builder(context).build();
Uri uri = Uri.parse(dataSource);
DataSource.Factory dataSourceFactory;
if (isHTTP(uri)) {
dataSourceFactory =
new DefaultHttpDataSourceFactory(
"ExoPlayer",
null,
DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
true);
} else {
dataSourceFactory = new DefaultDataSourceFactory(context, "ExoPlayer");
}
MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context);
exoPlayer.setMediaSource(mediaSource);
exoPlayer.prepare();
setupVideoPlayer(eventChannel, textureEntry);
}
private static boolean isHTTP(Uri uri) {
if (uri == null || uri.getScheme() == null) {
return false;
}
String scheme = uri.getScheme();
return scheme.equals("http") || scheme.equals("https");
}
private MediaSource buildMediaSource(
Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) {
int type;
if (formatHint == null) {
type = Util.inferContentType(uri.getLastPathSegment());
} else {
switch (formatHint) {
case FORMAT_SS:
type = C.TYPE_SS;
break;
case FORMAT_DASH:
type = C.TYPE_DASH;
break;
case FORMAT_HLS:
type = C.TYPE_HLS;
break;
case FORMAT_OTHER:
type = C.TYPE_OTHER;
break;
default:
type = -1;
break;
}
}
switch (type) {
case C.TYPE_SS:
return new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
new DefaultDataSourceFactory(context, null, mediaDataSourceFactory))
.createMediaSource(MediaItem.fromUri(uri));
case C.TYPE_DASH:
return new DashMediaSource.Factory(
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
new DefaultDataSourceFactory(context, null, mediaDataSourceFactory))
.createMediaSource(MediaItem.fromUri(uri));
case C.TYPE_HLS:
return new HlsMediaSource.Factory(mediaDataSourceFactory)
.createMediaSource(MediaItem.fromUri(uri));
case C.TYPE_OTHER:
return new ProgressiveMediaSource.Factory(mediaDataSourceFactory)
.createMediaSource(MediaItem.fromUri(uri));
default:
{
throw new IllegalStateException("Unsupported type: " + type);
}
}
}
private void setupVideoPlayer(
EventChannel eventChannel, TextureRegistry.SurfaceTextureEntry textureEntry) {
eventChannel.setStreamHandler(
new EventChannel.StreamHandler() {
@Override
public void onListen(Object o, EventChannel.EventSink sink) {
eventSink.setDelegate(sink);
}
@Override
public void onCancel(Object o) {
eventSink.setDelegate(null);
}
});
surface = new Surface(textureEntry.surfaceTexture());
exoPlayer.setVideoSurface(surface);
setAudioAttributes(exoPlayer, options.mixWithOthers);
exoPlayer.addListener(
new EventListener() {
private boolean isBuffering = false;
public void setBuffering(boolean buffering) {
if (isBuffering != buffering) {
isBuffering = buffering;
Map<String, Object> event = new HashMap<>();
event.put("event", isBuffering ? "bufferingStart" : "bufferingEnd");
eventSink.success(event);
}
}
@Override
public void onPlaybackStateChanged(final int playbackState) {
if (playbackState == Player.STATE_BUFFERING) {
setBuffering(true);
sendBufferingUpdate();
} else if (playbackState == Player.STATE_READY) {
if (!isInitialized) {
isInitialized = true;
sendInitialized();
}
} else if (playbackState == Player.STATE_ENDED) {
Map<String, Object> event = new HashMap<>();
event.put("event", "completed");
eventSink.success(event);
}
if (playbackState != Player.STATE_BUFFERING) {
setBuffering(false);
}
}
@Override
public void onPlayerError(final ExoPlaybackException error) {
setBuffering(false);
if (eventSink != null) {
eventSink.error("VideoError", "Video player had error " + error, null);
}
}
});
}
void sendBufferingUpdate() {
Map<String, Object> event = new HashMap<>();
event.put("event", "bufferingUpdate");
List<? extends Number> range = Arrays.asList(0, exoPlayer.getBufferedPosition());
// iOS supports a list of buffered ranges, so here is a list with a single range.
event.put("values", Collections.singletonList(range));
eventSink.success(event);
}
@SuppressWarnings("deprecation")
private static void setAudioAttributes(SimpleExoPlayer exoPlayer, boolean isMixMode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
exoPlayer.setAudioAttributes(
new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build(), !isMixMode);
} else {
exoPlayer.setAudioStreamType(C.STREAM_TYPE_MUSIC);
}
}
void play() {
exoPlayer.setPlayWhenReady(true);
}
void pause() {
exoPlayer.setPlayWhenReady(false);
}
void setLooping(boolean value) {
exoPlayer.setRepeatMode(value ? REPEAT_MODE_ALL : REPEAT_MODE_OFF);
}
void setVolume(double value) {
float bracketedValue = (float) Math.max(0.0, Math.min(1.0, value));
exoPlayer.setVolume(bracketedValue);
}
void setPlaybackSpeed(double value) {
// We do not need to consider pitch and skipSilence for now as we do not handle them and
// therefore never diverge from the default values.
final PlaybackParameters playbackParameters = new PlaybackParameters(((float) value));
exoPlayer.setPlaybackParameters(playbackParameters);
}
void seekTo(int location) {
exoPlayer.seekTo(location);
}
long getPosition() {
return exoPlayer.getCurrentPosition();
}
@SuppressWarnings("SuspiciousNameCombination")
private void sendInitialized() {
if (isInitialized) {
Map<String, Object> event = new HashMap<>();
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;
// Switch the width/height if video was taken in portrait mode
if (rotationDegrees == 90 || rotationDegrees == 270) {
width = exoPlayer.getVideoFormat().height;
height = exoPlayer.getVideoFormat().width;
}
event.put("width", width);
event.put("height", height);
}
eventSink.success(event);
}
}
void dispose() {
if (isInitialized) {
exoPlayer.stop();
}
textureEntry.release();
eventChannel.setStreamHandler(null);
if (surface != null) {
surface.release();
}
if (exoPlayer != null) {
exoPlayer.release();
}
}
}