@shortkitsdk/react-native 0.2.12 → 0.2.15

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 (65) hide show
  1. package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +47 -4
  2. package/android/src/main/java/com/shortkit/reactnative/ReactVideoCarouselOverlayHost.kt +431 -0
  3. package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +117 -2
  4. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +7 -5
  5. package/ios/ReactCarouselOverlayHost.swift +67 -35
  6. package/ios/ReactOverlayHost.swift +85 -75
  7. package/ios/ReactVideoCarouselOverlayHost.swift +283 -0
  8. package/ios/ShortKitBridge.swift +122 -20
  9. package/ios/ShortKitModule.mm +15 -5
  10. package/ios/ShortKitSDK.xcframework/Info.plist +5 -4
  11. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -0
  12. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Info.plist +5 -1
  13. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +2488 -281
  14. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +65 -5
  15. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  16. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +65 -5
  17. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  18. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +168 -0
  19. package/ios/{ShortKitSDK.xcframework.bak/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +2 -1
  20. package/ios/ShortKitSDK.xcframework/{ios-arm64-simulator → ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Info.plist +5 -1
  21. package/ios/{ShortKitSDK.xcframework.bak/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +8233 -3592
  22. package/ios/{ShortKitSDK.xcframework.bak/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +120 -19
  23. package/ios/{ShortKitSDK.xcframework.bak/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  24. package/ios/{ShortKitSDK.xcframework.bak/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +120 -19
  25. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +33558 -0
  26. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +925 -0
  27. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  28. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +925 -0
  29. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  30. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +212 -0
  31. package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -1
  32. package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +7338 -3272
  33. package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +106 -15
  34. package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  35. package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +106 -15
  36. package/ios/ShortKitSDK.xcframework.dev-backup/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  37. package/ios/ShortKitSDK.xcframework.dev-backup/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +168 -0
  38. package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +1838 -206
  39. package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +51 -1
  40. package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  41. package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +51 -1
  42. package/ios/ShortKitSDK.xcframework.dev-backup/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  43. package/ios/ShortKitSDK.xcframework.dev-backup/ios-arm64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +168 -0
  44. package/package.json +1 -1
  45. package/src/ShortKitCarouselOverlaySurface.tsx +57 -2
  46. package/src/ShortKitContext.ts +11 -0
  47. package/src/ShortKitFeed.tsx +25 -10
  48. package/src/ShortKitOverlaySurface.tsx +24 -11
  49. package/src/ShortKitProvider.tsx +4 -2
  50. package/src/ShortKitVideoCarouselOverlaySurface.tsx +156 -0
  51. package/src/ShortKitWidget.tsx +3 -3
  52. package/src/index.ts +5 -1
  53. package/src/serialization.ts +7 -0
  54. package/src/specs/NativeShortKitModule.ts +18 -2
  55. package/src/types.ts +48 -3
  56. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  57. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  58. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  59. package/ios/ShortKitSDK.xcframework/{ios-arm64-simulator → ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/module.modulemap +0 -0
  60. package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/Info.plist +4 -4
  61. /package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Info.plist +0 -0
  62. /package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/module.modulemap +0 -0
  63. /package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +0 -0
  64. /package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Info.plist +0 -0
  65. /package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/module.modulemap +0 -0
@@ -56,6 +56,13 @@ class ReactCarouselOverlayHost(context: Context) : FrameLayout(context), Carouse
56
56
  private var pendingWriteJob: Job? = null
57
57
  private var configureGeneration: Int = 0
58
58
 
59
+ /** Unique identifier for this overlay instance, used for event routing. */
60
+ val surfaceId: String = java.util.UUID.randomUUID().toString()
61
+
62
+ private var cachedItemJSON: String? = null
63
+ private var isActive: Boolean = false
64
+ private var activeImageIndex: Int = 0
65
+
59
66
  // ------------------------------------------------------------------
60
67
  // Fabric layout workaround
61
68
  // ------------------------------------------------------------------
@@ -123,6 +130,9 @@ class ReactCarouselOverlayHost(context: Context) : FrameLayout(context), Carouse
123
130
  // ------------------------------------------------------------------
124
131
 
125
132
  override fun configure(item: ImageCarouselItem) {
133
+ isActive = false
134
+ activeImageIndex = 0
135
+
126
136
  // Increment generation — any in-flight coroutine from a previous
127
137
  // configure() call will see a stale generation and bail out.
128
138
  val gen = ++configureGeneration
@@ -155,8 +165,8 @@ class ReactCarouselOverlayHost(context: Context) : FrameLayout(context), Carouse
155
165
  } else item
156
166
 
157
167
  val json = Json.encodeToString(fastItem)
158
- val bundle = Bundle().apply { putString("item", json) }
159
- applySurfaceProps(bundle)
168
+ cachedItemJSON = json
169
+ pushProps()
160
170
 
161
171
  // Pre-size the surface view NOW — before the overlay is attached to a cell.
162
172
  val parentView = parent as? android.view.View
@@ -203,12 +213,45 @@ class ReactCarouselOverlayHost(context: Context) : FrameLayout(context), Carouse
203
213
  val hasLocalImages = modifiedItem.images.any { it.url.startsWith("file://") }
204
214
  if (hasLocalImages) {
205
215
  val localJson = Json.encodeToString(modifiedItem)
206
- val localBundle = Bundle().apply { putString("item", localJson) }
207
- applySurfaceProps(localBundle)
216
+ cachedItemJSON = localJson
217
+ pushProps()
208
218
  }
209
219
  }
210
220
  }
211
221
 
222
+ override fun activatePlayback() {
223
+ isActive = true
224
+ val params = com.facebook.react.bridge.Arguments.createMap().apply {
225
+ putString("surfaceId", surfaceId)
226
+ putBoolean("isActive", true)
227
+ putString("playerState", "idle")
228
+ putBoolean("isMuted", true)
229
+ putDouble("playbackRate", 1.0)
230
+ putBoolean("captionsEnabled", false)
231
+ putNull("activeCue")
232
+ putNull("feedScrollPhase")
233
+ }
234
+ ShortKitBridge.shared?.emitEvent("onOverlayFullState", params)
235
+ }
236
+
237
+ override fun updateActiveImage(index: Int) {
238
+ activeImageIndex = index
239
+ val params = com.facebook.react.bridge.Arguments.createMap().apply {
240
+ putString("surfaceId", surfaceId)
241
+ putInt("activeImageIndex", index)
242
+ }
243
+ ShortKitBridge.shared?.emitEvent("onCarouselActiveImageChanged", params)
244
+ }
245
+
246
+ /** Build a props bundle with item data and surfaceId. */
247
+ private fun pushProps() {
248
+ val bundle = Bundle().apply {
249
+ putString("surfaceId", surfaceId)
250
+ cachedItemJSON?.let { putString("item", it) }
251
+ }
252
+ applySurfaceProps(bundle)
253
+ }
254
+
212
255
  /** Apply props to the surface, handling all lifecycle states. */
213
256
  private fun applySurfaceProps(bundle: Bundle) {
214
257
  val s = surface
@@ -0,0 +1,431 @@
1
+ package com.shortkit.reactnative
2
+
3
+ import android.content.Context
4
+ import android.os.Bundle
5
+ import android.os.Handler
6
+ import android.os.Looper
7
+ import android.widget.FrameLayout
8
+ import com.facebook.react.ReactApplication
9
+ import com.facebook.react.bridge.Arguments
10
+ import com.facebook.react.interfaces.fabric.ReactSurface
11
+ import com.facebook.react.runtime.ReactSurfaceImpl
12
+ import com.shortkit.sdk.ShortKitPlayer
13
+ import com.shortkit.sdk.model.ContentItem
14
+ import com.shortkit.sdk.model.PlayerState
15
+ import com.shortkit.sdk.model.VideoCarouselItem
16
+ import com.shortkit.sdk.overlay.VideoCarouselOverlay
17
+ import kotlinx.coroutines.CoroutineScope
18
+ import kotlinx.coroutines.Dispatchers
19
+ import kotlinx.coroutines.SupervisorJob
20
+ import kotlinx.coroutines.cancel
21
+ import kotlinx.coroutines.launch
22
+ import kotlinx.serialization.encodeToString
23
+ import kotlinx.serialization.json.Json
24
+ import java.util.UUID
25
+
26
+ /**
27
+ * A [FrameLayout] that conforms to [VideoCarouselOverlay] and hosts a React Native
28
+ * Fabric [ReactSurface] for rendering the developer's React video carousel overlay
29
+ * component inside a feed cell.
30
+ *
31
+ * Pushes [VideoCarouselItem] and active video data as surface properties, and
32
+ * emits playback state (isActive, time, playerState, isMuted) via bridge events
33
+ * using the same event names as [ReactOverlayHost] (routed by [surfaceId]).
34
+ *
35
+ * Android equivalent of `react_native_sdk/ios/ReactVideoCarouselOverlayHost.swift`.
36
+ */
37
+ class ReactVideoCarouselOverlayHost(context: Context) : FrameLayout(context), VideoCarouselOverlay {
38
+
39
+ private companion object {
40
+ const val TAG = "SK:VidCarouselHost"
41
+ }
42
+
43
+ // Touch handling: this view sits below the ViewPager2 in z-order.
44
+ // The cell's root FrameLayout dispatches taps to this overlay when the
45
+ // ViewPager2 doesn't consume them (i.e. non-scroll touches).
46
+
47
+ // ------------------------------------------------------------------
48
+ // Configuration
49
+ // ------------------------------------------------------------------
50
+
51
+ /** Suffix for the surface module name (e.g. "news" -> "ShortKitVideoCarouselOverlay_news"). */
52
+ var videoCarouselOverlayName: String = "Default"
53
+
54
+ // ------------------------------------------------------------------
55
+ // State
56
+ // ------------------------------------------------------------------
57
+
58
+ private var surface: ReactSurface? = null
59
+ private var pendingProps: Bundle? = null
60
+ private var isInLayoutPass: Boolean = false
61
+
62
+ /** Unique identifier for this overlay instance, used for event routing. */
63
+ val surfaceId: String = UUID.randomUUID().toString()
64
+
65
+ /** Cached carouselItem JSON — updateInitProps replaces all props, doesn't merge. */
66
+ private var carouselItemJSON: String? = null
67
+
68
+ // Player state
69
+ private var player: ShortKitPlayer? = null
70
+ private var isActive: Boolean = false
71
+ private var cachedPlayerState: String = "idle"
72
+ private var cachedIsMuted: Boolean = true
73
+ private var cachedCurrentTime: Double = 0.0
74
+ private var cachedDuration: Double = 0.0
75
+ private var cachedBuffered: Double = 0.0
76
+ private var timeDirty: Boolean = false
77
+ private val handler = Handler(Looper.getMainLooper())
78
+ private var timeCoalesceRunnable: Runnable? = null
79
+ private var flowScope: CoroutineScope? = null
80
+
81
+ // ------------------------------------------------------------------
82
+ // Fabric layout workaround
83
+ // ------------------------------------------------------------------
84
+
85
+ private val layoutHandler = Handler(Looper.getMainLooper())
86
+
87
+ private val layoutRunnable = Runnable {
88
+ measure(
89
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
90
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
91
+ )
92
+ layout(left, top, right, bottom)
93
+ }
94
+
95
+ override fun requestLayout() {
96
+ super.requestLayout()
97
+ if (!isInLayoutPass) {
98
+ @Suppress("UNNECESSARY_SAFE_CALL")
99
+ layoutHandler?.post(layoutRunnable)
100
+ }
101
+ }
102
+
103
+ // ------------------------------------------------------------------
104
+ // Init
105
+ // ------------------------------------------------------------------
106
+
107
+ init {
108
+ setBackgroundColor(android.graphics.Color.TRANSPARENT)
109
+ }
110
+
111
+ // ------------------------------------------------------------------
112
+ // Surface warmup
113
+ // ------------------------------------------------------------------
114
+
115
+ fun prepareSurface() {
116
+ createSurfaceIfNeeded()
117
+
118
+ val displayW = context.resources.displayMetrics.widthPixels
119
+ val displayH = context.resources.displayMetrics.heightPixels
120
+ val wSpec = MeasureSpec.makeMeasureSpec(displayW, MeasureSpec.EXACTLY)
121
+ val hSpec = MeasureSpec.makeMeasureSpec(displayH, MeasureSpec.EXACTLY)
122
+ measure(wSpec, hSpec)
123
+ layout(0, 0, displayW, displayH)
124
+ measureAndLayoutSurfaceView(displayW, displayH)
125
+ }
126
+
127
+ // ------------------------------------------------------------------
128
+ // VideoCarouselOverlay
129
+ // ------------------------------------------------------------------
130
+
131
+ override fun configure(item: VideoCarouselItem) {
132
+ isActive = false
133
+ timeDirty = false
134
+ stopTimeCoalescing()
135
+ cachedCurrentTime = 0.0
136
+ cachedDuration = 0.0
137
+ cachedBuffered = 0.0
138
+ cachedPlayerState = "idle"
139
+
140
+ val json = Json.encodeToString(item)
141
+ carouselItemJSON = json
142
+ val bundle = Bundle().apply {
143
+ putString("surfaceId", surfaceId)
144
+ putString("carouselItem", json)
145
+ putBoolean("isActive", false)
146
+ putString("playerState", "idle")
147
+ putBoolean("isMuted", cachedIsMuted)
148
+ if (item.videos.isNotEmpty()) {
149
+ putString("activeVideo", ShortKitBridge.serializeContentItemToJSON(item.videos.first()))
150
+ putInt("activeVideoIndex", 0)
151
+ }
152
+ }
153
+ applySurfaceProps(bundle)
154
+
155
+ // Pre-size the surface view
156
+ val parentView = parent as? android.view.View
157
+ val w = if (width > 0) width
158
+ else if (parentView != null && parentView.width > 0) parentView.width
159
+ else context.resources.displayMetrics.widthPixels
160
+ val h = if (height > 0) height
161
+ else if (parentView != null && parentView.height > 0) parentView.height
162
+ else context.resources.displayMetrics.heightPixels
163
+ measureAndLayoutSurfaceView(w, h)
164
+ }
165
+
166
+ override fun updateActiveVideo(index: Int, item: ContentItem) {
167
+ // Only emit when active — matches ReactOverlayHost pattern.
168
+ // During initial setup, configure() sets the first video via surface props.
169
+ if (!isActive) return
170
+
171
+ val params = Arguments.createMap().apply {
172
+ putString("surfaceId", surfaceId)
173
+ putString("activeVideo", ShortKitBridge.serializeContentItemToJSON(item))
174
+ putInt("activeVideoIndex", index)
175
+ }
176
+ ShortKitBridge.shared?.emitEvent("onVideoCarouselActiveVideoChanged", params)
177
+ }
178
+
179
+ override fun resetState() {
180
+ // Don't clear surface props — configure() will overwrite.
181
+ }
182
+
183
+ override fun attach(player: ShortKitPlayer) {
184
+ this.player = player
185
+ resubscribeToPlayer(player)
186
+ }
187
+
188
+ override fun activatePlayback() {
189
+ isActive = true
190
+ startTimeCoalescing()
191
+
192
+ handler.post {
193
+ if (isActive) {
194
+ emitFullState()
195
+ }
196
+ }
197
+ }
198
+
199
+ override fun deactivatePlayback() {
200
+ isActive = false
201
+ timeDirty = false
202
+ stopTimeCoalescing()
203
+ }
204
+
205
+ // ------------------------------------------------------------------
206
+ // Player Subscriptions
207
+ // ------------------------------------------------------------------
208
+
209
+ private fun resubscribeToPlayer(player: ShortKitPlayer) {
210
+ flowScope?.cancel()
211
+ val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
212
+ flowScope = scope
213
+
214
+ scope.launch {
215
+ player.playerState.collect { state ->
216
+ cachedPlayerState = ShortKitBridge.playerStateString(state)
217
+ if (isActive) {
218
+ val params = Arguments.createMap().apply {
219
+ putString("surfaceId", surfaceId)
220
+ putString("playerState", cachedPlayerState)
221
+ }
222
+ ShortKitBridge.shared?.emitEvent("onOverlayPlayerStateChanged", params)
223
+ }
224
+ }
225
+ }
226
+
227
+ scope.launch {
228
+ player.isMuted.collect { muted ->
229
+ cachedIsMuted = muted
230
+ if (isActive) {
231
+ val params = Arguments.createMap().apply {
232
+ putString("surfaceId", surfaceId)
233
+ putBoolean("isMuted", cachedIsMuted)
234
+ }
235
+ ShortKitBridge.shared?.emitEvent("onOverlayMutedChanged", params)
236
+ }
237
+ }
238
+ }
239
+
240
+ scope.launch {
241
+ player.time.collect { time ->
242
+ cachedCurrentTime = time.currentMs / 1000.0
243
+ cachedDuration = time.durationMs / 1000.0
244
+ cachedBuffered = time.bufferedMs / 1000.0
245
+ if (isActive) {
246
+ timeDirty = true
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ // ------------------------------------------------------------------
253
+ // Time Coalescing (250ms)
254
+ // ------------------------------------------------------------------
255
+
256
+ private fun startTimeCoalescing() {
257
+ stopTimeCoalescing()
258
+ val runnable = object : Runnable {
259
+ override fun run() {
260
+ if (isActive && timeDirty && cachedPlayerState != "seeking") {
261
+ emitTimeUpdate()
262
+ timeDirty = false
263
+ }
264
+ if (isActive) {
265
+ handler.postDelayed(this, 250)
266
+ }
267
+ }
268
+ }
269
+ timeCoalesceRunnable = runnable
270
+ handler.postDelayed(runnable, 250)
271
+ }
272
+
273
+ private fun stopTimeCoalescing() {
274
+ timeCoalesceRunnable?.let { handler.removeCallbacks(it) }
275
+ timeCoalesceRunnable = null
276
+ }
277
+
278
+ private fun emitTimeUpdate() {
279
+ val params = Arguments.createMap().apply {
280
+ putString("surfaceId", surfaceId)
281
+ putDouble("current", cachedCurrentTime)
282
+ putDouble("duration", cachedDuration)
283
+ putDouble("buffered", cachedBuffered)
284
+ }
285
+ ShortKitBridge.shared?.emitEvent("onOverlayTimeUpdate", params)
286
+ }
287
+
288
+ // ------------------------------------------------------------------
289
+ // Full State Emission
290
+ // ------------------------------------------------------------------
291
+
292
+ private fun emitFullState() {
293
+ val bridge = ShortKitBridge.shared ?: return
294
+
295
+ val params = Arguments.createMap().apply {
296
+ putString("surfaceId", surfaceId)
297
+ putBoolean("isActive", true)
298
+ putString("playerState", cachedPlayerState)
299
+ putBoolean("isMuted", cachedIsMuted)
300
+ putDouble("playbackRate", 1.0)
301
+ putBoolean("captionsEnabled", false)
302
+ putNull("activeCue")
303
+ putNull("feedScrollPhase")
304
+ }
305
+ bridge.emitEvent("onOverlayFullState", params)
306
+ }
307
+
308
+ // ------------------------------------------------------------------
309
+ // Surface Creation
310
+ // ------------------------------------------------------------------
311
+
312
+ /** Apply props to the surface, handling all lifecycle states. */
313
+ private fun applySurfaceProps(bundle: Bundle) {
314
+ val s = surface
315
+ if (s != null && s.isRunning) {
316
+ (s as? ReactSurfaceImpl)?.updateInitProps(bundle)
317
+ } else if (s != null && !s.isRunning) {
318
+ (s as? ReactSurfaceImpl)?.updateInitProps(bundle)
319
+ s.start()
320
+ } else {
321
+ pendingProps = bundle
322
+ createSurfaceIfNeeded()
323
+ }
324
+ }
325
+
326
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
327
+ super.onSizeChanged(w, h, oldw, oldh)
328
+ if (w > 0 && h > 0) {
329
+ measureAndLayoutSurfaceView(w, h)
330
+ }
331
+ }
332
+
333
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
334
+ isInLayoutPass = true
335
+ try {
336
+ super.onLayout(changed, left, top, right, bottom)
337
+ val w = right - left
338
+ val h = bottom - top
339
+ if (w > 0 && h > 0) {
340
+ measureAndLayoutSurfaceView(w, h)
341
+ } else {
342
+ val parentView = parent as? android.view.View
343
+ val pw = parentView?.width ?: 0
344
+ val ph = parentView?.height ?: 0
345
+ if (pw > 0 && ph > 0) {
346
+ measureAndLayoutSurfaceView(pw, ph)
347
+ }
348
+ }
349
+ } finally {
350
+ isInLayoutPass = false
351
+ }
352
+ }
353
+
354
+ private fun measureAndLayoutSurfaceView(w: Int, h: Int) {
355
+ val sv = surface?.view ?: return
356
+ if (sv.width == w && sv.height == h) return
357
+ val wSpec = android.view.View.MeasureSpec.makeMeasureSpec(w, android.view.View.MeasureSpec.EXACTLY)
358
+ val hSpec = android.view.View.MeasureSpec.makeMeasureSpec(h, android.view.View.MeasureSpec.EXACTLY)
359
+ sv.measure(wSpec, hSpec)
360
+ sv.layout(0, 0, w, h)
361
+ }
362
+
363
+ override fun onAttachedToWindow() {
364
+ super.onAttachedToWindow()
365
+
366
+ // Re-subscribe to player flows if we were detached and reattached
367
+ if (flowScope == null) {
368
+ player?.let { resubscribeToPlayer(it) }
369
+ }
370
+
371
+ val parentView = parent as? android.view.View
372
+ if (parentView != null && parentView.width > 0 && parentView.height > 0 && (width == 0 || height == 0)) {
373
+ val wSpec = MeasureSpec.makeMeasureSpec(parentView.width, MeasureSpec.EXACTLY)
374
+ val hSpec = MeasureSpec.makeMeasureSpec(parentView.height, MeasureSpec.EXACTLY)
375
+ measure(wSpec, hSpec)
376
+ layout(0, 0, parentView.width, parentView.height)
377
+ }
378
+ }
379
+
380
+ private fun createSurfaceIfNeeded() {
381
+ if (surface != null) return
382
+
383
+ val reactHost = (context.applicationContext as? ReactApplication)?.reactHost
384
+ if (reactHost == null) {
385
+ android.util.Log.e(TAG, "createSurface FAILED: reactHost is null")
386
+ return
387
+ }
388
+ val moduleName = "ShortKitVideoCarouselOverlay_$videoCarouselOverlayName"
389
+
390
+ val initialProps = pendingProps
391
+ pendingProps = null
392
+
393
+ val newSurface = reactHost.createSurface(context, moduleName, initialProps)
394
+ surface = newSurface
395
+
396
+ newSurface.view?.let { surfaceView ->
397
+ surfaceView.layoutParams = LayoutParams(
398
+ LayoutParams.MATCH_PARENT,
399
+ LayoutParams.MATCH_PARENT
400
+ )
401
+ addView(surfaceView)
402
+ }
403
+
404
+ // Only start if we have item data
405
+ if (initialProps != null) {
406
+ newSurface.start()
407
+ }
408
+
409
+ val parentView = parent as? android.view.View
410
+ val w = if (width > 0) width else parentView?.width ?: 0
411
+ val h = if (height > 0) height else parentView?.height ?: 0
412
+ if (w > 0 && h > 0) {
413
+ measureAndLayoutSurfaceView(w, h)
414
+ }
415
+ }
416
+
417
+ // ------------------------------------------------------------------
418
+ // Cleanup
419
+ // ------------------------------------------------------------------
420
+
421
+ override fun onDetachedFromWindow() {
422
+ super.onDetachedFromWindow()
423
+ flowScope?.cancel()
424
+ flowScope = null
425
+ isActive = false
426
+ stopTimeCoalescing()
427
+ if (surface?.isRunning == true) {
428
+ surface?.stop()
429
+ }
430
+ }
431
+ }