{"record":{"id":"796ae67bd9f27820","repo":"google/ExoPlayer","slug":"player-is-accessed-on-the-wrong-thread-current-th","errorCode":null,"errorMessage":"Player is accessed on the wrong thread.\nCurrent thread: '%s'\nExpected thread: '%s'\nSee https://developer.android.com/guide/topics/media/issues/player-accessed-on-wrong-thread","messagePattern":"Player is accessed on the wrong thread\\.\nCurrent thread: '(.+?)'\nExpected thread: '(.+?)'\nSee https://developer\\.android\\.com/guide/topics/media/issues/player-accessed-on-wrong-thread","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"library/common/src/main/java/com/google/android/exoplayer2/SimpleBasePlayer.java","lineNumber":3517,"sourceCode":"      listeners.queueEvent(\n          Player.EVENT_AVAILABLE_COMMANDS_CHANGED,\n          listener -> listener.onAvailableCommandsChanged(newState.availableCommands));\n    }\n    listeners.flushEvents();\n  }\n\n  @EnsuresNonNull(\"state\")\n  private void verifyApplicationThreadAndInitState() {\n    if (Thread.currentThread() != applicationLooper.getThread()) {\n      String message =\n          Util.formatInvariant(\n              \"Player is accessed on the wrong thread.\\n\"\n                  + \"Current thread: '%s'\\n\"\n                  + \"Expected thread: '%s'\\n\"\n                  + \"See https://developer.android.com/guide/topics/media/issues/\"\n                  + \"player-accessed-on-wrong-thread\",\n              Thread.currentThread().getName(), applicationLooper.getThread().getName());\n      throw new IllegalStateException(message);\n    }\n    if (state == null) {\n      // First time accessing state.\n      state = getState();\n    }\n  }\n\n  @RequiresNonNull(\"state\")\n  private void updateStateForPendingOperation(\n      ListenableFuture<?> pendingOperation, Supplier<State> placeholderStateSupplier) {\n    updateStateForPendingOperation(\n        pendingOperation,\n        placeholderStateSupplier,\n        /* seeked= */ false,\n        /* isRepeatingCurrentItem= */ false);\n  }\n\n  @RequiresNonNull(\"state\")","sourceCodeStart":3499,"sourceCodeEnd":3535,"githubUrl":"https://github.com/google/ExoPlayer/blob/dd430f7053a1a3958deea3ead6a0565150c06bfc/library/common/src/main/java/com/google/android/exoplayer2/SimpleBasePlayer.java#L3499-L3535","documentation":"SimpleBasePlayer is single-threaded: every public Player method first calls verifyApplicationThreadAndInitState(), which throws IllegalStateException unless the caller runs on the thread of the applicationLooper supplied in the constructor. The message names the current and expected threads and links to Android's threading guidance. The check also lazily initializes the player State on first access from the correct thread.","triggerScenarios":"Invoking any Player method (or creating it and immediately calling getters) from a background thread, a coroutine on Dispatchers.Default, a callback on a different looper, or before the looper thread has started — anywhere Thread.currentThread() != applicationLooper.getThread().","commonSituations":"Calling player.pause() from a network or media-session callback thread; accessing the player from a non-main coroutine dispatcher when it was built with the main Looper; tests that touch the player on the JUnit thread instead of a ShadowLooper; data-layer code reading player.getCurrentPosition off the UI thread.","solutions":["Route every player interaction onto the looper thread: wrap calls in new Handler(applicationLooper).post(...) (or withContext(Dispatchers.Main) when the player uses the main looper).","Construct the player with a Looper you control (e.g. Looper.getMainLooper()) and standardize all call sites on that thread.","In Robolectric tests, shadow the player's looper and run tasks on it (shadowOf(looper).idle()) rather than calling from the test thread.","For periodic position polling, post a self-re-scheduling Runnable on the same handler instead of polling from another thread."],"exampleFix":"// before\nexecutor.execute(() -> player.seekTo(position)); // wrong thread -> IllegalStateException\n\n// after\nnew Handler(player.getApplicationLooper()).post(() -> player.seekTo(position));\n// or, when the player uses the main looper:\nwithContext(Dispatchers.Main) { player.seekTo(position) }","handlingStrategy":"validation","validationCode":"public void runOnPlayerThread(Player player, Runnable action) {\n  if (Thread.currentThread() == player.getApplicationLooper().getThread()) {\n    action.run();\n  } else {\n    new Handler(player.getApplicationLooper()).post(action);\n  }\n}","typeGuard":null,"tryCatchPattern":"// Last-resort guard at boundaries (e.g. library entry points)\ntry {\n  player.pause();\n} catch (IllegalStateException e) {\n  if (e.getMessage() != null && e.getMessage().contains(\"wrong thread\")) {\n    Log.w(TAG, \"Player touched off looper thread; reposting\", e);\n    new Handler(player.getApplicationLooper()).post(player::pause);\n    return;\n  }\n  throw e;\n}","preventionTips":["Construct SimpleBasePlayer subclasses with Looper.getMainLooper() unless you have a dedicated playback thread, then confine all calls to that thread.","In coroutines use withContext(Dispatchers.Main) (or a custom dispatcher backed by the player's looper) around every player call.","In Robolectric, idle the player's shadow looper (shadowOf(looper).idle()) instead of calling from the test thread.","Wrap the player in a facade that funnels every method through a single Handler post."],"tags":["exoplayer","media3","player","threading","looper"],"backgroundTag":null,"analyzedSha":"dd430f7053a1a3958deea3ead6a0565150c06bfc","analyzedAt":"2026-08-14T12:22:02.982Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}