kopia lustrzana https://github.com/ryukoposting/Signal-Android
Implement ExoPlayerPool for better reuse and performance.
rodzic
a5c51ff801
commit
5c1b57e4ba
|
@ -439,7 +439,7 @@ public class ThumbnailView extends FrameLayout {
|
|||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.transition(withCrossFade()), fit);
|
||||
|
||||
boolean doNotShowMissingThumbnailImage = GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInConversation() == 0;
|
||||
boolean doNotShowMissingThumbnailImage = Build.VERSION.SDK_INT < 23;
|
||||
|
||||
if (slide.isInProgress() || doNotShowMissingThumbnailImage) return request;
|
||||
else return request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture));
|
||||
|
|
|
@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.util.EarlyMessageCache;
|
|||
import org.thoughtcrime.securesms.util.FrameRateTracker;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.IasKeyStore;
|
||||
import org.thoughtcrime.securesms.video.exo.SimpleExoPlayerPool;
|
||||
import org.thoughtcrime.securesms.video.exo.GiphyMp4Cache;
|
||||
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
|
@ -99,7 +100,8 @@ public class ApplicationDependencies {
|
|||
private static volatile TextSecureSessionStore sessionStore;
|
||||
private static volatile TextSecurePreKeyStore preKeyStore;
|
||||
private static volatile SignalSenderKeyStore senderKeyStore;
|
||||
private static volatile GiphyMp4Cache giphyMp4Cache;
|
||||
private static volatile GiphyMp4Cache giphyMp4Cache;
|
||||
private static volatile SimpleExoPlayerPool exoPlayerPool;
|
||||
|
||||
@MainThread
|
||||
public static void init(@NonNull Application application, @NonNull Provider provider) {
|
||||
|
@ -564,6 +566,17 @@ public class ApplicationDependencies {
|
|||
return giphyMp4Cache;
|
||||
}
|
||||
|
||||
public static @NonNull SimpleExoPlayerPool getExoPlayerPool() {
|
||||
if (exoPlayerPool == null) {
|
||||
synchronized (LOCK) {
|
||||
if (exoPlayerPool == null) {
|
||||
exoPlayerPool = provider.provideExoPlayerPool();
|
||||
}
|
||||
}
|
||||
}
|
||||
return exoPlayerPool;
|
||||
}
|
||||
|
||||
public interface Provider {
|
||||
@NonNull GroupsV2Operations provideGroupsV2Operations();
|
||||
@NonNull SignalServiceAccountManager provideSignalServiceAccountManager();
|
||||
|
@ -597,5 +610,6 @@ public class ApplicationDependencies {
|
|||
@NonNull TextSecurePreKeyStore providePreKeyStore();
|
||||
@NonNull SignalSenderKeyStore provideSenderKeyStore();
|
||||
@NonNull GiphyMp4Cache provideGiphyMp4Cache();
|
||||
@NonNull SimpleExoPlayerPool provideExoPlayerPool();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ import org.thoughtcrime.securesms.util.EarlyMessageCache;
|
|||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.FrameRateTracker;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.video.exo.SimpleExoPlayerPool;
|
||||
import org.thoughtcrime.securesms.video.exo.GiphyMp4Cache;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
|
@ -289,6 +290,11 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
|||
return new GiphyMp4Cache(ByteUnit.MEGABYTES.toBytes(16));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull SimpleExoPlayerPool provideExoPlayerPool() {
|
||||
return new SimpleExoPlayerPool(context);
|
||||
}
|
||||
|
||||
private @NonNull WebSocketFactory provideWebSocketFactory(@NonNull SignalWebSocketHealthMonitor healthMonitor) {
|
||||
return new WebSocketFactory() {
|
||||
@Override
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.net.ContentProxySelector;
|
||||
import org.thoughtcrime.securesms.video.exo.SignalDataSource;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
/**
|
||||
* Provider which creates ExoPlayer instances for displaying Giphy content.
|
||||
*/
|
||||
final class GiphyMp4ExoPlayerProvider implements DefaultLifecycleObserver {
|
||||
|
||||
private final Context context;
|
||||
private final OkHttpClient okHttpClient = ApplicationDependencies.getOkHttpClient().newBuilder().proxySelector(new ContentProxySelector()).build();
|
||||
private final DataSource.Factory dataSourceFactory = new SignalDataSource.Factory(ApplicationDependencies.getApplication(), okHttpClient, null);
|
||||
private final MediaSourceFactory mediaSourceFactory = new ProgressiveMediaSource.Factory(dataSourceFactory);
|
||||
|
||||
GiphyMp4ExoPlayerProvider(@NonNull Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@MainThread final @NonNull ExoPlayer create() {
|
||||
SimpleExoPlayer exoPlayer = new SimpleExoPlayer.Builder(context)
|
||||
.setMediaSourceFactory(mediaSourceFactory)
|
||||
.build();
|
||||
|
||||
exoPlayer.setRepeatMode(Player.REPEAT_MODE_ALL);
|
||||
exoPlayer.setVolume(0f);
|
||||
|
||||
return exoPlayer;
|
||||
}
|
||||
}
|
|
@ -1,11 +1,5 @@
|
|||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.DeviceProperties;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
@ -17,10 +11,6 @@ import java.util.concurrent.TimeUnit;
|
|||
*/
|
||||
public final class GiphyMp4PlaybackPolicy {
|
||||
|
||||
private static final int MAXIMUM_SUPPORTED_PLAYBACK_PRE_23 = 6;
|
||||
private static final int MAXIMUM_SUPPORTED_PLAYBACK_PRE_23_LOW_MEM = 3;
|
||||
private static final float SEARCH_RESULT_RATIO = 0.75f;
|
||||
|
||||
private GiphyMp4PlaybackPolicy() { }
|
||||
|
||||
public static boolean sendAsMp4() {
|
||||
|
@ -39,35 +29,11 @@ public final class GiphyMp4PlaybackPolicy {
|
|||
return TimeUnit.SECONDS.toMillis(8);
|
||||
}
|
||||
|
||||
public static int maxSimultaneousPlaybackInConversation() {
|
||||
return Build.VERSION.SDK_INT >= 23 ? maxSimultaneousPlaybackWithRatio(1f - SEARCH_RESULT_RATIO) : 0;
|
||||
}
|
||||
|
||||
public static int maxSimultaneousPlaybackInSearchResults() {
|
||||
return maxSimultaneousPlaybackWithRatio(SEARCH_RESULT_RATIO);
|
||||
return 12;
|
||||
}
|
||||
|
||||
private static int maxSimultaneousPlaybackWithRatio(float ratio) {
|
||||
int maxInstances = 0;
|
||||
|
||||
try {
|
||||
MediaCodecInfo info = MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_H264, false, false);
|
||||
|
||||
if (info != null && info.getMaxSupportedInstances() > 0) {
|
||||
maxInstances = (int) (info.getMaxSupportedInstances() * ratio);
|
||||
}
|
||||
|
||||
} catch (MediaCodecUtil.DecoderQueryException ignored) {
|
||||
}
|
||||
|
||||
if (maxInstances > 0) {
|
||||
return maxInstances;
|
||||
}
|
||||
|
||||
if (DeviceProperties.isLowMemoryDevice(ApplicationDependencies.getApplication())) {
|
||||
return (int) (MAXIMUM_SUPPORTED_PLAYBACK_PRE_23_LOW_MEM * ratio);
|
||||
} else {
|
||||
return (int) (MAXIMUM_SUPPORTED_PLAYBACK_PRE_23 * ratio);
|
||||
}
|
||||
public static int maxSimultaneousPlaybackInConversation() {
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,15 +8,20 @@ import android.widget.FrameLayout;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.video.exo.ExoPlayerKt;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -24,7 +29,9 @@ import java.util.List;
|
|||
/**
|
||||
* Object which holds on to an injected video player.
|
||||
*/
|
||||
public final class GiphyMp4ProjectionPlayerHolder implements Player.Listener {
|
||||
public final class GiphyMp4ProjectionPlayerHolder implements Player.Listener, DefaultLifecycleObserver {
|
||||
private static final String TAG = Log.tag(GiphyMp4ProjectionPlayerHolder.class);
|
||||
|
||||
private final FrameLayout container;
|
||||
private final GiphyMp4VideoPlayer player;
|
||||
|
||||
|
@ -45,6 +52,20 @@ public final class GiphyMp4ProjectionPlayerHolder implements Player.Listener {
|
|||
this.mediaItem = mediaItem;
|
||||
this.policyEnforcer = policyEnforcer;
|
||||
|
||||
if (player.getExoPlayer() == null) {
|
||||
SimpleExoPlayer fromPool = ApplicationDependencies.getExoPlayerPool().get();
|
||||
|
||||
if (fromPool == null) {
|
||||
Log.i(TAG, "Could not get exoplayer from pool.");
|
||||
return;
|
||||
} else {
|
||||
ExoPlayerKt.configureForGifPlayback(fromPool);
|
||||
fromPool.addListener(this);
|
||||
}
|
||||
|
||||
player.setExoPlayer(fromPool);
|
||||
}
|
||||
|
||||
player.setVideoItem(mediaItem);
|
||||
player.play();
|
||||
}
|
||||
|
@ -52,7 +73,14 @@ public final class GiphyMp4ProjectionPlayerHolder implements Player.Listener {
|
|||
public void clearMedia() {
|
||||
this.mediaItem = null;
|
||||
this.policyEnforcer = null;
|
||||
player.stop();
|
||||
|
||||
SimpleExoPlayer exoPlayer = player.getExoPlayer();
|
||||
if (exoPlayer != null) {
|
||||
player.stop();
|
||||
player.setExoPlayer(null);
|
||||
exoPlayer.removeListener(this);
|
||||
ApplicationDependencies.getExoPlayerPool().pool(exoPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable MediaItem getMediaItem() {
|
||||
|
@ -98,25 +126,46 @@ public final class GiphyMp4ProjectionPlayerHolder implements Player.Listener {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume(@NonNull LifecycleOwner owner) {
|
||||
if (mediaItem != null) {
|
||||
SimpleExoPlayer fromPool = ApplicationDependencies.getExoPlayerPool().get();
|
||||
if (fromPool != null) {
|
||||
ExoPlayerKt.configureForGifPlayback(fromPool);
|
||||
fromPool.addListener(this);
|
||||
player.setExoPlayer(fromPool);
|
||||
player.setVideoItem(mediaItem);
|
||||
player.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause(@NonNull LifecycleOwner owner) {
|
||||
if (player.getExoPlayer() != null) {
|
||||
player.getExoPlayer().stop();
|
||||
player.getExoPlayer().clearMediaItems();
|
||||
player.getExoPlayer().removeListener(this);
|
||||
ApplicationDependencies.getExoPlayerPool().pool(player.getExoPlayer());
|
||||
player.setExoPlayer(null);
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull List<GiphyMp4ProjectionPlayerHolder> injectVideoViews(@NonNull Context context,
|
||||
@NonNull Lifecycle lifecycle,
|
||||
@NonNull ViewGroup viewGroup,
|
||||
int nPlayers)
|
||||
{
|
||||
List<GiphyMp4ProjectionPlayerHolder> holders = new ArrayList<>(nPlayers);
|
||||
GiphyMp4ExoPlayerProvider playerProvider = new GiphyMp4ExoPlayerProvider(context);
|
||||
List<GiphyMp4ProjectionPlayerHolder> holders = new ArrayList<>(nPlayers);
|
||||
|
||||
for (int i = 0; i < nPlayers; i++) {
|
||||
FrameLayout container = (FrameLayout) LayoutInflater.from(context)
|
||||
.inflate(R.layout.giphy_mp4_player, viewGroup, false);
|
||||
GiphyMp4VideoPlayer player = container.findViewById(R.id.video_player);
|
||||
ExoPlayer exoPlayer = playerProvider.create();
|
||||
GiphyMp4ProjectionPlayerHolder holder = new GiphyMp4ProjectionPlayerHolder(container, player);
|
||||
GiphyMp4VideoPlayer player = container.findViewById(R.id.video_player);
|
||||
GiphyMp4ProjectionPlayerHolder holder = new GiphyMp4ProjectionPlayerHolder(container, player);
|
||||
|
||||
lifecycle.addObserver(player);
|
||||
player.setExoPlayer(exoPlayer);
|
||||
lifecycle.addObserver(holder);
|
||||
player.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FILL);
|
||||
exoPlayer.addListener(holder);
|
||||
|
||||
holders.add(holder);
|
||||
viewGroup.addView(container);
|
||||
|
|
|
@ -13,13 +13,16 @@ import androidx.lifecycle.LifecycleOwner;
|
|||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.CornerMask;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.video.exo.ExoPlayerKt;
|
||||
|
||||
/**
|
||||
* Video Player class specifically created for the GiphyMp4Fragment.
|
||||
|
@ -29,9 +32,10 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
|
|||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(GiphyMp4VideoPlayer.class);
|
||||
|
||||
private final PlayerView exoView;
|
||||
private ExoPlayer exoPlayer;
|
||||
private CornerMask cornerMask;
|
||||
private final PlayerView exoView;
|
||||
private SimpleExoPlayer exoPlayer;
|
||||
private CornerMask cornerMask;
|
||||
private MediaItem mediaItem;
|
||||
|
||||
public GiphyMp4VideoPlayer(Context context) {
|
||||
this(context, null);
|
||||
|
@ -64,16 +68,25 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
|
|||
}
|
||||
}
|
||||
|
||||
void setExoPlayer(@NonNull ExoPlayer exoPlayer) {
|
||||
@Nullable SimpleExoPlayer getExoPlayer() {
|
||||
return exoPlayer;
|
||||
}
|
||||
|
||||
void setExoPlayer(@Nullable SimpleExoPlayer exoPlayer) {
|
||||
exoView.setPlayer(exoPlayer);
|
||||
this.exoPlayer = exoPlayer;
|
||||
}
|
||||
|
||||
int getPlaybackState() {
|
||||
return exoPlayer.getPlaybackState();
|
||||
if (exoPlayer != null) {
|
||||
return exoPlayer.getPlaybackState();
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
void setVideoItem(@NonNull MediaItem mediaItem) {
|
||||
this.mediaItem = mediaItem;
|
||||
exoPlayer.setMediaItem(mediaItem);
|
||||
exoPlayer.prepare();
|
||||
}
|
||||
|
@ -98,6 +111,7 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
|
|||
if (exoPlayer != null) {
|
||||
exoPlayer.stop();
|
||||
exoPlayer.clearMediaItems();
|
||||
mediaItem = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,11 +126,4 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
|
|||
void setResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode) {
|
||||
exoView.setResizeMode(resizeMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(@NonNull LifecycleOwner owner) {
|
||||
if (exoPlayer != null) {
|
||||
exoPlayer.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.thoughtcrime.securesms.mms;
|
|||
import android.content.Context;
|
||||
import android.content.res.Resources.Theme;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -55,7 +56,7 @@ public class VideoSlide extends Slide {
|
|||
|
||||
@Override
|
||||
public boolean hasPlayOverlay() {
|
||||
return !(isVideoGif() && GiphyMp4PlaybackPolicy.autoplay()) || GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInConversation() == 0;
|
||||
return !(isVideoGif() && GiphyMp4PlaybackPolicy.autoplay()) || Build.VERSION.SDK_INT < 23;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -30,18 +30,14 @@ import com.google.android.exoplayer2.MediaItem;
|
|||
import com.google.android.exoplayer2.PlaybackException;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.video.exo.SignalDataSource;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -61,6 +57,8 @@ public class VideoPlayer extends FrameLayout {
|
|||
private PlayerCallback playerCallback;
|
||||
private boolean clipped;
|
||||
private long clippedStartUs;
|
||||
private ExoPlayerListener exoPlayerListener;
|
||||
private Player.Listener playerListener;
|
||||
|
||||
public VideoPlayer(Context context) {
|
||||
this(context, null);
|
||||
|
@ -78,51 +76,49 @@ public class VideoPlayer extends FrameLayout {
|
|||
this.exoView = findViewById(R.id.video_view);
|
||||
this.exoControls = new PlayerControlView(getContext());
|
||||
this.exoControls.setShowTimeoutMs(-1);
|
||||
|
||||
this.exoPlayerListener = new ExoPlayerListener(this, window, playerStateCallback, playerPositionDiscontinuityCallback);
|
||||
this.playerListener = new Player.Listener() {
|
||||
@Override
|
||||
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
|
||||
onPlaybackStateChanged(playWhenReady, exoPlayer.getPlaybackState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStateChanged(int playbackState) {
|
||||
onPlaybackStateChanged(exoPlayer.getPlayWhenReady(), playbackState);
|
||||
}
|
||||
|
||||
private void onPlaybackStateChanged(boolean playWhenReady, int playbackState) {
|
||||
if (playerCallback != null) {
|
||||
switch (playbackState) {
|
||||
case Player.STATE_READY:
|
||||
if (playWhenReady) playerCallback.onPlaying();
|
||||
break;
|
||||
case Player.STATE_ENDED:
|
||||
playerCallback.onStopped();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(@NonNull PlaybackException error) {
|
||||
Log.w(TAG, "A player error occurred", error);
|
||||
if (playerCallback != null) {
|
||||
playerCallback.onError();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private MediaItem mediaItem;
|
||||
|
||||
public void setVideoSource(@NonNull VideoSlide videoSource, boolean autoplay) {
|
||||
Context context = getContext();
|
||||
|
||||
if (exoPlayer == null) {
|
||||
DataSource.Factory attachmentDataSourceFactory = new SignalDataSource.Factory(context, null, null);
|
||||
MediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(attachmentDataSourceFactory);
|
||||
|
||||
exoPlayer = new SimpleExoPlayer.Builder(context).setMediaSourceFactory(mediaSourceFactory).build();
|
||||
exoPlayer.addListener(new ExoPlayerListener(this, window, playerStateCallback, playerPositionDiscontinuityCallback));
|
||||
exoPlayer.addListener(new Player.Listener() {
|
||||
@Override
|
||||
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
|
||||
onPlaybackStateChanged(playWhenReady, exoPlayer.getPlaybackState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStateChanged(int playbackState) {
|
||||
onPlaybackStateChanged(exoPlayer.getPlayWhenReady(), playbackState);
|
||||
}
|
||||
|
||||
private void onPlaybackStateChanged(boolean playWhenReady, int playbackState) {
|
||||
if (playerCallback != null) {
|
||||
switch (playbackState) {
|
||||
case Player.STATE_READY:
|
||||
if (playWhenReady) playerCallback.onPlaying();
|
||||
break;
|
||||
case Player.STATE_ENDED:
|
||||
playerCallback.onStopped();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(@NonNull PlaybackException error) {
|
||||
Log.w(TAG, "A player error occurred", error);
|
||||
if (playerCallback != null) {
|
||||
playerCallback.onError();
|
||||
}
|
||||
}
|
||||
});
|
||||
exoPlayer = ApplicationDependencies.getExoPlayerPool().require();
|
||||
exoPlayer.addListener(exoPlayerListener);
|
||||
exoPlayer.addListener(playerListener);
|
||||
exoView.setPlayer(exoPlayer);
|
||||
exoControls.setPlayer(exoPlayer);
|
||||
}
|
||||
|
@ -159,7 +155,16 @@ public class VideoPlayer extends FrameLayout {
|
|||
|
||||
public void cleanup() {
|
||||
if (this.exoPlayer != null) {
|
||||
this.exoPlayer.release();
|
||||
exoPlayer.stop();
|
||||
exoPlayer.clearMediaItems();
|
||||
|
||||
exoView.setPlayer(null);
|
||||
exoControls.setPlayer(null);
|
||||
|
||||
exoPlayer.removeListener(playerListener);
|
||||
exoPlayer.removeListener(exoPlayerListener);
|
||||
|
||||
ApplicationDependencies.getExoPlayerPool().pool(exoPlayer);
|
||||
this.exoPlayer = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package org.thoughtcrime.securesms.video.exo
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlayer
|
||||
import com.google.android.exoplayer2.Player
|
||||
|
||||
fun ExoPlayer.configureForGifPlayback() {
|
||||
repeatMode = Player.REPEAT_MODE_ALL
|
||||
volume = 0f
|
||||
}
|
||||
|
||||
fun ExoPlayer.configureForVideoPlayback() {
|
||||
repeatMode = Player.REPEAT_MODE_OFF
|
||||
volume = 1f
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
package org.thoughtcrime.securesms.video.exo
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.MainThread
|
||||
import com.google.android.exoplayer2.ExoPlayer
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||
import com.google.android.exoplayer2.upstream.DataSource
|
||||
import com.google.android.exoplayer2.util.MimeTypes
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.net.ContentProxySelector
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver
|
||||
import org.thoughtcrime.securesms.util.DeviceProperties
|
||||
|
||||
/**
|
||||
* ExoPlayerPool concrete instance which helps to manage a pool of SimpleExoPlayer objects
|
||||
*/
|
||||
class SimpleExoPlayerPool(context: Context) : ExoPlayerPool<SimpleExoPlayer>(MAXIMUM_RESERVED_PLAYERS) {
|
||||
private val context: Context = context.applicationContext
|
||||
private val okHttpClient = ApplicationDependencies.getOkHttpClient().newBuilder().proxySelector(ContentProxySelector()).build()
|
||||
private val dataSourceFactory: DataSource.Factory = SignalDataSource.Factory(ApplicationDependencies.getApplication(), okHttpClient, null)
|
||||
private val mediaSourceFactory: MediaSourceFactory = ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
|
||||
init {
|
||||
ApplicationDependencies.getAppForegroundObserver().addListener(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to get the max number of instances that can be played back on the screen at a time, based off of
|
||||
* the device API level and decoder info.
|
||||
*/
|
||||
override fun getMaxSimultaneousPlayback(): Int {
|
||||
val maxInstances = try {
|
||||
val info = MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_H264, false, false)
|
||||
if (info != null && info.maxSupportedInstances > 0) {
|
||||
info.maxSupportedInstances
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} catch (ignored: DecoderQueryException) {
|
||||
0
|
||||
}
|
||||
|
||||
if (maxInstances > 0) {
|
||||
return maxInstances
|
||||
}
|
||||
|
||||
return if (DeviceProperties.isLowMemoryDevice(ApplicationDependencies.getApplication())) {
|
||||
MAXIMUM_SUPPORTED_PLAYBACK_PRE_23_LOW_MEM
|
||||
} else {
|
||||
MAXIMUM_SUPPORTED_PLAYBACK_PRE_23
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
override fun createPlayer(): SimpleExoPlayer {
|
||||
return SimpleExoPlayer.Builder(context)
|
||||
.setMediaSourceFactory(mediaSourceFactory)
|
||||
.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAXIMUM_RESERVED_PLAYERS = 1
|
||||
private const val MAXIMUM_SUPPORTED_PLAYBACK_PRE_23 = 6
|
||||
private const val MAXIMUM_SUPPORTED_PLAYBACK_PRE_23_LOW_MEM = 3
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ExoPlayer pool which allows for the quick and efficient reuse of ExoPlayer instances instead of creating and destroying them
|
||||
* as needed. This class will, if added as an AppForegroundObserver.Listener, evict players when the app is backgrounded to try to
|
||||
* make sure it is a good citizen on the device.
|
||||
*
|
||||
* This class also supports reserving a number of players, which count against its total specified by getMaxSimultaneousPlayback. These
|
||||
* players will be returned first when a player is requested via require.
|
||||
*/
|
||||
abstract class ExoPlayerPool<T : ExoPlayer>(
|
||||
private val maximumReservedPlayers: Int,
|
||||
) : AppForegroundObserver.Listener {
|
||||
|
||||
private val pool: MutableMap<T, PoolState> = mutableMapOf()
|
||||
|
||||
/**
|
||||
* Try to get a player from the non-reserved pool.
|
||||
*
|
||||
* @return A player if one is available, otherwise null
|
||||
*/
|
||||
@MainThread
|
||||
fun get(): T? {
|
||||
return get(allowReserved = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a player, preferring reserved players.
|
||||
*
|
||||
* @return A non-null player instance. If one is not available, an exception is thrown.
|
||||
* @throws IllegalStateException if no player is available.
|
||||
*/
|
||||
@MainThread
|
||||
fun require(): T {
|
||||
return checkNotNull(get(allowReserved = true)) { "Required exoPlayer could not be acquired! :: ${poolStats()}" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a player to the pool. If the player is not from the pool, an exception is thrown.
|
||||
*
|
||||
* @throws IllegalArgumentException if the player passed is not in the pool
|
||||
*/
|
||||
@MainThread
|
||||
fun pool(exoPlayer: T) {
|
||||
val poolState = pool[exoPlayer]
|
||||
if (poolState != null) {
|
||||
pool[exoPlayer] = poolState.copy(available = true)
|
||||
} else {
|
||||
throw IllegalArgumentException("Tried to return unknown ExoPlayer to pool :: ${poolStats()}")
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun get(allowReserved: Boolean): T? {
|
||||
val player = findAvailablePlayer(allowReserved)
|
||||
return if (player == null && pool.size < getMaximumAllowed(allowReserved)) {
|
||||
val newPlayer = createPlayer()
|
||||
val poolState = createPoolStateForNewEntry(allowReserved)
|
||||
pool[newPlayer] = poolState
|
||||
newPlayer
|
||||
} else if (player != null) {
|
||||
val poolState = pool[player]!!.copy(available = false)
|
||||
pool[player] = poolState
|
||||
player
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMaximumAllowed(allowReserved: Boolean): Int {
|
||||
return if (allowReserved) getMaxSimultaneousPlayback() else getMaxSimultaneousPlayback() - maximumReservedPlayers
|
||||
}
|
||||
|
||||
private fun createPoolStateForNewEntry(allowReserved: Boolean): PoolState {
|
||||
return if (allowReserved && pool.none { (_, v) -> v.reserved }) {
|
||||
PoolState(available = false, reserved = true)
|
||||
} else {
|
||||
PoolState(available = false, reserved = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findAvailablePlayer(allowReserved: Boolean): T? {
|
||||
return if (allowReserved) {
|
||||
findFirstReservedAndAvailablePlayer() ?: findFirstUnreservedAndAvailablePlayer()
|
||||
} else {
|
||||
findFirstUnreservedAndAvailablePlayer()
|
||||
}
|
||||
}
|
||||
|
||||
private fun findFirstReservedAndAvailablePlayer(): T? {
|
||||
return pool.filter { (_, v) -> v.reservedAndAvailable }.keys.firstOrNull()
|
||||
}
|
||||
|
||||
private fun findFirstUnreservedAndAvailablePlayer(): T? {
|
||||
return pool.filter { (_, v) -> v.unreservedAndAvailable }.keys.firstOrNull()
|
||||
}
|
||||
|
||||
protected abstract fun createPlayer(): T
|
||||
|
||||
@MainThread
|
||||
override fun onBackground() {
|
||||
val playersToRelease = pool.filter { (_, v) -> v.available }.keys
|
||||
pool -= playersToRelease
|
||||
|
||||
playersToRelease.forEach { it.release() }
|
||||
}
|
||||
|
||||
private fun poolStats(): String {
|
||||
val poolStats = PoolStats(
|
||||
created = pool.size,
|
||||
maxUnreserved = getMaxSimultaneousPlayback() - maximumReservedPlayers,
|
||||
maxReserved = maximumReservedPlayers
|
||||
)
|
||||
|
||||
pool.values.fold(poolStats) { acc, state ->
|
||||
acc.copy(
|
||||
unreservedAndAvailable = acc.unreservedAndAvailable + if (state.unreservedAndAvailable) 1 else 0,
|
||||
reservedAndAvailable = acc.reservedAndAvailable + if (state.reservedAndAvailable) 1 else 0,
|
||||
unreserved = acc.unreserved + if (!state.reserved) 1 else 0,
|
||||
reserved = acc.reserved + if (state.reserved) 1 else 0
|
||||
)
|
||||
}
|
||||
|
||||
return poolStats.toString()
|
||||
}
|
||||
|
||||
protected abstract fun getMaxSimultaneousPlayback(): Int
|
||||
|
||||
private data class PoolStats(
|
||||
val created: Int = 0,
|
||||
val maxUnreserved: Int = 0,
|
||||
val maxReserved: Int = 0,
|
||||
val unreservedAndAvailable: Int = 0,
|
||||
val reservedAndAvailable: Int = 0,
|
||||
val unreserved: Int = 0,
|
||||
val reserved: Int = 0
|
||||
)
|
||||
|
||||
private data class PoolState(
|
||||
val available: Boolean,
|
||||
val reserved: Boolean
|
||||
) {
|
||||
val unreservedAndAvailable = available && !reserved
|
||||
val reservedAndAvailable = available && reserved
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package org.thoughtcrime.securesms.video.exo
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlayer
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.mockito.Mockito.mock
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
class ExoPlayerPoolTest {
|
||||
|
||||
@Test
|
||||
fun `Given an empty pool, when I require a player, then I expect a player`() {
|
||||
// GIVEN
|
||||
val testSubject = createTestSubject(1, 1)
|
||||
|
||||
// WHEN
|
||||
val player = testSubject.require()
|
||||
|
||||
// THEN
|
||||
assertNotNull(player)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun `Given a pool without available players, when I require a player, then I expect an exception`() {
|
||||
// GIVEN
|
||||
val testSubject = createTestSubject(1, 0)
|
||||
|
||||
// WHEN
|
||||
testSubject.require()
|
||||
|
||||
// THEN
|
||||
fail("Expected an IllegalStateException")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a pool that allows 10 unreserved items, when I ask for 20, then I expect 10 items and 10 nulls`() {
|
||||
// GIVEN
|
||||
val testSubject = createTestSubject(0, 10)
|
||||
|
||||
// WHEN
|
||||
val players = (1..10).map { testSubject.get() }
|
||||
val nulls = (1..10).map { testSubject.get() }
|
||||
|
||||
// THEN
|
||||
assertTrue(players.all { it != null })
|
||||
assertTrue(nulls.all { it == null })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a pool that allows 10 items and has all items checked out, when I return then check them all out again, then I expect 10 non null players`() {
|
||||
// GIVEN
|
||||
val testSubject = createTestSubject(0, 10)
|
||||
val players = (1..10).map { testSubject.get() }
|
||||
|
||||
// WHEN
|
||||
players.filterNotNull().forEach { testSubject.pool(it) }
|
||||
val morePlayers = (1..10).map { testSubject.get() }
|
||||
|
||||
assertTrue(morePlayers.all { it != null })
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun `Given an ExoPlayer not in the pool, when I pool it, then I expect an IllegalArgumentException`() {
|
||||
// GIVEN
|
||||
val player = mock(ExoPlayer::class.java)
|
||||
val pool = createTestSubject(1, 10)
|
||||
|
||||
// WHEN
|
||||
pool.pool(player)
|
||||
|
||||
// THEN
|
||||
fail("Expected an IllegalArgumentException to be thrown")
|
||||
}
|
||||
|
||||
private fun createTestSubject(
|
||||
maximumReservedPlayers: Int,
|
||||
maximumSimultaneousPlayback: Int
|
||||
): ExoPlayerPool<ExoPlayer> {
|
||||
return object : ExoPlayerPool<ExoPlayer>(maximumReservedPlayers) {
|
||||
override fun createPlayer(): ExoPlayer {
|
||||
return mock(ExoPlayer::class.java)
|
||||
}
|
||||
|
||||
override fun getMaxSimultaneousPlayback(): Int {
|
||||
return maximumSimultaneousPlayback
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Ładowanie…
Reference in New Issue