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,337 @@
1
+ //
2
+ // ViewController.swift
3
+ // UnifiedVideoPlayer Sample App
4
+ //
5
+ // Example of integrating UnifiedVideoPlayer into an existing iOS app
6
+ //
7
+
8
+ import UIKit
9
+ import UnifiedVideoPlayer
10
+
11
+ class ViewController: UIViewController {
12
+
13
+ // MARK: - IBOutlets
14
+ @IBOutlet weak var playerContainer: UIView!
15
+ @IBOutlet weak var playPauseButton: UIButton!
16
+ @IBOutlet weak var progressSlider: UISlider!
17
+ @IBOutlet weak var currentTimeLabel: UILabel!
18
+ @IBOutlet weak var durationLabel: UILabel!
19
+ @IBOutlet weak var volumeSlider: UISlider!
20
+ @IBOutlet weak var loadingIndicator: UIActivityIndicatorView!
21
+ @IBOutlet weak var errorLabel: UILabel!
22
+
23
+ // MARK: - Properties
24
+ private var videoPlayer: UnifiedVideoPlayer!
25
+ private var isSliderTracking = false
26
+
27
+ // Sample video URLs
28
+ private let sampleVideos = [
29
+ "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
30
+ "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8",
31
+ "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8"
32
+ ]
33
+
34
+ // MARK: - Lifecycle
35
+ override func viewDidLoad() {
36
+ super.viewDidLoad()
37
+ setupUI()
38
+ setupVideoPlayer()
39
+ loadSampleVideo()
40
+ }
41
+
42
+ override func viewWillDisappear(_ animated: Bool) {
43
+ super.viewWillDisappear(animated)
44
+ videoPlayer?.pause()
45
+ }
46
+
47
+ deinit {
48
+ videoPlayer?.destroy()
49
+ }
50
+
51
+ // MARK: - Setup
52
+ private func setupUI() {
53
+ errorLabel.isHidden = true
54
+ loadingIndicator.hidesWhenStopped = true
55
+
56
+ // Configure sliders
57
+ progressSlider.value = 0
58
+ volumeSlider.value = 1.0
59
+
60
+ // Style the player container
61
+ playerContainer.backgroundColor = .black
62
+ playerContainer.layer.cornerRadius = 8
63
+ playerContainer.clipsToBounds = true
64
+ }
65
+
66
+ private func setupVideoPlayer() {
67
+ // Initialize the player
68
+ videoPlayer = UnifiedVideoPlayer()
69
+
70
+ // Configure player
71
+ let config = PlayerConfiguration(dictionary: [
72
+ "autoPlay": false,
73
+ "controls": false, // Using custom controls
74
+ "muted": false,
75
+ "debug": true
76
+ ])
77
+
78
+ // Initialize with container
79
+ videoPlayer.initialize(container: playerContainer, configuration: config)
80
+
81
+ // Set up event listeners
82
+ setupPlayerEventListeners()
83
+ }
84
+
85
+ private func setupPlayerEventListeners() {
86
+ // Ready event
87
+ videoPlayer.onReady = { [weak self] in
88
+ self?.handlePlayerReady()
89
+ }
90
+
91
+ // Play/Pause events
92
+ videoPlayer.onPlay = { [weak self] in
93
+ self?.updatePlayPauseButton(isPlaying: true)
94
+ }
95
+
96
+ videoPlayer.onPause = { [weak self] in
97
+ self?.updatePlayPauseButton(isPlaying: false)
98
+ }
99
+
100
+ // Time updates
101
+ videoPlayer.onTimeUpdate = { [weak self] currentTime in
102
+ self?.updateProgress(currentTime: currentTime)
103
+ }
104
+
105
+ // Buffering
106
+ videoPlayer.onBuffering = { [weak self] isBuffering in
107
+ self?.handleBuffering(isBuffering)
108
+ }
109
+
110
+ // Metadata loaded
111
+ videoPlayer.onLoadedMetadata = { [weak self] metadata in
112
+ self?.handleMetadataLoaded(metadata)
113
+ }
114
+
115
+ // Error handling
116
+ videoPlayer.onError = { [weak self] error in
117
+ self?.handleError(error)
118
+ }
119
+
120
+ // State changes
121
+ videoPlayer.onStateChange = { [weak self] state in
122
+ self?.handleStateChange(state)
123
+ }
124
+
125
+ // Video ended
126
+ videoPlayer.onEnded = { [weak self] in
127
+ self?.handleVideoEnded()
128
+ }
129
+ }
130
+
131
+ // MARK: - Loading Content
132
+ private func loadSampleVideo() {
133
+ // Load the first sample video
134
+ let videoUrl = sampleVideos[0]
135
+
136
+ let source = MediaSource(url: videoUrl)
137
+ videoPlayer.load(source: source)
138
+ }
139
+
140
+ // MARK: - Event Handlers
141
+ private func handlePlayerReady() {
142
+ DispatchQueue.main.async { [weak self] in
143
+ self?.loadingIndicator.stopAnimating()
144
+ self?.playPauseButton.isEnabled = true
145
+ }
146
+ }
147
+
148
+ private func handleBuffering(_ isBuffering: Bool) {
149
+ DispatchQueue.main.async { [weak self] in
150
+ if isBuffering {
151
+ self?.loadingIndicator.startAnimating()
152
+ } else {
153
+ self?.loadingIndicator.stopAnimating()
154
+ }
155
+ }
156
+ }
157
+
158
+ private func handleMetadataLoaded(_ metadata: [String: Any]) {
159
+ print("Video metadata loaded: \(metadata)")
160
+
161
+ DispatchQueue.main.async { [weak self] in
162
+ if let duration = metadata["duration"] as? Double {
163
+ self?.progressSlider.maximumValue = Float(duration)
164
+ self?.durationLabel.text = self?.formatTime(duration) ?? "00:00"
165
+ }
166
+
167
+ if let width = metadata["width"] as? CGFloat,
168
+ let height = metadata["height"] as? CGFloat {
169
+ print("Video resolution: \(width)x\(height)")
170
+ }
171
+ }
172
+ }
173
+
174
+ private func handleError(_ error: Error) {
175
+ DispatchQueue.main.async { [weak self] in
176
+ self?.loadingIndicator.stopAnimating()
177
+ self?.errorLabel.isHidden = false
178
+ self?.errorLabel.text = "Error: \(error.localizedDescription)"
179
+ self?.playPauseButton.isEnabled = false
180
+ }
181
+ }
182
+
183
+ private func handleStateChange(_ state: PlayerState) {
184
+ print("Player state changed: \(state)")
185
+ }
186
+
187
+ private func handleVideoEnded() {
188
+ DispatchQueue.main.async { [weak self] in
189
+ self?.updatePlayPauseButton(isPlaying: false)
190
+ self?.progressSlider.value = 0
191
+ self?.currentTimeLabel.text = "00:00"
192
+ }
193
+ }
194
+
195
+ // MARK: - UI Updates
196
+ private func updatePlayPauseButton(isPlaying: Bool) {
197
+ DispatchQueue.main.async { [weak self] in
198
+ let title = isPlaying ? "Pause" : "Play"
199
+ self?.playPauseButton.setTitle(title, for: .normal)
200
+ }
201
+ }
202
+
203
+ private func updateProgress(currentTime: Double) {
204
+ guard !isSliderTracking else { return }
205
+
206
+ DispatchQueue.main.async { [weak self] in
207
+ self?.progressSlider.value = Float(currentTime)
208
+ self?.currentTimeLabel.text = self?.formatTime(currentTime) ?? "00:00"
209
+ }
210
+ }
211
+
212
+ private func formatTime(_ seconds: Double) -> String {
213
+ let mins = Int(seconds) / 60
214
+ let secs = Int(seconds) % 60
215
+ return String(format: "%02d:%02d", mins, secs)
216
+ }
217
+
218
+ // MARK: - IBActions
219
+ @IBAction func playPauseButtonTapped(_ sender: UIButton) {
220
+ videoPlayer.togglePlayPause()
221
+ }
222
+
223
+ @IBAction func progressSliderValueChanged(_ sender: UISlider) {
224
+ videoPlayer.seek(to: Double(sender.value))
225
+ }
226
+
227
+ @IBAction func progressSliderTouchDown(_ sender: UISlider) {
228
+ isSliderTracking = true
229
+ }
230
+
231
+ @IBAction func progressSliderTouchUp(_ sender: UISlider) {
232
+ isSliderTracking = false
233
+ }
234
+
235
+ @IBAction func volumeSliderValueChanged(_ sender: UISlider) {
236
+ videoPlayer.setVolume(sender.value)
237
+ }
238
+
239
+ @IBAction func muteButtonTapped(_ sender: UIButton) {
240
+ videoPlayer.toggleMute()
241
+ sender.setTitle(sender.title(for: .normal) == "Mute" ? "Unmute" : "Mute", for: .normal)
242
+ }
243
+
244
+ @IBAction func skipBackwardButtonTapped(_ sender: UIButton) {
245
+ videoPlayer.seekBackward(seconds: 10)
246
+ }
247
+
248
+ @IBAction func skipForwardButtonTapped(_ sender: UIButton) {
249
+ videoPlayer.seekForward(seconds: 10)
250
+ }
251
+
252
+ @IBAction func loadVideoButtonTapped(_ sender: UIButton) {
253
+ // Show action sheet with video options
254
+ let actionSheet = UIAlertController(title: "Select Video", message: nil, preferredStyle: .actionSheet)
255
+
256
+ actionSheet.addAction(UIAlertAction(title: "MP4 Video", style: .default) { [weak self] _ in
257
+ self?.videoPlayer.load(url: self?.sampleVideos[0] ?? "")
258
+ })
259
+
260
+ actionSheet.addAction(UIAlertAction(title: "HLS Stream", style: .default) { [weak self] _ in
261
+ self?.videoPlayer.load(url: self?.sampleVideos[1] ?? "")
262
+ })
263
+
264
+ actionSheet.addAction(UIAlertAction(title: "Test Stream", style: .default) { [weak self] _ in
265
+ self?.videoPlayer.load(url: self?.sampleVideos[2] ?? "")
266
+ })
267
+
268
+ actionSheet.addAction(UIAlertAction(title: "Custom URL", style: .default) { [weak self] _ in
269
+ self?.showCustomURLDialog()
270
+ })
271
+
272
+ actionSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))
273
+
274
+ // iPad compatibility
275
+ if let popover = actionSheet.popoverPresentationController {
276
+ popover.sourceView = sender
277
+ popover.sourceRect = sender.bounds
278
+ }
279
+
280
+ present(actionSheet, animated: true)
281
+ }
282
+
283
+ @IBAction func fullscreenButtonTapped(_ sender: UIButton) {
284
+ // Present player in fullscreen
285
+ videoPlayer.initializeWithViewController(viewController: self, configuration: nil)
286
+ }
287
+
288
+ // MARK: - Helper Methods
289
+ private func showCustomURLDialog() {
290
+ let alert = UIAlertController(title: "Enter Video URL", message: nil, preferredStyle: .alert)
291
+
292
+ alert.addTextField { textField in
293
+ textField.placeholder = "https://example.com/video.mp4"
294
+ textField.keyboardType = .URL
295
+ }
296
+
297
+ alert.addAction(UIAlertAction(title: "Load", style: .default) { [weak self] _ in
298
+ if let url = alert.textFields?.first?.text, !url.isEmpty {
299
+ self?.videoPlayer.load(url: url)
300
+ }
301
+ })
302
+
303
+ alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
304
+
305
+ present(alert, animated: true)
306
+ }
307
+ }
308
+
309
+ // MARK: - Extension for DRM Content (Advanced Usage)
310
+ extension ViewController {
311
+
312
+ func loadDRMProtectedContent() {
313
+ let drm = DRMConfiguration(
314
+ type: "fairplay",
315
+ licenseUrl: "https://license.server.com/fairplay"
316
+ )
317
+ drm.certificateUrl = "https://certificate.server.com/cert"
318
+
319
+ let source = MediaSource(url: "https://example.com/protected-content.m3u8")
320
+ source.drm = drm
321
+
322
+ videoPlayer.load(source: source)
323
+ }
324
+
325
+ func loadVideoWithSubtitles() {
326
+ let subtitle = SubtitleTrack(
327
+ url: "https://example.com/subtitles-en.vtt",
328
+ language: "en",
329
+ label: "English"
330
+ )
331
+
332
+ let source = MediaSource(url: "https://example.com/video.mp4")
333
+ source.subtitles = [subtitle]
334
+
335
+ videoPlayer.load(source: source)
336
+ }
337
+ }
@@ -0,0 +1,304 @@
1
+ //
2
+ // ContentView.swift
3
+ // UnifiedVideoPlayer SwiftUI Sample App
4
+ //
5
+ // Example of using UnifiedVideoPlayer with SwiftUI
6
+ //
7
+
8
+ import SwiftUI
9
+ import UnifiedVideoPlayer
10
+
11
+ struct ContentView: View {
12
+ @StateObject private var playerModel = UnifiedVideoPlayerModel(
13
+ url: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
14
+ )
15
+
16
+ @State private var selectedVideoIndex = 0
17
+ @State private var showVideoSelection = false
18
+ @State private var customURL = ""
19
+ @State private var showCustomURLAlert = false
20
+
21
+ let sampleVideos = [
22
+ ("Big Buck Bunny", "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"),
23
+ ("HLS Stream", "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8"),
24
+ ("Test Stream", "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8")
25
+ ]
26
+
27
+ var body: some View {
28
+ NavigationView {
29
+ VStack(spacing: 0) {
30
+ // Video Player
31
+ CustomUnifiedVideoPlayer(
32
+ url: sampleVideos[selectedVideoIndex].1,
33
+ configuration: PlayerConfiguration(dictionary: [
34
+ "autoPlay": false,
35
+ "controls": true,
36
+ "debug": true
37
+ ])
38
+ )
39
+ .aspectRatio(16/9, contentMode: .fit)
40
+ .background(Color.black)
41
+
42
+ // Controls Section
43
+ ScrollView {
44
+ VStack(spacing: 20) {
45
+ // Video Selection
46
+ VStack(alignment: .leading, spacing: 10) {
47
+ Text("Select Video")
48
+ .font(.headline)
49
+
50
+ ForEach(0..<sampleVideos.count, id: \.self) { index in
51
+ Button(action: {
52
+ selectedVideoIndex = index
53
+ loadVideo(url: sampleVideos[index].1)
54
+ }) {
55
+ HStack {
56
+ Image(systemName: selectedVideoIndex == index ? "checkmark.circle.fill" : "circle")
57
+ Text(sampleVideos[index].0)
58
+ Spacer()
59
+ }
60
+ .foregroundColor(selectedVideoIndex == index ? .blue : .primary)
61
+ }
62
+ .buttonStyle(PlainButtonStyle())
63
+ }
64
+
65
+ Button("Load Custom URL") {
66
+ showCustomURLAlert = true
67
+ }
68
+ .foregroundColor(.blue)
69
+ }
70
+ .padding()
71
+ .background(Color.gray.opacity(0.1))
72
+ .cornerRadius(10)
73
+
74
+ // Player Info
75
+ VStack(alignment: .leading, spacing: 10) {
76
+ Text("Player Information")
77
+ .font(.headline)
78
+
79
+ InfoRow(label: "State", value: playerStateText)
80
+ InfoRow(label: "Duration", value: formatTime(playerModel.duration))
81
+ InfoRow(label: "Current Time", value: formatTime(playerModel.currentTime))
82
+ InfoRow(label: "Volume", value: String(format: "%.0f%%", playerModel.volume * 100))
83
+ InfoRow(label: "Quality", value: playerModel.currentVideoQuality)
84
+
85
+ if let error = playerModel.error {
86
+ InfoRow(label: "Error", value: error.localizedDescription)
87
+ .foregroundColor(.red)
88
+ }
89
+ }
90
+ .padding()
91
+ .background(Color.gray.opacity(0.1))
92
+ .cornerRadius(10)
93
+
94
+ // Advanced Features
95
+ VStack(alignment: .leading, spacing: 10) {
96
+ Text("Advanced Features")
97
+ .font(.headline)
98
+
99
+ Button("Load DRM Content") {
100
+ loadDRMContent()
101
+ }
102
+
103
+ Button("Load with Subtitles") {
104
+ loadVideoWithSubtitles()
105
+ }
106
+ }
107
+ .padding()
108
+ .background(Color.gray.opacity(0.1))
109
+ .cornerRadius(10)
110
+ }
111
+ .padding()
112
+ }
113
+ }
114
+ .navigationTitle("SwiftUI Video Player")
115
+ .navigationBarTitleDisplayMode(.inline)
116
+ }
117
+ .alert("Enter Video URL", isPresented: $showCustomURLAlert) {
118
+ TextField("https://example.com/video.mp4", text: $customURL)
119
+ Button("Cancel", role: .cancel) { }
120
+ Button("Load") {
121
+ if !customURL.isEmpty {
122
+ loadVideo(url: customURL)
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ private var playerStateText: String {
129
+ if playerModel.isPlaying {
130
+ return "Playing"
131
+ } else if playerModel.isBuffering {
132
+ return "Buffering..."
133
+ } else {
134
+ return "Paused"
135
+ }
136
+ }
137
+
138
+ private func loadVideo(url: String) {
139
+ let source = MediaSource(url: url)
140
+ // Note: In a real implementation, you'd reinitialize the player with the new source
141
+ // This is simplified for demonstration
142
+ }
143
+
144
+ private func loadDRMContent() {
145
+ let drm = DRMConfiguration(
146
+ type: "fairplay",
147
+ licenseUrl: "https://license.server.com/fairplay"
148
+ )
149
+ drm.certificateUrl = "https://certificate.server.com/cert"
150
+
151
+ let source = MediaSource(url: "https://example.com/protected-content.m3u8")
152
+ source.drm = drm
153
+
154
+ // Load the DRM protected content
155
+ // playerModel would need to be reinitialized with this source
156
+ }
157
+
158
+ private func loadVideoWithSubtitles() {
159
+ let subtitle = SubtitleTrack(
160
+ url: "https://example.com/subtitles-en.vtt",
161
+ language: "en",
162
+ label: "English"
163
+ )
164
+
165
+ let source = MediaSource(url: "https://example.com/video.mp4")
166
+ source.subtitles = [subtitle]
167
+
168
+ // Load video with subtitles
169
+ // playerModel would need to be reinitialized with this source
170
+ }
171
+
172
+ private func formatTime(_ time: Double) -> String {
173
+ guard !time.isNaN && !time.isInfinite else { return "00:00" }
174
+ let minutes = Int(time) / 60
175
+ let seconds = Int(time) % 60
176
+ return String(format: "%02d:%02d", minutes, seconds)
177
+ }
178
+ }
179
+
180
+ struct InfoRow: View {
181
+ let label: String
182
+ let value: String
183
+
184
+ var body: some View {
185
+ HStack {
186
+ Text(label + ":")
187
+ .fontWeight(.medium)
188
+ Spacer()
189
+ Text(value)
190
+ .foregroundColor(.secondary)
191
+ }
192
+ }
193
+ }
194
+
195
+ // MARK: - Alternative Full-Screen Player View
196
+ struct FullScreenPlayerView: View {
197
+ @Environment(\.presentationMode) var presentationMode
198
+ let videoURL: String
199
+
200
+ var body: some View {
201
+ ZStack {
202
+ Color.black.edgesIgnoringSafeArea(.all)
203
+
204
+ CustomUnifiedVideoPlayer(
205
+ url: videoURL,
206
+ configuration: PlayerConfiguration(dictionary: [
207
+ "autoPlay": true,
208
+ "controls": true
209
+ ])
210
+ )
211
+
212
+ // Close button
213
+ VStack {
214
+ HStack {
215
+ Spacer()
216
+ Button(action: {
217
+ presentationMode.wrappedValue.dismiss()
218
+ }) {
219
+ Image(systemName: "xmark.circle.fill")
220
+ .font(.title)
221
+ .foregroundColor(.white)
222
+ .background(Color.black.opacity(0.5))
223
+ .clipShape(Circle())
224
+ }
225
+ .padding()
226
+ }
227
+ Spacer()
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ // MARK: - List View with Multiple Videos
234
+ struct VideoListView: View {
235
+ let videos = [
236
+ VideoItem(title: "Big Buck Bunny", url: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", thumbnail: "thumbnail1"),
237
+ VideoItem(title: "Elephant Dream", url: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", thumbnail: "thumbnail2"),
238
+ VideoItem(title: "For Bigger Blazes", url: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", thumbnail: "thumbnail3")
239
+ ]
240
+
241
+ @State private var selectedVideo: VideoItem?
242
+
243
+ var body: some View {
244
+ NavigationView {
245
+ List(videos) { video in
246
+ VideoRow(video: video) {
247
+ selectedVideo = video
248
+ }
249
+ }
250
+ .navigationTitle("Video Library")
251
+ .sheet(item: $selectedVideo) { video in
252
+ FullScreenPlayerView(videoURL: video.url)
253
+ }
254
+ }
255
+ }
256
+ }
257
+
258
+ struct VideoItem: Identifiable {
259
+ let id = UUID()
260
+ let title: String
261
+ let url: String
262
+ let thumbnail: String
263
+ }
264
+
265
+ struct VideoRow: View {
266
+ let video: VideoItem
267
+ let action: () -> Void
268
+
269
+ var body: some View {
270
+ Button(action: action) {
271
+ HStack {
272
+ // Thumbnail placeholder
273
+ RoundedRectangle(cornerRadius: 8)
274
+ .fill(Color.gray.opacity(0.3))
275
+ .frame(width: 120, height: 67.5)
276
+ .overlay(
277
+ Image(systemName: "play.circle.fill")
278
+ .font(.title)
279
+ .foregroundColor(.white)
280
+ )
281
+
282
+ VStack(alignment: .leading, spacing: 4) {
283
+ Text(video.title)
284
+ .font(.headline)
285
+ .foregroundColor(.primary)
286
+ Text("Tap to play")
287
+ .font(.caption)
288
+ .foregroundColor(.secondary)
289
+ }
290
+
291
+ Spacer()
292
+ }
293
+ .padding(.vertical, 8)
294
+ }
295
+ .buttonStyle(PlainButtonStyle())
296
+ }
297
+ }
298
+
299
+ // MARK: - Preview
300
+ struct ContentView_Previews: PreviewProvider {
301
+ static var previews: some View {
302
+ ContentView()
303
+ }
304
+ }