2525
2626import android .content .Context ;
2727import android .net .Uri ;
28+ import android .os .Handler ;
29+ import android .os .Looper ;
2830import android .util .Log ;
2931import android .view .Surface ;
3032
3133import androidx .annotation .NonNull ;
32-
3334import androidx .media3 .common .C ;
34- import androidx .media3 .exoplayer .DefaultLoadControl ;
35- import androidx .media3 .exoplayer .ExoPlayer ;
35+ import androidx .media3 .common .MediaItem ;
3636import androidx .media3 .common .PlaybackException ;
3737import androidx .media3 .common .Player ;
38- import androidx .media3 .extractor .DefaultExtractorsFactory ;
39- import androidx .media3 .extractor .ExtractorsFactory ;
40- import androidx .media3 .exoplayer .source .MediaSource ;
41- import androidx .media3 .exoplayer .source .ProgressiveMediaSource ;
38+ import androidx .media3 .common .util .Util ;
39+ import androidx .media3 .datasource .DataSource ;
40+ import androidx .media3 .datasource .DefaultDataSourceFactory ;
41+ import androidx .media3 .datasource .RawResourceDataSource ;
42+ import androidx .media3 .exoplayer .DefaultLoadControl ;
43+ import androidx .media3 .exoplayer .ExoPlayer ;
4244import androidx .media3 .exoplayer .dash .DashMediaSource ;
4345import androidx .media3 .exoplayer .hls .HlsMediaSource ;
4446import androidx .media3 .exoplayer .smoothstreaming .SsMediaSource ;
45- import androidx .media3 .exoplayer .trackselection .AdaptiveTrackSelection ;
47+ import androidx .media3 .exoplayer .source .MediaSource ;
48+ import androidx .media3 .exoplayer .source .ProgressiveMediaSource ;
4649import androidx .media3 .exoplayer .trackselection .DefaultTrackSelector ;
47- import androidx .media3 .datasource .DataSource ;
48- import androidx .media3 .exoplayer .upstream .DefaultBandwidthMeter ;
49- import androidx .media3 .datasource .DefaultDataSourceFactory ;
50- import androidx .media3 .datasource .RawResourceDataSource ;
51- import androidx .media3 .common .util .Util ;
52- import androidx .media3 .common .MediaItem ;
50+ import androidx .media3 .extractor .DefaultExtractorsFactory ;
51+ import androidx .media3 .extractor .ExtractorsFactory ;
52+
5353import com .google .common .base .Ascii ;
5454
55+ import java .util .concurrent .Callable ;
56+ import java .util .concurrent .ExecutionException ;
57+ import java .util .concurrent .FutureTask ;
58+
5559/**
5660 * Wraps the Android ExoPlayer and can be controlled via JNI.
5761 */
@@ -72,6 +76,9 @@ private enum State {
7276 }
7377
7478 private final ExoPlayer mExoPlayer ;
79+
80+ private Handler mainThreadHandler = new Handler (Looper .getMainLooper ());
81+
7582 private float mVolume ;
7683 private final long mNativeReference ;
7784 private boolean mLoop ;
@@ -96,6 +103,7 @@ public AVPlayer(long nativeReference, Context context) {
96103
97104 @ Override
98105 public void onPlayerStateChanged (boolean playWhenReady , int playbackState ) {
106+ Log .i (TAG , "AVPlayer onPlayerStateChanged " + mPrevExoPlayerState + " => " + playbackState );
99107 // this function sometimes gets called back w/ the same playbackState.
100108 if (mPrevExoPlayerState == playbackState ) {
101109 return ;
@@ -131,6 +139,24 @@ public void onPlayerError(@NonNull PlaybackException error) {
131139 });
132140 }
133141
142+ @ FunctionalInterface
143+ public interface PlayerAction <T > {
144+ T performAction (ExoPlayer player );
145+ }
146+
147+ private <T > T runSynchronouslyOnMainThread (PlayerAction <T > action ) throws ExecutionException , InterruptedException {
148+ Callable <T > callable = () -> action .performAction (mExoPlayer );
149+ FutureTask <T > future = new FutureTask <>(callable );
150+
151+ mainThreadHandler .post (future );
152+ try {
153+ return future .get ();
154+ } catch (Exception e ) {
155+ Log .e (TAG , "AVPlayer ExoPlayer failed to run action on the main thread" , e );
156+ throw e ;
157+ }
158+ }
159+
134160 public boolean setDataSourceURL (String resourceOrURL , final Context context ) {
135161 try {
136162 reset ();
@@ -157,14 +183,15 @@ public DataSource createDataSource() {
157183
158184 MediaSource mediaSource = buildMediaSource (uri , dataSourceFactory , extractorsFactory );
159185
160- mExoPlayer .prepare (mediaSource );
161- mExoPlayer .seekToDefaultPosition ();
162- mState = State .PREPARED ;
163-
164- Log .i (TAG , "AVPlayer prepared for playback" );
165- nativeOnPrepared (mNativeReference );
166-
167- return true ;
186+ return runSynchronouslyOnMainThread (player -> {
187+ player .setMediaSource (mediaSource );
188+ player .prepare ();
189+ player .seekToDefaultPosition ();
190+ mState = State .PREPARED ;
191+ Log .i (TAG , "AVPlayer prepared for playback" );
192+ nativeOnPrepared (mNativeReference );
193+ return true ;
194+ });
168195 } catch (Exception e ) {
169196 Log .w (TAG , "AVPlayer failed to load video at URL [" + resourceOrURL + "]" , e );
170197 reset ();
@@ -208,15 +235,28 @@ private int inferContentType(String fileName) {
208235 }
209236
210237 public void setVideoSink (Surface videoSink ) {
211- mExoPlayer .setVideoSurface (videoSink );
238+ try {
239+ runSynchronouslyOnMainThread (player -> {
240+ player .setVideoSurface (videoSink );
241+ return null ;
242+ });
243+ } catch (Exception e ) {
244+ Log .e (TAG , "AVPlayer failed to set video" , e );
245+ }
212246 }
213247
214248 public void reset () {
215- mExoPlayer .stop ();
216- mExoPlayer .seekToDefaultPosition ();
217- mState = State .IDLE ;
218-
219- Log .i (TAG , "AVPlayer reset" );
249+ try {
250+ runSynchronouslyOnMainThread (player -> {
251+ player .stop ();
252+ player .seekToDefaultPosition ();
253+ mState = State .IDLE ;
254+ return null ;
255+ });
256+ Log .i (TAG , "AVPlayer reset" );
257+ } catch (Exception e ) {
258+ Log .e (TAG , "AVPlayer failed reset" , e );
259+ }
220260 }
221261
222262 public void destroy () {
@@ -228,17 +268,31 @@ public void destroy() {
228268
229269 public void play () {
230270 if (mState == State .PREPARED || mState == State .PAUSED ) {
231- mExoPlayer .setPlayWhenReady (true );
232- mState = State .STARTED ;
271+ try {
272+ runSynchronouslyOnMainThread (player -> {
273+ player .setPlayWhenReady (true );
274+ mState = State .STARTED ;
275+ return null ;
276+ });
277+ } catch (Exception e ) {
278+ Log .e (TAG , "AVPlayer failed to play video" , e );
279+ }
233280 } else {
234281 Log .w (TAG , "AVPlayer could not play video in " + mState .toString () + " state" );
235282 }
236283 }
237284
238285 public void pause () {
239286 if (mState == State .STARTED ) {
240- mExoPlayer .setPlayWhenReady (false );
241- mState = State .PAUSED ;
287+ try {
288+ runSynchronouslyOnMainThread (player -> {
289+ player .setPlayWhenReady (false );
290+ mState = State .PAUSED ;
291+ return null ;
292+ });
293+ } catch (Exception e ) {
294+ Log .e (TAG , "AVPlayer failed to pause video" , e );
295+ }
242296 } else {
243297 Log .w (TAG , "AVPlayer could not pause video in " + mState .toString () + " state" );
244298 }
@@ -250,24 +304,46 @@ public boolean isPaused() {
250304
251305 public void setLoop (boolean loop ) {
252306 mLoop = loop ;
253- if (mExoPlayer .getPlaybackState () == ExoPlayer .STATE_ENDED ) {
254- mExoPlayer .seekToDefaultPosition ();
307+ try {
308+ runSynchronouslyOnMainThread (player -> {
309+ if (player .getPlaybackState () == ExoPlayer .STATE_ENDED ) {
310+ player .seekToDefaultPosition ();
311+ }
312+ return null ;
313+ });
314+ } catch (Exception e ) {
315+ Log .e (TAG , "AVPlayer failed to set loop" , e );
255316 }
256317 }
257318
258319 public void setVolume (float volume ) {
259320 mVolume = volume ;
260- if (!mMute ) {
261- mExoPlayer .setVolume (mVolume );
321+ if (mMute ) {
322+ return ;
323+ }
324+ try {
325+ runSynchronouslyOnMainThread (player -> {
326+ player .setVolume (mVolume );
327+ return null ;
328+ });
329+ } catch (Exception e ) {
330+ Log .e (TAG , "AVPlayer failed to set volume" , e );
262331 }
263332 }
264333
265334 public void setMuted (boolean muted ) {
266335 mMute = muted ;
267- if (muted ) {
268- mExoPlayer .setVolume (0 );
269- } else {
270- mExoPlayer .setVolume (mVolume );
336+ try {
337+ runSynchronouslyOnMainThread (player -> {
338+ if (muted ) {
339+ player .setVolume (0 );
340+ } else {
341+ player .setVolume (mVolume );
342+ }
343+ return null ;
344+ });
345+ } catch (Exception e ) {
346+ Log .e (TAG , "AVPlayer failed to set muted " + muted , e );
271347 }
272348 }
273349
@@ -276,8 +352,14 @@ public void seekToTime(float seconds) {
276352 Log .w (TAG , "AVPlayer could not seek while in IDLE state" );
277353 return ;
278354 }
279-
280- mExoPlayer .seekTo ((long ) (seconds * 1000 ));
355+ try {
356+ runSynchronouslyOnMainThread (player -> {
357+ player .seekTo ((long ) (seconds * 1000 ));
358+ return null ;
359+ });
360+ } catch (Exception e ) {
361+ Log .e (TAG , "AVPlayer failed to seek" , e );
362+ }
281363 }
282364
283365 public float getCurrentTimeInSeconds () {
@@ -286,18 +368,33 @@ public float getCurrentTimeInSeconds() {
286368 return 0 ;
287369 }
288370
289- return mExoPlayer .getCurrentPosition () / 1000.0f ;
371+ long currentPosition = 0 ;
372+ try {
373+ currentPosition = runSynchronouslyOnMainThread (player -> player .getCurrentPosition ());
374+ } catch (Exception e ) {
375+ Log .e (TAG , "AVPlayer could not get video current position" , e );
376+ }
377+
378+ return currentPosition / 1000.0f ;
290379 }
291380
292381 public float getVideoDurationInSeconds () {
293382 if (mState == State .IDLE ) {
294383 Log .w (TAG , "AVPlayer could not get video duration in IDLE state" );
295384 return 0 ;
296- } else if (mExoPlayer .getDuration () == C .TIME_UNSET ) {
385+ }
386+
387+ long duration = 0 ;
388+ try {
389+ duration = runSynchronouslyOnMainThread (player -> player .getDuration ());
390+ } catch (Exception e ) {
391+ Log .e (TAG , "AVPlayer could not get video duration" , e );
392+ }
393+ if (duration == C .TIME_UNSET ) {
297394 return 0 ;
298395 }
299396
300- return mExoPlayer . getDuration () / 1000.0f ;
397+ return duration / 1000.0f ;
301398 }
302399
303400 /**
0 commit comments