blob: b65373ae0fe9dc7893cdc94db550d48aaf16818e [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
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.media.AudioManager;
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.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DefaultEventListener;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
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.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelector;
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.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.Registrar;
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;
public class VideoPlayerPlugin implements MethodCallHandler {
private static class VideoPlayer {
private SimpleExoPlayer exoPlayer;
private Surface surface;
private final TextureRegistry.SurfaceTextureEntry textureEntry;
private EventChannel.EventSink eventSink;
private final EventChannel eventChannel;
private boolean isInitialized = false;
VideoPlayer(
Context context,
EventChannel eventChannel,
TextureRegistry.SurfaceTextureEntry textureEntry,
String dataSource,
Result result) {
this.eventChannel = eventChannel;
this.textureEntry = textureEntry;
TrackSelector trackSelector = new DefaultTrackSelector();
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
Uri uri = Uri.parse(dataSource);
DataSource.Factory dataSourceFactory;
if (uri.getScheme().equals("asset") || uri.getScheme().equals("file")) {
dataSourceFactory = new DefaultDataSourceFactory(context, "ExoPlayer");
} else {
dataSourceFactory =
new DefaultHttpDataSourceFactory(
"ExoPlayer",
null,
DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
true);
}
MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory);
exoPlayer.prepare(mediaSource);
setupVideoPlayer(eventChannel, textureEntry, result);
}
private MediaSource buildMediaSource(Uri uri, DataSource.Factory mediaDataSourceFactory) {
int type = Util.inferContentType(uri.getLastPathSegment());
switch (type) {
case C.TYPE_SS:
return new SsMediaSource(
uri, null, new DefaultSsChunkSource.Factory(mediaDataSourceFactory), null, null);
case C.TYPE_DASH:
return new DashMediaSource(
uri, null, new DefaultDashChunkSource.Factory(mediaDataSourceFactory), null, null);
case C.TYPE_HLS:
return new HlsMediaSource(uri, mediaDataSourceFactory, null, null);
case C.TYPE_OTHER:
return new ExtractorMediaSource(
uri, mediaDataSourceFactory, new DefaultExtractorsFactory(), null, null);
default:
{
throw new IllegalStateException("Unsupported type: " + type);
}
}
}
private void setupVideoPlayer(
EventChannel eventChannel,
TextureRegistry.SurfaceTextureEntry textureEntry,
Result result) {
eventChannel.setStreamHandler(
new EventChannel.StreamHandler() {
@Override
public void onListen(Object o, EventChannel.EventSink sink) {
eventSink = sink;
}
@Override
public void onCancel(Object o) {
eventSink = null;
}
});
surface = new Surface(textureEntry.surfaceTexture());
exoPlayer.setVideoSurface(surface);
setAudioAttributes(exoPlayer);
exoPlayer.addListener(
new DefaultEventListener() {
@Override
public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
super.onPlayerStateChanged(playWhenReady, playbackState);
if (playbackState == Player.STATE_BUFFERING) {
if (eventSink != null) {
Map<String, Object> event = new HashMap<>();
event.put("event", "bufferingUpdate");
List<Integer> range = Arrays.asList(0, exoPlayer.getBufferedPercentage());
// 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);
}
} else if (playbackState == Player.STATE_READY && !isInitialized) {
isInitialized = true;
sendInitialized();
}
}
@Override
public void onPlayerError(final ExoPlaybackException error) {
super.onPlayerError(error);
if (eventSink != null) {
eventSink.error("VideoError", "Video player had error " + error, null);
}
}
});
Map<String, Object> reply = new HashMap<>();
reply.put("textureId", textureEntry.id());
result.success(reply);
}
@SuppressWarnings("deprecation")
private static void setAudioAttributes(SimpleExoPlayer exoPlayer) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
exoPlayer.setAudioAttributes(
new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build());
} else {
exoPlayer.setAudioStreamType(AudioManager.STREAM_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 seekTo(int location) {
exoPlayer.seekTo(location);
}
long getPosition() {
return exoPlayer.getCurrentPosition();
}
private void sendInitialized() {
if (isInitialized && eventSink != null) {
Map<String, Object> event = new HashMap<>();
event.put("event", "initialized");
event.put("duration", exoPlayer.getDuration());
if (exoPlayer.getVideoFormat() != null) {
event.put("width", exoPlayer.getVideoFormat().width);
event.put("height", exoPlayer.getVideoFormat().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();
}
}
}
public static void registerWith(Registrar registrar) {
final MethodChannel channel =
new MethodChannel(registrar.messenger(), "flutter.io/videoPlayer");
channel.setMethodCallHandler(new VideoPlayerPlugin(registrar));
}
private VideoPlayerPlugin(Registrar registrar) {
this.registrar = registrar;
this.videoPlayers = new HashMap<>();
}
private final Map<Long, VideoPlayer> videoPlayers;
private final Registrar registrar;
@Override
public void onMethodCall(MethodCall call, Result result) {
TextureRegistry textures = registrar.textures();
if (textures == null) {
result.error("no_activity", "video_player plugin requires a foreground activity", null);
return;
}
switch (call.method) {
case "init":
for (VideoPlayer player : videoPlayers.values()) {
player.dispose();
}
videoPlayers.clear();
break;
case "create":
{
TextureRegistry.SurfaceTextureEntry handle = textures.createSurfaceTexture();
EventChannel eventChannel =
new EventChannel(
registrar.messenger(), "flutter.io/videoPlayer/videoEvents" + handle.id());
VideoPlayer player;
if (call.argument("asset") != null) {
String assetLookupKey;
if (call.argument("package") != null) {
assetLookupKey =
registrar.lookupKeyForAsset(
(String) call.argument("asset"), (String) call.argument("package"));
} else {
assetLookupKey = registrar.lookupKeyForAsset((String) call.argument("asset"));
}
player =
new VideoPlayer(
registrar.context(),
eventChannel,
handle,
"asset:///" + assetLookupKey,
result);
videoPlayers.put(handle.id(), player);
} else {
player =
new VideoPlayer(
registrar.context(),
eventChannel,
handle,
(String) call.argument("uri"),
result);
videoPlayers.put(handle.id(), player);
}
break;
}
default:
{
long textureId = ((Number) call.argument("textureId")).longValue();
VideoPlayer player = videoPlayers.get(textureId);
if (player == null) {
result.error(
"Unknown textureId",
"No video player associated with texture id " + textureId,
null);
return;
}
onMethodCall(call, result, textureId, player);
break;
}
}
}
private void onMethodCall(MethodCall call, Result result, long textureId, VideoPlayer player) {
switch (call.method) {
case "setLooping":
player.setLooping((Boolean) call.argument("looping"));
result.success(null);
break;
case "setVolume":
player.setVolume((Double) call.argument("volume"));
result.success(null);
break;
case "play":
player.play();
result.success(null);
break;
case "pause":
player.pause();
result.success(null);
break;
case "seekTo":
int location = ((Number) call.argument("location")).intValue();
player.seekTo(location);
result.success(null);
break;
case "position":
result.success(player.getPosition());
break;
case "dispose":
player.dispose();
videoPlayers.remove(textureId);
result.success(null);
break;
default:
result.notImplemented();
break;
}
}
}