Implement ExoPlayerPool for better reuse and performance.

fork-5.53.8
Alex Hart 2021-09-24 13:10:48 -03:00 zatwierdzone przez GitHub
rodzic a5c51ff801
commit 5c1b57e4ba
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
12 zmienionych plików z 478 dodań i 154 usunięć

Wyświetl plik

@ -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));

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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();
}
}
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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
}
}

Wyświetl plik

@ -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
}
}
}
}