react-native-nitro-player 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NitroPlayer.podspec +31 -0
- package/README.md +610 -0
- package/android/CMakeLists.txt +29 -0
- package/android/build.gradle +147 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +7 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +29 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +116 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +167 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +93 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/NitroPlayerPackage.kt +21 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/connection/AndroidAutoConnectionDetector.kt +171 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +639 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +352 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibrary.kt +58 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibraryManager.kt +77 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibraryParser.kt +73 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +506 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/Playlist.kt +21 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +454 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/queue/Queue.kt +94 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/queue/QueueManager.kt +143 -0
- package/ios/HybridAudioRoutePicker.swift +53 -0
- package/ios/HybridTrackPlayer.swift +100 -0
- package/ios/core/TrackPlayerCore.swift +1040 -0
- package/ios/media/MediaSessionManager.swift +230 -0
- package/ios/playlist/PlaylistManager.swift +446 -0
- package/ios/playlist/PlaylistModel.swift +49 -0
- package/ios/queue/HybridPlayerQueue.swift +95 -0
- package/ios/queue/Queue.swift +126 -0
- package/ios/queue/QueueManager.swift +157 -0
- package/lib/hooks/index.d.ts +6 -0
- package/lib/hooks/index.js +6 -0
- package/lib/hooks/useAndroidAutoConnection.d.ts +13 -0
- package/lib/hooks/useAndroidAutoConnection.js +26 -0
- package/lib/hooks/useAudioDevices.d.ts +26 -0
- package/lib/hooks/useAudioDevices.js +55 -0
- package/lib/hooks/useOnChangeTrack.d.ts +9 -0
- package/lib/hooks/useOnChangeTrack.js +17 -0
- package/lib/hooks/useOnPlaybackProgressChange.d.ts +9 -0
- package/lib/hooks/useOnPlaybackProgressChange.js +19 -0
- package/lib/hooks/useOnPlaybackStateChange.d.ts +9 -0
- package/lib/hooks/useOnPlaybackStateChange.js +17 -0
- package/lib/hooks/useOnSeek.d.ts +8 -0
- package/lib/hooks/useOnSeek.js +17 -0
- package/lib/index.d.ts +14 -0
- package/lib/index.js +24 -0
- package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +21 -0
- package/lib/specs/AndroidAutoMediaLibrary.nitro.js +1 -0
- package/lib/specs/AudioDevices.nitro.d.ts +24 -0
- package/lib/specs/AudioDevices.nitro.js +1 -0
- package/lib/specs/AudioRoutePicker.nitro.d.ts +10 -0
- package/lib/specs/AudioRoutePicker.nitro.js +1 -0
- package/lib/specs/TrackPlayer.nitro.d.ts +39 -0
- package/lib/specs/TrackPlayer.nitro.js +1 -0
- package/lib/types/AndroidAutoMediaLibrary.d.ts +44 -0
- package/lib/types/AndroidAutoMediaLibrary.js +1 -0
- package/lib/types/PlayerQueue.d.ts +32 -0
- package/lib/types/PlayerQueue.js +1 -0
- package/lib/utils/androidAutoMediaLibrary.d.ts +47 -0
- package/lib/utils/androidAutoMediaLibrary.js +62 -0
- package/nitro.json +31 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroPlayer+autolinking.cmake +91 -0
- package/nitrogen/generated/android/NitroPlayer+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +88 -0
- package/nitrogen/generated/android/NitroPlayerOnLoad.hpp +25 -0
- package/nitrogen/generated/android/c++/JFunc_void_TrackItem_std__optional_Reason_.hpp +85 -0
- package/nitrogen/generated/android/c++/JFunc_void_TrackPlayerState_std__optional_Reason_.hpp +80 -0
- package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_double_double_std__optional_bool_.hpp +76 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string_Playlist_std__optional_QueueOperation_.hpp +88 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Playlist__std__optional_QueueOperation_.hpp +106 -0
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +55 -0
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +66 -0
- package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.cpp +70 -0
- package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.hpp +66 -0
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +143 -0
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +77 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +137 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +78 -0
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +65 -0
- package/nitrogen/generated/android/c++/JPlayerState.hpp +87 -0
- package/nitrogen/generated/android/c++/JPlaylist.hpp +99 -0
- package/nitrogen/generated/android/c++/JQueueOperation.hpp +65 -0
- package/nitrogen/generated/android/c++/JReason.hpp +65 -0
- package/nitrogen/generated/android/c++/JTAudioDevice.hpp +69 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +86 -0
- package/nitrogen/generated/android/c++/JTrackPlayerState.hpp +62 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.hpp +77 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_String.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_String.hpp +70 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.hpp +74 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_TrackItem_std__optional_Reason_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_TrackPlayerState_std__optional_Reason_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_bool.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_double_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_double_double_std__optional_bool_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__string_Playlist_std__optional_QueueOperation_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_Playlist__std__optional_QueueOperation_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrarySpec.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAudioDevicesSpec.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +116 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +134 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/NitroPlayerOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerConfig.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerState.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Playlist.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/QueueOperation.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Reason.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TAudioDevice.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackItem.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackPlayerState.kt +22 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Playlist.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_String.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_TrackItem.kt +59 -0
- package/nitrogen/generated/ios/NitroPlayer+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +123 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +531 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +80 -0
- package/nitrogen/generated/ios/NitroPlayerAutolinking.mm +49 -0
- package/nitrogen/generated/ios/NitroPlayerAutolinking.swift +55 -0
- package/nitrogen/generated/ios/c++/HybridAudioRoutePickerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridAudioRoutePickerSpecSwift.hpp +74 -0
- package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +167 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +174 -0
- package/nitrogen/generated/ios/swift/Func_void_TrackItem_std__optional_Reason_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_TrackPlayerState_std__optional_Reason_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_double_double.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_double_double_std__optional_bool_.swift +54 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string_Playlist_std__optional_QueueOperation_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_Playlist__std__optional_QueueOperation_.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridAudioRoutePickerSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridAudioRoutePickerSpec_cxx.swift +130 -0
- package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec.swift +68 -0
- package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec_cxx.swift +349 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +69 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +325 -0
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +115 -0
- package/nitrogen/generated/ios/swift/PlayerState.swift +181 -0
- package/nitrogen/generated/ios/swift/Playlist.swift +182 -0
- package/nitrogen/generated/ios/swift/QueueOperation.swift +48 -0
- package/nitrogen/generated/ios/swift/Reason.swift +48 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +147 -0
- package/nitrogen/generated/ios/swift/TrackPlayerState.swift +44 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_Playlist.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_String.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_TrackItem.swift +18 -0
- package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.hpp +63 -0
- package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.hpp +65 -0
- package/nitrogen/generated/shared/c++/HybridAudioRoutePickerSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridAudioRoutePickerSpec.hpp +62 -0
- package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.cpp +33 -0
- package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.hpp +87 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +34 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +91 -0
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +83 -0
- package/nitrogen/generated/shared/c++/PlayerState.hpp +103 -0
- package/nitrogen/generated/shared/c++/Playlist.hpp +97 -0
- package/nitrogen/generated/shared/c++/QueueOperation.hpp +84 -0
- package/nitrogen/generated/shared/c++/Reason.hpp +84 -0
- package/nitrogen/generated/shared/c++/TAudioDevice.hpp +87 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +102 -0
- package/nitrogen/generated/shared/c++/TrackPlayerState.hpp +80 -0
- package/package.json +172 -0
- package/react-native.config.js +16 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useAndroidAutoConnection.ts +30 -0
- package/src/hooks/useAudioDevices.ts +64 -0
- package/src/hooks/useOnChangeTrack.ts +24 -0
- package/src/hooks/useOnPlaybackProgressChange.ts +30 -0
- package/src/hooks/useOnPlaybackStateChange.ts +24 -0
- package/src/hooks/useOnSeek.ts +25 -0
- package/src/index.ts +47 -0
- package/src/specs/AndroidAutoMediaLibrary.nitro.ts +22 -0
- package/src/specs/AudioDevices.nitro.ts +25 -0
- package/src/specs/AudioRoutePicker.nitro.ts +9 -0
- package/src/specs/TrackPlayer.nitro.ts +81 -0
- package/src/types/AndroidAutoMediaLibrary.ts +58 -0
- package/src/types/PlayerQueue.ts +38 -0
- package/src/utils/androidAutoMediaLibrary.ts +66 -0
|
@@ -0,0 +1,1040 @@
|
|
|
1
|
+
//
|
|
2
|
+
// TrackPlayerCore.swift
|
|
3
|
+
// NitroPlayer
|
|
4
|
+
//
|
|
5
|
+
// Created by Ritesh Shukla on 10/12/25.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import AVFoundation
|
|
9
|
+
import Foundation
|
|
10
|
+
import MediaPlayer
|
|
11
|
+
import NitroModules
|
|
12
|
+
import ObjectiveC
|
|
13
|
+
|
|
14
|
+
class TrackPlayerCore: NSObject {
|
|
15
|
+
// MARK: - Constants
|
|
16
|
+
|
|
17
|
+
private enum Constants {
|
|
18
|
+
// Time thresholds (in seconds)
|
|
19
|
+
static let skipToPreviousThreshold: Double = 2.0
|
|
20
|
+
static let stateChangeDelay: TimeInterval = 0.1
|
|
21
|
+
|
|
22
|
+
// Duration thresholds for boundary intervals (in seconds)
|
|
23
|
+
static let twoHoursInSeconds: Double = 7200
|
|
24
|
+
static let oneHourInSeconds: Double = 3600
|
|
25
|
+
|
|
26
|
+
// Boundary time intervals (in seconds)
|
|
27
|
+
static let boundaryIntervalLong: Double = 5.0 // For tracks > 2 hours
|
|
28
|
+
static let boundaryIntervalMedium: Double = 2.0 // For tracks > 1 hour
|
|
29
|
+
static let boundaryIntervalDefault: Double = 1.0 // Default interval
|
|
30
|
+
|
|
31
|
+
// UI/Display constants
|
|
32
|
+
static let separatorLineLength: Int = 80
|
|
33
|
+
static let playlistSeparatorLength: Int = 40
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// MARK: - Properties
|
|
37
|
+
|
|
38
|
+
private var player: AVQueuePlayer?
|
|
39
|
+
private let playlistManager = PlaylistManager.shared
|
|
40
|
+
private var mediaSessionManager: MediaSessionManager?
|
|
41
|
+
private var currentPlaylistId: String?
|
|
42
|
+
private var currentTrackIndex: Int = -1
|
|
43
|
+
private var currentTracks: [TrackItem] = []
|
|
44
|
+
private var isManuallySeeked = false
|
|
45
|
+
private var boundaryTimeObserver: Any?
|
|
46
|
+
private var currentItemObservers: [NSKeyValueObservation] = []
|
|
47
|
+
|
|
48
|
+
var onChangeTrack: ((TrackItem, Reason?) -> Void)?
|
|
49
|
+
var onPlaybackStateChange: ((TrackPlayerState, Reason?) -> Void)?
|
|
50
|
+
var onSeek: ((Double, Double) -> Void)?
|
|
51
|
+
var onPlaybackProgressChange: ((Double, Double, Bool?) -> Void)?
|
|
52
|
+
|
|
53
|
+
static let shared = TrackPlayerCore()
|
|
54
|
+
|
|
55
|
+
// MARK: - Initialization
|
|
56
|
+
|
|
57
|
+
private override init() {
|
|
58
|
+
super.init()
|
|
59
|
+
setupAudioSession()
|
|
60
|
+
setupPlayer()
|
|
61
|
+
mediaSessionManager = MediaSessionManager()
|
|
62
|
+
mediaSessionManager?.setTrackPlayerCore(self)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// MARK: - Setup
|
|
66
|
+
|
|
67
|
+
private func setupAudioSession() {
|
|
68
|
+
do {
|
|
69
|
+
let audioSession = AVAudioSession.sharedInstance()
|
|
70
|
+
try audioSession.setCategory(.playback, mode: .default, options: [])
|
|
71
|
+
try audioSession.setActive(true)
|
|
72
|
+
} catch {
|
|
73
|
+
print("โ TrackPlayerCore: Failed to setup audio session - \(error)")
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private func setupPlayer() {
|
|
78
|
+
player = AVQueuePlayer()
|
|
79
|
+
setupPlayerObservers()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private func setupPlayerObservers() {
|
|
83
|
+
guard let player = player else { return }
|
|
84
|
+
|
|
85
|
+
// Observe player status
|
|
86
|
+
player.addObserver(self, forKeyPath: "status", options: [.new], context: nil)
|
|
87
|
+
player.addObserver(self, forKeyPath: "rate", options: [.new], context: nil)
|
|
88
|
+
|
|
89
|
+
// Observe time control status
|
|
90
|
+
player.addObserver(self, forKeyPath: "timeControlStatus", options: [.new], context: nil)
|
|
91
|
+
|
|
92
|
+
// Observe current item changes
|
|
93
|
+
NotificationCenter.default.addObserver(
|
|
94
|
+
self,
|
|
95
|
+
selector: #selector(playerItemDidPlayToEndTime),
|
|
96
|
+
name: .AVPlayerItemDidPlayToEndTime,
|
|
97
|
+
object: nil
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
NotificationCenter.default.addObserver(
|
|
101
|
+
self,
|
|
102
|
+
selector: #selector(playerItemFailedToPlayToEndTime),
|
|
103
|
+
name: .AVPlayerItemFailedToPlayToEndTime,
|
|
104
|
+
object: nil
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
// Observe player item errors
|
|
108
|
+
NotificationCenter.default.addObserver(
|
|
109
|
+
self,
|
|
110
|
+
selector: #selector(playerItemNewErrorLogEntry),
|
|
111
|
+
name: .AVPlayerItemNewErrorLogEntry,
|
|
112
|
+
object: nil
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
// Observe time jumps (seeks)
|
|
116
|
+
NotificationCenter.default.addObserver(
|
|
117
|
+
self,
|
|
118
|
+
selector: #selector(playerItemTimeJumped),
|
|
119
|
+
name: .AVPlayerItemTimeJumped,
|
|
120
|
+
object: nil
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
// Observe when item changes (using KVO on currentItem)
|
|
124
|
+
player.addObserver(self, forKeyPath: "currentItem", options: [.new], context: nil)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// MARK: - Boundary Time Observer
|
|
128
|
+
|
|
129
|
+
private func setupBoundaryTimeObserver() {
|
|
130
|
+
// Remove existing boundary observer if any
|
|
131
|
+
if let existingObserver = boundaryTimeObserver, let currentPlayer = player {
|
|
132
|
+
currentPlayer.removeTimeObserver(existingObserver)
|
|
133
|
+
boundaryTimeObserver = nil
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
guard let player = player,
|
|
137
|
+
let currentItem = player.currentItem
|
|
138
|
+
else {
|
|
139
|
+
print("โ ๏ธ TrackPlayerCore: Cannot setup boundary observer - no player or item")
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Wait for duration to be available
|
|
144
|
+
guard currentItem.status == .readyToPlay else {
|
|
145
|
+
print("โ ๏ธ TrackPlayerCore: Item not ready, will setup boundaries when ready")
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let duration = currentItem.duration.seconds
|
|
150
|
+
guard duration > 0 && !duration.isNaN && !duration.isInfinite else {
|
|
151
|
+
print("โ ๏ธ TrackPlayerCore: Invalid duration: \(duration), cannot setup boundaries")
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Determine interval based on duration
|
|
156
|
+
let interval: Double
|
|
157
|
+
if duration > Constants.twoHoursInSeconds {
|
|
158
|
+
interval = Constants.boundaryIntervalLong
|
|
159
|
+
} else if duration > Constants.oneHourInSeconds {
|
|
160
|
+
interval = Constants.boundaryIntervalMedium
|
|
161
|
+
} else {
|
|
162
|
+
interval = Constants.boundaryIntervalDefault
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Create boundary times at each interval
|
|
166
|
+
var boundaryTimes: [NSValue] = []
|
|
167
|
+
var time: Double = 0
|
|
168
|
+
while time <= duration {
|
|
169
|
+
let cmTime = CMTime(seconds: time, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
|
170
|
+
boundaryTimes.append(NSValue(time: cmTime))
|
|
171
|
+
time += interval
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
print(
|
|
175
|
+
"โฑ๏ธ TrackPlayerCore: Setting up \(boundaryTimes.count) boundary observers (interval: \(interval)s, duration: \(Int(duration))s)"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
// Add boundary time observer
|
|
179
|
+
boundaryTimeObserver = player.addBoundaryTimeObserver(forTimes: boundaryTimes, queue: .main) {
|
|
180
|
+
[weak self] in
|
|
181
|
+
guard let self = self else { return }
|
|
182
|
+
self.handleBoundaryTimeCrossed()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
print("โฑ๏ธ TrackPlayerCore: Boundary time observer setup complete")
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private func handleBoundaryTimeCrossed() {
|
|
189
|
+
guard let player = player,
|
|
190
|
+
let currentItem = player.currentItem
|
|
191
|
+
else { return }
|
|
192
|
+
|
|
193
|
+
// Don't fire progress when paused
|
|
194
|
+
guard player.rate > 0 else { return }
|
|
195
|
+
|
|
196
|
+
let position = currentItem.currentTime().seconds
|
|
197
|
+
let duration = currentItem.duration.seconds
|
|
198
|
+
|
|
199
|
+
guard duration > 0 && !duration.isNaN && !duration.isInfinite else { return }
|
|
200
|
+
|
|
201
|
+
print(
|
|
202
|
+
"โฑ๏ธ TrackPlayerCore: Boundary crossed - position: \(Int(position))s / \(Int(duration))s, callback exists: \(onPlaybackProgressChange != nil)"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
onPlaybackProgressChange?(
|
|
206
|
+
position,
|
|
207
|
+
duration,
|
|
208
|
+
isManuallySeeked ? true : nil
|
|
209
|
+
)
|
|
210
|
+
isManuallySeeked = false
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// MARK: - Notification Handlers
|
|
214
|
+
|
|
215
|
+
@objc private func playerItemDidPlayToEndTime(notification: Notification) {
|
|
216
|
+
print("\n๐ TrackPlayerCore: Track finished playing")
|
|
217
|
+
|
|
218
|
+
guard let finishedItem = notification.object as? AVPlayerItem else {
|
|
219
|
+
print("โ ๏ธ Cannot identify finished item")
|
|
220
|
+
skipToNext()
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if let trackId = finishedItem.trackId,
|
|
225
|
+
let track = currentTracks.first(where: { $0.id == trackId })
|
|
226
|
+
{
|
|
227
|
+
print("๐ Finished: \(track.title)")
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check remaining queue
|
|
231
|
+
if let player = player {
|
|
232
|
+
print("๐ Remaining items in queue: \(player.items().count)")
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Track ended naturally
|
|
236
|
+
onChangeTrack?(
|
|
237
|
+
getCurrentTrack()
|
|
238
|
+
?? TrackItem(
|
|
239
|
+
id: "",
|
|
240
|
+
title: "",
|
|
241
|
+
artist: "",
|
|
242
|
+
album: "",
|
|
243
|
+
duration: 0,
|
|
244
|
+
url: "",
|
|
245
|
+
artwork: nil
|
|
246
|
+
), .end)
|
|
247
|
+
|
|
248
|
+
// Try to play next track
|
|
249
|
+
skipToNext()
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@objc private func playerItemFailedToPlayToEndTime(notification: Notification) {
|
|
253
|
+
if let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error {
|
|
254
|
+
print("โ TrackPlayerCore: Playback failed - \(error)")
|
|
255
|
+
onPlaybackStateChange?(.stopped, .error)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@objc private func playerItemNewErrorLogEntry(notification: Notification) {
|
|
260
|
+
guard let item = notification.object as? AVPlayerItem,
|
|
261
|
+
let errorLog = item.errorLog()
|
|
262
|
+
else { return }
|
|
263
|
+
|
|
264
|
+
for event in errorLog.events ?? [] {
|
|
265
|
+
print(
|
|
266
|
+
"โ TrackPlayerCore: Error log - \(event.errorComment ?? "Unknown error") - Code: \(event.errorStatusCode)"
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Also check item error
|
|
271
|
+
if let error = item.error {
|
|
272
|
+
print("โ TrackPlayerCore: Item error - \(error.localizedDescription)")
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
@objc private func playerItemTimeJumped(notification: Notification) {
|
|
277
|
+
guard let player = player,
|
|
278
|
+
let currentItem = player.currentItem
|
|
279
|
+
else { return }
|
|
280
|
+
|
|
281
|
+
let position = currentItem.currentTime().seconds
|
|
282
|
+
let duration = currentItem.duration.seconds
|
|
283
|
+
|
|
284
|
+
print("๐ฏ TrackPlayerCore: Time jumped (seek detected) - position: \(Int(position))s")
|
|
285
|
+
|
|
286
|
+
// Call onSeek callback immediately
|
|
287
|
+
onSeek?(position, duration)
|
|
288
|
+
|
|
289
|
+
// Mark that this was a manual seek
|
|
290
|
+
isManuallySeeked = true
|
|
291
|
+
|
|
292
|
+
// Trigger immediate progress update
|
|
293
|
+
handleBoundaryTimeCrossed()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// MARK: - KVO Observer
|
|
297
|
+
|
|
298
|
+
override func observeValue(
|
|
299
|
+
forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?,
|
|
300
|
+
context: UnsafeMutableRawPointer?
|
|
301
|
+
) {
|
|
302
|
+
guard let player = player else { return }
|
|
303
|
+
|
|
304
|
+
print("๐ TrackPlayerCore: KVO - keyPath: \(keyPath ?? "nil")")
|
|
305
|
+
|
|
306
|
+
if keyPath == "status" {
|
|
307
|
+
print("๐ TrackPlayerCore: Player status changed to: \(player.status.rawValue)")
|
|
308
|
+
if player.status == .readyToPlay {
|
|
309
|
+
emitStateChange()
|
|
310
|
+
} else if player.status == .failed {
|
|
311
|
+
print("โ TrackPlayerCore: Player failed")
|
|
312
|
+
onPlaybackStateChange?(.stopped, .error)
|
|
313
|
+
}
|
|
314
|
+
} else if keyPath == "rate" {
|
|
315
|
+
print("๐ TrackPlayerCore: Rate changed to: \(player.rate)")
|
|
316
|
+
emitStateChange()
|
|
317
|
+
} else if keyPath == "timeControlStatus" {
|
|
318
|
+
print("๐ TrackPlayerCore: TimeControlStatus changed to: \(player.timeControlStatus.rawValue)")
|
|
319
|
+
emitStateChange()
|
|
320
|
+
} else if keyPath == "currentItem" {
|
|
321
|
+
print("๐ TrackPlayerCore: Current item changed")
|
|
322
|
+
currentItemDidChange()
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// MARK: - Item Change Handling
|
|
327
|
+
|
|
328
|
+
@objc private func currentItemDidChange() {
|
|
329
|
+
// Clear old item observers
|
|
330
|
+
currentItemObservers.removeAll()
|
|
331
|
+
|
|
332
|
+
// Track changed - update index
|
|
333
|
+
guard let player = player,
|
|
334
|
+
let currentItem = player.currentItem
|
|
335
|
+
else {
|
|
336
|
+
print("โ ๏ธ TrackPlayerCore: Current item changed to nil")
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
print("\n" + String(repeating: "โถ", count: Constants.separatorLineLength))
|
|
341
|
+
print("๐ TrackPlayerCore: CURRENT ITEM CHANGED")
|
|
342
|
+
print(String(repeating: "โถ", count: Constants.separatorLineLength))
|
|
343
|
+
|
|
344
|
+
// Log current item details
|
|
345
|
+
if let trackId = currentItem.trackId,
|
|
346
|
+
let track = currentTracks.first(where: { $0.id == trackId })
|
|
347
|
+
{
|
|
348
|
+
print("โถ๏ธ NOW PLAYING: \(track.title) - \(track.artist) (ID: \(track.id))")
|
|
349
|
+
} else {
|
|
350
|
+
print("โ ๏ธ NOW PLAYING: Unknown track (trackId: \(currentItem.trackId ?? "nil"))")
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Show remaining items in queue
|
|
354
|
+
let remainingItems = player.items()
|
|
355
|
+
print("\n๐ REMAINING ITEMS IN QUEUE: \(remainingItems.count)")
|
|
356
|
+
for (index, item) in remainingItems.enumerated() {
|
|
357
|
+
if let trackId = item.trackId, let track = currentTracks.first(where: { $0.id == trackId }) {
|
|
358
|
+
let marker = item == currentItem ? "โถ๏ธ" : " "
|
|
359
|
+
print("\(marker) [\(index + 1)] \(track.title) - \(track.artist)")
|
|
360
|
+
} else {
|
|
361
|
+
print(" [\(index + 1)] โ ๏ธ Unknown track")
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
print(String(repeating: "โถ", count: Constants.separatorLineLength) + "\n")
|
|
366
|
+
|
|
367
|
+
// Log item status
|
|
368
|
+
print("๐ฑ TrackPlayerCore: Item status: \(currentItem.status.rawValue)")
|
|
369
|
+
|
|
370
|
+
// Check for errors
|
|
371
|
+
if let error = currentItem.error {
|
|
372
|
+
print("โ TrackPlayerCore: Current item has error - \(error.localizedDescription)")
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Setup KVO observers for current item
|
|
376
|
+
setupCurrentItemObservers(item: currentItem)
|
|
377
|
+
|
|
378
|
+
// Update track index
|
|
379
|
+
if let trackId = currentItem.trackId {
|
|
380
|
+
print("๐ TrackPlayerCore: Looking up trackId '\(trackId)' in currentTracks...")
|
|
381
|
+
print(" Current index BEFORE lookup: \(currentTrackIndex)")
|
|
382
|
+
|
|
383
|
+
if let index = currentTracks.firstIndex(where: { $0.id == trackId }) {
|
|
384
|
+
print(" โ
Found track at index: \(index)")
|
|
385
|
+
print(" Setting currentTrackIndex from \(currentTrackIndex) to \(index)")
|
|
386
|
+
|
|
387
|
+
let oldIndex = currentTrackIndex
|
|
388
|
+
currentTrackIndex = index
|
|
389
|
+
|
|
390
|
+
if let track = currentTracks[safe: index] {
|
|
391
|
+
print(" ๐ต Track: \(track.title) - \(track.artist)")
|
|
392
|
+
|
|
393
|
+
// Only emit onChangeTrack if index actually changed
|
|
394
|
+
// This prevents duplicate emissions
|
|
395
|
+
if oldIndex != index {
|
|
396
|
+
print(" ๐ข Emitting onChangeTrack (index changed from \(oldIndex) to \(index))")
|
|
397
|
+
onChangeTrack?(track, .skip)
|
|
398
|
+
mediaSessionManager?.onTrackChanged()
|
|
399
|
+
} else {
|
|
400
|
+
print(" โญ๏ธ Skipping onChangeTrack emission (index unchanged)")
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} else {
|
|
404
|
+
print(" โ ๏ธ Track ID '\(trackId)' NOT FOUND in currentTracks!")
|
|
405
|
+
print(" Current tracks:")
|
|
406
|
+
for (idx, track) in currentTracks.enumerated() {
|
|
407
|
+
print(" [\(idx)] \(track.id) - \(track.title)")
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Setup boundary observers when item is ready
|
|
413
|
+
if currentItem.status == .readyToPlay {
|
|
414
|
+
setupBoundaryTimeObserver()
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private func setupCurrentItemObservers(item: AVPlayerItem) {
|
|
419
|
+
print("๐ฑ TrackPlayerCore: Setting up item observers")
|
|
420
|
+
|
|
421
|
+
// Observe status - recreate boundaries when ready
|
|
422
|
+
let statusObserver = item.observe(\.status, options: [.new]) { [weak self] item, _ in
|
|
423
|
+
if item.status == .readyToPlay {
|
|
424
|
+
print("โ
TrackPlayerCore: Item ready, setting up boundaries")
|
|
425
|
+
self?.setupBoundaryTimeObserver()
|
|
426
|
+
} else if item.status == .failed {
|
|
427
|
+
print("โ TrackPlayerCore: Item failed")
|
|
428
|
+
self?.onPlaybackStateChange?(.stopped, .error)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
currentItemObservers.append(statusObserver)
|
|
432
|
+
|
|
433
|
+
// Observe playback buffer
|
|
434
|
+
let bufferEmptyObserver = item.observe(\.isPlaybackBufferEmpty, options: [.new]) { item, _ in
|
|
435
|
+
if item.isPlaybackBufferEmpty {
|
|
436
|
+
print("โธ๏ธ TrackPlayerCore: Buffer empty (buffering)")
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
currentItemObservers.append(bufferEmptyObserver)
|
|
440
|
+
|
|
441
|
+
let bufferKeepUpObserver = item.observe(\.isPlaybackLikelyToKeepUp, options: [.new]) {
|
|
442
|
+
item, _ in
|
|
443
|
+
if item.isPlaybackLikelyToKeepUp {
|
|
444
|
+
print("โถ๏ธ TrackPlayerCore: Buffer likely to keep up")
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
currentItemObservers.append(bufferKeepUpObserver)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// MARK: - Playlist Management
|
|
451
|
+
|
|
452
|
+
func loadPlaylist(playlistId: String) {
|
|
453
|
+
DispatchQueue.main.async { [weak self] in
|
|
454
|
+
guard let self = self else { return }
|
|
455
|
+
|
|
456
|
+
print("\n" + String(repeating: "๐ผ", count: Constants.playlistSeparatorLength))
|
|
457
|
+
print("๐ TrackPlayerCore: LOAD PLAYLIST REQUEST")
|
|
458
|
+
print(" Playlist ID: \(playlistId)")
|
|
459
|
+
|
|
460
|
+
let playlist = self.playlistManager.getPlaylist(playlistId: playlistId)
|
|
461
|
+
if let playlist = playlist {
|
|
462
|
+
print(" โ
Found playlist: \(playlist.name)")
|
|
463
|
+
print(" ๐ Contains \(playlist.tracks.count) tracks:")
|
|
464
|
+
for (index, track) in playlist.tracks.enumerated() {
|
|
465
|
+
print(" [\(index + 1)] \(track.title) - \(track.artist)")
|
|
466
|
+
}
|
|
467
|
+
print(String(repeating: "๐ผ", count: Constants.playlistSeparatorLength) + "\n")
|
|
468
|
+
|
|
469
|
+
self.currentPlaylistId = playlistId
|
|
470
|
+
self.updatePlayerQueue(tracks: playlist.tracks)
|
|
471
|
+
// Emit initial state (paused/stopped before play)
|
|
472
|
+
self.emitStateChange()
|
|
473
|
+
// Automatically start playback after loading
|
|
474
|
+
self.play()
|
|
475
|
+
} else {
|
|
476
|
+
print(" โ Playlist NOT FOUND")
|
|
477
|
+
print(String(repeating: "๐ผ", count: Constants.playlistSeparatorLength) + "\n")
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
func updatePlaylist(playlistId: String) {
|
|
483
|
+
DispatchQueue.main.async { [weak self] in
|
|
484
|
+
guard let self = self else { return }
|
|
485
|
+
if self.currentPlaylistId == playlistId {
|
|
486
|
+
let playlist = self.playlistManager.getPlaylist(playlistId: playlistId)
|
|
487
|
+
if let playlist = playlist {
|
|
488
|
+
self.updatePlayerQueue(tracks: playlist.tracks)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// MARK: - Public Methods
|
|
495
|
+
|
|
496
|
+
func getCurrentPlaylistId() -> String? {
|
|
497
|
+
return currentPlaylistId
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
func getPlaylistManager() -> PlaylistManager {
|
|
501
|
+
return playlistManager
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private func emitStateChange(reason: Reason? = nil) {
|
|
505
|
+
guard let player = player else { return }
|
|
506
|
+
|
|
507
|
+
let state: TrackPlayerState
|
|
508
|
+
if player.rate == 0 {
|
|
509
|
+
state = .paused
|
|
510
|
+
} else if player.timeControlStatus == .playing {
|
|
511
|
+
state = .playing
|
|
512
|
+
} else if player.timeControlStatus == .waitingToPlayAtSpecifiedRate {
|
|
513
|
+
state = .paused // Buffering
|
|
514
|
+
} else {
|
|
515
|
+
state = .stopped
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
print("๐ TrackPlayerCore: Emitting state change: \(state)")
|
|
519
|
+
print("๐ TrackPlayerCore: Callback exists: \(onPlaybackStateChange != nil)")
|
|
520
|
+
onPlaybackStateChange?(state, reason)
|
|
521
|
+
mediaSessionManager?.onPlaybackStateChanged()
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// MARK: - Queue Management
|
|
525
|
+
|
|
526
|
+
private func updatePlayerQueue(tracks: [TrackItem]) {
|
|
527
|
+
print("\n" + String(repeating: "=", count: Constants.separatorLineLength))
|
|
528
|
+
print("๐ TrackPlayerCore: UPDATE PLAYER QUEUE - Received \(tracks.count) tracks")
|
|
529
|
+
print(String(repeating: "=", count: Constants.separatorLineLength))
|
|
530
|
+
|
|
531
|
+
// Print the full playlist being fed
|
|
532
|
+
for (index, track) in tracks.enumerated() {
|
|
533
|
+
print(" [\(index + 1)] ๐ต \(track.title) - \(track.artist) (ID: \(track.id))")
|
|
534
|
+
}
|
|
535
|
+
print(String(repeating: "=", count: Constants.separatorLineLength) + "\n")
|
|
536
|
+
|
|
537
|
+
// Store tracks for index tracking
|
|
538
|
+
currentTracks = tracks
|
|
539
|
+
currentTrackIndex = 0
|
|
540
|
+
print("๐ข TrackPlayerCore: Reset currentTrackIndex to 0 (will be updated by KVO observer)")
|
|
541
|
+
|
|
542
|
+
// Remove old boundary observer if exists (this is safe)
|
|
543
|
+
if let boundaryObserver = boundaryTimeObserver, let currentPlayer = player {
|
|
544
|
+
currentPlayer.removeTimeObserver(boundaryObserver)
|
|
545
|
+
boundaryTimeObserver = nil
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Create AVPlayerItems from tracks
|
|
549
|
+
let items = tracks.compactMap { track -> AVPlayerItem? in
|
|
550
|
+
guard let url = URL(string: track.url) else {
|
|
551
|
+
print("โ TrackPlayerCore: Invalid URL for track: \(track.title) - \(track.url)")
|
|
552
|
+
return nil
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
let item = AVPlayerItem(url: url)
|
|
556
|
+
|
|
557
|
+
// Set metadata using AVMutableMetadataItem
|
|
558
|
+
let metadata = AVMutableMetadataItem()
|
|
559
|
+
metadata.identifier = .commonIdentifierTitle
|
|
560
|
+
metadata.value = track.title as NSString
|
|
561
|
+
metadata.locale = Locale.current
|
|
562
|
+
|
|
563
|
+
let artistMetadata = AVMutableMetadataItem()
|
|
564
|
+
artistMetadata.identifier = .commonIdentifierArtist
|
|
565
|
+
artistMetadata.value = track.artist as NSString
|
|
566
|
+
artistMetadata.locale = Locale.current
|
|
567
|
+
|
|
568
|
+
let albumMetadata = AVMutableMetadataItem()
|
|
569
|
+
albumMetadata.identifier = .commonIdentifierAlbumName
|
|
570
|
+
albumMetadata.value = track.album as NSString
|
|
571
|
+
albumMetadata.locale = Locale.current
|
|
572
|
+
|
|
573
|
+
// Note: AVPlayerItem doesn't have externalMetadata property
|
|
574
|
+
// Metadata will be set via MPNowPlayingInfoCenter in MediaSessionManager
|
|
575
|
+
|
|
576
|
+
// Store track ID in item for later reference
|
|
577
|
+
item.trackId = track.id
|
|
578
|
+
|
|
579
|
+
return item
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
guard !items.isEmpty else {
|
|
583
|
+
print("โ TrackPlayerCore: No valid items to play")
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Replace current queue (player should always exist after setupPlayer)
|
|
588
|
+
guard let existingPlayer = self.player else {
|
|
589
|
+
print("โ TrackPlayerCore: No player available - this should never happen!")
|
|
590
|
+
return
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
print(
|
|
594
|
+
"๐ TrackPlayerCore: Updating queue - removing \(existingPlayer.items().count) items, adding \(items.count) new items"
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
// Remove all existing items
|
|
598
|
+
existingPlayer.removeAllItems()
|
|
599
|
+
|
|
600
|
+
// Add new items IN ORDER
|
|
601
|
+
// IMPORTANT: insert(after: nil) puts item at the start
|
|
602
|
+
// To maintain order, we need to track the last inserted item
|
|
603
|
+
var lastItem: AVPlayerItem? = nil
|
|
604
|
+
for (index, item) in items.enumerated() {
|
|
605
|
+
existingPlayer.insert(item, after: lastItem)
|
|
606
|
+
lastItem = item
|
|
607
|
+
|
|
608
|
+
if let trackId = item.trackId, let track = tracks.first(where: { $0.id == trackId }) {
|
|
609
|
+
print(" โ Added to player queue [\(index + 1)]: \(track.title)")
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Verify what's actually in the player now
|
|
614
|
+
print(
|
|
615
|
+
"\n๐ TrackPlayerCore: VERIFICATION - Player now has \(existingPlayer.items().count) items:")
|
|
616
|
+
for (index, item) in existingPlayer.items().enumerated() {
|
|
617
|
+
if let trackId = item.trackId, let track = tracks.first(where: { $0.id == trackId }) {
|
|
618
|
+
print(" [\(index + 1)] โ \(track.title) - \(track.artist) (ID: \(track.id))")
|
|
619
|
+
} else {
|
|
620
|
+
print(" [\(index + 1)] โ ๏ธ Unknown item (no trackId)")
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if let currentItem = existingPlayer.currentItem, let trackId = currentItem.trackId {
|
|
625
|
+
if let track = tracks.first(where: { $0.id == trackId }) {
|
|
626
|
+
print("โถ๏ธ Current item: \(track.title)")
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
print(String(repeating: "=", count: Constants.separatorLineLength) + "\n")
|
|
630
|
+
|
|
631
|
+
// Note: Boundary time observers will be set up automatically when item becomes ready
|
|
632
|
+
// This happens in setupCurrentItemObservers() -> status observer -> setupBoundaryTimeObserver()
|
|
633
|
+
|
|
634
|
+
// Notify track change
|
|
635
|
+
if let firstTrack = tracks.first {
|
|
636
|
+
print("๐ต TrackPlayerCore: Emitting track change: \(firstTrack.title)")
|
|
637
|
+
print("๐ต TrackPlayerCore: onChangeTrack callback exists: \(onChangeTrack != nil)")
|
|
638
|
+
onChangeTrack?(firstTrack, nil)
|
|
639
|
+
mediaSessionManager?.onTrackChanged()
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
print("โ
TrackPlayerCore: Queue updated with \(items.count) tracks")
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
func getCurrentTrack() -> TrackItem? {
|
|
646
|
+
guard currentTrackIndex >= 0 && currentTrackIndex < currentTracks.count else {
|
|
647
|
+
return nil
|
|
648
|
+
}
|
|
649
|
+
return currentTracks[currentTrackIndex]
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
func play() {
|
|
653
|
+
print("โถ๏ธ TrackPlayerCore: play() called")
|
|
654
|
+
DispatchQueue.main.async { [weak self] in
|
|
655
|
+
guard let self = self else { return }
|
|
656
|
+
print("โถ๏ธ TrackPlayerCore: Calling player.play()")
|
|
657
|
+
if let player = self.player {
|
|
658
|
+
print("โถ๏ธ TrackPlayerCore: Player status: \(player.status.rawValue)")
|
|
659
|
+
if let currentItem = player.currentItem {
|
|
660
|
+
print("โถ๏ธ TrackPlayerCore: Current item status: \(currentItem.status.rawValue)")
|
|
661
|
+
if let error = currentItem.error {
|
|
662
|
+
print("โ TrackPlayerCore: Current item error: \(error.localizedDescription)")
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
player.play()
|
|
666
|
+
// Emit state change immediately for responsive UI
|
|
667
|
+
// KVO will also fire, but this ensures immediate feedback
|
|
668
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + Constants.stateChangeDelay) {
|
|
669
|
+
[weak self] in
|
|
670
|
+
self?.emitStateChange()
|
|
671
|
+
}
|
|
672
|
+
} else {
|
|
673
|
+
print("โ TrackPlayerCore: No player available")
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
func pause() {
|
|
679
|
+
print("โธ๏ธ TrackPlayerCore: pause() called")
|
|
680
|
+
DispatchQueue.main.async { [weak self] in
|
|
681
|
+
guard let self = self else { return }
|
|
682
|
+
self.player?.pause()
|
|
683
|
+
// Emit state change immediately for responsive UI
|
|
684
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + Constants.stateChangeDelay) { [weak self] in
|
|
685
|
+
self?.emitStateChange()
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
func playSong(songId: String, fromPlaylist: String?) {
|
|
691
|
+
print(
|
|
692
|
+
"๐ต TrackPlayerCore: playSong() called - songId: \(songId), fromPlaylist: \(fromPlaylist ?? "nil")"
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
DispatchQueue.main.async { [weak self] in
|
|
696
|
+
guard let self = self else { return }
|
|
697
|
+
|
|
698
|
+
var targetPlaylistId: String?
|
|
699
|
+
var songIndex: Int = -1
|
|
700
|
+
|
|
701
|
+
// Case 1: If fromPlaylist is provided, use that playlist
|
|
702
|
+
if let playlistId = fromPlaylist {
|
|
703
|
+
print("๐ต TrackPlayerCore: Looking for song in specified playlist: \(playlistId)")
|
|
704
|
+
if let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) {
|
|
705
|
+
if let index = playlist.tracks.firstIndex(where: { $0.id == songId }) {
|
|
706
|
+
targetPlaylistId = playlistId
|
|
707
|
+
songIndex = index
|
|
708
|
+
print("โ
Found song at index \(index) in playlist \(playlistId)")
|
|
709
|
+
} else {
|
|
710
|
+
print("โ ๏ธ Song \(songId) not found in specified playlist \(playlistId)")
|
|
711
|
+
return
|
|
712
|
+
}
|
|
713
|
+
} else {
|
|
714
|
+
print("โ ๏ธ Playlist \(playlistId) not found")
|
|
715
|
+
return
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
// Case 2: If fromPlaylist is not provided, search in current/loaded playlist first
|
|
719
|
+
else {
|
|
720
|
+
print("๐ต TrackPlayerCore: No playlist specified, checking current playlist")
|
|
721
|
+
|
|
722
|
+
// Check if song exists in currently loaded playlist
|
|
723
|
+
if let currentId = self.currentPlaylistId,
|
|
724
|
+
let currentPlaylist = self.playlistManager.getPlaylist(playlistId: currentId)
|
|
725
|
+
{
|
|
726
|
+
if let index = currentPlaylist.tracks.firstIndex(where: { $0.id == songId }) {
|
|
727
|
+
targetPlaylistId = currentId
|
|
728
|
+
songIndex = index
|
|
729
|
+
print("โ
Found song at index \(index) in current playlist \(currentId)")
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// If not found in current playlist, search in all playlists
|
|
734
|
+
if songIndex == -1 {
|
|
735
|
+
print("๐ Song not found in current playlist, searching all playlists...")
|
|
736
|
+
let allPlaylists = self.playlistManager.getAllPlaylists()
|
|
737
|
+
|
|
738
|
+
for playlist in allPlaylists {
|
|
739
|
+
if let index = playlist.tracks.firstIndex(where: { $0.id == songId }) {
|
|
740
|
+
targetPlaylistId = playlist.id
|
|
741
|
+
songIndex = index
|
|
742
|
+
print("โ
Found song at index \(index) in playlist \(playlist.id)")
|
|
743
|
+
break
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// If still not found, just use the first playlist if available
|
|
748
|
+
if songIndex == -1 && !allPlaylists.isEmpty {
|
|
749
|
+
targetPlaylistId = allPlaylists[0].id
|
|
750
|
+
songIndex = 0
|
|
751
|
+
print("โ ๏ธ Song not found in any playlist, using first playlist and starting at index 0")
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Now play the song
|
|
757
|
+
guard let playlistId = targetPlaylistId, songIndex >= 0 else {
|
|
758
|
+
print("โ Could not determine playlist or song index")
|
|
759
|
+
return
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Load playlist if it's different from current
|
|
763
|
+
if self.currentPlaylistId != playlistId {
|
|
764
|
+
print("๐ Loading new playlist: \(playlistId)")
|
|
765
|
+
if let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) {
|
|
766
|
+
self.currentPlaylistId = playlistId
|
|
767
|
+
self.updatePlayerQueue(tracks: playlist.tracks)
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Play from the found index
|
|
772
|
+
print("โถ๏ธ Playing from index: \(songIndex)")
|
|
773
|
+
self.playFromIndex(index: songIndex)
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
func skipToNext() {
|
|
778
|
+
DispatchQueue.main.async { [weak self] in
|
|
779
|
+
guard let self = self,
|
|
780
|
+
let queuePlayer = self.player
|
|
781
|
+
else {
|
|
782
|
+
return
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
print("\nโญ๏ธ TrackPlayerCore: SKIP TO NEXT")
|
|
786
|
+
print(" BEFORE:")
|
|
787
|
+
print(" currentTrackIndex: \(self.currentTrackIndex)")
|
|
788
|
+
print(" Total tracks in currentTracks: \(self.currentTracks.count)")
|
|
789
|
+
print(" Items in player queue: \(queuePlayer.items().count)")
|
|
790
|
+
|
|
791
|
+
if let currentItem = queuePlayer.currentItem, let trackId = currentItem.trackId {
|
|
792
|
+
if let track = self.currentTracks.first(where: { $0.id == trackId }) {
|
|
793
|
+
print(" Currently playing: \(track.title) (ID: \(track.id))")
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Check if there are more items in the queue
|
|
798
|
+
if self.currentTrackIndex + 1 < self.currentTracks.count {
|
|
799
|
+
print(" ๐ Calling advanceToNextItem()...")
|
|
800
|
+
queuePlayer.advanceToNextItem()
|
|
801
|
+
|
|
802
|
+
// NOTE: Don't manually update currentTrackIndex here!
|
|
803
|
+
// The KVO observer (currentItemDidChange) will update it automatically
|
|
804
|
+
|
|
805
|
+
print(" AFTER advanceToNextItem():")
|
|
806
|
+
print(" Items in player queue: \(queuePlayer.items().count)")
|
|
807
|
+
|
|
808
|
+
if let newCurrentItem = queuePlayer.currentItem, let trackId = newCurrentItem.trackId {
|
|
809
|
+
if let track = self.currentTracks.first(where: { $0.id == trackId }) {
|
|
810
|
+
print(" New current item: \(track.title) (ID: \(track.id))")
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
print(" โณ Waiting for KVO observer to update index...")
|
|
815
|
+
} else {
|
|
816
|
+
print(" โ ๏ธ No more tracks in playlist")
|
|
817
|
+
// At end of playlist - stop or loop
|
|
818
|
+
queuePlayer.pause()
|
|
819
|
+
self.onPlaybackStateChange?(.stopped, .end)
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
func skipToPrevious() {
|
|
825
|
+
DispatchQueue.main.async { [weak self] in
|
|
826
|
+
guard let self = self,
|
|
827
|
+
let queuePlayer = self.player
|
|
828
|
+
else {
|
|
829
|
+
return
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
print("\nโฎ๏ธ TrackPlayerCore: SKIP TO PREVIOUS")
|
|
833
|
+
print(" Current index: \(self.currentTrackIndex)")
|
|
834
|
+
print(" Current time: \(queuePlayer.currentTime().seconds)s")
|
|
835
|
+
|
|
836
|
+
let currentTime = queuePlayer.currentTime()
|
|
837
|
+
if currentTime.seconds > Constants.skipToPreviousThreshold {
|
|
838
|
+
// If more than threshold seconds in, restart current track
|
|
839
|
+
print(
|
|
840
|
+
" ๐ More than \(Int(Constants.skipToPreviousThreshold))s in, restarting current track")
|
|
841
|
+
queuePlayer.seek(to: .zero)
|
|
842
|
+
} else if self.currentTrackIndex > 0 {
|
|
843
|
+
// Go to previous track
|
|
844
|
+
let previousIndex = self.currentTrackIndex - 1
|
|
845
|
+
print(" โฎ๏ธ Going to previous track at index \(previousIndex)")
|
|
846
|
+
self.playFromIndex(index: previousIndex)
|
|
847
|
+
} else {
|
|
848
|
+
// Already at first track, restart it
|
|
849
|
+
print(" ๐ Already at first track, restarting it")
|
|
850
|
+
queuePlayer.seek(to: .zero)
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
func seek(position: Double) {
|
|
856
|
+
DispatchQueue.main.async { [weak self] in
|
|
857
|
+
guard let self = self, let player = self.player else { return }
|
|
858
|
+
|
|
859
|
+
self.isManuallySeeked = true
|
|
860
|
+
let time = CMTime(seconds: position, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
|
861
|
+
player.seek(to: time) { [weak self] completed in
|
|
862
|
+
if completed {
|
|
863
|
+
let duration = player.currentItem?.duration.seconds ?? 0.0
|
|
864
|
+
self?.onSeek?(position, duration)
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// MARK: - State Management
|
|
871
|
+
|
|
872
|
+
func getState() -> PlayerState {
|
|
873
|
+
guard let player = player else {
|
|
874
|
+
return PlayerState(
|
|
875
|
+
currentTrack: nil,
|
|
876
|
+
currentPosition: 0.0,
|
|
877
|
+
totalDuration: 0.0,
|
|
878
|
+
currentState: .stopped,
|
|
879
|
+
currentPlaylistId: currentPlaylistId.map { Variant_NullType_String.second($0) },
|
|
880
|
+
currentIndex: -1.0
|
|
881
|
+
)
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
let currentTrack = getCurrentTrack()
|
|
885
|
+
let currentPosition = player.currentTime().seconds
|
|
886
|
+
let totalDuration = player.currentItem?.duration.seconds ?? 0.0
|
|
887
|
+
|
|
888
|
+
let currentState: TrackPlayerState
|
|
889
|
+
if player.rate == 0 {
|
|
890
|
+
currentState = .paused
|
|
891
|
+
} else if player.timeControlStatus == .playing {
|
|
892
|
+
currentState = .playing
|
|
893
|
+
} else {
|
|
894
|
+
currentState = .stopped
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Get current index
|
|
898
|
+
let currentIndex: Double = currentTrackIndex >= 0 ? Double(currentTrackIndex) : -1.0
|
|
899
|
+
|
|
900
|
+
return PlayerState(
|
|
901
|
+
currentTrack: currentTrack.map { Variant_NullType_TrackItem.second($0) },
|
|
902
|
+
currentPosition: currentPosition,
|
|
903
|
+
totalDuration: totalDuration,
|
|
904
|
+
currentState: currentState,
|
|
905
|
+
currentPlaylistId: currentPlaylistId.map { Variant_NullType_String.second($0) },
|
|
906
|
+
currentIndex: currentIndex
|
|
907
|
+
)
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
func configure(
|
|
911
|
+
androidAutoEnabled: Bool?,
|
|
912
|
+
carPlayEnabled: Bool?,
|
|
913
|
+
showInNotification: Bool?
|
|
914
|
+
) {
|
|
915
|
+
DispatchQueue.main.async { [weak self] in
|
|
916
|
+
self?.mediaSessionManager?.configure(
|
|
917
|
+
androidAutoEnabled: androidAutoEnabled,
|
|
918
|
+
carPlayEnabled: carPlayEnabled,
|
|
919
|
+
showInNotification: showInNotification
|
|
920
|
+
)
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
func getAllPlaylists() -> [Playlist] {
|
|
925
|
+
return playlistManager.getAllPlaylists().map { $0.toGeneratedPlaylist() }
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
func playFromIndex(index: Int) {
|
|
929
|
+
DispatchQueue.main.async { [weak self] in
|
|
930
|
+
guard let self = self,
|
|
931
|
+
index >= 0 && index < self.currentTracks.count
|
|
932
|
+
else {
|
|
933
|
+
print("โ TrackPlayerCore: playFromIndex - invalid index \(index)")
|
|
934
|
+
return
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
print("\n๐ฏ TrackPlayerCore: PLAY FROM INDEX \(index)")
|
|
938
|
+
print(" Total tracks in playlist: \(self.currentTracks.count)")
|
|
939
|
+
print(" Current index: \(self.currentTrackIndex), target index: \(index)")
|
|
940
|
+
|
|
941
|
+
// Store the full playlist
|
|
942
|
+
let fullPlaylist = self.currentTracks
|
|
943
|
+
|
|
944
|
+
// Update currentTrackIndex BEFORE updating queue
|
|
945
|
+
self.currentTrackIndex = index
|
|
946
|
+
|
|
947
|
+
// Recreate the queue starting from the target index
|
|
948
|
+
// This ensures all remaining tracks are in the queue
|
|
949
|
+
let tracksToPlay = Array(fullPlaylist[index...])
|
|
950
|
+
print(" ๐ Creating queue with \(tracksToPlay.count) tracks starting from index \(index)")
|
|
951
|
+
|
|
952
|
+
// Update the queue (but keep the full currentTracks for reference)
|
|
953
|
+
let items = tracksToPlay.compactMap { track -> AVPlayerItem? in
|
|
954
|
+
guard let url = URL(string: track.url) else { return nil }
|
|
955
|
+
let item = AVPlayerItem(url: url)
|
|
956
|
+
item.trackId = track.id
|
|
957
|
+
return item
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
guard let player = self.player, !items.isEmpty else {
|
|
961
|
+
print("โ No player or no items to play")
|
|
962
|
+
return
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Remove old boundary observer
|
|
966
|
+
if let boundaryObserver = self.boundaryTimeObserver {
|
|
967
|
+
player.removeTimeObserver(boundaryObserver)
|
|
968
|
+
self.boundaryTimeObserver = nil
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Clear and rebuild queue
|
|
972
|
+
player.removeAllItems()
|
|
973
|
+
var lastItem: AVPlayerItem? = nil
|
|
974
|
+
for item in items {
|
|
975
|
+
player.insert(item, after: lastItem)
|
|
976
|
+
lastItem = item
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Restore the full playlist reference (don't slice it!)
|
|
980
|
+
self.currentTracks = fullPlaylist
|
|
981
|
+
|
|
982
|
+
print(" โ
Queue recreated. Now at index: \(self.currentTrackIndex)")
|
|
983
|
+
if let track = self.getCurrentTrack() {
|
|
984
|
+
print(" ๐ต Playing: \(track.title)")
|
|
985
|
+
self.onChangeTrack?(track, .skip)
|
|
986
|
+
self.mediaSessionManager?.onTrackChanged()
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
player.play()
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// MARK: - Cleanup
|
|
994
|
+
|
|
995
|
+
deinit {
|
|
996
|
+
print("๐งน TrackPlayerCore: Cleaning up...")
|
|
997
|
+
|
|
998
|
+
// Remove boundary time observer
|
|
999
|
+
if let boundaryObserver = boundaryTimeObserver, let currentPlayer = player {
|
|
1000
|
+
currentPlayer.removeTimeObserver(boundaryObserver)
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Clear item observers (modern KVO automatically releases)
|
|
1004
|
+
currentItemObservers.removeAll()
|
|
1005
|
+
|
|
1006
|
+
// Remove player KVO observers (these were added in setupPlayer)
|
|
1007
|
+
if let currentPlayer = player {
|
|
1008
|
+
currentPlayer.removeObserver(self, forKeyPath: "status")
|
|
1009
|
+
currentPlayer.removeObserver(self, forKeyPath: "rate")
|
|
1010
|
+
currentPlayer.removeObserver(self, forKeyPath: "timeControlStatus")
|
|
1011
|
+
currentPlayer.removeObserver(self, forKeyPath: "currentItem")
|
|
1012
|
+
print("โ
TrackPlayerCore: Player observers removed")
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Remove all notification observers
|
|
1016
|
+
NotificationCenter.default.removeObserver(self)
|
|
1017
|
+
print("โ
TrackPlayerCore: Cleanup complete")
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Safe array access extension
|
|
1022
|
+
extension Array {
|
|
1023
|
+
subscript(safe index: Int) -> Element? {
|
|
1024
|
+
return indices.contains(index) ? self[index] : nil
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Associated object extension for AVPlayerItem
|
|
1029
|
+
private var trackIdKey: UInt8 = 0
|
|
1030
|
+
|
|
1031
|
+
extension AVPlayerItem {
|
|
1032
|
+
var trackId: String? {
|
|
1033
|
+
get {
|
|
1034
|
+
return objc_getAssociatedObject(self, &trackIdKey) as? String
|
|
1035
|
+
}
|
|
1036
|
+
set {
|
|
1037
|
+
objc_setAssociatedObject(self, &trackIdKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|