mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-22 01:35:10 +01:00
Move the VideoEditorFragment to the media-send feature module.
This commit is contained in:
committed by
Greyson Parrelli
parent
987f92245d
commit
83cb48d119
@@ -0,0 +1,524 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.signal.video;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioFocusRequest;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.PlaybackException;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.Tracks;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.exoplayer.analytics.AnalyticsListener;
|
||||
import androidx.media3.exoplayer.source.ClippingMediaSource;
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
|
||||
import androidx.media3.exoplayer.source.LoadEventInfo;
|
||||
import androidx.media3.exoplayer.source.MediaLoadData;
|
||||
import androidx.media3.exoplayer.source.MediaSource;
|
||||
import androidx.media3.ui.AspectRatioFrameLayout;
|
||||
import androidx.media3.ui.LegacyPlayerControlView;
|
||||
import androidx.media3.ui.PlayerView;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.incrementalmac.InvalidMacException;
|
||||
import org.signal.video.exo.ExoPlayerPool;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
public class VideoPlayer extends FrameLayout {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(VideoPlayer.class);
|
||||
|
||||
private final PlayerView exoView;
|
||||
private final View progressBar;
|
||||
private final DefaultMediaSourceFactory mediaSourceFactory;
|
||||
|
||||
private ExoPlayer exoPlayer;
|
||||
private ExoPlayerPool<ExoPlayer> exoPlayerPool;
|
||||
private LegacyPlayerControlView exoControls;
|
||||
private Window window;
|
||||
private PlayerStateCallback playerStateCallback;
|
||||
private PlayerPositionDiscontinuityCallback playerPositionDiscontinuityCallback;
|
||||
private PlayerCallback playerCallback;
|
||||
private boolean clipped;
|
||||
private long clippedStartUs;
|
||||
private ExoPlayerListener exoPlayerListener;
|
||||
private Player.Listener playerListener;
|
||||
private AnalyticsListener analyticsListener;
|
||||
private boolean muted;
|
||||
private AudioFocusRequest audioFocusRequest;
|
||||
private boolean requestAudioFocus = true;
|
||||
|
||||
public VideoPlayer(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public VideoPlayer(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public VideoPlayer(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.VideoPlayer);
|
||||
int videPlayerLayout = typedArray.getResourceId(R.styleable.VideoPlayer_playerLayoutId, R.layout.video_player);
|
||||
|
||||
typedArray.recycle();
|
||||
inflate(context, videPlayerLayout, this);
|
||||
|
||||
this.mediaSourceFactory = new DefaultMediaSourceFactory(context);
|
||||
|
||||
this.exoView = findViewById(R.id.video_view);
|
||||
this.progressBar = findViewById(R.id.progress_bar);
|
||||
this.exoControls = createPlayerControls(getContext());
|
||||
|
||||
final AudioManager audioManager = ContextCompat.getSystemService(context, AudioManager.class);
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
|
||||
.setAudioAttributes(
|
||||
new AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.build()
|
||||
)
|
||||
.setOnAudioFocusChangeListener(focusChange -> {
|
||||
|
||||
})
|
||||
.build();
|
||||
} else {
|
||||
audioFocusRequest = null;
|
||||
}
|
||||
|
||||
this.exoPlayerListener = new ExoPlayerListener();
|
||||
this.analyticsListener = new AnalyticsListener() {
|
||||
@Override
|
||||
public void onLoadError(EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) {
|
||||
if (error instanceof InvalidMacException) {
|
||||
Log.w(TAG, "Bad incremental mac!", error);
|
||||
playerCallback.onError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.playerListener = new Player.Listener() {
|
||||
|
||||
@Override
|
||||
public void onIsPlayingChanged(boolean isPlaying) {
|
||||
if (!isPlaying && exoPlayer.getCurrentPosition() >= exoPlayer.getDuration()) {
|
||||
exoPlayer.seekTo(0);
|
||||
exoPlayer.setPlayWhenReady(false);
|
||||
}
|
||||
|
||||
if (audioManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 26 && audioFocusRequest != null) {
|
||||
if (isPlaying) {
|
||||
if (requestAudioFocus) {
|
||||
audioManager.requestAudioFocus(audioFocusRequest);
|
||||
}
|
||||
} else {
|
||||
audioManager.abandonAudioFocusRequest(audioFocusRequest);
|
||||
}
|
||||
} else {
|
||||
if (isPlaying) {
|
||||
if (requestAudioFocus) {
|
||||
audioManager.requestAudioFocus(
|
||||
focusChange -> {
|
||||
// Do nothing
|
||||
},
|
||||
AudioManager.STREAM_MUSIC,
|
||||
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
|
||||
);
|
||||
}
|
||||
} else {
|
||||
audioManager.abandonAudioFocus(
|
||||
focusChange -> {
|
||||
// Do nothing
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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 (progressBar != null) {
|
||||
if (playbackState == Player.STATE_BUFFERING) {
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
if (playerCallback != null) {
|
||||
switch (playbackState) {
|
||||
case Player.STATE_READY:
|
||||
playerCallback.onReady();
|
||||
if (playWhenReady) {
|
||||
playerCallback.onPlaying();
|
||||
} else {
|
||||
playerCallback.onStopped();
|
||||
}
|
||||
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(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private LegacyPlayerControlView createPlayerControls(Context context) {
|
||||
final LegacyPlayerControlView playerControlView = new LegacyPlayerControlView(context);
|
||||
playerControlView.setShowTimeoutMs(-1);
|
||||
playerControlView.setShowNextButton(false);
|
||||
playerControlView.setShowPreviousButton(false);
|
||||
return playerControlView;
|
||||
}
|
||||
|
||||
private MediaItem mediaItem;
|
||||
|
||||
public void setExoPlayerPool(ExoPlayerPool<ExoPlayer> exoPlayerPool) {
|
||||
this.exoPlayerPool = exoPlayerPool;
|
||||
}
|
||||
|
||||
public void setVideoSource(@NonNull Uri uri, boolean autoplay, String poolTag) {
|
||||
setVideoSource(uri, autoplay, poolTag, 0, 0);
|
||||
}
|
||||
|
||||
public void setVideoSource(@NonNull Uri uri, boolean autoplay, String poolTag, long clipStartMs, long clipEndMs) {
|
||||
if (exoPlayer == null) {
|
||||
exoPlayer = exoPlayerPool.require(poolTag);
|
||||
exoPlayer.addListener(exoPlayerListener);
|
||||
exoPlayer.addListener(playerListener);
|
||||
exoPlayer.addAnalyticsListener(analyticsListener);
|
||||
exoView.setPlayer(exoPlayer);
|
||||
exoControls.setPlayer(exoPlayer);
|
||||
if (muted) {
|
||||
mute();
|
||||
}
|
||||
}
|
||||
|
||||
mediaItem = MediaItem.fromUri(Objects.requireNonNull(uri)).buildUpon()
|
||||
.setClippingConfiguration(getClippingConfiguration(clipStartMs, clipEndMs))
|
||||
.build();
|
||||
|
||||
exoPlayer.setMediaItem(mediaItem);
|
||||
exoPlayer.prepare();
|
||||
exoPlayer.setPlayWhenReady(autoplay);
|
||||
}
|
||||
|
||||
public void mute() {
|
||||
this.muted = true;
|
||||
if (exoPlayer != null) {
|
||||
exoPlayer.setVolume(0f);
|
||||
}
|
||||
}
|
||||
|
||||
public void unmute() {
|
||||
this.muted = false;
|
||||
if (exoPlayer != null) {
|
||||
exoPlayer.setVolume(1f);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasAudioTrack() {
|
||||
if (exoPlayer != null) {
|
||||
Tracks tracks = exoPlayer.getCurrentTracks();
|
||||
return tracks.containsType(C.TRACK_TYPE_AUDIO);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return exoPlayer != null;
|
||||
}
|
||||
|
||||
public void setResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode) {
|
||||
exoView.setResizeMode(resizeMode);
|
||||
}
|
||||
|
||||
public boolean isPlaying() {
|
||||
if (this.exoPlayer != null) {
|
||||
return this.exoPlayer.isPlaying();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
if (this.exoPlayer != null) {
|
||||
this.exoPlayer.setPlayWhenReady(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void hideControls() {
|
||||
if (this.exoView != null) {
|
||||
this.exoView.hideController();
|
||||
}
|
||||
}
|
||||
|
||||
public void setKeepContentOnPlayerReset(boolean keepContentOnPlayerReset) {
|
||||
if (this.exoView != null) {
|
||||
this.exoView.setKeepContentOnPlayerReset(keepContentOnPlayerReset);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnClickListener(@Nullable OnClickListener l) {
|
||||
if (this.exoView != null) {
|
||||
this.exoView.setClickable(false);
|
||||
}
|
||||
|
||||
super.setOnClickListener(l);
|
||||
}
|
||||
|
||||
public @Nullable LegacyPlayerControlView getControlView() {
|
||||
return this.exoControls;
|
||||
}
|
||||
|
||||
public void setControlView(LegacyPlayerControlView controller) {
|
||||
exoControls = controller;
|
||||
exoControls.setPlayer(exoPlayer);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (this.exoPlayer != null) {
|
||||
exoPlayer.stop();
|
||||
exoPlayer.clearMediaItems();
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
stop();
|
||||
|
||||
if (this.exoPlayer != null) {
|
||||
exoView.setPlayer(null);
|
||||
|
||||
if (exoPlayer.equals(exoControls.getPlayer())) {
|
||||
exoControls.setPlayer(null);
|
||||
}
|
||||
|
||||
exoPlayer.removeListener(playerListener);
|
||||
exoPlayer.removeListener(exoPlayerListener);
|
||||
|
||||
exoPlayerPool.pool(exoPlayer);
|
||||
this.exoPlayer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void loopForever() {
|
||||
if (this.exoPlayer != null) {
|
||||
exoPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
|
||||
}
|
||||
}
|
||||
|
||||
public long getDuration() {
|
||||
if (this.exoPlayer != null) {
|
||||
return this.exoPlayer.getDuration();
|
||||
}
|
||||
return 0L;
|
||||
}
|
||||
|
||||
public long getPlaybackPosition() {
|
||||
if (this.exoPlayer != null) {
|
||||
return this.exoPlayer.getCurrentPosition();
|
||||
}
|
||||
return 0L;
|
||||
}
|
||||
|
||||
/**
|
||||
* After calling {@link #setPlaybackPosition}, the underlying {@link Player} resets the current position to 0.
|
||||
* We manually store the offset of where we clipped to, and add that here.
|
||||
*
|
||||
* @return the current playback position, rounded to the nearest millisecond
|
||||
*/
|
||||
public long getTruePlaybackPosition() {
|
||||
if (this.exoPlayer != null) {
|
||||
return this.exoPlayer.getCurrentPosition() + Math.round(clippedStartUs / 1000.0);
|
||||
}
|
||||
return -1L;
|
||||
}
|
||||
|
||||
public void setPlaybackPosition(long positionMs) {
|
||||
if (this.exoPlayer != null) {
|
||||
this.exoPlayer.seekTo(positionMs);
|
||||
}
|
||||
}
|
||||
|
||||
public void clip(long fromUs, long toUs, boolean playWhenReady) {
|
||||
if (this.exoPlayer != null && mediaItem != null) {
|
||||
MediaSource mediaItemSource = mediaSourceFactory.createMediaSource(mediaItem);
|
||||
ClippingMediaSource clippedSource = new ClippingMediaSource(mediaItemSource, fromUs, toUs);
|
||||
|
||||
exoPlayer.setMediaSource(clippedSource);
|
||||
exoPlayer.prepare();
|
||||
exoPlayer.setPlayWhenReady(playWhenReady);
|
||||
clipped = true;
|
||||
clippedStartUs = fromUs;
|
||||
}
|
||||
}
|
||||
|
||||
public void removeClip(boolean playWhenReady) {
|
||||
if (exoPlayer != null && mediaItem != null) {
|
||||
if (clipped) {
|
||||
exoPlayer.setMediaItem(mediaItem);
|
||||
exoPlayer.prepare();
|
||||
clipped = false;
|
||||
clippedStartUs = 0;
|
||||
}
|
||||
exoPlayer.setPlayWhenReady(playWhenReady);
|
||||
}
|
||||
}
|
||||
|
||||
public void setWindow(@Nullable Window window) {
|
||||
this.window = window;
|
||||
}
|
||||
|
||||
public void setPlayerStateCallbacks(@Nullable PlayerStateCallback playerStateCallback) {
|
||||
this.playerStateCallback = playerStateCallback;
|
||||
}
|
||||
|
||||
public void setPlayerCallback(PlayerCallback playerCallback) {
|
||||
this.playerCallback = playerCallback;
|
||||
}
|
||||
|
||||
public void setPlayerPositionDiscontinuityCallback(@NonNull PlayerPositionDiscontinuityCallback playerPositionDiscontinuityCallback) {
|
||||
this.playerPositionDiscontinuityCallback = playerPositionDiscontinuityCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes a paused video, or restarts if at end of video.
|
||||
*/
|
||||
public void play() {
|
||||
if (exoPlayer != null) {
|
||||
exoPlayer.setPlayWhenReady(true);
|
||||
if (exoPlayer.getCurrentPosition() >= exoPlayer.getDuration()) {
|
||||
exoPlayer.seekTo(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void disableAudioFocus() {
|
||||
requestAudioFocus = false;
|
||||
}
|
||||
|
||||
private @NonNull MediaItem.ClippingConfiguration getClippingConfiguration(long startMs, long endMs) {
|
||||
return startMs != endMs ? new MediaItem.ClippingConfiguration.Builder()
|
||||
.setStartPositionMs(startMs)
|
||||
.setEndPositionMs(endMs)
|
||||
.build()
|
||||
: MediaItem.ClippingConfiguration.UNSET;
|
||||
}
|
||||
|
||||
private class ExoPlayerListener implements 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) {
|
||||
switch (playbackState) {
|
||||
case Player.STATE_IDLE:
|
||||
case Player.STATE_BUFFERING:
|
||||
case Player.STATE_ENDED:
|
||||
if (window != null) {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
break;
|
||||
case Player.STATE_READY:
|
||||
if (window != null) {
|
||||
if (playWhenReady) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
}
|
||||
notifyPlayerReady();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition,
|
||||
@NonNull Player.PositionInfo newPosition,
|
||||
int reason)
|
||||
{
|
||||
if (playerPositionDiscontinuityCallback != null) {
|
||||
playerPositionDiscontinuityCallback.onPositionDiscontinuity(VideoPlayer.this, reason);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyPlayerReady() {
|
||||
if (playerStateCallback != null) playerStateCallback.onPlayerReady();
|
||||
}
|
||||
}
|
||||
|
||||
public interface PlayerStateCallback {
|
||||
void onPlayerReady();
|
||||
}
|
||||
|
||||
public interface PlayerPositionDiscontinuityCallback {
|
||||
void onPositionDiscontinuity(@NonNull VideoPlayer player, int reason);
|
||||
}
|
||||
|
||||
public interface PlayerCallback {
|
||||
|
||||
default void onReady() {}
|
||||
|
||||
void onPlaying();
|
||||
|
||||
void onStopped();
|
||||
|
||||
void onError(Exception e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.video.exo
|
||||
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
|
||||
fun ExoPlayer.configureForVideoPlayback() {
|
||||
repeatMode = Player.REPEAT_MODE_OFF
|
||||
volume = 1f
|
||||
trackSelectionParameters = trackSelectionParameters.buildUpon()
|
||||
.setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, false)
|
||||
.build()
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.video.exo
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.DataSpec
|
||||
import androidx.media3.datasource.TransferListener
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import org.signal.core.util.AppForegroundObserver
|
||||
import org.signal.core.util.logging.Log
|
||||
import kotlin.collections.iterator
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ExoPlayerPool::class.java)
|
||||
}
|
||||
|
||||
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(tag: String): T? {
|
||||
return get(allowReserved = false, tag = tag)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(tag: String): T {
|
||||
return checkNotNull(get(allowReserved = true, tag = tag)) { "Required exoPlayer could not be acquired for $tag! :: ${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) {
|
||||
exoPlayer.stop()
|
||||
exoPlayer.clearMediaItems()
|
||||
|
||||
pool[exoPlayer] = poolState.copy(available = true, tag = null)
|
||||
} else {
|
||||
throw IllegalArgumentException("Tried to return unknown ExoPlayer to pool :: ${poolStats()}")
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun get(allowReserved: Boolean, tag: String): T? {
|
||||
val player = findAvailablePlayer(allowReserved)
|
||||
val toReturn = if (player == null && pool.size < getMaximumAllowed(allowReserved)) {
|
||||
val newPlayer = createPlayer()
|
||||
val poolState = createPoolStateForNewEntry(allowReserved, tag)
|
||||
pool[newPlayer] = poolState
|
||||
newPlayer
|
||||
} else if (player != null) {
|
||||
val poolState = pool[player]!!.copy(available = false, tag = tag)
|
||||
pool[player] = poolState
|
||||
player
|
||||
} else {
|
||||
Log.d(TAG, "Failed to get an ExoPlayer instance for tag: $tag :: ${poolStats()}")
|
||||
null
|
||||
}
|
||||
|
||||
return toReturn?.apply {
|
||||
configureForVideoPlayback()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMaximumAllowed(allowReserved: Boolean): Int {
|
||||
return if (allowReserved) getMaxSimultaneousPlayback() else getMaxSimultaneousPlayback() - maximumReservedPlayers
|
||||
}
|
||||
|
||||
private fun createPoolStateForNewEntry(allowReserved: Boolean, tag: String?): PoolState {
|
||||
return if (allowReserved && pool.none { (_, v) -> v.reserved }) {
|
||||
PoolState(available = false, reserved = true, tag = tag)
|
||||
} else {
|
||||
PoolState(available = false, reserved = false, tag = tag)
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
for ((player, state) in pool) {
|
||||
if (!state.available && player.playWhenReady) {
|
||||
Log.w(TAG, "Force-stopping orphaned playing player on background. Owner: ${state.tag}")
|
||||
player.stop()
|
||||
player.clearMediaItems()
|
||||
}
|
||||
}
|
||||
|
||||
val playersToRelease = pool.filter { (_, v) -> v.available }.keys
|
||||
pool -= playersToRelease
|
||||
|
||||
playersToRelease.forEach { it.release() }
|
||||
}
|
||||
|
||||
private fun poolStats(): String {
|
||||
return getPoolStats().toString()
|
||||
}
|
||||
|
||||
fun getPoolStats(): PoolStats {
|
||||
val poolStats = PoolStats(
|
||||
created = pool.size,
|
||||
maxUnreserved = getMaxSimultaneousPlayback() - maximumReservedPlayers,
|
||||
maxReserved = maximumReservedPlayers,
|
||||
owners = emptyList()
|
||||
)
|
||||
|
||||
return pool.values.fold(poolStats) { acc, state ->
|
||||
Log.d(TAG, "$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,
|
||||
owners = if (!state.available) acc.owners + OwnershipInfo(state.tag!!, state.reserved) else acc.owners
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
object DataSourceTransferListener : TransferListener {
|
||||
private val TAG = Log.tag(DataSourceTransferListener::class)
|
||||
override fun onTransferInitializing(source: DataSource, dataSpec: DataSpec, isNetwork: Boolean) {
|
||||
Log.d(TAG, "onTransferInitializing() for ${source.uri}")
|
||||
}
|
||||
|
||||
override fun onTransferStart(source: DataSource, dataSpec: DataSpec, isNetwork: Boolean) {
|
||||
Log.d(TAG, "onTransferStart() for ${source.uri}")
|
||||
}
|
||||
|
||||
override fun onBytesTransferred(source: DataSource, dataSpec: DataSpec, isNetwork: Boolean, bytesTransferred: Int) {}
|
||||
|
||||
override fun onTransferEnd(source: DataSource, dataSpec: DataSpec, isNetwork: Boolean) {
|
||||
Log.d(TAG, "onTransferEnd() for ${source.uri}")
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun getMaxSimultaneousPlayback(): Int
|
||||
|
||||
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,
|
||||
val owners: List<OwnershipInfo>
|
||||
)
|
||||
|
||||
data class OwnershipInfo(
|
||||
val tag: String,
|
||||
val isReserved: Boolean
|
||||
)
|
||||
|
||||
private data class PoolState(
|
||||
val available: Boolean,
|
||||
val reserved: Boolean,
|
||||
val tag: String?
|
||||
) {
|
||||
val unreservedAndAvailable = available && !reserved
|
||||
val reservedAndAvailable = available && reserved
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.video.interfaces
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import okio.IOException
|
||||
|
||||
interface MediaInputFactory {
|
||||
@Throws(IOException::class)
|
||||
fun createForUri(context: Context, uri: Uri): MediaInput
|
||||
}
|
||||
+6
-7
@@ -18,12 +18,11 @@ import org.thoughtcrime.securesms.video.videoconverter.utils.MediaCodecCompat;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
@RequiresApi(api = 23)
|
||||
final class VideoThumbnailsExtractor {
|
||||
public final class VideoThumbnailsExtractor {
|
||||
|
||||
private static final String TAG = Log.tag(VideoThumbnailsExtractor.class);
|
||||
|
||||
interface Callback {
|
||||
public interface Callback {
|
||||
void durationKnown(long duration);
|
||||
|
||||
boolean publishProgress(int index, Bitmap thumbnail);
|
||||
@@ -31,10 +30,10 @@ final class VideoThumbnailsExtractor {
|
||||
void failed();
|
||||
}
|
||||
|
||||
static void extractThumbnails(final @NonNull MediaInput input,
|
||||
final int thumbnailCount,
|
||||
final int thumbnailResolution,
|
||||
final @NonNull Callback callback)
|
||||
public static void extractThumbnails(final @NonNull MediaInput input,
|
||||
final int thumbnailCount,
|
||||
final int thumbnailResolution,
|
||||
final @NonNull Callback callback)
|
||||
{
|
||||
MediaExtractor extractor = null;
|
||||
MediaCodec decoder = null;
|
||||
|
||||
Reference in New Issue
Block a user