mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 17:29:32 +01:00
Implement ExoPlayerPool for better reuse and performance.
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user