diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/AudioFocusManager.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/AudioFocusManager.kt index aa911b1..671bf05 100644 --- a/node_modules/expo-video/android/src/main/java/expo/modules/video/AudioFocusManager.kt +++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/AudioFocusManager.kt @@ -21,7 +21,9 @@ class AudioFocusManager(private val appContext: AppContext) : AudioManager.OnAud } } - private var players: MutableList> = mutableListOf() + // CopyOnWriteArrayList z tego samego powodu co listeners w VideoPlayer: register/unregister + // przychodza z watku JS i sprzatajacego, a forEach po liscie idzie na glownym (Goon, GOON-7A). + private val players = java.util.concurrent.CopyOnWriteArrayList>() private var currentFocusRequest: AudioFocusRequest? = null private var currentMixingMode: AudioMixingMode = AudioMixingMode.MIX_WITH_OTHERS private val anyPlayerRequiresFocus: Boolean @@ -89,8 +91,10 @@ class AudioFocusManager(private val appContext: AppContext) : AudioManager.OnAud } fun registerPlayer(player: VideoPlayer) { - players.find { it.get() == player } ?: run { - players.add(WeakReference(player)) + synchronized(players) { + players.find { it.get() == player } ?: run { + players.add(WeakReference(player)) + } } player.addListener(this) updateAudioFocus() @@ -98,7 +102,7 @@ class AudioFocusManager(private val appContext: AppContext) : AudioManager.OnAud fun unregisterPlayer(player: VideoPlayer) { player.removeListener(this) - players.removeAll { it.get() == player } + players.removeIf { it.get() == player } updateAudioFocus() } diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt index cd798cf..8cb6dfb 100644 --- a/node_modules/expo-video/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt @@ -26,7 +26,7 @@ class PlaybackServiceBinder(val service: ExpoVideoPlaybackService) : Binder() @OptIn(UnstableApi::class) class ExpoVideoPlaybackService : MediaSessionService() { - private val mediaSessions = mutableMapOf() + private val mediaSessions = java.util.concurrent.ConcurrentHashMap() private val binder = PlaybackServiceBinder(this) private val commandSeekForward = SessionCommand(SEEK_FORWARD_COMMAND, Bundle.EMPTY) @@ -136,10 +136,20 @@ class ExpoVideoPlaybackService : MediaSessionService() { private fun cleanup() { hideAllNotifications() - mediaSessions.forEach { (_, session) -> + // Kopia przed iteracja: session.release() wola z powrotem unregisterPlayer, ktory + // usuwa wpis z mediaSessions w trakcie forEach -> ConcurrentModificationException + // przy zamykaniu apki z listy ostatnich (Goon, Sentry 147164135, 2026-09-15). + // Mapa jest ConcurrentHashMap (patrz deklaracja), bo sama kopia tez iteruje mape, + // a wpisy znikaja z innego watku przy zwalnianiu playera: na LinkedHashMap `toList()` + // wywalalo sie wtedy tak samo (Goon 0.4.1, Sentry GOON-6W, 2026-09-17). + // ArrayList(values), nie `toList()`: toList przy rozmiarze 1 robi iterator().next(), + // a wpis moze zniknac miedzy size() i next() -> NoSuchElementException (Goon 0.4.2, + // Sentry GOON-72). Konstruktor idzie przez toArray(), ktore mapa robi bezpiecznie. + val sessions = ArrayList(mediaSessions.values) + mediaSessions.clear() + sessions.forEach { session -> session.release() } - mediaSessions.clear() } private fun hidePlayerNotification(player: ExoPlayer) { diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/player/VideoPlayer.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/player/VideoPlayer.kt index ec7accf..dd365e3 100644 --- a/node_modules/expo-video/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/player/VideoPlayer.kt @@ -44,7 +44,11 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou private var renderersFactory = DefaultRenderersFactory(context) .forceEnableMediaCodecAsynchronousQueueing() .setEnableDecoderFallback(true) - private var listeners: MutableList> = mutableListOf() + // CopyOnWriteArrayList, nie ArrayList: close() (watek JS albo sprzatajacy obiekty) wypisuje + // stad AudioFocusManager, a glowny watek w tym samym czasie rozsyla zdarzenie po prepare() + // i iteruje liste -> ConcurrentModificationException (Goon 0.4.2, Sentry GOON-7A, Edge + // tworzy i zwalnia odtwarzacze przy kazdym przewinieciu). Iteracja idzie po migawce. + private val listeners = java.util.concurrent.CopyOnWriteArrayList>() val loadControl: VideoPlayerLoadControl = VideoPlayerLoadControl.Builder().build() val subtitles: VideoPlayerSubtitles = VideoPlayerSubtitles(this) val trackSelector = DefaultTrackSelector(context) @@ -320,13 +324,16 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou } fun addListener(videoPlayerListener: VideoPlayerListener) { - if (listeners.all { it.get() != videoPlayerListener }) { - listeners.add(WeakReference(videoPlayerListener)) + synchronized(listeners) { + if (listeners.all { it.get() != videoPlayerListener }) { + listeners.add(WeakReference(videoPlayerListener)) + } } } fun removeListener(videoPlayerListener: VideoPlayerListener) { - listeners.removeAll { it.get() == videoPlayerListener } + // removeIf listy (atomowe), nie kotlinowe removeAll { }, ktore usuwa po indeksach. + listeners.removeIf { it.get() == videoPlayerListener } } private fun sendEvent(event: PlayerEvent) {