Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix video crashing on Android because of unresolved ExoPlayer breaking changes #154

Merged
merged 3 commits into from
May 31, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 167 additions & 49 deletions android/sharedCode/src/main/java/com/viro/core/internal/AVPlayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,37 @@

import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.Surface;

import androidx.annotation.NonNull;

import androidx.media3.common.C;
import androidx.media3.exoplayer.DefaultLoadControl;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.extractor.DefaultExtractorsFactory;
import androidx.media3.extractor.ExtractorsFactory;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.ProgressiveMediaSource;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DefaultDataSourceFactory;
import androidx.media3.datasource.RawResourceDataSource;
import androidx.media3.exoplayer.DefaultLoadControl;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.dash.DashMediaSource;
import androidx.media3.exoplayer.hls.HlsMediaSource;
import androidx.media3.exoplayer.smoothstreaming.SsMediaSource;
import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.ProgressiveMediaSource;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.datasource.DataSource;
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter;
import androidx.media3.datasource.DefaultDataSourceFactory;
import androidx.media3.datasource.RawResourceDataSource;
import androidx.media3.common.util.Util;
import androidx.media3.common.MediaItem;
import androidx.media3.extractor.DefaultExtractorsFactory;
import androidx.media3.extractor.ExtractorsFactory;

import com.google.common.base.Ascii;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
* Wraps the Android ExoPlayer and can be controlled via JNI.
*/
Expand All @@ -72,6 +76,9 @@ private enum State {
}

private final ExoPlayer mExoPlayer;

private Handler mainThreadHandler = new Handler(Looper.getMainLooper());

private float mVolume;
private final long mNativeReference;
private boolean mLoop;
Expand All @@ -96,6 +103,7 @@ public AVPlayer(long nativeReference, Context context) {

@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
Log.i(TAG, "AVPlayer onPlayerStateChanged " + mPrevExoPlayerState + " => " + playbackState);
// this function sometimes gets called back w/ the same playbackState.
if (mPrevExoPlayerState == playbackState) {
return;
Expand Down Expand Up @@ -131,6 +139,35 @@ public void onPlayerError(@NonNull PlaybackException error) {
});
}

@FunctionalInterface
public interface PlayerAction<T> {
T performAction(ExoPlayer player);
}

private <T> T runSynchronouslyOnMainThread(PlayerAction<T> action) throws ExecutionException, InterruptedException {
return runSynchronouslyOnMainThread(action, true);
}

private <T> T runSynchronouslyOnMainThread(PlayerAction<T> action, boolean waitForResult) throws ExecutionException, InterruptedException {
if (Looper.myLooper() == Looper.getMainLooper()) {
return action.performAction(mExoPlayer);
}

Callable<T> callable = () -> action.performAction(mExoPlayer);
FutureTask<T> future = new FutureTask<>(callable);

mainThreadHandler.post(future);

if (!waitForResult) return null;

try {
return future.get();
} catch (Exception e) {
Log.e(TAG, "AVPlayer ExoPlayer failed to run action on the main thread", e);
throw e;
}
}

public boolean setDataSourceURL(String resourceOrURL, final Context context) {
try {
reset();
Expand All @@ -157,14 +194,15 @@ public DataSource createDataSource() {

MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, extractorsFactory);

mExoPlayer.prepare(mediaSource);
mExoPlayer.seekToDefaultPosition();
mState = State.PREPARED;

Log.i(TAG, "AVPlayer prepared for playback");
nativeOnPrepared(mNativeReference);

return true;
return runSynchronouslyOnMainThread(player -> {
player.setMediaSource(mediaSource);
player.prepare();
player.seekToDefaultPosition();
mState = State.PREPARED;
Log.i(TAG, "AVPlayer prepared for playback");
nativeOnPrepared(mNativeReference);
return true;
});
} catch (Exception e) {
Log.w(TAG, "AVPlayer failed to load video at URL [" + resourceOrURL + "]", e);
reset();
Expand Down Expand Up @@ -208,37 +246,74 @@ private int inferContentType(String fileName) {
}

public void setVideoSink(Surface videoSink) {
mExoPlayer.setVideoSurface(videoSink);
try {
runSynchronouslyOnMainThread(player -> {
player.setVideoSurface(videoSink);
return null;
});
} catch (Exception e) {
Log.e(TAG, "AVPlayer failed to set video", e);
}
}

public void reset() {
mExoPlayer.stop();
mExoPlayer.seekToDefaultPosition();
mState = State.IDLE;

Log.i(TAG, "AVPlayer reset");
try {
runSynchronouslyOnMainThread(player -> {
player.stop();
player.seekToDefaultPosition();
mState = State.IDLE;
return null;
});
Log.i(TAG, "AVPlayer reset");
} catch (Exception e) {
Log.e(TAG, "AVPlayer failed reset", e);
}
}

public void destroy() {
reset();
mExoPlayer.release();

Log.i(TAG, "AVPlayer destroyed");
try {
runSynchronouslyOnMainThread(player -> {
player.stop();
player.seekToDefaultPosition();
player.release();
mState = State.IDLE;
return null;
},
false // don't wait for the result to prevent ANR issue
);
Log.i(TAG, "AVPlayer destroyed");
} catch (Exception e) {
Log.e(TAG, "AVPlayer destroy failed", e);
}
}

public void play() {
if (mState == State.PREPARED || mState == State.PAUSED) {
mExoPlayer.setPlayWhenReady(true);
mState = State.STARTED;
try {
runSynchronouslyOnMainThread(player -> {
player.setPlayWhenReady(true);
mState = State.STARTED;
return null;
});
} catch (Exception e) {
Log.e(TAG, "AVPlayer failed to play video", e);
}
} else {
Log.w(TAG, "AVPlayer could not play video in " + mState.toString() + " state");
}
}

public void pause() {
if (mState == State.STARTED) {
mExoPlayer.setPlayWhenReady(false);
mState = State.PAUSED;
try {
runSynchronouslyOnMainThread(player -> {
player.setPlayWhenReady(false);
mState = State.PAUSED;
return null;
});
} catch (Exception e) {
Log.e(TAG, "AVPlayer failed to pause video", e);
}
} else {
Log.w(TAG, "AVPlayer could not pause video in " + mState.toString() + " state");
}
Expand All @@ -250,24 +325,46 @@ public boolean isPaused() {

public void setLoop(boolean loop) {
mLoop = loop;
if (mExoPlayer.getPlaybackState() == ExoPlayer.STATE_ENDED) {
mExoPlayer.seekToDefaultPosition();
try {
runSynchronouslyOnMainThread(player -> {
if (player.getPlaybackState() == ExoPlayer.STATE_ENDED) {
player.seekToDefaultPosition();
}
return null;
});
} catch (Exception e) {
Log.e(TAG, "AVPlayer failed to set loop", e);
}
}

public void setVolume(float volume) {
mVolume = volume;
if (!mMute) {
mExoPlayer.setVolume(mVolume);
if (mMute) {
return;
}
try {
runSynchronouslyOnMainThread(player -> {
player.setVolume(mVolume);
return null;
});
} catch (Exception e) {
Log.e(TAG, "AVPlayer failed to set volume", e);
}
}

public void setMuted(boolean muted) {
mMute = muted;
if (muted) {
mExoPlayer.setVolume(0);
} else {
mExoPlayer.setVolume(mVolume);
try {
runSynchronouslyOnMainThread(player -> {
if (muted) {
player.setVolume(0);
} else {
player.setVolume(mVolume);
}
return null;
});
} catch (Exception e) {
Log.e(TAG, "AVPlayer failed to set muted " + muted, e);
}
}

Expand All @@ -276,8 +373,14 @@ public void seekToTime(float seconds) {
Log.w(TAG, "AVPlayer could not seek while in IDLE state");
return;
}

mExoPlayer.seekTo((long) (seconds * 1000));
try {
runSynchronouslyOnMainThread(player -> {
player.seekTo((long) (seconds * 1000));
return null;
});
} catch (Exception e) {
Log.e(TAG, "AVPlayer failed to seek", e);
}
}

public float getCurrentTimeInSeconds() {
Expand All @@ -286,18 +389,33 @@ public float getCurrentTimeInSeconds() {
return 0;
}

return mExoPlayer.getCurrentPosition() / 1000.0f;
long currentPosition = 0;
try {
currentPosition = runSynchronouslyOnMainThread(player -> player.getCurrentPosition());
} catch (Exception e) {
Log.e(TAG, "AVPlayer could not get video current position", e);
}

return currentPosition / 1000.0f;
}

public float getVideoDurationInSeconds() {
if (mState == State.IDLE) {
Log.w(TAG, "AVPlayer could not get video duration in IDLE state");
return 0;
} else if (mExoPlayer.getDuration() == C.TIME_UNSET) {
}

long duration = 0;
try {
duration = runSynchronouslyOnMainThread(player -> player.getDuration());
} catch (Exception e) {
Log.e(TAG, "AVPlayer could not get video duration", e);
}
if (duration == C.TIME_UNSET) {
return 0;
}

return mExoPlayer.getDuration() / 1000.0f;
return duration / 1000.0f;
}

/**
Expand Down
Loading