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,436 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import AVFoundation
|
|
3
|
+
import AVKit
|
|
4
|
+
import React
|
|
5
|
+
|
|
6
|
+
@objc(UnifiedVideoPlayer)
|
|
7
|
+
class UnifiedVideoPlayer: RCTEventEmitter {
|
|
8
|
+
private var player: AVPlayer?
|
|
9
|
+
private var playerItem: AVPlayerItem?
|
|
10
|
+
private var playerLayer: AVPlayerLayer?
|
|
11
|
+
private var playerViewController: AVPlayerViewController?
|
|
12
|
+
private var timeObserver: Any?
|
|
13
|
+
private var statusObserver: NSKeyValueObservation?
|
|
14
|
+
private var bufferObserver: NSKeyValueObservation?
|
|
15
|
+
private var currentQualityIndex: Int = -1
|
|
16
|
+
private var availableQualities: [VideoQuality] = []
|
|
17
|
+
|
|
18
|
+
struct VideoQuality {
|
|
19
|
+
let height: Int
|
|
20
|
+
let width: Int
|
|
21
|
+
let bitrate: Int
|
|
22
|
+
let label: String
|
|
23
|
+
let index: Int
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
override static func requiresMainQueueSetup() -> Bool {
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override func supportedEvents() -> [String]! {
|
|
31
|
+
return [
|
|
32
|
+
"onReady",
|
|
33
|
+
"onPlay",
|
|
34
|
+
"onPause",
|
|
35
|
+
"onEnded",
|
|
36
|
+
"onTimeUpdate",
|
|
37
|
+
"onBuffering",
|
|
38
|
+
"onError",
|
|
39
|
+
"onQualityChanged",
|
|
40
|
+
"onVolumeChanged",
|
|
41
|
+
"onFullscreenChanged",
|
|
42
|
+
"onProgress",
|
|
43
|
+
"onSeeking",
|
|
44
|
+
"onSeeked",
|
|
45
|
+
"onLoadedMetadata"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@objc
|
|
50
|
+
func initialize(_ config: NSDictionary) {
|
|
51
|
+
DispatchQueue.main.async {
|
|
52
|
+
self.player = AVPlayer()
|
|
53
|
+
self.setupNotifications()
|
|
54
|
+
self.sendEvent(withName: "onReady", body: nil)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@objc
|
|
59
|
+
func load(_ source: NSDictionary) {
|
|
60
|
+
guard let urlString = source["url"] as? String,
|
|
61
|
+
let url = URL(string: urlString) else {
|
|
62
|
+
sendEvent(withName: "onError", body: [
|
|
63
|
+
"code": "INVALID_URL",
|
|
64
|
+
"message": "Invalid video URL"
|
|
65
|
+
])
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
DispatchQueue.main.async {
|
|
70
|
+
// Handle DRM if provided
|
|
71
|
+
if let drm = source["drm"] as? NSDictionary {
|
|
72
|
+
self.loadDRMContent(url: url, drm: drm)
|
|
73
|
+
} else {
|
|
74
|
+
self.loadStandardContent(url: url)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Handle subtitles if provided
|
|
78
|
+
if let subtitles = source["subtitles"] as? [[String: Any]] {
|
|
79
|
+
self.loadSubtitles(subtitles)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private func loadStandardContent(url: URL) {
|
|
85
|
+
let asset = AVAsset(url: url)
|
|
86
|
+
playerItem = AVPlayerItem(asset: asset)
|
|
87
|
+
|
|
88
|
+
// Setup quality detection for HLS
|
|
89
|
+
if url.absoluteString.contains(".m3u8") {
|
|
90
|
+
detectHLSQualities(asset: asset)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setupPlayerItemObservers()
|
|
94
|
+
player?.replaceCurrentItem(with: playerItem)
|
|
95
|
+
|
|
96
|
+
// Get video metadata
|
|
97
|
+
asset.loadValuesAsynchronously(forKeys: ["duration", "tracks"]) {
|
|
98
|
+
var error: NSError? = nil
|
|
99
|
+
let status = asset.statusOfValue(forKey: "duration", error: &error)
|
|
100
|
+
|
|
101
|
+
if status == .loaded {
|
|
102
|
+
let duration = CMTimeGetSeconds(asset.duration)
|
|
103
|
+
let tracks = asset.tracks(withMediaType: .video)
|
|
104
|
+
|
|
105
|
+
if let videoTrack = tracks.first {
|
|
106
|
+
let size = videoTrack.naturalSize.applying(videoTrack.preferredTransform)
|
|
107
|
+
|
|
108
|
+
DispatchQueue.main.async {
|
|
109
|
+
self.sendEvent(withName: "onLoadedMetadata", body: [
|
|
110
|
+
"duration": duration,
|
|
111
|
+
"width": abs(size.width),
|
|
112
|
+
"height": abs(size.height)
|
|
113
|
+
])
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private func loadDRMContent(url: URL, drm: NSDictionary) {
|
|
121
|
+
guard let licenseUrl = drm["licenseUrl"] as? String,
|
|
122
|
+
let drmType = drm["type"] as? String else {
|
|
123
|
+
sendEvent(withName: "onError", body: [
|
|
124
|
+
"code": "DRM_CONFIG_ERROR",
|
|
125
|
+
"message": "Invalid DRM configuration"
|
|
126
|
+
])
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if drmType == "fairplay" {
|
|
131
|
+
loadFairPlayContent(url: url, licenseUrl: licenseUrl, headers: drm["headers"] as? [String: String])
|
|
132
|
+
} else {
|
|
133
|
+
sendEvent(withName: "onError", body: [
|
|
134
|
+
"code": "DRM_NOT_SUPPORTED",
|
|
135
|
+
"message": "DRM type \(drmType) not supported on iOS"
|
|
136
|
+
])
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private func loadFairPlayContent(url: URL, licenseUrl: String, headers: [String: String]?) {
|
|
141
|
+
// FairPlay implementation would go here
|
|
142
|
+
// This is a simplified version
|
|
143
|
+
let asset = AVURLAsset(url: url)
|
|
144
|
+
|
|
145
|
+
// Set up content key session for FairPlay
|
|
146
|
+
// Implementation details omitted for brevity
|
|
147
|
+
|
|
148
|
+
playerItem = AVPlayerItem(asset: asset)
|
|
149
|
+
setupPlayerItemObservers()
|
|
150
|
+
player?.replaceCurrentItem(with: playerItem)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private func detectHLSQualities(asset: AVAsset) {
|
|
154
|
+
guard let urlAsset = asset as? AVURLAsset else { return }
|
|
155
|
+
|
|
156
|
+
// Access the available media selection options
|
|
157
|
+
Task {
|
|
158
|
+
do {
|
|
159
|
+
let characteristics = try await urlAsset.load(.availableMediaCharacteristicsWithMediaSelectionOptions)
|
|
160
|
+
|
|
161
|
+
for characteristic in characteristics {
|
|
162
|
+
if let group = try await urlAsset.loadMediaSelectionGroup(for: characteristic) {
|
|
163
|
+
let options = group.options
|
|
164
|
+
|
|
165
|
+
availableQualities = options.enumerated().map { index, option in
|
|
166
|
+
let displayName = option.displayName ?? "Quality \(index + 1)"
|
|
167
|
+
|
|
168
|
+
// Try to extract resolution from display name
|
|
169
|
+
var height = 0
|
|
170
|
+
if let match = displayName.range(of: "\\d+p", options: .regularExpression) {
|
|
171
|
+
let heightStr = displayName[match].dropLast()
|
|
172
|
+
height = Int(heightStr) ?? 0
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return VideoQuality(
|
|
176
|
+
height: height,
|
|
177
|
+
width: 0,
|
|
178
|
+
bitrate: 0,
|
|
179
|
+
label: displayName,
|
|
180
|
+
index: index
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
print("Error loading HLS qualities: \(error)")
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private func loadSubtitles(_ subtitles: [[String: Any]]) {
|
|
192
|
+
guard let playerItem = playerItem else { return }
|
|
193
|
+
|
|
194
|
+
for subtitle in subtitles {
|
|
195
|
+
guard let urlString = subtitle["url"] as? String,
|
|
196
|
+
let url = URL(string: urlString),
|
|
197
|
+
let language = subtitle["language"] as? String,
|
|
198
|
+
let label = subtitle["label"] as? String else { continue }
|
|
199
|
+
|
|
200
|
+
let asset = AVAsset(url: url)
|
|
201
|
+
let track = AVMutableCompositionTrack()
|
|
202
|
+
|
|
203
|
+
// Add subtitle track to player item
|
|
204
|
+
// Implementation details for subtitle loading
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private func setupPlayerItemObservers() {
|
|
209
|
+
guard let playerItem = playerItem else { return }
|
|
210
|
+
|
|
211
|
+
// Status observer
|
|
212
|
+
statusObserver = playerItem.observe(\.status) { [weak self] item, _ in
|
|
213
|
+
if item.status == .failed {
|
|
214
|
+
self?.sendEvent(withName: "onError", body: [
|
|
215
|
+
"code": "PLAYBACK_ERROR",
|
|
216
|
+
"message": item.error?.localizedDescription ?? "Unknown playback error"
|
|
217
|
+
])
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Buffer observer
|
|
222
|
+
bufferObserver = playerItem.observe(\.isPlaybackBufferEmpty) { [weak self] item, _ in
|
|
223
|
+
self?.sendEvent(withName: "onBuffering", body: ["isBuffering": item.isPlaybackBufferEmpty])
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Time observer
|
|
227
|
+
let interval = CMTime(seconds: 0.25, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
|
228
|
+
timeObserver = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
|
|
229
|
+
let seconds = CMTimeGetSeconds(time)
|
|
230
|
+
self?.sendEvent(withName: "onTimeUpdate", body: ["currentTime": seconds])
|
|
231
|
+
|
|
232
|
+
// Calculate buffer progress
|
|
233
|
+
if let timeRanges = self?.playerItem?.loadedTimeRanges,
|
|
234
|
+
let duration = self?.playerItem?.duration {
|
|
235
|
+
var buffered: Double = 0
|
|
236
|
+
|
|
237
|
+
for value in timeRanges {
|
|
238
|
+
let timeRange = value.timeRangeValue
|
|
239
|
+
let start = CMTimeGetSeconds(timeRange.start)
|
|
240
|
+
let end = start + CMTimeGetSeconds(timeRange.duration)
|
|
241
|
+
|
|
242
|
+
if seconds >= start && seconds <= end {
|
|
243
|
+
buffered = end
|
|
244
|
+
break
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let total = CMTimeGetSeconds(duration)
|
|
249
|
+
if total > 0 {
|
|
250
|
+
let percentage = (buffered / total) * 100
|
|
251
|
+
self?.sendEvent(withName: "onProgress", body: ["bufferedPercentage": percentage])
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private func setupNotifications() {
|
|
258
|
+
NotificationCenter.default.addObserver(
|
|
259
|
+
self,
|
|
260
|
+
selector: #selector(playerDidFinishPlaying),
|
|
261
|
+
name: .AVPlayerItemDidPlayToEndTime,
|
|
262
|
+
object: nil
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
@objc private func playerDidFinishPlaying() {
|
|
267
|
+
sendEvent(withName: "onEnded", body: nil)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@objc
|
|
271
|
+
func play() {
|
|
272
|
+
DispatchQueue.main.async {
|
|
273
|
+
self.player?.play()
|
|
274
|
+
self.sendEvent(withName: "onPlay", body: nil)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
@objc
|
|
279
|
+
func pause() {
|
|
280
|
+
DispatchQueue.main.async {
|
|
281
|
+
self.player?.pause()
|
|
282
|
+
self.sendEvent(withName: "onPause", body: nil)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
@objc
|
|
287
|
+
func seek(_ time: Double) {
|
|
288
|
+
DispatchQueue.main.async {
|
|
289
|
+
let cmTime = CMTime(seconds: time, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
|
290
|
+
self.sendEvent(withName: "onSeeking", body: nil)
|
|
291
|
+
|
|
292
|
+
self.player?.seek(to: cmTime) { [weak self] completed in
|
|
293
|
+
if completed {
|
|
294
|
+
self?.sendEvent(withName: "onSeeked", body: ["currentTime": time])
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
@objc
|
|
301
|
+
func setVolume(_ volume: Double) {
|
|
302
|
+
DispatchQueue.main.async {
|
|
303
|
+
self.player?.volume = Float(volume)
|
|
304
|
+
self.sendEvent(withName: "onVolumeChanged", body: ["volume": volume])
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
@objc
|
|
309
|
+
func setPlaybackRate(_ rate: Double) {
|
|
310
|
+
DispatchQueue.main.async {
|
|
311
|
+
self.player?.rate = Float(rate)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@objc
|
|
316
|
+
func setQuality(_ index: Int) {
|
|
317
|
+
guard index >= 0 && index < availableQualities.count else { return }
|
|
318
|
+
|
|
319
|
+
currentQualityIndex = index
|
|
320
|
+
let quality = availableQualities[index]
|
|
321
|
+
|
|
322
|
+
// Implementation for quality switching in HLS
|
|
323
|
+
// This would involve selecting the appropriate variant in the HLS manifest
|
|
324
|
+
|
|
325
|
+
sendEvent(withName: "onQualityChanged", body: [
|
|
326
|
+
"height": quality.height,
|
|
327
|
+
"width": quality.width,
|
|
328
|
+
"bitrate": quality.bitrate,
|
|
329
|
+
"label": quality.label,
|
|
330
|
+
"index": quality.index
|
|
331
|
+
])
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
@objc
|
|
335
|
+
func getQualities(_ callback: @escaping RCTResponseSenderBlock) {
|
|
336
|
+
let qualities = availableQualities.map { quality in
|
|
337
|
+
return [
|
|
338
|
+
"height": quality.height,
|
|
339
|
+
"width": quality.width,
|
|
340
|
+
"bitrate": quality.bitrate,
|
|
341
|
+
"label": quality.label,
|
|
342
|
+
"index": quality.index
|
|
343
|
+
]
|
|
344
|
+
}
|
|
345
|
+
callback([NSNull(), qualities])
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@objc
|
|
349
|
+
func getCurrentTime(_ callback: @escaping RCTResponseSenderBlock) {
|
|
350
|
+
guard let player = player else {
|
|
351
|
+
callback([0])
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let time = CMTimeGetSeconds(player.currentTime())
|
|
356
|
+
callback([time])
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
@objc
|
|
360
|
+
func getDuration(_ callback: @escaping RCTResponseSenderBlock) {
|
|
361
|
+
guard let duration = playerItem?.duration else {
|
|
362
|
+
callback([0])
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let seconds = CMTimeGetSeconds(duration)
|
|
367
|
+
callback([seconds])
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
@objc
|
|
371
|
+
func enterFullscreen() {
|
|
372
|
+
DispatchQueue.main.async {
|
|
373
|
+
guard self.playerViewController == nil else { return }
|
|
374
|
+
|
|
375
|
+
self.playerViewController = AVPlayerViewController()
|
|
376
|
+
self.playerViewController?.player = self.player
|
|
377
|
+
|
|
378
|
+
if let rootViewController = UIApplication.shared.keyWindow?.rootViewController {
|
|
379
|
+
rootViewController.present(self.playerViewController!, animated: true) {
|
|
380
|
+
self.sendEvent(withName: "onFullscreenChanged", body: ["isFullscreen": true])
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
@objc
|
|
387
|
+
func exitFullscreen() {
|
|
388
|
+
DispatchQueue.main.async {
|
|
389
|
+
self.playerViewController?.dismiss(animated: true) {
|
|
390
|
+
self.playerViewController = nil
|
|
391
|
+
self.sendEvent(withName: "onFullscreenChanged", body: ["isFullscreen": false])
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
@objc
|
|
397
|
+
func enterPictureInPicture() {
|
|
398
|
+
DispatchQueue.main.async {
|
|
399
|
+
guard AVPictureInPictureController.isPictureInPictureSupported() else {
|
|
400
|
+
self.sendEvent(withName: "onError", body: [
|
|
401
|
+
"code": "PIP_NOT_SUPPORTED",
|
|
402
|
+
"message": "Picture in Picture is not supported on this device"
|
|
403
|
+
])
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// PiP implementation would go here
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
@objc
|
|
412
|
+
func destroy() {
|
|
413
|
+
DispatchQueue.main.async {
|
|
414
|
+
self.player?.pause()
|
|
415
|
+
self.player?.replaceCurrentItem(with: nil)
|
|
416
|
+
|
|
417
|
+
if let timeObserver = self.timeObserver {
|
|
418
|
+
self.player?.removeTimeObserver(timeObserver)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
self.statusObserver?.invalidate()
|
|
422
|
+
self.bufferObserver?.invalidate()
|
|
423
|
+
|
|
424
|
+
NotificationCenter.default.removeObserver(self)
|
|
425
|
+
|
|
426
|
+
self.player = nil
|
|
427
|
+
self.playerItem = nil
|
|
428
|
+
self.playerLayer = nil
|
|
429
|
+
self.playerViewController = nil
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
deinit {
|
|
434
|
+
destroy()
|
|
435
|
+
}
|
|
436
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unified-video/react-native",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React Native implementation of Unified Video Framework for iOS and Android",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"react-native": "src/index.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src",
|
|
11
|
+
"ios",
|
|
12
|
+
"android"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"watch": "tsc -p tsconfig.json --watch",
|
|
17
|
+
"clean": "rm -rf dist",
|
|
18
|
+
"test": "jest",
|
|
19
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
20
|
+
"prepare": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@unified-video/core": "^1.0.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": ">=16.8.0",
|
|
27
|
+
"react-native": ">=0.60.0",
|
|
28
|
+
"react-native-video": "^6.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/react": "^18.0.0",
|
|
32
|
+
"@types/react-native": "^0.70.0",
|
|
33
|
+
"@react-native-community/eslint-config": "^3.0.0",
|
|
34
|
+
"typescript": "^4.9.0",
|
|
35
|
+
"metro-react-native-babel-preset": "^0.73.0"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"video",
|
|
39
|
+
"player",
|
|
40
|
+
"react-native",
|
|
41
|
+
"ios",
|
|
42
|
+
"android",
|
|
43
|
+
"mobile",
|
|
44
|
+
"streaming"
|
|
45
|
+
],
|
|
46
|
+
"author": "Your Company",
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|