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,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
|
+
}
|