From 4b577a296b215baa4fadd1de7ebdbcd492887995 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Wed, 19 Nov 2025 18:01:02 +0100 Subject: [PATCH] Fix array index out of bounds crash in audio track handling This commit addresses crashes caused by race conditions when accessing audio track arrays: - MPVBackend.swift: Use safe index clamping to prevent array out of bounds crashes when selecting audio tracks - PlayerModel.swift: Add selectedAudioTrack computed property for thread-safe audio track access - ControlsOverlay.swift: Use safe accessor with "Original" fallback label - PlaybackSettings.swift: Use safe accessor with "Original" fallback label This fix resolves approximately 37% of crashes (23 out of 62 crash logs) that were caused by index out of range errors in MPVBackend.playStream at line 345. --- Model/Player/Backends/MPVBackend.swift | 8 ++++---- Model/Player/PlayerModel.swift | 7 +++++++ Shared/Player/Controls/ControlsOverlay.swift | 4 ++-- Shared/Player/PlaybackSettings.swift | 4 ++-- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index d8a98ff9..e67da018 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -338,11 +338,11 @@ final class MPVBackend: PlayerBackend { // Handle streams with multiple audio tracks if !stream.audioTracks.isEmpty { - if stream.selectedAudioTrackIndex >= stream.audioTracks.count { - stream.selectedAudioTrackIndex = 0 - } + // Ensure the index is within bounds to prevent race conditions + let safeIndex = min(max(0, stream.selectedAudioTrackIndex), stream.audioTracks.count - 1) + stream.selectedAudioTrackIndex = safeIndex - stream.audioAsset = AVURLAsset(url: stream.audioTracks[stream.selectedAudioTrackIndex].url) + stream.audioAsset = AVURLAsset(url: stream.audioTracks[safeIndex].url) let fileToLoad = self.model.musicMode ? stream.audioAsset.url : stream.videoAsset.url let audioTrack = self.model.musicMode ? nil : stream.audioAsset.url diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index a38f79cc..3388f66d 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -1575,4 +1575,11 @@ final class PlayerModel: ObservableObject { var availableAudioTracks: [Stream.AudioTrack] { (backend as? MPVBackend)?.availableAudioTracks ?? [] } + + var selectedAudioTrack: Stream.AudioTrack? { + let tracks = availableAudioTracks + guard !tracks.isEmpty else { return nil } + let safeIndex = min(max(0, selectedAudioTrackIndex), tracks.count - 1) + return tracks[safeIndex] + } } diff --git a/Shared/Player/Controls/ControlsOverlay.swift b/Shared/Player/Controls/ControlsOverlay.swift index cbd818d0..fc8399c5 100644 --- a/Shared/Player/Controls/ControlsOverlay.swift +++ b/Shared/Player/Controls/ControlsOverlay.swift @@ -458,7 +458,7 @@ struct ControlsOverlay: View { Menu { audioTrackPicker } label: { - Text(player.availableAudioTracks[player.selectedAudioTrackIndex].displayLanguage) + Text(player.selectedAudioTrack?.displayLanguage ?? "Original") .frame(maxWidth: 240, alignment: .trailing) } .transaction { t in t.animation = .none } @@ -468,7 +468,7 @@ struct ControlsOverlay: View { .frame(height: 40) #else ControlsOverlayButton(focusedField: $focusedField, field: .audioTrack) { - Text(player.availableAudioTracks[player.selectedAudioTrackIndex].displayLanguage) + Text(player.selectedAudioTrack?.displayLanguage ?? "Original") .frame(maxWidth: 320) } .contextMenu { diff --git a/Shared/Player/PlaybackSettings.swift b/Shared/Player/PlaybackSettings.swift index 09bf0054..104bf6ee 100644 --- a/Shared/Player/PlaybackSettings.swift +++ b/Shared/Player/PlaybackSettings.swift @@ -503,7 +503,7 @@ struct PlaybackSettings: View { Menu { audioTrackPicker } label: { - Text(player.availableAudioTracks[player.selectedAudioTrackIndex].displayLanguage) + Text(player.selectedAudioTrack?.displayLanguage ?? "Original") .frame(maxWidth: 240, alignment: .trailing) } .transaction { t in t.animation = .none } @@ -513,7 +513,7 @@ struct PlaybackSettings: View { .frame(height: 40) #else ControlsOverlayButton(focusedField: $focusedField, field: .audioTrack) { - Text(player.availableAudioTracks[player.selectedAudioTrackIndex].displayLanguage) + Text(player.selectedAudioTrack?.displayLanguage ?? "Original") .frame(maxWidth: 320) } .contextMenu {