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