@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.
- package/README.md +45 -0
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +20 -0
- package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +241 -0
- package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +145 -0
- package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
- package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
- package/android/src/main/java/expo/modules/video/PictureInPictureHelperFragment.kt +26 -0
- package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +36 -0
- package/android/src/main/java/expo/modules/video/VideoCache.kt +104 -0
- package/android/src/main/java/expo/modules/video/VideoExceptions.kt +34 -0
- package/android/src/main/java/expo/modules/video/VideoManager.kt +133 -0
- package/android/src/main/java/expo/modules/video/VideoModule.kt +414 -0
- package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
- package/android/src/main/java/expo/modules/video/VideoView.kt +367 -0
- package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
- package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
- package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
- package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
- package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
- package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
- package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
- package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +184 -0
- package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +39 -0
- package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
- package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +93 -0
- package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +164 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +460 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +125 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +32 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +525 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +125 -0
- package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +15 -0
- package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +25 -0
- package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +19 -0
- package/android/src/main/java/expo/modules/video/records/Tracks.kt +81 -0
- package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
- package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +12 -0
- package/android/src/main/java/expo/modules/video/records/VideoSize.kt +14 -0
- package/android/src/main/java/expo/modules/video/records/VideoSource.kt +104 -0
- package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +24 -0
- package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +75 -0
- package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
- package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +15 -0
- package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
- package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
- package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
- package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
- package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
- package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
- package/android/src/main/res/layout/fullscreen_player_activity.xml +16 -0
- package/android/src/main/res/layout/surface_player_view.xml +7 -0
- package/android/src/main/res/layout/texture_player_view.xml +7 -0
- package/android/src/main/res/values/styles.xml +9 -0
- package/app.plugin.js +1 -0
- package/build/NativeVideoModule.d.ts +16 -0
- package/build/NativeVideoModule.d.ts.map +1 -0
- package/build/NativeVideoModule.js +3 -0
- package/build/NativeVideoModule.js.map +1 -0
- package/build/NativeVideoModule.web.d.ts +3 -0
- package/build/NativeVideoModule.web.d.ts.map +1 -0
- package/build/NativeVideoModule.web.js +2 -0
- package/build/NativeVideoModule.web.js.map +1 -0
- package/build/NativeVideoView.d.ts +4 -0
- package/build/NativeVideoView.d.ts.map +1 -0
- package/build/NativeVideoView.js +6 -0
- package/build/NativeVideoView.js.map +1 -0
- package/build/VideoModule.d.ts +38 -0
- package/build/VideoModule.d.ts.map +1 -0
- package/build/VideoModule.js +53 -0
- package/build/VideoModule.js.map +1 -0
- package/build/VideoPlayer.d.ts +15 -0
- package/build/VideoPlayer.d.ts.map +1 -0
- package/build/VideoPlayer.js +52 -0
- package/build/VideoPlayer.js.map +1 -0
- package/build/VideoPlayer.types.d.ts +532 -0
- package/build/VideoPlayer.types.d.ts.map +1 -0
- package/build/VideoPlayer.types.js +2 -0
- package/build/VideoPlayer.types.js.map +1 -0
- package/build/VideoPlayer.web.d.ts +75 -0
- package/build/VideoPlayer.web.d.ts.map +1 -0
- package/build/VideoPlayer.web.js +376 -0
- package/build/VideoPlayer.web.js.map +1 -0
- package/build/VideoPlayerEvents.types.d.ts +262 -0
- package/build/VideoPlayerEvents.types.d.ts.map +1 -0
- package/build/VideoPlayerEvents.types.js +2 -0
- package/build/VideoPlayerEvents.types.js.map +1 -0
- package/build/VideoThumbnail.d.ts +29 -0
- package/build/VideoThumbnail.d.ts.map +1 -0
- package/build/VideoThumbnail.js +3 -0
- package/build/VideoThumbnail.js.map +1 -0
- package/build/VideoView.d.ts +44 -0
- package/build/VideoView.d.ts.map +1 -0
- package/build/VideoView.js +76 -0
- package/build/VideoView.js.map +1 -0
- package/build/VideoView.types.d.ts +147 -0
- package/build/VideoView.types.d.ts.map +1 -0
- package/build/VideoView.types.js +2 -0
- package/build/VideoView.types.js.map +1 -0
- package/build/VideoView.web.d.ts +9 -0
- package/build/VideoView.web.d.ts.map +1 -0
- package/build/VideoView.web.js +180 -0
- package/build/VideoView.web.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +7 -0
- package/build/index.js.map +1 -0
- package/build/resolveAssetSource.d.ts +3 -0
- package/build/resolveAssetSource.d.ts.map +1 -0
- package/build/resolveAssetSource.js +3 -0
- package/build/resolveAssetSource.js.map +1 -0
- package/build/resolveAssetSource.web.d.ts +4 -0
- package/build/resolveAssetSource.web.d.ts.map +1 -0
- package/build/resolveAssetSource.web.js +16 -0
- package/build/resolveAssetSource.web.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Cache/CachableRequest.swift +44 -0
- package/ios/Cache/CachedResource.swift +97 -0
- package/ios/Cache/CachingHelpers.swift +92 -0
- package/ios/Cache/MediaFileHandle.swift +94 -0
- package/ios/Cache/MediaInfo.swift +147 -0
- package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
- package/ios/Cache/SynchronizedHashTable.swift +23 -0
- package/ios/Cache/VideoCacheManager.swift +338 -0
- package/ios/ContentKeyDelegate.swift +214 -0
- package/ios/ContentKeyManager.swift +21 -0
- package/ios/Enums/AudioMixingMode.swift +37 -0
- package/ios/Enums/ContentType.swift +12 -0
- package/ios/Enums/DRMType.swift +20 -0
- package/ios/Enums/PlayerStatus.swift +10 -0
- package/ios/Enums/VideoContentFit.swift +39 -0
- package/ios/ExpoVideo.podspec +29 -0
- package/ios/NowPlayingManager.swift +296 -0
- package/ios/Records/BufferOptions.swift +12 -0
- package/ios/Records/DRMOptions.swift +24 -0
- package/ios/Records/PlaybackError.swift +10 -0
- package/ios/Records/Tracks.swift +176 -0
- package/ios/Records/VideoEventPayloads.swift +76 -0
- package/ios/Records/VideoMetadata.swift +16 -0
- package/ios/Records/VideoSize.swift +15 -0
- package/ios/Records/VideoSource.swift +25 -0
- package/ios/Thumbnails/VideoThumbnail.swift +27 -0
- package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
- package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
- package/ios/VideoAsset.swift +123 -0
- package/ios/VideoExceptions.swift +53 -0
- package/ios/VideoItem.swift +11 -0
- package/ios/VideoManager.swift +140 -0
- package/ios/VideoModule.swift +383 -0
- package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
- package/ios/VideoPlayer.swift +435 -0
- package/ios/VideoPlayerAudioTracks.swift +72 -0
- package/ios/VideoPlayerItem.swift +97 -0
- package/ios/VideoPlayerObserver.swift +523 -0
- package/ios/VideoPlayerSubtitles.swift +71 -0
- package/ios/VideoSourceLoader.swift +89 -0
- package/ios/VideoSourceLoaderListener.swift +34 -0
- package/ios/VideoView.swift +224 -0
- package/package.json +59 -0
- package/plugin/build/tsconfig.tsbuildinfo +1 -0
- package/plugin/build/withExpoVideo.d.ts +7 -0
- package/plugin/build/withExpoVideo.js +38 -0
- package/src/NativeVideoModule.ts +20 -0
- package/src/NativeVideoModule.web.ts +1 -0
- package/src/NativeVideoView.ts +8 -0
- package/src/VideoModule.ts +59 -0
- package/src/VideoPlayer.tsx +67 -0
- package/src/VideoPlayer.types.ts +613 -0
- package/src/VideoPlayer.web.tsx +451 -0
- package/src/VideoPlayerEvents.types.ts +313 -0
- package/src/VideoThumbnail.ts +31 -0
- package/src/VideoView.tsx +86 -0
- package/src/VideoView.types.ts +165 -0
- package/src/VideoView.web.tsx +214 -0
- package/src/index.ts +46 -0
- package/src/resolveAssetSource.ts +2 -0
- package/src/resolveAssetSource.web.ts +17 -0
- 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
|
+
}
|