fix(mobile): expo-video bez ConcurrentModificationException przy liscie sluchaczy

close() odtwarzacza (watek JS albo sprzatajacy obiekty) wypisywal AudioFocusManager
z listy sluchaczy, a glowny watek w tym samym czasie rozsylal zdarzenie po prepare()
i iterowal ta liste (0.4.2, GOON-7A, Edge przy przewijaniu). Listy sluchaczy
odtwarzacza i odtwarzaczy w AudioFocusManager to teraz CopyOnWriteArrayList,
dopisywanie pod synchronized, usuwanie przez atomowe removeIf. Natywne, czeka na APK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
goon-foss 2026-09-24 08:58:37 +02:00
parent 2a2c0d7e9d
commit 30dd83557d

View file

@ -1,3 +1,40 @@
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<WeakReference<VideoPlayer>> = 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<WeakReference<VideoPlayer>>()
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 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 index cd798cf..8cb6dfb 100644
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt --- a/node_modules/expo-video/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt
@ -34,3 +71,40 @@ index cd798cf..8cb6dfb 100644
} }
private fun hidePlayerNotification(player: ExoPlayer) { 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<WeakReference<VideoPlayerListener>> = 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<WeakReference<VideoPlayerListener>>()
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) {