@stepincto/expo-video 1.0.0

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.
Files changed (180) hide show
  1. package/README.md +45 -0
  2. package/android/build.gradle +32 -0
  3. package/android/src/main/AndroidManifest.xml +20 -0
  4. package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +241 -0
  5. package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +145 -0
  6. package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
  7. package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
  8. package/android/src/main/java/expo/modules/video/PictureInPictureHelperFragment.kt +26 -0
  9. package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +36 -0
  10. package/android/src/main/java/expo/modules/video/VideoCache.kt +104 -0
  11. package/android/src/main/java/expo/modules/video/VideoExceptions.kt +34 -0
  12. package/android/src/main/java/expo/modules/video/VideoManager.kt +133 -0
  13. package/android/src/main/java/expo/modules/video/VideoModule.kt +414 -0
  14. package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
  15. package/android/src/main/java/expo/modules/video/VideoView.kt +367 -0
  16. package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
  17. package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
  18. package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
  19. package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
  20. package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
  21. package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
  22. package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
  23. package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +184 -0
  24. package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +39 -0
  25. package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
  26. package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +93 -0
  27. package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +164 -0
  28. package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +460 -0
  29. package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +125 -0
  30. package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +32 -0
  31. package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +525 -0
  32. package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +125 -0
  33. package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +15 -0
  34. package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +25 -0
  35. package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +19 -0
  36. package/android/src/main/java/expo/modules/video/records/Tracks.kt +81 -0
  37. package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
  38. package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +12 -0
  39. package/android/src/main/java/expo/modules/video/records/VideoSize.kt +14 -0
  40. package/android/src/main/java/expo/modules/video/records/VideoSource.kt +104 -0
  41. package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +24 -0
  42. package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +75 -0
  43. package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
  44. package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +15 -0
  45. package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
  46. package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
  47. package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
  48. package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
  49. package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
  50. package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
  51. package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
  52. package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
  53. package/android/src/main/res/layout/fullscreen_player_activity.xml +16 -0
  54. package/android/src/main/res/layout/surface_player_view.xml +7 -0
  55. package/android/src/main/res/layout/texture_player_view.xml +7 -0
  56. package/android/src/main/res/values/styles.xml +9 -0
  57. package/app.plugin.js +1 -0
  58. package/build/NativeVideoModule.d.ts +16 -0
  59. package/build/NativeVideoModule.d.ts.map +1 -0
  60. package/build/NativeVideoModule.js +3 -0
  61. package/build/NativeVideoModule.js.map +1 -0
  62. package/build/NativeVideoModule.web.d.ts +3 -0
  63. package/build/NativeVideoModule.web.d.ts.map +1 -0
  64. package/build/NativeVideoModule.web.js +2 -0
  65. package/build/NativeVideoModule.web.js.map +1 -0
  66. package/build/NativeVideoView.d.ts +4 -0
  67. package/build/NativeVideoView.d.ts.map +1 -0
  68. package/build/NativeVideoView.js +6 -0
  69. package/build/NativeVideoView.js.map +1 -0
  70. package/build/VideoModule.d.ts +38 -0
  71. package/build/VideoModule.d.ts.map +1 -0
  72. package/build/VideoModule.js +53 -0
  73. package/build/VideoModule.js.map +1 -0
  74. package/build/VideoPlayer.d.ts +15 -0
  75. package/build/VideoPlayer.d.ts.map +1 -0
  76. package/build/VideoPlayer.js +52 -0
  77. package/build/VideoPlayer.js.map +1 -0
  78. package/build/VideoPlayer.types.d.ts +532 -0
  79. package/build/VideoPlayer.types.d.ts.map +1 -0
  80. package/build/VideoPlayer.types.js +2 -0
  81. package/build/VideoPlayer.types.js.map +1 -0
  82. package/build/VideoPlayer.web.d.ts +75 -0
  83. package/build/VideoPlayer.web.d.ts.map +1 -0
  84. package/build/VideoPlayer.web.js +376 -0
  85. package/build/VideoPlayer.web.js.map +1 -0
  86. package/build/VideoPlayerEvents.types.d.ts +262 -0
  87. package/build/VideoPlayerEvents.types.d.ts.map +1 -0
  88. package/build/VideoPlayerEvents.types.js +2 -0
  89. package/build/VideoPlayerEvents.types.js.map +1 -0
  90. package/build/VideoThumbnail.d.ts +29 -0
  91. package/build/VideoThumbnail.d.ts.map +1 -0
  92. package/build/VideoThumbnail.js +3 -0
  93. package/build/VideoThumbnail.js.map +1 -0
  94. package/build/VideoView.d.ts +44 -0
  95. package/build/VideoView.d.ts.map +1 -0
  96. package/build/VideoView.js +76 -0
  97. package/build/VideoView.js.map +1 -0
  98. package/build/VideoView.types.d.ts +147 -0
  99. package/build/VideoView.types.d.ts.map +1 -0
  100. package/build/VideoView.types.js +2 -0
  101. package/build/VideoView.types.js.map +1 -0
  102. package/build/VideoView.web.d.ts +9 -0
  103. package/build/VideoView.web.d.ts.map +1 -0
  104. package/build/VideoView.web.js +180 -0
  105. package/build/VideoView.web.js.map +1 -0
  106. package/build/index.d.ts +9 -0
  107. package/build/index.d.ts.map +1 -0
  108. package/build/index.js +7 -0
  109. package/build/index.js.map +1 -0
  110. package/build/resolveAssetSource.d.ts +3 -0
  111. package/build/resolveAssetSource.d.ts.map +1 -0
  112. package/build/resolveAssetSource.js +3 -0
  113. package/build/resolveAssetSource.js.map +1 -0
  114. package/build/resolveAssetSource.web.d.ts +4 -0
  115. package/build/resolveAssetSource.web.d.ts.map +1 -0
  116. package/build/resolveAssetSource.web.js +16 -0
  117. package/build/resolveAssetSource.web.js.map +1 -0
  118. package/expo-module.config.json +9 -0
  119. package/ios/Cache/CachableRequest.swift +44 -0
  120. package/ios/Cache/CachedResource.swift +97 -0
  121. package/ios/Cache/CachingHelpers.swift +92 -0
  122. package/ios/Cache/MediaFileHandle.swift +94 -0
  123. package/ios/Cache/MediaInfo.swift +147 -0
  124. package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
  125. package/ios/Cache/SynchronizedHashTable.swift +23 -0
  126. package/ios/Cache/VideoCacheManager.swift +338 -0
  127. package/ios/ContentKeyDelegate.swift +214 -0
  128. package/ios/ContentKeyManager.swift +21 -0
  129. package/ios/Enums/AudioMixingMode.swift +37 -0
  130. package/ios/Enums/ContentType.swift +12 -0
  131. package/ios/Enums/DRMType.swift +20 -0
  132. package/ios/Enums/PlayerStatus.swift +10 -0
  133. package/ios/Enums/VideoContentFit.swift +39 -0
  134. package/ios/ExpoVideo.podspec +29 -0
  135. package/ios/NowPlayingManager.swift +296 -0
  136. package/ios/Records/BufferOptions.swift +12 -0
  137. package/ios/Records/DRMOptions.swift +24 -0
  138. package/ios/Records/PlaybackError.swift +10 -0
  139. package/ios/Records/Tracks.swift +176 -0
  140. package/ios/Records/VideoEventPayloads.swift +76 -0
  141. package/ios/Records/VideoMetadata.swift +16 -0
  142. package/ios/Records/VideoSize.swift +15 -0
  143. package/ios/Records/VideoSource.swift +25 -0
  144. package/ios/Thumbnails/VideoThumbnail.swift +27 -0
  145. package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
  146. package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
  147. package/ios/VideoAsset.swift +123 -0
  148. package/ios/VideoExceptions.swift +53 -0
  149. package/ios/VideoItem.swift +11 -0
  150. package/ios/VideoManager.swift +140 -0
  151. package/ios/VideoModule.swift +383 -0
  152. package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
  153. package/ios/VideoPlayer.swift +435 -0
  154. package/ios/VideoPlayerAudioTracks.swift +72 -0
  155. package/ios/VideoPlayerItem.swift +97 -0
  156. package/ios/VideoPlayerObserver.swift +523 -0
  157. package/ios/VideoPlayerSubtitles.swift +71 -0
  158. package/ios/VideoSourceLoader.swift +89 -0
  159. package/ios/VideoSourceLoaderListener.swift +34 -0
  160. package/ios/VideoView.swift +224 -0
  161. package/package.json +59 -0
  162. package/plugin/build/tsconfig.tsbuildinfo +1 -0
  163. package/plugin/build/withExpoVideo.d.ts +7 -0
  164. package/plugin/build/withExpoVideo.js +38 -0
  165. package/src/NativeVideoModule.ts +20 -0
  166. package/src/NativeVideoModule.web.ts +1 -0
  167. package/src/NativeVideoView.ts +8 -0
  168. package/src/VideoModule.ts +59 -0
  169. package/src/VideoPlayer.tsx +67 -0
  170. package/src/VideoPlayer.types.ts +613 -0
  171. package/src/VideoPlayer.web.tsx +451 -0
  172. package/src/VideoPlayerEvents.types.ts +313 -0
  173. package/src/VideoThumbnail.ts +31 -0
  174. package/src/VideoView.tsx +86 -0
  175. package/src/VideoView.types.ts +165 -0
  176. package/src/VideoView.web.tsx +214 -0
  177. package/src/index.ts +46 -0
  178. package/src/resolveAssetSource.ts +2 -0
  179. package/src/resolveAssetSource.web.ts +17 -0
  180. package/src/ts-declarations/react-native-assets.d.ts +1 -0
@@ -0,0 +1,523 @@
1
+ // Copyright 2024-present 650 Industries. All rights reserved.
2
+
3
+ import Foundation
4
+ import ExpoModulesCore
5
+ import AVFoundation
6
+
7
+ private struct Weak<T: AnyObject> {
8
+ weak var value: T?
9
+
10
+ init(_ value: T?) {
11
+ self.value = value
12
+ }
13
+ }
14
+
15
+ protocol VideoPlayerObserverDelegate: AnyObject {
16
+ func onStatusChanged(player: AVPlayer, oldStatus: PlayerStatus?, newStatus: PlayerStatus, error: Exception?)
17
+ func onIsPlayingChanged(player: AVPlayer, oldIsPlaying: Bool?, newIsPlaying: Bool)
18
+ func onRateChanged(player: AVPlayer, oldRate: Float?, newRate: Float)
19
+ func onVolumeChanged(player: AVPlayer, oldVolume: Float?, newVolume: Float)
20
+ func onPlayedToEnd(player: AVPlayer)
21
+ func onItemChanged(player: AVPlayer, oldVideoPlayerItem: VideoPlayerItem?, newVideoPlayerItem: VideoPlayerItem?)
22
+ func onIsMutedChanged(player: AVPlayer, oldIsMuted: Bool?, newIsMuted: Bool)
23
+ func onPlayerItemStatusChanged(player: AVPlayer, oldStatus: AVPlayerItem.Status?, newStatus: AVPlayerItem.Status)
24
+ func onTimeUpdate(player: AVPlayer, timeUpdate: TimeUpdate)
25
+ func onAudioMixingModeChanged(player: AVPlayer, oldAudioMixingMode: AudioMixingMode, newAudioMixingMode: AudioMixingMode)
26
+ func onSubtitleSelectionChanged(player: AVPlayer, playerItem: AVPlayerItem?, subtitleTrack: SubtitleTrack?)
27
+ func onAudioTrackSelectionChanged(player: AVPlayer, playerItem: AVPlayerItem?, audioTrack: AudioTrack?)
28
+ func onLoadedPlayerItem(player: AVPlayer, playerItem: AVPlayerItem?)
29
+ func onVideoTrackChanged(player: AVPlayer, oldVideoTrack: VideoTrack?, newVideoTrack: VideoTrack?)
30
+ }
31
+
32
+ // Default implementations for the delegate
33
+ extension VideoPlayerObserverDelegate {
34
+ func onStatusChanged(player: AVPlayer, oldStatus: PlayerStatus?, newStatus: PlayerStatus, error: Exception?) {}
35
+ func onIsPlayingChanged(player: AVPlayer, oldIsPlaying: Bool?, newIsPlaying: Bool) {}
36
+ func onRateChanged(player: AVPlayer, oldRate: Float?, newRate: Float) {}
37
+ func onVolumeChanged(player: AVPlayer, oldVolume: Float?, newVolume: Float) {}
38
+ func onPlayedToEnd(player: AVPlayer) {}
39
+ func onItemChanged(player: AVPlayer, oldVideoPlayerItem: VideoPlayerItem?, newVideoPlayerItem: VideoPlayerItem?) {}
40
+ func onIsMutedChanged(player: AVPlayer, oldIsMuted: Bool?, newIsMuted: Bool) {}
41
+ func onPlayerItemStatusChanged(player: AVPlayer, oldStatus: AVPlayerItem.Status?, newStatus: AVPlayerItem.Status) {}
42
+ func onTimeUpdate(player: AVPlayer, timeUpdate: TimeUpdate) {}
43
+ func onAudioMixingModeChanged(player: AVPlayer, oldAudioMixingMode: AudioMixingMode, newAudioMixingMode: AudioMixingMode) {}
44
+ func onSubtitleSelectionChanged(player: AVPlayer, playerItem: AVPlayerItem?, subtitleTrack: SubtitleTrack?) {}
45
+ func onAudioTrackSelectionChanged(player: AVPlayer, playerItem: AVPlayerItem?, audioTrack: AudioTrack?) {}
46
+ func onLoadedPlayerItem(player: AVPlayer, playerItem: AVPlayerItem?) {}
47
+ func onVideoTrackChanged(player: AVPlayer, oldVideoTrack: VideoTrack?, newVideoTrack: VideoTrack?) {}
48
+ }
49
+
50
+ // Wrapper used to store WeakReferences to the observer delegate
51
+ final class WeakPlayerObserverDelegate: Hashable {
52
+ private(set) weak var value: VideoPlayerObserverDelegate?
53
+
54
+ init(value: VideoPlayerObserverDelegate? = nil) {
55
+ self.value = value
56
+ }
57
+
58
+ static func == (lhs: WeakPlayerObserverDelegate, rhs: WeakPlayerObserverDelegate) -> Bool {
59
+ guard let lhsValue = lhs.value, let rhsValue = rhs.value else {
60
+ return lhs.value == nil && rhs.value == nil
61
+ }
62
+ return ObjectIdentifier(lhsValue) == ObjectIdentifier(rhsValue)
63
+ }
64
+
65
+ func hash(into hasher: inout Hasher) {
66
+ if let value {
67
+ hasher.combine(ObjectIdentifier(value))
68
+ }
69
+ }
70
+ }
71
+
72
+ class VideoPlayerObserver: VideoSourceLoaderListener {
73
+ private weak var owner: VideoPlayer?
74
+ private weak var videoSourceLoader: VideoSourceLoader?
75
+ var player: AVPlayer? {
76
+ owner?.ref
77
+ }
78
+ var delegates = Set<WeakPlayerObserverDelegate>()
79
+ private var currentItem: VideoPlayerItem?
80
+ private var isLoadingAsynchronously = false
81
+ private var loadedCurrentItem = false
82
+ private var periodicTimeObserver: Any?
83
+ private var currentVideoTrack: VideoTrack? {
84
+ didSet {
85
+ if let player, oldValue != currentVideoTrack {
86
+ delegates.forEach { delegate in
87
+ delegate.value?.onVideoTrackChanged(player: player, oldVideoTrack: oldValue, newVideoTrack: currentVideoTrack)
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ private var isPlaying: Bool = false {
94
+ didSet {
95
+ if let player, oldValue != isPlaying {
96
+ delegates.forEach { delegate in
97
+ delegate.value?.onIsPlayingChanged(player: player, oldIsPlaying: oldValue, newIsPlaying: isPlaying)
98
+ }
99
+ }
100
+ }
101
+ }
102
+ private var error: Exception?
103
+ private var _status: PlayerStatus = .idle
104
+ private var status: PlayerStatus {
105
+ get {
106
+ return _status
107
+ }
108
+ set {
109
+ if newValue != .loading && isLoadingAsynchronously {
110
+ return
111
+ }
112
+
113
+ if let player, newValue != status {
114
+ let oldStatus = self._status
115
+ _status = newValue
116
+ delegates.forEach { delegate in
117
+ delegate.value?.onStatusChanged(player: player, oldStatus: oldStatus, newStatus: status, error: error)
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ private var playerItemObserver: NSObjectProtocol?
124
+ private var playerRateObserver: NSKeyValueObservation?
125
+
126
+ // Player observers
127
+ private var playerStatusObserver: NSKeyValueObservation?
128
+ private var playerTimeControlStatusObserver: NSKeyValueObservation?
129
+ private var playerVolumeObserver: NSKeyValueObservation?
130
+ private var playerCurrentItemObserver: NSKeyValueObservation?
131
+ private var playerIsMutedObserver: NSKeyValueObservation?
132
+ private var playerAudioMixingModeObserver: NSKeyValueObservation?
133
+ private var tracksObserver: NSKeyValueObservation?
134
+
135
+ // Current player item observers
136
+ private var playbackBufferEmptyObserver: NSKeyValueObservation?
137
+ private var playerItemStatusObserver: NSKeyValueObservation?
138
+ private var playbackLikelyToKeepUpObserver: NSKeyValueObservation?
139
+ private var currentSubtitlesObserver: NSObjectProtocol?
140
+ private var currentAudioTracksObserver: NSObjectProtocol?
141
+
142
+ init(owner: VideoPlayer, videoSourceLoader: VideoSourceLoader) {
143
+ self.owner = owner
144
+ self.videoSourceLoader = videoSourceLoader
145
+ initializePlayerObservers()
146
+ self.videoSourceLoader?.registerListener(listener: self)
147
+ }
148
+
149
+ deinit {
150
+ cleanup()
151
+ }
152
+
153
+ func registerDelegate(delegate: VideoPlayerObserverDelegate) {
154
+ let weakDelegate = WeakPlayerObserverDelegate(value: delegate)
155
+ delegates.insert(weakDelegate)
156
+ }
157
+
158
+ func unregisterDelegate(delegate: VideoPlayerObserverDelegate) {
159
+ delegates.remove(WeakPlayerObserverDelegate(value: delegate))
160
+ }
161
+
162
+ func cleanup() {
163
+ self.videoSourceLoader?.unregisterListener(listener: self)
164
+ delegates.removeAll()
165
+ invalidatePlayerObservers()
166
+ invalidateCurrentPlayerItemObservers()
167
+ stopTimeUpdates()
168
+ }
169
+
170
+ private func initializePlayerObservers() {
171
+ guard let player else {
172
+ return
173
+ }
174
+ playerRateObserver = player.observe(\.rate, options: [.initial, .new, .old]) { [weak self] player, change in
175
+ self?.onPlayerRateChanged(player, change)
176
+ }
177
+ playerStatusObserver = player.observe(\.status, options: [.initial, .new, .old]) { [weak self] player, change in
178
+ self?.onPlayerStatusChanged(player, change)
179
+ }
180
+ playerTimeControlStatusObserver = player.observe(\.timeControlStatus, options: [.new, .old]) { [weak self] player, change in
181
+ self?.onTimeControlStatusChanged(player, change)
182
+ }
183
+ playerVolumeObserver = player.observe(\.volume, options: [.initial, .new, .old]) { [weak self] player, change in
184
+ self?.onPlayerVolumeChanged(player, change)
185
+ }
186
+ playerIsMutedObserver = player.observe(\.isMuted, options: [.initial, .new, .old]) { [weak self] player, change in
187
+ self?.onPlayerIsMutedChanged(player, change)
188
+ }
189
+ playerCurrentItemObserver = player.observe(\.currentItem, options: [.initial, .new]) { [weak self] player, change in
190
+ self?.onPlayerCurrentItemChanged(player, change)
191
+ }
192
+ }
193
+
194
+ private func invalidatePlayerObservers() {
195
+ playerRateObserver?.invalidate()
196
+ playerStatusObserver?.invalidate()
197
+ playerTimeControlStatusObserver?.invalidate()
198
+ playerVolumeObserver?.invalidate()
199
+ playerIsMutedObserver?.invalidate()
200
+ playerCurrentItemObserver?.invalidate()
201
+ }
202
+
203
+ private func initializeCurrentPlayerItemObservers(player: AVPlayer, playerItem: AVPlayerItem) {
204
+ playbackBufferEmptyObserver = playerItem.observe(\.isPlaybackBufferEmpty) { [weak self] item, change in
205
+ self?.onIsBufferEmptyChanged(item, change)
206
+ }
207
+
208
+ playbackLikelyToKeepUpObserver = playerItem.observe(\.isPlaybackLikelyToKeepUp) { [weak self] item, change in
209
+ self?.onPlayerLikelyToKeepUpChanged(item, change)
210
+ }
211
+
212
+ playerItemStatusObserver = playerItem.observe(\.status, options: [.initial, .new]) { [weak self] item, change in
213
+ self?.onItemStatusChanged(item, change)
214
+ }
215
+
216
+ tracksObserver = playerItem.observe(\.tracks) { [weak self] item, _ in
217
+ // For HLS sources AVPlayer doesn't provide the tracks when they change
218
+ // But it does call this event when they are loaded and when the current track changes.
219
+ // We have to extract the necessary information ourselves.
220
+ Task { [weak self] in
221
+ // For HLS sources
222
+ if let videoPlayerItem = playerItem as? VideoPlayerItem, videoPlayerItem.isHls {
223
+ guard let itemUri = videoPlayerItem.videoSource.uri else {
224
+ return
225
+ }
226
+ let lastLog = playerItem.accessLog()?.events.last(where: { $0.uri != nil })
227
+ self?.currentVideoTrack = lastLog?.matchToVideoTrack(videoTracks: await videoPlayerItem.videoTracks, itemUrl: itemUri)
228
+ return
229
+ }
230
+
231
+ // For "regular sources
232
+ // The track which is currently playing will be first
233
+ let currentTrack = try? await item.asset.loadTracks(withMediaType: .video).first
234
+
235
+ if let currentTrack {
236
+ let oldVideoTrack = self?.currentVideoTrack
237
+ self?.currentVideoTrack = await VideoTrack.from(assetTrack: currentTrack)
238
+ }
239
+ }
240
+ }
241
+
242
+ playerItemObserver = NotificationCenter.default.addObserver(
243
+ forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
244
+ object: playerItem,
245
+ queue: nil
246
+ ) { [weak self] _ in
247
+ self?.delegates.forEach { delegate in
248
+ delegate.value?.onPlayedToEnd(player: player)
249
+ }
250
+ }
251
+
252
+ currentSubtitlesObserver = NotificationCenter.default.addObserver(
253
+ forName: AVPlayerItem.mediaSelectionDidChangeNotification,
254
+ object: playerItem,
255
+ queue: nil
256
+ ) { [weak self] _ in
257
+ self?.delegates.forEach { delegate in
258
+ let subtitleTrack = VideoPlayerSubtitles.findCurrentSubtitleTrack(for: playerItem)
259
+ delegate.value?.onSubtitleSelectionChanged(player: player, playerItem: playerItem, subtitleTrack: subtitleTrack)
260
+ }
261
+ }
262
+
263
+ currentAudioTracksObserver = NotificationCenter.default.addObserver(
264
+ forName: AVPlayerItem.mediaSelectionDidChangeNotification,
265
+ object: playerItem,
266
+ queue: nil
267
+ ) { [weak self] _ in
268
+ self?.delegates.forEach { delegate in
269
+ let audioTrack = VideoPlayerAudioTracks.findCurrentAudioTrack(for: playerItem)
270
+ delegate.value?.onAudioTrackSelectionChanged(player: player, playerItem: playerItem, audioTrack: audioTrack)
271
+ }
272
+ }
273
+ }
274
+
275
+ private func invalidateCurrentPlayerItemObservers() {
276
+ playbackLikelyToKeepUpObserver?.invalidate()
277
+ playbackBufferEmptyObserver?.invalidate()
278
+ playerItemStatusObserver?.invalidate()
279
+ tracksObserver?.invalidate()
280
+ NotificationCenter.default.removeObserver(playerItemObserver as Any)
281
+ NotificationCenter.default.removeObserver(currentSubtitlesObserver as Any)
282
+ NotificationCenter.default.removeObserver(currentAudioTracksObserver as Any)
283
+ }
284
+
285
+ func startOrUpdateTimeUpdates(forInterval interval: Double) {
286
+ let interval = CMTimeMake(value: Int64(interval * 1000), timescale: CMTimeScale(1000))
287
+
288
+ stopTimeUpdates()
289
+ self.periodicTimeObserver = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] _ in
290
+ guard let self, let player, let owner else {
291
+ return
292
+ }
293
+ let update = TimeUpdate(
294
+ currentTime: player.currentTime().seconds,
295
+ currentLiveTimestamp: owner.currentLiveTimestamp,
296
+ currentOffsetFromLive: owner.currentOffsetFromLive,
297
+ bufferedPosition: owner.bufferedPosition
298
+ )
299
+
300
+ delegates.forEach { delegate in
301
+ delegate.value?.onTimeUpdate(player: player, timeUpdate: update)
302
+ }
303
+ }
304
+ }
305
+
306
+ func stopTimeUpdates() {
307
+ if let periodicTimeObserver {
308
+ player?.removeTimeObserver(periodicTimeObserver)
309
+ self.periodicTimeObserver = nil
310
+ }
311
+ }
312
+
313
+ // MARK: - VideoPlayerObserverDelegate
314
+
315
+ private func onPlayerCurrentItemChanged(_ player: AVPlayer, _ change: NSKeyValueObservedChange<AVPlayerItem?>) {
316
+ // Unwraps Optional<Optional<AVPlayerItem>> into Optional<AVPlayerItem>
317
+ let newPlayerItem = change.newValue?.flatMap({ $0 })
318
+
319
+ invalidateCurrentPlayerItemObservers()
320
+ currentVideoTrack = nil
321
+
322
+ if let videoPlayerItem = newPlayerItem as? VideoPlayerItem {
323
+ initializeCurrentPlayerItemObservers(player: player, playerItem: videoPlayerItem)
324
+ currentItem = videoPlayerItem
325
+
326
+ delegates.forEach { delegate in
327
+ delegate.value?.onItemChanged(player: player, oldVideoPlayerItem: currentItem, newVideoPlayerItem: videoPlayerItem)
328
+ }
329
+ loadedCurrentItem = false
330
+ return
331
+ }
332
+
333
+ if newPlayerItem == nil {
334
+ delegates.forEach { delegate in
335
+ delegate.value?.onItemChanged(player: player, oldVideoPlayerItem: currentItem, newVideoPlayerItem: nil)
336
+ }
337
+ status = .idle
338
+ } else {
339
+ log.warn(
340
+ "VideoPlayer's AVPlayer has been initialized with a `AVPlayerItem` instead of a `VideoPlayerItem`. " +
341
+ "Always use `VideoPlayerItem` as a wrapper for media played in `VideoPlayer`."
342
+ )
343
+ }
344
+ currentItem = nil
345
+ // Nil player item will be loaded instantly
346
+ onLoadedPlayerItem(player: player, playerItem: nil)
347
+ }
348
+
349
+ private func onLoadedPlayerItem(player: AVPlayer, playerItem: AVPlayerItem?) {
350
+ loadedCurrentItem = true
351
+ delegates.forEach { delegate in
352
+ delegate.value?.onLoadedPlayerItem(player: player, playerItem: playerItem)
353
+ }
354
+
355
+ // Changing the player item will disable the subtitles
356
+ delegates.forEach { delegate in
357
+ delegate.value?.onSubtitleSelectionChanged(player: player, playerItem: playerItem, subtitleTrack: nil)
358
+ }
359
+ }
360
+
361
+ private func onItemStatusChanged(_ playerItem: AVPlayerItem, _ change: NSKeyValueObservedChange<AVPlayerItem.Status>) {
362
+ if player?.status != .failed {
363
+ error = nil
364
+ }
365
+ if owner?.videoSourceLoader.isLoading == true {
366
+ status = .loading
367
+ return
368
+ }
369
+
370
+ let newStatus = playerItem.status.toVideoPlayerStatus(isPlaybackBufferEmpty: playerItem.isPlaybackBufferEmpty)
371
+
372
+ // The AVPlayerItem.error can't be modified, so we have a custom field for caching errors
373
+ if newStatus == .error {
374
+ let playerItemError = (playerItem as? VideoPlayerItem)?.urlAsset.cachingError ?? playerItem.error ?? error
375
+ error = PlayerItemLoadException(playerItemError?.localizedDescription)
376
+ status = .error
377
+ }
378
+
379
+ if let player, !loadedCurrentItem && (status == .readyToPlay || status == .error) {
380
+ onLoadedPlayerItem(player: player, playerItem: playerItem)
381
+ }
382
+
383
+ delegates.forEach { delegate in
384
+ if let player {
385
+ delegate.value?.onPlayerItemStatusChanged(player: player, oldStatus: change.oldValue, newStatus: playerItem.status)
386
+ }
387
+ }
388
+ }
389
+
390
+ private func onPlayerStatusChanged(_ player: AVPlayer, _ change: NSKeyValueObservedChange<AVPlayer.Status>) {
391
+ if player.currentItem?.status != .failed {
392
+ error = nil
393
+ }
394
+
395
+ if player.status == .failed {
396
+ error = PlayerException(player.error?.localizedDescription)
397
+ status = .error
398
+ }
399
+ }
400
+
401
+ private func onTimeControlStatusChanged(_ player: AVPlayer, _ change: NSKeyValueObservedChange<AVPlayer.TimeControlStatus>) {
402
+ // iOS changes timeControlStatus after an error, so we need to check for errors.
403
+ if player.status == .failed || player.currentItem?.status == .failed {
404
+ isPlaying = false
405
+ return
406
+ }
407
+ error = nil
408
+
409
+ if player.timeControlStatus != .waitingToPlayAtSpecifiedRate && player.status == .readyToPlay && currentItem?.isPlaybackBufferEmpty != true {
410
+ status = .readyToPlay
411
+ } else if player.timeControlStatus == .waitingToPlayAtSpecifiedRate {
412
+ switch player.reasonForWaitingToPlay {
413
+ case .noItemToPlay:
414
+ status = .idle
415
+ case .evaluatingBufferingRate:
416
+ // Every time the player is unpaused timeControlStatus goes into .waitingToPlayAtSpecifiedRate while evaluating buffering rate.
417
+ // This takes less than a frame and we can ignore this change to avoid unnecessary status changes.
418
+ break
419
+ default:
420
+ status = .loading
421
+ }
422
+ }
423
+
424
+ if isPlaying != (player.timeControlStatus == .playing) {
425
+ isPlaying = player.timeControlStatus == .playing
426
+ }
427
+ }
428
+
429
+ private func onIsBufferEmptyChanged(_ playerItem: AVPlayerItem, _ change: NSKeyValueObservedChange<Bool>) {
430
+ if playerItem.isPlaybackBufferEmpty {
431
+ status = .loading
432
+ }
433
+ }
434
+
435
+ private func onPlayerLikelyToKeepUpChanged(_ playerItem: AVPlayerItem, _ change: NSKeyValueObservedChange<Bool>) {
436
+ if !playerItem.isPlaybackLikelyToKeepUp && playerItem.isPlaybackBufferEmpty {
437
+ status = .loading
438
+ } else if playerItem.isPlaybackLikelyToKeepUp {
439
+ status = .readyToPlay
440
+ }
441
+ }
442
+
443
+ private func onPlayerRateChanged(_ player: AVPlayer, _ change: NSKeyValueObservedChange<Float>) {
444
+ if let newRate = change.newValue, change.oldValue != change.newValue {
445
+ delegates.forEach { delegate in
446
+ delegate.value?.onRateChanged(player: player, oldRate: change.oldValue, newRate: newRate)
447
+ }
448
+ }
449
+ }
450
+
451
+ private func onPlayerVolumeChanged(_ player: AVPlayer, _ change: NSKeyValueObservedChange<Float>) {
452
+ if let newVolume = change.newValue, change.oldValue != change.newValue {
453
+ delegates.forEach { delegate in
454
+ delegate.value?.onVolumeChanged(player: player, oldVolume: change.oldValue, newVolume: newVolume)
455
+ }
456
+ }
457
+ }
458
+
459
+ private func onPlayerIsMutedChanged(_ player: AVPlayer, _ change: NSKeyValueObservedChange<Bool>) {
460
+ if let newIsMuted = change.newValue, change.oldValue != change.newValue {
461
+ delegates.forEach { delegate in
462
+ delegate.value?.onIsMutedChanged(player: player, oldIsMuted: change.oldValue, newIsMuted: newIsMuted)
463
+ }
464
+ }
465
+ }
466
+
467
+ // MARK: - VideoSourceLoaderListener
468
+ func onLoadingStarted(loader: VideoSourceLoader, videoSource: VideoSource?) {
469
+ isLoadingAsynchronously = true
470
+ status = .loading
471
+ }
472
+
473
+ func onLoadingCancelled(loader: VideoSourceLoader, videoSource: VideoSource?) {
474
+ isLoadingAsynchronously = false
475
+ status = .idle
476
+ }
477
+
478
+ func onLoadingFinished(loader: VideoSourceLoader, videoSource: VideoSource?, result: VideoPlayerItem?) {
479
+ isLoadingAsynchronously = false
480
+ }
481
+ }
482
+
483
+ private extension AVPlayerItemAccessLogEvent {
484
+ // Matches the LogEvent to an existing VideoTrack based on the uri, or returns null if doesn't exist
485
+ func matchToVideoTrack(videoTracks: [VideoTrack], itemUrl: URL) -> VideoTrack? {
486
+ // The logUri should contain the track id from `VideoTrack`, it's used for playing selected track
487
+ guard let logUri = self.uri else {
488
+ return nil
489
+ }
490
+
491
+ // We need to find a base uri to which the track id is added for streaming a specific source
492
+ var components = URLComponents(url: itemUrl, resolvingAgainstBaseURL: false)
493
+ components?.query = nil
494
+
495
+ guard let baseUriString = components?.url?.deletingLastPathComponent().absoluteString else {
496
+ return nil
497
+ }
498
+
499
+ // Removing the base uri from the log uri allows us to get the id, which can be matched to an existing VideoTrack
500
+ let id = logUri.replacingOccurrences(of: baseUriString, with: "")
501
+
502
+ return videoTracks.first { $0.id == id }
503
+ }
504
+ }
505
+
506
+ fileprivate extension AVPlayerItem.Status {
507
+ func toVideoPlayerStatus(isPlaybackBufferEmpty: Bool) -> PlayerStatus {
508
+ switch self {
509
+ case .unknown:
510
+ return .loading
511
+ case .failed:
512
+ return .error
513
+ case .readyToPlay:
514
+ if isPlaybackBufferEmpty {
515
+ return .loading
516
+ }
517
+ return .readyToPlay
518
+ @unknown default:
519
+ log.error("Unhandled `AVPlayerItem.Status` value: \(self), returning `.loading` as fallback. Add the missing case as soon as possible.")
520
+ return .loading
521
+ }
522
+ }
523
+ }
@@ -0,0 +1,71 @@
1
+ import Foundation
2
+ import AVFoundation
3
+
4
+ class VideoPlayerSubtitles {
5
+ weak var owner: VideoPlayer?
6
+ private(set) var availableSubtitleTracks: [SubtitleTrack] = []
7
+ private(set) var currentSubtitleTrack: SubtitleTrack?
8
+
9
+ init (owner: VideoPlayer) {
10
+ self.owner = owner
11
+ }
12
+
13
+ func onNewSubtitleTrackSelected(subtitleTrack: SubtitleTrack?) {
14
+ currentSubtitleTrack = subtitleTrack
15
+ }
16
+
17
+ func onNewPlayerItemLoaded(playerItem: AVPlayerItem?) {
18
+ availableSubtitleTracks = Self.findAvailableSubtitleTracks(for: playerItem)
19
+ }
20
+
21
+ func selectSubtitleTrack(subtitleTrack: SubtitleTrack?) {
22
+ guard let currentItem = self.owner?.ref.currentItem else {
23
+ return
24
+ }
25
+
26
+ if let group = currentItem.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) {
27
+ let option = group.options.first {
28
+ $0.displayName == subtitleTrack?.label && $0.locale?.identifier == subtitleTrack?.language
29
+ }
30
+ currentItem.select(option, in: group)
31
+ }
32
+ }
33
+
34
+ static func findAvailableSubtitleTracks(for playerItem: AVPlayerItem?) -> [SubtitleTrack] {
35
+ var availableSubtitleTracks: [SubtitleTrack] = []
36
+
37
+ guard let asset = playerItem?.asset else {
38
+ return availableSubtitleTracks
39
+ }
40
+ let mediaSelectionCharacteristics = asset.availableMediaCharacteristicsWithMediaSelectionOptions
41
+
42
+ for characteristic in mediaSelectionCharacteristics {
43
+ guard characteristic == .legible else {
44
+ continue
45
+ }
46
+
47
+ if let group = asset.mediaSelectionGroup(forMediaCharacteristic: characteristic) {
48
+ for option in group.options {
49
+ guard let subtitleTrack = SubtitleTrack.from(mediaSelectionOption: option) else {
50
+ continue
51
+ }
52
+
53
+ availableSubtitleTracks.append(subtitleTrack)
54
+ }
55
+ }
56
+ }
57
+ return availableSubtitleTracks
58
+ }
59
+
60
+ static func findCurrentSubtitleTrack(for playerItem: AVPlayerItem?) -> SubtitleTrack? {
61
+ guard
62
+ let currentItem = playerItem,
63
+ let mediaSelectionGroup = currentItem.asset.mediaSelectionGroup(forMediaCharacteristic: .legible),
64
+ let selectedMediaOption = currentItem.currentMediaSelection.selectedMediaOption(in: mediaSelectionGroup)
65
+ else {
66
+ return nil
67
+ }
68
+
69
+ return SubtitleTrack.from(mediaSelectionOption: selectedMediaOption)
70
+ }
71
+ }
@@ -0,0 +1,89 @@
1
+ import AVKit
2
+
3
+ internal class VideoSourceLoader {
4
+ private(set) var isLoading: Bool = true
5
+ private var currentSource: VideoSource?
6
+ private var currentTask: Task<LoadingResult, Error>?
7
+
8
+ private var listeners = Set<WeakVideoSourceLoaderListener>()
9
+
10
+ func registerListener(listener: VideoSourceLoaderListener) {
11
+ let weakListener = WeakVideoSourceLoaderListener(value: listener)
12
+ listeners.insert(weakListener)
13
+ }
14
+
15
+ func unregisterListener(listener: VideoSourceLoaderListener) {
16
+ listeners.remove(WeakVideoSourceLoaderListener(value: listener))
17
+ }
18
+
19
+ /**
20
+ Asynchronously loads a video item from the provided `videoSource`. If another loading operation is in progress, it will be cancelled.
21
+
22
+ - Parameter videoSource: The source description for the video to load. If `nil`, the current player item will be cleared.
23
+ - Parameter player: The `VideoPlayer` instance whose current item will be replaced.
24
+ */
25
+ func load(videoSource: VideoSource) async throws -> VideoPlayerItem? {
26
+ isLoading = true
27
+ if let currentTask {
28
+ currentTask.cancel()
29
+ listeners.forEach { listener in
30
+ listener.value?.onLoadingCancelled(loader: self, videoSource: currentSource)
31
+ }
32
+ }
33
+
34
+ let newTask = Task {
35
+ return try await loadImpl(videoSource: videoSource)
36
+ }
37
+
38
+ self.currentTask = newTask
39
+ self.currentSource = videoSource
40
+ let loadingResult = try await newTask.value
41
+
42
+ if !loadingResult.isCancelled {
43
+ listeners.forEach { listener in
44
+ listener.value?.onLoadingFinished(loader: self, videoSource: videoSource, result: loadingResult.value)
45
+ }
46
+ }
47
+
48
+ isLoading = false
49
+ self.currentSource = nil
50
+ self.currentTask = nil
51
+ return loadingResult.value
52
+ }
53
+
54
+ func cancelCurrentTask() {
55
+ currentTask?.cancel()
56
+ currentTask = nil
57
+ isLoading = false
58
+ }
59
+
60
+ deinit {
61
+ cancelCurrentTask()
62
+ }
63
+
64
+ private func loadImpl(videoSource: VideoSource) async throws -> LoadingResult {
65
+ listeners.forEach { listener in
66
+ listener.value?.onLoadingStarted(loader: self, videoSource: videoSource)
67
+ }
68
+
69
+ guard
70
+ let url = videoSource.uri
71
+ else {
72
+ return LoadingResult(value: nil, isCancelled: false)
73
+ }
74
+
75
+ let playerItem = try await VideoPlayerItem(videoSource: videoSource)
76
+
77
+ if Task.isCancelled {
78
+ print("The loading task has been cancelled")
79
+ return LoadingResult(value: nil, isCancelled: true)
80
+ }
81
+
82
+ return LoadingResult(value: playerItem, isCancelled: false)
83
+ }
84
+ }
85
+
86
+ private struct LoadingResult {
87
+ let value: VideoPlayerItem?
88
+ let isCancelled: Bool
89
+ }