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.
Files changed (129) hide show
  1. package/.github/workflows/ci.yml +253 -0
  2. package/ANDROID_TV_IMPLEMENTATION.md +313 -0
  3. package/COMPLETION_STATUS.md +165 -0
  4. package/CONTRIBUTING.md +376 -0
  5. package/FINAL_STATUS_REPORT.md +170 -0
  6. package/FRAMEWORK_REVIEW.md +247 -0
  7. package/IMPROVEMENTS_SUMMARY.md +168 -0
  8. package/LICENSE +21 -0
  9. package/NATIVE_APP_INTEGRATION_GUIDE.md +903 -0
  10. package/PAYWALL_RENTAL_FLOW.md +499 -0
  11. package/PLATFORM_SETUP_GUIDE.md +1636 -0
  12. package/README.md +315 -0
  13. package/RUN_LOCALLY.md +151 -0
  14. package/apps/demo/cast-sender-min.html +173 -0
  15. package/apps/demo/custom-player.html +883 -0
  16. package/apps/demo/demo.html +990 -0
  17. package/apps/demo/enhanced-player.html +3556 -0
  18. package/apps/demo/index.html +159 -0
  19. package/apps/rental-api/.env.example +24 -0
  20. package/apps/rental-api/README.md +23 -0
  21. package/apps/rental-api/migrations/001_init.sql +35 -0
  22. package/apps/rental-api/migrations/002_videos.sql +10 -0
  23. package/apps/rental-api/migrations/003_add_gateway_subref.sql +4 -0
  24. package/apps/rental-api/migrations/004_update_gateways.sql +4 -0
  25. package/apps/rental-api/migrations/005_seed_demo_video.sql +5 -0
  26. package/apps/rental-api/package-lock.json +2045 -0
  27. package/apps/rental-api/package.json +33 -0
  28. package/apps/rental-api/scripts/run-migration.js +42 -0
  29. package/apps/rental-api/scripts/update-video-currency.js +21 -0
  30. package/apps/rental-api/scripts/update-video-price.js +19 -0
  31. package/apps/rental-api/src/config.ts +14 -0
  32. package/apps/rental-api/src/db.ts +10 -0
  33. package/apps/rental-api/src/routes/cashfree.ts +167 -0
  34. package/apps/rental-api/src/routes/pesapal.ts +92 -0
  35. package/apps/rental-api/src/routes/rentals.ts +242 -0
  36. package/apps/rental-api/src/routes/webhooks.ts +73 -0
  37. package/apps/rental-api/src/server.ts +41 -0
  38. package/apps/rental-api/src/services/entitlements.ts +45 -0
  39. package/apps/rental-api/src/services/payments.ts +22 -0
  40. package/apps/rental-api/tsconfig.json +17 -0
  41. package/check-urls.ps1 +74 -0
  42. package/comparison-report.md +181 -0
  43. package/docs/PAYWALL.md +95 -0
  44. package/docs/PLAYER_UI_VISIBILITY.md +431 -0
  45. package/docs/README.md +7 -0
  46. package/docs/SYSTEM_ARCHITECTURE.md +612 -0
  47. package/docs/VDOCIPHER_CLONE_REQUIREMENTS.md +403 -0
  48. package/examples/android/JavaSampleApp/MainActivity.java +641 -0
  49. package/examples/android/JavaSampleApp/activity_main.xml +226 -0
  50. package/examples/android/SampleApp/MainActivity.kt +430 -0
  51. package/examples/ios/SampleApp/ViewController.swift +337 -0
  52. package/examples/ios/SwiftUISampleApp/ContentView.swift +304 -0
  53. package/iOS_IMPLEMENTATION_OPTIONS.md +470 -0
  54. package/ios/UnifiedVideoPlayer/UnifiedVideoPlayer.podspec +33 -0
  55. package/jest.config.js +33 -0
  56. package/jitpack.yml +5 -0
  57. package/lerna.json +35 -0
  58. package/package.json +69 -0
  59. package/packages/PLATFORM_STATUS.md +163 -0
  60. package/packages/android/build.gradle +135 -0
  61. package/packages/android/src/main/AndroidManifest.xml +36 -0
  62. package/packages/android/src/main/java/com/unifiedvideo/player/PlayerConfiguration.java +221 -0
  63. package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.java +1037 -0
  64. package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.kt +707 -0
  65. package/packages/android/src/main/java/com/unifiedvideo/player/analytics/AnalyticsProvider.java +9 -0
  66. package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastManager.java +141 -0
  67. package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastOptionsProvider.java +29 -0
  68. package/packages/android/src/main/java/com/unifiedvideo/player/overlay/WatermarkOverlayView.java +88 -0
  69. package/packages/android/src/main/java/com/unifiedvideo/player/pip/PipActionReceiver.java +33 -0
  70. package/packages/android/src/main/java/com/unifiedvideo/player/services/PlaybackService.java +110 -0
  71. package/packages/android/src/main/java/com/unifiedvideo/player/services/PlayerHolder.java +19 -0
  72. package/packages/core/package.json +34 -0
  73. package/packages/core/src/BasePlayer.ts +250 -0
  74. package/packages/core/src/VideoPlayer.ts +237 -0
  75. package/packages/core/src/VideoPlayerFactory.ts +145 -0
  76. package/packages/core/src/index.ts +20 -0
  77. package/packages/core/src/interfaces/IVideoPlayer.ts +184 -0
  78. package/packages/core/src/interfaces.ts +240 -0
  79. package/packages/core/src/utils/EventEmitter.ts +66 -0
  80. package/packages/core/src/utils/PlatformDetector.ts +300 -0
  81. package/packages/core/tsconfig.json +20 -0
  82. package/packages/enact/package.json +51 -0
  83. package/packages/enact/src/VideoPlayer.js +365 -0
  84. package/packages/enact/src/adapters/TizenAdapter.js +354 -0
  85. package/packages/enact/src/index.js +82 -0
  86. package/packages/ios/BUILD_INSTRUCTIONS.md +108 -0
  87. package/packages/ios/FIX_EMBED_ISSUE.md +142 -0
  88. package/packages/ios/GETTING_STARTED.md +100 -0
  89. package/packages/ios/Package.swift +35 -0
  90. package/packages/ios/README.md +84 -0
  91. package/packages/ios/Sources/UnifiedVideoPlayer/Analytics/AnalyticsEmitter.swift +26 -0
  92. package/packages/ios/Sources/UnifiedVideoPlayer/DRM/FairPlayDRMManager.swift +102 -0
  93. package/packages/ios/Sources/UnifiedVideoPlayer/Info.plist +24 -0
  94. package/packages/ios/Sources/UnifiedVideoPlayer/Remote/RemoteCommandCenter.swift +109 -0
  95. package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayer.swift +811 -0
  96. package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayerView.swift +640 -0
  97. package/packages/ios/Sources/UnifiedVideoPlayer/Utilities/Color+Hex.swift +36 -0
  98. package/packages/ios/UnifiedVideoPlayer.podspec +27 -0
  99. package/packages/ios/UnifiedVideoPlayer.xcodeproj/project.pbxproj +385 -0
  100. package/packages/ios/build_framework.sh +55 -0
  101. package/packages/react-native/android/src/main/java/com/unifiedvideo/UnifiedVideoPlayerModule.kt +482 -0
  102. package/packages/react-native/ios/UnifiedVideoPlayer.swift +436 -0
  103. package/packages/react-native/package.json +51 -0
  104. package/packages/react-native/src/ReactNativePlayer.tsx +423 -0
  105. package/packages/react-native/src/VideoPlayer.tsx +224 -0
  106. package/packages/react-native/src/index.ts +28 -0
  107. package/packages/react-native/src/utils/EventEmitter.ts +66 -0
  108. package/packages/react-native/tsconfig.json +31 -0
  109. package/packages/roku/components/UnifiedVideoPlayer.brs +400 -0
  110. package/packages/roku/package.json +44 -0
  111. package/packages/roku/source/VideoPlayer.brs +231 -0
  112. package/packages/roku/source/main.brs +28 -0
  113. package/packages/web/GETTING_STARTED.md +292 -0
  114. package/packages/web/jest.config.js +28 -0
  115. package/packages/web/jest.setup.ts +110 -0
  116. package/packages/web/package.json +50 -0
  117. package/packages/web/src/SecureVideoPlayer.ts +1164 -0
  118. package/packages/web/src/WebPlayer.ts +3110 -0
  119. package/packages/web/src/__tests__/WebPlayer.test.ts +314 -0
  120. package/packages/web/src/index.ts +14 -0
  121. package/packages/web/src/paywall/PaywallController.ts +215 -0
  122. package/packages/web/src/react/WebPlayerView.tsx +177 -0
  123. package/packages/web/tsconfig.json +23 -0
  124. package/packages/web/webpack.config.js +45 -0
  125. package/server.js +131 -0
  126. package/server.py +84 -0
  127. package/test-urls.ps1 +97 -0
  128. package/test-video-urls.ps1 +87 -0
  129. package/tsconfig.json +39 -0
@@ -0,0 +1,640 @@
1
+ //
2
+ // UnifiedVideoPlayerView.swift
3
+ // UnifiedVideoPlayer
4
+ //
5
+ // SwiftUI implementation of the Unified Video Framework
6
+ //
7
+
8
+ import SwiftUI
9
+ import AVKit
10
+ import AVFoundation
11
+ import Combine
12
+ import UIKit
13
+
14
+ // MARK: - SwiftUI Video Player View
15
+ @available(iOS 14.0, *)
16
+ public struct UnifiedVideoPlayerView: UIViewControllerRepresentable {
17
+ @Binding var player: UnifiedVideoPlayerModel
18
+ var showsControls: Bool = true
19
+
20
+ public init(player: Binding<UnifiedVideoPlayerModel>, showsControls: Bool = true) {
21
+ self._player = player
22
+ self.showsControls = showsControls
23
+ }
24
+
25
+ public func makeUIViewController(context: Context) -> AVPlayerViewController {
26
+ let controller = AVPlayerViewController()
27
+ controller.player = player.avPlayer
28
+ controller.showsPlaybackControls = showsControls
29
+ return controller
30
+ }
31
+
32
+ public func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
33
+ uiViewController.player = player.avPlayer
34
+ uiViewController.showsPlaybackControls = showsControls
35
+ }
36
+ }
37
+
38
+ // MARK: - Custom SwiftUI Video Player with Controls
39
+ @available(iOS 14.0, *)
40
+ public struct CustomUnifiedVideoPlayer: View {
41
+ @StateObject private var playerModel: UnifiedVideoPlayerModel
42
+ @State private var showControls: Bool = true
43
+ @State private var hideControlsTimer: Timer?
44
+ @State private var pipController: AVPictureInPictureController? = nil
45
+
46
+ // Configuration
47
+ private let configuration: PlayerConfiguration
48
+ private var accent: Color { Color(hex: configuration.themeColorHex ?? "") ?? .white }
49
+ private var uiAccent: UIColor { UIColor(hex: configuration.themeColorHex ?? "") ?? .white }
50
+
51
+ public init(url: String, configuration: PlayerConfiguration = PlayerConfiguration()) {
52
+ self.configuration = configuration
53
+ self._playerModel = StateObject(wrappedValue: UnifiedVideoPlayerModel(url: url, configuration: configuration))
54
+ }
55
+
56
+ public init(source: MediaSource, configuration: PlayerConfiguration = PlayerConfiguration()) {
57
+ self.configuration = configuration
58
+ self._playerModel = StateObject(wrappedValue: UnifiedVideoPlayerModel(source: source, configuration: configuration))
59
+ }
60
+
61
+ public var body: some View {
62
+ ZStack {
63
+ // Video Player Layer
64
+ VideoPlayerLayer(player: playerModel.avPlayer, pipController: $pipController)
65
+ .onTapGesture {
66
+ withAnimation(.easeInOut(duration: 0.3)) {
67
+ showControls.toggle()
68
+ resetHideControlsTimer()
69
+ }
70
+ }
71
+
72
+ // Custom Controls Overlay
73
+ if showControls && configuration.controls {
74
+ VideoControlsOverlay(
75
+ playerModel: playerModel,
76
+ accent: accent,
77
+ uiAccent: uiAccent,
78
+ pipController: pipController,
79
+ onTogglePiP: {
80
+ if let c = pipController {
81
+ if c.isPictureInPictureActive { c.stopPictureInPicture() }
82
+ else { c.startPictureInPicture() }
83
+ }
84
+ }
85
+ )
86
+ .transition(.opacity)
87
+ }
88
+
89
+ // Loading Indicator
90
+ if playerModel.isBuffering {
91
+ ProgressView()
92
+ .progressViewStyle(CircularProgressViewStyle(tint: .white))
93
+ .scaleEffect(1.5)
94
+ }
95
+
96
+ // Error Message
97
+ if let error = playerModel.error {
98
+ VStack {
99
+ Image(systemName: "exclamationmark.triangle")
100
+ .font(.largeTitle)
101
+ .foregroundColor(.white)
102
+ Text(error.localizedDescription)
103
+ .foregroundColor(.white)
104
+ .padding()
105
+ }
106
+ .background(Color.black.opacity(0.7))
107
+ .cornerRadius(10)
108
+ }
109
+ }
110
+ .background(Color.black)
111
+ .onAppear {
112
+ playerModel.initialize()
113
+ resetHideControlsTimer()
114
+ }
115
+ .onDisappear {
116
+ playerModel.cleanup()
117
+ }
118
+ }
119
+
120
+ private func resetHideControlsTimer() {
121
+ hideControlsTimer?.invalidate()
122
+ hideControlsTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in
123
+ withAnimation {
124
+ showControls = false
125
+ }
126
+ }
127
+ }
128
+ }
129
+
130
+ // MARK: - Video Player Layer
131
+ @available(iOS 14.0, *)
132
+ struct VideoPlayerLayer: UIViewRepresentable {
133
+ let player: AVPlayer?
134
+ @Binding var pipController: AVPictureInPictureController?
135
+
136
+ func makeUIView(context: Context) -> PlayerUIView {
137
+ let view = PlayerUIView(player: player)
138
+ if AVPictureInPictureController.isPictureInPictureSupported(), pipController == nil, let layer = view.playerLayer {
139
+ pipController = AVPictureInPictureController(playerLayer: layer)
140
+ }
141
+ return view
142
+ }
143
+
144
+ func updateUIView(_ uiView: PlayerUIView, context: Context) {
145
+ uiView.updatePlayer(player)
146
+ if AVPictureInPictureController.isPictureInPictureSupported(), pipController == nil, let layer = uiView.playerLayer {
147
+ pipController = AVPictureInPictureController(playerLayer: layer)
148
+ }
149
+ }
150
+
151
+ class PlayerUIView: UIView {
152
+ var playerLayer: AVPlayerLayer?
153
+
154
+ init(player: AVPlayer?) {
155
+ super.init(frame: .zero)
156
+ setupPlayerLayer(with: player)
157
+ }
158
+
159
+ required init?(coder: NSCoder) {
160
+ super.init(coder: coder)
161
+ }
162
+
163
+ override func layoutSubviews() {
164
+ super.layoutSubviews()
165
+ playerLayer?.frame = bounds
166
+ }
167
+
168
+ func setupPlayerLayer(with player: AVPlayer?) {
169
+ playerLayer?.removeFromSuperlayer()
170
+
171
+ guard let player = player else { return }
172
+
173
+ let newPlayerLayer = AVPlayerLayer(player: player)
174
+ newPlayerLayer.videoGravity = .resizeAspect
175
+ newPlayerLayer.frame = bounds
176
+ layer.addSublayer(newPlayerLayer)
177
+ playerLayer = newPlayerLayer
178
+ }
179
+
180
+ func updatePlayer(_ player: AVPlayer?) {
181
+ playerLayer?.player = player
182
+ }
183
+ }
184
+ }
185
+
186
+ // MARK: - AirPlay Route Picker (SwiftUI wrapper)
187
+ @available(iOS 13.0, *)
188
+ struct AirPlayRoutePicker: UIViewRepresentable {
189
+ let tint: UIColor
190
+ func makeUIView(context: Context) -> AVRoutePickerView {
191
+ let v = AVRoutePickerView(frame: .zero)
192
+ v.tintColor = tint
193
+ if #available(iOS 13.0, *) { v.activeTintColor = tint }
194
+ return v
195
+ }
196
+ func updateUIView(_ uiView: AVRoutePickerView, context: Context) {
197
+ uiView.tintColor = tint
198
+ if #available(iOS 13.0, *) { uiView.activeTintColor = tint }
199
+ }
200
+ }
201
+
202
+ // MARK: - Video Controls Overlay
203
+ @available(iOS 14.0, *)
204
+ struct VideoControlsOverlay: View {
205
+ @ObservedObject var playerModel: UnifiedVideoPlayerModel
206
+ let accent: Color
207
+ let uiAccent: UIColor
208
+ let pipController: AVPictureInPictureController?
209
+ let onTogglePiP: () -> Void
210
+ @State private var isDraggingSlider: Bool = false
211
+
212
+ var body: some View {
213
+ VStack {
214
+ // Top Bar
215
+ HStack {
216
+ if let title = playerModel.title {
217
+ Text(title)
218
+ .font(.headline)
219
+ .foregroundColor(.white)
220
+ }
221
+ Spacer()
222
+
223
+ // AirPlay + PiP
224
+ HStack(spacing: 16) {
225
+ if #available(iOS 13.0, *) {
226
+ AirPlayRoutePicker(tint: uiAccent)
227
+ .frame(width: 28, height: 28)
228
+ }
229
+ if AVPictureInPictureController.isPictureInPictureSupported() && pipController != nil {
230
+ Button(action: onTogglePiP) {
231
+ if #available(iOS 15.0, *) {
232
+ Image(systemName: (pipController?.isPictureInPictureActive ?? false) ? "pip.exit" : "pip.enter")
233
+ .foregroundColor(accent)
234
+ .font(.title2)
235
+ } else {
236
+ Image(systemName: "rectangle")
237
+ .foregroundColor(accent)
238
+ .font(.title2)
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ // Settings Menu
245
+ Menu {
246
+ // Quality Selection
247
+ Menu("Quality") {
248
+ ForEach(["Auto", "1080p", "720p", "480p", "360p"], id: \.self) { quality in
249
+ Button(quality) {
250
+ playerModel.setVideoQuality(quality)
251
+ }
252
+ }
253
+ }
254
+
255
+ // Playback Speed
256
+ Menu("Speed") {
257
+ ForEach([0.5, 0.75, 1.0, 1.25, 1.5, 2.0], id: \.self) { speed in
258
+ Button("\(speed)x") {
259
+ playerModel.setPlaybackRate(Float(speed))
260
+ }
261
+ }
262
+ }
263
+
264
+ // Subtitles
265
+ if !playerModel.availableSubtitles.isEmpty {
266
+ Menu("Subtitles") {
267
+ Button("Off") {
268
+ playerModel.selectSubtitle(nil)
269
+ }
270
+ ForEach(playerModel.availableSubtitles, id: \.language) { subtitle in
271
+ Button(subtitle.label) {
272
+ playerModel.selectSubtitle(subtitle)
273
+ }
274
+ }
275
+ }
276
+ }
277
+ } label: {
278
+ Image(systemName: "ellipsis")
279
+ .foregroundColor(accent)
280
+ .font(.title2)
281
+ }
282
+ }
283
+ .padding()
284
+ .background(LinearGradient(
285
+ gradient: Gradient(colors: [Color.black.opacity(0.7), Color.clear]),
286
+ startPoint: .top,
287
+ endPoint: .bottom
288
+ ))
289
+
290
+ Spacer()
291
+
292
+ // Center Play/Pause Button
293
+ if !playerModel.isPlaying {
294
+ Button(action: {
295
+ playerModel.play()
296
+ }) {
297
+ Image(systemName: "play.circle.fill")
298
+ .font(.system(size: 70))
299
+ .foregroundColor(accent.opacity(0.9))
300
+ }
301
+ }
302
+
303
+ Spacer()
304
+
305
+ // Bottom Controls
306
+ VStack(spacing: 10) {
307
+ // Progress Bar
308
+ HStack {
309
+ Text(formatTime(playerModel.currentTime))
310
+ .font(.caption)
311
+ .foregroundColor(.white)
312
+
313
+ Slider(
314
+ value: Binding(
315
+ get: { isDraggingSlider ? playerModel.seekTime : playerModel.currentTime },
316
+ set: { playerModel.seekTime = $0 }
317
+ ),
318
+ in: 0...max(playerModel.duration, 1),
319
+ onEditingChanged: { editing in
320
+ isDraggingSlider = editing
321
+ if !editing {
322
+ playerModel.seek(to: playerModel.seekTime)
323
+ }
324
+ }
325
+ )
326
+ .accentColor(accent)
327
+
328
+ Text(formatTime(playerModel.duration))
329
+ .font(.caption)
330
+ .foregroundColor(.white)
331
+ }
332
+ .padding(.horizontal)
333
+
334
+ // Control Buttons
335
+ HStack(spacing: 30) {
336
+ // Play/Pause
337
+ Button(action: {
338
+ playerModel.togglePlayPause()
339
+ }) {
340
+ Image(systemName: playerModel.isPlaying ? "pause.fill" : "play.fill")
341
+ .font(.title)
342
+ .foregroundColor(accent)
343
+ }
344
+
345
+ // Skip Backward
346
+ Button(action: {
347
+ playerModel.seekBackward()
348
+ }) {
349
+ Image(systemName: "gobackward.10")
350
+ .font(.title2)
351
+ .foregroundColor(accent)
352
+ }
353
+
354
+ // Skip Forward
355
+ Button(action: {
356
+ playerModel.seekForward()
357
+ }) {
358
+ Image(systemName: "goforward.10")
359
+ .font(.title2)
360
+ .foregroundColor(accent)
361
+ }
362
+
363
+ Spacer()
364
+
365
+ // Volume
366
+ HStack {
367
+ Image(systemName: playerModel.isMuted ? "speaker.slash.fill" : "speaker.wave.2.fill")
368
+ .foregroundColor(accent)
369
+ .onTapGesture {
370
+ playerModel.toggleMute()
371
+ }
372
+
373
+ Slider(value: $playerModel.volume, in: 0...1)
374
+ .frame(width: 80)
375
+ .accentColor(accent)
376
+ }
377
+
378
+ // Fullscreen
379
+ Button(action: {
380
+ playerModel.toggleFullscreen()
381
+ }) {
382
+ Image(systemName: playerModel.isFullscreen ? "arrow.down.right.and.arrow.up.left" : "arrow.up.left.and.arrow.down.right")
383
+ .font(.title2)
384
+ .foregroundColor(accent)
385
+ }
386
+ }
387
+ .padding(.horizontal)
388
+ }
389
+ .padding(.vertical)
390
+ .background(LinearGradient(
391
+ gradient: Gradient(colors: [Color.clear, Color.black.opacity(0.7)]),
392
+ startPoint: .top,
393
+ endPoint: .bottom
394
+ ))
395
+ }
396
+ }
397
+
398
+ private func formatTime(_ time: Double) -> String {
399
+ let minutes = Int(time) / 60
400
+ let seconds = Int(time) % 60
401
+ return String(format: "%02d:%02d", minutes, seconds)
402
+ }
403
+ }
404
+
405
+ // MARK: - Player Model (ObservableObject)
406
+ @available(iOS 14.0, *)
407
+ public class UnifiedVideoPlayerModel: ObservableObject {
408
+ // Published Properties
409
+ @Published public var isPlaying: Bool = false
410
+ @Published public var isBuffering: Bool = false
411
+ @Published public var currentTime: Double = 0
412
+ @Published public var duration: Double = 0
413
+ @Published public var seekTime: Double = 0
414
+ @Published public var volume: Float = 1.0 {
415
+ didSet { avPlayer?.volume = volume }
416
+ }
417
+ @Published public var isMuted: Bool = false {
418
+ didSet { avPlayer?.isMuted = isMuted }
419
+ }
420
+ @Published public var error: Error?
421
+ @Published public var isFullscreen: Bool = false
422
+ @Published public var title: String?
423
+ @Published public var availableSubtitles: [SubtitleTrack] = []
424
+ @Published public var currentVideoQuality: String = "Auto"
425
+
426
+ // Player properties
427
+ public private(set) var avPlayer: AVPlayer?
428
+ private var playerObserver: Any?
429
+ private var timeObserver: Any?
430
+ private var cancellables = Set<AnyCancellable>()
431
+ private let configuration: PlayerConfiguration
432
+ private var mediaSource: MediaSource?
433
+
434
+ // Events
435
+ public var onReady: (() -> Void)?
436
+ public var onEnded: (() -> Void)?
437
+ public var onError: ((Error) -> Void)?
438
+
439
+ // MARK: - Initialization
440
+
441
+ public init(url: String, configuration: PlayerConfiguration = PlayerConfiguration()) {
442
+ self.configuration = configuration
443
+ self.mediaSource = MediaSource(url: url)
444
+ setupPlayer()
445
+ }
446
+
447
+ public init(source: MediaSource, configuration: PlayerConfiguration = PlayerConfiguration()) {
448
+ self.configuration = configuration
449
+ self.mediaSource = source
450
+ setupPlayer()
451
+ }
452
+
453
+ // MARK: - Player Setup
454
+
455
+ private func setupPlayer() {
456
+ guard let source = mediaSource,
457
+ let url = URL(string: source.url) else { return }
458
+
459
+ let playerItem = AVPlayerItem(url: url)
460
+ avPlayer = AVPlayer(playerItem: playerItem)
461
+
462
+ // Apply configuration
463
+ avPlayer?.volume = configuration.volume
464
+ avPlayer?.isMuted = configuration.muted
465
+
466
+ // Setup observers
467
+ setupObservers()
468
+
469
+ // Load metadata
470
+ if let metadata = source.metadata {
471
+ title = metadata["title"] as? String
472
+ }
473
+
474
+ // Load subtitles if available
475
+ if let subtitles = source.subtitles {
476
+ availableSubtitles = subtitles
477
+ }
478
+ }
479
+
480
+ public func initialize() {
481
+ if configuration.autoPlay {
482
+ play()
483
+ }
484
+ }
485
+
486
+ // MARK: - Playback Control
487
+
488
+ public func play() {
489
+ avPlayer?.play()
490
+ isPlaying = true
491
+ }
492
+
493
+ public func pause() {
494
+ avPlayer?.pause()
495
+ isPlaying = false
496
+ }
497
+
498
+ public func togglePlayPause() {
499
+ if isPlaying {
500
+ pause()
501
+ } else {
502
+ play()
503
+ }
504
+ }
505
+
506
+ public func stop() {
507
+ avPlayer?.pause()
508
+ avPlayer?.seek(to: .zero)
509
+ isPlaying = false
510
+ currentTime = 0
511
+ }
512
+
513
+ public func seek(to time: Double) {
514
+ let cmTime = CMTime(seconds: time, preferredTimescale: 1000)
515
+ avPlayer?.seek(to: cmTime)
516
+ }
517
+
518
+ public func seekForward(seconds: Double = 10) {
519
+ let newTime = currentTime + seconds
520
+ seek(to: min(newTime, duration))
521
+ }
522
+
523
+ public func seekBackward(seconds: Double = 10) {
524
+ let newTime = currentTime - seconds
525
+ seek(to: max(newTime, 0))
526
+ }
527
+
528
+ // MARK: - Configuration
529
+
530
+ public func setPlaybackRate(_ rate: Float) {
531
+ avPlayer?.rate = rate
532
+ }
533
+
534
+ public func setVideoQuality(_ quality: String) {
535
+ currentVideoQuality = quality
536
+ // Implementation would handle quality switching
537
+ }
538
+
539
+ public func selectSubtitle(_ subtitle: SubtitleTrack?) {
540
+ // Implementation would handle subtitle selection
541
+ }
542
+
543
+ public func toggleMute() {
544
+ isMuted.toggle()
545
+ }
546
+
547
+ public func toggleFullscreen() {
548
+ isFullscreen.toggle()
549
+ }
550
+
551
+ // MARK: - Observers
552
+
553
+ private func setupObservers() {
554
+ // Time observer
555
+ let interval = CMTime(seconds: 0.1, preferredTimescale: 1000)
556
+ timeObserver = avPlayer?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
557
+ self?.currentTime = time.seconds
558
+ }
559
+
560
+ // Status observer
561
+ avPlayer?.publisher(for: \.status)
562
+ .sink { [weak self] status in
563
+ if status == .readyToPlay {
564
+ self?.updateDuration()
565
+ self?.onReady?()
566
+ }
567
+ }
568
+ .store(in: &cancellables)
569
+
570
+ // Rate observer
571
+ avPlayer?.publisher(for: \.rate)
572
+ .sink { [weak self] rate in
573
+ self?.isPlaying = rate > 0
574
+ }
575
+ .store(in: &cancellables)
576
+
577
+ // Buffering observer
578
+ avPlayer?.currentItem?.publisher(for: \.isPlaybackBufferEmpty)
579
+ .sink { [weak self] isEmpty in
580
+ self?.isBuffering = isEmpty
581
+ }
582
+ .store(in: &cancellables)
583
+
584
+ // End observer
585
+ NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime)
586
+ .sink { [weak self] _ in
587
+ self?.handlePlaybackEnded()
588
+ }
589
+ .store(in: &cancellables)
590
+
591
+ // Error observer
592
+ avPlayer?.currentItem?.publisher(for: \.error)
593
+ .compactMap { $0 }
594
+ .sink { [weak self] error in
595
+ self?.error = error
596
+ self?.onError?(error)
597
+ }
598
+ .store(in: &cancellables)
599
+ }
600
+
601
+ private func updateDuration() {
602
+ if let duration = avPlayer?.currentItem?.duration {
603
+ self.duration = duration.seconds
604
+ }
605
+ }
606
+
607
+ private func handlePlaybackEnded() {
608
+ if configuration.loop {
609
+ seek(to: 0)
610
+ play()
611
+ } else {
612
+ isPlaying = false
613
+ onEnded?()
614
+ }
615
+ }
616
+
617
+ // MARK: - Cleanup
618
+
619
+ public func cleanup() {
620
+ avPlayer?.pause()
621
+ if let observer = timeObserver {
622
+ avPlayer?.removeTimeObserver(observer)
623
+ }
624
+ cancellables.removeAll()
625
+ }
626
+
627
+ deinit {
628
+ cleanup()
629
+ }
630
+ }
631
+
632
+ // MARK: - SwiftUI Preview Provider
633
+ @available(iOS 14.0, *)
634
+ struct UnifiedVideoPlayerView_Previews: PreviewProvider {
635
+ static var previews: some View {
636
+ CustomUnifiedVideoPlayer(
637
+ url: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
638
+ )
639
+ }
640
+ }
@@ -0,0 +1,36 @@
1
+ import UIKit
2
+ import SwiftUI
3
+
4
+ public extension UIColor {
5
+ // Supports #RRGGBB and #AARRGGBB (with or without #)
6
+ convenience init?(hex: String) {
7
+ var cleaned = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
8
+ if cleaned.hasPrefix("#") { cleaned.removeFirst() }
9
+ guard cleaned.count == 6 || cleaned.count == 8 else { return nil }
10
+
11
+ var rgba: UInt64 = 0
12
+ guard Scanner(string: cleaned).scanHexInt64(&rgba) else { return nil }
13
+
14
+ let a, r, g, b: CGFloat
15
+ if cleaned.count == 8 {
16
+ a = CGFloat((rgba & 0xFF000000) >> 24) / 255.0
17
+ r = CGFloat((rgba & 0x00FF0000) >> 16) / 255.0
18
+ g = CGFloat((rgba & 0x0000FF00) >> 8) / 255.0
19
+ b = CGFloat(rgba & 0x000000FF) / 255.0
20
+ } else {
21
+ a = 1.0
22
+ r = CGFloat((rgba & 0xFF0000) >> 16) / 255.0
23
+ g = CGFloat((rgba & 0x00FF00) >> 8) / 255.0
24
+ b = CGFloat(rgba & 0x0000FF) / 255.0
25
+ }
26
+ self.init(red: r, green: g, blue: b, alpha: a)
27
+ }
28
+ }
29
+
30
+ public extension Color {
31
+ init?(hex: String) {
32
+ guard let ui = UIColor(hex: hex) else { return nil }
33
+ self = Color(ui)
34
+ }
35
+ }
36
+
@@ -0,0 +1,27 @@
1
+ Pod::Spec.new do |s|
2
+ s.name = 'UnifiedVideoPlayer'
3
+ s.version = '1.0.0'
4
+ s.summary = 'Unified Video Player SDK for iOS'
5
+ s.description = <<-DESC
6
+ A powerful, unified video player SDK for iOS that provides a consistent API for video playback
7
+ with support for HLS, DASH, DRM, subtitles, and more.
8
+ DESC
9
+
10
+ s.homepage = 'https://github.com/yourcompany/unified-video-ios'
11
+ s.license = { :type => 'MIT', :file => 'LICENSE' }
12
+ s.author = { 'Your Company' => 'contact@yourcompany.com' }
13
+ s.source = { :git => 'https://github.com/yourcompany/unified-video-ios.git', :tag => s.version.to_s }
14
+
15
+ s.ios.deployment_target = '13.0'
16
+ s.tvos.deployment_target = '13.0'
17
+ s.swift_version = '5.0'
18
+
19
+ s.source_files = 'Sources/UnifiedVideoPlayer/**/*.{swift,h,m}'
20
+ s.public_header_files = 'Sources/UnifiedVideoPlayer/**/*.h'
21
+
22
+ s.frameworks = 'UIKit', 'AVFoundation', 'AVKit', 'Foundation'
23
+
24
+ s.resource_bundles = {
25
+ 'UnifiedVideoPlayer' => ['Sources/UnifiedVideoPlayer/Resources/**/*']
26
+ }
27
+ end