@bluebillywig/react-native-bb-player 8.44.0 → 8.45.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 (54) hide show
  1. package/README.md +80 -59
  2. package/android/build.gradle +2 -1
  3. package/android/src/main/java/com/bluebillywig/bbplayer/BBPlayerModule.kt +146 -28
  4. package/android/src/main/java/com/bluebillywig/bbplayer/BBPlayerView.kt +74 -165
  5. package/android/src/main/java/com/bluebillywig/bbplayer/BBPlayerViewManager.kt +0 -6
  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 +138 -26
  9. package/ios/BBPlayerView.swift +41 -140
  10. package/ios/BBPlayerViewManager.m +0 -2
  11. package/lib/commonjs/BBModalPlayer.js +21 -0
  12. package/lib/commonjs/BBModalPlayer.js.map +1 -0
  13. package/lib/commonjs/BBOutstreamView.js +0 -1
  14. package/lib/commonjs/BBOutstreamView.js.map +1 -1
  15. package/lib/commonjs/BBPlayerView.js +0 -1
  16. package/lib/commonjs/BBPlayerView.js.map +1 -1
  17. package/lib/commonjs/NativeCommands.js +24 -24
  18. package/lib/commonjs/NativeCommands.js.map +1 -1
  19. package/lib/commonjs/index.js +9 -1
  20. package/lib/commonjs/index.js.map +1 -1
  21. package/lib/commonjs/specs/NativeBBPlayerModule.js.map +1 -1
  22. package/lib/module/BBModalPlayer.js +17 -0
  23. package/lib/module/BBModalPlayer.js.map +1 -0
  24. package/lib/module/BBOutstreamView.js +0 -1
  25. package/lib/module/BBOutstreamView.js.map +1 -1
  26. package/lib/module/BBPlayerView.js +0 -1
  27. package/lib/module/BBPlayerView.js.map +1 -1
  28. package/lib/module/NativeCommands.js +24 -24
  29. package/lib/module/NativeCommands.js.map +1 -1
  30. package/lib/module/index.js +1 -0
  31. package/lib/module/index.js.map +1 -1
  32. package/lib/module/specs/NativeBBPlayerModule.js.map +1 -1
  33. package/lib/typescript/src/BBModalPlayer.d.ts +13 -0
  34. package/lib/typescript/src/BBModalPlayer.d.ts.map +1 -0
  35. package/lib/typescript/src/BBOutstreamView.d.ts.map +1 -1
  36. package/lib/typescript/src/BBPlayer.types.d.ts +24 -20
  37. package/lib/typescript/src/BBPlayer.types.d.ts.map +1 -1
  38. package/lib/typescript/src/BBPlayerView.d.ts +0 -2
  39. package/lib/typescript/src/BBPlayerView.d.ts.map +1 -1
  40. package/lib/typescript/src/NativeCommands.d.ts +8 -9
  41. package/lib/typescript/src/NativeCommands.d.ts.map +1 -1
  42. package/lib/typescript/src/index.d.ts +2 -0
  43. package/lib/typescript/src/index.d.ts.map +1 -1
  44. package/lib/typescript/src/specs/NativeBBPlayerModule.d.ts +13 -8
  45. package/lib/typescript/src/specs/NativeBBPlayerModule.d.ts.map +1 -1
  46. package/package.json +8 -11
  47. package/src/BBModalPlayer.ts +32 -0
  48. package/src/BBOutstreamView.tsx +0 -1
  49. package/src/BBPlayer.types.ts +31 -17
  50. package/src/BBPlayerView.tsx +0 -12
  51. package/src/NativeCommands.ts +37 -26
  52. package/src/index.ts +2 -0
  53. package/src/specs/NativeBBPlayerModule.ts +25 -8
  54. 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) {
@@ -469,9 +399,7 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
469
399
 
470
400
  fun seekRelative(offsetInSeconds: Double) {
471
401
  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)
402
+ playerView.player?.seekRelative(offsetInSeconds)
475
403
  }
476
404
  }
477
405
 
@@ -566,6 +494,20 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
566
494
  return null
567
495
  }
568
496
 
497
+ fun presentModal() {
498
+ Log.d("BBPlayerView", "presentModal() called")
499
+ if (::playerView.isInitialized) {
500
+ playerView.player?.enterFullScreen()
501
+ }
502
+ }
503
+
504
+ fun closeModal() {
505
+ Log.d("BBPlayerView", "closeModal() called")
506
+ if (::playerView.isInitialized) {
507
+ playerView.player?.exitFullScreen()
508
+ }
509
+ }
510
+
569
511
  fun destroy() {
570
512
  Log.d("BBPlayerView", "destroy() called")
571
513
  if (::playerView.isInitialized) {
@@ -610,22 +552,6 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
610
552
  } else null
611
553
  }
612
554
 
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
555
  fun getDuration(): Double? {
630
556
  return if (::playerView.isInitialized) {
631
557
  playerView.getApiProperty(com.bluebillywig.bbnativeshared.enums.ApiProperty.duration) as? Double
@@ -672,73 +598,63 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
672
598
  return null
673
599
  }
674
600
 
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)
601
+ // Helper to parse context JSON into a Map for the native SDK
602
+ private fun parseContext(contextJson: String?): Map<String, Any?>? {
603
+ if (contextJson.isNullOrBlank()) return null
604
+ return try {
605
+ val json = JSONObject(contextJson)
606
+ mapOf(
607
+ "contextEntityType" to json.optString("contextEntityType", null),
608
+ "contextEntityId" to json.optString("contextEntityId", null),
609
+ "contextCollectionType" to json.optString("contextCollectionType", null),
610
+ "contextCollectionId" to json.optString("contextCollectionId", null)
611
+ ).filterValues { it != null }
612
+ } catch (e: Exception) {
613
+ Log.w("BBPlayerView", "Failed to parse context JSON: $contextJson", e)
614
+ null
679
615
  }
680
616
  }
681
617
 
682
- fun loadWithClipListId(clipListId: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null) {
618
+ // Load methods
619
+ fun loadWithClipId(clipId: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null, contextJson: String? = null) {
683
620
  if (::playerView.isInitialized) {
684
- playerView.player?.loadWithClipListId(clipListId, initiator, autoPlay, seekTo)
621
+ val context = parseContext(contextJson)
622
+ playerView.player?.loadWithClipId(clipId, initiator, autoPlay, seekTo, context)
685
623
  }
686
624
  }
687
625
 
688
- fun loadWithProjectId(projectId: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null) {
626
+ fun loadWithClipListId(clipListId: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null, contextJson: String? = null) {
689
627
  if (::playerView.isInitialized) {
690
- playerView.player?.loadWithProjectId(projectId, initiator, autoPlay, seekTo)
628
+ val context = parseContext(contextJson)
629
+ playerView.player?.loadWithClipListId(clipListId, initiator, autoPlay, seekTo, context)
691
630
  }
692
631
  }
693
632
 
694
- fun loadWithClipJson(clipJson: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null) {
633
+ fun loadWithProjectId(projectId: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null, contextJson: String? = null) {
695
634
  if (::playerView.isInitialized) {
696
- playerView.player?.loadWithClipJson(clipJson, initiator, autoPlay, seekTo)
635
+ val context = parseContext(contextJson)
636
+ playerView.player?.loadWithProjectId(projectId, initiator, autoPlay, seekTo, context)
697
637
  }
698
638
  }
699
639
 
700
- fun loadWithClipListJson(clipListJson: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null) {
640
+ fun loadWithClipJson(clipJson: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null, contextJson: String? = null) {
701
641
  if (::playerView.isInitialized) {
702
- playerView.player?.loadWithClipListJson(clipListJson, initiator, autoPlay, seekTo)
642
+ val context = parseContext(contextJson)
643
+ playerView.player?.loadWithClipJson(clipJson, initiator, autoPlay, seekTo, context)
703
644
  }
704
645
  }
705
646
 
706
- fun loadWithProjectJson(projectJson: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null) {
647
+ fun loadWithClipListJson(clipListJson: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null, contextJson: String? = null) {
707
648
  if (::playerView.isInitialized) {
708
- playerView.player?.loadWithProjectJson(projectJson, initiator, autoPlay, seekTo)
709
- }
710
- }
711
-
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
- }
649
+ val context = parseContext(contextJson)
650
+ playerView.player?.loadWithClipListJson(clipListJson, initiator, autoPlay, seekTo, context)
733
651
  }
734
-
735
- timeUpdateHandler.postDelayed(timeUpdateRunnable!!, 1000)
736
652
  }
737
653
 
738
- private fun stopTimeUpdates() {
739
- timeUpdateRunnable?.let {
740
- timeUpdateHandler.removeCallbacks(it)
741
- timeUpdateRunnable = null
654
+ fun loadWithProjectJson(projectJson: String, initiator: String? = "external", autoPlay: Boolean? = true, seekTo: Double? = null, contextJson: String? = null) {
655
+ if (::playerView.isInitialized) {
656
+ val context = parseContext(contextJson)
657
+ playerView.player?.loadWithProjectJson(projectJson, initiator, autoPlay, seekTo, context)
742
658
  }
743
659
  }
744
660
 
@@ -756,10 +672,12 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
756
672
  }
757
673
 
758
674
  override fun didTriggerMediaClipLoaded(view: BBNativePlayerView, clipData: MediaClip?) {
759
- debugLog("BBPlayerView") { "didTriggerMediaClipLoaded: ${clipData?.title}" }
675
+ debugLog("BBPlayerView") { "didTriggerMediaClipLoaded: ${clipData?.title} (${clipData?.width}x${clipData?.height})" }
760
676
  val params = Arguments.createMap().apply {
761
677
  putString("title", clipData?.title)
762
678
  putString("id", clipData?.id)
679
+ clipData?.width?.let { putDouble("width", it.toDouble()) }
680
+ clipData?.height?.let { putDouble("height", it.toDouble()) }
763
681
  }
764
682
  sendEvent("onDidTriggerMediaClipLoaded", params)
765
683
  }
@@ -839,24 +757,20 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
839
757
  }
840
758
 
841
759
  override fun didTriggerPlaying(view: BBNativePlayerView) {
842
- debugLog("BBPlayerView") { "didTriggerPlaying - starting periodic time updates" }
760
+ debugLog("BBPlayerView") { "didTriggerPlaying" }
843
761
  isPlaying = true
844
- playbackStartTimestamp = System.currentTimeMillis()
845
- startTimeUpdates()
846
762
  sendEvent("onDidTriggerPlaying")
847
763
  }
848
764
 
849
765
  override fun didTriggerPause(view: BBNativePlayerView) {
850
- debugLog("BBPlayerView") { "didTriggerPause - stopping periodic time updates" }
766
+ debugLog("BBPlayerView") { "didTriggerPause" }
851
767
  isPlaying = false
852
- stopTimeUpdates()
853
768
  sendEvent("onDidTriggerPause")
854
769
  }
855
770
 
856
771
  override fun didTriggerEnded(view: BBNativePlayerView) {
857
- debugLog("BBPlayerView") { "didTriggerEnded - stopping periodic time updates" }
772
+ debugLog("BBPlayerView") { "didTriggerEnded" }
858
773
  isPlaying = false
859
- stopTimeUpdates()
860
774
  sendEvent("onDidTriggerEnded")
861
775
  }
862
776
 
@@ -867,9 +781,6 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
867
781
 
868
782
  override fun didTriggerSeeked(view: BBNativePlayerView, seekOffset: Double?) {
869
783
  debugLog("BBPlayerView") { "didTriggerSeeked: $seekOffset" }
870
- lastKnownTime = seekOffset ?: 0.0
871
- playbackStartTimestamp = System.currentTimeMillis()
872
-
873
784
  val params = Arguments.createMap().apply {
874
785
  putDouble("payload", seekOffset ?: 0.0)
875
786
  }
@@ -1062,8 +973,6 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
1062
973
 
1063
974
  override fun onDetachedFromWindow() {
1064
975
  debugLog("BBPlayerView") { "onDetachedFromWindow - cleaning up player" }
1065
- stopTimeUpdates()
1066
- timeUpdateHandler.removeCallbacksAndMessages(null)
1067
976
  removePlayer()
1068
977
  super.onDetachedFromWindow()
1069
978
  }
@@ -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)
@@ -186,7 +181,6 @@ class BBPlayerViewManager : ViewGroupManager<BBPlayerView>() {
186
181
  "onDidTriggerEnded",
187
182
  "onDidTriggerSeeking",
188
183
  "onDidTriggerSeeked",
189
- "onDidTriggerTimeUpdate",
190
184
  "onDidTriggerDurationChange",
191
185
  "onDidTriggerVolumeChange",
192
186
  "onDidTriggerCanPlay",
@@ -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);
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)
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