unified-video-framework 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/.github/workflows/ci.yml +253 -0
- package/ANDROID_TV_IMPLEMENTATION.md +313 -0
- package/COMPLETION_STATUS.md +165 -0
- package/CONTRIBUTING.md +376 -0
- package/FINAL_STATUS_REPORT.md +170 -0
- package/FRAMEWORK_REVIEW.md +247 -0
- package/IMPROVEMENTS_SUMMARY.md +168 -0
- package/LICENSE +21 -0
- package/NATIVE_APP_INTEGRATION_GUIDE.md +903 -0
- package/PAYWALL_RENTAL_FLOW.md +499 -0
- package/PLATFORM_SETUP_GUIDE.md +1636 -0
- package/README.md +315 -0
- package/RUN_LOCALLY.md +151 -0
- package/apps/demo/cast-sender-min.html +173 -0
- package/apps/demo/custom-player.html +883 -0
- package/apps/demo/demo.html +990 -0
- package/apps/demo/enhanced-player.html +3556 -0
- package/apps/demo/index.html +159 -0
- package/apps/rental-api/.env.example +24 -0
- package/apps/rental-api/README.md +23 -0
- package/apps/rental-api/migrations/001_init.sql +35 -0
- package/apps/rental-api/migrations/002_videos.sql +10 -0
- package/apps/rental-api/migrations/003_add_gateway_subref.sql +4 -0
- package/apps/rental-api/migrations/004_update_gateways.sql +4 -0
- package/apps/rental-api/migrations/005_seed_demo_video.sql +5 -0
- package/apps/rental-api/package-lock.json +2045 -0
- package/apps/rental-api/package.json +33 -0
- package/apps/rental-api/scripts/run-migration.js +42 -0
- package/apps/rental-api/scripts/update-video-currency.js +21 -0
- package/apps/rental-api/scripts/update-video-price.js +19 -0
- package/apps/rental-api/src/config.ts +14 -0
- package/apps/rental-api/src/db.ts +10 -0
- package/apps/rental-api/src/routes/cashfree.ts +167 -0
- package/apps/rental-api/src/routes/pesapal.ts +92 -0
- package/apps/rental-api/src/routes/rentals.ts +242 -0
- package/apps/rental-api/src/routes/webhooks.ts +73 -0
- package/apps/rental-api/src/server.ts +41 -0
- package/apps/rental-api/src/services/entitlements.ts +45 -0
- package/apps/rental-api/src/services/payments.ts +22 -0
- package/apps/rental-api/tsconfig.json +17 -0
- package/check-urls.ps1 +74 -0
- package/comparison-report.md +181 -0
- package/docs/PAYWALL.md +95 -0
- package/docs/PLAYER_UI_VISIBILITY.md +431 -0
- package/docs/README.md +7 -0
- package/docs/SYSTEM_ARCHITECTURE.md +612 -0
- package/docs/VDOCIPHER_CLONE_REQUIREMENTS.md +403 -0
- package/examples/android/JavaSampleApp/MainActivity.java +641 -0
- package/examples/android/JavaSampleApp/activity_main.xml +226 -0
- package/examples/android/SampleApp/MainActivity.kt +430 -0
- package/examples/ios/SampleApp/ViewController.swift +337 -0
- package/examples/ios/SwiftUISampleApp/ContentView.swift +304 -0
- package/iOS_IMPLEMENTATION_OPTIONS.md +470 -0
- package/ios/UnifiedVideoPlayer/UnifiedVideoPlayer.podspec +33 -0
- package/jest.config.js +33 -0
- package/jitpack.yml +5 -0
- package/lerna.json +35 -0
- package/package.json +69 -0
- package/packages/PLATFORM_STATUS.md +163 -0
- package/packages/android/build.gradle +135 -0
- package/packages/android/src/main/AndroidManifest.xml +36 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/PlayerConfiguration.java +221 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.java +1037 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.kt +707 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/analytics/AnalyticsProvider.java +9 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastManager.java +141 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastOptionsProvider.java +29 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/overlay/WatermarkOverlayView.java +88 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/pip/PipActionReceiver.java +33 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/services/PlaybackService.java +110 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/services/PlayerHolder.java +19 -0
- package/packages/core/package.json +34 -0
- package/packages/core/src/BasePlayer.ts +250 -0
- package/packages/core/src/VideoPlayer.ts +237 -0
- package/packages/core/src/VideoPlayerFactory.ts +145 -0
- package/packages/core/src/index.ts +20 -0
- package/packages/core/src/interfaces/IVideoPlayer.ts +184 -0
- package/packages/core/src/interfaces.ts +240 -0
- package/packages/core/src/utils/EventEmitter.ts +66 -0
- package/packages/core/src/utils/PlatformDetector.ts +300 -0
- package/packages/core/tsconfig.json +20 -0
- package/packages/enact/package.json +51 -0
- package/packages/enact/src/VideoPlayer.js +365 -0
- package/packages/enact/src/adapters/TizenAdapter.js +354 -0
- package/packages/enact/src/index.js +82 -0
- package/packages/ios/BUILD_INSTRUCTIONS.md +108 -0
- package/packages/ios/FIX_EMBED_ISSUE.md +142 -0
- package/packages/ios/GETTING_STARTED.md +100 -0
- package/packages/ios/Package.swift +35 -0
- package/packages/ios/README.md +84 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Analytics/AnalyticsEmitter.swift +26 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/DRM/FairPlayDRMManager.swift +102 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Info.plist +24 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Remote/RemoteCommandCenter.swift +109 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayer.swift +811 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayerView.swift +640 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Utilities/Color+Hex.swift +36 -0
- package/packages/ios/UnifiedVideoPlayer.podspec +27 -0
- package/packages/ios/UnifiedVideoPlayer.xcodeproj/project.pbxproj +385 -0
- package/packages/ios/build_framework.sh +55 -0
- package/packages/react-native/android/src/main/java/com/unifiedvideo/UnifiedVideoPlayerModule.kt +482 -0
- package/packages/react-native/ios/UnifiedVideoPlayer.swift +436 -0
- package/packages/react-native/package.json +51 -0
- package/packages/react-native/src/ReactNativePlayer.tsx +423 -0
- package/packages/react-native/src/VideoPlayer.tsx +224 -0
- package/packages/react-native/src/index.ts +28 -0
- package/packages/react-native/src/utils/EventEmitter.ts +66 -0
- package/packages/react-native/tsconfig.json +31 -0
- package/packages/roku/components/UnifiedVideoPlayer.brs +400 -0
- package/packages/roku/package.json +44 -0
- package/packages/roku/source/VideoPlayer.brs +231 -0
- package/packages/roku/source/main.brs +28 -0
- package/packages/web/GETTING_STARTED.md +292 -0
- package/packages/web/jest.config.js +28 -0
- package/packages/web/jest.setup.ts +110 -0
- package/packages/web/package.json +50 -0
- package/packages/web/src/SecureVideoPlayer.ts +1164 -0
- package/packages/web/src/WebPlayer.ts +3110 -0
- package/packages/web/src/__tests__/WebPlayer.test.ts +314 -0
- package/packages/web/src/index.ts +14 -0
- package/packages/web/src/paywall/PaywallController.ts +215 -0
- package/packages/web/src/react/WebPlayerView.tsx +177 -0
- package/packages/web/tsconfig.json +23 -0
- package/packages/web/webpack.config.js +45 -0
- package/server.js +131 -0
- package/server.py +84 -0
- package/test-urls.ps1 +97 -0
- package/test-video-urls.ps1 +87 -0
- package/tsconfig.json +39 -0
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
//
|
|
2
|
+
// UnifiedVideoPlayer.swift
|
|
3
|
+
// UnifiedVideoPlayer
|
|
4
|
+
//
|
|
5
|
+
// Unified Video Framework - iOS Native SDK
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
import AVFoundation
|
|
10
|
+
import AVKit
|
|
11
|
+
import UIKit
|
|
12
|
+
import MediaPlayer
|
|
13
|
+
|
|
14
|
+
// MARK: - Player Configuration
|
|
15
|
+
@objc public class PlayerConfiguration: NSObject {
|
|
16
|
+
@objc public var autoPlay: Bool = false
|
|
17
|
+
@objc public var controls: Bool = true
|
|
18
|
+
@objc public var muted: Bool = false
|
|
19
|
+
@objc public var loop: Bool = false
|
|
20
|
+
@objc public var preload: String = "auto"
|
|
21
|
+
@objc public var startTime: Double = 0
|
|
22
|
+
@objc public var playbackRate: Float = 1.0
|
|
23
|
+
@objc public var volume: Float = 1.0
|
|
24
|
+
@objc public var debug: Bool = false
|
|
25
|
+
// Dynamic theme color for controls (e.g., #FF5722 or #AAFF5722)
|
|
26
|
+
@objc public var themeColorHex: String? = nil
|
|
27
|
+
|
|
28
|
+
@objc public override init() {
|
|
29
|
+
super.init()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@objc public init(dictionary: [String: Any]) {
|
|
33
|
+
super.init()
|
|
34
|
+
autoPlay = dictionary["autoPlay"] as? Bool ?? false
|
|
35
|
+
controls = dictionary["controls"] as? Bool ?? true
|
|
36
|
+
muted = dictionary["muted"] as? Bool ?? false
|
|
37
|
+
loop = dictionary["loop"] as? Bool ?? false
|
|
38
|
+
preload = dictionary["preload"] as? String ?? "auto"
|
|
39
|
+
startTime = dictionary["startTime"] as? Double ?? 0
|
|
40
|
+
playbackRate = dictionary["playbackRate"] as? Float ?? 1.0
|
|
41
|
+
volume = dictionary["volume"] as? Float ?? 1.0
|
|
42
|
+
debug = dictionary["debug"] as? Bool ?? false
|
|
43
|
+
themeColorHex = dictionary["themeColorHex"] as? String
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// MARK: - Media Source
|
|
48
|
+
@objc public class MediaSource: NSObject {
|
|
49
|
+
@objc public var url: String
|
|
50
|
+
@objc public var type: String?
|
|
51
|
+
@objc public var drm: DRMConfiguration?
|
|
52
|
+
@objc public var metadata: [String: Any]?
|
|
53
|
+
@objc public var subtitles: [SubtitleTrack]?
|
|
54
|
+
|
|
55
|
+
@objc public init(url: String, type: String? = nil) {
|
|
56
|
+
self.url = url
|
|
57
|
+
self.type = type ?? MediaSource.detectType(from: url)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private static func detectType(from url: String) -> String {
|
|
61
|
+
if url.contains(".m3u8") { return "hls" }
|
|
62
|
+
if url.contains(".mpd") { return "dash" }
|
|
63
|
+
if url.contains(".mp4") { return "mp4" }
|
|
64
|
+
if url.contains(".webm") { return "webm" }
|
|
65
|
+
return "mp4"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// MARK: - DRM Configuration
|
|
70
|
+
@objc public class DRMConfiguration: NSObject {
|
|
71
|
+
@objc public var type: String // fairplay, widevine
|
|
72
|
+
@objc public var licenseUrl: String
|
|
73
|
+
@objc public var certificateUrl: String?
|
|
74
|
+
@objc public var headers: [String: String]?
|
|
75
|
+
|
|
76
|
+
@objc public init(type: String, licenseUrl: String) {
|
|
77
|
+
self.type = type
|
|
78
|
+
self.licenseUrl = licenseUrl
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// MARK: - Subtitle Track
|
|
83
|
+
@objc public class SubtitleTrack: NSObject {
|
|
84
|
+
@objc public var url: String
|
|
85
|
+
@objc public var language: String
|
|
86
|
+
@objc public var label: String
|
|
87
|
+
@objc public var kind: String // subtitles, captions
|
|
88
|
+
|
|
89
|
+
@objc public init(url: String, language: String, label: String, kind: String = "subtitles") {
|
|
90
|
+
self.url = url
|
|
91
|
+
self.language = language
|
|
92
|
+
self.label = label
|
|
93
|
+
self.kind = kind
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// MARK: - Player State
|
|
98
|
+
@objc public enum PlayerState: Int {
|
|
99
|
+
case idle = 0
|
|
100
|
+
case loading = 1
|
|
101
|
+
case ready = 2
|
|
102
|
+
case playing = 3
|
|
103
|
+
case paused = 4
|
|
104
|
+
case buffering = 5
|
|
105
|
+
case seeking = 6
|
|
106
|
+
case ended = 7
|
|
107
|
+
case error = 8
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// MARK: - Main Player Class
|
|
111
|
+
@objc public class UnifiedVideoPlayer: NSObject {
|
|
112
|
+
|
|
113
|
+
// MARK: Properties
|
|
114
|
+
private var player: AVPlayer?
|
|
115
|
+
private var playerLayer: AVPlayerLayer?
|
|
116
|
+
private var playerViewController: AVPlayerViewController?
|
|
117
|
+
private var containerView: UIView?
|
|
118
|
+
private var configuration: PlayerConfiguration?
|
|
119
|
+
private var currentSource: MediaSource?
|
|
120
|
+
|
|
121
|
+
private var timeObserver: Any?
|
|
122
|
+
private var statusObserver: NSKeyValueObservation?
|
|
123
|
+
private var rateObserver: NSKeyValueObservation?
|
|
124
|
+
private var currentItemObserver: NSKeyValueObservation?
|
|
125
|
+
|
|
126
|
+
// Add-ons
|
|
127
|
+
private var drmManager: FairPlayDRMManager?
|
|
128
|
+
private var pipController: AVPictureInPictureController?
|
|
129
|
+
private let analyticsEmitter = AnalyticsEmitter()
|
|
130
|
+
private let remoteCenter = RemoteCommandCenter()
|
|
131
|
+
private var themeUIColor: UIColor? = nil
|
|
132
|
+
|
|
133
|
+
@objc public private(set) var state: PlayerState = .idle
|
|
134
|
+
@objc public private(set) var isPlaying: Bool = false
|
|
135
|
+
@objc public private(set) var duration: Double = 0
|
|
136
|
+
@objc public private(set) var currentTime: Double = 0
|
|
137
|
+
@objc public private(set) var bufferedTime: Double = 0
|
|
138
|
+
|
|
139
|
+
// MARK: - Event Callbacks
|
|
140
|
+
@objc public var onReady: (() -> Void)?
|
|
141
|
+
@objc public var onPlay: (() -> Void)?
|
|
142
|
+
@objc public var onPause: (() -> Void)?
|
|
143
|
+
@objc public var onTimeUpdate: ((Double) -> Void)?
|
|
144
|
+
@objc public var onBuffering: ((Bool) -> Void)?
|
|
145
|
+
@objc public var onSeek: ((Double) -> Void)?
|
|
146
|
+
@objc public var onEnded: (() -> Void)?
|
|
147
|
+
@objc public var onError: ((Error) -> Void)?
|
|
148
|
+
@objc public var onLoadedMetadata: (([String: Any]) -> Void)?
|
|
149
|
+
@objc public var onVolumeChange: ((Float) -> Void)?
|
|
150
|
+
@objc public var onStateChange: ((PlayerState) -> Void)?
|
|
151
|
+
@objc public var onProgress: ((Double) -> Void)?
|
|
152
|
+
@objc public var onQualityChange: ((Double) -> Void)?
|
|
153
|
+
|
|
154
|
+
// MARK: - Initialization
|
|
155
|
+
|
|
156
|
+
@objc public override init() {
|
|
157
|
+
super.init()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@objc public func initialize(container: UIView, configuration: PlayerConfiguration? = nil) {
|
|
161
|
+
self.containerView = container
|
|
162
|
+
self.configuration = configuration ?? PlayerConfiguration()
|
|
163
|
+
|
|
164
|
+
setupPlayer()
|
|
165
|
+
applyConfiguration()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@objc public func initializeWithViewController(viewController: UIViewController, configuration: PlayerConfiguration? = nil) {
|
|
169
|
+
self.configuration = configuration ?? PlayerConfiguration()
|
|
170
|
+
|
|
171
|
+
playerViewController = AVPlayerViewController()
|
|
172
|
+
setupPlayer()
|
|
173
|
+
applyConfiguration()
|
|
174
|
+
|
|
175
|
+
if let playerVC = playerViewController {
|
|
176
|
+
playerVC.player = player
|
|
177
|
+
viewController.present(playerVC, animated: true)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private func setupPlayer() {
|
|
182
|
+
player = AVPlayer()
|
|
183
|
+
|
|
184
|
+
// Background audio / playback category
|
|
185
|
+
do {
|
|
186
|
+
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback, options: [])
|
|
187
|
+
try AVAudioSession.sharedInstance().setActive(true)
|
|
188
|
+
} catch {
|
|
189
|
+
if configuration?.debug == true {
|
|
190
|
+
print("[UnifiedVideoPlayer] Failed to set audio session: \(error)")
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if let container = containerView {
|
|
195
|
+
playerLayer = AVPlayerLayer(player: player)
|
|
196
|
+
playerLayer?.frame = container.bounds
|
|
197
|
+
playerLayer?.videoGravity = .resizeAspect
|
|
198
|
+
|
|
199
|
+
if let layer = playerLayer {
|
|
200
|
+
container.layer.addSublayer(layer)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Add default controls if enabled
|
|
204
|
+
if configuration?.controls == true && playerViewController == nil {
|
|
205
|
+
addDefaultControls()
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
setupObservers()
|
|
210
|
+
updateState(.idle)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private func applyConfiguration() {
|
|
214
|
+
guard let config = configuration else { return }
|
|
215
|
+
|
|
216
|
+
player?.volume = config.volume
|
|
217
|
+
player?.rate = config.playbackRate
|
|
218
|
+
player?.isMuted = config.muted
|
|
219
|
+
|
|
220
|
+
if let hex = config.themeColorHex, let ui = UIColor(hex: hex) {
|
|
221
|
+
themeUIColor = ui
|
|
222
|
+
} else {
|
|
223
|
+
themeUIColor = nil
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if config.debug {
|
|
227
|
+
enableDebugLogging()
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// MARK: - Loading Content
|
|
232
|
+
|
|
233
|
+
@objc public func load(source: MediaSource) {
|
|
234
|
+
currentSource = source
|
|
235
|
+
updateState(.loading)
|
|
236
|
+
|
|
237
|
+
guard let url = URL(string: source.url) else {
|
|
238
|
+
let error = NSError(domain: "UnifiedVideoPlayer", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])
|
|
239
|
+
handleError(error)
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Handle DRM if configured
|
|
244
|
+
if let drm = source.drm, drm.type == "fairplay" {
|
|
245
|
+
loadFairPlayContent(url: url, drm: drm)
|
|
246
|
+
} else {
|
|
247
|
+
loadStandardContent(url: url)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Load subtitles if provided
|
|
251
|
+
if let subtitles = source.subtitles {
|
|
252
|
+
loadSubtitles(subtitles)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@objc public func load(url: String) {
|
|
257
|
+
let source = MediaSource(url: url)
|
|
258
|
+
load(source: source)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private func loadStandardContent(url: URL) {
|
|
262
|
+
let asset = AVAsset(url: url)
|
|
263
|
+
let playerItem = AVPlayerItem(asset: asset)
|
|
264
|
+
|
|
265
|
+
// Add observers for the player item
|
|
266
|
+
observePlayerItem(playerItem)
|
|
267
|
+
|
|
268
|
+
player?.replaceCurrentItem(with: playerItem)
|
|
269
|
+
|
|
270
|
+
// Analytics & Remote Center
|
|
271
|
+
analyticsEmitter.onBitrate = { [weak self] br in self?.onQualityChange?(br) }
|
|
272
|
+
analyticsEmitter.startObserving(item: playerItem)
|
|
273
|
+
if let p = player {
|
|
274
|
+
let title = currentSource?.metadata?["title"] as? String
|
|
275
|
+
remoteCenter.enable(for: p, title: title, duration: nil, artworkURL: nil)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Apply start time if configured
|
|
279
|
+
if let startTime = configuration?.startTime, startTime > 0 {
|
|
280
|
+
seek(to: startTime)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Auto-play if configured
|
|
284
|
+
if configuration?.autoPlay == true {
|
|
285
|
+
play()
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private func loadFairPlayContent(url: URL, drm: DRMConfiguration) {
|
|
290
|
+
// FairPlay implementation
|
|
291
|
+
let asset = AVURLAsset(url: url)
|
|
292
|
+
|
|
293
|
+
// Configure FairPlay
|
|
294
|
+
drmManager = FairPlayDRMManager(drm: drm)
|
|
295
|
+
asset.resourceLoader.setDelegate(drmManager, queue: DispatchQueue.main)
|
|
296
|
+
|
|
297
|
+
let playerItem = AVPlayerItem(asset: asset)
|
|
298
|
+
observePlayerItem(playerItem)
|
|
299
|
+
player?.replaceCurrentItem(with: playerItem)
|
|
300
|
+
|
|
301
|
+
// Analytics & Remote Center
|
|
302
|
+
analyticsEmitter.onBitrate = { [weak self] br in self?.onQualityChange?(br) }
|
|
303
|
+
analyticsEmitter.startObserving(item: playerItem)
|
|
304
|
+
if let p = player {
|
|
305
|
+
let title = currentSource?.metadata?["title"] as? String
|
|
306
|
+
remoteCenter.enable(for: p, title: title, duration: nil, artworkURL: nil)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if configuration?.autoPlay == true {
|
|
310
|
+
play()
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private func loadSubtitles(_ subtitles: [SubtitleTrack]) {
|
|
315
|
+
guard let playerItem = player?.currentItem else { return }
|
|
316
|
+
|
|
317
|
+
for subtitle in subtitles {
|
|
318
|
+
guard let url = URL(string: subtitle.url) else { continue }
|
|
319
|
+
|
|
320
|
+
let asset = AVAsset(url: url)
|
|
321
|
+
let track = AVMutableMetadataItem()
|
|
322
|
+
track.identifier = .commonIdentifierTitle
|
|
323
|
+
track.value = subtitle.label as NSString
|
|
324
|
+
track.extendedLanguageTag = subtitle.language
|
|
325
|
+
|
|
326
|
+
// Note: Full subtitle implementation would require more complex AVAssetReader setup
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// MARK: - Playback Control
|
|
331
|
+
|
|
332
|
+
@objc public func play() {
|
|
333
|
+
player?.play()
|
|
334
|
+
isPlaying = true
|
|
335
|
+
updateState(.playing)
|
|
336
|
+
updatePlayPauseIcon()
|
|
337
|
+
onPlay?()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
@objc public func pause() {
|
|
341
|
+
player?.pause()
|
|
342
|
+
isPlaying = false
|
|
343
|
+
updateState(.paused)
|
|
344
|
+
updatePlayPauseIcon()
|
|
345
|
+
onPause?()
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@objc public func stop() {
|
|
349
|
+
player?.pause()
|
|
350
|
+
player?.seek(to: .zero)
|
|
351
|
+
isPlaying = false
|
|
352
|
+
updateState(.idle)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
@objc public func togglePlayPause() {
|
|
356
|
+
if isPlaying {
|
|
357
|
+
pause()
|
|
358
|
+
} else {
|
|
359
|
+
play()
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
@objc public func seek(to time: Double) {
|
|
364
|
+
updateState(.seeking)
|
|
365
|
+
let cmTime = CMTime(seconds: time, preferredTimescale: 1000)
|
|
366
|
+
|
|
367
|
+
player?.seek(to: cmTime, completionHandler: { [weak self] _ in
|
|
368
|
+
self?.updateState(self?.isPlaying == true ? .playing : .paused)
|
|
369
|
+
self?.onSeek?(time)
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
@objc public func seekForward(seconds: Double = 10) {
|
|
374
|
+
let newTime = currentTime + seconds
|
|
375
|
+
seek(to: min(newTime, duration))
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
@objc public func seekBackward(seconds: Double = 10) {
|
|
379
|
+
let newTime = currentTime - seconds
|
|
380
|
+
seek(to: max(newTime, 0))
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// MARK: - Volume Control
|
|
384
|
+
|
|
385
|
+
@objc public func setVolume(_ volume: Float) {
|
|
386
|
+
let clampedVolume = max(0, min(1, volume))
|
|
387
|
+
player?.volume = clampedVolume
|
|
388
|
+
onVolumeChange?(clampedVolume)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
@objc public func mute() {
|
|
392
|
+
player?.isMuted = true
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
@objc public func unmute() {
|
|
396
|
+
player?.isMuted = false
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
@objc public func toggleMute() {
|
|
400
|
+
player?.isMuted = !(player?.isMuted ?? false)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// MARK: - Playback Rate
|
|
404
|
+
|
|
405
|
+
@objc public func setPlaybackRate(_ rate: Float) {
|
|
406
|
+
player?.rate = rate
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
@objc public func getPlaybackRate() -> Float {
|
|
410
|
+
return player?.rate ?? 1.0
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// MARK: - State Management
|
|
414
|
+
|
|
415
|
+
private func updateState(_ newState: PlayerState) {
|
|
416
|
+
state = newState
|
|
417
|
+
onStateChange?(newState)
|
|
418
|
+
|
|
419
|
+
if configuration?.debug == true {
|
|
420
|
+
print("[UnifiedVideoPlayer] State changed to: \(newState)")
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// MARK: - Observers
|
|
425
|
+
|
|
426
|
+
private func setupObservers() {
|
|
427
|
+
// Time observer
|
|
428
|
+
let interval = CMTime(seconds: 0.1, preferredTimescale: 1000)
|
|
429
|
+
timeObserver = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
|
|
430
|
+
self?.handleTimeUpdate(time)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Rate observer (play/pause)
|
|
434
|
+
rateObserver = player?.observe(\.rate, options: [.new]) { [weak self] player, _ in
|
|
435
|
+
self?.handleRateChange(player.rate)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Status observer
|
|
439
|
+
statusObserver = player?.observe(\.status, options: [.new]) { [weak self] player, _ in
|
|
440
|
+
self?.handleStatusChange(player.status)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Notification observers
|
|
444
|
+
NotificationCenter.default.addObserver(
|
|
445
|
+
self,
|
|
446
|
+
selector: #selector(playerDidFinishPlaying),
|
|
447
|
+
name: .AVPlayerItemDidPlayToEndTime,
|
|
448
|
+
object: nil
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
NotificationCenter.default.addObserver(
|
|
452
|
+
self,
|
|
453
|
+
selector: #selector(handlePlaybackStalled),
|
|
454
|
+
name: .AVPlayerItemPlaybackStalled,
|
|
455
|
+
object: nil
|
|
456
|
+
)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private func observePlayerItem(_ playerItem: AVPlayerItem) {
|
|
460
|
+
currentItemObserver = playerItem.observe(\.status, options: [.new]) { [weak self] item, _ in
|
|
461
|
+
self?.handleItemStatusChange(item.status)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Observe buffering
|
|
465
|
+
playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .new, context: nil)
|
|
466
|
+
playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil)
|
|
467
|
+
playerItem.addObserver(self, forKeyPath: "playbackBufferFull", options: .new, context: nil)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
|
471
|
+
if keyPath == "playbackBufferEmpty" {
|
|
472
|
+
updateState(.buffering)
|
|
473
|
+
onBuffering?(true)
|
|
474
|
+
} else if keyPath == "playbackLikelyToKeepUp" || keyPath == "playbackBufferFull" {
|
|
475
|
+
if isPlaying {
|
|
476
|
+
updateState(.playing)
|
|
477
|
+
} else {
|
|
478
|
+
updateState(.paused)
|
|
479
|
+
}
|
|
480
|
+
onBuffering?(false)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private func handleTimeUpdate(_ time: CMTime) {
|
|
485
|
+
currentTime = time.seconds
|
|
486
|
+
onTimeUpdate?(currentTime)
|
|
487
|
+
|
|
488
|
+
// Update buffered time
|
|
489
|
+
if let timeRanges = player?.currentItem?.loadedTimeRanges,
|
|
490
|
+
let range = timeRanges.first?.timeRangeValue {
|
|
491
|
+
let bufferedEnd = range.start + range.duration
|
|
492
|
+
bufferedTime = bufferedEnd.seconds
|
|
493
|
+
onProgress?(bufferedTime)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Sync lock screen info
|
|
497
|
+
remoteCenter.updateProgress(elapsed: currentTime, duration: duration, isPlaying: player?.timeControlStatus == .playing)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private func handleRateChange(_ rate: Float) {
|
|
501
|
+
isPlaying = rate > 0
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private func handleStatusChange(_ status: AVPlayer.Status) {
|
|
505
|
+
switch status {
|
|
506
|
+
case .readyToPlay:
|
|
507
|
+
updateDuration()
|
|
508
|
+
updateState(.ready)
|
|
509
|
+
onReady?()
|
|
510
|
+
emitLoadedMetadata()
|
|
511
|
+
case .failed:
|
|
512
|
+
if let error = player?.error {
|
|
513
|
+
handleError(error)
|
|
514
|
+
}
|
|
515
|
+
default:
|
|
516
|
+
break
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private func handleItemStatusChange(_ status: AVPlayerItem.Status) {
|
|
521
|
+
switch status {
|
|
522
|
+
case .readyToPlay:
|
|
523
|
+
updateDuration()
|
|
524
|
+
case .failed:
|
|
525
|
+
if let error = player?.currentItem?.error {
|
|
526
|
+
handleError(error)
|
|
527
|
+
}
|
|
528
|
+
default:
|
|
529
|
+
break
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
@objc private func playerDidFinishPlaying() {
|
|
534
|
+
updateState(.ended)
|
|
535
|
+
onEnded?()
|
|
536
|
+
|
|
537
|
+
if configuration?.loop == true {
|
|
538
|
+
seek(to: 0)
|
|
539
|
+
play()
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
@objc private func handlePlaybackStalled() {
|
|
544
|
+
updateState(.buffering)
|
|
545
|
+
onBuffering?(true)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private func updateDuration() {
|
|
549
|
+
if let duration = player?.currentItem?.duration {
|
|
550
|
+
self.duration = duration.seconds
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private func emitLoadedMetadata() {
|
|
555
|
+
guard let item = player?.currentItem else { return }
|
|
556
|
+
|
|
557
|
+
var metadata: [String: Any] = [:]
|
|
558
|
+
metadata["duration"] = duration
|
|
559
|
+
|
|
560
|
+
if let videoTrack = item.asset.tracks(withMediaType: .video).first {
|
|
561
|
+
metadata["width"] = videoTrack.naturalSize.width
|
|
562
|
+
metadata["height"] = videoTrack.naturalSize.height
|
|
563
|
+
metadata["fps"] = videoTrack.nominalFrameRate
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if let audioTrack = item.asset.tracks(withMediaType: .audio).first {
|
|
567
|
+
metadata["audioChannels"] = audioTrack.formatDescriptions
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
onLoadedMetadata?(metadata)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// MARK: - Tracks / PiP / AirPlay
|
|
574
|
+
|
|
575
|
+
@objc public func audioTracks() -> [String] {
|
|
576
|
+
guard let group = player?.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .audible) else { return [] }
|
|
577
|
+
return group.options.map { $0.displayName }
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
@objc public func subtitleTracks() -> [String] {
|
|
581
|
+
guard let group = player?.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else { return [] }
|
|
582
|
+
return group.options.map { $0.displayName }
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
@objc public func selectAudioTrack(index: Int) {
|
|
586
|
+
guard let group = player?.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .audible), index >= 0, index < group.options.count else { return }
|
|
587
|
+
let option = group.options[index]
|
|
588
|
+
player?.currentItem?.select(option, in: group)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
@objc public func selectSubtitleTrack(index: Int) {
|
|
592
|
+
guard let group = player?.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else { return }
|
|
593
|
+
if index >= 0, index < group.options.count {
|
|
594
|
+
let option = group.options[index]
|
|
595
|
+
player?.currentItem?.select(option, in: group)
|
|
596
|
+
} else {
|
|
597
|
+
player?.currentItem?.select(nil, in: group)
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
@objc public func startPictureInPicture() {
|
|
602
|
+
guard AVPictureInPictureController.isPictureInPictureSupported() else { return }
|
|
603
|
+
if pipController == nil, let layer = playerLayer { pipController = AVPictureInPictureController(playerLayer: layer) }
|
|
604
|
+
pipController?.startPictureInPicture()
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
@objc public func stopPictureInPicture() {
|
|
608
|
+
pipController?.stopPictureInPicture()
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
@objc public func makeAirPlayPickerView(frame: CGRect = .zero) -> AVRoutePickerView {
|
|
612
|
+
return AVRoutePickerView(frame: frame)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// MARK: - UI Controls
|
|
616
|
+
|
|
617
|
+
private func addDefaultControls() {
|
|
618
|
+
guard let container = containerView else { return }
|
|
619
|
+
|
|
620
|
+
// Create a simple control overlay
|
|
621
|
+
let controlsView = UIView(frame: container.bounds)
|
|
622
|
+
controlsView.backgroundColor = UIColor.black.withAlphaComponent(0.3)
|
|
623
|
+
controlsView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
624
|
+
controlsView.tag = 999 // Tag for identification
|
|
625
|
+
|
|
626
|
+
// Play/Pause button (SF Symbol)
|
|
627
|
+
let playPauseButton = UIButton(type: .system)
|
|
628
|
+
playPauseButton.tag = 1001
|
|
629
|
+
playPauseButton.frame = CGRect(x: container.bounds.width/2 - 30, y: container.bounds.height/2 - 30, width: 60, height: 60)
|
|
630
|
+
let initialIcon = UIImage(systemName: "play.fill")
|
|
631
|
+
playPauseButton.setImage(initialIcon, for: .normal)
|
|
632
|
+
playPauseButton.imageView?.contentMode = .scaleAspectFit
|
|
633
|
+
playPauseButton.tintColor = themeUIColor ?? .white
|
|
634
|
+
playPauseButton.addTarget(self, action: #selector(handlePlayPauseButton), for: .touchUpInside)
|
|
635
|
+
|
|
636
|
+
controlsView.addSubview(playPauseButton)
|
|
637
|
+
|
|
638
|
+
// AirPlay route picker (top-right)
|
|
639
|
+
let routePicker = AVRoutePickerView(frame: CGRect(x: container.bounds.width - 56, y: 16, width: 40, height: 40))
|
|
640
|
+
routePicker.tintColor = themeUIColor ?? .white
|
|
641
|
+
if #available(iOS 13.0, *) { routePicker.activeTintColor = themeUIColor ?? .white }
|
|
642
|
+
routePicker.autoresizingMask = [.flexibleLeftMargin, .flexibleBottomMargin]
|
|
643
|
+
controlsView.addSubview(routePicker)
|
|
644
|
+
|
|
645
|
+
// PiP button next to AirPlay
|
|
646
|
+
let pipButton = UIButton(type: .system)
|
|
647
|
+
pipButton.tag = 1002
|
|
648
|
+
pipButton.frame = CGRect(x: routePicker.frame.minX - 48, y: 16, width: 40, height: 40)
|
|
649
|
+
let pipIcon: UIImage?
|
|
650
|
+
if #available(iOS 15.0, *) { pipIcon = UIImage(systemName: "pip") } else { pipIcon = UIImage(systemName: "rectangle") }
|
|
651
|
+
pipButton.setImage(pipIcon, for: .normal)
|
|
652
|
+
pipButton.tintColor = themeUIColor ?? .white
|
|
653
|
+
pipButton.addTarget(self, action: #selector(handlePiPButton), for: .touchUpInside)
|
|
654
|
+
pipButton.autoresizingMask = [.flexibleLeftMargin, .flexibleBottomMargin]
|
|
655
|
+
controlsView.addSubview(pipButton)
|
|
656
|
+
|
|
657
|
+
container.addSubview(controlsView)
|
|
658
|
+
|
|
659
|
+
// Auto-hide controls
|
|
660
|
+
setupControlsAutoHide(controlsView)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
private func setupControlsAutoHide(_ controlsView: UIView) {
|
|
664
|
+
// Hide controls after 3 seconds
|
|
665
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak controlsView] in
|
|
666
|
+
UIView.animate(withDuration: 0.3) {
|
|
667
|
+
controlsView?.alpha = 0
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Show controls on tap
|
|
672
|
+
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleControlsTap))
|
|
673
|
+
containerView?.addGestureRecognizer(tapGesture)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
@objc private func handlePlayPauseButton() {
|
|
677
|
+
togglePlayPause()
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
private func updatePlayPauseIcon() {
|
|
681
|
+
guard let controlsView = containerView?.viewWithTag(999),
|
|
682
|
+
let button = controlsView.viewWithTag(1001) as? UIButton else { return }
|
|
683
|
+
let iconName = isPlaying ? "pause.fill" : "play.fill"
|
|
684
|
+
button.setImage(UIImage(systemName: iconName), for: .normal)
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
@objc private func handlePiPButton() {
|
|
688
|
+
if let c = pipController, c.isPictureInPictureActive {
|
|
689
|
+
c.stopPictureInPicture()
|
|
690
|
+
} else {
|
|
691
|
+
startPictureInPicture()
|
|
692
|
+
}
|
|
693
|
+
updatePiPButtonIcon()
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
private func updatePiPButtonIcon() {
|
|
697
|
+
guard let controlsView = containerView?.viewWithTag(999),
|
|
698
|
+
let button = controlsView.viewWithTag(1002) as? UIButton else { return }
|
|
699
|
+
if #available(iOS 15.0, *) {
|
|
700
|
+
let name = (pipController?.isPictureInPictureActive ?? false) ? "pip.exit" : "pip"
|
|
701
|
+
button.setImage(UIImage(systemName: name), for: .normal)
|
|
702
|
+
} else {
|
|
703
|
+
button.setImage(UIImage(systemName: "rectangle"), for: .normal)
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
@objc private func handleControlsTap() {
|
|
708
|
+
guard let controlsView = containerView?.viewWithTag(999) else { return }
|
|
709
|
+
|
|
710
|
+
UIView.animate(withDuration: 0.3) {
|
|
711
|
+
controlsView.alpha = controlsView.alpha == 0 ? 1 : 0
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if controlsView.alpha == 1 {
|
|
715
|
+
// Auto-hide after 3 seconds
|
|
716
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak controlsView] in
|
|
717
|
+
UIView.animate(withDuration: 0.3) {
|
|
718
|
+
controlsView?.alpha = 0
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// MARK: - Error Handling
|
|
725
|
+
|
|
726
|
+
private func handleError(_ error: Error) {
|
|
727
|
+
updateState(.error)
|
|
728
|
+
onError?(error)
|
|
729
|
+
|
|
730
|
+
if configuration?.debug == true {
|
|
731
|
+
print("[UnifiedVideoPlayer] Error: \(error.localizedDescription)")
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// MARK: - Debug
|
|
736
|
+
|
|
737
|
+
private func enableDebugLogging() {
|
|
738
|
+
print("[UnifiedVideoPlayer] Debug mode enabled")
|
|
739
|
+
print("[UnifiedVideoPlayer] Configuration: \(String(describing: configuration))")
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// MARK: - Cleanup
|
|
743
|
+
|
|
744
|
+
@objc public func destroy() {
|
|
745
|
+
player?.pause()
|
|
746
|
+
|
|
747
|
+
if let observer = timeObserver {
|
|
748
|
+
player?.removeTimeObserver(observer)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
statusObserver?.invalidate()
|
|
752
|
+
rateObserver?.invalidate()
|
|
753
|
+
currentItemObserver?.invalidate()
|
|
754
|
+
|
|
755
|
+
NotificationCenter.default.removeObserver(self)
|
|
756
|
+
analyticsEmitter.stopObserving()
|
|
757
|
+
remoteCenter.disable()
|
|
758
|
+
|
|
759
|
+
playerLayer?.removeFromSuperlayer()
|
|
760
|
+
playerViewController?.dismiss(animated: false)
|
|
761
|
+
|
|
762
|
+
player = nil
|
|
763
|
+
playerLayer = nil
|
|
764
|
+
playerViewController = nil
|
|
765
|
+
containerView = nil
|
|
766
|
+
|
|
767
|
+
updateState(.idle)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
deinit {
|
|
771
|
+
destroy()
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// MARK: - AVAssetResourceLoaderDelegate (for FairPlay DRM)
|
|
776
|
+
extension UnifiedVideoPlayer: AVAssetResourceLoaderDelegate {
|
|
777
|
+
|
|
778
|
+
public func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
|
|
779
|
+
// FairPlay DRM implementation
|
|
780
|
+
guard let url = loadingRequest.request.url,
|
|
781
|
+
url.scheme == "skd",
|
|
782
|
+
let drm = currentSource?.drm,
|
|
783
|
+
drm.type == "fairplay" else {
|
|
784
|
+
return false
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Handle FairPlay license request
|
|
788
|
+
handleFairPlayRequest(loadingRequest, drm: drm)
|
|
789
|
+
return true
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
private func handleFairPlayRequest(_ loadingRequest: AVAssetResourceLoadingRequest, drm: DRMConfiguration) {
|
|
793
|
+
// Implementation for FairPlay license acquisition
|
|
794
|
+
// This would include:
|
|
795
|
+
// 1. Getting the content ID from the URL
|
|
796
|
+
// 2. Getting the SPC (Server Playback Context) from the request
|
|
797
|
+
// 3. Sending the SPC to the license server
|
|
798
|
+
// 4. Processing the CKC (Content Key Context) response
|
|
799
|
+
// 5. Providing the CKC to the loading request
|
|
800
|
+
|
|
801
|
+
// Simplified example:
|
|
802
|
+
guard let certificateUrl = drm.certificateUrl,
|
|
803
|
+
let certificateURL = URL(string: certificateUrl) else {
|
|
804
|
+
loadingRequest.finishLoading(with: NSError(domain: "UnifiedVideoPlayer", code: -2, userInfo: nil))
|
|
805
|
+
return
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// This is a simplified implementation
|
|
809
|
+
// Real implementation would involve proper FairPlay flow
|
|
810
|
+
}
|
|
811
|
+
}
|