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
package/packages/react-native/android/src/main/java/com/unifiedvideo/UnifiedVideoPlayerModule.kt
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
package com.unifiedvideo
|
|
2
|
+
|
|
3
|
+
import android.net.Uri
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.Looper
|
|
6
|
+
import android.view.View
|
|
7
|
+
import com.facebook.react.bridge.*
|
|
8
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
9
|
+
import com.google.android.exoplayer2.*
|
|
10
|
+
import com.google.android.exoplayer2.analytics.AnalyticsListener
|
|
11
|
+
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager
|
|
12
|
+
import com.google.android.exoplayer2.drm.DrmSessionManager
|
|
13
|
+
import com.google.android.exoplayer2.drm.FrameworkMediaDrm
|
|
14
|
+
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback
|
|
15
|
+
import com.google.android.exoplayer2.source.MediaSource
|
|
16
|
+
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
|
17
|
+
import com.google.android.exoplayer2.source.dash.DashMediaSource
|
|
18
|
+
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
|
19
|
+
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection
|
|
20
|
+
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
|
|
21
|
+
import com.google.android.exoplayer2.ui.PlayerView
|
|
22
|
+
import com.google.android.exoplayer2.upstream.DataSource
|
|
23
|
+
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter
|
|
24
|
+
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
|
25
|
+
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
|
|
26
|
+
import com.google.android.exoplayer2.util.MimeTypes
|
|
27
|
+
import com.google.android.exoplayer2.util.Util
|
|
28
|
+
import com.google.android.exoplayer2.video.VideoSize
|
|
29
|
+
import java.util.*
|
|
30
|
+
import kotlin.collections.ArrayList
|
|
31
|
+
|
|
32
|
+
class UnifiedVideoPlayerModule(private val reactContext: ReactApplicationContext) :
|
|
33
|
+
ReactContextBaseJavaModule(reactContext), Player.Listener, AnalyticsListener {
|
|
34
|
+
|
|
35
|
+
private var exoPlayer: ExoPlayer? = null
|
|
36
|
+
private var playerView: PlayerView? = null
|
|
37
|
+
private var trackSelector: DefaultTrackSelector? = null
|
|
38
|
+
private var dataSourceFactory: DataSource.Factory? = null
|
|
39
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
40
|
+
private var progressRunnable: Runnable? = null
|
|
41
|
+
private var currentSource: ReadableMap? = null
|
|
42
|
+
private var availableQualities: ArrayList<VideoQuality> = ArrayList()
|
|
43
|
+
private var currentQualityIndex = -1
|
|
44
|
+
|
|
45
|
+
data class VideoQuality(
|
|
46
|
+
val height: Int,
|
|
47
|
+
val width: Int,
|
|
48
|
+
val bitrate: Int,
|
|
49
|
+
val label: String,
|
|
50
|
+
val index: Int
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
override fun getName(): String = "UnifiedVideoPlayer"
|
|
54
|
+
|
|
55
|
+
@ReactMethod
|
|
56
|
+
fun initialize(config: ReadableMap) {
|
|
57
|
+
handler.post {
|
|
58
|
+
try {
|
|
59
|
+
// Initialize ExoPlayer
|
|
60
|
+
val bandwidthMeter = DefaultBandwidthMeter.Builder(reactContext).build()
|
|
61
|
+
|
|
62
|
+
trackSelector = DefaultTrackSelector(
|
|
63
|
+
reactContext,
|
|
64
|
+
AdaptiveTrackSelection.Factory()
|
|
65
|
+
).apply {
|
|
66
|
+
setParameters(
|
|
67
|
+
buildUponParameters()
|
|
68
|
+
.setMaxVideoSizeSd()
|
|
69
|
+
.setAllowVideoMixedMimeTypeAdaptiveness(true)
|
|
70
|
+
.build()
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
exoPlayer = ExoPlayer.Builder(reactContext)
|
|
75
|
+
.setTrackSelector(trackSelector!!)
|
|
76
|
+
.setBandwidthMeter(bandwidthMeter)
|
|
77
|
+
.build()
|
|
78
|
+
.apply {
|
|
79
|
+
addListener(this@UnifiedVideoPlayerModule)
|
|
80
|
+
addAnalyticsListener(this@UnifiedVideoPlayerModule)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Initialize data source factory
|
|
84
|
+
dataSourceFactory = DefaultDataSourceFactory(
|
|
85
|
+
reactContext,
|
|
86
|
+
Util.getUserAgent(reactContext, "UnifiedVideoPlayer")
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
sendEvent("onReady", null)
|
|
90
|
+
} catch (e: Exception) {
|
|
91
|
+
sendError("INIT_ERROR", "Failed to initialize player: ${e.message}")
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@ReactMethod
|
|
97
|
+
fun load(source: ReadableMap) {
|
|
98
|
+
currentSource = source
|
|
99
|
+
val url = source.getString("url") ?: run {
|
|
100
|
+
sendError("INVALID_URL", "Video URL is required")
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
handler.post {
|
|
105
|
+
try {
|
|
106
|
+
val uri = Uri.parse(url)
|
|
107
|
+
val mediaSource = createMediaSource(uri, source)
|
|
108
|
+
|
|
109
|
+
exoPlayer?.apply {
|
|
110
|
+
setMediaSource(mediaSource)
|
|
111
|
+
prepare()
|
|
112
|
+
|
|
113
|
+
// Apply config if provided
|
|
114
|
+
source.getBoolean("autoPlay").let { autoPlay ->
|
|
115
|
+
playWhenReady = autoPlay
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Extract available qualities after loading
|
|
120
|
+
extractQualities()
|
|
121
|
+
|
|
122
|
+
} catch (e: Exception) {
|
|
123
|
+
sendError("LOAD_ERROR", "Failed to load video: ${e.message}")
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private fun createMediaSource(uri: Uri, source: ReadableMap): MediaSource {
|
|
129
|
+
val drmSessionManager = if (source.hasKey("drm")) {
|
|
130
|
+
createDrmSessionManager(source.getMap("drm")!!)
|
|
131
|
+
} else {
|
|
132
|
+
DrmSessionManager.DRM_UNSUPPORTED
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return when (val type = Util.inferContentType(uri)) {
|
|
136
|
+
C.TYPE_DASH -> {
|
|
137
|
+
DashMediaSource.Factory(dataSourceFactory!!)
|
|
138
|
+
.setDrmSessionManager(drmSessionManager)
|
|
139
|
+
.createMediaSource(MediaItem.fromUri(uri))
|
|
140
|
+
}
|
|
141
|
+
C.TYPE_HLS -> {
|
|
142
|
+
HlsMediaSource.Factory(dataSourceFactory!!)
|
|
143
|
+
.setDrmSessionManager(drmSessionManager)
|
|
144
|
+
.setAllowChunklessPreparation(true)
|
|
145
|
+
.createMediaSource(MediaItem.fromUri(uri))
|
|
146
|
+
}
|
|
147
|
+
C.TYPE_OTHER -> {
|
|
148
|
+
ProgressiveMediaSource.Factory(dataSourceFactory!!)
|
|
149
|
+
.setDrmSessionManager(drmSessionManager)
|
|
150
|
+
.createMediaSource(MediaItem.fromUri(uri))
|
|
151
|
+
}
|
|
152
|
+
else -> {
|
|
153
|
+
throw IllegalArgumentException("Unsupported type: $type")
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private fun createDrmSessionManager(drmConfig: ReadableMap): DrmSessionManager {
|
|
159
|
+
val licenseUrl = drmConfig.getString("licenseUrl") ?: throw IllegalArgumentException("License URL required")
|
|
160
|
+
val drmType = drmConfig.getString("type") ?: "widevine"
|
|
161
|
+
|
|
162
|
+
val drmSchemeUuid = when (drmType) {
|
|
163
|
+
"widevine" -> C.WIDEVINE_UUID
|
|
164
|
+
"playready" -> C.PLAYREADY_UUID
|
|
165
|
+
"clearkey" -> C.CLEARKEY_UUID
|
|
166
|
+
else -> throw IllegalArgumentException("Unsupported DRM type: $drmType")
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
val httpDataSourceFactory = DefaultHttpDataSource.Factory()
|
|
170
|
+
.setUserAgent(Util.getUserAgent(reactContext, "UnifiedVideoPlayer"))
|
|
171
|
+
|
|
172
|
+
// Add headers if provided
|
|
173
|
+
if (drmConfig.hasKey("headers")) {
|
|
174
|
+
val headers = drmConfig.getMap("headers")
|
|
175
|
+
headers?.let { headerMap ->
|
|
176
|
+
val httpHeaders = HashMap<String, String>()
|
|
177
|
+
val iterator = headerMap.keySetIterator()
|
|
178
|
+
while (iterator.hasNextKey()) {
|
|
179
|
+
val key = iterator.nextKey()
|
|
180
|
+
httpHeaders[key] = headerMap.getString(key) ?: ""
|
|
181
|
+
}
|
|
182
|
+
httpDataSourceFactory.setDefaultRequestProperties(httpHeaders)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
val drmCallback = HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory)
|
|
187
|
+
|
|
188
|
+
return DefaultDrmSessionManager.Builder()
|
|
189
|
+
.setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
|
|
190
|
+
.build(drmCallback)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private fun extractQualities() {
|
|
194
|
+
val player = exoPlayer ?: return
|
|
195
|
+
val selector = trackSelector ?: return
|
|
196
|
+
|
|
197
|
+
availableQualities.clear()
|
|
198
|
+
|
|
199
|
+
val trackGroups = selector.currentMappedTrackInfo
|
|
200
|
+
if (trackGroups != null) {
|
|
201
|
+
for (rendererIndex in 0 until trackGroups.rendererCount) {
|
|
202
|
+
if (trackGroups.getRendererType(rendererIndex) == C.TRACK_TYPE_VIDEO) {
|
|
203
|
+
val trackGroupArray = trackGroups.getTrackGroups(rendererIndex)
|
|
204
|
+
|
|
205
|
+
for (groupIndex in 0 until trackGroupArray.length) {
|
|
206
|
+
val trackGroup = trackGroupArray[groupIndex]
|
|
207
|
+
|
|
208
|
+
for (trackIndex in 0 until trackGroup.length) {
|
|
209
|
+
val format = trackGroup.getFormat(trackIndex)
|
|
210
|
+
|
|
211
|
+
val quality = VideoQuality(
|
|
212
|
+
height = format.height,
|
|
213
|
+
width = format.width,
|
|
214
|
+
bitrate = format.bitrate,
|
|
215
|
+
label = "${format.height}p",
|
|
216
|
+
index = availableQualities.size
|
|
217
|
+
)
|
|
218
|
+
availableQualities.add(quality)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Send available qualities to JS
|
|
226
|
+
val qualitiesArray = WritableNativeArray()
|
|
227
|
+
availableQualities.forEach { quality ->
|
|
228
|
+
val qualityMap = WritableNativeMap().apply {
|
|
229
|
+
putInt("height", quality.height)
|
|
230
|
+
putInt("width", quality.width)
|
|
231
|
+
putInt("bitrate", quality.bitrate)
|
|
232
|
+
putString("label", quality.label)
|
|
233
|
+
putInt("index", quality.index)
|
|
234
|
+
}
|
|
235
|
+
qualitiesArray.pushMap(qualityMap)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
val event = WritableNativeMap().apply {
|
|
239
|
+
putArray("qualities", qualitiesArray)
|
|
240
|
+
}
|
|
241
|
+
sendEvent("onQualitiesAvailable", event)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@ReactMethod
|
|
245
|
+
fun play() {
|
|
246
|
+
handler.post {
|
|
247
|
+
exoPlayer?.play()
|
|
248
|
+
sendEvent("onPlay", null)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@ReactMethod
|
|
253
|
+
fun pause() {
|
|
254
|
+
handler.post {
|
|
255
|
+
exoPlayer?.pause()
|
|
256
|
+
sendEvent("onPause", null)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@ReactMethod
|
|
261
|
+
fun seek(time: Double) {
|
|
262
|
+
handler.post {
|
|
263
|
+
exoPlayer?.seekTo((time * 1000).toLong())
|
|
264
|
+
sendEvent("onSeeking", null)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
@ReactMethod
|
|
269
|
+
fun setVolume(volume: Double) {
|
|
270
|
+
handler.post {
|
|
271
|
+
exoPlayer?.volume = volume.toFloat()
|
|
272
|
+
val event = WritableNativeMap().apply {
|
|
273
|
+
putDouble("volume", volume)
|
|
274
|
+
}
|
|
275
|
+
sendEvent("onVolumeChanged", event)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
@ReactMethod
|
|
280
|
+
fun setPlaybackRate(rate: Double) {
|
|
281
|
+
handler.post {
|
|
282
|
+
exoPlayer?.setPlaybackSpeed(rate.toFloat())
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
@ReactMethod
|
|
287
|
+
fun setQuality(index: Int) {
|
|
288
|
+
if (index < 0 || index >= availableQualities.size) return
|
|
289
|
+
|
|
290
|
+
handler.post {
|
|
291
|
+
currentQualityIndex = index
|
|
292
|
+
val quality = availableQualities[index]
|
|
293
|
+
|
|
294
|
+
// Apply quality selection to track selector
|
|
295
|
+
trackSelector?.let { selector ->
|
|
296
|
+
val parametersBuilder = selector.buildUponParameters()
|
|
297
|
+
parametersBuilder.setMaxVideoSize(quality.width, quality.height)
|
|
298
|
+
parametersBuilder.setMaxVideoBitrate(quality.bitrate)
|
|
299
|
+
selector.setParameters(parametersBuilder.build())
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
val event = WritableNativeMap().apply {
|
|
303
|
+
putInt("height", quality.height)
|
|
304
|
+
putInt("width", quality.width)
|
|
305
|
+
putInt("bitrate", quality.bitrate)
|
|
306
|
+
putString("label", quality.label)
|
|
307
|
+
putInt("index", quality.index)
|
|
308
|
+
}
|
|
309
|
+
sendEvent("onQualityChanged", event)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
@ReactMethod
|
|
314
|
+
fun getQualities(callback: Callback) {
|
|
315
|
+
val qualitiesArray = WritableNativeArray()
|
|
316
|
+
availableQualities.forEach { quality ->
|
|
317
|
+
val qualityMap = WritableNativeMap().apply {
|
|
318
|
+
putInt("height", quality.height)
|
|
319
|
+
putInt("width", quality.width)
|
|
320
|
+
putInt("bitrate", quality.bitrate)
|
|
321
|
+
putString("label", quality.label)
|
|
322
|
+
putInt("index", quality.index)
|
|
323
|
+
}
|
|
324
|
+
qualitiesArray.pushMap(qualityMap)
|
|
325
|
+
}
|
|
326
|
+
callback.invoke(null, qualitiesArray)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
@ReactMethod
|
|
330
|
+
fun getCurrentTime(callback: Callback) {
|
|
331
|
+
val currentPosition = exoPlayer?.currentPosition ?: 0
|
|
332
|
+
callback.invoke(currentPosition / 1000.0)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
@ReactMethod
|
|
336
|
+
fun getDuration(callback: Callback) {
|
|
337
|
+
val duration = exoPlayer?.duration ?: 0
|
|
338
|
+
callback.invoke(if (duration > 0) duration / 1000.0 else 0)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
@ReactMethod
|
|
342
|
+
fun enterFullscreen() {
|
|
343
|
+
handler.post {
|
|
344
|
+
playerView?.let { view ->
|
|
345
|
+
// Implementation for fullscreen
|
|
346
|
+
val event = WritableNativeMap().apply {
|
|
347
|
+
putBoolean("isFullscreen", true)
|
|
348
|
+
}
|
|
349
|
+
sendEvent("onFullscreenChanged", event)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
@ReactMethod
|
|
355
|
+
fun exitFullscreen() {
|
|
356
|
+
handler.post {
|
|
357
|
+
playerView?.let { view ->
|
|
358
|
+
// Implementation for exit fullscreen
|
|
359
|
+
val event = WritableNativeMap().apply {
|
|
360
|
+
putBoolean("isFullscreen", false)
|
|
361
|
+
}
|
|
362
|
+
sendEvent("onFullscreenChanged", event)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
@ReactMethod
|
|
368
|
+
fun destroy() {
|
|
369
|
+
handler.post {
|
|
370
|
+
stopProgressTimer()
|
|
371
|
+
exoPlayer?.apply {
|
|
372
|
+
stop()
|
|
373
|
+
release()
|
|
374
|
+
}
|
|
375
|
+
exoPlayer = null
|
|
376
|
+
playerView = null
|
|
377
|
+
trackSelector = null
|
|
378
|
+
availableQualities.clear()
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Player.Listener implementation
|
|
383
|
+
override fun onPlaybackStateChanged(playbackState: Int) {
|
|
384
|
+
when (playbackState) {
|
|
385
|
+
Player.STATE_READY -> {
|
|
386
|
+
sendEvent("onReady", null)
|
|
387
|
+
startProgressTimer()
|
|
388
|
+
}
|
|
389
|
+
Player.STATE_ENDED -> {
|
|
390
|
+
sendEvent("onEnded", null)
|
|
391
|
+
stopProgressTimer()
|
|
392
|
+
}
|
|
393
|
+
Player.STATE_BUFFERING -> {
|
|
394
|
+
val event = WritableNativeMap().apply {
|
|
395
|
+
putBoolean("isBuffering", true)
|
|
396
|
+
}
|
|
397
|
+
sendEvent("onBuffering", event)
|
|
398
|
+
}
|
|
399
|
+
Player.STATE_IDLE -> {
|
|
400
|
+
stopProgressTimer()
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
406
|
+
if (isPlaying) {
|
|
407
|
+
startProgressTimer()
|
|
408
|
+
} else {
|
|
409
|
+
stopProgressTimer()
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
override fun onPlayerError(error: PlaybackException) {
|
|
414
|
+
val errorMap = WritableNativeMap().apply {
|
|
415
|
+
putString("code", "PLAYBACK_ERROR")
|
|
416
|
+
putString("message", error.message ?: "Unknown playback error")
|
|
417
|
+
}
|
|
418
|
+
sendEvent("onError", errorMap)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
override fun onVideoSizeChanged(videoSize: VideoSize) {
|
|
422
|
+
val event = WritableNativeMap().apply {
|
|
423
|
+
putInt("width", videoSize.width)
|
|
424
|
+
putInt("height", videoSize.height)
|
|
425
|
+
}
|
|
426
|
+
sendEvent("onVideoSizeChanged", event)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
override fun onRenderedFirstFrame() {
|
|
430
|
+
val event = WritableNativeMap().apply {
|
|
431
|
+
putDouble("duration", (exoPlayer?.duration ?: 0) / 1000.0)
|
|
432
|
+
}
|
|
433
|
+
sendEvent("onLoadedMetadata", event)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Progress timer
|
|
437
|
+
private fun startProgressTimer() {
|
|
438
|
+
stopProgressTimer()
|
|
439
|
+
progressRunnable = object : Runnable {
|
|
440
|
+
override fun run() {
|
|
441
|
+
exoPlayer?.let { player ->
|
|
442
|
+
val currentTime = player.currentPosition / 1000.0
|
|
443
|
+
val bufferedPercentage = player.bufferedPercentage
|
|
444
|
+
|
|
445
|
+
val event = WritableNativeMap().apply {
|
|
446
|
+
putDouble("currentTime", currentTime)
|
|
447
|
+
}
|
|
448
|
+
sendEvent("onTimeUpdate", event)
|
|
449
|
+
|
|
450
|
+
val progressEvent = WritableNativeMap().apply {
|
|
451
|
+
putDouble("bufferedPercentage", bufferedPercentage.toDouble())
|
|
452
|
+
}
|
|
453
|
+
sendEvent("onProgress", progressEvent)
|
|
454
|
+
}
|
|
455
|
+
handler.postDelayed(this, 250)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
handler.post(progressRunnable!!)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private fun stopProgressTimer() {
|
|
462
|
+
progressRunnable?.let {
|
|
463
|
+
handler.removeCallbacks(it)
|
|
464
|
+
}
|
|
465
|
+
progressRunnable = null
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Event sending
|
|
469
|
+
private fun sendEvent(eventName: String, params: WritableMap?) {
|
|
470
|
+
reactContext
|
|
471
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
472
|
+
.emit(eventName, params)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private fun sendError(code: String, message: String) {
|
|
476
|
+
val errorMap = WritableNativeMap().apply {
|
|
477
|
+
putString("code", code)
|
|
478
|
+
putString("message", message)
|
|
479
|
+
}
|
|
480
|
+
sendEvent("onError", errorMap)
|
|
481
|
+
}
|
|
482
|
+
}
|