@bluebillywig/react-native-bb-player 8.44.0 → 8.45.4

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 (55) hide show
  1. package/README.md +80 -59
  2. package/android/build.gradle +4 -3
  3. package/android/src/main/java/com/bluebillywig/bbplayer/BBPlayerModule.kt +174 -28
  4. package/android/src/main/java/com/bluebillywig/bbplayer/BBPlayerView.kt +70 -167
  5. package/android/src/main/java/com/bluebillywig/bbplayer/BBPlayerViewManager.kt +9 -7
  6. package/android/src/paper/java/com/bluebillywig/bbplayer/NativeBBPlayerModuleSpec.java +19 -8
  7. package/ios/BBPlayerModule.mm +17 -8
  8. package/ios/BBPlayerModule.swift +192 -26
  9. package/ios/BBPlayerView.swift +55 -140
  10. package/ios/BBPlayerViewManager.m +2 -2
  11. package/ios/BBPlayerViewManager.swift +12 -0
  12. package/lib/commonjs/BBModalPlayer.js +21 -0
  13. package/lib/commonjs/BBModalPlayer.js.map +1 -0
  14. package/lib/commonjs/BBOutstreamView.js +2 -1
  15. package/lib/commonjs/BBOutstreamView.js.map +1 -1
  16. package/lib/commonjs/BBPlayerView.js +0 -1
  17. package/lib/commonjs/BBPlayerView.js.map +1 -1
  18. package/lib/commonjs/NativeCommands.js +32 -24
  19. package/lib/commonjs/NativeCommands.js.map +1 -1
  20. package/lib/commonjs/index.js +9 -1
  21. package/lib/commonjs/index.js.map +1 -1
  22. package/lib/commonjs/specs/NativeBBPlayerModule.js.map +1 -1
  23. package/lib/module/BBModalPlayer.js +17 -0
  24. package/lib/module/BBModalPlayer.js.map +1 -0
  25. package/lib/module/BBOutstreamView.js +2 -1
  26. package/lib/module/BBOutstreamView.js.map +1 -1
  27. package/lib/module/BBPlayerView.js +0 -1
  28. package/lib/module/BBPlayerView.js.map +1 -1
  29. package/lib/module/NativeCommands.js +32 -24
  30. package/lib/module/NativeCommands.js.map +1 -1
  31. package/lib/module/index.js +1 -0
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/specs/NativeBBPlayerModule.js.map +1 -1
  34. package/lib/typescript/src/BBModalPlayer.d.ts +13 -0
  35. package/lib/typescript/src/BBModalPlayer.d.ts.map +1 -0
  36. package/lib/typescript/src/BBOutstreamView.d.ts.map +1 -1
  37. package/lib/typescript/src/BBPlayer.types.d.ts +28 -20
  38. package/lib/typescript/src/BBPlayer.types.d.ts.map +1 -1
  39. package/lib/typescript/src/BBPlayerView.d.ts +0 -2
  40. package/lib/typescript/src/BBPlayerView.d.ts.map +1 -1
  41. package/lib/typescript/src/NativeCommands.d.ts +10 -9
  42. package/lib/typescript/src/NativeCommands.d.ts.map +1 -1
  43. package/lib/typescript/src/index.d.ts +2 -0
  44. package/lib/typescript/src/index.d.ts.map +1 -1
  45. package/lib/typescript/src/specs/NativeBBPlayerModule.d.ts +13 -8
  46. package/lib/typescript/src/specs/NativeBBPlayerModule.d.ts.map +1 -1
  47. package/package.json +9 -10
  48. package/src/BBModalPlayer.ts +33 -0
  49. package/src/BBOutstreamView.tsx +2 -1
  50. package/src/BBPlayer.types.ts +35 -17
  51. package/src/BBPlayerView.tsx +0 -12
  52. package/src/NativeCommands.ts +45 -26
  53. package/src/index.ts +2 -0
  54. package/src/specs/NativeBBPlayerModule.ts +23 -8
  55. package/android/proguard-rules.pro +0 -59
@@ -12,6 +12,7 @@ import android.widget.FrameLayout
12
12
  import com.facebook.react.modules.core.ReactChoreographer
13
13
  import androidx.collection.ArrayMap
14
14
  import androidx.mediarouter.app.MediaRouteButton
15
+ import org.json.JSONObject
15
16
  import com.bluebillywig.bbnativeplayersdk.BBNativePlayer
16
17
  import com.bluebillywig.bbnativeplayersdk.BBNativePlayerView
17
18
  import com.bluebillywig.bbnativeplayersdk.BBNativePlayerViewDelegate
@@ -176,14 +177,8 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
176
177
  // END NATIVE LAYOUT INTEGRATION
177
178
  // ==================================================================================
178
179
 
179
- // Timer for periodic time updates (opt-in for performance)
180
- private val timeUpdateHandler = Handler(Looper.getMainLooper())
181
- private var timeUpdateRunnable: Runnable? = null
182
180
  private var isPlaying = false
183
181
  private var currentDuration: Double = 0.0
184
- private var lastKnownTime: Double = 0.0
185
- private var playbackStartTimestamp: Long = 0
186
- private var enableTimeUpdates: Boolean = false
187
182
 
188
183
  // Event emission helper using modern EventDispatcher
189
184
  private fun sendEvent(eventName: String, params: WritableMap?) {
@@ -222,17 +217,6 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
222
217
  this.options["autoPlay"] = autoPlay
223
218
  }
224
219
 
225
- fun setEnableTimeUpdates(enabled: Boolean) {
226
- enableTimeUpdates = enabled
227
- debugLog("BBPlayerView") { "Time updates ${if (enabled) "enabled" else "disabled"}" }
228
-
229
- if (!enabled && timeUpdateRunnable != null) {
230
- stopTimeUpdates()
231
- } else if (enabled && isPlaying && timeUpdateRunnable == null) {
232
- startTimeUpdates()
233
- }
234
- }
235
-
236
220
  fun setOptions(optionsMap: ReadableMap?) {
237
221
  optionsMap?.let { map ->
238
222
  val iterator = map.keySetIterator()
@@ -275,91 +259,36 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
275
259
 
276
260
  addView(playerView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
277
261
 
278
- // Set ExoPlayer's resize mode to FILL to prevent letterboxing margins
279
- // This addresses top/bottom margin issues caused by AspectRatioFrameLayout
262
+ // RESIZE_MODE_FILL prevents SurfaceView bleed-through at container edges
263
+ // when embedding inline above a WebView. The container aspect ratio is
264
+ // adapted dynamically via onDidTriggerMediaClipLoaded to avoid stretching.
280
265
  postDelayed({
281
- setExoPlayerResizeMode(playerView, RESIZE_MODE_FILL)
282
- }, 1000)
266
+ setExoPlayerResizeMode(playerView)
267
+ }, 500)
283
268
 
284
269
  playerSetup = true
285
270
  debugLog("BBPlayerView") { "Player setup complete with URL: $jsonUrl" }
286
271
  }
287
272
 
288
- companion object {
289
- // AspectRatioFrameLayout resize mode constants
290
- private const val RESIZE_MODE_FILL = 3
291
- }
292
-
293
273
  /**
294
- * Recursively find ExoPlayer's PlayerView or AspectRatioFrameLayout and set resize mode.
295
- * Uses reflection since media3 classes aren't directly available in RN module classpath.
296
- * This fixes top/bottom margin issues caused by AspectRatioFrameLayout letterboxing.
274
+ * Set ExoPlayer's resize mode to FILL via reflection.
275
+ * FILL prevents SurfaceView bleed-through artifacts at container edges.
297
276
  */
298
- private fun setExoPlayerResizeMode(view: View, resizeMode: Int): Boolean {
299
- // Try Media3 AspectRatioFrameLayout via reflection
300
- try {
301
- val aspectRatioClass = Class.forName("androidx.media3.ui.AspectRatioFrameLayout")
302
- if (aspectRatioClass.isInstance(view)) {
303
- val method = aspectRatioClass.getMethod("setResizeMode", Int::class.javaPrimitiveType)
304
- method.invoke(view, resizeMode)
305
- return true
306
- }
307
- } catch (_: ClassNotFoundException) {
308
- // Media3 not available
309
- } catch (_: Exception) {
310
- // Failed to set resize mode
311
- }
312
-
313
- // Try Media3 PlayerView via reflection
277
+ private fun setExoPlayerResizeMode(view: View): Boolean {
314
278
  try {
315
279
  val playerViewClass = Class.forName("androidx.media3.ui.PlayerView")
316
280
  if (playerViewClass.isInstance(view)) {
317
281
  val method = playerViewClass.getMethod("setResizeMode", Int::class.javaPrimitiveType)
318
- method.invoke(view, resizeMode)
319
- // Continue searching - there might be an AspectRatioFrameLayout inside
320
- }
321
- } catch (_: ClassNotFoundException) {
322
- // Media3 PlayerView not available
323
- } catch (_: Exception) {
324
- // Failed to set resize mode
325
- }
326
-
327
- // Try legacy ExoPlayer2 AspectRatioFrameLayout via reflection
328
- try {
329
- val legacyAspectRatioClass = Class.forName("com.google.android.exoplayer2.ui.AspectRatioFrameLayout")
330
- if (legacyAspectRatioClass.isInstance(view)) {
331
- val method = legacyAspectRatioClass.getMethod("setResizeMode", Int::class.javaPrimitiveType)
332
- method.invoke(view, resizeMode)
282
+ method.invoke(view, 3) // RESIZE_MODE_FILL = 3
333
283
  return true
334
284
  }
335
- } catch (_: ClassNotFoundException) {
336
- // ExoPlayer2 not available
337
- } catch (_: Exception) {
338
- // Failed to set resize mode
339
- }
285
+ } catch (_: Exception) {}
340
286
 
341
- // Try legacy ExoPlayer2 StyledPlayerView via reflection
342
- try {
343
- val styledPlayerViewClass = Class.forName("com.google.android.exoplayer2.ui.StyledPlayerView")
344
- if (styledPlayerViewClass.isInstance(view)) {
345
- val method = styledPlayerViewClass.getMethod("setResizeMode", Int::class.javaPrimitiveType)
346
- method.invoke(view, resizeMode)
347
- }
348
- } catch (_: ClassNotFoundException) {
349
- // StyledPlayerView not available
350
- } catch (_: Exception) {
351
- // Failed to set resize mode
352
- }
353
-
354
- // Recursively search children
355
287
  if (view is ViewGroup) {
356
288
  for (i in 0 until view.childCount) {
357
- if (setExoPlayerResizeMode(view.getChildAt(i), resizeMode)) {
358
- return true
359
- }
289
+ if (setExoPlayerResizeMode(view.getChildAt(i))) return true
360
290
  }
361
291
  }
362
-
363
292
  return false
364
293
  }
365
294
 
@@ -370,13 +299,13 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
370
299
  *
371
300
  * Note: Shorts URLs (/sh/{id}.json) are NOT supported here - use BBShortsView instead.
372
301
  */
373
- fun loadWithJsonUrl(url: String, autoPlay: Boolean = true) {
302
+ fun loadWithJsonUrl(url: String, autoPlay: Boolean = true, contextJson: String? = null) {
374
303
  if (!::playerView.isInitialized) {
375
304
  Log.w("BBPlayerView", "Cannot load content - playerView not initialized")
376
305
  return
377
306
  }
378
307
 
379
- Log.d("BBPlayerView", "loadWithJsonUrl called with URL: $url")
308
+ Log.d("BBPlayerView", "loadWithJsonUrl called with URL: $url, context: $contextJson")
380
309
 
381
310
  // Extract ID from URL patterns like:
382
311
  // /c/{id}.json or /mediaclip/{id}.json -> clip ID
@@ -388,6 +317,8 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
388
317
  val projectIdRegex = Regex("/pj/([0-9]+)\\.json|/project/([0-9]+)")
389
318
  val shortsIdRegex = Regex("/sh/([0-9]+)\\.json")
390
319
 
320
+ val context = parseContext(contextJson)
321
+
391
322
  when {
392
323
  shortsIdRegex.containsMatchIn(url) -> {
393
324
  // Shorts require a separate BBShortsView component
@@ -401,7 +332,7 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
401
332
  val clipListId = match?.groupValues?.drop(1)?.firstOrNull { it.isNotEmpty() }
402
333
  if (clipListId != null) {
403
334
  Log.d("BBPlayerView", "Loading ClipList by ID: $clipListId")
404
- playerView.player?.loadWithClipListId(clipListId, "external", autoPlay, null)
335
+ playerView.player?.loadWithClipListId(clipListId, "external", autoPlay, null, context)
405
336
  } else {
406
337
  Log.e("BBPlayerView", "Failed to extract cliplist ID from URL: $url")
407
338
  }
@@ -411,7 +342,7 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
411
342
  val projectId = match?.groupValues?.drop(1)?.firstOrNull { it.isNotEmpty() }
412
343
  if (projectId != null) {
413
344
  Log.d("BBPlayerView", "Loading Project by ID: $projectId")
414
- playerView.player?.loadWithProjectId(projectId, "external", autoPlay, null)
345
+ playerView.player?.loadWithProjectId(projectId, "external", autoPlay, null, context)
415
346
  } else {
416
347
  Log.e("BBPlayerView", "Failed to extract project ID from URL: $url")
417
348
  }
@@ -421,7 +352,7 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
421
352
  val clipId = match?.groupValues?.drop(1)?.firstOrNull { it.isNotEmpty() }
422
353
  if (clipId != null) {
423
354
  Log.d("BBPlayerView", "Loading Clip by ID: $clipId")
424
- playerView.player?.loadWithClipId(clipId, "external", autoPlay, null)
355
+ playerView.player?.loadWithClipId(clipId, "external", autoPlay, null, context)
425
356
  } else {
426
357
  Log.e("BBPlayerView", "Failed to extract clip ID from URL: $url")
427
358
  }
@@ -439,7 +370,6 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
439
370
  debugLog("BBPlayerView") { "removePlayer called" }
440
371
  playerSetup = false
441
372
  isPlaying = false
442
- stopTimeUpdates()
443
373
  removeAllViews()
444
374
 
445
375
  if (::playerView.isInitialized) {
@@ -468,11 +398,8 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
468
398
  }
469
399
 
470
400
  fun seekRelative(offsetInSeconds: Double) {
471
- if (::playerView.isInitialized) {
472
- val currentTime = calculateEstimatedCurrentTime()
473
- val newPosition = kotlin.math.max(0.0, kotlin.math.min(currentDuration, currentTime + offsetInSeconds))
474
- playerView.player?.seek(newPosition)
475
- }
401
+ // seekRelative not yet available in native SDK
402
+ android.util.Log.w("BBPlayerView", "seekRelative not supported by native SDK")
476
403
  }
477
404
 
478
405
  fun setVolume(volume: Double) {
@@ -505,6 +432,15 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
505
432
  }
506
433
  }
507
434
 
435
+ fun presentModal() {
436
+ // Use in-place fullscreen with landscape; modalPlayer option hides fullscreen button
437
+ enterFullscreenLandscape()
438
+ }
439
+
440
+ fun closeModal() {
441
+ exitFullscreen()
442
+ }
443
+
508
444
  fun enterFullscreen() {
509
445
  if (::playerView.isInitialized) {
510
446
  shouldForceLandscape = false
@@ -610,22 +546,6 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
610
546
  } else null
611
547
  }
612
548
 
613
- private fun calculateEstimatedCurrentTime(): Double {
614
- return if (isPlaying && playbackStartTimestamp > 0) {
615
- val elapsedSeconds = (System.currentTimeMillis() - playbackStartTimestamp) / 1000.0
616
- val estimatedTime = lastKnownTime + elapsedSeconds
617
- kotlin.math.min(estimatedTime, currentDuration)
618
- } else {
619
- lastKnownTime
620
- }
621
- }
622
-
623
- fun getCurrentTime(): Double? {
624
- return if (::playerView.isInitialized) {
625
- calculateEstimatedCurrentTime()
626
- } else null
627
- }
628
-
629
549
  fun getDuration(): Double? {
630
550
  return if (::playerView.isInitialized) {
631
551
  playerView.getApiProperty(com.bluebillywig.bbnativeshared.enums.ApiProperty.duration) as? Double
@@ -672,73 +592,63 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
672
592
  return null
673
593
  }
674
594
 
675
- // Load methods
676
- fun loadWithClipId(clipId: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null) {
677
- if (::playerView.isInitialized) {
678
- playerView.player?.loadWithClipId(clipId, initiator, autoPlay, seekTo)
595
+ // Helper to parse context JSON into a Map for the native SDK
596
+ private fun parseContext(contextJson: String?): Map<String, Any?>? {
597
+ if (contextJson.isNullOrBlank()) return null
598
+ return try {
599
+ val json = JSONObject(contextJson)
600
+ mapOf(
601
+ "contextEntityType" to json.optString("contextEntityType", null),
602
+ "contextEntityId" to json.optString("contextEntityId", null),
603
+ "contextCollectionType" to json.optString("contextCollectionType", null),
604
+ "contextCollectionId" to json.optString("contextCollectionId", null)
605
+ ).filterValues { it != null }
606
+ } catch (e: Exception) {
607
+ Log.w("BBPlayerView", "Failed to parse context JSON: $contextJson", e)
608
+ null
679
609
  }
680
610
  }
681
611
 
682
- fun loadWithClipListId(clipListId: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null) {
612
+ // Load methods
613
+ fun loadWithClipId(clipId: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null, contextJson: String? = null) {
683
614
  if (::playerView.isInitialized) {
684
- playerView.player?.loadWithClipListId(clipListId, initiator, autoPlay, seekTo)
615
+ val context = parseContext(contextJson)
616
+ playerView.player?.loadWithClipId(clipId, initiator, autoPlay, seekTo, context)
685
617
  }
686
618
  }
687
619
 
688
- fun loadWithProjectId(projectId: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null) {
620
+ fun loadWithClipListId(clipListId: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null, contextJson: String? = null) {
689
621
  if (::playerView.isInitialized) {
690
- playerView.player?.loadWithProjectId(projectId, initiator, autoPlay, seekTo)
622
+ val context = parseContext(contextJson)
623
+ playerView.player?.loadWithClipListId(clipListId, initiator, autoPlay, seekTo, context)
691
624
  }
692
625
  }
693
626
 
694
- fun loadWithClipJson(clipJson: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null) {
627
+ fun loadWithProjectId(projectId: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null, contextJson: String? = null) {
695
628
  if (::playerView.isInitialized) {
696
- playerView.player?.loadWithClipJson(clipJson, initiator, autoPlay, seekTo)
629
+ val context = parseContext(contextJson)
630
+ playerView.player?.loadWithProjectId(projectId, initiator, autoPlay, seekTo, context)
697
631
  }
698
632
  }
699
633
 
700
- fun loadWithClipListJson(clipListJson: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null) {
634
+ fun loadWithClipJson(clipJson: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null, contextJson: String? = null) {
701
635
  if (::playerView.isInitialized) {
702
- playerView.player?.loadWithClipListJson(clipListJson, initiator, autoPlay, seekTo)
636
+ val context = parseContext(contextJson)
637
+ playerView.player?.loadWithClipJson(clipJson, initiator, autoPlay, seekTo, context)
703
638
  }
704
639
  }
705
640
 
706
- fun loadWithProjectJson(projectJson: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null) {
641
+ fun loadWithClipListJson(clipListJson: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null, contextJson: String? = null) {
707
642
  if (::playerView.isInitialized) {
708
- playerView.player?.loadWithProjectJson(projectJson, initiator, autoPlay, seekTo)
643
+ val context = parseContext(contextJson)
644
+ playerView.player?.loadWithClipListJson(clipListJson, initiator, autoPlay, seekTo, context)
709
645
  }
710
646
  }
711
647
 
712
- private fun startTimeUpdates() {
713
- if (!enableTimeUpdates || timeUpdateRunnable != null) {
714
- return
715
- }
716
-
717
- timeUpdateRunnable = object : Runnable {
718
- override fun run() {
719
- if (::playerView.isInitialized && isPlaying) {
720
- val currentTime = calculateEstimatedCurrentTime()
721
-
722
- if (currentDuration > 0) {
723
- val params = Arguments.createMap().apply {
724
- putDouble("currentTime", currentTime)
725
- putDouble("duration", currentDuration)
726
- }
727
- sendEvent("onDidTriggerTimeUpdate", params)
728
- }
729
-
730
- timeUpdateHandler.postDelayed(this, 1000)
731
- }
732
- }
733
- }
734
-
735
- timeUpdateHandler.postDelayed(timeUpdateRunnable!!, 1000)
736
- }
737
-
738
- private fun stopTimeUpdates() {
739
- timeUpdateRunnable?.let {
740
- timeUpdateHandler.removeCallbacks(it)
741
- timeUpdateRunnable = null
648
+ fun loadWithProjectJson(projectJson: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null, contextJson: String? = null) {
649
+ if (::playerView.isInitialized) {
650
+ val context = parseContext(contextJson)
651
+ playerView.player?.loadWithProjectJson(projectJson, initiator, autoPlay, seekTo, context)
742
652
  }
743
653
  }
744
654
 
@@ -756,10 +666,12 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
756
666
  }
757
667
 
758
668
  override fun didTriggerMediaClipLoaded(view: BBNativePlayerView, clipData: MediaClip?) {
759
- debugLog("BBPlayerView") { "didTriggerMediaClipLoaded: ${clipData?.title}" }
669
+ debugLog("BBPlayerView") { "didTriggerMediaClipLoaded: ${clipData?.title} (${clipData?.width}x${clipData?.height})" }
760
670
  val params = Arguments.createMap().apply {
761
671
  putString("title", clipData?.title)
762
672
  putString("id", clipData?.id)
673
+ clipData?.width?.let { putDouble("width", it.toDouble()) }
674
+ clipData?.height?.let { putDouble("height", it.toDouble()) }
763
675
  }
764
676
  sendEvent("onDidTriggerMediaClipLoaded", params)
765
677
  }
@@ -839,24 +751,20 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
839
751
  }
840
752
 
841
753
  override fun didTriggerPlaying(view: BBNativePlayerView) {
842
- debugLog("BBPlayerView") { "didTriggerPlaying - starting periodic time updates" }
754
+ debugLog("BBPlayerView") { "didTriggerPlaying" }
843
755
  isPlaying = true
844
- playbackStartTimestamp = System.currentTimeMillis()
845
- startTimeUpdates()
846
756
  sendEvent("onDidTriggerPlaying")
847
757
  }
848
758
 
849
759
  override fun didTriggerPause(view: BBNativePlayerView) {
850
- debugLog("BBPlayerView") { "didTriggerPause - stopping periodic time updates" }
760
+ debugLog("BBPlayerView") { "didTriggerPause" }
851
761
  isPlaying = false
852
- stopTimeUpdates()
853
762
  sendEvent("onDidTriggerPause")
854
763
  }
855
764
 
856
765
  override fun didTriggerEnded(view: BBNativePlayerView) {
857
- debugLog("BBPlayerView") { "didTriggerEnded - stopping periodic time updates" }
766
+ debugLog("BBPlayerView") { "didTriggerEnded" }
858
767
  isPlaying = false
859
- stopTimeUpdates()
860
768
  sendEvent("onDidTriggerEnded")
861
769
  }
862
770
 
@@ -867,9 +775,6 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
867
775
 
868
776
  override fun didTriggerSeeked(view: BBNativePlayerView, seekOffset: Double?) {
869
777
  debugLog("BBPlayerView") { "didTriggerSeeked: $seekOffset" }
870
- lastKnownTime = seekOffset ?: 0.0
871
- playbackStartTimestamp = System.currentTimeMillis()
872
-
873
778
  val params = Arguments.createMap().apply {
874
779
  putDouble("payload", seekOffset ?: 0.0)
875
780
  }
@@ -1062,8 +967,6 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
1062
967
 
1063
968
  override fun onDetachedFromWindow() {
1064
969
  debugLog("BBPlayerView") { "onDetachedFromWindow - cleaning up player" }
1065
- stopTimeUpdates()
1066
- timeUpdateHandler.removeCallbacksAndMessages(null)
1067
970
  removePlayer()
1068
971
  super.onDetachedFromWindow()
1069
972
  }
@@ -43,11 +43,6 @@ class BBPlayerViewManager : ViewGroupManager<BBPlayerView>() {
43
43
  view.setAutoPlay(autoPlay)
44
44
  }
45
45
 
46
- @ReactProp(name = "enableTimeUpdates")
47
- fun setEnableTimeUpdates(view: BBPlayerView, enabled: Boolean) {
48
- view.setEnableTimeUpdates(enabled)
49
- }
50
-
51
46
  @ReactProp(name = "options")
52
47
  fun setOptions(view: BBPlayerView, options: ReadableMap?) {
53
48
  view.setOptions(options)
@@ -74,7 +69,9 @@ class BBPlayerViewManager : ViewGroupManager<BBPlayerView>() {
74
69
  "loadWithProjectId" to COMMAND_LOAD_WITH_PROJECT_ID,
75
70
  "loadWithClipJson" to COMMAND_LOAD_WITH_CLIP_JSON,
76
71
  "loadWithClipListJson" to COMMAND_LOAD_WITH_CLIP_LIST_JSON,
77
- "loadWithProjectJson" to COMMAND_LOAD_WITH_PROJECT_JSON
72
+ "loadWithProjectJson" to COMMAND_LOAD_WITH_PROJECT_JSON,
73
+ "presentModal" to COMMAND_PRESENT_MODAL,
74
+ "closeModal" to COMMAND_CLOSE_MODAL
78
75
  )
79
76
 
80
77
  // Override for Old Architecture (numeric command IDs)
@@ -102,6 +99,8 @@ class BBPlayerViewManager : ViewGroupManager<BBPlayerView>() {
102
99
  COMMAND_LOAD_WITH_CLIP_JSON -> "loadWithClipJson"
103
100
  COMMAND_LOAD_WITH_CLIP_LIST_JSON -> "loadWithClipListJson"
104
101
  COMMAND_LOAD_WITH_PROJECT_JSON -> "loadWithProjectJson"
102
+ COMMAND_PRESENT_MODAL -> "presentModal"
103
+ COMMAND_CLOSE_MODAL -> "closeModal"
105
104
  else -> return
106
105
  }
107
106
  receiveCommand(view, commandName, args)
@@ -124,6 +123,8 @@ class BBPlayerViewManager : ViewGroupManager<BBPlayerView>() {
124
123
  "exitFullscreen" -> view.exitFullscreen()
125
124
  "showCastPicker" -> view.showCastPicker()
126
125
  "destroy" -> view.destroy()
126
+ "presentModal" -> view.presentModal()
127
+ "closeModal" -> view.closeModal()
127
128
  "loadWithClipId" -> {
128
129
  val clipId = args?.getString(0) ?: return
129
130
  val initiator = args.getString(1)
@@ -186,7 +187,6 @@ class BBPlayerViewManager : ViewGroupManager<BBPlayerView>() {
186
187
  "onDidTriggerEnded",
187
188
  "onDidTriggerSeeking",
188
189
  "onDidTriggerSeeked",
189
- "onDidTriggerTimeUpdate",
190
190
  "onDidTriggerDurationChange",
191
191
  "onDidTriggerVolumeChange",
192
192
  "onDidTriggerCanPlay",
@@ -249,5 +249,7 @@ class BBPlayerViewManager : ViewGroupManager<BBPlayerView>() {
249
249
  private const val COMMAND_LOAD_WITH_CLIP_JSON = 18
250
250
  private const val COMMAND_LOAD_WITH_CLIP_LIST_JSON = 19
251
251
  private const val COMMAND_LOAD_WITH_PROJECT_JSON = 20
252
+ private const val COMMAND_PRESENT_MODAL = 21
253
+ private const val COMMAND_CLOSE_MODAL = 22
252
254
  }
253
255
  }
@@ -31,23 +31,34 @@ public abstract class NativeBBPlayerModuleSpec extends ReactContextBaseJavaModul
31
31
  public abstract void collapse(int viewTag);
32
32
  public abstract void expand(int viewTag);
33
33
 
34
+ // Void methods - modal control
35
+ public abstract void presentModal(int viewTag);
36
+ public abstract void closeModal(int viewTag);
37
+
34
38
  // Void methods - other commands
35
39
  public abstract void autoPlayNextCancel(int viewTag);
36
40
  public abstract void destroy(int viewTag);
37
41
  public abstract void showCastPicker(int viewTag);
38
42
 
43
+ // Modal player (module-level)
44
+ public abstract void presentModalPlayer(String jsonUrl, String optionsJson, String contextJson);
45
+ public abstract void dismissModalPlayer();
46
+
47
+ // Event emitter support
48
+ public abstract void addListener(String eventName);
49
+ public abstract void removeListeners(double count);
50
+
39
51
  // Load methods
40
- public abstract void loadWithClipId(int viewTag, String clipId, String initiator, boolean autoPlay, double seekTo);
41
- public abstract void loadWithClipListId(int viewTag, String clipListId, String initiator, boolean autoPlay, double seekTo);
42
- public abstract void loadWithProjectId(int viewTag, String projectId, String initiator, boolean autoPlay, double seekTo);
43
- public abstract void loadWithClipJson(int viewTag, String clipJson, String initiator, boolean autoPlay, double seekTo);
44
- public abstract void loadWithClipListJson(int viewTag, String clipListJson, String initiator, boolean autoPlay, double seekTo);
45
- public abstract void loadWithProjectJson(int viewTag, String projectJson, String initiator, boolean autoPlay, double seekTo);
46
- public abstract void loadWithJsonUrl(int viewTag, String jsonUrl, boolean autoPlay);
52
+ public abstract void loadWithClipId(int viewTag, String clipId, String initiator, boolean autoPlay, double seekTo, String contextJson);
53
+ public abstract void loadWithClipListId(int viewTag, String clipListId, String initiator, boolean autoPlay, double seekTo, String contextJson);
54
+ public abstract void loadWithProjectId(int viewTag, String projectId, String initiator, boolean autoPlay, double seekTo, String contextJson);
55
+ public abstract void loadWithClipJson(int viewTag, String clipJson, String initiator, boolean autoPlay, double seekTo, String contextJson);
56
+ public abstract void loadWithClipListJson(int viewTag, String clipListJson, String initiator, boolean autoPlay, double seekTo, String contextJson);
57
+ public abstract void loadWithProjectJson(int viewTag, String projectJson, String initiator, boolean autoPlay, double seekTo, String contextJson);
58
+ public abstract void loadWithJsonUrl(int viewTag, String jsonUrl, boolean autoPlay, String contextJson);
47
59
 
48
60
  // Promise getters
49
61
  public abstract void getDuration(int viewTag, Promise promise);
50
- public abstract void getCurrentTime(int viewTag, Promise promise);
51
62
  public abstract void getMuted(int viewTag, Promise promise);
52
63
  public abstract void getVolume(int viewTag, Promise promise);
53
64
  public abstract void getPhase(int viewTag, Promise promise);
@@ -1,10 +1,11 @@
1
1
  #import <React/RCTBridgeModule.h>
2
+ #import <React/RCTEventEmitter.h>
2
3
 
3
4
  #ifdef RCT_NEW_ARCH_ENABLED
4
5
  #import <BBPlayerModuleSpec/BBPlayerModuleSpec.h>
5
6
  #endif
6
7
 
7
- @interface RCT_EXTERN_MODULE(BBPlayerModule, NSObject)
8
+ @interface RCT_EXTERN_MODULE(BBPlayerModule, RCTEventEmitter)
8
9
 
9
10
  // Playback control
10
11
  RCT_EXTERN_METHOD(play:(nonnull NSNumber *)viewTag)
@@ -29,13 +30,13 @@ RCT_EXTERN_METHOD(destroy:(nonnull NSNumber *)viewTag)
29
30
  RCT_EXTERN_METHOD(showCastPicker:(nonnull NSNumber *)viewTag)
30
31
 
31
32
  // Load methods
32
- RCT_EXTERN_METHOD(loadWithClipId:(nonnull NSNumber *)viewTag clipId:(NSString *)clipId initiator:(NSString *)initiator autoPlay:(BOOL)autoPlay seekTo:(NSNumber *)seekTo)
33
- RCT_EXTERN_METHOD(loadWithClipListId:(nonnull NSNumber *)viewTag clipListId:(NSString *)clipListId initiator:(NSString *)initiator autoPlay:(BOOL)autoPlay seekTo:(NSNumber *)seekTo)
34
- RCT_EXTERN_METHOD(loadWithProjectId:(nonnull NSNumber *)viewTag projectId:(NSString *)projectId initiator:(NSString *)initiator autoPlay:(BOOL)autoPlay seekTo:(NSNumber *)seekTo)
35
- RCT_EXTERN_METHOD(loadWithClipJson:(nonnull NSNumber *)viewTag clipJson:(NSString *)clipJson initiator:(NSString *)initiator autoPlay:(BOOL)autoPlay seekTo:(NSNumber *)seekTo)
36
- RCT_EXTERN_METHOD(loadWithClipListJson:(nonnull NSNumber *)viewTag clipListJson:(NSString *)clipListJson initiator:(NSString *)initiator autoPlay:(BOOL)autoPlay seekTo:(NSNumber *)seekTo)
37
- RCT_EXTERN_METHOD(loadWithProjectJson:(nonnull NSNumber *)viewTag projectJson:(NSString *)projectJson initiator:(NSString *)initiator autoPlay:(BOOL)autoPlay seekTo:(NSNumber *)seekTo)
38
- RCT_EXTERN_METHOD(loadWithJsonUrl:(nonnull NSNumber *)viewTag jsonUrl:(NSString *)jsonUrl autoPlay:(BOOL)autoPlay)
33
+ RCT_EXTERN_METHOD(loadWithClipId:(nonnull NSNumber *)viewTag clipId:(NSString *)clipId initiator:(NSString *)initiator autoPlay:(BOOL)autoPlay seekTo:(NSNumber *)seekTo contextJson:(NSString *)contextJson)
34
+ RCT_EXTERN_METHOD(loadWithClipListId:(nonnull NSNumber *)viewTag clipListId:(NSString *)clipListId initiator:(NSString *)initiator autoPlay:(BOOL)autoPlay seekTo:(NSNumber *)seekTo contextJson:(NSString *)contextJson)
35
+ RCT_EXTERN_METHOD(loadWithProjectId:(nonnull NSNumber *)viewTag projectId:(NSString *)projectId initiator:(NSString *)initiator autoPlay:(BOOL)autoPlay seekTo:(NSNumber *)seekTo contextJson:(NSString *)contextJson)
36
+ RCT_EXTERN_METHOD(loadWithClipJson:(nonnull NSNumber *)viewTag clipJson:(NSString *)clipJson initiator:(NSString *)initiator autoPlay:(BOOL)autoPlay seekTo:(NSNumber *)seekTo contextJson:(NSString *)contextJson)
37
+ RCT_EXTERN_METHOD(loadWithClipListJson:(nonnull NSNumber *)viewTag clipListJson:(NSString *)clipListJson initiator:(NSString *)initiator autoPlay:(BOOL)autoPlay seekTo:(NSNumber *)seekTo contextJson:(NSString *)contextJson)
38
+ RCT_EXTERN_METHOD(loadWithProjectJson:(nonnull NSNumber *)viewTag projectJson:(NSString *)projectJson initiator:(NSString *)initiator autoPlay:(BOOL)autoPlay seekTo:(NSNumber *)seekTo contextJson:(NSString *)contextJson)
39
+ RCT_EXTERN_METHOD(loadWithJsonUrl:(nonnull NSNumber *)viewTag jsonUrl:(NSString *)jsonUrl autoPlay:(BOOL)autoPlay contextJson:(NSString *)contextJson)
39
40
 
40
41
  // Getter methods with Promise
41
42
  RCT_EXTERN_METHOD(getDuration:(nonnull NSNumber *)viewTag resolver:(RCTPromiseResolveBlock)resolver rejecter:(RCTPromiseRejectBlock)rejecter)
@@ -49,6 +50,14 @@ RCT_EXTERN_METHOD(getClipData:(nonnull NSNumber *)viewTag resolver:(RCTPromiseRe
49
50
  RCT_EXTERN_METHOD(getProjectData:(nonnull NSNumber *)viewTag resolver:(RCTPromiseResolveBlock)resolver rejecter:(RCTPromiseRejectBlock)rejecter)
50
51
  RCT_EXTERN_METHOD(getPlayoutData:(nonnull NSNumber *)viewTag resolver:(RCTPromiseResolveBlock)resolver rejecter:(RCTPromiseRejectBlock)rejecter)
51
52
 
53
+ // Modal player (module-level)
54
+ RCT_EXTERN_METHOD(presentModalPlayer:(NSString *)jsonUrl optionsJson:(NSString *)optionsJson contextJson:(NSString *)contextJson)
55
+ RCT_EXTERN_METHOD(dismissModalPlayer)
56
+
57
+ // Event emitter support
58
+ RCT_EXTERN_METHOD(addListener:(NSString *)eventName)
59
+ RCT_EXTERN_METHOD(removeListeners:(double)count)
60
+
52
61
  #ifdef RCT_NEW_ARCH_ENABLED
53
62
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
54
63
  (const facebook::react::ObjCTurboModule::InitParams &)params