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>
110 lines
6.1 KiB
Diff
110 lines
6.1 KiB
Diff
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
|
|
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<ExoPlayer, MediaSession>()
|
|
+ private val mediaSessions = java.util.concurrent.ConcurrentHashMap<ExoPlayer, MediaSession>()
|
|
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<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) {
|